From c892689eb65e09048579754ca542bdef72afea22 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Jun 2024 21:12:21 -0400 Subject: [PATCH] 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