From facaee63dff6b245987b7b066b4a1535746016ce Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Sun, 20 Oct 2024 13:01:27 -0500 Subject: [PATCH] Add support for PostgreSQL Closes #8 --- .github/workflows/php.yml | 14 +++++++- composer.json | 6 ++-- lib/Options.php | 3 ++ lib/PeachySql.php | 4 +++ test/DbTestCase.php | 48 ++++++++++++++++++++++++--- test/Pgsql/PgsqlDbTest.php | 66 ++++++++++++++++++++++++++++++++++++++ test/src/Config.php | 15 +++++++++ 7 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 test/Pgsql/PgsqlDbTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 0928a3b..60ad049 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -18,6 +18,16 @@ jobs: ports: - 3306:3306 options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 + postgres: + image: postgres + env: + POSTGRES_HOST: localhost + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 steps: - name: Checkout @@ -42,4 +52,6 @@ jobs: if: ${{ matrix.php == '8.3' }} - name: Run PHPUnit - run: composer test-mysql + run: composer test-without-mssql + env: + POSTGRES_HOST: localhost diff --git a/composer.json b/composer.json index 7e9c377..f13f715 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,9 @@ "scripts": { "analyze": "psalm", "test": "phpunit", - "test-mssql": "phpunit --exclude-group mysql", - "test-mysql": "phpunit --exclude-group mssql" + "test-mssql": "phpunit --exclude-group mysql,pgsql", + "test-mysql": "phpunit --exclude-group mssql,pgsql", + "test-pgsql": "phpunit --exclude-group mssql,mysql", + "test-without-mssql": "phpunit --exclude-group mssql" } } diff --git a/lib/Options.php b/lib/Options.php index 9235abb..3ff2c5c 100644 --- a/lib/Options.php +++ b/lib/Options.php @@ -24,6 +24,9 @@ class Options public bool $fetchNextSyntax = false; public bool $multiRowset = false; public bool $sqlsrvBinaryEncoding = false; + public bool $binarySelectedAsStream = false; + public bool $nativeBoolColumns = false; + public bool $floatSelectedAsString = false; /** * The character used to quote identifiers. diff --git a/lib/PeachySql.php b/lib/PeachySql.php index 0a1396c..0383df0 100644 --- a/lib/PeachySql.php +++ b/lib/PeachySql.php @@ -38,6 +38,10 @@ public function __construct(PDO $connection, ?Options $options = null) } elseif ($driver === 'mysql') { $options->lastIdIsFirstOfBatch = true; $options->identifierQuote = '`'; // needed since not everyone uses ANSI mode + } elseif ($driver === 'pgsql') { + $options->binarySelectedAsStream = true; + $options->nativeBoolColumns = true; + $options->floatSelectedAsString = true; } } diff --git a/test/DbTestCase.php b/test/DbTestCase.php index d39e86a..b9ded3c 100644 --- a/test/DbTestCase.php +++ b/test/DbTestCase.php @@ -75,7 +75,7 @@ public function testTransactions(): void $options = $peachySql->options; $this->assertSame($options->affectedIsRowCount ? 1 : -1, $result->getAffected()); - $expected = ['user_id' => $id, 'is_disabled' => 1]; + $expected = ['user_id' => $id, 'is_disabled' => $options->nativeBoolColumns ? true : 1]; $this->assertSame($expected, $result->getFirst()); // the row should be selectable $peachySql->rollback(); // cancel the transaction @@ -132,7 +132,18 @@ public function testIteratorQuery(): void foreach ($iterator as $row) { unset($row['user_id']); - $row['is_disabled'] = (bool)$row['is_disabled']; + + if ($options->floatSelectedAsString) { + $row['weight'] = (float) $row['weight']; + } + if (!$options->nativeBoolColumns) { + $row['is_disabled'] = (bool) $row['is_disabled']; + } + if ($options->binarySelectedAsStream && $row['uuid'] !== null) { + /** @psalm-suppress MixedArgument */ + $row['uuid'] = stream_get_contents($row['uuid']); + } + $colValsCompare[] = $row; } @@ -164,6 +175,13 @@ public function testIteratorQuery(): void $updatedNames = $result->getAll(); $this->assertSame($options->affectedIsRowCount ? 2 : -1, $result->getAffected()); + if ($options->binarySelectedAsStream) { + /** @var array{uuid: resource} $row */ + foreach ($updatedNames as &$row) { + $row['uuid'] = stream_get_contents($row['uuid']); + } + } + $this->assertSame($realNames, $updatedNames); } @@ -192,8 +210,9 @@ public function testInsertBulk(): void $insertColVals[] = $row; } + $options = $peachySql->options; $totalBoundParams = count($insertColVals[0]) * $rowCount; - $expectedQueries = ($totalBoundParams > $peachySql->options->maxBoundParams) ? 2 : 1; + $expectedQueries = ($totalBoundParams > $options->maxBoundParams) ? 2 : 1; $result = $peachySql->insertRows($this->table, $insertColVals); $this->assertSame($expectedQueries, $result->queryCount); @@ -205,6 +224,22 @@ public function testInsertBulk(): void $rows = $peachySql->selectFrom("SELECT {$columns} FROM {$this->table}") ->where(['user_id' => $ids])->query()->getAll(); + if ($options->binarySelectedAsStream || $options->nativeBoolColumns || $options->floatSelectedAsString) { + /** @var array{weight: float|string, is_disabled: int|bool, uuid: string|resource} $row */ + foreach ($rows as &$row) { + if (!is_float($row['weight'])) { + $row['weight'] = (float) $row['weight']; + } + if (!is_int($row['is_disabled'])) { + $row['is_disabled'] = (int) $row['is_disabled']; + } + if (!is_string($row['uuid'])) { + /** @psalm-suppress InvalidArgument */ + $row['uuid'] = stream_get_contents($row['uuid']); + } + } + } + $this->assertSame($colVals, $rows); // update the inserted rows @@ -216,9 +251,14 @@ public function testInsertBulk(): void $userId = $ids[0]; $set = ['uuid' => $peachySql->makeBinaryParam($newUuid)]; $peachySql->updateRows($this->table, $set, ['user_id' => $userId]); - /** @var array{uuid: string} $updatedRow */ + /** @var array{uuid: string|resource} $updatedRow */ $updatedRow = $peachySql->selectFrom("SELECT uuid FROM {$this->table}") ->where(['user_id' => $userId])->query()->getFirst(); + + if (!is_string($updatedRow['uuid'])) { + $updatedRow['uuid'] = stream_get_contents($updatedRow['uuid']); // needed for PostgreSQL + } + $this->assertSame($newUuid, $updatedRow['uuid']); // delete the inserted rows diff --git a/test/Pgsql/PgsqlDbTest.php b/test/Pgsql/PgsqlDbTest.php new file mode 100644 index 0000000..908b52f --- /dev/null +++ b/test/Pgsql/PgsqlDbTest.php @@ -0,0 +1,66 @@ +getPgsqlDsn($dbName), $c->getPgsqlUser(), $c->getPgsqlPassword(), [ + PDO::ATTR_EMULATE_PREPARES => false, + ]); + + self::$db = new PeachySql($pdo); + self::createTestTable(self::$db); + } + + return self::$db; + } + + private static function createTestTable(PeachySql $db): void + { + $sql = " + CREATE TABLE Users ( + user_id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + dob DATE NOT NULL, + weight REAL NOT NULL, + is_disabled BOOLEAN NOT NULL, + uuid bytea NULL + )"; + + $db->query("DROP TABLE IF EXISTS Users"); + $db->query($sql); + } +} diff --git a/test/src/Config.php b/test/src/Config.php index 3a65cd0..7ca35cc 100644 --- a/test/src/Config.php +++ b/test/src/Config.php @@ -24,6 +24,21 @@ public function getMysqlPassword(): string return ''; } + public function getPgsqlDsn(string $database): string + { + return "pgsql:host=localhost;dbname=$database"; + } + + public function getPgsqlUser(): string + { + return 'postgres'; + } + + public function getPgsqlPassword(): string + { + return 'postgres'; + } + public function getSqlsrvServer(): string { return '(local)\SQLEXPRESS';