From 9729c50c00646bfd79bdf33eaa751c4cf3d16e59 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Jun 2024 11:00:08 -0400 Subject: [PATCH 1/5] Add field match enum --- composer.json | 2 +- composer.lock | 4 ++-- src/Count.php | 6 +++--- src/Delete.php | 6 +++--- src/Exists.php | 6 +++--- src/FieldMatch.php | 28 ++++++++++++++++++++++++++++ src/Find.php | 12 ++++++------ src/Patch.php | 6 +++--- src/Query.php | 10 +++++++--- src/Query/Count.php | 9 +++++---- src/Query/Delete.php | 10 ++++++---- src/Query/Exists.php | 10 ++++++---- src/Query/Find.php | 10 ++++++---- src/Query/Patch.php | 8 ++++---- src/Query/RemoveFields.php | 8 ++++---- src/RemoveFields.php | 6 +++--- tests/unit/FieldMatchTest.php | 22 ++++++++++++++++++++++ tests/unit/Query/FindTest.php | 5 +++-- tests/unit/QueryTest.php | 9 +++++---- 19 files changed, 120 insertions(+), 57 deletions(-) create mode 100644 src/FieldMatch.php create mode 100644 tests/unit/FieldMatchTest.php diff --git a/composer.json b/composer.json index 0cd85ed..3c33d3e 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss" }, "require": { - "php": ">=8.3", + "php": ">=8.1", "netresearch/jsonmapper": "^4", "ext-pdo": "*" }, diff --git a/composer.lock b/composer.lock index c9a364a..5cb3710 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ca79f450e8e715ad61ba3581734c0fe7", + "content-hash": "f4563891566be8872ae85552261303bd", "packages": [ { "name": "netresearch/jsonmapper", @@ -1697,7 +1697,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3", + "php": ">=8.1", "ext-pdo": "*" }, "platform-dev": [], diff --git a/src/Count.php b/src/Count.php index df91c72..62b2513 100644 --- a/src/Count.php +++ b/src/Count.php @@ -26,14 +26,14 @@ class Count * * @param string $tableName The name of the table in which documents should be counted * @param array|Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return int The count of documents matching the field comparison * @throws DocumentException If one is encountered */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): int + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): int { $namedFields = Parameters::nameFields($fields); - return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $conjunction), + return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new CountMapper()); } } diff --git a/src/Delete.php b/src/Delete.php index 00a408f..ee4fcb8 100644 --- a/src/Delete.php +++ b/src/Delete.php @@ -24,13 +24,13 @@ class Delete * * @param string $tableName The table from which documents should be deleted * @param array|Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): void + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): void { $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $conjunction), + Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, [])); } } diff --git a/src/Exists.php b/src/Exists.php index 11548d4..bc9e1a5 100644 --- a/src/Exists.php +++ b/src/Exists.php @@ -27,14 +27,14 @@ class Exists * * @param string $tableName The name of the table in which document existence should be determined * @param Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return bool True if any documents match the field comparison, false if not * @throws DocumentException If any is encountered */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): bool + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): bool { $namedFields = Parameters::nameFields($fields); - return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $conjunction), + return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new ExistsMapper()); } } diff --git a/src/FieldMatch.php b/src/FieldMatch.php new file mode 100644 index 0000000..6cb7b6d --- /dev/null +++ b/src/FieldMatch.php @@ -0,0 +1,28 @@ + 'AND', + FieldMatch::Any => 'OR' + }; + } +} diff --git a/src/Find.php b/src/Find.php index a3bb007..aae5b35 100644 --- a/src/Find.php +++ b/src/Find.php @@ -45,15 +45,15 @@ class Find * @param string $tableName The table from which documents should be retrieved * @param array|Field[] $fields The field comparison to match * @param class-string $className The name of the class to be retrieved - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return DocumentList A list of documents matching the given field comparison * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, string $className, - string $conjunction = 'AND'): DocumentList + ?FieldMatch $match = null): DocumentList { $namedFields = Parameters::nameFields($fields); - return Custom::list(Query\Find::byFields($tableName, $namedFields, $conjunction), + return Custom::list(Query\Find::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new DocumentMapper($className)); } @@ -64,15 +64,15 @@ class Find * @param string $tableName The table from which the document should be retrieved * @param array|Field[] $fields The field comparison to match * @param class-string $className The name of the class to be retrieved - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return false|TDoc The first document if any matches are found, false otherwise * @throws DocumentException If any is encountered */ public static function firstByFields(string $tableName, array $fields, string $className, - string $conjunction = 'AND'): mixed + ?FieldMatch $match = null): mixed { $namedFields = Parameters::nameFields($fields); - return Custom::single(Query\Find::byFields($tableName, $namedFields, $conjunction), + return Custom::single(Query\Find::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, []), new DocumentMapper($className)); } } diff --git a/src/Patch.php b/src/Patch.php index 98e5378..75c7983 100644 --- a/src/Patch.php +++ b/src/Patch.php @@ -27,14 +27,14 @@ class Patch * @param string $tableName The table in which documents should be patched * @param array|Field[] $fields The field comparison to match * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, array|object $patch, - string $conjunction = 'AND'): void + ?FieldMatch $match = null): void { $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $conjunction), + Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $match), Parameters::addFields($namedFields, Parameters::json(':data', $patch))); } } diff --git a/src/Query.php b/src/Query.php index 495315a..c6cc83f 100644 --- a/src/Query.php +++ b/src/Query.php @@ -22,12 +22,14 @@ class Query * Create a WHERE clause fragment to implement a comparison on fields in a JSON document * * @param Field[] $fields The field comparison to generate - * @param string $conjunction How to join multiple conditions (optional; defaults to AND) + * @param FieldMatch|null $match How to join multiple conditions (optional; defaults to All) * @return string The WHERE clause fragment matching the given fields and parameter + * @throws DocumentException If the database mode has not been set */ - public static function whereByFields(array $fields, string $conjunction = 'AND'): string + public static function whereByFields(array $fields, ?FieldMatch $match = null): string { - return implode(" $conjunction ", array_map(fn($it) => $it->toWhere(), $fields)); + return implode(' ' . ($match ?? FieldMatch::All)->toString() . ' ', + array_map(fn($it) => $it->toWhere(), $fields)); } /** @@ -35,6 +37,7 @@ class Query * * @param string $paramName The parameter name where the value of the ID will be provided (optional; default @id) * @return string The WHERE clause fragment to match by ID + * @throws DocumentException If the database mode has not been set */ public static function whereById(string $paramName = ':id'): string { @@ -69,6 +72,7 @@ class Query * * @param string $tableName The name of the table in which the document should be updated * @return string The UPDATE query for the document + * @throws DocumentException If the database mode has not been set */ public static function update(string $tableName): string { diff --git a/src/Query/Count.php b/src/Query/Count.php index 87c47e3..8727e96 100644 --- a/src/Query/Count.php +++ b/src/Query/Count.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Field, Query}; +use BitBadger\PDODocument\{DocumentException, Field, FieldMatch, Query}; /** * Queries for counting documents @@ -25,11 +25,12 @@ class Count * * @param string $tableName The name of the table in which documents should be counted * @param Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to join multiple conditions (optional; defaults to All) * @return string The query to count documents using a field comparison + * @throws DocumentException If the database mode has not been set */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): string + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): string { - return self::all($tableName) . ' WHERE ' . Query::whereByFields($fields, $conjunction); + return self::all($tableName) . ' WHERE ' . Query::whereByFields($fields, $match); } } diff --git a/src/Query/Delete.php b/src/Query/Delete.php index 84bd2a0..a8303ab 100644 --- a/src/Query/Delete.php +++ b/src/Query/Delete.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Field, Query}; +use BitBadger\PDODocument\{DocumentException, Field, FieldMatch, Query}; /** * Queries to delete documents @@ -14,6 +14,7 @@ class Delete * * @param string $tableName The name of the table from which a document should be deleted * @return string The DELETE statement to delete a document by its ID + * @throws DocumentException If the database mode has not been set */ public static function byId(string $tableName): string { @@ -25,11 +26,12 @@ class Delete * * @param string $tableName The name of the table from which documents should be deleted * @param Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The DELETE statement to delete documents via field comparison + * @throws DocumentException If the database mode has not been set */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): string + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): string { - return "DELETE FROM $tableName WHERE " . Query::whereByFields($fields, $conjunction); + return "DELETE FROM $tableName WHERE " . Query::whereByFields($fields, $match); } } diff --git a/src/Query/Exists.php b/src/Query/Exists.php index a7750b1..6ff86f4 100644 --- a/src/Query/Exists.php +++ b/src/Query/Exists.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Field, Query}; +use BitBadger\PDODocument\{DocumentException, Field, FieldMatch, Query}; /** * Queries to determine document existence @@ -26,6 +26,7 @@ class Exists * * @param string $tableName The name of the table in which document existence should be checked * @return string The query to determine document existence by ID + * @throws DocumentException If the database mode has not been set */ public static function byId(string $tableName): string { @@ -37,11 +38,12 @@ class Exists * * @param string $tableName The name of the table in which document existence should be checked * @param Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The query to determine document existence by field comparison + * @throws DocumentException If the database mode has not been set */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): string + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): string { - return self::query($tableName, Query::whereByFields($fields, $conjunction)); + return self::query($tableName, Query::whereByFields($fields, $match)); } } diff --git a/src/Query/Find.php b/src/Query/Find.php index 2e65385..824e8f3 100644 --- a/src/Query/Find.php +++ b/src/Query/Find.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Field, Query}; +use BitBadger\PDODocument\{DocumentException, Field, FieldMatch, Query}; /** * Queries for retrieving documents @@ -14,6 +14,7 @@ class Find * * @param string $tableName The name of the table from which a document should be retrieved * @return string The SELECT statement to retrieve a document by its ID + * @throws DocumentException If the database mode has not been set */ public static function byId(string $tableName): string { @@ -25,11 +26,12 @@ class Find * * @param string $tableName The name of the table from which documents should be retrieved * @param Field[] $fields The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The SELECT statement to retrieve documents by field comparison + * @throws DocumentException If the database mode has not been set */ - public static function byFields(string $tableName, array $fields, string $conjunction = 'AND'): string + public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): string { - return Query::selectFromTable($tableName) . ' WHERE ' . Query::whereByFields($fields, $conjunction); + return Query::selectFromTable($tableName) . ' WHERE ' . Query::whereByFields($fields, $match); } } diff --git a/src/Query/Patch.php b/src/Query/Patch.php index 6b1ab76..50a696b 100644 --- a/src/Query/Patch.php +++ b/src/Query/Patch.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Configuration, DocumentException, Field, Mode, Query}; +use BitBadger\PDODocument\{Configuration, DocumentException, Field, FieldMatch, Mode, Query}; /** * Queries to perform partial updates on documents @@ -44,12 +44,12 @@ class Patch * * @param string $tableName The name of the table in which documents should be patched * @param array|Field[] $field The field comparison to match - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The query to patch documents via field comparison * @throws DocumentException If the database mode has not been set */ - public static function byFields(string $tableName, array $field, string $conjunction = 'AND'): string + public static function byFields(string $tableName, array $field, ?FieldMatch $match = null): string { - return self::update($tableName, Query::whereByFields($field, $conjunction)); + return self::update($tableName, Query::whereByFields($field, $match)); } } diff --git a/src/Query/RemoveFields.php b/src/Query/RemoveFields.php index 0e3fea0..c668abf 100644 --- a/src/Query/RemoveFields.php +++ b/src/Query/RemoveFields.php @@ -2,7 +2,7 @@ namespace BitBadger\PDODocument\Query; -use BitBadger\PDODocument\{Configuration, DocumentException, Field, Mode, Query}; +use BitBadger\PDODocument\{Configuration, DocumentException, Field, FieldMatch, Mode, Query}; /** * Queries to remove fields from documents @@ -54,13 +54,13 @@ class RemoveFields * @param string $tableName The name of the table in which documents should be manipulated * @param array|Field[] $fields The field comparison to match * @param array $parameters The parameter list for the query - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The UPDATE statement to remove fields from documents via field comparison * @throws DocumentException If the database mode has not been set */ public static function byFields(string $tableName, array $fields, array $parameters, - string $conjunction = 'AND'): string + ?FieldMatch $match = null): string { - return self::update($tableName, $parameters, Query::whereByFields($fields, $conjunction)); + return self::update($tableName, $parameters, Query::whereByFields($fields, $match)); } } diff --git a/src/RemoveFields.php b/src/RemoveFields.php index b32e2f4..3933e7d 100644 --- a/src/RemoveFields.php +++ b/src/RemoveFields.php @@ -28,15 +28,15 @@ class RemoveFields * @param string $tableName The table in which documents should have fields removed * @param array|Field[] $fields The field comparison to match * @param array|string[] $fieldNames The names of the fields to be removed - * @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`) + * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, array $fieldNames, - string $conjunction = 'AND'): void + ?FieldMatch $match = null): void { $nameParams = Parameters::fieldNames(':name', $fieldNames); $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $conjunction), + Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $match), Parameters::addFields($namedFields, $nameParams)); } } diff --git a/tests/unit/FieldMatchTest.php b/tests/unit/FieldMatchTest.php new file mode 100644 index 0000000..9fd8942 --- /dev/null +++ b/tests/unit/FieldMatchTest.php @@ -0,0 +1,22 @@ +assertEquals('AND', FieldMatch::All->toString(), 'All should have returned AND'); + } + + public function testToStringSucceedsForAny(): void + { + $this->assertEquals('OR', FieldMatch::Any->toString(), 'Any should have returned OR'); + } +} diff --git a/tests/unit/Query/FindTest.php b/tests/unit/Query/FindTest.php index 5b2fb9a..10e7d73 100644 --- a/tests/unit/Query/FindTest.php +++ b/tests/unit/Query/FindTest.php @@ -2,7 +2,7 @@ namespace Test\Unit\Query; -use BitBadger\PDODocument\{Configuration, Field, Mode}; +use BitBadger\PDODocument\{Configuration, Field, FieldMatch, Mode}; use BitBadger\PDODocument\Query\Find; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -32,7 +32,8 @@ class FindTest extends TestCase public function testByFieldsSucceeds(): void { $this->assertEquals("SELECT data FROM there WHERE data->>'active' = :act OR data->>'locked' = :lock", - Find::byFields('there', [Field::EQ('active', true, ':act'), Field::EQ('locked', true, ':lock')], 'OR'), + Find::byFields('there', [Field::EQ('active', true, ':act'), Field::EQ('locked', true, ':lock')], + FieldMatch::Any), 'SELECT query not generated correctly'); } } diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index b3ac80b..d92d4b7 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -2,7 +2,7 @@ namespace Test\Unit; -use BitBadger\PDODocument\{Configuration, Field, Mode, Query}; +use BitBadger\PDODocument\{Configuration, Field, FieldMatch, Mode, Query}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -33,17 +33,18 @@ class QueryTest extends TestCase Query::whereByFields([Field::LE('test_field', '', ':it')]), 'WHERE fragment not constructed correctly'); } - public function testWhereByFieldsSucceedsForMultipleFields(): void + public function testWhereByFieldsSucceedsForMultipleFieldsAll(): void { $this->assertEquals("data->>'test_field' <= :it AND data->>'other_field' = :other", Query::whereByFields([Field::LE('test_field', '', ':it'), Field::EQ('other_field', '', ':other')]), 'WHERE fragment not constructed correctly'); } - public function testWhereByFieldsSucceedsForMultipleFieldsWithOr(): void + public function testWhereByFieldsSucceedsForMultipleFieldsAny(): void { $this->assertEquals("data->>'test_field' <= :it OR data->>'other_field' = :other", - Query::whereByFields([Field::LE('test_field', '', ':it'), Field::EQ('other_field', '', ':other')], 'OR'), + Query::whereByFields([Field::LE('test_field', '', ':it'), Field::EQ('other_field', '', ':other')], + FieldMatch::Any), 'WHERE fragment not constructed correctly'); } -- 2.45.1 From c892689eb65e09048579754ca542bdef72afea22 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Jun 2024 21:12:21 -0400 Subject: [PATCH 2/5] Add auto ID enum, modify insert queries --- composer.json | 2 +- composer.lock | 4 +- src/AutoId.php | 51 ++++++++++++ src/Configuration.php | 12 ++- src/Document.php | 2 + src/Query.php | 38 +++++++-- tests/unit/ConfigurationTest.php | 14 +++- tests/unit/QueryTest.php | 135 ++++++++++++++++++++++++++++++- 8 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 src/AutoId.php diff --git a/composer.json b/composer.json index 3c33d3e..7adb8c7 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "netresearch/jsonmapper": "^4", "ext-pdo": "*" }, diff --git a/composer.lock b/composer.lock index 5cb3710..6d32386 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4563891566be8872ae85552261303bd", + "content-hash": "20bf0d96304e429b431535d05ff4585a", "packages": [ { "name": "netresearch/jsonmapper", @@ -1697,7 +1697,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1", + "php": ">=8.2", "ext-pdo": "*" }, "platform-dev": [], diff --git a/src/AutoId.php b/src/AutoId.php new file mode 100644 index 0000000..222ca69 --- /dev/null +++ b/src/AutoId.php @@ -0,0 +1,51 @@ + match (Configuration::$autoId) { + AutoId::None => ':data', + AutoId::Number => "json_set(:data, '$.$id', " + . "(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))", + AutoId::UUID => "json_set(:data, '$.$id', '" . AutoId::generateUUID() . "')", + AutoId::RandomString => "json_set(:data, '$.$id', '" . AutoId::generateRandom() ."')" + }, + Mode::PgSQL => match (Configuration::$autoId) { + AutoId::None => ':data', + AutoId::Number => ":data || ('{\"$id\":' || " + . "(SELECT COALESCE(MAX(data->>'$id'), 0) + 1 FROM $tableName) || '}')", + AutoId::UUID => ":data || '{\"$id\":\"" . AutoId::generateUUID() . "\"}'", + AutoId::RandomString => ":data || '{\"$id\":\"" . AutoId::generateRandom() . "\"}'", + }, + default => + throw new DocumentException('Database mode not set; cannot generate auto-ID INSERT statement'), + }; + return "INSERT INTO $tableName VALUES ($values)"; + } catch (RandomException $ex) { + throw new DocumentException('Unable to generate ID: ' . $ex->getMessage(), previous: $ex); + } } /** @@ -63,8 +89,8 @@ class Query */ public static function save(string $tableName): string { - return self::insert($tableName) - . " ON CONFLICT ((data->>'" . Configuration::$idField . "')) DO UPDATE SET data = EXCLUDED.data"; + $id = Configuration::$idField; + return "INSERT INTO $tableName VALUES (:data) ON CONFLICT ((data->>'$id')) DO UPDATE SET data = EXCLUDED.data"; } /** diff --git a/tests/unit/ConfigurationTest.php b/tests/unit/ConfigurationTest.php index 1416ceb..50dd909 100644 --- a/tests/unit/ConfigurationTest.php +++ b/tests/unit/ConfigurationTest.php @@ -2,7 +2,7 @@ namespace Test\Unit; -use BitBadger\PDODocument\{Configuration, DocumentException}; +use BitBadger\PDODocument\{AutoId, Configuration, DocumentException}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -29,6 +29,18 @@ class ConfigurationTest extends TestCase } } + #[TestDox('Auto ID default succeeds')] + public function testAutoIdDefaultSucceeds(): void + { + $this->assertEquals(AutoId::None, Configuration::$autoId, 'Auto ID should default to None'); + } + + #[TestDox('ID string length default succeeds')] + public function testIdStringLengthDefaultSucceeds(): void + { + $this->assertEquals(16, Configuration::$idStringLength, 'ID string length should default to 16'); + } + #[TestDox("Db conn fails when no DSN specified")] public function testDbConnFailsWhenNoDSNSpecified(): void { diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index d92d4b7..4f985d5 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -2,7 +2,7 @@ namespace Test\Unit; -use BitBadger\PDODocument\{Configuration, Field, FieldMatch, Mode, Query}; +use BitBadger\PDODocument\{AutoId, Configuration, DocumentException, Field, FieldMatch, Mode, Query}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -60,10 +60,137 @@ class QueryTest extends TestCase $this->assertEquals("data->>'id' = :di", Query::whereById(':di'), 'WHERE fragment not constructed correctly'); } - public function testInsertSucceeds(): void + #[TestDox('Insert succeeds with no auto-ID for PostgreSQL')] + public function testInsertSucceedsWithNoAutoIdForPostgreSQL(): void { - $this->assertEquals('INSERT INTO my_table VALUES (:data)', Query::insert('my_table'), - 'INSERT statement not constructed correctly'); + Configuration::$mode = Mode::PgSQL; + try { + $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), + 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('Insert succeeds with no auto-ID for SQLite')] + public function testInsertSucceedsWithNoAutoIdForSQLite(): void + { + Configuration::$mode = Mode::SQLite; + try { + $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), + 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('Insert succeeds with auto numeric ID for PostgreSQL')] + public function testInsertSucceedsWithAutoNumericIdForPostgreSQL(): void + { + Configuration::$mode = Mode::PgSQL; + Configuration::$autoId = AutoId::Number; + try { + $this->assertEquals( + "INSERT INTO test_tbl VALUES (:data || ('{\"id\":' " + . "|| (SELECT COALESCE(MAX(data->>'id'), 0) + 1 FROM test_tbl) || '}'))", + Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds with auto numeric ID for SQLite')] + public function testInsertSucceedsWithAutoNumericIdForSQLite(): void + { + Configuration::$mode = Mode::SQLite; + Configuration::$autoId = AutoId::Number; + try { + $this->assertEquals( + "INSERT INTO test_tbl VALUES (json_set(:data, '$.id', " + . "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM test_tbl)))", + Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds with auto UUID for PostgreSQL')] + public function testInsertSucceedsWithAutoUuidForPostgreSQL(): void + { + Configuration::$mode = Mode::PgSQL; + Configuration::$autoId = AutoId::UUID; + try { + $query = Query::insert('test_tbl'); + $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data || '{\"id\":\"", $query, + 'INSERT statement not constructed correctly'); + $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds with auto UUID for SQLite')] + public function testInsertSucceedsWithAutoUuidForSQLite(): void + { + Configuration::$mode = Mode::SQLite; + Configuration::$autoId = AutoId::UUID; + try { + $query = Query::insert('test_tbl'); + $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, + 'INSERT statement not constructed correctly'); + $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds with auto random string for PostgreSQL')] + public function testInsertSucceedsWithAutoRandomStringForPostgreSQL(): void + { + Configuration::$mode = Mode::PgSQL; + Configuration::$autoId = AutoId::RandomString; + Configuration::$idStringLength = 8; + try { + $query = Query::insert('test_tbl'); + $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data || '{\"id\":\"", $query, + 'INSERT statement not constructed correctly'); + $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); + $id = str_replace(["INSERT INTO test_tbl VALUES (:data || '{\"id\":\"", "\"}')"], '', $query); + $this->assertEquals(8, strlen($id), "Generated ID [$id] should have been 8 characters long"); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + Configuration::$idStringLength = 16; + } + } + + #[TestDox('Insert succeeds with auto random string for SQLite')] + public function testInsertSucceedsWithAutoRandomStringForSQLite(): void + { + Configuration::$mode = Mode::SQLite; + Configuration::$autoId = AutoId::RandomString; + try { + $query = Query::insert('test_tbl'); + $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, + 'INSERT statement not constructed correctly'); + $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); + $id = str_replace(["INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", "'))"], '', $query); + $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); + } finally { + Configuration::$mode = null; + Configuration::$autoId = AutoId::None; + } + } + + public function testInsertFailsWhenModeNotSet(): void + { + $this->expectException(DocumentException::class); + Configuration::$mode = null; + Query::insert('kaboom'); } public function testSaveSucceeds(): void -- 2.45.1 From 9e617e7e23dc26590f65b636d1149f7c7c47e1c2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Jun 2024 21:40:47 -0400 Subject: [PATCH 3/5] First cut of ID generation need detection --- src/Document.php | 22 +++++++++++++++++++--- src/Query.php | 7 ++++--- tests/unit/QueryTest.php | 40 ++++++++++++++-------------------------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Document.php b/src/Document.php index b765f52..ad645d4 100644 --- a/src/Document.php +++ b/src/Document.php @@ -2,8 +2,6 @@ namespace BitBadger\PDODocument; -use BitBadger\PDODocument\Query\Insert; - /** * Functions that apply at a whole document level */ @@ -18,7 +16,25 @@ class Document */ public static function insert(string $tableName, array|object $document): void { - Custom::nonQuery(Query::insert($tableName), Parameters::json(':data', $document)); + $doInsert = fn() => Custom::nonQuery(Query::insert($tableName), Parameters::json(':data', $document)); + + if (Configuration::$autoId == AutoId::None) { + $doInsert(); + return; + } + + $id = Configuration::$idField; + $idProvided = + (is_array( $document) && is_int( $document[$id]) && $document[$id] <> 0) + || (is_array( $document) && is_string($document[$id]) && $document[$id] <> '') + || (is_object($document) && is_int( $document->{$id}) && $document->{$id} <> 0) + || (is_object($document) && is_string($document->{$id}) && $document->{$id} <> ''); + + if ($idProvided) { + $doInsert(); + } else { + Custom::nonQuery(Query::insert($tableName, Configuration::$autoId), Parameters::json(':data', $document)); + } } /** diff --git a/src/Query.php b/src/Query.php index fcdab00..66f10c1 100644 --- a/src/Query.php +++ b/src/Query.php @@ -50,22 +50,23 @@ class Query * Create an `INSERT` statement for a document * * @param string $tableName The name of the table into which the document will be inserted + * @param AutoId|null $autoId The version of automatic ID query to generate (optional, defaults to None) * @return string The `INSERT` statement to insert a document * @throws DocumentException If the database mode is not set */ - public static function insert(string $tableName): string + public static function insert(string $tableName, ?AutoId $autoId = null): string { try { $id = Configuration::$idField; $values = match (Configuration::$mode) { - Mode::SQLite => match (Configuration::$autoId) { + Mode::SQLite => match ($autoId ?? AutoId::None) { AutoId::None => ':data', AutoId::Number => "json_set(:data, '$.$id', " . "(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))", AutoId::UUID => "json_set(:data, '$.$id', '" . AutoId::generateUUID() . "')", AutoId::RandomString => "json_set(:data, '$.$id', '" . AutoId::generateRandom() ."')" }, - Mode::PgSQL => match (Configuration::$autoId) { + Mode::PgSQL => match ($autoId ?? AutoId::None) { AutoId::None => ':data', AutoId::Number => ":data || ('{\"$id\":' || " . "(SELECT COALESCE(MAX(data->>'$id'), 0) + 1 FROM $tableName) || '}')", diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 4f985d5..b9e38d4 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -87,64 +87,56 @@ class QueryTest extends TestCase #[TestDox('Insert succeeds with auto numeric ID for PostgreSQL')] public function testInsertSucceedsWithAutoNumericIdForPostgreSQL(): void { - Configuration::$mode = Mode::PgSQL; - Configuration::$autoId = AutoId::Number; + Configuration::$mode = Mode::PgSQL; try { $this->assertEquals( "INSERT INTO test_tbl VALUES (:data || ('{\"id\":' " . "|| (SELECT COALESCE(MAX(data->>'id'), 0) + 1 FROM test_tbl) || '}'))", - Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); + Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } finally { - Configuration::$mode = null; - Configuration::$autoId = AutoId::None; + Configuration::$mode = null; } } #[TestDox('Insert succeeds with auto numeric ID for SQLite')] public function testInsertSucceedsWithAutoNumericIdForSQLite(): void { - Configuration::$mode = Mode::SQLite; - Configuration::$autoId = AutoId::Number; + Configuration::$mode = Mode::SQLite; try { $this->assertEquals( "INSERT INTO test_tbl VALUES (json_set(:data, '$.id', " . "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM test_tbl)))", - Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); + Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } finally { - Configuration::$mode = null; - Configuration::$autoId = AutoId::None; + Configuration::$mode = null; } } #[TestDox('Insert succeeds with auto UUID for PostgreSQL')] public function testInsertSucceedsWithAutoUuidForPostgreSQL(): void { - Configuration::$mode = Mode::PgSQL; - Configuration::$autoId = AutoId::UUID; + Configuration::$mode = Mode::PgSQL; try { - $query = Query::insert('test_tbl'); + $query = Query::insert('test_tbl', AutoId::UUID); $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data || '{\"id\":\"", $query, 'INSERT statement not constructed correctly'); $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); } finally { - Configuration::$mode = null; - Configuration::$autoId = AutoId::None; + Configuration::$mode = null; } } #[TestDox('Insert succeeds with auto UUID for SQLite')] public function testInsertSucceedsWithAutoUuidForSQLite(): void { - Configuration::$mode = Mode::SQLite; - Configuration::$autoId = AutoId::UUID; + Configuration::$mode = Mode::SQLite; try { - $query = Query::insert('test_tbl'); + $query = Query::insert('test_tbl', AutoId::UUID); $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, 'INSERT statement not constructed correctly'); $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); } finally { - Configuration::$mode = null; - Configuration::$autoId = AutoId::None; + Configuration::$mode = null; } } @@ -152,10 +144,9 @@ class QueryTest extends TestCase public function testInsertSucceedsWithAutoRandomStringForPostgreSQL(): void { Configuration::$mode = Mode::PgSQL; - Configuration::$autoId = AutoId::RandomString; Configuration::$idStringLength = 8; try { - $query = Query::insert('test_tbl'); + $query = Query::insert('test_tbl', AutoId::RandomString); $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data || '{\"id\":\"", $query, 'INSERT statement not constructed correctly'); $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); @@ -163,7 +154,6 @@ class QueryTest extends TestCase $this->assertEquals(8, strlen($id), "Generated ID [$id] should have been 8 characters long"); } finally { Configuration::$mode = null; - Configuration::$autoId = AutoId::None; Configuration::$idStringLength = 16; } } @@ -172,9 +162,8 @@ class QueryTest extends TestCase public function testInsertSucceedsWithAutoRandomStringForSQLite(): void { Configuration::$mode = Mode::SQLite; - Configuration::$autoId = AutoId::RandomString; try { - $query = Query::insert('test_tbl'); + $query = Query::insert('test_tbl', AutoId::RandomString); $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, 'INSERT statement not constructed correctly'); $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); @@ -182,7 +171,6 @@ class QueryTest extends TestCase $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); } finally { Configuration::$mode = null; - Configuration::$autoId = AutoId::None; } } -- 2.45.1 From 2f1db190d7516ee07521edcca767cb5edafa7e52 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Jun 2024 23:06:53 -0400 Subject: [PATCH 4/5] Add tests for autogenerated IDs --- tests/integration/NumDocument.php | 11 ++ tests/integration/sqlite/DocumentTest.php | 224 +++++++++++++++++++++- 2 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 tests/integration/NumDocument.php diff --git a/tests/integration/NumDocument.php b/tests/integration/NumDocument.php new file mode 100644 index 0000000..664f3bb --- /dev/null +++ b/tests/integration/NumDocument.php @@ -0,0 +1,11 @@ + 'turkey', 'sub' => ['foo' => 'gobble', 'bar' => 'gobble']]); + $doc = Find::byId(ThrowawayDb::TABLE, 'turkey', TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document inserted'); + $this->assertEquals('turkey', $doc->id, 'The ID was incorrect'); + $this->assertEquals('', $doc->value, 'The value was incorrect'); + $this->assertEquals(0, $doc->num_value, 'The numeric value was incorrect'); + $this->assertNotNull($doc->sub, 'The sub-document should not have been null'); + $this->assertEquals('gobble', $doc->sub->foo, 'The sub-document foo property was incorrect'); + $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); + } + + #[TestDox('Insert succeeds for array with auto number ID not provided')] + public function testInsertSucceedsForArrayWithAutoNumberIdNotProvided(): void + { + Configuration::$autoId = AutoId::Number; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + + Document::insert(ThrowawayDb::TABLE, ['id' => 0, 'value' => 'new', 'num_value' => 8]); + $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE, [], new ArrayMapper()); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $obj = json_decode($doc['data']); + $this->assertEquals(1, $obj->id, 'The ID 1 should have been auto-generated'); + + Document::insert(ThrowawayDb::TABLE, ['id' => 0, 'value' => 'again', 'num_value' => 7]); + $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = 2", [], + new ArrayMapper()); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $obj = json_decode($doc['data']); + $this->assertEquals(2, $obj->id, 'The ID 2 should have been auto-generated'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for array with auto number ID with ID provided')] + public function testInsertSucceedsForArrayWithAutoNumberIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::Number; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, ['id' => 7, 'value' => 'new', 'num_value' => 8]); + $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE, [], new ArrayMapper()); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $obj = json_decode($doc['data']); + $this->assertEquals(7, $obj->id, 'The ID 7 should have been stored'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for array with auto UUID ID not provided')] + public function testInsertSucceedsForArrayWithAutoUuidIdNotProvided(): void + { + Configuration::$autoId = AutoId::UUID; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, ['id' => '', 'num_value' => 5]); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 5)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertNotEmpty($doc->id, 'The ID should have been auto-generated'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for array with auto UUID ID with ID provided')] + public function testInsertSucceedsForArrayWithAutoUuidIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::UUID; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + $uuid = AutoId::generateUUID(); + Document::insert(ThrowawayDb::TABLE, ['id' => $uuid, 'value' => 'uuid', 'num_value' => 12]); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 12)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals($uuid, $doc->id, 'The ID should not have been changed'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for array with auto string ID not provided')] + public function testInsertSucceedsForArrayWithAutoStringIdNotProvided(): void + { + Configuration::$autoId = AutoId::RandomString; + Configuration::$idStringLength = 6; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, ['id' => '', 'value' => 'new', 'num_value' => 8]); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 8)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals(6, strlen($doc->id), 'The ID should have been auto-generated and had 6 characters'); + } finally { + Configuration::$autoId = AutoId::None; + Configuration::$idStringLength = 16; + } + } + + #[TestDox('Insert succeeds for array with auto string ID with ID provided')] + public function testInsertSucceedsForArrayWithAutoStringIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::RandomString; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, ['id' => 'my-key', 'value' => 'old', 'num_value' => 3]); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals('my-key', $doc->id, 'The ID should not have been changed'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for object no auto ID')] + public function testInsertSucceedsForObjectNoAutoId(): void { Document::insert(ThrowawayDb::TABLE, new TestDocument('turkey', sub: new SubDocument('gobble', 'gobble'))); $doc = Find::byId(ThrowawayDb::TABLE, 'turkey', TestDocument::class); @@ -41,6 +160,105 @@ class DocumentTest extends TestCase $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); } + #[TestDox('Insert succeeds for object with auto number ID not provided')] + public function testInsertSucceedsForObjectWithAutoNumberIdNotProvided(): void + { + Configuration::$autoId = AutoId::Number; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + + Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'taco')); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'taco')], NumDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals(1, $doc->id, 'The ID 1 should have been auto-generated'); + + Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'burrito')); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burrito')], NumDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals(2, $doc->id, 'The ID 2 should have been auto-generated'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for object with auto number ID with ID provided')] + public function testInsertSucceedsForObjectWithAutoNumberIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::Number; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, new NumDocument(64, 'large')); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'large')], NumDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals(64, $doc->id, 'The ID 64 should have been stored'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for object with auto UUID ID not provided')] + public function testInsertSucceedsForObjectWithAutoUuidIdNotProvided(): void + { + Configuration::$autoId = AutoId::UUID; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, new TestDocument(value: 'something', num_value: 9)); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EX('value')], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertNotEmpty($doc->id, 'The ID should have been auto-generated'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for object with auto UUID ID with ID provided')] + public function testInsertSucceedsForObjectWithAutoUuidIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::UUID; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + $uuid = AutoId::generateUUID(); + Document::insert(ThrowawayDb::TABLE, new TestDocument($uuid, num_value: 14)); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 14)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals($uuid, $doc->id, 'The ID should not have been changed'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + + #[TestDox('Insert succeeds for object with auto string ID not provided')] + public function testInsertSucceedsForObjectWithAutoStringIdNotProvided(): void + { + Configuration::$autoId = AutoId::RandomString; + Configuration::$idStringLength = 40; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, new TestDocument(num_value: 55)); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 55)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals(40, strlen($doc->id), 'The ID should have been auto-generated and had 40 characters'); + } finally { + Configuration::$autoId = AutoId::None; + Configuration::$idStringLength = 16; + } + } + + #[TestDox('Insert succeeds for object with auto string ID with ID provided')] + public function testInsertSucceedsForObjectWithAutoStringIdWithIdProvided(): void + { + Configuration::$autoId = AutoId::RandomString; + try { + Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); + Document::insert(ThrowawayDb::TABLE, new TestDocument('my-key', num_value: 3)); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $this->assertNotFalse($doc, 'There should have been a document returned'); + $this->assertEquals('my-key', $doc->id, 'The ID should not have been changed'); + } finally { + Configuration::$autoId = AutoId::None; + } + } + public function testInsertFailsForDuplicateKey(): void { $this->expectException(DocumentException::class); -- 2.45.1 From d6d70fe406a21b600a0fa0ca403313dd8c8a746d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 11 Jun 2024 07:03:50 -0400 Subject: [PATCH 5/5] Add config details to README --- README.md | 19 +++++++++++++++++++ tests/unit/QueryTest.php | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7da0a6f..2692701 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,25 @@ This library allows SQLite (and, by v1.0.0-beta1, PostgreSQL) to be treated as a `composer require bit-badger/pdo-document` +## Configuration + +### Connection Details + +The static variable `Configuration::$pdoDSN` must be set to the [PDO data source name](https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters) for your database. `Configuration` also has `$username`, `$password`, and `$options` variables that will be used to construct the PDO object it will use for data access. + +### Document Identifiers + +Each document must have a unique identifier. By default, the library assumes that this is a property or array key named `id`, but this can be controlled by setting `Configuration::$idField`. Once documents exist, this should not be changed. + +IDs can be generated automatically on insert. The `AutoId` enumeration has 4 values: + +- `AutoId::None` is the default; no IDs will be generated +- `AutoId::Number` will assign max-ID-plus-one to documents with an ID of 0 +- `AutoId::UUID` will generate a v4 UUID for documents with an empty `string` ID +- `AutoId::RandomString` will generate a string of letters and numbers for documents with an empty `string` ID; `Configuration::$idStringLength` controls the length of the generated string, and defaults to 16 characters + +In all generated scenarios, if the ID value is not 0 or blank, that ID will be used instead of a generated one. + ## Usage Documentation for this library is not complete; however, its structure is very similar to the .NET version, so [its documentation will help](https://bitbadger.solutions/open-source/relational-documents/basic-usage.html) until its project specific documentation is developed. Things like `Count.All()` become `Count::all`, and all the `byField` operations are named `byFields` and take an array of fields. diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index b9e38d4..a816157 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -161,7 +161,7 @@ class QueryTest extends TestCase #[TestDox('Insert succeeds with auto random string for SQLite')] public function testInsertSucceedsWithAutoRandomStringForSQLite(): void { - Configuration::$mode = Mode::SQLite; + Configuration::$mode = Mode::SQLite; try { $query = Query::insert('test_tbl', AutoId::RandomString); $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, @@ -170,7 +170,7 @@ class QueryTest extends TestCase $id = str_replace(["INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", "'))"], '', $query); $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); } finally { - Configuration::$mode = null; + Configuration::$mode = null; } } -- 2.45.1