diff --git a/src/Configuration.php b/src/Configuration.php index 5d44172..e214aa8 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -24,6 +24,9 @@ class Configuration /** @var array|null Options to use for connections (driver-specific) */ public static ?array $options = null; + /** @var Mode|null The mode in which the library is operating (filled after first connection if not configured) */ + public static ?Mode $mode = null; + /** * Retrieve a new connection to the database * @@ -38,8 +41,15 @@ class Configuration $db = new PDO(self::$pdoDSN, $_ENV['PDO_DOC_USERNAME'] ?? self::$username, $_ENV['PDO_DOC_PASSWORD'] ?? self::$password, self::$options); - // TODO: determine driver, set mode for other queries - echo $db->getAttribute(PDO::ATTR_DRIVER_NAME); + if (is_null(self::$mode)) { + $driver = $db->getAttribute(PDO::ATTR_DRIVER_NAME); + self::$mode = match ($driver) { + 'pgsql' => Mode::PgSQL, + 'sqlite' => Mode::SQLite, + default => throw new DocumentException( + "Unsupported driver $driver: this library currently supports PostgreSQL and SQLite") + }; + } return $db; } } diff --git a/src/Mode.php b/src/Mode.php new file mode 100644 index 0000000..f6e21c7 --- /dev/null +++ b/src/Mode.php @@ -0,0 +1,15 @@ + 'JSONB', + Mode::SQLite => 'TEXT', + default => throw new DocumentException('Database mode not set; cannot make create table statement') + }; return "CREATE TABLE IF NOT EXISTS $name (data $dataType NOT NULL)"; } diff --git a/src/Query/Patch.php b/src/Query/Patch.php new file mode 100644 index 0000000..5811647 --- /dev/null +++ b/src/Query/Patch.php @@ -0,0 +1,59 @@ + 'data || @data', + Mode::SQLite => 'json_patch(data, json(@data))', + default => throw new DocumentException('Database mode not set; cannot make patch statement') + }; + return "UPDATE $tableName SET data = $setValue WHERE $whereClause"; + } + + /** + * Query to patch (partially update) a document by its ID + * + * @param string $tableName The name of the table in which a document should be patched + * @return string The query to patch a document by its ID + * @throws DocumentException If the database mode has not been set + */ + public static function byId(string $tableName): string + { + return self::update($tableName, Query::whereById()); + } + + /** + * Query to patch (partially update) a document via a comparison on a JSON field + * + * @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`) + * @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 + { + return self::update($tableName, Query::whereByFields($field, $conjunction)); + } +} diff --git a/tests/unit/Query/DefinitionTest.php b/tests/unit/Query/DefinitionTest.php index 680b2e1..dbdcb02 100644 --- a/tests/unit/Query/DefinitionTest.php +++ b/tests/unit/Query/DefinitionTest.php @@ -2,7 +2,11 @@ namespace Test\Unit\Query; +use BitBadger\PDODocument\Configuration; +use BitBadger\PDODocument\DocumentException; +use BitBadger\PDODocument\Mode; use BitBadger\PDODocument\Query\Definition; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -10,10 +14,35 @@ use PHPUnit\Framework\TestCase; */ class DefinitionTest extends TestCase { - public function testEnsureTableForSucceeds(): void + #[TestDox('Ensure table succeeds for PosgtreSQL')] + public function testEnsureTableSucceedsForPostgreSQL(): void { - $this->assertEquals('CREATE TABLE IF NOT EXISTS documents (data JSON NOT NULL)', - Definition::ensureTableFor('documents', 'JSON'), 'CREATE TABLE statement not generated correctly'); + try { + Configuration::$mode = Mode::PgSQL; + $this->assertEquals('CREATE TABLE IF NOT EXISTS documents (data JSONB NOT NULL)', + Definition::ensureTable('documents'), 'CREATE TABLE statement not generated correctly'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('Ensure table succeeds for SQLite')] + public function testEnsureTableSucceedsForSQLite(): void + { + try { + Configuration::$mode = Mode::SQLite; + $this->assertEquals('CREATE TABLE IF NOT EXISTS dox (data TEXT NOT NULL)', Definition::ensureTable('dox'), + 'CREATE TABLE statement not generated correctly'); + } finally { + Configuration::$mode = null; + } + } + + public function testEnsureTableFailsWhenModeNotSet(): void + { + $this->expectException(DocumentException::class); + Configuration::$mode = null; + Definition::ensureTable('boom'); } public function testEnsureIndexOnSucceedsWithoutSchemaSingleAscendingField(): void diff --git a/tests/unit/Query/DeleteTest.php b/tests/unit/Query/DeleteTest.php index 7921631..8636713 100644 --- a/tests/unit/Query/DeleteTest.php +++ b/tests/unit/Query/DeleteTest.php @@ -4,6 +4,7 @@ namespace Test\Unit\Query; use BitBadger\PDODocument\Field; use BitBadger\PDODocument\Query\Delete; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -11,6 +12,7 @@ use PHPUnit\Framework\TestCase; */ class DeleteTest extends TestCase { + #[TestDox('By ID succeeds')] public function testByIdSucceeds(): void { $this->assertEquals("DELETE FROM over_there WHERE data->>'id' = @id", Delete::byId('over_there'), diff --git a/tests/unit/Query/ExistsTest.php b/tests/unit/Query/ExistsTest.php index dcbf023..60d1093 100644 --- a/tests/unit/Query/ExistsTest.php +++ b/tests/unit/Query/ExistsTest.php @@ -4,6 +4,7 @@ namespace Test\Unit\Query; use BitBadger\PDODocument\Field; use BitBadger\PDODocument\Query\Exists; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -17,6 +18,7 @@ class ExistsTest extends TestCase 'Existence query not generated correctly'); } + #[TestDox('By ID succeeds')] public function testByIdSucceeds(): void { $this->assertEquals("SELECT EXISTS (SELECT 1 FROM dox WHERE data->>'id' = @id)", Exists::byId('dox'), diff --git a/tests/unit/Query/FindTest.php b/tests/unit/Query/FindTest.php index 1811caf..5a92241 100644 --- a/tests/unit/Query/FindTest.php +++ b/tests/unit/Query/FindTest.php @@ -4,6 +4,7 @@ namespace Test\Unit\Query; use BitBadger\PDODocument\Field; use BitBadger\PDODocument\Query\Find; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -11,6 +12,7 @@ use PHPUnit\Framework\TestCase; */ class FindTest extends TestCase { + #[TestDox('By ID succeeds')] public function testByIdSucceeds(): void { $this->assertEquals("SELECT data FROM here WHERE data->>'id' = @id", Find::byId('here'), diff --git a/tests/unit/Query/PatchTest.php b/tests/unit/Query/PatchTest.php new file mode 100644 index 0000000..81c5fd8 --- /dev/null +++ b/tests/unit/Query/PatchTest.php @@ -0,0 +1,83 @@ +assertEquals("UPDATE doc_table SET data = data || @data WHERE data->>'id' = @id", + Patch::byId('doc_table'), 'Patch UPDATE statement is not correct'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('By ID succeeds for SQLite')] + public function testByIdSucceedsForSQLite(): void + { + try { + Configuration::$mode = Mode::SQLite; + $this->assertEquals("UPDATE my_table SET data = json_patch(data, json(@data)) WHERE data->>'id' = @id", + Patch::byId('my_table'), 'Patch UPDATE statement is not correct'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('By ID fails when mode not set')] + public function testByIdFailsWhenModeNotSet(): void + { + $this->expectException(DocumentException::class); + Configuration::$mode = null; + Patch::byId('oof'); + } + + #[TestDox('By fields succeeds for PostgreSQL')] + public function testByFieldsSucceedsForPostgreSQL(): void + { + try { + Configuration::$mode = Mode::PgSQL; + $this->assertEquals("UPDATE that SET data = data || @data WHERE data->>'something' < @some", + Patch::byFields('that', [Field::LT('something', 17, '@some')]), + 'Patch UPDATE statement is not correct'); + } finally { + Configuration::$mode = null; + } + } + + #[TestDox('By fields succeeds for SQLite')] + public function testByFieldsSucceedsForSQLite(): void + { + try { + Configuration::$mode = Mode::SQLite; + $this->assertEquals( + "UPDATE a_table SET data = json_patch(data, json(@data)) WHERE data->>'something' > @it", + Patch::byFields('a_table', [Field::GT('something', 17, '@it')]), + 'Patch UPDATE statement is not correct'); + } finally { + Configuration::$mode = null; + } + } + + public function testByFieldsFailsWhenModeNotSet(): void + { + $this->expectException(DocumentException::class); + Configuration::$mode = null; + Patch::byFields('oops', []); + } +} diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index afdd261..3ed215e 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -4,6 +4,7 @@ namespace Test\Unit; use BitBadger\PDODocument\Field; use BitBadger\PDODocument\Query; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -37,11 +38,13 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } + #[TestDox('Where by ID succeeds with default parameter')] public function testWhereByIdSucceedsWithDefaultParameter(): void { $this->assertEquals("data->>'id' = @id", Query::whereById(), 'WHERE fragment not constructed correctly'); } + #[TestDox('Where by ID succeeds with specific parameter')] public function testWhereByIdSucceedsWithSpecificParameter(): void { $this->assertEquals("data->>'id' = @di", Query::whereById('@di'), 'WHERE fragment not constructed correctly');