From 88681e7542101e9cb5f58fe8f336093fe2018ec1 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 24 Aug 2026 15:37:41 +1000 Subject: [PATCH 1/2] Implement Result::getQueryResult() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit php-db/phpdb#172 added getQueryResult() to Driver\ResultInterface, so Pgsql\Result no longer satisfied the contract and loading the class was a fatal before any test could run. Follows Pdo\Result upstream: guard on isQueryResult(), clone the given prototype or a default ResultSet, and initialize it from this result. php-db/phpdb 0.6.x-dev requires php ^8.3, so the php constraint, config.platform.php and the PhpStan check in .laminas-ci.json move to 8.3 — the lock cannot pick up the interface change otherwise. Covered by unit tests over a ResultStub that answers the two methods reading the pgsql resource, and by integration tests running a real select and a fields-less statement through the native driver. --- .laminas-ci.json | 2 +- composer.json | 4 +-- composer.lock | 46 +++++++++++++------------- src/Result.php | 23 +++++++++++++ test/asset/ResultStub.php | 32 ++++++++++++++++++ test/integration/ResultTest.php | 57 +++++++++++++++++++++++++++++++++ test/unit/ResultTest.php | 43 +++++++++++++++++++++++++ 7 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 test/asset/ResultStub.php create mode 100644 test/integration/ResultTest.php create mode 100644 test/unit/ResultTest.php diff --git a/.laminas-ci.json b/.laminas-ci.json index 10c530b..a9bb393 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -3,7 +3,7 @@ { "name": "PhpStan", "job": { - "php": "8.2", + "php": "8.3", "dependencies": "latest", "command": "composer require --dev phpstan/phpstan && vendor/bin/phpstan analyse" } diff --git a/composer.json b/composer.json index e97c8a7..5edeaea 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "config": { "sort-packages": true, "platform": { - "php": "8.2.99" + "php": "8.3.99" }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true @@ -30,7 +30,7 @@ } }, "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "php-db/phpdb": "^0.6.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 7e4a8d7..4dc7a80 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7c98d39a182766356052ff8f400e7623", + "content-hash": "8e8b5fceee61a5291e5cafae5b83c5d9", "packages": [ { "name": "brick/varexporter", @@ -57,16 +57,16 @@ }, { "name": "laminas/laminas-servicemanager", - "version": "4.5.0", + "version": "4.5.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-servicemanager.git", - "reference": "a6996829c8ce55025cca1b57b1e8a8b165e3926c" + "reference": "11192d588876ad04ba2988984c77b4ecb5c771c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/a6996829c8ce55025cca1b57b1e8a8b165e3926c", - "reference": "a6996829c8ce55025cca1b57b1e8a8b165e3926c", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/11192d588876ad04ba2988984c77b4ecb5c771c2", + "reference": "11192d588876ad04ba2988984c77b4ecb5c771c2", "shasum": "" }, "require": { @@ -130,7 +130,7 @@ "chat": "https://laminas.dev/chat", "forum": "https://discourse.laminas.dev", "issues": "https://github.com/laminas/laminas-servicemanager/issues", - "source": "https://github.com/laminas/laminas-servicemanager/tree/4.5.0" + "source": "https://github.com/laminas/laminas-servicemanager/tree/4.5.1" }, "funding": [ { @@ -138,7 +138,7 @@ "type": "community_bridge" } ], - "time": "2025-10-14T09:41:04+00:00" + "time": "2026-05-12T09:53:32+00:00" }, { "name": "laminas/laminas-stdlib", @@ -201,20 +201,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -253,9 +252,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "php-db/phpdb", @@ -263,30 +262,29 @@ "source": { "type": "git", "url": "https://github.com/php-db/phpdb.git", - "reference": "81844a5699bf397bf044bbe36ea90be361b4a021" + "reference": "b57f549b411d0b4fe46de8a469983816395be850" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-db/phpdb/zipball/81844a5699bf397bf044bbe36ea90be361b4a021", - "reference": "81844a5699bf397bf044bbe36ea90be361b4a021", + "url": "https://api.github.com/repos/php-db/phpdb/zipball/b57f549b411d0b4fe46de8a469983816395be850", + "reference": "b57f549b411d0b4fe46de8a469983816395be850", "shasum": "" }, "require": { "laminas/laminas-servicemanager": "^3.0.0 || ^4.0.0", "laminas/laminas-stdlib": "^3.20.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + "php": "~8.3.0 || ~8.4.0 || ~8.5.0" }, "conflict": { "laminas/laminas-db": "*", "zendframework/zend-db": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "^3.0.1", + "infection/infection": "^0.34.1", "laminas/laminas-eventmanager": "^3.14.0", "laminas/laminas-hydrator": "^4.6.0", + "php-db/phpdb-qa-tools": "0.1.x-dev", "phpbench/phpbench": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^11.5.42", "rector/rector": "^2.0" }, @@ -324,7 +322,7 @@ "issues": "https://github.com/php-db/phpdb/issues", "source": "https://github.com/php-db/phpdb" }, - "time": "2026-04-13T01:24:01+00:00" + "time": "2026-08-24T01:49:39+00:00" }, { "name": "psr/container", @@ -2615,14 +2613,14 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + "php": "~8.3.0 || ~8.4.0 || ~8.5.0" }, "platform-dev": { "ext-pdo_pgsql": "*", "ext-pgsql": "*" }, "platform-overrides": { - "php": "8.2.99" + "php": "8.3.99" }, "plugin-api-version": "2.9.0" } diff --git a/src/Result.php b/src/Result.php index fb18b10..43e43a9 100644 --- a/src/Result.php +++ b/src/Result.php @@ -7,6 +7,9 @@ use Override; use PgSql\Result as PgSqlResult; use PhpDb\Adapter\Driver\ResultInterface; +use PhpDb\Adapter\Exception; +use PhpDb\ResultSet\ResultSet; +use PhpDb\ResultSet\ResultSetInterface; use function pg_affected_rows; use function pg_fetch_assoc; @@ -94,6 +97,26 @@ public function getGeneratedValue(): int|string|false|null return $this->generatedValue; } + /** + * @throws Exception\RuntimeException When this result is not a query result. + */ + #[Override] + public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface + { + if (! $this->isQueryResult()) { + throw new Exception\RuntimeException( + 'Cannot produce a query result set from a result that is not a query result;' + . ' check isQueryResult() first' + ); + } + + $resultPrototype ??= new ResultSet(); + $resultSet = clone $resultPrototype; + $resultSet->initialize($this); + + return $resultSet; + } + /** * Get resource */ diff --git a/test/asset/ResultStub.php b/test/asset/ResultStub.php new file mode 100644 index 0000000..814e290 --- /dev/null +++ b/test/asset/ResultStub.php @@ -0,0 +1,32 @@ +isQueryResult; + } + + #[Override] + public function getFieldCount(): int + { + return $this->fieldCount; + } +} diff --git a/test/integration/ResultTest.php b/test/integration/ResultTest.php new file mode 100644 index 0000000..e25f435 --- /dev/null +++ b/test/integration/ResultTest.php @@ -0,0 +1,57 @@ +getAdapter()->executeQuery('SELECT id, name, value FROM test'); + + self::assertSame($result->getFieldCount(), $result->getQueryResult()->getFieldCount()); + } + + public function testGetQueryResultIteratesTheSelectedRows(): void + { + $result = $this->getAdapter()->executeQuery('SELECT name FROM test ORDER BY id'); + + $names = []; + foreach ($result->getQueryResult() as $row) { + $names[] = $row['name']; + } + + self::assertSame(['foo', 'bar'], $names); + } + + public function testAStatementReturningNoFieldsIsNotAQueryResult(): void + { + $result = $this->getAdapter()->executeQuery('SET search_path TO public'); + + self::assertFalse($result->isQueryResult()); + } + + public function testGetQueryResultRejectsAStatementThatReturnsNoFields(): void + { + $result = $this->getAdapter()->executeQuery('SET search_path TO public'); + + $this->expectException(Exception\RuntimeException::class); + $this->expectExceptionMessage( + 'Cannot produce a query result set from a result that is not a query result;' + . ' check isQueryResult() first' + ); + + $result->getQueryResult(); + } +} diff --git a/test/unit/ResultTest.php b/test/unit/ResultTest.php new file mode 100644 index 0000000..0300f96 --- /dev/null +++ b/test/unit/ResultTest.php @@ -0,0 +1,43 @@ +getQueryResult(); + + self::assertSame(3, $resultSet->getFieldCount()); + } + + public function testGetQueryResultClonesTheGivenPrototype(): void + { + $prototype = new ResultSet(); + + $resultSet = (new ResultStub(true, 1))->getQueryResult($prototype); + + self::assertNotSame($prototype, $resultSet); + } + + public function testGetQueryResultRejectsAResultThatIsNotAQueryResult(): void + { + $this->expectException(Exception\RuntimeException::class); + $this->expectExceptionMessage( + 'Cannot produce a query result set from a result that is not a query result;' + . ' check isQueryResult() first' + ); + + (new ResultStub(false))->getQueryResult(); + } +} From f7ccb24e12460a2170d7d4d96eabb59f869172ea Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 24 Aug 2026 15:42:36 +1000 Subject: [PATCH 2/2] Require phpstan ^2.2 and drop the two stale implode baseline entries The PhpStan check in .laminas-ci.json runs on latest dependencies, so it picks up phpstan 2.2, which no longer reports the two implode argument.type errors in Metadata\Source. The baseline still ignored them, so the job failed on ignore.unmatched, which is non-ignorable. 2.1 does still report them, so the entries cannot simply be removed while the lock stays on 2.1; require-dev moves to ^2.2 so both the locked and latest jobs analyse with a version that agrees. Pre-existing on 0.1.x, and unrelated to getQueryResult, but it fails this PR's CI. --- composer.json | 2 +- composer.lock | 21 ++++++++++++++++----- phpstan-baseline.neon | 11 ----------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 5edeaea..538edda 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-pdo_pgsql": "*", "ext-pgsql": "*", "laminas/laminas-coding-standard": "^3.0.1", - "phpstan/phpstan": "^2.1", + "phpstan/phpstan": "^2.2", "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^11.5.42" }, diff --git a/composer.lock b/composer.lock index 4dc7a80..0e7a753 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8e8b5fceee61a5291e5cafae5b83c5d9", + "content-hash": "75ac9cd93692f950a839f5ff317be05b", "packages": [ { "name": "brick/varexporter", @@ -755,11 +755,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.2.9", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/13d6b4f347bad222da436580c8304fa6f83e6bd0", + "reference": "13d6b4f347bad222da436580c8304fa6f83e6bd0", "shasum": "" }, "require": { @@ -782,6 +782,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -804,7 +815,7 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-08-22T07:38:16+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ddb8f5c..933345b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,14 +6,3 @@ parameters: count: 1 path: src/Container/ConnectionInterfaceFactory.php - - - message: '#^Parameter \#2 \$array of function implode expects array\, array\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Metadata/Source.php - - - - message: '#^Parameter \#2 \$array of function implode expects array\, array\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Metadata/Source.php