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