Skip to content

Commit 60552f5

Browse files
author
Alexander Miertsch
authored
Merge pull request #25 from netiul/feature/extract-orderby-processor
Extract order by processing logic to a separate class
2 parents 2c7daec + 909c5d4 commit 60552f5

File tree

5 files changed

+237
-31
lines changed

5 files changed

+237
-31
lines changed

src/OrderBy/OrderByClause.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\OrderBy;
13+
14+
final class OrderByClause
15+
{
16+
private $clause;
17+
private $args;
18+
19+
public function __construct(?string $clause, array $args = [])
20+
{
21+
$this->clause = $clause;
22+
$this->args = $args;
23+
}
24+
25+
public function clause(): ?string
26+
{
27+
return $this->clause;
28+
}
29+
30+
public function args(): array
31+
{
32+
return $this->args;
33+
}
34+
}

src/OrderBy/OrderByProcessor.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\OrderBy;
13+
14+
use EventEngine\DocumentStore\OrderBy\OrderBy;
15+
16+
interface OrderByProcessor
17+
{
18+
public function process(OrderBy $orderBy): OrderByClause;
19+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\OrderBy;
13+
14+
use EventEngine\DocumentStore;
15+
use EventEngine\DocumentStore\OrderBy\OrderBy;
16+
17+
final class PostgresOrderByProcessor implements OrderByProcessor
18+
{
19+
/**
20+
* @var bool
21+
*/
22+
private $useMetadataColumns;
23+
24+
public function __construct(bool $useMetadataColumns = false)
25+
{
26+
$this->useMetadataColumns = $useMetadataColumns;
27+
}
28+
29+
public function process(OrderBy $orderBy): OrderByClause
30+
{
31+
[$orderByClause, $args] = $this->processOrderBy($orderBy);
32+
33+
return new OrderByClause($orderByClause, $args);
34+
}
35+
36+
private function processOrderBy(OrderBy $orderBy): array
37+
{
38+
if($orderBy instanceof DocumentStore\OrderBy\AndOrder) {
39+
[$sortA, $sortAArgs] = $this->processOrderBy($orderBy->a());
40+
[$sortB, $sortBArgs] = $this->processOrderBy($orderBy->b());
41+
42+
return ["$sortA, $sortB", array_merge($sortAArgs, $sortBArgs)];
43+
}
44+
45+
/** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderBy */
46+
$direction = $orderBy instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC';
47+
$prop = $this->propToJsonPath($orderBy->prop());
48+
49+
return ["{$prop} $direction", []];
50+
}
51+
52+
private function propToJsonPath(string $field): string
53+
{
54+
if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) {
55+
return str_replace('metadata.', '', $field);
56+
}
57+
58+
return "doc->'" . str_replace('.', "'->'", $field) . "'";
59+
}
60+
}

src/PostgresDocumentStore.php

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
use EventEngine\DocumentStore\OrderBy\OrderBy;
1818
use EventEngine\DocumentStore\PartialSelect;
1919
use EventEngine\DocumentStore\Postgres\Exception\RuntimeException;
20-
use EventEngine\DocumentStore\Postgres\Filter\PostgresFilterProcessor;
2120
use EventEngine\DocumentStore\Postgres\Filter\FilterProcessor;
21+
use EventEngine\DocumentStore\Postgres\Filter\PostgresFilterProcessor;
22+
use EventEngine\DocumentStore\Postgres\OrderBy\OrderByClause;
23+
use EventEngine\DocumentStore\Postgres\OrderBy\OrderByProcessor;
24+
use EventEngine\DocumentStore\Postgres\OrderBy\PostgresOrderByProcessor;
2225
use EventEngine\Util\VariableType;
2326

2427
use function implode;
@@ -43,6 +46,11 @@ final class PostgresDocumentStore implements DocumentStore\DocumentStore
4346
*/
4447
private $filterProcessor;
4548

49+
/**
50+
* @var OrderByProcessor
51+
*/
52+
private $orderByProcessor;
53+
4654
private $tablePrefix = 'em_ds_';
4755

4856
private $docIdSchema = 'UUID NOT NULL';
@@ -57,7 +65,8 @@ public function __construct(
5765
string $docIdSchema = null,
5866
bool $transactional = true,
5967
bool $useMetadataColumns = false,
60-
FilterProcessor $filterProcessor = null
68+
FilterProcessor $filterProcessor = null,
69+
OrderByProcessor $orderByProcessor = null
6170
) {
6271
$this->connection = $connection;
6372
$this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
@@ -67,6 +76,11 @@ public function __construct(
6776
}
6877
$this->filterProcessor = $filterProcessor;
6978

79+
if (null === $orderByProcessor) {
80+
$orderByProcessor = new PostgresOrderByProcessor($useMetadataColumns);
81+
}
82+
$this->orderByProcessor = $orderByProcessor;
83+
7084
if(null !== $tablePrefix) {
7185
$this->tablePrefix = $tablePrefix;
7286
}
@@ -441,7 +455,7 @@ public function upsertDoc(string $collectionName, string $docId, array $docOrSub
441455
{
442456
$doc = $this->getDoc($collectionName, $docId);
443457

444-
if ($doc !== null) {
458+
if($doc !== null) {
445459
$this->updateDoc($collectionName, $docId, $docOrSubset);
446460
} else {
447461
$this->addDoc($collectionName, $docId, $docOrSubset);
@@ -625,12 +639,16 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n
625639
$filterStr = $filterClause->clause();
626640
$args = $filterClause->args();
627641

642+
$orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []);
643+
$orderByStr = $orderByClause->clause();
644+
$orderByArgs = $orderByClause->args();
645+
628646
$where = $filterStr ? "WHERE $filterStr" : '';
629647

630648
$offset = $skip !== null ? "OFFSET $skip" : '';
631649
$limit = $limit !== null ? "LIMIT $limit" : '';
632650

633-
$orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : '';
651+
$orderBy = $orderByStr ? "ORDER BY $orderByStr" : '';
634652

635653
$query = <<<EOT
636654
SELECT doc
@@ -642,7 +660,7 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n
642660
EOT;
643661
$stmt = $this->connection->prepare($query);
644662

645-
$stmt->execute($args);
663+
$stmt->execute(array_merge($args, $orderByArgs));
646664

647665
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
648666
yield json_decode($row['doc'], true);
@@ -658,12 +676,16 @@ public function findDocs(string $collectionName, Filter $filter, int $skip = nul
658676
$filterStr = $filterClause->clause();
659677
$args = $filterClause->args();
660678

679+
$orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []);
680+
$orderByStr = $orderByClause->clause();
681+
$orderByArgs = $orderByClause->args();
682+
661683
$where = $filterStr ? "WHERE $filterStr" : '';
662684

663685
$offset = $skip !== null ? "OFFSET $skip" : '';
664686
$limit = $limit !== null ? "LIMIT $limit" : '';
665687

666-
$orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : '';
688+
$orderBy = $orderByStr ? "ORDER BY $orderByStr" : '';
667689

668690
$query = <<<EOT
669691
SELECT id, doc
@@ -675,7 +697,7 @@ public function findDocs(string $collectionName, Filter $filter, int $skip = nul
675697
EOT;
676698
$stmt = $this->connection->prepare($query);
677699

678-
$stmt->execute($args);
700+
$stmt->execute(array_merge($args, $orderByArgs));
679701

680702
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
681703
yield $row['id'] => json_decode($row['doc'], true);
@@ -688,14 +710,18 @@ public function findPartialDocs(string $collectionName, PartialSelect $partialSe
688710
$filterStr = $filterClause->clause();
689711
$args = $filterClause->args();
690712

713+
$orderByClause = $orderBy ? $this->orderByProcessor->process($orderBy) : new OrderByClause(null, []);
714+
$orderByStr = $orderByClause->clause();
715+
$orderByArgs = $orderByClause->args();
716+
691717
$select = $this->makeSelect($partialSelect);
692718

693719
$where = $filterStr ? "WHERE $filterStr" : '';
694720

695721
$offset = $skip !== null ? "OFFSET $skip" : '';
696722
$limit = $limit !== null ? "LIMIT $limit" : '';
697723

698-
$orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : '';
724+
$orderBy = $orderByStr ? "ORDER BY $orderByStr" : '';
699725

700726
$query = <<<EOT
701727
SELECT $select
@@ -708,7 +734,7 @@ public function findPartialDocs(string $collectionName, PartialSelect $partialSe
708734

709735
$stmt = $this->connection->prepare($query);
710736

711-
$stmt->execute($args);
737+
$stmt->execute(array_merge($args, $orderByArgs));
712738

713739
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
714740
yield $row[self::PARTIAL_SELECT_DOC_ID] => $this->transformPartialDoc($partialSelect, $row);
@@ -870,28 +896,6 @@ private function transformPartialDoc(PartialSelect $partialSelect, array $select
870896
return $partialDoc;
871897
}
872898

873-
private function orderByToSort(DocumentStore\OrderBy\OrderBy $orderBy): array
874-
{
875-
$sort = [];
876-
877-
if($orderBy instanceof DocumentStore\OrderBy\AndOrder) {
878-
/** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderByA */
879-
$orderByA = $orderBy->a();
880-
$direction = $orderByA instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC';
881-
$prop = $this->propToJsonPath($orderByA->prop());
882-
$sort[] = "{$prop} $direction";
883-
884-
$sortB = $this->orderByToSort($orderBy->b());
885-
886-
return array_merge($sort, $sortB);
887-
}
888-
889-
/** @var DocumentStore\OrderBy\Asc|DocumentStore\OrderBy\Desc $orderBy */
890-
$direction = $orderBy instanceof DocumentStore\OrderBy\Asc ? 'ASC' : 'DESC';
891-
$prop = $this->propToJsonPath($orderBy->prop());
892-
return ["{$prop} $direction"];
893-
}
894-
895899
private function indexToSqlCmd(Index $index, string $collectionName): string
896900
{
897901
if($index instanceof DocumentStore\FieldIndex) {

tests/PostgresDocumentStoreTest.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
use EventEngine\DocumentStore\Filter\LtFilter;
2424
use EventEngine\DocumentStore\Filter\NotFilter;
2525
use EventEngine\DocumentStore\Filter\OrFilter;
26+
use EventEngine\DocumentStore\OrderBy\AndOrder;
27+
use EventEngine\DocumentStore\OrderBy\Asc;
28+
use EventEngine\DocumentStore\OrderBy\Desc;
2629
use EventEngine\DocumentStore\PartialSelect;
2730
use PHPUnit\Framework\TestCase;
2831
use EventEngine\DocumentStore\FieldIndex;
@@ -813,6 +816,92 @@ public function it_counts_any_of_filter()
813816
$this->assertSame(2, $count);
814817
}
815818

819+
/**
820+
* @test
821+
*/
822+
public function it_handles_order_by()
823+
{
824+
$collectionName = 'test_it_handles_order_by';
825+
$this->documentStore->addCollection($collectionName);
826+
827+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'foo']]);
828+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar']]);
829+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bas']]);
830+
831+
$filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs(
832+
$collectionName,
833+
new AnyFilter(),
834+
null,
835+
null,
836+
Asc::fromString('some.prop')
837+
)));
838+
839+
$this->assertCount(3, $filteredDocs);
840+
841+
$this->assertEquals(
842+
[
843+
['some' => ['prop' => 'bar']],
844+
['some' => ['prop' => 'bas']],
845+
['some' => ['prop' => 'foo']],
846+
],
847+
$filteredDocs
848+
);
849+
850+
$filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs(
851+
$collectionName,
852+
new AnyFilter(),
853+
null,
854+
null,
855+
Desc::fromString('some.prop')
856+
)));
857+
858+
$this->assertCount(3, $filteredDocs);
859+
860+
$this->assertEquals(
861+
[
862+
['some' => ['prop' => 'foo']],
863+
['some' => ['prop' => 'bas']],
864+
['some' => ['prop' => 'bar']],
865+
],
866+
$filteredDocs
867+
);
868+
}
869+
870+
/**
871+
* @test
872+
*/
873+
public function it_handles_and_order_by()
874+
{
875+
$collectionName = 'test_it_handles_order_by';
876+
$this->documentStore->addCollection($collectionName);
877+
878+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bas']]]);
879+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]]);
880+
$this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar']]);
881+
882+
$filteredDocs = \array_values(\iterator_to_array($this->documentStore->findDocs(
883+
$collectionName,
884+
new AnyFilter(),
885+
null,
886+
null,
887+
AndOrder::by(
888+
Asc::fromString('some.prop'),
889+
Desc::fromString('some.other')
890+
)
891+
)));
892+
893+
$this->assertCount(3, $filteredDocs);
894+
895+
$this->assertEquals(
896+
[
897+
['some' => ['prop' => 'bar']],
898+
['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]],
899+
['some' => ['prop' => 'foo', 'other' => ['prop' => 'bas']]],
900+
],
901+
$filteredDocs
902+
);
903+
}
904+
816905
/**
817906
* @test
818907
*/

0 commit comments

Comments
 (0)