Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F12397989
D3836.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D3836.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D3836: Improve PDO support with prepare and bind
Attached
Detach File
Event Timeline
Log In to Comment