From cf426835fad09a493456b1e30f603b90b0f03122 Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 17:03:08 +0100 Subject: [PATCH 1/6] #86 feat(*): use unbuffered result queries --- src/Driver/MysqliDriver.php | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Driver/MysqliDriver.php b/src/Driver/MysqliDriver.php index 2801720a..e962c46b 100755 --- a/src/Driver/MysqliDriver.php +++ b/src/Driver/MysqliDriver.php @@ -196,17 +196,29 @@ public function getPArray(string $query, array $params): Generator throw new QueryException('Could not execute statement: ' . $this->getError(), $query, $params); } - $result = $statement->get_result(); - - if ($result === false) { - return; + $meta = $statement->result_metadata(); + if ($meta === false) { + throw new QueryException('Could not get result meta', $query, $params); } - while ($row = $result->fetch_assoc()) { - yield $row; - } + $columnNames = array_column($meta->fetch_fields(), 'name'); + + $meta->free(); + + $boundValues = array_fill(0, count($columnNames), null); - $result->free_result(); + $refs = &$boundValues; + + $statement->bind_result(...$refs); + + while ($statement->fetch()) { + $values = []; + foreach ($boundValues as $value) { + $values[] = $value; + } + + yield array_combine($columnNames, $values); + } } /** From 8bf10ae8694793566368d14cbb2458955fc95241 Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 17:22:06 +0100 Subject: [PATCH 2/6] #86 feat(*): fix phpstan --- src/Connection.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index a42310b8..3234576f 100755 --- a/src/Connection.php +++ b/src/Connection.php @@ -1222,11 +1222,11 @@ public function getCacheSize(): int * An internal wrapper to dbsafeString, used to process a complete array of parameters * as used by prepared statements. * - * @param array $params + * @param array $params * @param list|false $escapes An array of boolean for each param, used to block the escaping of html-special chars. * If not passed, all params will be cleaned. * - * @return list + * @return list * * @see Db::dbsafeString($string, $htmlSpecialChars = true) */ @@ -1257,6 +1257,7 @@ private function dbsafeParams(array $params, array | false $escapes = []): array $replace[$key] = $param; } + /** @var list */ return array_values($replace); } From 59237b56a058306d3e09ad6b1edba7678faeb081 Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 17:26:29 +0100 Subject: [PATCH 3/6] #86 feat(*): ignore prepared statements cache for getPArray since resuing statements make only sense for insert or update queries, this prevents also many statement out-of-sync errors --- src/Driver/MysqliDriver.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Driver/MysqliDriver.php b/src/Driver/MysqliDriver.php index e962c46b..0bef5e21 100755 --- a/src/Driver/MysqliDriver.php +++ b/src/Driver/MysqliDriver.php @@ -177,7 +177,7 @@ public function _pQuery(string $query, array $params): bool #[Override] public function getPArray(string $query, array $params): Generator { - $statement = $this->getPreparedStatement($query); + $statement = $this->getPreparedStatement($query, true); $types = ''; if ($statement === false) { @@ -658,11 +658,11 @@ private function getPreparedStatementName(string $query): string /** * Prepares a statement or uses an instance from the cache. */ - private function getPreparedStatement(string $query): false | mysqli_stmt + private function getPreparedStatement(string $query, bool $ignoreCache = false): false | mysqli_stmt { $name = $this->getPreparedStatementName($query); - if (isset($this->statementsCache[$name])) { + if ($ignoreCache === false && isset($this->statementsCache[$name])) { return $this->statementsCache[$name]; } From 30922b09e861933771e0e53610550ab553c8cacc Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 17:42:08 +0100 Subject: [PATCH 4/6] #86 feat(*): skip test if pg_dump is not available --- tests/Driver/PostgresDriverTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Driver/PostgresDriverTest.php b/tests/Driver/PostgresDriverTest.php index d6817e31..8c3cf976 100644 --- a/tests/Driver/PostgresDriverTest.php +++ b/tests/Driver/PostgresDriverTest.php @@ -7,8 +7,10 @@ use Artemeon\Database\ConnectionParameters; use Artemeon\Database\Driver\PostgresDriver; use Mockery; +use Override; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; /** @@ -16,6 +18,15 @@ */ final class PostgresDriverTest extends TestCase { + #[Override] + protected function setUp(): void + { + $dumpBin = new ExecutableFinder()->find('pg_dump'); + if ($dumpBin === null) { + self::markTestSkipped('pg_dump not available'); + } + } + public function testBuildsDatabaseSpecificSubstringExpression(): void { $postgresDriver = new PostgresDriver(); From b450af7b3c9f174b156f9e668adfd641138ec02c Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 17:42:34 +0100 Subject: [PATCH 5/6] #86 feat(*): improve style --- tests/ConnectionTestCase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ConnectionTestCase.php b/tests/ConnectionTestCase.php index fb8e59c6..97fed1be 100644 --- a/tests/ConnectionTestCase.php +++ b/tests/ConnectionTestCase.php @@ -19,6 +19,7 @@ use Artemeon\Database\Exception\ConnectionException; use Artemeon\Database\Exception\QueryException; use Artemeon\Database\Schema\DataType; +use Override; use PHPUnit\Framework\TestCase; abstract class ConnectionTestCase extends TestCase @@ -27,7 +28,7 @@ abstract class ConnectionTestCase extends TestCase protected const string TEST_TABLE_NAME = 'agp_test_table'; - #[\Override] + #[Override] protected function setUp(): void { parent::setUp(); @@ -36,7 +37,7 @@ protected function setUp(): void $this->setupFixture(); } - #[\Override] + #[Override] protected function tearDown(): void { parent::tearDown(); From 4cca394c61f119c2c32802778cb4aa7bf0013bc5 Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 12 Feb 2026 18:09:58 +0100 Subject: [PATCH 6/6] #86 feat(*): use store result and remove cache --- src/Driver/MysqliDriver.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Driver/MysqliDriver.php b/src/Driver/MysqliDriver.php index 0bef5e21..a4e3f640 100755 --- a/src/Driver/MysqliDriver.php +++ b/src/Driver/MysqliDriver.php @@ -177,7 +177,7 @@ public function _pQuery(string $query, array $params): bool #[Override] public function getPArray(string $query, array $params): Generator { - $statement = $this->getPreparedStatement($query, true); + $statement = $this->getPreparedStatement($query); $types = ''; if ($statement === false) { @@ -205,6 +205,8 @@ public function getPArray(string $query, array $params): Generator $meta->free(); + $statement->store_result(); + $boundValues = array_fill(0, count($columnNames), null); $refs = &$boundValues; @@ -658,11 +660,11 @@ private function getPreparedStatementName(string $query): string /** * Prepares a statement or uses an instance from the cache. */ - private function getPreparedStatement(string $query, bool $ignoreCache = false): false | mysqli_stmt + private function getPreparedStatement(string $query): false | mysqli_stmt { $name = $this->getPreparedStatementName($query); - if ($ignoreCache === false && isset($this->statementsCache[$name])) { + if (isset($this->statementsCache[$name])) { return $this->statementsCache[$name]; }