From f7dd2ceb073932729390a6a7c98523cd4064bfb6 Mon Sep 17 00:00:00 2001 From: codeliner Date: Wed, 10 Jul 2019 00:25:40 +0200 Subject: [PATCH 01/23] Do not encode metadata column val --- src/PostgresDocumentStore.php | 32 ++++++++++---- tests/MetadataPostgresDocumentStoreTest.php | 46 +++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 6e724f5..3eea1b4 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -566,27 +566,27 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array return $this->makeInClause('id', $filter->valList(), $argsCount); case DocumentStore\Filter\AnyOfFilter::class: /** @var DocumentStore\Filter\AnyOfFilter $filter */ - return $this->makeInClause($this->propToJsonPath($filter->prop()), $filter->valList(), $argsCount, true); + return $this->makeInClause($this->propToJsonPath($filter->prop()), $filter->valList(), $argsCount, $this->shouldJsonEncodeVal($filter->prop())); case DocumentStore\Filter\EqFilter::class: /** @var DocumentStore\Filter\EqFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop = :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop = :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\GtFilter::class: /** @var DocumentStore\Filter\GtFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop > :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop > :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\GteFilter::class: /** @var DocumentStore\Filter\GteFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop >= :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop >= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\LtFilter::class: /** @var DocumentStore\Filter\LtFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop < :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop < :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\LteFilter::class: /** @var DocumentStore\Filter\LteFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop <= :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop <= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\LikeFilter::class: /** @var DocumentStore\Filter\LikeFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); @@ -614,7 +614,7 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array case DocumentStore\Filter\InArrayFilter::class: /** @var DocumentStore\Filter\InArrayFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop @> :a$argsCount", ["a$argsCount" => json_encode($filter->val())], ++$argsCount]; + return ["$prop @> :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; case DocumentStore\Filter\ExistsFilter::class: /** @var DocumentStore\Filter\ExistsFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); @@ -708,6 +708,24 @@ private function indexToSqlCmd(Index $index, string $collectionName): string return $cmd; } + private function prepareVal($value, string $prop) + { + if(!$this->shouldJsonEncodeVal($prop)) { + return $value; + } + + return \json_encode($value); + } + + private function shouldJsonEncodeVal(string $prop): bool + { + if($this->useMetadataColumns && strpos($prop, 'metadata.') === 0) { + return false; + } + + return true; + } + private function getIndexName(Index $index): ?string { if(method_exists($index, 'name')) { diff --git a/tests/MetadataPostgresDocumentStoreTest.php b/tests/MetadataPostgresDocumentStoreTest.php index d4abfe5..402d3fc 100644 --- a/tests/MetadataPostgresDocumentStoreTest.php +++ b/tests/MetadataPostgresDocumentStoreTest.php @@ -315,6 +315,52 @@ public function it_fills_and_queries_metadata_column() $this->assertEquals('v4', $docs[0]['state']['name']); } + /** + * @test + */ + public function it_fills_and_queries_metadata_varchar_column() + { + $collectionName = 'test_col_query_name_meta'; + + $index1 = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_name', 'metadata.name'), + new Column('name VARCHAR(10)') + ); + + $this->documentStore->addCollection($collectionName, $index1); + + $docId1 = Uuid::uuid4()->toString(); + $docId2 = Uuid::uuid4()->toString(); + $docId3 = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $docId1, ['state' => ['name' => 'v1'], 'metadata' => ['name' => 'v1']]); + $this->documentStore->addDoc($collectionName, $docId2, ['state' => ['name' => 'v2'], 'metadata' => ['name' => 'v2']]); + $this->documentStore->addDoc($collectionName, $docId3, ['state' => ['name' => 'v3'], 'metadata' => ['name' => 'v3']]); + + $prefix = self::TABLE_PREFIX; + $stmt = "SELECT * FROM $prefix{$collectionName} WHERE name = 'v2';"; + $stmt = $this->connection->prepare($stmt); + $stmt->execute(); + $docs = $stmt->fetchAll(); + + $this->assertCount(1, $docs); + $this->assertEquals($docId2, $docs[0]['id']); + $this->assertEquals(['state' => ['name' => 'v2']], json_decode($docs[0]['doc'], true)); + $this->assertEquals('v2', $docs[0]['name']); + + $docs = iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new GteFilter('metadata.name', 'v2'), + null, + null, + Desc::byProp('metadata.name') + )); + + $this->assertCount(2, $docs); + $this->assertEquals('v3', $docs[0]['state']['name']); + $this->assertEquals('v2', $docs[1]['state']['name']); + } + private function getIndexes(string $collectionName): array { return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); From d699231f12dcc77c156b94036eb47bcc2e9947ac Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Mon, 9 Sep 2019 09:08:01 +0200 Subject: [PATCH 02/23] Allow usage of different schemas for documents This commit adds support for using dedicated schemas for storing documents. This way one can separate storage of documents of different scopes do dedicated schemas per scope. THis allows for a better structuring of the documents within the database. This commit does **not** handle creating the schemas. The schema needs to be available to the DocumentStore for this to work. Basically this allows one to set the schema right within the table-name by separating schema and tablename by a dot like this: `schemaname.tablename`. The name will then be split on the dot and the former part be used as schema-name, the later part as table-name. Any table-prefixes will only be added to the table name, not to the scheme. --- src/PostgresDocumentStore.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 3eea1b4..f0fa8d7 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -119,6 +119,7 @@ public function hasCollection(string $collectionName): bool SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_NAME = '{$this->tableName($collectionName)}' +AND TABLE_SCHEMA = '{$this->schemaName($collectionName)}' EOT; $stmt = $this->connection->prepare($query); @@ -148,7 +149,7 @@ public function addCollection(string $collectionName, Index ...$indices): void } $cmd = <<tableName($collectionName)} ( +CREATE TABLE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} ( id {$this->docIdSchema}, doc JSONB NOT NULL, $metadataColumns @@ -751,6 +752,19 @@ private function extractFieldPartFromFieldIndex(DocumentStore\FieldIndex $fieldI private function tableName(string $collectionName): string { + if (false !== $dotPosition = strpos($collectionName, '.')) { + $collectionName = substr($collectionName, $dotPosition+1); + } + return mb_strtolower($this->tablePrefix . $collectionName); } + + private function schemaName(string $collectionName): string + { + $schemaName = 'public'; + if (false !== $dotPosition = strpos($collectionName, '.')) { + $schemaName = substr($collectionName, 0, $dotPosition); + } + return mb_strtolower($schemaName); + } } From 2cedf4521fddf61e9b70f55030ba1f5cbb73a7e1 Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Mon, 9 Sep 2019 09:48:17 +0200 Subject: [PATCH 03/23] Add schema to all queries containing the tablename This adds the schema-enabling to all queries that target the tablename. --- docker-compose.yml | 4 ++-- src/PostgresDocumentStore.php | 25 ++++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b7c1b34..8cb3945 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,13 +7,13 @@ services: - .:/app environment: - PROOPH_ENV=dev - - PDO_DSN=pgsql:host=postgres port=5432 dbname=event_engine + - PDO_DSN=pgsql:host=postgres port=5433 dbname=event_engine - PDO_USER=postgres - PDO_PWD= postgres: image: postgres:alpine ports: - - 5432:5432 + - 5433:5432 environment: - POSTGRES_DB=event_engine diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index f0fa8d7..705a5b3 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -177,7 +177,7 @@ public function addCollection(string $collectionName, Index ...$indices): void public function dropCollection(string $collectionName): void { $cmd = <<tableName($collectionName)}; +DROP TABLE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)}; EOT; $this->transactional(function () use ($cmd) { @@ -191,6 +191,7 @@ public function hasCollectionIndex(string $collectionName, string $indexName): b SELECT INDEXNAME FROM pg_indexes WHERE TABLENAME = '{$this->tableName($collectionName)}' +AND SCHEMANAME = '{$this->schemaName($collectionName)}' AND INDEXNAME = '$indexName' EOT; @@ -223,7 +224,7 @@ public function addCollectionIndex(string $collectionName, Index $index): void $columnsSql = substr($columnsSql, 2); $metadataColumnCmd = <<tableName($collectionName)} +ALTER TABLE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} $columnsSql; EOT; @@ -263,7 +264,7 @@ public function dropCollectionIndex(string $collectionName, $index): void $columnsSql = substr($columnsSql, 2); $metadataColumnCmd = <<tableName($collectionName)} +ALTER TABLE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} $columnsSql; EOT; $index = $index->indexCmd(); @@ -313,7 +314,9 @@ public function addDoc(string $collectionName, string $docId, array $doc): void } $cmd = <<tableName($collectionName)} (id, doc{$metadataKeysStr}) VALUES (:id, :doc{$metadataValsStr}); +INSERT INTO {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} ( + id, doc{$metadataKeysStr}) VALUES (:id, :doc{$metadataValsStr} +); EOT; $this->transactional(function () use ($cmd, $docId, $doc, $metadata) { @@ -346,7 +349,7 @@ public function updateDoc(string $collectionName, string $docId, array $docOrSub } $cmd = <<tableName($collectionName)} +UPDATE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} SET doc = (to_jsonb(doc) || :doc){$metadataStr} WHERE id = :id ; @@ -385,7 +388,7 @@ public function updateMany(string $collectionName, Filter $filter, array $set): } $cmd = <<tableName($collectionName)} +UPDATE {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} SET doc = (to_jsonb(doc) || :doc){$metadataStr} $where; EOT; @@ -425,7 +428,7 @@ public function upsertDoc(string $collectionName, string $docId, array $docOrSub public function deleteDoc(string $collectionName, string $docId): void { $cmd = <<tableName($collectionName)} +DELETE FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} WHERE id = :id EOT; @@ -448,7 +451,7 @@ public function deleteMany(string $collectionName, Filter $filter): void $where = $filterStr? "WHERE $filterStr" : ''; $cmd = <<tableName($collectionName)} +DELETE FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} $where; EOT; @@ -466,7 +469,7 @@ public function getDoc(string $collectionName, string $docId): ?array { $query = <<tableName($collectionName)} +FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} WHERE id = :id EOT; $stmt = $this->connection->prepare($query); @@ -503,7 +506,7 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n $query = <<tableName($collectionName)} +FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} $where $orderBy $limit @@ -702,7 +705,7 @@ private function indexToSqlCmd(Index $index, string $collectionName): string $name = $index->name() ?? ''; $cmd = <<tableName($collectionName)} +CREATE $type $name ON {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} $fields; EOT; From d5d68ab0d865178e0f25ebd133df06382671f34a Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Mon, 9 Sep 2019 10:00:04 +0200 Subject: [PATCH 04/23] Add creation of schema to docstore creation This commit adds the creation of the schema to the process of creating the document store. So now the schema does not need to be already available when the document store is created --- src/PostgresDocumentStore.php | 5 +- tests/SchemedPostgresDocumentStoreTest.php | 60 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/SchemedPostgresDocumentStoreTest.php diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 705a5b3..9c70dec 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -148,6 +148,8 @@ public function addCollection(string $collectionName, Index ...$indices): void } } + $createSchemaCmd = "CREATE SCHEMA IF NOT EXISTS {$this->schemaName($collectionName)}"; + $cmd = <<schemaName($collectionName)}.{$this->tableName($collectionName)} ( id {$this->docIdSchema}, @@ -161,7 +163,8 @@ public function addCollection(string $collectionName, Index ...$indices): void return $this->indexToSqlCmd($index, $collectionName); }, $indices); - $this->transactional(function() use ($cmd, $indicesCmds) { + $this->transactional(function() use ($createSchemaCmd, $cmd, $indicesCmds) { + $this->connection->prepare($createSchemaCmd)->execute(); $this->connection->prepare($cmd)->execute(); array_walk($indicesCmds, function ($cmd) { diff --git a/tests/SchemedPostgresDocumentStoreTest.php b/tests/SchemedPostgresDocumentStoreTest.php new file mode 100644 index 0000000..8e54346 --- /dev/null +++ b/tests/SchemedPostgresDocumentStoreTest.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\DocumentStoreTest\Postgres; + +use EventEngine\DocumentStore\Filter\AnyOfDocIdFilter; +use EventEngine\DocumentStore\Filter\AnyOfFilter; +use EventEngine\DocumentStore\Filter\DocIdFilter; +use EventEngine\DocumentStore\Filter\NotFilter; +use PHPUnit\Framework\TestCase; +use EventEngine\DocumentStore\FieldIndex; +use EventEngine\DocumentStore\Index; +use EventEngine\DocumentStore\MultiFieldIndex; +use EventEngine\DocumentStore\Postgres\PostgresDocumentStore; +use Ramsey\Uuid\Uuid; + +class SchemedPostgresDocumentStoreTest extends TestCase +{ + private CONST TABLE_PREFIX = 'test_'; + private CONST SCHEMA = 'test.'; + + /** + * @var PostgresDocumentStore + */ + protected $documentStore; + + /** + * @var \PDO + */ + protected $connection; + + protected function setUp(): void + { + $this->connection = TestUtil::getConnection(); + $this->documentStore = new PostgresDocumentStore($this->connection, self::TABLE_PREFIX); + } + + public function tearDown(): void + { + TestUtil::tearDownDatabase(); + } + + /** + * @test + */ + public function it_adds_collection_with_schema(): void + { + $this->documentStore->addCollection(self::SCHEMA . 'test'); + $this->assertFalse($this->documentStore->hasCollection('test')); + $this->assertTrue($this->documentStore->hasCollection(self::SCHEMA . 'test')); + } +} From b2ee535aaeafbde4d6b730aeea4759467a9e648d Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Mon, 9 Sep 2019 10:07:10 +0200 Subject: [PATCH 05/23] Remove changes accidentally introduces --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8cb3945..b7c1b34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,13 +7,13 @@ services: - .:/app environment: - PROOPH_ENV=dev - - PDO_DSN=pgsql:host=postgres port=5433 dbname=event_engine + - PDO_DSN=pgsql:host=postgres port=5432 dbname=event_engine - PDO_USER=postgres - PDO_PWD= postgres: image: postgres:alpine ports: - - 5433:5432 + - 5432:5432 environment: - POSTGRES_DB=event_engine From 11b9c4ade00998b6d5ee84ce080a4145d683cc02 Mon Sep 17 00:00:00 2001 From: codeliner Date: Wed, 2 Oct 2019 22:13:19 +0200 Subject: [PATCH 06/23] Fix InArray filter for object items --- src/PostgresDocumentStore.php | 2 +- tests/PostgresDocumentStoreTest.php | 91 ++++++++++++++++++++++ tests/SchemedPostgresDocumentStoreTest.php | 2 +- tests/TestUtil.php | 1 + 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 9c70dec..f80b3fa 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -621,7 +621,7 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array case DocumentStore\Filter\InArrayFilter::class: /** @var DocumentStore\Filter\InArrayFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); - return ["$prop @> :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + return ["$prop @> :a$argsCount", ["a$argsCount" => '[' . $this->prepareVal($filter->val(), $filter->prop()) . ']'], ++$argsCount]; case DocumentStore\Filter\ExistsFilter::class: /** @var DocumentStore\Filter\ExistsFilter $filter */ $prop = $this->propToJsonPath($filter->prop()); diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index fbfd40f..f42ea9c 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -14,6 +14,7 @@ use EventEngine\DocumentStore\Filter\AnyOfDocIdFilter; use EventEngine\DocumentStore\Filter\AnyOfFilter; use EventEngine\DocumentStore\Filter\DocIdFilter; +use EventEngine\DocumentStore\Filter\InArrayFilter; use EventEngine\DocumentStore\Filter\NotFilter; use PHPUnit\Framework\TestCase; use EventEngine\DocumentStore\FieldIndex; @@ -305,6 +306,96 @@ public function it_handles_not_any_of_id_filter() $this->assertEquals(['bat'], $vals); } + /** + * @test + */ + public function it_handles_in_array_filter() + { + $collectionName = 'test_in_array_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => ['bar' => ['tag1', 'tag2'], 'ref' => $firstDocId]]); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => ['bar' => ['tag2', 'tag3'], 'ref' => $secondDocId]]); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => ['bar' => ['tag3', 'tag4'], 'ref' => $thirdDocId]]); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new InArrayFilter('foo.bar', 'tag3') + )); + + $this->assertCount(2, $filteredDocs); + + $refs = array_map(function (array $doc) { + return $doc['foo']['ref']; + }, $filteredDocs); + + $this->assertEquals([$secondDocId, $thirdDocId], $refs); + } + + /** + * @test + */ + public function it_handles_not_in_array_filter() + { + $collectionName = 'test_not_in_array_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => ['bar' => ['tag1', 'tag2'], 'ref' => $firstDocId]]); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => ['bar' => ['tag2', 'tag3'], 'ref' => $secondDocId]]); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => ['bar' => ['tag3', 'tag4'], 'ref' => $thirdDocId]]); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new NotFilter(new InArrayFilter('foo.bar', 'tag3')) + )); + + $this->assertCount(1, $filteredDocs); + + $refs = array_map(function (array $doc) { + return $doc['foo']['ref']; + }, $filteredDocs); + + $this->assertEquals([$firstDocId], $refs); + } + + /** + * @test + */ + public function it_handles_in_array_filter_with_object_items() + { + $collectionName = 'test_in_array_with_object_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => ['bar' => [['tag' => 'tag1', 'other' => 'data'], ['tag' => 'tag2']], 'ref' => $firstDocId]]); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => ['bar' => [['tag' => 'tag2', 'other' => 'data'], ['tag' => 'tag3']], 'ref' => $secondDocId]]); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => ['bar' => [['tag' => 'tag3', 'other' => 'data'], ['tag' => 'tag4']], 'ref' => $thirdDocId]]); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new InArrayFilter('foo.bar', ['tag' => 'tag3']) + )); + + $this->assertCount(2, $filteredDocs); + + $refs = array_map(function (array $doc) { + return $doc['foo']['ref']; + }, $filteredDocs); + + $this->assertEquals([$secondDocId, $thirdDocId], $refs); + } + private function getIndexes(string $collectionName): array { return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); diff --git a/tests/SchemedPostgresDocumentStoreTest.php b/tests/SchemedPostgresDocumentStoreTest.php index 8e54346..4c513ad 100644 --- a/tests/SchemedPostgresDocumentStoreTest.php +++ b/tests/SchemedPostgresDocumentStoreTest.php @@ -43,7 +43,7 @@ protected function setUp(): void $this->documentStore = new PostgresDocumentStore($this->connection, self::TABLE_PREFIX); } - public function tearDown(): void + protected function tearDown(): void { TestUtil::tearDownDatabase(); } diff --git a/tests/TestUtil.php b/tests/TestUtil.php index fe263d0..8ce61bf 100644 --- a/tests/TestUtil.php +++ b/tests/TestUtil.php @@ -86,6 +86,7 @@ public static function tearDownDatabase(): void $connection = self::getConnection(); $statement = $connection->prepare('SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\';'); $connection->exec('DROP SCHEMA IF EXISTS prooph CASCADE'); + $connection->exec('DROP SCHEMA IF EXISTS test CASCADE'); $statement->execute(); $tables = $statement->fetchAll(PDO::FETCH_COLUMN); From 7acd9fe001bf6426128d64438fdedb4a77e2b08a Mon Sep 17 00:00:00 2001 From: codeliner Date: Tue, 12 Nov 2019 23:08:16 +0100 Subject: [PATCH 07/23] Bump php-persistence to v0.5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 90d005e..ba97b63 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1", "ext-pdo": "*", - "event-engine/php-persistence": "^0.4" + "event-engine/php-persistence": "^0.5" }, "require-dev": { "roave/security-advisories": "dev-master", From 6a1d2dbeba3db543f32c147eaaf7fc5c0c840378 Mon Sep 17 00:00:00 2001 From: codeliner Date: Wed, 19 Feb 2020 00:25:16 +0100 Subject: [PATCH 08/23] Fix that nested NOT filter is not working properly --- src/PostgresDocumentStore.php | 2 +- tests/PostgresDocumentStoreTest.php | 38 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index f80b3fa..d242afa 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -609,7 +609,7 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array throw new RuntimeException('Not filter cannot be combined with a non prop filter!'); } - [$innerFilterStr, $args, $argsCount] = $this->filterToWhereClause($innerFilter); + [$innerFilterStr, $args, $argsCount] = $this->filterToWhereClause($innerFilter, $argsCount); if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) { $inPos = strpos($innerFilterStr, ' IN('); diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index f42ea9c..a5dcf9c 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -11,9 +11,11 @@ namespace EventEngine\DocumentStoreTest\Postgres; +use EventEngine\DocumentStore\Filter\AndFilter; use EventEngine\DocumentStore\Filter\AnyOfDocIdFilter; use EventEngine\DocumentStore\Filter\AnyOfFilter; use EventEngine\DocumentStore\Filter\DocIdFilter; +use EventEngine\DocumentStore\Filter\EqFilter; use EventEngine\DocumentStore\Filter\InArrayFilter; use EventEngine\DocumentStore\Filter\NotFilter; use PHPUnit\Framework\TestCase; @@ -22,6 +24,7 @@ use EventEngine\DocumentStore\MultiFieldIndex; use EventEngine\DocumentStore\Postgres\PostgresDocumentStore; use Ramsey\Uuid\Uuid; +use function array_map; class PostgresDocumentStoreTest extends TestCase { @@ -396,6 +399,41 @@ public function it_handles_in_array_filter_with_object_items() $this->assertEquals([$secondDocId, $thirdDocId], $refs); } + /** + * @test + */ + public function it_handles_not_filter_nested_in_and_filter() + { + $collectionName = 'test_not_filter_nested_in_and_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => ['bar' => 'bas'], 'ref' => $firstDocId]); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => ['bar' => 'bat'], 'ref' => $secondDocId]); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => ['bar' => 'bat'], 'ref' => $thirdDocId]); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new AndFilter( + new EqFilter('foo.bar', 'bat'), + new NotFilter( + new EqFilter('ref', $secondDocId) + ) + ) + )); + + $this->assertCount(1, $filteredDocs); + + $refs = array_map(function (array $doc) { + return $doc['ref']; + }, $filteredDocs); + + $this->assertEquals([$thirdDocId], $refs); + } + private function getIndexes(string $collectionName): array { return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); From dd7468e9da33b7ce8f6eb52cdbc3c20c5bafa352 Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Tue, 24 Mar 2020 19:31:57 +0100 Subject: [PATCH 09/23] Implement countDocs from DocumentStore interface This implements the countDoc feature from the DocumentStore interface so it can be used together with the latest version of the DocumentStore. This allows top retrieve the number of affected documents without having to actually retrieve all the documents. --- src/PostgresDocumentStore.php | 23 +++++++++++++++++++++++ tests/PostgresDocumentStoreTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index d242afa..ba9fb81 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -524,6 +524,29 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n } } + /** + * @param string $collectionName + * @param Filter $filter + * @return int number of docs + */ + public function countDocs(string $collectionName, Filter $filter): int + { + [$filterStr, $args] = $this->filterToWhereClause($filter); + + $where = $filterStr? "WHERE $filterStr" : ''; + + $query = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +$where; +EOT; + $stmt = $this->connection->prepare($query); + + $stmt->execute($args); + + return (int) $stmt->fetchColumn(0); + } + private function transactional(callable $callback) { if($this->manageTransactions) { diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index a5dcf9c..e76c6b4 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -434,6 +434,32 @@ public function it_handles_not_filter_nested_in_and_filter() $this->assertEquals([$thirdDocId], $refs); } + /** + * @test + */ + public function it_counts_any_of_filter() + { + $collectionName = 'test_any_of_filter'; + $this->documentStore->addCollection($collectionName); + + $doc1 = ["foo" => "bar"]; + $doc2 = ["foo" => "baz"]; + $doc3 = ["foo" => "bat"]; + + $docs = [$doc1, $doc2, $doc3]; + + array_walk($docs, function (array $doc) use ($collectionName) { + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), $doc); + }); + + $count = $this->documentStore->countDocs( + $collectionName, + new AnyOfFilter("foo", ["bar", "bat"]) + ); + + $this->assertSame(2, $count); + } + private function getIndexes(string $collectionName): array { return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); From a33946ffe61b4abc93e10339e01703571d29a605 Mon Sep 17 00:00:00 2001 From: Martin Schilling Date: Tue, 17 Mar 2020 15:18:07 +0100 Subject: [PATCH 10/23] Implemented doc id retrieval as altered in the DocumentStore interface --- src/PostgresDocumentStore.php | 25 ++++++++++++++++++++++++- tests/PostgresDocumentStoreTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index ba9fb81..d88dcdb 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -500,7 +500,7 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n { [$filterStr, $args] = $this->filterToWhereClause($filter); - $where = $filterStr? "WHERE $filterStr" : ''; + $where = $filterStr ? "WHERE $filterStr" : ''; $offset = $skip !== null ? "OFFSET $skip" : ''; $limit = $limit !== null ? "LIMIT $limit" : ''; @@ -524,6 +524,29 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n } } + /** + * @param string $collectionName + * @param Filter $filter + * @return array + */ + public function filterDocIds(string $collectionName, Filter $filter): array + { + [$filterStr, $args] = $this->filterToWhereClause($filter); + + $where = $filterStr ? "WHERE {$filterStr}" : ''; + $query = "SELECT id FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} {$where}"; + + $stmt = $this->connection->prepare($query); + $stmt->execute($args); + + $docIds = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $docIds[] = $row['id']; + } + + return $docIds; + } + /** * @param string $collectionName * @param Filter $filter diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index e76c6b4..7aeeb07 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -16,8 +16,11 @@ use EventEngine\DocumentStore\Filter\AnyOfFilter; use EventEngine\DocumentStore\Filter\DocIdFilter; use EventEngine\DocumentStore\Filter\EqFilter; +use EventEngine\DocumentStore\Filter\GtFilter; use EventEngine\DocumentStore\Filter\InArrayFilter; +use EventEngine\DocumentStore\Filter\LtFilter; use EventEngine\DocumentStore\Filter\NotFilter; +use EventEngine\DocumentStore\Filter\OrFilter; use PHPUnit\Framework\TestCase; use EventEngine\DocumentStore\FieldIndex; use EventEngine\DocumentStore\Index; @@ -434,6 +437,30 @@ public function it_handles_not_filter_nested_in_and_filter() $this->assertEquals([$thirdDocId], $refs); } + /** + * @test + */ + public function it_retrieves_doc_ids_by_filter() + { + $collectionName = 'test_not_filter_nested_in_and_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['number' => 10]); + $this->documentStore->addDoc($collectionName, $secondDocId, ['number' => 20]); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['number' => 30]); + + $result = $this->documentStore->filterDocIds($collectionName, new OrFilter( + new GtFilter('number', 21), + new LtFilter('number', 19) + )); + + $this->assertEquals([$firstDocId, $thirdDocId], $result); + } + /** * @test */ From 1c60da63d16caeb0e1bff4f22476daf9653895f5 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 29 Mar 2020 22:37:51 +0200 Subject: [PATCH 11/23] Find partial docs --- composer.json | 2 +- src/PostgresDocumentStore.php | 156 +++++++++++++++++++++++- tests/PostgresDocumentStoreTest.php | 183 ++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index ba97b63..694da52 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1", "ext-pdo": "*", - "event-engine/php-persistence": "^0.5" + "event-engine/php-persistence": "^0.6" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index d88dcdb..9524805 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -15,12 +15,22 @@ use EventEngine\DocumentStore\Filter\Filter; use EventEngine\DocumentStore\Index; use EventEngine\DocumentStore\OrderBy\OrderBy; +use EventEngine\DocumentStore\PartialSelect; use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException; use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; use EventEngine\Util\VariableType; +use function implode; +use function is_string; +use function json_decode; +use function mb_strlen; +use function mb_substr; +use function sprintf; final class PostgresDocumentStore implements DocumentStore\DocumentStore { + private const PARTIAL_SELECT_DOC_ID = '__partial_sel_doc_id__'; + private const PARTIAL_SELECT_MERGE = '__partial_sel_merge__'; + /** * @var \PDO */ @@ -489,12 +499,7 @@ public function getDoc(string $collectionName, string $docId): ?array } /** - * @param string $collectionName - * @param Filter $filter - * @param int|null $skip - * @param int|null $limit - * @param OrderBy|null $orderBy - * @return \Traversable list of docs + * @inheritDoc */ public function filterDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable { @@ -524,6 +529,68 @@ 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 + { + [$filterStr, $args] = $this->filterToWhereClause($filter); + + $where = $filterStr ? "WHERE $filterStr" : ''; + + $offset = $skip !== null ? "OFFSET $skip" : ''; + $limit = $limit !== null ? "LIMIT $limit" : ''; + + $orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : ''; + + $query = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +$where +$orderBy +$limit +$offset; +EOT; + $stmt = $this->connection->prepare($query); + + $stmt->execute($args); + + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + yield $row['id'] => json_decode($row['doc'], true); + } + } + + public function findPartialDocs(string $collectionName, PartialSelect $partialSelect, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable + { + [$filterStr, $args] = $this->filterToWhereClause($filter); + + $select = $this->makeSelect($partialSelect); + + $where = $filterStr ? "WHERE $filterStr" : ''; + + $offset = $skip !== null ? "OFFSET $skip" : ''; + $limit = $limit !== null ? "LIMIT $limit" : ''; + + $orderBy = $orderBy ? "ORDER BY " . implode(', ', $this->orderByToSort($orderBy)) : ''; + + $query = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +$where +$orderBy +$limit +$offset; +EOT; + + $stmt = $this->connection->prepare($query); + + $stmt->execute($args); + + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + yield $row[self::PARTIAL_SELECT_DOC_ID] => $this->transformPartialDoc($partialSelect, $row); + } + } + /** * @param string $collectionName * @param Filter $filter @@ -714,6 +781,83 @@ private function makeInClause(string $prop, array $valList, int $argsCount, bool return ["$prop IN($params)", $argList, $argsCount]; } + private function makeSelect(PartialSelect $partialSelect): string + { + $select = 'id as "'.self::PARTIAL_SELECT_DOC_ID.'", '; + + foreach ($partialSelect->fieldAliasMap() as $mapItem) { + + if($mapItem['alias'] === self::PARTIAL_SELECT_DOC_ID) { + throw new RuntimeException(sprintf( + "Invalid select alias. You cannot use %s as alias, because it is reserved for internal use", + self::PARTIAL_SELECT_DOC_ID + )); + } + + if($mapItem['alias'] === self::PARTIAL_SELECT_MERGE) { + throw new RuntimeException(sprintf( + "Invalid select alias. You cannot use %s as alias, because it is reserved for internal use", + self::PARTIAL_SELECT_MERGE + )); + } + + if($mapItem['alias'] === PartialSelect::MERGE_ALIAS) { + $mapItem['alias'] = self::PARTIAL_SELECT_MERGE; + } + + $select.= $this->propToJsonPath($mapItem['field']) . ' as "' . $mapItem['alias'] . '", '; + } + + $select = mb_substr($select, 0, mb_strlen($select) - 2); + + return $select; + } + + private function transformPartialDoc(PartialSelect $partialSelect, array $selectedDoc): array + { + $partialDoc = []; + + foreach ($partialSelect->fieldAliasMap() as ['field' => $field, 'alias' => $alias]) { + if($alias === PartialSelect::MERGE_ALIAS) { + if(null === $selectedDoc[self::PARTIAL_SELECT_MERGE] ?? null) { + continue; + } + + $value = json_decode($selectedDoc[self::PARTIAL_SELECT_MERGE], true); + + if(!is_array($value)) { + throw new RuntimeException('Merge not possible. $merge alias was specified for field: ' . $field . ' but field value is not an array: ' . json_encode($value)); + } + + foreach ($value as $k => $v) { + $partialDoc[$k] = $v; + } + + continue; + } + + $value = $selectedDoc[$alias] ?? null; + + if(is_string($value)) { + $value = json_decode($value, true); + } + + $keys = explode('.', $alias); + + $ref = &$partialDoc; + foreach ($keys as $i => $key) { + if(!array_key_exists($key, $ref)) { + $ref[$key] = []; + } + $ref = &$ref[$key]; + } + $ref = $value; + unset($ref); + } + + return $partialDoc; + } + private function orderByToSort(DocumentStore\OrderBy\OrderBy $orderBy): array { $sort = []; diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 7aeeb07..4db9ecf 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -12,6 +12,7 @@ namespace EventEngine\DocumentStoreTest\Postgres; use EventEngine\DocumentStore\Filter\AndFilter; +use EventEngine\DocumentStore\Filter\AnyFilter; use EventEngine\DocumentStore\Filter\AnyOfDocIdFilter; use EventEngine\DocumentStore\Filter\AnyOfFilter; use EventEngine\DocumentStore\Filter\DocIdFilter; @@ -21,6 +22,7 @@ use EventEngine\DocumentStore\Filter\LtFilter; use EventEngine\DocumentStore\Filter\NotFilter; use EventEngine\DocumentStore\Filter\OrFilter; +use EventEngine\DocumentStore\PartialSelect; use PHPUnit\Framework\TestCase; use EventEngine\DocumentStore\FieldIndex; use EventEngine\DocumentStore\Index; @@ -28,6 +30,8 @@ use EventEngine\DocumentStore\Postgres\PostgresDocumentStore; use Ramsey\Uuid\Uuid; use function array_map; +use function array_walk; +use function iterator_to_array; class PostgresDocumentStoreTest extends TestCase { @@ -198,6 +202,35 @@ public function it_handles_any_of_filter() $this->assertCount(2, $filteredDocs); } + /** + * @test + */ + public function it_uses_doc_ids_as_iterator_keys() + { + $collectionName = 'test_any_of_filter'; + $this->documentStore->addCollection($collectionName); + + $doc1 = ['id' => Uuid::uuid4()->toString(), 'doc' => ["foo" => "bar"]]; + $doc2 = ['id' => Uuid::uuid4()->toString(), 'doc' => ["foo" => "baz"]]; + $doc3 = ['id' => Uuid::uuid4()->toString(), 'doc' => ["foo" => "bat"]]; + + $docs = [$doc1, $doc2, $doc3]; + + array_walk($docs, function (array $doc) use ($collectionName) { + $this->documentStore->addDoc($collectionName, $doc['id'], $doc['doc']); + }); + + $filteredDocs = iterator_to_array($this->documentStore->findDocs( + $collectionName, + new AnyOfFilter("foo", ["bar", "bat"]) + )); + + $this->assertEquals([ + $doc1['id'] => $doc1['doc'], + $doc3['id'] => $doc3['doc'] + ], $filteredDocs); + } + /** * @test */ @@ -487,6 +520,156 @@ public function it_counts_any_of_filter() $this->assertSame(2, $count); } + /** + * @test + */ + public function it_finds_partial_docs() + { + $collectionName = 'test_find_partial_docs'; + $this->documentStore->addCollection($collectionName); + + $docAId = Uuid::uuid4()->toString(); + $docA = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ] + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docAId, $docA); + + $docBId = Uuid::uuid4()->toString(); + $docB = [ + 'some' => [ + 'prop' => 'bar', + 'other' => [ + 'nested' => 43 + ], + //'baz' => 'bat', missing so should be null + ], + ]; + $this->documentStore->addDoc($collectionName, $docBId, $docB); + + $docCId = Uuid::uuid4()->toString(); + $docC = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + //'nested' => 42, missing, so should be null + 'ignoredNested' => 'value' + ] + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docCId, $docC); + + $partialSelect = new PartialSelect([ + 'some.alias' => 'some.prop', // Nested alias <- Nested field + 'magicNumber' => 'some.other.nested', // Top level alias <- Nested Field + 'baz', // Top level field, + ]); + + $result = iterator_to_array($this->documentStore->findPartialDocs($collectionName, $partialSelect, new AnyFilter())); + + $this->assertEquals([ + 'some' => [ + 'alias' => 'foo', + ], + 'magicNumber' => 42, + 'baz' => 'bat', + ], $result[$docAId]); + + $this->assertEquals([ + 'some' => [ + 'alias' => 'bar', + ], + 'magicNumber' => 43, + 'baz' => null, + ], $result[$docBId]); + + $this->assertEquals([ + 'some' => [ + 'alias' => 'foo', + ], + 'magicNumber' => null, + 'baz' => 'bat', + ], $result[$docCId]); + } + + /** + * @test + */ + public function it_applies_merge_alias_for_nested_fields_if_specified() + { + $collectionName = 'test_applies_merge_alias'; + $this->documentStore->addCollection($collectionName); + + $docAId = Uuid::uuid4()->toString(); + $docA = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ] + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docAId, $docA); + + $docBId = Uuid::uuid4()->toString(); + $docB = [ + 'differentTopLevel' => [ + 'prop' => 'bar', + 'other' => [ + 'nested' => 43 + ], + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docBId, $docB); + + $docCId = Uuid::uuid4()->toString(); + $docC = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 43 + ], + ], + //'baz' => 'bat', missing top level + ]; + $this->documentStore->addDoc($collectionName, $docCId, $docC); + + $partialSelect = new PartialSelect([ + '$merge' => 'some', // $merge alias <- Nested field + 'baz', // Top level field + ]); + + $result = iterator_to_array($this->documentStore->findPartialDocs($collectionName, $partialSelect, new AnyFilter())); + + $this->assertEquals([ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ], + 'baz' => 'bat' + ], $result[$docAId]); + + $this->assertEquals([ + 'baz' => 'bat', + ], $result[$docBId]); + + $this->assertEquals([ + 'prop' => 'foo', + 'other' => [ + 'nested' => 43 + ], + 'baz' => null + ], $result[$docCId]); + } + private function getIndexes(string $collectionName): array { return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); From 1e66123ebeb32567d79c7a2a250cc3dab9ddd8c1 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 29 Mar 2020 22:53:19 +0200 Subject: [PATCH 12/23] Use iLike for case insensitive like queries --- src/PostgresDocumentStore.php | 2 +- tests/PostgresDocumentStoreTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 9524805..1ac0b5b 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -713,7 +713,7 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array $propParts = explode('->', $prop); $lastProp = array_pop($propParts); $prop = implode('->', $propParts) . '->>'.$lastProp; - return ["$prop LIKE :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; + return ["$prop iLIKE :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; case DocumentStore\Filter\NotFilter::class: /** @var DocumentStore\Filter\NotFilter $filter */ $innerFilter = $filter->innerFilter(); diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 4db9ecf..54f959b 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -19,6 +19,7 @@ use EventEngine\DocumentStore\Filter\EqFilter; use EventEngine\DocumentStore\Filter\GtFilter; use EventEngine\DocumentStore\Filter\InArrayFilter; +use EventEngine\DocumentStore\Filter\LikeFilter; use EventEngine\DocumentStore\Filter\LtFilter; use EventEngine\DocumentStore\Filter\NotFilter; use EventEngine\DocumentStore\Filter\OrFilter; @@ -345,6 +346,34 @@ public function it_handles_not_any_of_id_filter() $this->assertEquals(['bat'], $vals); } + /** + * @test + */ + public function it_handles_case_insensitive_like_filter() + { + $collectionName = 'test_case_insensitive_like_filter'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => 'some BaR val']); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => 'some bAt val']); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => 'SOME baz VAL']); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new LikeFilter('foo', '%bat%') + )); + + $vals = array_map(function (array $doc) { + return $doc['foo']; + }, $filteredDocs); + + $this->assertEquals(['some bAt val'], $vals); + } + /** * @test */ From 018737f79f248f214fc961729b560f5bde5edc6a Mon Sep 17 00:00:00 2001 From: codeliner Date: Tue, 31 Mar 2020 22:11:39 +0200 Subject: [PATCH 13/23] Add method to retrieve a single partial doc --- composer.json | 2 +- src/PostgresDocumentStore.php | 25 +++++++++++ tests/PostgresDocumentStoreTest.php | 66 +++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 694da52..4c18e4b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1", "ext-pdo": "*", - "event-engine/php-persistence": "^0.6" + "event-engine/php-persistence": "^0.7" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 1ac0b5b..4e7ef62 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -498,6 +498,31 @@ public function getDoc(string $collectionName, string $docId): ?array return json_decode($row, true); } + /** + * @inheritDoc + */ + public function getPartialDoc(string $collectionName, PartialSelect $partialSelect, string $docId): ?array + { + $select = $this->makeSelect($partialSelect); + + $query = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +WHERE id = :id +EOT; + $stmt = $this->connection->prepare($query); + + $stmt->execute(['id' => $docId]); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) { + return null; + } + + return $this->transformPartialDoc($partialSelect, $row); + } + /** * @inheritDoc */ diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 54f959b..6753023 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -627,6 +627,72 @@ public function it_finds_partial_docs() ], $result[$docCId]); } + /** + * @test + */ + public function it_gets_partial_doc_by_id() + { + $collectionName = 'test_get_partial_doc'; + $this->documentStore->addCollection($collectionName); + + $docAId = Uuid::uuid4()->toString(); + $docA = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ] + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docAId, $docA); + + $docBId = Uuid::uuid4()->toString(); + $docB = [ + 'some' => [ + 'prop' => 'bar', + 'other' => [ + 'nested' => 43 + ], + //'baz' => 'bat', missing so should be null + ], + ]; + $this->documentStore->addDoc($collectionName, $docBId, $docB); + + $docCId = Uuid::uuid4()->toString(); + $docC = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + //'nested' => 42, missing, so should be null + 'ignoredNested' => 'value' + ] + ], + 'baz' => 'bat', + ]; + $this->documentStore->addDoc($collectionName, $docCId, $docC); + + $partialSelect = new PartialSelect([ + 'some.alias' => 'some.prop', // Nested alias <- Nested field + 'magicNumber' => 'some.other.nested', // Top level alias <- Nested Field + 'baz', // Top level field, + ]); + + $partialDocA = $this->documentStore->getPartialDoc($collectionName, $partialSelect, $docAId); + + $this->assertEquals([ + 'some' => [ + 'alias' => 'foo', + ], + 'magicNumber' => 42, + 'baz' => 'bat', + ], $partialDocA); + + $partialDocD = $this->documentStore->getPartialDoc($collectionName, $partialSelect, Uuid::uuid4()->toString()); + + $this->assertNull($partialDocD); + } + /** * @test */ From 83446936209e719eef63827ad77da7e331cb326d Mon Sep 17 00:00:00 2001 From: Andreas Heigl Date: Wed, 29 Apr 2020 11:20:25 +0200 Subject: [PATCH 14/23] Fix issue with empty array for IN-clause When an IN-clause is created with an empty array PostgreSQL will blow up with an SQL-Error as the resulting SQL would be ` x IN()` which is not allowed. This commit works around that by returning a query-part that always returns false as querying for something that is not part of something else will always return false. --- src/PostgresDocumentStore.php | 11 +++ tests/PostgresDocumentStoreTest.php | 117 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 4e7ef62..8bf7265 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -750,6 +750,14 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array [$innerFilterStr, $args, $argsCount] = $this->filterToWhereClause($innerFilter, $argsCount); if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) { + if ($argsCount === 0) { + return [ + substr_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr), + $args, + $argsCount + ]; + } + $inPos = strpos($innerFilterStr, ' IN('); $filterStr = substr_replace($innerFilterStr, ' NOT IN(', $inPos, 4 /* " IN(" */); return [$filterStr, $args, $argsCount]; @@ -795,6 +803,9 @@ private function isPropFilter(Filter $filter): bool private function makeInClause(string $prop, array $valList, int $argsCount, bool $jsonEncode = false): array { + if ($valList === []) { + return [' 1 != 1 ', [], 0]; + } $argList = []; $params = \implode(",", \array_map(function ($val) use (&$argsCount, &$argList, $jsonEncode) { $param = ":a$argsCount"; diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 6753023..b8dc9b0 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -203,6 +203,32 @@ public function it_handles_any_of_filter() $this->assertCount(2, $filteredDocs); } + /** + * @test + */ + public function it_handles_any_of_filter_with_empty_args() + { + $collectionName = 'test_any_of_filter_with_empty_args'; + $this->documentStore->addCollection($collectionName); + + $doc1 = ["foo" => "bar"]; + $doc2 = ["foo" => "baz"]; + $doc3 = ["foo" => "bat"]; + + $docs = [$doc1, $doc2, $doc3]; + + array_walk($docs, function (array $doc) use ($collectionName) { + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), $doc); + }); + + $filteredDocs = $this->documentStore->filterDocs( + $collectionName, + new AnyOfFilter("foo", []) + ); + + $this->assertCount(0, $filteredDocs); + } + /** * @test */ @@ -262,6 +288,37 @@ public function it_handles_not_any_of_filter() $this->assertSame('baz', $filteredDocs[0]['foo']); } + /** + * @test + */ + public function it_handles_not_any_of_filter_with_empty_args() + { + $collectionName = 'test_not_any_of_filter_with_empty_args'; + $this->documentStore->addCollection($collectionName); + + $doc1 = ["foo" => "bar"]; + $doc2 = ["foo" => "baz"]; + $doc3 = ["foo" => "bat"]; + + $docs = [$doc1, $doc2, $doc3]; + + array_walk($docs, function (array $doc) use ($collectionName) { + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), $doc); + }); + + $filteredDocs = $this->documentStore->filterDocs( + $collectionName, + new NotFilter(new AnyOfFilter("foo", [])) + ); + + $filteredDocs = iterator_to_array($filteredDocs); + + $this->assertCount(3, $filteredDocs); + + $this->assertSame('baz', $filteredDocs[1]['foo']); + $this->assertSame('bat', $filteredDocs[2]['foo']); + } + /** * @test */ @@ -316,6 +373,36 @@ public function it_handles_any_of_doc_id_filter() $this->assertEquals(['bar', 'baz'], $vals); } + /** + * @test + */ + public function it_handles_any_of_doc_id_filter_with_empty_args() + { + $collectionName = 'test_any_of_doc_id_filter_with_empty_args'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => 'bar']); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => 'bat']); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => 'baz']); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new AnyOfDocIdFilter([]) + )); + + $this->assertCount(0, $filteredDocs); + + $vals = array_map(function (array $doc) { + return $doc['foo']; + }, $filteredDocs); + + $this->assertEquals([], $vals); + } + /** * @test */ @@ -346,6 +433,36 @@ public function it_handles_not_any_of_id_filter() $this->assertEquals(['bat'], $vals); } + /** + * @test + */ + public function it_handles_not_any_of_id_filter_with_empty_args() + { + $collectionName = 'test_any_of_doc_id_filter_with_empty_args'; + $this->documentStore->addCollection($collectionName); + + $firstDocId = Uuid::uuid4()->toString(); + $secondDocId = Uuid::uuid4()->toString(); + $thirdDocId = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $firstDocId, ['foo' => 'bar']); + $this->documentStore->addDoc($collectionName, $secondDocId, ['foo' => 'bat']); + $this->documentStore->addDoc($collectionName, $thirdDocId, ['foo' => 'baz']); + + $filteredDocs = \iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new NotFilter(new AnyOfDocIdFilter([])) + )); + + $this->assertCount(3, $filteredDocs); + + $vals = array_map(function (array $doc) { + return $doc['foo']; + }, $filteredDocs); + + $this->assertEquals(['bar', 'bat', 'baz'], $vals); + } + /** * @test */ From 076ee4b76e9657bf147f2fb69961eb0ac8ca1d9f Mon Sep 17 00:00:00 2001 From: Gary Lockett Date: Sun, 27 Sep 2020 17:47:28 +0100 Subject: [PATCH 15/23] tests: confirm that (and how) associative array values are handled in updates --- docker-compose.yml | 3 +- phpunit.xml.dist | 2 +- tests/PostgresDocumentStoreTest.php | 65 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b7c1b34..834e7fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - PROOPH_ENV=dev - PDO_DSN=pgsql:host=postgres port=5432 dbname=event_engine - PDO_USER=postgres - - PDO_PWD= + - PDO_PWD=test postgres: image: postgres:alpine @@ -17,3 +17,4 @@ services: - 5432:5432 environment: - POSTGRES_DB=event_engine + - POSTGRES_PASSWORD=test diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f0ed2fe..d1121ea 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -22,7 +22,7 @@ - + diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index b8dc9b0..bd23fbc 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -151,6 +151,71 @@ public function it_adds_collection_with_multi_field_index_unique(): void $this->assertStringStartsWith('CREATE UNIQUE INDEX', $indexes[1]['indexdef']); } + /** + * @test + */ + public function it_adds_and_updates_a_doc() + { + $collectionName = 'test_adds_and_updates_a_doc'; + $this->documentStore->addCollection($collectionName); + + $doc = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ] + ], + 'baz' => 'bat', + ]; + + $docId = Uuid::uuid4()->toString(); + $this->documentStore->addDoc($collectionName, $docId, $doc); + + $persistedDoc = $this->documentStore->getDoc($collectionName, $docId); + + $this->assertEquals($doc, $persistedDoc); + + $doc['baz'] = 'changed val'; + + $this->documentStore->updateDoc($collectionName, $docId, $doc); + + $filter = new EqFilter('baz', 'changed val'); + + $filteredDocs = $this->documentStore->findDocs($collectionName, $filter); + + $this->assertCount(1, $filteredDocs); + } + + /** + * @test + */ + public function it_updates_a_subset_of_a_doc() + { + $collectionName = 'test_updates_a_subset_of_a_doc'; + $this->documentStore->addCollection($collectionName); + + $doc = [ + 'some' => [ + 'prop' => 'foo', + 'other' => 'bar' + ], + 'baz' => 'bat', + ]; + + $docId = Uuid::uuid4()->toString(); + $this->documentStore->addDoc($collectionName, $docId, $doc); + + $this->documentStore->updateDoc($collectionName, $docId, [ + 'some' => [ + 'prop' => 'fuzz' + ] + ]); + + $filteredDocs = array_values(iterator_to_array($this->documentStore->findDocs($collectionName, new EqFilter('some.prop', 'fuzz')))); + $this->assertArrayNotHasKey('other', $filteredDocs[0]['some']); + } + /** * @test */ From 5a179ea2c7855f76c56ba2c9664d39953e6a7cc8 Mon Sep 17 00:00:00 2001 From: Gary Lockett Date: Sun, 27 Sep 2020 22:05:03 +0100 Subject: [PATCH 16/23] feature: add replaceDoc and replaceMany methods --- src/PostgresDocumentStore.php | 75 +++++++++++++++++++++++++++++ tests/PostgresDocumentStoreTest.php | 58 ++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 8bf7265..b18817b 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -19,6 +19,7 @@ use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException; use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; use EventEngine\Util\VariableType; + use function implode; use function is_string; use function json_decode; @@ -433,6 +434,80 @@ public function upsertDoc(string $collectionName, string $docId, array $docOrSub } } + /** + * @param string $collectionName + * @param string $docId + * @param array $doc + * @throws \Throwable if updating did not succeed + */ + public function replaceDoc(string $collectionName, string $docId, array $doc): void + { + $metadataStr = ''; + $metadata = []; + + if($this->useMetadataColumns && array_key_exists('metadata', $doc)) { + $metadata = $doc['metadata']; + unset($doc['metadata']); + + + foreach ($metadata as $k => $v) { + $metadataStr .= ', '.$k.' = :'.$k; + } + } + + $cmd = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +SET doc = :doc{$metadataStr} +WHERE id = :id +; +EOT; + $this->transactional(function () use ($cmd, $docId, $doc, $metadata) { + $this->connection->prepare($cmd)->execute(array_merge([ + 'id' => $docId, + 'doc' => json_encode($doc) + ], $metadata)); + }); + } + + /** + * @param string $collectionName + * @param Filter $filter + * @param array $set + * @throws \Throwable in case of connection error or other issues + */ + public function replaceMany(string $collectionName, Filter $filter, array $set): void + { + [$filterStr, $args] = $this->filterToWhereClause($filter); + + $where = $filterStr? "WHERE $filterStr" : ''; + + $metadataStr = ''; + $metadata = []; + + if($this->useMetadataColumns && array_key_exists('metadata', $set)) { + $metadata = $set['metadata']; + unset($set['metadata']); + + + foreach ($metadata as $k => $v) { + $metadataStr .= ', '.$k.' = :'.$k; + } + } + + $cmd = <<schemaName($collectionName)}.{$this->tableName($collectionName)} +SET doc = :doc{$metadataStr} +$where; +EOT; + + $args['doc'] = json_encode($set); + $args = array_merge($args, $metadata); + + $this->transactional(function () use ($cmd, $args) { + $this->connection->prepare($cmd)->execute($args); + }); + } + /** * @param string $collectionName * @param string $docId diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index b8dc9b0..10671d5 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -151,6 +151,64 @@ public function it_adds_collection_with_multi_field_index_unique(): void $this->assertStringStartsWith('CREATE UNIQUE INDEX', $indexes[1]['indexdef']); } + /** + * @test + */ + public function it_replaces_a_doc() + { + $collectionName = 'test_replaces_a_doc'; + $this->documentStore->addCollection($collectionName); + + $doc = [ + 'some' => [ + 'prop' => 'foo', + 'other' => [ + 'nested' => 42 + ] + ], + 'baz' => 'bat', + ]; + + $docId = Uuid::uuid4()->toString(); + $this->documentStore->addDoc($collectionName, $docId, $doc); + + $doc = ['baz' => 'changed val']; + + $this->documentStore->replaceDoc($collectionName, $docId, $doc); + + $filter = new EqFilter('baz', 'changed val'); + + $filteredDocs = $this->documentStore->findDocs($collectionName, $filter); + + $this->assertCount(1, $filteredDocs); + } + + /** + * @test + */ + public function it_replaces_many() + { + $collectionName = 'test_replaces_many'; + $this->documentStore->addCollection($collectionName); + + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]]); + $this->documentStore->addDoc($collectionName, Uuid::uuid4()->toString(), ['some' => ['prop' => 'bar']]); + + $doc = ['some' => ['prop' => 'fuzz']]; + $this->documentStore->replaceMany( + $collectionName, + new EqFilter('some.other.prop', 'bat'), + $doc + ); + + $filteredDocs = array_values(iterator_to_array($this->documentStore->findDocs($collectionName, new EqFilter('some.prop', 'fuzz')))); + + $this->assertCount(2, $filteredDocs); + $this->assertEquals($doc, $filteredDocs[0]); + $this->assertEquals($doc, $filteredDocs[1]); + } + /** * @test */ From 8171a2db7bf7bab6c73051affef2a1a8101d82a0 Mon Sep 17 00:00:00 2001 From: Gary Lockett Date: Tue, 6 Oct 2020 22:01:22 +0100 Subject: [PATCH 17/23] fix(deps): update event-engine/php-persistence to ^0.8 chore(deps): update dependencies to latest PHP 7.2 version --- composer.json | 22 +++++++++++----------- phpunit.xml.dist | 1 - 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 4c18e4b..860458e 100644 --- a/composer.json +++ b/composer.json @@ -16,21 +16,21 @@ } ], "require": { - "php": "^7.1", + "php": "^7.2", "ext-pdo": "*", - "event-engine/php-persistence": "^0.7" + "event-engine/php-persistence": "^0.8" }, "require-dev": { + "infection/infection": "^0.15.3", + "malukenho/docheader": "^0.1.8", + "phpspec/prophecy": "^1.12.1", + "phpstan/phpstan": "^0.12.48", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpunit/phpunit": "^8.5.8", + "prooph/php-cs-fixer-config": "^0.3.1", + "ramsey/uuid" : "^4.1.1", "roave/security-advisories": "dev-master", - "ramsey/uuid" : "^3.6", - "infection/infection": "^0.11.0", - "malukenho/docheader": "^0.1.4", - "phpspec/prophecy": "^1.7", - "phpstan/phpstan": "^0.10.5", - "phpstan/phpstan-strict-rules": "^0.10.1", - "phpunit/phpunit": "^8.0", - "prooph/php-cs-fixer-config": "^0.3", - "satooshi/php-coveralls": "^1.0" + "php-coveralls/php-coveralls": "^2.2.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d1121ea..529e2db 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,7 +7,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="vendor/autoload.php" > From 7ad5feb168bc1bc269d8a00a7623ba2fe50b57ff Mon Sep 17 00:00:00 2001 From: Zacharias Luiten Date: Sat, 30 Jan 2021 16:44:58 +0100 Subject: [PATCH 18/23] - Allow PHP 8 - Updated (dev) dependencies - Bugfix: replaced substr_replace with str_repalce - Updated doc header with copyright year --- composer.json | 4 ++-- docker-compose.yml | 2 +- src/Exception/InvalidArgumentException.php | 2 +- src/Exception/PostgresDocumentStoreException.php | 2 +- src/Exception/RuntimeException.php | 2 +- src/Index/RawSqlIndexCmd.php | 8 ++++++++ src/Metadata/Column.php | 8 ++++++++ src/Metadata/MetadataColumnIndex.php | 8 ++++++++ src/PostgresDocumentStore.php | 4 ++-- tests/MetadataPostgresDocumentStoreTest.php | 8 ++++++++ tests/PostgresDocumentStoreTest.php | 2 +- tests/SchemedPostgresDocumentStoreTest.php | 2 +- tests/TestUtil.php | 2 +- 13 files changed, 43 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 860458e..d83e631 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "^7.2", + "php": "^7.2 || ^8.0", "ext-pdo": "*", "event-engine/php-persistence": "^0.8" }, @@ -27,7 +27,7 @@ "phpstan/phpstan": "^0.12.48", "phpstan/phpstan-strict-rules": "^0.12.5", "phpunit/phpunit": "^8.5.8", - "prooph/php-cs-fixer-config": "^0.3.1", + "prooph/php-cs-fixer-config": "^0.4.0", "ramsey/uuid" : "^4.1.1", "roave/security-advisories": "dev-master", "php-coveralls/php-coveralls": "^2.2.0" diff --git a/docker-compose.yml b/docker-compose.yml index 834e7fd..18581e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '2' services: php: - image: prooph/php:7.2-cli + image: prooph/php:8.0-cli volumes: - .:/app environment: diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 6cb0c61..4d271f4 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Exception/PostgresDocumentStoreException.php b/src/Exception/PostgresDocumentStoreException.php index b58a56f..a4de8ee 100644 --- a/src/Exception/PostgresDocumentStoreException.php +++ b/src/Exception/PostgresDocumentStoreException.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index d0c6274..efef111 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Index/RawSqlIndexCmd.php b/src/Index/RawSqlIndexCmd.php index 87c6c8c..b776179 100644 --- a/src/Index/RawSqlIndexCmd.php +++ b/src/Index/RawSqlIndexCmd.php @@ -1,4 +1,12 @@ + * + * 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\Index; diff --git a/src/Metadata/Column.php b/src/Metadata/Column.php index 276a893..2101565 100644 --- a/src/Metadata/Column.php +++ b/src/Metadata/Column.php @@ -1,4 +1,12 @@ + * + * 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\Metadata; diff --git a/src/Metadata/MetadataColumnIndex.php b/src/Metadata/MetadataColumnIndex.php index 02e98a6..4948466 100644 --- a/src/Metadata/MetadataColumnIndex.php +++ b/src/Metadata/MetadataColumnIndex.php @@ -1,4 +1,12 @@ + * + * 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\Metadata; diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index b18817b..ddbbfc2 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -827,7 +827,7 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) { if ($argsCount === 0) { return [ - substr_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr), + str_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr), $args, $argsCount ]; diff --git a/tests/MetadataPostgresDocumentStoreTest.php b/tests/MetadataPostgresDocumentStoreTest.php index 402d3fc..1b5b7bd 100644 --- a/tests/MetadataPostgresDocumentStoreTest.php +++ b/tests/MetadataPostgresDocumentStoreTest.php @@ -1,4 +1,12 @@ + * + * 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\DocumentStoreTest\Postgres; diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 93d5a50..1b43640 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/SchemedPostgresDocumentStoreTest.php b/tests/SchemedPostgresDocumentStoreTest.php index 4c513ad..180377d 100644 --- a/tests/SchemedPostgresDocumentStoreTest.php +++ b/tests/SchemedPostgresDocumentStoreTest.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/TestUtil.php b/tests/TestUtil.php index 8ce61bf..9efe706 100644 --- a/tests/TestUtil.php +++ b/tests/TestUtil.php @@ -1,7 +1,7 @@ + * (c) 2019-2021 prooph software GmbH * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. From 92b60a8cdaeb7c985545764f25a7a6678b54da07 Mon Sep 17 00:00:00 2001 From: Alexander Miertsch Date: Sat, 6 Feb 2021 23:36:29 +0100 Subject: [PATCH 19/23] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d83e631..ecfca2e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-pdo": "*", - "event-engine/php-persistence": "^0.8" + "event-engine/php-persistence": "^0.9" }, "require-dev": { "infection/infection": "^0.15.3", From 6162636b2843f920a916691e3db829dd4baee108 Mon Sep 17 00:00:00 2001 From: Sandro Keil Date: Tue, 25 May 2021 13:36:16 +0200 Subject: [PATCH 20/23] Check if doc is null - Close #22 --- src/PostgresDocumentStore.php | 2 +- tests/PostgresDocumentStoreTest.php | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index ddbbfc2..2d9362c 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -427,7 +427,7 @@ public function upsertDoc(string $collectionName, string $docId, array $docOrSub { $doc = $this->getDoc($collectionName, $docId); - if($doc) { + if ($doc !== null) { $this->updateDoc($collectionName, $docId, $docOrSubset); } else { $this->addDoc($collectionName, $docId, $docOrSubset); diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index 1b43640..55922eb 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -150,7 +150,7 @@ public function it_adds_collection_with_multi_field_index_unique(): void ); $this->assertStringStartsWith('CREATE UNIQUE INDEX', $indexes[1]['indexdef']); } - + /** * @test */ @@ -216,6 +216,30 @@ public function it_updates_a_subset_of_a_doc() $this->assertArrayNotHasKey('other', $filteredDocs[0]['some']); } + /** + * @test + */ + public function it_upserts_empty_array_doc(): void + { + $collectionName = 'test_upserts_empty_doc'; + $this->documentStore->addCollection($collectionName); + + $doc = []; + + $docId = Uuid::uuid4()->toString(); + $this->documentStore->addDoc($collectionName, $docId, $doc); + + // be aware that this will add the data as an entry to the array which is wrong, because it should be transformed to an object + $this->documentStore->upsertDoc($collectionName, $docId, [ + 'some' => [ + 'prop' => 'fuzz', + ], + ]); + + $doc = $this->documentStore->getDoc($collectionName, $docId); + $this->assertArrayHasKey('some', $doc[0], \var_export($doc, true)); + } + /** * @test */ From 1937cc12e48880984b7c164f530a1072377b10a9 Mon Sep 17 00:00:00 2001 From: Zacharias Luiten Date: Tue, 23 Feb 2021 12:16:11 +0100 Subject: [PATCH 21/23] Extract filter processing logic to a seperate class --- composer.json | 7 +- src/Filter/FilterClause.php | 34 ++++ src/Filter/FilterProcessor.php | 19 +++ src/Filter/PostgresFilterProcessor.php | 199 +++++++++++++++++++++++ src/PostgresDocumentStore.php | 213 ++++++------------------- 5 files changed, 306 insertions(+), 166 deletions(-) create mode 100644 src/Filter/FilterClause.php create mode 100644 src/Filter/FilterProcessor.php create mode 100644 src/Filter/PostgresFilterProcessor.php diff --git a/composer.json b/composer.json index ecfca2e..8470564 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,12 @@ ], "require": { "php": "^7.2 || ^8.0", + "ext-json": "*", "ext-pdo": "*", "event-engine/php-persistence": "^0.9" }, "require-dev": { - "infection/infection": "^0.15.3", + "infection/infection": "^0.26.6", "malukenho/docheader": "^0.1.8", "phpspec/prophecy": "^1.12.1", "phpstan/phpstan": "^0.12.48", @@ -45,6 +46,10 @@ "config": { "sort-packages": true, "platform": { + }, + "allow-plugins": { + "ocramius/package-versions": true, + "infection/extension-installer": true } }, "prefer-stable": true, diff --git a/src/Filter/FilterClause.php b/src/Filter/FilterClause.php new file mode 100644 index 0000000..8147c88 --- /dev/null +++ b/src/Filter/FilterClause.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\Filter; + +final class FilterClause +{ + 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/Filter/FilterProcessor.php b/src/Filter/FilterProcessor.php new file mode 100644 index 0000000..3a62c53 --- /dev/null +++ b/src/Filter/FilterProcessor.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\Filter; + +use EventEngine\DocumentStore\Filter\Filter; + +interface FilterProcessor +{ + public function process(Filter $filter): FilterClause; +} diff --git a/src/Filter/PostgresFilterProcessor.php b/src/Filter/PostgresFilterProcessor.php new file mode 100644 index 0000000..c8091c3 --- /dev/null +++ b/src/Filter/PostgresFilterProcessor.php @@ -0,0 +1,199 @@ + + * + * 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\Filter; + +use EventEngine\DocumentStore; +use EventEngine\DocumentStore\Filter\Filter; +use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException; +use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; + +/** + * Default filter processor class for converting a filter to a where clause. + */ +final class PostgresFilterProcessor implements FilterProcessor +{ + /** + * @var bool + */ + private $useMetadataColumns; + + public function __construct(bool $useMetadataColumns = false) + { + $this->useMetadataColumns = $useMetadataColumns; + } + + public function process(Filter $filter): FilterClause + { + [$filterClause, $args] = $this->processFilter($filter); + + return new FilterClause($filterClause, $args); + } + + /** + * @param Filter $filter + * @param int $argsCount + * @return array + */ + private function processFilter(Filter $filter, int $argsCount = 0): array + { + if($filter instanceof DocumentStore\Filter\AnyFilter) { + if($argsCount > 0) { + throw new InvalidArgumentException('AnyFilter cannot be used together with other filters.'); + } + return [null, [], $argsCount]; + } + + if($filter instanceof DocumentStore\Filter\AndFilter) { + [$filterA, $argsA, $argsCount] = $this->processFilter($filter->aFilter(), $argsCount); + [$filterB, $argsB, $argsCount] = $this->processFilter($filter->bFilter(), $argsCount); + return ["($filterA AND $filterB)", array_merge($argsA, $argsB), $argsCount]; + } + + if($filter instanceof DocumentStore\Filter\OrFilter) { + [$filterA, $argsA, $argsCount] = $this->processFilter($filter->aFilter(), $argsCount); + [$filterB, $argsB, $argsCount] = $this->processFilter($filter->bFilter(), $argsCount); + return ["($filterA OR $filterB)", array_merge($argsA, $argsB), $argsCount]; + } + + switch (get_class($filter)) { + case DocumentStore\Filter\DocIdFilter::class: + /** @var DocumentStore\Filter\DocIdFilter $filter */ + return ["id = :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; + case DocumentStore\Filter\AnyOfDocIdFilter::class: + /** @var DocumentStore\Filter\AnyOfDocIdFilter $filter */ + return $this->makeInClause('id', $filter->valList(), $argsCount); + case DocumentStore\Filter\AnyOfFilter::class: + /** @var DocumentStore\Filter\AnyOfFilter $filter */ + return $this->makeInClause($this->propToJsonPath($filter->prop()), $filter->valList(), $argsCount, $this->shouldJsonEncodeVal($filter->prop())); + case DocumentStore\Filter\EqFilter::class: + /** @var DocumentStore\Filter\EqFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop = :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + case DocumentStore\Filter\GtFilter::class: + /** @var DocumentStore\Filter\GtFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop > :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + case DocumentStore\Filter\GteFilter::class: + /** @var DocumentStore\Filter\GteFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop >= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + case DocumentStore\Filter\LtFilter::class: + /** @var DocumentStore\Filter\LtFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop < :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + case DocumentStore\Filter\LteFilter::class: + /** @var DocumentStore\Filter\LteFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop <= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; + case DocumentStore\Filter\LikeFilter::class: + /** @var DocumentStore\Filter\LikeFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + $propParts = explode('->', $prop); + $lastProp = array_pop($propParts); + $prop = implode('->', $propParts) . '->>'.$lastProp; + return ["$prop iLIKE :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; + case DocumentStore\Filter\NotFilter::class: + /** @var DocumentStore\Filter\NotFilter $filter */ + $innerFilter = $filter->innerFilter(); + + if (!$this->isPropFilter($innerFilter)) { + throw new RuntimeException('Not filter cannot be combined with a non prop filter!'); + } + + [$innerFilterStr, $args, $argsCount] = $this->processFilter($innerFilter, $argsCount); + + if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) { + if ($argsCount === 0) { + return [ + str_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr), + $args, + $argsCount + ]; + } + + $inPos = strpos($innerFilterStr, ' IN('); + $filterStr = substr_replace($innerFilterStr, ' NOT IN(', $inPos, 4 /* " IN(" */); + return [$filterStr, $args, $argsCount]; + } + + return ["NOT $innerFilterStr", $args, $argsCount]; + case DocumentStore\Filter\InArrayFilter::class: + /** @var DocumentStore\Filter\InArrayFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + return ["$prop @> :a$argsCount", ["a$argsCount" => '[' . $this->prepareVal($filter->val(), $filter->prop()) . ']'], ++$argsCount]; + case DocumentStore\Filter\ExistsFilter::class: + /** @var DocumentStore\Filter\ExistsFilter $filter */ + $prop = $this->propToJsonPath($filter->prop()); + $propParts = explode('->', $prop); + $lastProp = trim(array_pop($propParts), "'"); + $parentProps = implode('->', $propParts); + return ["JSONB_EXISTS($parentProps, '$lastProp')", [], $argsCount]; + default: + throw new RuntimeException('Unsupported filter type. Got ' . get_class($filter)); + } + } + + private function makeInClause(string $prop, array $valList, int $argsCount, bool $jsonEncode = false): array + { + if ($valList === []) { + return [' 1 != 1 ', [], 0]; + } + $argList = []; + $params = \implode(",", \array_map(function ($val) use (&$argsCount, &$argList, $jsonEncode) { + $param = ":a$argsCount"; + $argList["a$argsCount"] = $jsonEncode? \json_encode($val) : $val; + $argsCount++; + return $param; + }, $valList)); + + return ["$prop IN($params)", $argList, $argsCount]; + } + + private function shouldJsonEncodeVal(string $prop): bool + { + if($this->useMetadataColumns && strpos($prop, 'metadata.') === 0) { + return false; + } + + return true; + } + + private function propToJsonPath(string $field): string + { + if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) { + return str_replace('metadata.', '', $field); + } + + return "doc->'" . str_replace('.', "'->'", $field) . "'"; + } + + private function isPropFilter(Filter $filter): bool + { + switch (get_class($filter)) { + case DocumentStore\Filter\AndFilter::class: + case DocumentStore\Filter\OrFilter::class: + case DocumentStore\Filter\NotFilter::class: + return false; + default: + return true; + } + } + + private function prepareVal($value, string $prop) + { + if(!$this->shouldJsonEncodeVal($prop)) { + return $value; + } + + return \json_encode($value); + } +} diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index ddbbfc2..39e0925 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -16,8 +16,9 @@ use EventEngine\DocumentStore\Index; use EventEngine\DocumentStore\OrderBy\OrderBy; use EventEngine\DocumentStore\PartialSelect; -use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException; use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; +use EventEngine\DocumentStore\Postgres\Filter\PostgresFilterProcessor; +use EventEngine\DocumentStore\Postgres\Filter\FilterProcessor; use EventEngine\Util\VariableType; use function implode; @@ -37,24 +38,35 @@ final class PostgresDocumentStore implements DocumentStore\DocumentStore */ private $connection; + /** + * @var FilterProcessor + */ + private $filterProcessor; + private $tablePrefix = 'em_ds_'; private $docIdSchema = 'UUID NOT NULL'; private $manageTransactions; - private $useMetadataColumns = false; + private $useMetadataColumns; public function __construct( \PDO $connection, string $tablePrefix = null, string $docIdSchema = null, bool $transactional = true, - bool $useMetadataColumns = false + bool $useMetadataColumns = false, + FilterProcessor $filterProcessor = null ) { $this->connection = $connection; $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + if (null === $filterProcessor) { + $filterProcessor = new PostgresFilterProcessor($useMetadataColumns); + } + $this->filterProcessor = $filterProcessor; + if(null !== $tablePrefix) { $this->tablePrefix = $tablePrefix; } @@ -384,9 +396,11 @@ public function updateDoc(string $collectionName, string $docId, array $docOrSub */ public function updateMany(string $collectionName, Filter $filter, array $set): void { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); - $where = $filterStr? "WHERE $filterStr" : ''; + $where = $filterStr ? "WHERE $filterStr" : ''; $metadataStr = ''; $metadata = []; @@ -477,7 +491,9 @@ public function replaceDoc(string $collectionName, string $docId, array $doc): v */ public function replaceMany(string $collectionName, Filter $filter, array $set): void { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr? "WHERE $filterStr" : ''; @@ -534,7 +550,9 @@ public function deleteDoc(string $collectionName, string $docId): void */ public function deleteMany(string $collectionName, Filter $filter): void { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr? "WHERE $filterStr" : ''; @@ -603,7 +621,9 @@ public function getPartialDoc(string $collectionName, PartialSelect $partialSele */ public function filterDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr ? "WHERE $filterStr" : ''; @@ -634,7 +654,9 @@ public function filterDocs(string $collectionName, Filter $filter, int $skip = n */ public function findDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr ? "WHERE $filterStr" : ''; @@ -662,7 +684,9 @@ 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 { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $select = $this->makeSelect($partialSelect); @@ -698,7 +722,9 @@ public function findPartialDocs(string $collectionName, PartialSelect $partialSe */ public function filterDocIds(string $collectionName, Filter $filter): array { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr ? "WHERE {$filterStr}" : ''; $query = "SELECT id FROM {$this->schemaName($collectionName)}.{$this->tableName($collectionName)} {$where}"; @@ -721,7 +747,9 @@ public function filterDocIds(string $collectionName, Filter $filter): array */ public function countDocs(string $collectionName, Filter $filter): int { - [$filterStr, $args] = $this->filterToWhereClause($filter); + $filterClause = $this->filterProcessor->process($filter); + $filterStr = $filterClause->clause(); + $args = $filterClause->args(); $where = $filterStr? "WHERE $filterStr" : ''; @@ -756,105 +784,6 @@ private function transactional(callable $callback) } } - private function filterToWhereClause(Filter $filter, $argsCount = 0): array - { - if($filter instanceof DocumentStore\Filter\AnyFilter) { - if($argsCount > 0) { - throw new InvalidArgumentException('AnyFilter cannot be used together with other filters.'); - } - return [null, [], $argsCount]; - } - - if($filter instanceof DocumentStore\Filter\AndFilter) { - [$filterA, $argsA, $argsCount] = $this->filterToWhereClause($filter->aFilter(), $argsCount); - [$filterB, $argsB, $argsCount] = $this->filterToWhereClause($filter->bFilter(), $argsCount); - return ["($filterA AND $filterB)", array_merge($argsA, $argsB), $argsCount]; - } - - if($filter instanceof DocumentStore\Filter\OrFilter) { - [$filterA, $argsA, $argsCount] = $this->filterToWhereClause($filter->aFilter(), $argsCount); - [$filterB, $argsB, $argsCount] = $this->filterToWhereClause($filter->bFilter(), $argsCount); - return ["($filterA OR $filterB)", array_merge($argsA, $argsB), $argsCount]; - } - - switch (get_class($filter)) { - case DocumentStore\Filter\DocIdFilter::class: - /** @var DocumentStore\Filter\DocIdFilter $filter */ - return ["id = :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; - case DocumentStore\Filter\AnyOfDocIdFilter::class: - /** @var DocumentStore\Filter\AnyOfDocIdFilter $filter */ - return $this->makeInClause('id', $filter->valList(), $argsCount); - case DocumentStore\Filter\AnyOfFilter::class: - /** @var DocumentStore\Filter\AnyOfFilter $filter */ - return $this->makeInClause($this->propToJsonPath($filter->prop()), $filter->valList(), $argsCount, $this->shouldJsonEncodeVal($filter->prop())); - case DocumentStore\Filter\EqFilter::class: - /** @var DocumentStore\Filter\EqFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop = :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; - case DocumentStore\Filter\GtFilter::class: - /** @var DocumentStore\Filter\GtFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop > :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; - case DocumentStore\Filter\GteFilter::class: - /** @var DocumentStore\Filter\GteFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop >= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; - case DocumentStore\Filter\LtFilter::class: - /** @var DocumentStore\Filter\LtFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop < :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; - case DocumentStore\Filter\LteFilter::class: - /** @var DocumentStore\Filter\LteFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop <= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount]; - case DocumentStore\Filter\LikeFilter::class: - /** @var DocumentStore\Filter\LikeFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - $propParts = explode('->', $prop); - $lastProp = array_pop($propParts); - $prop = implode('->', $propParts) . '->>'.$lastProp; - return ["$prop iLIKE :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount]; - case DocumentStore\Filter\NotFilter::class: - /** @var DocumentStore\Filter\NotFilter $filter */ - $innerFilter = $filter->innerFilter(); - - if (!$this->isPropFilter($innerFilter)) { - throw new RuntimeException('Not filter cannot be combined with a non prop filter!'); - } - - [$innerFilterStr, $args, $argsCount] = $this->filterToWhereClause($innerFilter, $argsCount); - - if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) { - if ($argsCount === 0) { - return [ - str_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr), - $args, - $argsCount - ]; - } - - $inPos = strpos($innerFilterStr, ' IN('); - $filterStr = substr_replace($innerFilterStr, ' NOT IN(', $inPos, 4 /* " IN(" */); - return [$filterStr, $args, $argsCount]; - } - - return ["NOT $innerFilterStr", $args, $argsCount]; - case DocumentStore\Filter\InArrayFilter::class: - /** @var DocumentStore\Filter\InArrayFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - return ["$prop @> :a$argsCount", ["a$argsCount" => '[' . $this->prepareVal($filter->val(), $filter->prop()) . ']'], ++$argsCount]; - case DocumentStore\Filter\ExistsFilter::class: - /** @var DocumentStore\Filter\ExistsFilter $filter */ - $prop = $this->propToJsonPath($filter->prop()); - $propParts = explode('->', $prop); - $lastProp = trim(array_pop($propParts), "'"); - $parentProps = implode('->', $propParts); - return ["JSONB_EXISTS($parentProps, '$lastProp')", [], $argsCount]; - default: - throw new RuntimeException('Unsupported filter type. Got ' . get_class($filter)); - } - } - private function propToJsonPath(string $field): string { if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) { @@ -864,34 +793,6 @@ private function propToJsonPath(string $field): string return "doc->'" . str_replace('.', "'->'", $field) . "'"; } - private function isPropFilter(Filter $filter): bool - { - switch (get_class($filter)) { - case DocumentStore\Filter\AndFilter::class: - case DocumentStore\Filter\OrFilter::class: - case DocumentStore\Filter\NotFilter::class: - return false; - default: - return true; - } - } - - private function makeInClause(string $prop, array $valList, int $argsCount, bool $jsonEncode = false): array - { - if ($valList === []) { - return [' 1 != 1 ', [], 0]; - } - $argList = []; - $params = \implode(",", \array_map(function ($val) use (&$argsCount, &$argList, $jsonEncode) { - $param = ":a$argsCount"; - $argList["a$argsCount"] = $jsonEncode? \json_encode($val) : $val; - $argsCount++; - return $param; - }, $valList)); - - return ["$prop IN($params)", $argList, $argsCount]; - } - private function makeSelect(PartialSelect $partialSelect): string { $select = 'id as "'.self::PARTIAL_SELECT_DOC_ID.'", '; @@ -1016,24 +917,6 @@ private function indexToSqlCmd(Index $index, string $collectionName): string return $cmd; } - private function prepareVal($value, string $prop) - { - if(!$this->shouldJsonEncodeVal($prop)) { - return $value; - } - - return \json_encode($value); - } - - private function shouldJsonEncodeVal(string $prop): bool - { - if($this->useMetadataColumns && strpos($prop, 'metadata.') === 0) { - return false; - } - - return true; - } - private function getIndexName(Index $index): ?string { if(method_exists($index, 'name')) { @@ -1066,12 +949,12 @@ private function tableName(string $collectionName): string return mb_strtolower($this->tablePrefix . $collectionName); } - private function schemaName(string $collectionName): string - { - $schemaName = 'public'; - if (false !== $dotPosition = strpos($collectionName, '.')) { - $schemaName = substr($collectionName, 0, $dotPosition); - } - return mb_strtolower($schemaName); - } + private function schemaName(string $collectionName): string + { + $schemaName = 'public'; + if (false !== $dotPosition = strpos($collectionName, '.')) { + $schemaName = substr($collectionName, 0, $dotPosition); + } + return mb_strtolower($schemaName); + } } From 909c5d46bbb8fa99e41f5606e787fcc95719c54e Mon Sep 17 00:00:00 2001 From: Zacharias Luiten Date: Mon, 1 Mar 2021 12:18:38 +0100 Subject: [PATCH 22/23] 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 23/23] 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(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy