Description
The fbird_query() function stub in stubs/firebird-stubs.php declares:
function fbird_query(mixed $link_or_query, mixed ...$args): \Firebird\ResultSet|int|bool {}
But the C implementation in fbird_query_exec.c (PHP_FUNCTION(fbird_query), line 1011) returns a raw resource for SELECT queries, not a Firebird\ResultSet object.
This is a stub/C mismatch — the only function in the M3 migration where the declared return type doesn't match the runtime behavior.
Root Cause
PHP_FUNCTION(fbird_execute) (line 1382) correctly wraps the result in a Firebird\ResultSet object:
// fbird_query_exec.c:1430-1437 (inside PHP_FUNCTION(fbird_execute))
/* M3 Phase G: wrap le_query result in Firebird\ResultSet */
if (Z_TYPE_P(return_value) == IS_RESOURCE &&
Z_RES_TYPE_P(return_value) == le_query) {
zend_resource *_res = Z_RES_P(return_value);
fbird_setup_resultset_object(return_value, _res);
}
But PHP_FUNCTION(fbird_query) (line 1011) never calls fbird_setup_resultset_object(). After _php_fbird_exec() returns (which sets return_value via RETVAL_RES() at lines 509, 677, or 877), the function does ownership transfer (lines 1190-1210) and returns — leaving return_value as a raw resource.
Verification
<?php
$conn = fbird_connect('localhost:/var/lib/firebird/data/test.fdb', 'SYSDBA', 'masterkey');
// fbird_query() — returns resource (WRONG: stub says Firebird\ResultSet)
$r1 = fbird_query($conn, 'SELECT 1 FROM RDB$DATABASE');
var_dump(gettype($r1)); // string(8) "resource"
var_dump($r1 instanceof \Firebird\ResultSet); // bool(false)
// fbird_execute() — returns object (CORRECT)
$stmt = fbird_prepare($conn, 'SELECT 1 FROM RDB$DATABASE');
$r2 = fbird_execute($stmt);
var_dump(gettype($r2)); // string(6) "object"
var_dump($r2 instanceof \Firebird\ResultSet); // bool(true)
fbird_close($conn);
Impact
-
Static analysis (PHPStan/Psalm) trusts the stub and assumes fbird_query() returns Firebird\ResultSet. Code that checks is_resource() on the return value is flagged as "always false" — but at runtime it's true.
-
doctrine-firebird-driver cannot use instanceof \Firebird\ResultSet for result validation — it must use the broader is_object() check, which also accepts unrelated objects.
-
Consistency: fbird_execute() wraps results in objects but fbird_query() doesn't, despite both calling the same _php_fbird_exec() internally. This forces consumers to handle the two functions differently.
Suggested Fix
Add the same wrapping block that PHP_FUNCTION(fbird_execute) uses, at the end of PHP_FUNCTION(fbird_query) (after the ownership transfer block, before the final efree(args)):
// In PHP_FUNCTION(fbird_query), after the existing ownership transfer
// block (around line 1210), add:
/* M3: wrap le_query result in Firebird\ResultSet (same as fbird_execute) */
if (Z_TYPE_P(return_value) == IS_RESOURCE &&
Z_RES_TYPE_P(return_value) == le_query) {
zend_resource *_res = Z_RES_P(return_value);
fbird_setup_resultset_object(return_value, _res);
}
The same block should also be added to:
PHP_FUNCTION(fbird_execute_query) (line 1589) — after the ownership transfer block
PHP_FUNCTION(fbird_query_params_tx) (line 1784) — after the ownership transfer block
PHP_FUNCTION(fbird_execute_auto) (line 1661) — before the ZVAL_COPY_VALUE restore
Environment
- php-firebird v11.0.1 (tag v11.0.1, commit 58775ee)
- PHP 8.4.22
- Firebird 3.0.14
Description
The
fbird_query()function stub instubs/firebird-stubs.phpdeclares:But the C implementation in
fbird_query_exec.c(PHP_FUNCTION(fbird_query), line 1011) returns a rawresourcefor SELECT queries, not aFirebird\ResultSetobject.This is a stub/C mismatch — the only function in the M3 migration where the declared return type doesn't match the runtime behavior.
Root Cause
PHP_FUNCTION(fbird_execute)(line 1382) correctly wraps the result in aFirebird\ResultSetobject:But
PHP_FUNCTION(fbird_query)(line 1011) never callsfbird_setup_resultset_object(). After_php_fbird_exec()returns (which setsreturn_valueviaRETVAL_RES()at lines 509, 677, or 877), the function does ownership transfer (lines 1190-1210) and returns — leavingreturn_valueas a raw resource.Verification
Impact
Static analysis (PHPStan/Psalm) trusts the stub and assumes
fbird_query()returnsFirebird\ResultSet. Code that checksis_resource()on the return value is flagged as "always false" — but at runtime it'strue.doctrine-firebird-driver cannot use
instanceof \Firebird\ResultSetfor result validation — it must use the broaderis_object()check, which also accepts unrelated objects.Consistency:
fbird_execute()wraps results in objects butfbird_query()doesn't, despite both calling the same_php_fbird_exec()internally. This forces consumers to handle the two functions differently.Suggested Fix
Add the same wrapping block that
PHP_FUNCTION(fbird_execute)uses, at the end ofPHP_FUNCTION(fbird_query)(after the ownership transfer block, before the finalefree(args)):The same block should also be added to:
PHP_FUNCTION(fbird_execute_query)(line 1589) — after the ownership transfer blockPHP_FUNCTION(fbird_query_params_tx)(line 1784) — after the ownership transfer blockPHP_FUNCTION(fbird_execute_auto)(line 1661) — before theZVAL_COPY_VALUErestoreEnvironment