Page MenuHomeDevCentral

D3836.id9940.diff
No OneTemporary

D3836.id9940.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;
@@ -158,7 +159,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 +173,24 @@
$callable($this->queryErrorEvents, [$this, $query, $ex], $ex);
}
+ ///
+ /// PDO features
+ ///
+
+ /**
+ * 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/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,140 @@
+<?php
+
+namespace Keruald\Database\Query;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PDO;
+use PDOStatement;
+
+class PDOQuery extends DatabaseQuery {
+
+ ///
+ /// Private members
+ ///
+
+ private PDOEngine $db;
+
+ private PDOStatement $statement;
+
+ private int $fetchMode = PDO::FETCH_ASSOC;
+
+ ///
+ /// 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 {
+ $result = $this->statement->execute();
+
+ if (!$result) {
+ $this->db->onQueryError($this->statement->queryString);
+ return null;
+ }
+
+ 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);
+
+ 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/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,128 @@
$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);
+ }
}
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,37 @@
+<?php
+
+namespace Keruald\Database\Tests\Query;
+
+use Keruald\Database\Query\PDOQuery;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+
+use PDO;
+
+class PDOQueryTest extends TestCase {
+
+ 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));
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 10, 05:51 (6 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3153493
Default Alt Text
D3836.id9940.diff (10 KB)

Event Timeline