Page MenuHomeDevCentral

D3836.diff
No OneTemporary

D3836.diff

diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -42,7 +42,7 @@
"replace": {
"keruald/cache": "0.1.0",
"keruald/commands": "0.0.1",
- "keruald/database": "0.6.0",
+ "keruald/database": "0.6.1",
"keruald/github": "0.2.1",
"keruald/omnitools": "0.16.0",
"keruald/report": "0.1.0",
diff --git a/database/src/Engines/PDOEngine.php b/database/src/Engines/PDOEngine.php
--- a/database/src/Engines/PDOEngine.php
+++ b/database/src/Engines/PDOEngine.php
@@ -10,6 +10,7 @@
use Keruald\Database\Exceptions\SqlException;
use Keruald\Database\Result\PDODatabaseResult;
+use Keruald\Database\Query\PDOQuery;
use PDO;
use PDOException;
use RuntimeException;
@@ -48,6 +49,10 @@
return $this->db->lastInsertId();
}
+ public function setLastException (PDOException $ex) : void {
+ $this->lastException = $ex;
+ }
+
protected function getExceptionContext () : array {
$info = match ($this->lastException) {
null => $this->db->errorInfo(),
@@ -158,7 +163,7 @@
$callable($this->cantConnectToHostEvents, [$this, $ex], $ex);
}
- protected function onQueryError (string $query) : void {
+ public function onQueryError (string $query) : void {
$ex = SqlException::fromQuery(
$query,
$this->getExceptionContext(),
@@ -172,6 +177,26 @@
$callable($this->queryErrorEvents, [$this, $query, $ex], $ex);
}
+ ///
+ /// PDO features
+ ///
+
+ public abstract function hasInOutSupport() : bool;
+
+ /**
+ * Prepares a query for later execution
+ *
+ * @param string $query
+ * @param int[] $options
+ * @return PDOQuery
+ */
+ public function prepare (string $query, array $options = []) : PDOQuery {
+ $statement = $this->db->prepare($query, $options);
+
+ return PDOQuery::from($this, $statement)
+ ->withFetchMode($this->fetchMode);
+ }
+
///
/// Not implemented features
///
diff --git a/database/src/Engines/PgsqlPDOEngine.php b/database/src/Engines/PgsqlPDOEngine.php
--- a/database/src/Engines/PgsqlPDOEngine.php
+++ b/database/src/Engines/PgsqlPDOEngine.php
@@ -52,6 +52,10 @@
END);
}
+ public function hasInOutSupport() : bool {
+ return false;
+ }
+
///
/// Engine-specific methods
///
diff --git a/database/src/Engines/PostgreSQLPDOEngine.php b/database/src/Engines/PostgreSQLPDOEngine.php
--- a/database/src/Engines/PostgreSQLPDOEngine.php
+++ b/database/src/Engines/PostgreSQLPDOEngine.php
@@ -57,4 +57,8 @@
END);
}
+ public function hasInOutSupport() : bool {
+ return false;
+ }
+
}
diff --git a/database/src/Query/DatabaseQuery.php b/database/src/Query/DatabaseQuery.php
new file mode 100644
--- /dev/null
+++ b/database/src/Query/DatabaseQuery.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Keruald\Database\Query;
+
+use Keruald\Database\Result\DatabaseResult;
+
+abstract class DatabaseQuery {
+
+ public abstract function query() : ?DatabaseResult;
+
+ public abstract function __toString() : string;
+
+}
diff --git a/database/src/Query/PDOQuery.php b/database/src/Query/PDOQuery.php
new file mode 100644
--- /dev/null
+++ b/database/src/Query/PDOQuery.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Keruald\Database\Query;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Exceptions\NotImplementedException;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PDO;
+use PDOException;
+use PDOStatement;
+
+class PDOQuery extends DatabaseQuery {
+
+ ///
+ /// Private members
+ ///
+
+ private PDOEngine $db;
+
+ private PDOStatement $statement;
+
+ private int $fetchMode = PDO::FETCH_ASSOC;
+
+ private bool $isInOutDefined = false;
+
+ ///
+ /// Constructors
+ ///
+
+ public function __construct (PDOEngine $db, PDOStatement $statement) {
+ $this->db = $db;
+ $this->statement = $statement;
+ }
+
+ public static function from (PDOEngine $db, PDOStatement $statement) : self {
+ return new self($db, $statement);
+ }
+
+ ///
+ /// Getters and setters
+ ///
+
+ public function getFetchMode () : int {
+ return $this->fetchMode;
+ }
+
+ public function setFetchMode (int $mode) : void {
+ $this->fetchMode = $mode;
+ }
+
+ public function withFetchMode (int $mode) : self {
+ $this->fetchMode = $mode;
+
+ return $this;
+ }
+
+ ///
+ /// PDO statements like interaction
+ ///
+
+ public function query() : ?PDODatabaseResult {
+ if ($this->isInOutDefined && !$this->db->hasInOutSupport()) {
+ throw new NotImplementedException("InOut parameters are not supported by this engine.");
+ }
+
+ try {
+ $result = $this->statement->execute();
+ } catch (PDOException $ex) {
+ if ($this->db->dontThrowExceptions) {
+ return null;
+ }
+
+ $this->db->setLastException($ex);
+ $this->db->onQueryError($this->statement->queryString);
+ }
+
+ return new PDODatabaseResult($this->statement, $this->fetchMode);
+ }
+
+ public function with (int|string $name, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($name, $value, $type);
+
+ return $this;
+ }
+
+ public function withIndexedValue(int $position, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($position, $value, $type);
+
+ return $this;
+ }
+
+ public function withValue(string $name, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($name, $value, $type);
+
+ return $this;
+ }
+
+ public function bind(string $name, mixed &$value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindParam($name, $value, $type);
+
+ return $this;
+ }
+
+ public function bindInOutParameter(string $name, mixed &$value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindParam($name, $value, $type | PDO::PARAM_INPUT_OUTPUT);
+
+ $this->isInOutDefined = true;
+
+ return $this;
+ }
+
+ ///
+ /// PDO_PARAM_* type resolution
+ ///
+
+ public static function resolveParameterType(mixed $value) : int {
+ if (is_int($value)) {
+ return PDO::PARAM_INT;
+ }
+
+ if (is_null($value)) {
+ return PDO::PARAM_NULL;
+ }
+
+ if (is_bool($value)) {
+ return PDO::PARAM_BOOL;
+ }
+
+ return PDO::PARAM_STR;
+ }
+
+ ///
+ /// Low-level interactions
+ ///
+
+ public function getUnderlyingStatement () : PDOStatement {
+ return $this->statement;
+ }
+
+ ///
+ /// Implements Stringable
+ ///
+
+ public function __toString () : string {
+ return $this->statement->queryString;
+ }
+
+}
diff --git a/database/tests/Engines/BasePDOPostgreSQLTestCase.php b/database/tests/Engines/BasePDOPostgreSQLTestCase.php
--- a/database/tests/Engines/BasePDOPostgreSQLTestCase.php
+++ b/database/tests/Engines/BasePDOPostgreSQLTestCase.php
@@ -6,6 +6,8 @@
use Keruald\Database\Exceptions\SqlException;
use Keruald\Database\Result\PDODatabaseResult;
+use PDO;
+
abstract class BasePDOPostgreSQLTestCase extends BasePDOTestCase {
const string DB_NAME = "test_keruald_db";
@@ -91,4 +93,15 @@
$this->db->escape("test'string");
}
+
+ public function testInOut () : void {
+ $this->expectException(NotImplementedException::class);
+
+ $port = 8000;
+ $query = $this->db
+ ->prepare("CALL define_port(:port);")
+ ->bindInOutParameter("port", $port, PDO::PARAM_INT)
+ ->query();
+ }
+
}
diff --git a/database/tests/Engines/BasePDOTestCase.php b/database/tests/Engines/BasePDOTestCase.php
--- a/database/tests/Engines/BasePDOTestCase.php
+++ b/database/tests/Engines/BasePDOTestCase.php
@@ -3,10 +3,16 @@
namespace Keruald\Database\Tests\Engines;
use Keruald\Database\Engines\PDOEngine;
-
use Keruald\Database\Exceptions\SqlException;
+use Keruald\Database\Query\PDOQuery;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
+use PDO;
+use PDOStatement;
+
abstract class BasePDOTestCase extends TestCase {
protected PDOEngine $db;
@@ -32,4 +38,147 @@
$sql = "DELETE FROM nonexisting";
$this->db->queryScalar($sql);
}
+
+ ///
+ /// Integration tests for PDOQuery
+ ///
+
+ public function testPrepare(): void {
+ $sql = "SELECT :word";
+
+ $query = $this->db->prepare($sql);
+ $this->assertInstanceOf(PDOQuery::class, $query);
+
+ $result = $query
+ ->with("word", "foo")
+ ->query();
+ $this->assertInstanceOf(PDODatabaseResult::class, $result);
+
+ $row = $result->fetchRow();
+ $this->assertContains("foo", $row);
+ }
+
+ public function testWithValue(): void {
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->withvalue("word", "foo");
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testWithValueWithVariableChange(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->withvalue("word", $word);
+
+ $word = "bar";
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testWithIndexedValue(): void {
+ $query = $this->db
+ ->prepare("SELECT ?")
+ ->withIndexedValue(1, "foo");
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+ }
+
+ public function testBind(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->bind("word", $word);
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testBindWithVariableChange(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->bind("word", $word);
+
+ $word = "bar";
+
+ $this->assertEquals("bar", $query->query()->fetchScalar());
+ }
+
+
+ public static function provideFetchModeAndScalarResults(): iterable {
+ yield "PDO::FETCH_ASSOC" => [
+ PDO::FETCH_ASSOC,
+ ["?column?" => "foo"],
+ ];
+
+ yield "PDO::FETCH_NUM" => [
+ PDO::FETCH_NUM,
+ [0 => "foo"],
+ ];
+
+ yield "PDO::FETCH_BOTH" => [
+ PDO::FETCH_BOTH,
+ [0 => "foo", "?column?" => "foo"],
+ ];
+ }
+
+ #[DataProvider("provideFetchModeAndScalarResults")]
+ public function testFetchMode($mode, $expected): void {
+ $actual = $this->db
+ ->prepare("SELECT :word")
+ ->with("word", "foo")
+ ->withFetchMode($mode)
+ ->query()
+ ->fetchRow();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetUnderlyingStatement () : void {
+ $query = $this->db
+ ->prepare("SELECT :word");
+
+ $this->assertInstanceOf(PDOStatement::class, $query->getUnderlyingStatement());
+ }
+
+ public function testToString () : void {
+ $query = $this->db
+ ->prepare("SELECT :word");
+
+ $this->assertEquals("SELECT :word", (string)$query);
+ }
+
+ public function testToStringIsInvariant () : void {
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->with("word", "foo");
+
+ $this->assertEquals("SELECT :word", (string)$query);
+ }
+
+ public function testQueryWithError () : void {
+ $this->expectException(SqlException::class);
+
+ $sql = "SELECT * FROM nonexisting";
+ $result = $this->db->prepare($sql)->query();
+
+ $this->assertNull($result);
+ }
+
+ public function testQueryWithErrorWhenExceptionsAreDisabled () : void {
+ $this->db->dontThrowExceptions = true;
+
+ $sql = "SELECT * FROM nonexisting";
+ $result = $this->db->prepare($sql)->query();
+
+ $this->assertNull($result);
+ }
+
}
diff --git a/database/tests/Query/PDOQueryTest.php b/database/tests/Query/PDOQueryTest.php
new file mode 100644
--- /dev/null
+++ b/database/tests/Query/PDOQueryTest.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Keruald\Database\Tests\Query;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Query\PDOQuery;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+
+use PDO;
+use PDOStatement;
+
+class PDOQueryTest extends TestCase {
+
+ private PDOQuery $query;
+
+ ///
+ /// Tests set up
+ ///
+
+ protected function setUp() : void {
+ $this->query = $this->mockQuery();
+ }
+
+ protected function mockQuery () : PDOQuery {
+ $engine = $this->createMock(PDOEngine::class);
+ $statement = $this->createMock(PDOStatement::class);
+
+ return new PDOQuery($engine, $statement);
+ }
+
+ ///
+ /// Getters and setters
+ ///
+
+ public function testGetAndSetFetchMode () : void {
+ $this->query->setFetchMode(PDO::FETCH_ASSOC);
+ $this->assertEquals(PDO::FETCH_ASSOC, $this->query->getFetchMode());
+ }
+
+ ///
+ /// Static methods
+ ///
+
+ public static function provideParameterTypes () : iterable {
+ yield "int" => [ PDO::PARAM_INT, 1 ];
+ yield "falsy int" => [ PDO::PARAM_INT, 0 ];
+ yield "negative int" => [ PDO::PARAM_INT, -1 ];
+
+ yield "bool" => [ PDO::PARAM_BOOL, true ];
+ yield "falsy bool" => [ PDO::PARAM_BOOL, false ];
+
+ yield "null" => [ PDO::PARAM_NULL, null ];
+
+ yield "string" => [ PDO::PARAM_STR, "foo" ];
+ yield "empty string" => [ PDO::PARAM_STR, "" ];
+ yield "zero string" => [ PDO::PARAM_STR, "0" ];
+
+ // Anything else should also be treated as a string
+ yield "float" => [ PDO::PARAM_STR, 1.0 ];
+ yield "zero float" => [ PDO::PARAM_STR, 0.0 ];
+ }
+
+ #[DataProvider("provideParameterTypes")]
+ public function testResolveParameterType ($type, $value) {
+ $this->assertEquals($type, PDOQuery::resolveParameterType($value));
+ }
+
+}
diff --git a/database/tests/data/postgresql.sql b/database/tests/data/postgresql.sql
--- a/database/tests/data/postgresql.sql
+++ b/database/tests/data/postgresql.sql
@@ -25,3 +25,11 @@
COUNT(category) AS "count(category)"
FROM ships
GROUP BY category;
+
+CREATE OR REPLACE PROCEDURE define_port (OUT Pout INTEGER)
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+ Pout := 1912;
+END;
+$$;

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 3, 06:32 (8 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3129628
Default Alt Text
D3836.diff (14 KB)

Event Timeline