From 909c5d46bbb8fa99e41f5606e787fcc95719c54e Mon Sep 17 00:00:00 2001 From: Zacharias Luiten Date: Mon, 1 Mar 2021 12:18:38 +0100 Subject: [PATCH 1/2] Extract order by processing logic to a separate class --- src/OrderBy/OrderByClause.php | 34 +++++++++ src/OrderBy/OrderByProcessor.php | 19 +++++ src/OrderBy/PostgresOrderByProcessor.php | 60 ++++++++++++++++ src/PostgresDocumentStore.php | 66 +++++++++--------- tests/PostgresDocumentStoreTest.php | 89 ++++++++++++++++++++++++ 5 files changed, 237 insertions(+), 31 deletions(-) create mode 100644 src/OrderBy/OrderByClause.php create mode 100644 src/OrderBy/OrderByProcessor.php create mode 100644 src/OrderBy/PostgresOrderByProcessor.php diff --git a/src/OrderBy/OrderByClause.php b/src/OrderBy/OrderByClause.php new file mode 100644 index 0000000..3567467 --- /dev/null +++ b/src/OrderBy/OrderByClause.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\DocumentStore\Postgres\OrderBy; + +final class OrderByClause +{ + private $clause; + private $args; + + public function __construct(?string $clause, array $args = []) + { + $this->clause = $clause; + $this->args = $args; + } + + public function clause(): ?string + { + return $this->clause; + } + + public function args(): array + { + return $this->args; + } +} diff --git a/src/OrderBy/OrderByProcessor.php b/src/OrderBy/OrderByProcessor.php new file mode 100644 index 0000000..3c04b47 --- /dev/null +++ b/src/OrderBy/OrderByProcessor.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\DocumentStore\Postgres\OrderBy; + +use EventEngine\DocumentStore\OrderBy\OrderBy; + +interface OrderByProcessor +{ + public function process(OrderBy $orderBy): OrderByClause; +} diff --git a/src/OrderBy/PostgresOrderByProcessor.php b/src/OrderBy/PostgresOrderByProcessor.php new file mode 100644 index 0000000..4d6ef4e --- /dev/null +++ b/src/OrderBy/PostgresOrderByProcessor.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\DocumentStore\Postgres\OrderBy; + +use EventEngine\DocumentStore; +use EventEngine\DocumentStore\OrderBy\OrderBy; + +final class PostgresOrderByProcessor implements OrderByProcessor +{ + /** + * @var bool + */ + private $useMetadataColumns; + + public function __construct(bool $useMetadataColumns = false) + { + $this->useMetadataColumns = $useMetadataColumns; + } + + public function process(OrderBy $orderBy): OrderByClause + { + [$orderByClause, $args] = $this->processOrderBy($orderBy); + + return new OrderByClause($orderByClause, $args); + } + + private function processOrderBy(OrderBy $orderBy): array + { + if($orderBy instanceof DocumentStore\OrderBy\AndOrder) { + [$sortA, $sortAArgs] = $this->processOrderBy($orderBy->a()); + [$sortB, $sortBArgs] = $this->processOrderBy($orderBy->b()); + + return ["$sortA, $sortB", array_merge($sortAArgs, $sortBArgs)]; + } + + /** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderBy */ + $direction = $orderBy instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC'; + $prop = $this->propToJsonPath($orderBy->prop()); + + return ["{$prop} $direction", []]; + } + + private function propToJsonPath(string $field): string + { + if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) { + return str_replace('metadata.', '', $field); + } + + return "doc->'" . str_replace('.', "'->'", $field) . "'"; + } +} diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 0dc0072..3d3d936 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -17,8 +17,11 @@ use EventEngine\DocumentStore\OrderBy\OrderBy; use EventEngine\DocumentStore\PartialSelect; use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; -use EventEngine\DocumentStore\Postgres\Filter\PostgresFilterProcessor; use EventEngine\DocumentStore\Postgres\Filter\FilterProcessor; +use EventEngine\DocumentStore\Postgres\Filter\PostgresFilterProcessor; +use EventEngine\DocumentStore\Postgres\OrderBy\OrderByClause; +use EventEngine\DocumentStore\Postgres\OrderBy\OrderByProcessor; +use EventEngine\DocumentStore\Postgres\OrderBy\PostgresOrderByProcessor; use EventEngine\Util\VariableType; use function implode; @@ -43,6 +46,11 @@ final class PostgresDocumentStore implements DocumentStore\DocumentStore */ private $filterProcessor; + /** + * @var OrderByProcessor + */ + private $orderByProcessor; + private $tablePrefix = 'em_ds_'; private $docIdSchema = 'UUID NOT NULL'; @@ -57,7 +65,8 @@ public function __construct( string $docIdSchema = null, bool $transactional = true, bool $useMetadataColumns = false, - FilterProcessor $filterProcessor = null + FilterProcessor $filterProcessor = null, + OrderByProcessor $orderByProcessor = null ) { $this->connection = $connection; $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); @@ -67,6 +76,11 @@ public function __construct( } $this->filterProcessor = $filterProcessor; + if (null === $orderByProcessor) { + $orderByProcessor = new PostgresOrderByProcessor($useMetadataColumns); + } + $this->orderByProcessor = $orderByProcessor; + if(null !== $tablePrefix) { $this->tablePrefix = $tablePrefix; } @@ -441,7 +455,7 @@ public function upsertDoc(string $collectionName, string $docId, array $docOrSub { $doc = $this->getDoc($collectionName, $docId); - if ($doc !== null) { + if($doc !== null) { $this->updateDoc($collectionName, $docId, $docOrSubset); } else { $this->addDoc($collectionName, $docId, $docOrSubset); @@ -625,12 +639,16 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n $filterStr = $filterClause->clause(); $args = $filterClause->args(); + $orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []); + $orderByStr = $orderByClause->clause(); + $orderByArgs = $orderByClause->args(); + $where = $filterStr ? "WHERE $filterStr" : ''; $offset = $skip !== null ? "OFFSET $skip" : ''; $limit = $limit !== null ? "LIMIT $limit" : ''; - $orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : ''; + $orderBy = $orderByStr ? "ORDER BY $orderByStr" : ''; $query = <<connection->prepare($query); - $stmt->execute($args); + $stmt->execute(array_merge($args, $orderByArgs)); while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { yield json_decode($row['doc'], true); @@ -658,12 +676,16 @@ public function findDocs(string $collectionName, Filter $filter, int $skip = nul $filterStr = $filterClause->clause(); $args = $filterClause->args(); + $orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []); + $orderByStr = $orderByClause->clause(); + $orderByArgs = $orderByClause->args(); + $where = $filterStr ? "WHERE $filterStr" : ''; $offset = $skip !== null ? "OFFSET $skip" : ''; $limit = $limit !== null ? "LIMIT $limit" : ''; - $orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : ''; + $orderBy = $orderByStr ? "ORDER BY $orderByStr" : ''; $query = <<connection->prepare($query); - $stmt->execute($args); + $stmt->execute(array_merge($args, $orderByArgs)); while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { yield $row['id'] => json_decode($row['doc'], true); @@ -688,6 +710,10 @@ public function findPartialDocs(string $collectionName, PartialSelect $partialSe $filterStr = $filterClause->clause(); $args = $filterClause->args(); + $orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []); + $orderByStr = $orderByClause->clause(); + $orderByArgs = $orderByClause->args(); + $select = $this->makeSelect($partialSelect); $where = $filterStr ? "WHERE $filterStr" : ''; @@ -695,7 +721,7 @@ public function findPartialDocs(string $collectionName, PartialSelect $partialSe $offset = $skip !== null ? "OFFSET $skip" : ''; $limit = $limit !== null ? "LIMIT $limit" : ''; - $orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : ''; + $orderBy = $orderByStr ? "ORDER BY $orderByStr" : ''; $query = <<connection->prepare($query); - $stmt->execute($args); + $stmt->execute(array_merge($args, $orderByArgs)); while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { yield $row[self::PARTIAL_SELECT_DOC_ID] => $this->transformPartialDoc($partialSelect, $row); @@ -870,28 +896,6 @@ private function transformPartialDoc(PartialSelect $partialSelect, array $select return $partialDoc; } - private function orderByToSort(DocumentStore\OrderBy\OrderBy $orderBy): array - { - $sort = []; - - if($orderBy instanceof DocumentStore\OrderBy\AndOrder) { - /** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderByA */ - $orderByA = $orderBy->a(); - $direction = $orderByA instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC'; - $prop = $this->propToJsonPath($orderByA->prop()); - $sort[] = "{$prop} $direction"; - - $sortB = $this->orderByToSort($orderBy->b()); - - return array_merge($sort, $sortB); - } - - /** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderBy */ - $direction = $orderBy instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC'; - $prop = $this->propToJsonPath($orderBy->prop()); - return ["{$prop} $direction"]; - } - private function indexToSqlCmd(Index $index, string $collectionName): string { if($index instanceof DocumentStore\FieldIndex) { diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 55922eb..c46f417 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -23,6 +23,9 @@ use EventEngine\DocumentStore\Filter\LtFilter; use EventEngine\DocumentStore\Filter\NotFilter; use EventEngine\DocumentStore\Filter\OrFilter; +use EventEngine\DocumentStore\OrderBy\AndOrder; +use EventEngine\DocumentStore\OrderBy\Asc; +use EventEngine\DocumentStore\OrderBy\Desc; use EventEngine\DocumentStore\PartialSelect; use PHPUnit\Framework\TestCase; use EventEngine\DocumentStore\FieldIndex; @@ -813,6 +816,92 @@ public function it_counts_any_of_filter() $this->assertSame(2, $count); } + /** + * @test + */ + public function it_handles_order_by() + { + $collectionName = 'test_it_handles_order_by'; + $this->documentStore->addCollection($collectionName); + + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'foo']]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar']]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bas']]); + + $filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs( + $collectionName, + new AnyFilter(), + null, + null, + Asc::fromString('some.prop') + ))); + + $this->assertCount(3, $filteredDocs); + + $this->assertEquals( + [ + ['some' => ['prop' => 'bar']], + ['some' => ['prop' => 'bas']], + ['some' => ['prop' => 'foo']], + ], + $filteredDocs + ); + + $filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs( + $collectionName, + new AnyFilter(), + null, + null, + Desc::fromString('some.prop') + ))); + + $this->assertCount(3, $filteredDocs); + + $this->assertEquals( + [ + ['some' => ['prop' => 'foo']], + ['some' => ['prop' => 'bas']], + ['some' => ['prop' => 'bar']], + ], + $filteredDocs + ); + } + + /** + * @test + */ + public function it_handles_and_order_by() + { + $collectionName = 'test_it_handles_order_by'; + $this->documentStore->addCollection($collectionName); + + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bas']]]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar']]); + + $filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs( + $collectionName, + new AnyFilter(), + null, + null, + AndOrder::by( + Asc::fromString('some.prop'), + Desc::fromString('some.other') + ) + ))); + + $this->assertCount(3, $filteredDocs); + + $this->assertEquals( + [ + ['some' => ['prop' => 'bar']], + ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]], + ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bas']]], + ], + $filteredDocs + ); + } + /** * @test */ From 10ab64d7a62881d256dd439627831d2fe820914a Mon Sep 17 00:00:00 2001 From: Arne De Smedt Date: Fri, 17 Jan 2025 18:52:05 +0100 Subject: [PATCH 2/2] Remove php8.4 deprecations --- src/Index/RawSqlIndexCmd.php | 2 +- src/PostgresDocumentStore.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Index/RawSqlIndexCmd.php b/src/Index/RawSqlIndexCmd.php index b776179..01d6a07 100644 --- a/src/Index/RawSqlIndexCmd.php +++ b/src/Index/RawSqlIndexCmd.php @@ -35,7 +35,7 @@ public static function fromArray(array $data): Index return new self($data['sql'], $data['name'] ?? null); } - public function __construct(string $sql, string $name = null) + public function __construct(string $sql, ?string $name = null) { $this->sql = $sql; $this->name = $name; diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 3d3d936..23cf01b 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -61,12 +61,12 @@ final class PostgresDocumentStore implements DocumentStore\DocumentStore public function __construct( \PDO $connection, - string $tablePrefix = null, - string $docIdSchema = null, + ?string $tablePrefix = null, + ?string $docIdSchema = null, bool $transactional = true, bool $useMetadataColumns = false, - FilterProcessor $filterProcessor = null, - OrderByProcessor $orderByProcessor = null + ?FilterProcessor $filterProcessor = null, + ?OrderByProcessor $orderByProcessor = null ) { $this->connection = $connection; $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); @@ -633,7 +633,7 @@ public function getPartialDoc(string $collectionName, PartialSelect $partialSele /** * @inheritDoc */ - public function filterDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable + public function filterDocs(string $collectionName, Filter $filter, ?int $skip = null, ?int $limit = null, ?OrderBy $orderBy = null): \Traversable { $filterClause = $this->filterProcessor->process($filter); $filterStr = $filterClause->clause(); @@ -670,7 +670,7 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n /** * @inheritDoc */ - public function findDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable + public function findDocs(string $collectionName, Filter $filter, ?int $skip = null, ?int $limit = null, ?OrderBy $orderBy = null): \Traversable { $filterClause = $this->filterProcessor->process($filter); $filterStr = $filterClause->clause(); @@ -704,7 +704,7 @@ public function findDocs(string $collectionName, Filter $filter, int $skip = nul } } - public function findPartialDocs(string $collectionName, PartialSelect $partialSelect, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable + public function findPartialDocs(string $collectionName, PartialSelect $partialSelect, Filter $filter, ?int $skip = null, ?int $limit = null, ?OrderBy $orderBy = null): \Traversable { $filterClause = $this->filterProcessor->process($filter); $filterStr = $filterClause->clause();