diff --git a/src/Count.php b/src/Count.php index 62b2513..8254d74 100644 --- a/src/Count.php +++ b/src/Count.php @@ -36,4 +36,31 @@ class Count return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new CountMapper()); } + + /** + * Count matching documents using a JSON containment query (`@>`; PostgreSQL only) + * + * @param string $tableName The name of the table in which documents should be counted + * @param array|object $criteria The criteria for the JSON containment query + * @return int The number of documents matching the JSON containment query + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria): int + { + return Custom::scalar(Query\Count::byContains($tableName), Parameters::json(':criteria', $criteria), + new CountMapper()); + } + + /** + * Count matching documents using a JSON Path match query (`@?`; PostgreSQL only) + * + * @param string $tableName The name of the table in which documents should be counted + * @param string $path The JSON Path match string + * @return int The number of documents matching the given JSON Path criteria + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path): int + { + return Custom::scalar(Query\Count::byJsonPath($tableName), [':path' => $path], new CountMapper()); + } } diff --git a/src/Delete.php b/src/Delete.php index 22b84c8..d325724 100644 --- a/src/Delete.php +++ b/src/Delete.php @@ -33,4 +33,28 @@ class Delete Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, [])); } + + /** + * Delete documents matching a JSON containment query (`@>`; PostgreSQL only) + * + * @param string $tableName The table from which documents should be deleted + * @param array|object $criteria The JSON containment query values + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria): void + { + Custom::nonQuery(Query\Delete::byContains($tableName), Parameters::json(':criteria', $criteria)); + } + + /** + * Delete documents matching a JSON Path match query (`@?`; PostgreSQL only) + * + * @param string $tableName The table from which documents should be deleted + * @param string $path The JSON Path match string + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path): void + { + Custom::nonQuery(Query\Delete::byJsonPath($tableName), [':path' => $path]); + } } diff --git a/src/Exists.php b/src/Exists.php index c668107..74142bc 100644 --- a/src/Exists.php +++ b/src/Exists.php @@ -37,4 +37,31 @@ class Exists return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new ExistsMapper()); } + + /** + * Determine if documents exist by a JSON containment query (`@>`; PostgreSQL only) + * + * @param string $tableName The name of the table in which document existence should be determined + * @param array|object $criteria The criteria for the JSON containment query + * @return bool True if any documents match the JSON containment query, false if not + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria): bool + { + return Custom::scalar(Query\Exists::byContains($tableName), Parameters::json(':criteria', $criteria), + new ExistsMapper()); + } + + /** + * Determine if documents exist by a JSON Path match query (`@?`; PostgreSQL only) + * + * @param string $tableName The name of the table in which document existence should be determined + * @param string $path The JSON Path match string + * @return bool True if any documents match the JSON Path string, false if not + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path): bool + { + return Custom::scalar(Query\Exists::byJsonPath($tableName), [':path' => $path], new ExistsMapper()); + } } diff --git a/src/Find.php b/src/Find.php index d6022f1..055868b 100644 --- a/src/Find.php +++ b/src/Find.php @@ -58,6 +58,37 @@ class Find Parameters::addFields($namedFields, []), new DocumentMapper($className)); } + /** + * Retrieve documents via a JSON containment query (`@>`; PostgreSQL only) + * + * @template TDoc The type of document to be retrieved + * @param string $tableName The name of the table from which documents should be retrieved + * @param array|object $criteria The criteria for the JSON containment query + * @param class-string $className The name of the class to be retrieved + * @return DocumentList A list of documents matching the JSON containment query + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria, string $className): DocumentList + { + return Custom::list(Query\Find::byContains($tableName), Parameters::json(':criteria', $criteria), + new DocumentMapper($className)); + } + + /** + * Retrieve documents via a JSON Path match query (`@?`; PostgreSQL only) + * + * @template TDoc The type of document to be retrieved + * @param string $tableName The name of the table from which documents should be retrieved + * @param string $path The JSON Path match string + * @param class-string $className The name of the class to be retrieved + * @return DocumentList A list of documents matching the JSON Path + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path, string $className): DocumentList + { + return Custom::list(Query\Find::byJsonPath($tableName), [':path' => $path], new DocumentMapper($className)); + } + /** * Retrieve documents via a comparison on JSON fields, returning only the first result * @@ -76,4 +107,35 @@ class Find return Custom::single(Query\Find::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new DocumentMapper($className)); } + + /** + * Retrieve documents via a JSON containment query (`@>`), returning only the first result (PostgreSQL only) + * + * @template TDoc The type of document to be retrieved + * @param string $tableName The name of the table from which documents should be retrieved + * @param array|object $criteria The criteria for the JSON containment query + * @param class-string $className The name of the class to be retrieved + * @return false|TDoc The first document matching the JSON containment query if any is found, false otherwise + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function firstByContains(string $tableName, array|object $criteria, string $className): mixed + { + return Custom::single(Query\Find::byContains($tableName), Parameters::json(':criteria', $criteria), + new DocumentMapper($className)); + } + + /** + * Retrieve documents via a JSON Path match query (`@?`), returning only the first result (PostgreSQL only) + * + * @template TDoc The type of document to be retrieved + * @param string $tableName The name of the table from which documents should be retrieved + * @param string $path The JSON Path match string + * @param class-string $className The name of the class to be retrieved + * @return false|TDoc The first document matching the JSON Path if any is found, false otherwise + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function firstByJsonPath(string $tableName, string $path, string $className): mixed + { + return Custom::single(Query\Find::byJsonPath($tableName), [':path' => $path], new DocumentMapper($className)); + } } diff --git a/src/Patch.php b/src/Patch.php index 038d9cd..f8e2737 100644 --- a/src/Patch.php +++ b/src/Patch.php @@ -37,4 +37,32 @@ class Patch Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, Parameters::json(':data', $patch))); } + + /** + * Patch documents using a JSON containment query (`@>`; PostgreSQL only) + * + * @param string $tableName The table in which documents should be patched + * @param array|object $criteria The JSON containment query values to match + * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria, array|object $patch): void + { + Custom::nonQuery(Query\Patch::byContains($tableName), + array_merge(Parameters::json(':criteria', $criteria), Parameters::json(':data', $patch))); + } + + /** + * Patch documents using a JSON Path match query (`@?`; PostgreSQL only) + * + * @param string $tableName The table in which documents should be patched + * @param string $path The JSON Path match string + * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path, array|object $patch): void + { + Custom::nonQuery(Query\Patch::byJsonPath($tableName), + array_merge([':path' => $path], Parameters::json(':data', $patch))); + } } diff --git a/src/Query.php b/src/Query.php index 51ed2e4..82f6824 100644 --- a/src/Query.php +++ b/src/Query.php @@ -75,7 +75,7 @@ class Query if (Configuration::$mode <> Mode::PgSQL) { throw new DocumentException('JSON Path matching is only supported on PostgreSQL'); } - return "data @? $paramName::jsonpath"; + return "jsonb_path_exists(data, $paramName::jsonpath)"; } /** diff --git a/src/RemoveFields.php b/src/RemoveFields.php index 792468b..1885bfe 100644 --- a/src/RemoveFields.php +++ b/src/RemoveFields.php @@ -39,4 +39,34 @@ class RemoveFields Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $match), Parameters::addFields($namedFields, $nameParams)); } + + /** + * Remove fields from documents via a JSON containment query (`@>`; PostgreSQL only) + * + * @param string $tableName The table in which documents should have fields removed + * @param array|object $criteria The JSON containment query values + * @param array|string[] $fieldNames The names of the fields to be removed + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byContains(string $tableName, array|object $criteria, array $fieldNames): void + { + $nameParams = Parameters::fieldNames(':name', $fieldNames); + Custom::nonQuery(Query\RemoveFields::byContains($tableName, $nameParams), + array_merge(Parameters::json(':criteria', $criteria), $nameParams)); + } + + /** + * Remove fields from documents via a JSON Path match query (`@?`; PostgreSQL only) + * + * @param string $tableName The table in which documents should have fields removed + * @param string $path The JSON Path match string + * @param array|string[] $fieldNames The names of the fields to be removed + * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs + */ + public static function byJsonPath(string $tableName, string $path, array $fieldNames): void + { + $nameParams = Parameters::fieldNames(':name', $fieldNames); + Custom::nonQuery(Query\RemoveFields::byJsonPath($tableName, $nameParams), + array_merge([':path' => $path], $nameParams)); + } } diff --git a/tests/integration/postgresql/CountTest.php b/tests/integration/postgresql/CountTest.php index 7403013..dbdb6ae 100644 --- a/tests/integration/postgresql/CountTest.php +++ b/tests/integration/postgresql/CountTest.php @@ -44,4 +44,30 @@ class CountTest extends TestCase $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); $this->assertEquals(1, $count, 'There should have been 1 matching document'); } + + public function testByContainsSucceedsWhenDocumentsMatch(): void + { + $this->assertEquals(2, Count::byContains(ThrowawayDb::TABLE, ['value' => 'purple']), + 'There should have been 2 matching documents'); + } + + public function testByContainsSucceedsWhenNoDocumentsMatch(): void + { + $this->assertEquals(0, Count::byContains(ThrowawayDb::TABLE, ['value' => 'magenta']), + 'There should have been no matching documents'); + } + + #[TestDox('By JSON Path succeeds when documents match')] + public function testByJsonPathSucceedsWhenDocumentsMatch(): void + { + $this->assertEquals(2, Count::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ < 5)'), + 'There should have been 2 matching documents'); + } + + #[TestDox('By JSON Path succeeds when no documents match')] + public function testByJsonPathSucceedsWhenNoDocumentsMatch(): void + { + $this->assertEquals(0, Count::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'), + 'There should have been no matching documents'); + } } diff --git a/tests/integration/postgresql/DeleteTest.php b/tests/integration/postgresql/DeleteTest.php index 2c85011..cac03cc 100644 --- a/tests/integration/postgresql/DeleteTest.php +++ b/tests/integration/postgresql/DeleteTest.php @@ -56,4 +56,34 @@ class DeleteTest extends TestCase Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + + public function testByContainsSucceedsWhenDocumentsAreDeleted(): void + { + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); + Delete::byContains(ThrowawayDb::TABLE, ['value' => 'purple']); + $this->assertEquals(3, Count::all(ThrowawayDb::TABLE), 'There should have been 3 documents remaining'); + } + + public function testByContainsSucceedsWhenDocumentsAreNotDeleted(): void + { + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); + Delete::byContains(ThrowawayDb::TABLE, ['target' => 'acquired']); + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); + } + + #[TestDox('By JSON Path succeeds when documents are deleted')] + public function testByJsonPathSucceedsWhenDocumentsAreDeleted(): void + { + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); + Delete::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ <> 0)'); + $this->assertEquals(1, Count::all(ThrowawayDb::TABLE), 'There should have been 1 document remaining'); + } + + #[TestDox('By JSON Path succeeds when documents are not deleted')] + public function testByJsonPathSucceedsWhenDocumentsAreNotDeleted(): void + { + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); + Delete::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ < 0)'); + $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); + } } diff --git a/tests/integration/postgresql/ExistsTest.php b/tests/integration/postgresql/ExistsTest.php index cc7509c..ef67b34 100644 --- a/tests/integration/postgresql/ExistsTest.php +++ b/tests/integration/postgresql/ExistsTest.php @@ -51,4 +51,30 @@ class ExistsTest extends TestCase $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), 'There should not have been any existing documents'); } + + public function testByContainsSucceedsWhenDocumentsExist(): void + { + $this->assertTrue(Exists::byContains(ThrowawayDb::TABLE, ['value' => 'purple']), + 'There should have been existing documents'); + } + + public function testByContainsSucceedsWhenNoMatchingDocumentsExist(): void + { + $this->assertFalse(Exists::byContains(ThrowawayDb::TABLE, ['value' => 'violet']), + 'There should not have been existing documents'); + } + + #[TestDox('By JSON Path succeeds when documents exist')] + public function testByJsonPathSucceedsWhenDocumentsExist(): void + { + $this->assertTrue(Exists::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)'), + 'There should have been existing documents'); + } + + #[TestDox('By JSON Path succeeds when no matching documents exist')] + public function testByJsonPathSucceedsWhenNoMatchingDocumentsExist(): void + { + $this->assertFalse(Exists::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10.1)'), + 'There should have been existing documents'); + } } diff --git a/tests/integration/postgresql/FindTest.php b/tests/integration/postgresql/FindTest.php index 4649911..3895d31 100644 --- a/tests/integration/postgresql/FindTest.php +++ b/tests/integration/postgresql/FindTest.php @@ -86,6 +86,40 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } + public function testByContainsSucceedsWhenDocumentsAreFound(): void + { + $docs = Find::byContains(ThrowawayDb::TABLE, ['value' => 'purple'], TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $this->assertEquals(2, $count, 'There should have been 2 documents in the list'); + } + + public function testByContainsSucceedsWhenNoDocumentsAreFound(): void + { + $docs = Find::byContains(ThrowawayDb::TABLE, ['value' => 'indigo'], TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertFalse($docs->hasItems(), 'The document list should be empty'); + } + + #[TestDox('By JSON Path succeeds when documents are found')] + public function testByJsonPathSucceedsWhenDocumentsAreFound(): void + { + $docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $this->assertEquals(2, $count, 'There should have been 2 documents in the list'); + } + + #[TestDox('By JSON Path succeeds when no documents are found')] + public function testByJsonPathSucceedsWhenNoDocumentsAreFound(): void + { + $docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)', TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertFalse($docs->hasItems(), 'The document list should be empty'); + } + public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void { $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); @@ -105,4 +139,46 @@ class FindTest extends TestCase $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class); $this->assertFalse($doc, 'There should not have been a document returned'); } + + public function testFirstByContainsSucceedsWhenADocumentIsFound(): void + { + $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'FIRST!'], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals('one', $doc->id, 'The incorrect document was returned'); + } + + public function testFirstByContainsSucceedsWhenMultipleDocumentsAreFound(): void + { + $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'purple'], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertContains($doc->id, ['four', 'five'], 'An incorrect document was returned'); + } + + public function testFirstByContainsSucceedsWhenADocumentIsNotFound(): void + { + $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'indigo'], TestDocument::class); + $this->assertFalse($doc, 'There should not have been a document returned'); + } + + #[TestDox('First by JSON Path succeeds when a document is found')] + public function testFirstByJsonPathSucceedsWhenADocumentIsFound(): void + { + $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)', TestDocument::class); + $this->assertEquals('two', $doc->id, 'The incorrect document was returned'); + } + + #[TestDox('First by JSON Path succeeds when multiple documents are found')] + public function testFirstByJsonPathSucceedsWhenMultipleDocumentsAreFound(): void + { + $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertContains($doc->id, ['four', 'five'], 'An incorrect document was returned'); + } + + #[TestDox('First by JSON Path succeeds when a document is not found')] + public function testFirstByJsonPathSucceedsWhenADocumentIsNotFound(): void + { + $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)', TestDocument::class); + $this->assertFalse($doc, 'There should not have been a document returned'); + } } diff --git a/tests/integration/postgresql/PatchTest.php b/tests/integration/postgresql/PatchTest.php index 1b58aa8..e8bfad1 100644 --- a/tests/integration/postgresql/PatchTest.php +++ b/tests/integration/postgresql/PatchTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\PostgreSQL; -use BitBadger\PDODocument\{Count, Field, Find, Patch}; +use BitBadger\PDODocument\{Count, Exists, Field, Find, Patch}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use Test\Integration\TestDocument; @@ -40,7 +40,9 @@ class PatchTest extends TestCase #[TestDox('By ID succeeds when no document is updated')] public function testByIdSucceedsWhenNoDocumentIsUpdated(): void { - Patch::byId(ThrowawayDb::TABLE, 'forty-seven', ['foo' => 'green']); + $id = 'forty-seven'; + $this->assertFalse(Exists::byId(ThrowawayDb::TABLE, $id), 'The document should not exist'); + Patch::byId(ThrowawayDb::TABLE, $id, ['foo' => 'green']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } @@ -53,7 +55,50 @@ class PatchTest extends TestCase public function testByFieldsSucceedsWhenNoDocumentIsUpdated(): void { - Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burgundy')], ['foo' => 'green']); + $fields = [Field::EQ('value', 'burgundy')]; + $this->assertEquals(0, Count::byFields(ThrowawayDb::TABLE, $fields), 'There should be no matching documents'); + Patch::byFields(ThrowawayDb::TABLE, $fields, ['foo' => 'green']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } + + public function testByContainsSucceedsWhenDocumentsAreUpdated(): void + { + Patch::byContains(ThrowawayDb::TABLE, ['value' => 'another'], ['num_value' => 12]); + $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'another'], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals('two', $doc->id, 'An incorrect document was returned'); + $this->assertEquals(12, $doc->num_value, 'The document was not patched'); + } + + public function testByContainsSucceedsWhenNoDocumentsAreUpdated(): void + { + $criteria = ['value' => 'updated']; + $this->assertEquals(0, Count::byContains(ThrowawayDb::TABLE, $criteria), + 'There should be no matching documents'); + Patch::byContains(ThrowawayDb::TABLE, $criteria, ['sub.foo' => 'green']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } + + #[TestDox('By JSON Path succeeds when documents are updated')] + public function testByJsonPathSucceedsWhenDocumentsAreUpdated(): void + { + Patch::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', ['value' => 'blue']); + $docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertTrue($docs->hasItems(), 'The document list should not be empty'); + foreach ($docs->items() as $item) { + $this->assertContains($item->id, ['four', 'five'], 'An incorrect document was returned'); + $this->assertEquals('blue', $item->value, 'The document was not patched'); + } + } + + #[TestDox('By JSON Path succeeds when documents are not updated')] + public function testByJsonPathSucceedsWhenDocumentsAreNotUpdated(): void + { + $path = '$.num_value ? (@ > 100)'; + $this->assertEquals(0, Count::byJsonPath(ThrowawayDb::TABLE, $path), + 'There should be no documents matching this path'); + Patch::byJsonPath(ThrowawayDb::TABLE, $path, ['value' => 'blue']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } } diff --git a/tests/integration/postgresql/RemoveFieldsTest.php b/tests/integration/postgresql/RemoveFieldsTest.php index 37c27e2..9015e7a 100644 --- a/tests/integration/postgresql/RemoveFieldsTest.php +++ b/tests/integration/postgresql/RemoveFieldsTest.php @@ -71,4 +71,57 @@ class RemoveFieldsTest extends TestCase RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + + public function testByContainsSucceedsWhenAFieldIsRemoved(): void + { + $criteria = ['sub' => ['foo' => 'green']]; + RemoveFields::byContains(ThrowawayDb::TABLE, $criteria, ['value']); + $docs = Find::byContains(ThrowawayDb::TABLE, $criteria, TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertTrue($docs->hasItems(), 'The document list should not have been empty'); + foreach ($docs->items() as $item) { + $this->assertContains($item->id, ['two', 'four'], 'An incorrect document was returned'); + $this->assertEquals('', $item->value, 'The value field was not removed'); + } + } + + public function testByContainsSucceedsWhenAFieldIsNotRemoved(): void + { + RemoveFields::byContains(ThrowawayDb::TABLE, ['sub' => ['foo' => 'green']], ['invalid_field']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } + + public function testByContainsSucceedsWhenNoDocumentIsMatched(): void + { + RemoveFields::byContains(ThrowawayDb::TABLE, ['value' => 'substantial'], ['num_value']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } + + #[TestDox('By JSON Path succeeds when a field is removed')] + public function testByJsonPathSucceedsWhenAFieldIsRemoved(): void + { + $path = '$.value ? (@ == "purple")'; + RemoveFields::byJsonPath(ThrowawayDb::TABLE, $path, ['sub']); + $docs = Find::byJsonPath(ThrowawayDb::TABLE, $path, TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertTrue($docs->hasItems(), 'The document list should not have been empty'); + foreach ($docs->items() as $item) { + $this->assertContains($item->id, ['four', 'five'], 'An incorrect document was returned'); + $this->assertNull($item->sub, 'The sub field was not removed'); + } + } + + #[TestDox('By JSON Path succeeds when a field is not removed')] + public function testByJsonPathSucceedsWhenAFieldIsNotRemoved(): void + { + RemoveFields::byJsonPath(ThrowawayDb::TABLE, '$.value ? (@ == "purple")', ['submarine']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } + + #[TestDox('By JSON Path succeeds when no document is matched')] + public function testByJsonPathSucceedsWhenNoDocumentIsMatched(): void + { + RemoveFields::byJsonPath(ThrowawayDb::TABLE, '$.value ? (@ == "mauve")', ['value']); + $this->assertTrue(true, 'The above not throwing an exception is the test'); + } } diff --git a/tests/integration/sqlite/CountTest.php b/tests/integration/sqlite/CountTest.php index 259bf28..285d4d2 100644 --- a/tests/integration/sqlite/CountTest.php +++ b/tests/integration/sqlite/CountTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Count, Field}; +use BitBadger\PDODocument\{Count, DocumentException, Field}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -44,4 +44,17 @@ class CountTest extends TestCase $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); $this->assertEquals(1, $count, 'There should have been 1 matching document'); } + + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + Count::byContains('', []); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Count::byJsonPath('', ''); + } } diff --git a/tests/integration/sqlite/DeleteTest.php b/tests/integration/sqlite/DeleteTest.php index 58a664a..e012d0b 100644 --- a/tests/integration/sqlite/DeleteTest.php +++ b/tests/integration/sqlite/DeleteTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Count, Delete, Field}; +use BitBadger\PDODocument\{Count, Delete, DocumentException, Field}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -56,4 +56,17 @@ class DeleteTest extends TestCase Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + Delete::byContains('', []); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Delete::byJsonPath('', ''); + } } diff --git a/tests/integration/sqlite/ExistsTest.php b/tests/integration/sqlite/ExistsTest.php index 0454b10..e9966ff 100644 --- a/tests/integration/sqlite/ExistsTest.php +++ b/tests/integration/sqlite/ExistsTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Exists, Field}; +use BitBadger\PDODocument\{DocumentException, Exists, Field}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -51,4 +51,18 @@ class ExistsTest extends TestCase $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), 'There should not have been any existing documents'); } + + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + Exists::byContains('', []); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Exists::byJsonPath('', ''); + } } + diff --git a/tests/integration/sqlite/FindTest.php b/tests/integration/sqlite/FindTest.php index c134433..c1677a0 100644 --- a/tests/integration/sqlite/FindTest.php +++ b/tests/integration/sqlite/FindTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Custom, Document, Field, Find}; +use BitBadger\PDODocument\{Custom, Document, DocumentException, Field, Find}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use Test\Integration\TestDocument; @@ -85,6 +85,19 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + Find::byContains('', [], TestDocument::class); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Find::byJsonPath('', '', TestDocument::class); + } + public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void { $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); @@ -104,4 +117,17 @@ class FindTest extends TestCase $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class); $this->assertFalse($doc, 'There should not have been a document returned'); } + + public function testFirstByContainsFails(): void + { + $this->expectException(DocumentException::class); + Find::firstByContains('', [], TestDocument::class); + } + + #[TestDox('First by JSON Path fails')] + public function testFirstByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Find::firstByJsonPath('', '', TestDocument::class); + } } diff --git a/tests/integration/sqlite/PatchTest.php b/tests/integration/sqlite/PatchTest.php index b366425..5598753 100644 --- a/tests/integration/sqlite/PatchTest.php +++ b/tests/integration/sqlite/PatchTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Count, Field, Find, Patch}; +use BitBadger\PDODocument\{Count, DocumentException, Field, Find, Patch}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use Test\Integration\TestDocument; @@ -56,4 +56,17 @@ class PatchTest extends TestCase Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burgundy')], ['foo' => 'green']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + Patch::byContains('', [], []); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + Patch::byJsonPath('', '', []); + } } diff --git a/tests/integration/sqlite/RemoveFieldsTest.php b/tests/integration/sqlite/RemoveFieldsTest.php index e1b3cf1..d656ded 100644 --- a/tests/integration/sqlite/RemoveFieldsTest.php +++ b/tests/integration/sqlite/RemoveFieldsTest.php @@ -2,7 +2,7 @@ namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Field, Find, RemoveFields}; +use BitBadger\PDODocument\{DocumentException, Field, Find, RemoveFields}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use Test\Integration\TestDocument; @@ -71,4 +71,17 @@ class RemoveFieldsTest extends TestCase RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + + public function testByContainsFails(): void + { + $this->expectException(DocumentException::class); + RemoveFields::byContains('', [], []); + } + + #[TestDox('By JSON Path fails')] + public function testByJsonPathFails(): void + { + $this->expectException(DocumentException::class); + RemoveFields::byJsonPath('', '', []); + } } diff --git a/tests/unit/Query/CountTest.php b/tests/unit/Query/CountTest.php index 58d9ac0..8917cea 100644 --- a/tests/unit/Query/CountTest.php +++ b/tests/unit/Query/CountTest.php @@ -37,7 +37,7 @@ class CountTest extends TestCase public function testByContainsSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals("SELECT COUNT(*) FROM the_table WHERE data @> :criteria", Count::byContains('the_table'), + $this->assertEquals('SELECT COUNT(*) FROM the_table WHERE data @> :criteria', Count::byContains('the_table'), 'SELECT statement not generated correctly'); } @@ -52,8 +52,8 @@ class CountTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals("SELECT COUNT(*) FROM a_table WHERE data @? :path::jsonpath", Count::byJsonPath('a_table'), - 'SELECT statement not generated correctly'); + $this->assertEquals('SELECT COUNT(*) FROM a_table WHERE jsonb_path_exists(data, :path::jsonpath)', + Count::byJsonPath('a_table'), 'SELECT statement not generated correctly'); } #[TestDox('By JSON Path fails for non PostgreSQL')] diff --git a/tests/unit/Query/DeleteTest.php b/tests/unit/Query/DeleteTest.php index 9efe38b..7206e3f 100644 --- a/tests/unit/Query/DeleteTest.php +++ b/tests/unit/Query/DeleteTest.php @@ -53,8 +53,8 @@ class DeleteTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals('DELETE FROM here WHERE data @? :path::jsonpath', Delete::byJsonPath('here'), - 'DELETE statement not constructed correctly'); + $this->assertEquals('DELETE FROM here WHERE jsonb_path_exists(data, :path::jsonpath)', + Delete::byJsonPath('here'), 'DELETE statement not constructed correctly'); } #[TestDox('By JSON Path fails for non PostgreSQL')] diff --git a/tests/unit/Query/ExistsTest.php b/tests/unit/Query/ExistsTest.php index ba77418..053e340 100644 --- a/tests/unit/Query/ExistsTest.php +++ b/tests/unit/Query/ExistsTest.php @@ -60,7 +60,7 @@ class ExistsTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals('SELECT EXISTS (SELECT 1 FROM lint WHERE data @? :path::jsonpath)', + $this->assertEquals('SELECT EXISTS (SELECT 1 FROM lint WHERE jsonb_path_exists(data, :path::jsonpath))', Exists::byJsonPath('lint'), 'Existence query not generated correctly'); } diff --git a/tests/unit/Query/FindTest.php b/tests/unit/Query/FindTest.php index 2a98d82..36677f2 100644 --- a/tests/unit/Query/FindTest.php +++ b/tests/unit/Query/FindTest.php @@ -54,8 +54,8 @@ class FindTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals('SELECT data FROM light WHERE data @? :path::jsonpath', Find::byJsonPath('light'), - 'SELECT query not generated correctly'); + $this->assertEquals('SELECT data FROM light WHERE jsonb_path_exists(data, :path::jsonpath)', + Find::byJsonPath('light'), 'SELECT query not generated correctly'); } #[TestDox('By JSON Path fails for non PostgreSQL')] diff --git a/tests/unit/Query/PatchTest.php b/tests/unit/Query/PatchTest.php index bf5e05e..8382730 100644 --- a/tests/unit/Query/PatchTest.php +++ b/tests/unit/Query/PatchTest.php @@ -83,7 +83,7 @@ class PatchTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals('UPDATE that SET data = data || :data WHERE data @? :path::jsonpath', + $this->assertEquals('UPDATE that SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)', Patch::byJsonPath('that'), 'Patch UPDATE statement is not correct'); } diff --git a/tests/unit/Query/RemoveFieldsTest.php b/tests/unit/Query/RemoveFieldsTest.php index 7d5dfac..c3b71a5 100644 --- a/tests/unit/Query/RemoveFieldsTest.php +++ b/tests/unit/Query/RemoveFieldsTest.php @@ -112,7 +112,8 @@ class RemoveFieldsTest extends TestCase public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - $this->assertEquals('UPDATE dessert SET data = data - :cake::text[] WHERE data @? :path::jsonpath', + $this->assertEquals( + 'UPDATE dessert SET data = data - :cake::text[] WHERE jsonb_path_exists(data, :path::jsonpath)', RemoveFields::byJsonPath('dessert', Parameters::fieldNames(':cake', ['b', 'c'])), 'UPDATE statement not correct'); } diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 3c0c945..623e7d7 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -96,7 +96,7 @@ class QueryTest extends TestCase { Configuration::$mode = Mode::PgSQL; try { - $this->assertEquals('data @? :path::jsonpath', Query::whereJsonPathMatches(), + $this->assertEquals('jsonb_path_exists(data, :path::jsonpath)', Query::whereJsonPathMatches(), 'WHERE fragment not constructed correctly'); } finally { Configuration::$mode = null; @@ -108,7 +108,7 @@ class QueryTest extends TestCase { Configuration::$mode = Mode::PgSQL; try { - $this->assertEquals('data @? :road::jsonpath', Query::whereJsonPathMatches(':road'), + $this->assertEquals('jsonb_path_exists(data, :road::jsonpath)', Query::whereJsonPathMatches(':road'), 'WHERE fragment not constructed correctly'); } finally { Configuration::$mode = null;