Add PostgreSQL Support #3

Merged
danieljsummers merged 4 commits from alpha3 into main 2024-06-21 13:46:42 +00:00
26 changed files with 569 additions and 22 deletions
Showing only changes of commit 7de39a41fc - Show all commits

View File

@ -36,4 +36,31 @@ class Count
return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $match), return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $match),
Parameters::addFields($namedFields, []), new CountMapper()); 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());
}
} }

View File

@ -33,4 +33,28 @@ class Delete
Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $match), Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $match),
Parameters::addFields($namedFields, [])); 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]);
}
} }

View File

@ -37,4 +37,31 @@ class Exists
return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $match), return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $match),
Parameters::addFields($namedFields, []), new ExistsMapper()); 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());
}
} }

View File

@ -58,6 +58,37 @@ class Find
Parameters::addFields($namedFields, []), new DocumentMapper($className)); 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<TDoc> $className The name of the class to be retrieved
* @return DocumentList<TDoc> 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<TDoc> $className The name of the class to be retrieved
* @return DocumentList<TDoc> 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 * 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), return Custom::single(Query\Find::byFields($tableName, $namedFields, $match),
Parameters::addFields($namedFields, []), new DocumentMapper($className)); 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<TDoc> $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<TDoc> $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));
}
} }

View File

@ -37,4 +37,32 @@ class Patch
Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $match), Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $match),
Parameters::addFields($namedFields, Parameters::json(':data', $patch))); 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)));
}
} }

View File

@ -75,7 +75,7 @@ class Query
if (Configuration::$mode <> Mode::PgSQL) { if (Configuration::$mode <> Mode::PgSQL) {
throw new DocumentException('JSON Path matching is only supported on PostgreSQL'); throw new DocumentException('JSON Path matching is only supported on PostgreSQL');
} }
return "data @? $paramName::jsonpath"; return "jsonb_path_exists(data, $paramName::jsonpath)";
} }
/** /**

View File

@ -39,4 +39,34 @@ class RemoveFields
Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $match), Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $match),
Parameters::addFields($namedFields, $nameParams)); 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));
}
} }

View File

@ -44,4 +44,30 @@ class CountTest extends TestCase
$count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]);
$this->assertEquals(1, $count, 'There should have been 1 matching document'); $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');
}
} }

View File

@ -56,4 +56,34 @@ class DeleteTest extends TestCase
Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]);
$this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); $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');
}
} }

View File

@ -51,4 +51,30 @@ class ExistsTest extends TestCase
$this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]),
'There should not have been any existing documents'); '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');
}
} }

View File

@ -86,6 +86,40 @@ class FindTest extends TestCase
$this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); $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 public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void
{ {
$doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); $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); $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class);
$this->assertFalse($doc, 'There should not have been a document returned'); $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');
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\PostgreSQL; 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\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument; use Test\Integration\TestDocument;
@ -40,7 +40,9 @@ class PatchTest extends TestCase
#[TestDox('By ID succeeds when no document is updated')] #[TestDox('By ID succeeds when no document is updated')]
public function testByIdSucceedsWhenNoDocumentIsUpdated(): void 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'); $this->assertTrue(true, 'The above not throwing an exception is the test');
} }
@ -53,7 +55,50 @@ class PatchTest extends TestCase
public function testByFieldsSucceedsWhenNoDocumentIsUpdated(): void 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'); $this->assertTrue(true, 'The above not throwing an exception is the test');
} }
} }

View File

@ -71,4 +71,57 @@ class RemoveFieldsTest extends TestCase
RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']);
$this->assertTrue(true, 'The above not throwing an exception is the test'); $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');
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; namespace Test\Integration\SQLite;
use BitBadger\PDODocument\{Count, Field}; use BitBadger\PDODocument\{Count, DocumentException, Field};
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -44,4 +44,17 @@ class CountTest extends TestCase
$count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]);
$this->assertEquals(1, $count, 'There should have been 1 matching document'); $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('', '');
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; 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\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -56,4 +56,17 @@ class DeleteTest extends TestCase
Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]);
$this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); $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('', '');
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; namespace Test\Integration\SQLite;
use BitBadger\PDODocument\{Exists, Field}; use BitBadger\PDODocument\{DocumentException, Exists, Field};
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -51,4 +51,18 @@ class ExistsTest extends TestCase
$this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]),
'There should not have been any existing documents'); '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('', '');
}
}

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; 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\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument; 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'); $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 public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void
{ {
$doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); $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); $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class);
$this->assertFalse($doc, 'There should not have been a document returned'); $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);
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; 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\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument; use Test\Integration\TestDocument;
@ -56,4 +56,17 @@ class PatchTest extends TestCase
Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burgundy')], ['foo' => 'green']); Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burgundy')], ['foo' => 'green']);
$this->assertTrue(true, 'The above not throwing an exception is the test'); $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('', '', []);
}
} }

View File

@ -2,7 +2,7 @@
namespace Test\Integration\SQLite; 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\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument; use Test\Integration\TestDocument;
@ -71,4 +71,17 @@ class RemoveFieldsTest extends TestCase
RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']);
$this->assertTrue(true, 'The above not throwing an exception is the test'); $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('', '', []);
}
} }

View File

@ -37,7 +37,7 @@ class CountTest extends TestCase
public function testByContainsSucceedsForPostgreSQL(): void public function testByContainsSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; 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'); 'SELECT statement not generated correctly');
} }
@ -52,8 +52,8 @@ class CountTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; Configuration::$mode = Mode::PgSQL;
$this->assertEquals("SELECT COUNT(*) FROM a_table WHERE data @? :path::jsonpath", Count::byJsonPath('a_table'), $this->assertEquals('SELECT COUNT(*) FROM a_table WHERE jsonb_path_exists(data, :path::jsonpath)',
'SELECT statement not generated correctly'); Count::byJsonPath('a_table'), 'SELECT statement not generated correctly');
} }
#[TestDox('By JSON Path fails for non PostgreSQL')] #[TestDox('By JSON Path fails for non PostgreSQL')]

View File

@ -53,8 +53,8 @@ class DeleteTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; Configuration::$mode = Mode::PgSQL;
$this->assertEquals('DELETE FROM here WHERE data @? :path::jsonpath', Delete::byJsonPath('here'), $this->assertEquals('DELETE FROM here WHERE jsonb_path_exists(data, :path::jsonpath)',
'DELETE statement not constructed correctly'); Delete::byJsonPath('here'), 'DELETE statement not constructed correctly');
} }
#[TestDox('By JSON Path fails for non PostgreSQL')] #[TestDox('By JSON Path fails for non PostgreSQL')]

View File

@ -60,7 +60,7 @@ class ExistsTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; 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'); Exists::byJsonPath('lint'), 'Existence query not generated correctly');
} }

View File

@ -54,8 +54,8 @@ class FindTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; Configuration::$mode = Mode::PgSQL;
$this->assertEquals('SELECT data FROM light WHERE data @? :path::jsonpath', Find::byJsonPath('light'), $this->assertEquals('SELECT data FROM light WHERE jsonb_path_exists(data, :path::jsonpath)',
'SELECT query not generated correctly'); Find::byJsonPath('light'), 'SELECT query not generated correctly');
} }
#[TestDox('By JSON Path fails for non PostgreSQL')] #[TestDox('By JSON Path fails for non PostgreSQL')]

View File

@ -83,7 +83,7 @@ class PatchTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; 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'); Patch::byJsonPath('that'), 'Patch UPDATE statement is not correct');
} }

View File

@ -112,7 +112,8 @@ class RemoveFieldsTest extends TestCase
public function testByJsonPathSucceedsForPostgreSQL(): void public function testByJsonPathSucceedsForPostgreSQL(): void
{ {
Configuration::$mode = Mode::PgSQL; 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'])), RemoveFields::byJsonPath('dessert', Parameters::fieldNames(':cake', ['b', 'c'])),
'UPDATE statement not correct'); 'UPDATE statement not correct');
} }

View File

@ -96,7 +96,7 @@ class QueryTest extends TestCase
{ {
Configuration::$mode = Mode::PgSQL; Configuration::$mode = Mode::PgSQL;
try { try {
$this->assertEquals('data @? :path::jsonpath', Query::whereJsonPathMatches(), $this->assertEquals('jsonb_path_exists(data, :path::jsonpath)', Query::whereJsonPathMatches(),
'WHERE fragment not constructed correctly'); 'WHERE fragment not constructed correctly');
} finally { } finally {
Configuration::$mode = null; Configuration::$mode = null;
@ -108,7 +108,7 @@ class QueryTest extends TestCase
{ {
Configuration::$mode = Mode::PgSQL; Configuration::$mode = Mode::PgSQL;
try { 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'); 'WHERE fragment not constructed correctly');
} finally { } finally {
Configuration::$mode = null; Configuration::$mode = null;