Page MenuHomeDevCentral

No OneTemporary

diff --git a/README.md b/README.md
index ed3e87d..d6d0878 100644
--- a/README.md
+++ b/README.md
@@ -1,115 +1,164 @@
# keruald/database
This library offers a simple layer of abstraction for database operations.
## Configuration
To get a database instance, you need to pass configuration as an array.
The properties and values depend on the engine you want to use.
### MySQLi
| Key | Value | |
|------------|--------------------------------------|:---------------|
| engine | MySQLiEngine class reference | |
| host | The MySQL hostname, e.g. "localhost" | |
| username | The MySQL user to use for connection | |
| password | The clear text password to use | |
| database | The default db to select for queries | (optional) |
| fetch_mode | The default mode to fetch rows | `MYSQLI_ASSOC` |
For example:
```php
[
'engine' => Keruald\Database\Engines\MySQLiEngine::class,
'host' => 'localhost',
'username' => 'app',
'password' => 'someSecret',
'database' => 'app', // optional
]
```
#### About fetch_mode parameter
The `fetch_mode` parameter is used to determine how to represent results:
* `MYSQLI_ASSOC` will use column names
* `MYSQLI_NUM` will use an enumerated array (0, 1, 2, …)
* `MYSQLI_BOTH` will use both of them
The code offers `MYSQLI_ASSOC` as default value to allow to directly represent
a row result as API output and encourage to take care of the column names for
better code maintenance. If you wish to switch to default MySQLi behavior,
use `MYSQLI_BOTH` instead.
Those constants are defined by the MySQLi extension.
+### PDO Drivers - PostgreSQL
+
+Two PDO drivers for PostgreSQL are supported:
+
+ - PDO\Pgsql for PHP 8.4+, as PgsqlPDOEngine
+ - The legacy PDO for other PHP versions, as PostgreSQLPDOEngine
+
+| Key | Value | |
+|------------|--------------------------------------|:-------------------|
+| engine | PgsqlPDOEngine class reference | |
+| host | The hostname, e.g. "localhost" | |
+| username | The user to use for connection | |
+| password | The clear text password to use | |
+| database | The default db to select for queries | (optional) |
+| fetch_mode | The default mode to fetch rows | `PDO::FETCH_ASSOC` |
+
+For example:
+
+```php
+[
+ 'engine' => Keruald\Database\Engines\PgsqlPDOEngine::class,
+ 'host' => 'localhost',
+ 'username' => 'app',
+ 'password' => 'someSecret',
+ 'database' => 'app', // optional
+]
+```
+
+#### About fetch_mode parameter
+
+The `fetch_mode` parameter is used to determine how to represent results:
+
+* `PDO::FETCH_ASSOC` or `2` will use column names
+* `PDO::FETCH_NUM` or `3` will use an enumerated array (0, 1, 2, …)
+* `PDO::FETCH_BOTH` or `4` will use both of them
+
+The code offers `PDO::FETCH_ASSOC` as default value to allow to directly
+represent a row result as API output and encourage to take care of the column
+names for better code maintenance. If you wish to switch to default PDO
+behavior, use `PDO::FETCH_BOTH` instead.
+
+If you need to use other PDO modes, the following methods aren't available:
+ - isExistingTable
+ - queryScalar
+
+Other methods should work perfectly fine.
+
+Those constants are defined by the PDO extension.
+
## Legacy drivers
The mysql extension has been deprecated in PHP 5.7 and removed in PHP 7.
As such, this extension isn't supported anymore. You can use straightforwardly
replace 'MySQL' by 'MySQLi' as engine.
## Specialized drivers for tests
### Blackhole
The black hole engine does nothing and always returns `true` as query result.
This engine can be used for mocks:
- directly, when database behavior does not matter
- to build a mock by overriding behavior of query() or any other method
It can also be used with the loader, without any engine-specific configuration:
```php
[
'engine' => Keruald\Database\Engines\BlackholeEngine::class,
]
```
### MockDatabaseEngine
The mock database is a simple implementation of the black hole engine as mocking
service to use when you want to return a deterministic response to known
queries.
A benefit is you don't need a running database server for your unit tests.
You can pass to the `withQueries` method an array with one item per query:
- key: the SQL query
- value: an array with all rows for that query
-
For example:
```php
public function testGetFruits () : void {
$queries = [
"SELECT name, color FROM fruits" => [
[ "name" => "strawberry", "color" => "red" ],
[ "name" => "blueberry", "color" => "violet" ],
],
];
$db = (new MockDatabaseEngine())
->withQueries($queries);
// Inject $db to a class and test it
}
```
To return only one row, you can use `[[…]]` to represent an array of one array:
```php
$queries = [
"SELECT 1+1" => [[ "1+1" => 2 ]],
];
```
The queries results are then wrapped in the MockDatabaseResult class.
When the query doesn't exist, an exception is thrown.
We recommend a mixed approach of the Blackhole engine when results don't matter,
and of this class when you need some control on it.
diff --git a/src/Engines/PDOEngine.php b/src/Engines/PDOEngine.php
new file mode 100644
index 0000000..63df654
--- /dev/null
+++ b/src/Engines/PDOEngine.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Keruald\Database\Engines;
+
+use Keruald\Database\DatabaseEngine;
+
+use Keruald\Database\Exceptions\EngineSetupException;
+use Keruald\Database\Exceptions\NotImplementedException;
+use Keruald\Database\Exceptions\SqlException;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PDO;
+use PDOException;
+use RuntimeException;
+
+abstract class PDOEngine extends DatabaseEngine {
+
+ protected PDO $db;
+
+ private int $fetchMode = PDO::FETCH_ASSOC;
+
+ protected ?PDOException $lastException = null;
+
+ /**
+ * Sends a unique query to the database.
+ *
+ * @param string $query
+ *
+ * @return PDODatabaseResult|bool
+ */
+ public function query (string $query) : PDODatabaseResult|bool {
+ try {
+ $result = $this->db->query($query);
+ } catch (PDOException $ex) {
+ if ($this->dontThrowExceptions) {
+ return false;
+ }
+
+ $this->lastException = $ex;
+ $this->onQueryError($query);
+ }
+
+ return new PDODatabaseResult($result, $this->fetchMode);
+ }
+
+ public function nextId () : int|string {
+ return $this->db->lastInsertId();
+ }
+
+ protected function getExceptionContext () : array {
+ $info = match ($this->lastException) {
+ null => $this->db->errorInfo(),
+ default => $this->lastException->errorInfo,
+ };
+
+ return self::parseErrorInfo($info);
+ }
+
+ private static function parseErrorInfo(array $info) : array {
+ $context = [];
+
+ // SQLSTATE error code
+ if ((int)$info[0] > 0) {
+ $context["errno"] = $info[0];
+ }
+
+ // Driver-specific error message
+ if ($info[2] !== null) {
+ $context["error"] = $info[2];
+ }
+
+ return $context;
+ }
+
+ public static function load (array $config) : DatabaseEngine {
+ $config = self::getConfig($config);
+
+ try {
+ $instance = new static(
+ $config['host'],
+ $config['username'],
+ $config['password'],
+ $config['database'],
+ );
+ } catch (RuntimeException $ex) {
+ throw new EngineSetupException(
+ $ex->getMessage(),
+ $ex->getCode(),
+ $ex
+ );
+ }
+
+ // Extra configuration
+ $instance->fetchMode = (int)$config["fetch_mode"];
+
+ return $instance;
+ }
+
+ private static function getConfig (array $config) : array {
+ return $config + [
+ 'host' => 'localhost',
+ 'username' => '',
+ 'password' => '',
+ 'database' => '',
+ 'fetch_mode' => PDO::FETCH_ASSOC,
+ ];
+ }
+
+ public function getUnderlyingDriver () : PDO {
+ return $this->db;
+ }
+
+ public function error () : array {
+ return self::parseErrorInfo($this->db->errorInfo());
+ }
+
+ ///
+ /// Events
+ ///
+
+ protected function onCantConnectToHost () : void {
+ $previous = $this->lastException;
+
+ $code = match($previous) {
+ null => 0,
+ default => $previous->getCode(),
+ };
+
+ $appendToMessage = match($previous) {
+ null => "",
+ default => ": " . $previous->getMessage(),
+ };
+
+ $ex = new RuntimeException(
+ "Can't connect to SQL server" . $appendToMessage,
+ $code,
+ $previous,
+ );
+
+ if (!class_exists(self::EVENTS_PROPAGATION_CLASS)) {
+ throw $ex;
+ }
+
+ $callable = [self::EVENTS_PROPAGATION_CLASS, "callOrThrow"];
+ $callable($this->cantConnectToHostEvents, [$this, $ex], $ex);
+ }
+
+ protected function onQueryError (string $query) : void {
+ $ex = SqlException::fromQuery(
+ $query,
+ $this->getExceptionContext(),
+ );
+
+ if (!class_exists(self::EVENTS_PROPAGATION_CLASS)) {
+ throw $ex;
+ }
+
+ $callable = [self::EVENTS_PROPAGATION_CLASS, "callOrThrow"];
+ $callable($this->queryErrorEvents, [$this, $query, $ex], $ex);
+ }
+
+ ///
+ /// Not implemented features
+ ///
+
+ /**
+ * @throws NotImplementedException
+ */
+ public function countAffectedRows () : int {
+ throw new NotImplementedException(<<<END
+ With PDO drivers, you can get the number of affected rows
+ for any SQL query using PDODatabaseResult::numRows().
+ END);
+ }
+
+}
diff --git a/src/Engines/PgsqlPDOEngine.php b/src/Engines/PgsqlPDOEngine.php
new file mode 100644
index 0000000..861f933
--- /dev/null
+++ b/src/Engines/PgsqlPDOEngine.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Keruald\Database\Engines;
+
+use Keruald\Database\Exceptions\NotImplementedException;
+use PDO;
+use Pdo\Pgsql;
+use RuntimeException;
+
+final class PgsqlPDOEngine extends PDOEngine {
+
+ use WithPDOPostgreSQL;
+
+ const string PDO_CLASS = Pgsql::class;
+
+ ///
+ /// Constructor
+ ///
+
+ public function __construct (
+ string $host = 'localhost',
+ string $username = 'root',
+ string $password = '',
+ string $database = ''
+ ) {
+ // Checks extension requirement
+ if (!class_exists(self::PDO_CLASS)) {
+ throw new RuntimeException("This engine requires PHP 8.4+ Pdo\Pgsql PostgreSQL PDO driver.");
+ }
+
+ // Connects to PostgreSQL server
+ $dsn = "pgsql:host=$host";
+ if ($database !== "") {
+ $dsn .= ";dbname=$database";
+ }
+
+ $this->db = new Pgsql($dsn, $username, $password);
+ $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+
+ ///
+ /// Not implemented features
+ ///
+
+ /**
+ * @throws NotImplementedException
+ */
+ public function escape (string $expression) : string {
+ throw new NotImplementedException(<<<END
+ This PDO engine does not support escape for literals.
+ Placeholders are recommended instead for PDO operations.
+ END);
+ }
+
+ ///
+ /// Engine-specific methods
+ ///
+
+ public function escapeIdentifier (string $expression) : string {
+ return $this->db->escapeIdentifier($expression);
+ }
+
+}
diff --git a/src/Engines/PostgreSQLPDOEngine.php b/src/Engines/PostgreSQLPDOEngine.php
new file mode 100644
index 0000000..4e21b42
--- /dev/null
+++ b/src/Engines/PostgreSQLPDOEngine.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Keruald\Database\Engines;
+
+use Keruald\Database\Exceptions\NotImplementedException;
+
+use PDO;
+use PDOException;
+use RuntimeException;
+
+class PostgreSQLPDOEngine extends PDOEngine {
+
+ use WithPDOPostgreSQL;
+
+ const string PDO_CLASS = PDO::class;
+
+ public function __construct (
+ string $host = 'localhost',
+ string $username = 'root',
+ string $password = '',
+ string $database = ''
+ ) {
+ // Checks extension requirement
+ if (!class_exists(self::PDO_CLASS)) {
+ throw new RuntimeException("This engine requires PDO extension.");
+ }
+
+ // Connects to PostgreSQL server
+ $dsn = "pgsql:host=$host";
+ if ($database !== "") {
+ $dsn .= ";dbname=$database";
+ }
+
+ try {
+ $this->db = new PDO($dsn, $username, $password);
+ } catch (PDOException $ex) {
+ $this->lastException = $ex;
+ $this->onCantConnectToHost();
+
+ return;
+ }
+
+ $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+
+ ///
+ /// Not implemented features
+ ///
+
+ /**
+ * @throws NotImplementedException
+ */
+ public function escape (string $expression) : string {
+ throw new NotImplementedException(<<<END
+ This PDO engine does not support escape for literals.
+ Placeholders are recommended instead for PDO operations.
+ END);
+ }
+
+}
diff --git a/src/Engines/WithPDOPostgreSQL.php b/src/Engines/WithPDOPostgreSQL.php
new file mode 100644
index 0000000..be31d7c
--- /dev/null
+++ b/src/Engines/WithPDOPostgreSQL.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Keruald\Database\Engines;
+
+use LogicException;
+use PDOException;
+
+trait WithPDOPostgreSQL {
+
+ public function isExistingTable (string $database, string $table) : bool {
+ /** @var \PDO $db */
+ $db = $this->getUnderlyingDriver();
+
+ $sql = "SELECT EXISTS (
+ SELECT FROM pg_tables
+ WHERE schemaname = ?
+ AND tablename = ?)::int;";
+
+ $stmt = $db->prepare($sql);
+ $stmt->execute([$database, $table]);
+
+ $result = $stmt->fetchColumn();
+ return (bool)$result;
+ }
+
+ public function queryScalar (string $query = "") : string {
+ if ($query === "") {
+ return "";
+ }
+
+ $result = $this->query($query);
+
+ $row = $result->fetchRow();
+ if ($row !== null) {
+ foreach ($row as $value) {
+ return (string)$value;
+ }
+ }
+
+ // No item
+ throw new LogicException("The queryScalar method is intended
+ to be used with SELECT queries and assimilated");
+ }
+
+}
diff --git a/src/Exceptions/NotImplementedException.php b/src/Exceptions/NotImplementedException.php
new file mode 100644
index 0000000..d83c273
--- /dev/null
+++ b/src/Exceptions/NotImplementedException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Keruald\Database\Exceptions;
+
+use RuntimeException;
+
+class NotImplementedException extends RuntimeException {
+
+}
diff --git a/src/Result/PDODatabaseResult.php b/src/Result/PDODatabaseResult.php
new file mode 100644
index 0000000..32e693d
--- /dev/null
+++ b/src/Result/PDODatabaseResult.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Keruald\Database\Result;
+
+use PDO;
+use PDOStatement;
+
+use Traversable;
+
+class PDODatabaseResult extends DatabaseResult {
+
+ public function __construct (
+ private readonly PDOStatement $statement,
+ private readonly int $fetchMode = PDO::FETCH_ASSOC,
+ ) { }
+
+ ///
+ /// DatabaseResult implementation
+ ///
+
+ public function numRows () : int {
+ return $this->statement->rowCount();
+ }
+
+ public function fetchRow () : ?array {
+ $row = $this->statement->fetch($this->fetchMode);
+
+ if ($row === false) {
+ return null;
+ }
+
+ return $row;
+ }
+
+ ///
+ /// IteratorAggregate implementation
+ ///
+
+ public function getIterator () : Traversable {
+ while ($row = $this->fetchRow()) {
+ yield $row;
+ }
+ }
+
+}
diff --git a/tests/Engines/BasePDOPostgreSQLTestCase.php b/tests/Engines/BasePDOPostgreSQLTestCase.php
new file mode 100644
index 0000000..aa5e527
--- /dev/null
+++ b/tests/Engines/BasePDOPostgreSQLTestCase.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Keruald\Database\Tests\Engines;
+
+use Keruald\Database\Exceptions\NotImplementedException;
+use Keruald\Database\Exceptions\SqlException;
+use Keruald\Database\Result\PDODatabaseResult;
+
+abstract class BasePDOPostgreSQLTestCase extends BasePDOTestCase {
+
+ const string DB_NAME = "test_keruald_db";
+
+ public function testQueryScalarWithNonSelectQuery(): void {
+ $this->expectException(\LogicException::class);
+
+ $sql = "UPDATE numbers SET number = number * 2";
+ $this->db->queryScalar($sql);
+ }
+
+ public function testFetchRow(): void {
+ $sql = "SELECT 10 UNION SELECT 20 UNION SELECT 30 ORDER BY 1";
+ $result = $this->db->query($sql);
+
+ // PostgreSQL uses "?column?" as default column name for unnamed columns
+ $this->assertEquals(10, $result->fetchRow()['?column?']);
+ $this->assertEquals(20, $result->fetchRow()['?column?']);
+ $this->assertEquals(30, $result->fetchRow()['?column?']);
+
+ // Then, we get a null value
+ $this->assertEquals(null, $result->fetchRow());
+ }
+
+ public function testArrayShapeForFetchRow(): void {
+ $sql = "SELECT 10 as score, 50 as \"limit\"";
+ $result = $this->db->query($sql);
+
+ $expected = [
+ // By column name
+ "score" => 10,
+ "limit" => 50
+ ];
+
+ $this->assertEquals($expected, $result->fetchRow());
+ }
+
+ public function testQueryWhenItSucceeds(): void {
+ $result = $this->db->query("DELETE FROM numbers");
+
+ $this->assertInstanceOf(PDODatabaseResult::class, $result);
+ }
+
+ public function testQueryWhenItFailsWithException(): void {
+ $this->expectException(SqlException::class);
+ $this->db->query("TRUNCATE not_existing_table");
+ }
+
+ public function testQueryWithWrongQueryInLegacyMode(): void {
+ $this->db->dontThrowExceptions = true;
+
+ $result = $this->db->query("TRUNCATE not_existing");
+
+ $this->assertFalse($result);
+ }
+
+ public function testNextId(): void {
+ // Use a transaction to isolate this test
+ // Arcanist creates a race condition on this when running from `arc diff`
+ $this->db->query("BEGIN");
+
+ try {
+ // PostgreSQL uses sequences for auto-increment
+ $this->db->query("TRUNCATE numbers RESTART IDENTITY");
+ $this->db->query("INSERT INTO numbers (id, number) VALUES (1700, 42742)");
+ $this->db->query("INSERT INTO numbers (number) VALUES (666)");
+
+ // Get the last inserted ID
+ $lastId = $this->db->nextId();
+ $this->assertGreaterThan(0, $lastId);
+
+ $this->db->query("ROLLBACK"); // No need to commit, tests done
+ } catch (\Exception $ex) {
+ $this->db->query("ROLLBACK");
+ $this->fail($ex->getMessage());
+ }
+ }
+
+ public function testEscapeNotImplemented(): void {
+ $this->expectException(NotImplementedException::class);
+ $this->expectExceptionMessage('This PDO engine does not support escape for literals');
+
+ $this->db->escape("test'string");
+ }
+
+}
diff --git a/tests/Engines/BasePDOTestCase.php b/tests/Engines/BasePDOTestCase.php
new file mode 100644
index 0000000..5ac5f1c
--- /dev/null
+++ b/tests/Engines/BasePDOTestCase.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Keruald\Database\Tests\Engines;
+
+use Keruald\Database\Engines\PDOEngine;
+
+use Keruald\Database\Exceptions\SqlException;
+use PHPUnit\Framework\TestCase;
+
+abstract class BasePDOTestCase extends TestCase {
+
+ protected PDOEngine $db;
+
+ protected static abstract function buildEngine () : PDOEngine;
+
+ protected function setUp (): void {
+ $this->db = static::buildEngine();
+ }
+
+ public function testQueryScalar(): void {
+ $sql = "SELECT 1+1";
+ $this->assertEquals(2, $this->db->queryScalar($sql));
+ }
+
+ public function testQueryScalarWithoutQuery(): void {
+ $this->assertEquals("", $this->db->queryScalar(""));
+ }
+
+ public function testQueryScalarWithWrongQuery(): void {
+ $this->expectException(SqlException::class);
+
+ $sql = "DELETE FROM nonexisting";
+ $this->db->queryScalar($sql);
+ }
+}
diff --git a/tests/Engines/PgsqlPDOEngineTest.php b/tests/Engines/PgsqlPDOEngineTest.php
new file mode 100644
index 0000000..9a9f50f
--- /dev/null
+++ b/tests/Engines/PgsqlPDOEngineTest.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Keruald\Database\Tests\Engines;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Engines\PgsqlPDOEngine;
+use Keruald\Database\Exceptions\EngineSetupException;
+
+use PDO;
+use Pdo\Pgsql;
+use PHPUnit\Framework\Attributes\DataProvider;
+
+class PgsqlPDOEngineTest extends BasePDOPostgreSQLTestCase {
+
+ protected function setUp () : void {
+ // Skip if not on PHP 8.4+
+ if (version_compare(PHP_VERSION, '8.4.0', '<')) {
+ $this->markTestSkipped('Pdo\Pgsql requires PHP version >= 8.4.0');
+ }
+
+ parent::setUp();
+ }
+
+ protected static function buildEngine () : PDOEngine {
+ return new PgsqlPDOEngine(
+ 'localhost',
+ 'keruald',
+ 'keruald',
+ self::DB_NAME,
+ );
+ }
+
+ public function testLoad () : void {
+ $instance = PgsqlPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'keruald',
+ 'password' => 'keruald',
+ 'database' => self::DB_NAME,
+ ]);
+
+ $this->assertInstanceOf(PDO::class, $instance->getUnderlyingDriver());
+ }
+
+ public function testLoadWithWrongPassword(): void {
+ $this->expectException(EngineSetupException::class);
+
+ PgsqlPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'notexisting',
+ 'password' => 'notexistingeither',
+ 'database' => self::DB_NAME,
+ ]);
+ }
+
+ public function testLoadWithFetchMode(): void {
+ $instance = PgsqlPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'keruald',
+ 'password' => 'keruald',
+ 'database' => self::DB_NAME,
+ 'fetch_mode' => PDO::FETCH_BOTH,
+ ]);
+
+ $result = $instance->query("SELECT 1 as num");
+ $row = $result->fetchRow();
+
+ // Should have both numeric and associative keys
+ $this->assertArrayHasKey(0, $row);
+ $this->assertArrayHasKey('num', $row);
+ }
+
+ ///
+ /// Engine-specific methods
+ ///
+
+ public static function provideIdentifiers () : iterable {
+ yield "Regular name" => ["foo", '"foo"'];
+ yield "No special escape for apostrophe" => ["y'sul", '"y\'sul"'];
+ yield "Reserved keyword" => ["user", '"user"'];
+ }
+
+ #[DataProvider("provideIdentifiers")]
+ public function testEscapeIdentifier ($toEscape, $expected) : void {
+ /** @var PgsqlPDOEngine $db */
+ $db = $this->db;
+
+ $actual = $db->escapeIdentifier($toEscape);
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/tests/Engines/PostgreSQLPDOEngineTest.php b/tests/Engines/PostgreSQLPDOEngineTest.php
new file mode 100644
index 0000000..eb91f80
--- /dev/null
+++ b/tests/Engines/PostgreSQLPDOEngineTest.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Keruald\Database\Tests\Engines;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Engines\PostgreSQLPDOEngine;
+use Keruald\Database\Exceptions\EngineSetupException;
+
+use PDO;
+
+class PostgreSQLPDOEngineTest extends BasePDOPostgreSQLTestCase {
+
+ protected static function buildEngine () : PDOEngine {
+ return new PostgreSQLPDOEngine(
+ 'localhost',
+ 'keruald',
+ 'keruald',
+ self::DB_NAME,
+ );
+ }
+
+ public function testLoad () : void {
+ $instance = PostgreSQLPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'keruald',
+ 'password' => 'keruald',
+ 'database' => self::DB_NAME,
+ ]);
+
+ $this->assertInstanceOf(PDO::class, $instance->getUnderlyingDriver());
+ }
+
+ public function testLoadWithWrongPassword(): void {
+ $this->expectException(EngineSetupException::class);
+
+ PostgreSQLPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'notexisting',
+ 'password' => 'notexistingeither',
+ 'database' => self::DB_NAME,
+ ]);
+ }
+
+ public function testLoadWithFetchMode(): void {
+ $instance = PostgreSQLPDOEngine::load([
+ 'host' => 'localhost',
+ 'username' => 'keruald',
+ 'password' => 'keruald',
+ 'database' => self::DB_NAME,
+ 'fetch_mode' => PDO::FETCH_BOTH,
+ ]);
+
+ $result = $instance->query("SELECT 1 as num");
+ $row = $result->fetchRow();
+
+ // Should have both numeric and associative keys
+ $this->assertArrayHasKey(0, $row);
+ $this->assertArrayHasKey('num', $row);
+ }
+}
diff --git a/tests/data/postgresql.sql b/tests/data/postgresql.sql
new file mode 100644
index 0000000..cf00b8b
--- /dev/null
+++ b/tests/data/postgresql.sql
@@ -0,0 +1,27 @@
+DROP TABLE IF EXISTS numbers CASCADE;
+CREATE TABLE numbers
+(
+ id SERIAL PRIMARY KEY,
+ number INTEGER NULL
+);
+
+DROP TABLE IF EXISTS ships CASCADE;
+CREATE TABLE ships
+(
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NULL,
+ category VARCHAR(3) NULL
+);
+
+INSERT INTO ships VALUES
+ (1, 'So Much For Subtlety', 'GSV'),
+ (2, 'Unfortunate Conflict Of Evidence', 'GSV'),
+ (3, 'Just Read The Instructions', 'GCU'),
+ (4, 'Just Another Victim Of The Ambient Morality', 'GCU');
+
+DROP VIEW IF EXISTS ships_count CASCADE;
+CREATE VIEW ships_count AS
+SELECT category,
+ COUNT(category) AS "count(category)"
+FROM ships
+GROUP BY category;

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 1, 17:53 (1 d, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3102246
Default Alt Text
(26 KB)

Event Timeline