Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [11.1.0] - 2026-07-02

### Summary
Minor release completing the M3 resource-to-object migration. All handle-returning
`fbird_*` functions now return typed `Firebird\*` objects. No raw resources are
returned to userland. The dual-accept bridge accepts both legacy resources and
new objects in all consuming functions — no consumer-side changes required.

### Fixed
- **[Issue #294]** `fbird_query($conn, $sql)` autocommit mode didn't see data committed
by other transactions. The default transaction was cached in `ib_link->tr_list->trans`
and reused across all autocommit calls without ever being committed, freezing the
snapshot at the time of the first query. Fix: true autocommit for non-persistent
connections — DML commits immediately after execution, SELECT commits when the result
is freed. This unblocks `doctrine-firebird-driver`'s `lastInsertId()` which queries
`RDB$RELATION_FIELDS` via autocommit. (`fbird_query_exec.c`, `fbird_query_prepare.c`)
- **[Issue #295]** SIGSEGV (exit code 139) during PHP shutdown after persistent connection
cleanup. `Transaction::commit()`, `rollback()`, and `rollbackNoThrow()` in
`src/cpp/fb_transaction.hpp` guarded on the cached `master_` member instead of calling
`getMaster()`. During MSHUTDOWN, `getMaster()` returns `nullptr` but `master_` was never
nulled, bypassing the guard and making a server-side call on a dead attachment. Same
class of bug as commit `881d375` fixed for `Connection::detachNoThrow()`. Fix: replace
`master_` with `getMaster()` in all three methods. Added `in_mshutdown` guard in
`fbird_transaction_free` as belt-and-suspenders. (`src/cpp/fb_transaction.hpp`,
`fbird_classes.c`)
- **[Issue #296]** `fbird_query()` stub declared `\Firebird\ResultSet` return type but
C code returned raw `resource`. Same issue affected `fbird_execute_query()` and
`fbird_query_params_tx()`. Fix: added `fbird_setup_resultset_object()` wrapping block
to all three functions (same pattern as `fbird_execute()`). (`fbird_query_exec.c`)

### Added
- **[Issue #297]** `fbird_prepare()` and `fbird_prepare_ex()` now return `Firebird\Statement`
objects instead of raw `resource` handles. Completes the M3 resource-to-object migration
— all handle-returning `fbird_*` functions now return typed objects. Added
`fbird_setup_statement_object()` and `fbird_statement_get_resource()` helpers.
`Firebird\Statement` branch added to `FBIRD_VALIDATE_QUERY_EX` macro and `fbird_execute()`
type validation. OOP `Connection::prepare()` and `Statement::execute()` refactored to
call internal C functions directly (avoids `call_user_function` segfault).
(`fbird_classes.c`, `fbird_classes.h`, `fbird_query_exec.c`, `php_fbird_includes.h`)
- `is_persistent` field added to `fbird_db_link` struct to distinguish persistent from
non-persistent connections in autocommit logic.

### Tests
- Added `tests/issue294_autocommit_visibility.phpt` — autocommit sees committed data.
- Added `tests/issue295_mshutdown_pconnect_sigsegv.phpt` — MSHUTDOWN cleanup with default tx.
- Added `tests/issue296_resultset_return_type.phpt` — all SELECT-returning functions return
`Firebird\ResultSet` objects.
- Added `tests/issue297_statement_return_type.phpt` — `fbird_prepare/ex` return
`Firebird\Statement` objects, dual-accept bridge works with `fbird_execute()`.
- Updated 7 existing tests to use `instanceof` checks instead of `is_resource()`.

### Additional Fixes
- `fbird_commit()` and `fbird_rollback()` on the default link are now silent no-ops
(return `true`) when the default transaction was already committed by autocommit.
- `fbird_execute()` on prepared statements now restarts the default transaction if
it was committed by a prior autocommit DML call.
- Autocommit commit in `php_fbird_free_query_rsrc` guarded to only fire for the
default transaction (first `tr_list` node), preventing use-after-free in
`fbird_execute_auto()` temp transactions.

### Breaking Changes (v11.1)
- Return types of `fbird_prepare()` and `fbird_prepare_ex()` changed from `resource` to
`Firebird\Statement`. Code using `is_resource()` checks will need updating to
`$x instanceof \Firebird\Statement`.
- `fbird_query()`, `fbird_execute_query()`, `fbird_query_params_tx()` now return
`Firebird\ResultSet` objects for SELECT (was `resource`). The stubs already declared
this type since v11.0.0 — this fix makes the C code match the stubs.
- `fbird_commit()`/`fbird_rollback()` on already-committed default transaction now
silently returns `true` (was: warning + `false`).

## [11.0.1] - 2026-04-20

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11.0.1
11.1.0
106 changes: 67 additions & 39 deletions fbird_classes.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "php_fbird_includes.h"
#include "firebird_utils.h"
#include "fbird_classes.h"
#include "php_fbird_query_prepare.h"
#include "php_fbird_connection.h"
#include "php_fbird_query_internal.h"

Expand Down Expand Up @@ -210,8 +211,13 @@ static void fbird_transaction_free(zend_object *obj)
{
fbird_transaction_obj *intern = fbird_transaction_from_obj(obj);
if (intern->fbt_trans) {
ISC_STATUS sv[20];
fbt_rollback(intern->fbt_trans, sv);
/* Issue #295: Skip server-side rollback during MSHUTDOWN to prevent
* SIGSEGV on dead attachments. Transaction::rollbackNoThrow() already
* guards via getMaster(), but this avoids the call entirely. */
if (!IBG(in_mshutdown)) {
ISC_STATUS sv[20];
fbt_rollback(intern->fbt_trans, sv);
}
fbt_free(intern->fbt_trans);
intern->fbt_trans = NULL;
}
Expand Down Expand Up @@ -470,36 +476,37 @@ PHP_METHOD(FirebirdStatement, execute)
ZEND_PARSE_PARAMETERS_END();

fbird_statement_obj *intern = Z_FBIRD_STATEMENT_P(ZEND_THIS);
if (!intern->query_res) {
zend_throw_exception(fbird_query_exception_ce, "Statement not prepared", 0);
if (!intern->query_res || intern->query_res->type <= 0) {
zend_throw_exception(fbird_query_exception_ce, "Statement not prepared or freed", 0);
RETURN_THROWS();
}

/* Call fbird_execute($query_res) */
zval res_zv, retval;
ZVAL_RES(&res_zv, intern->query_res);
GC_ADDREF(intern->query_res);
fbird_call_fn("fbird_execute", &res_zv, 1, &retval);
zval_ptr_dtor(&res_zv);
/* Issue #297: Call _php_fbird_exec() directly instead of going through
* fbird_execute() via call_user_function. This avoids argument validation
* overhead and segfaults from call_user_function context issues. */
fbird_query *ib_query = (fbird_query *)intern->query_res->ptr;
if (!ib_query) {
zend_throw_exception(fbird_query_exception_ce, "Statement has no query data", 0);
RETURN_THROWS();
}

if (Z_TYPE(retval) == IS_FALSE) {
zval_ptr_dtor(&retval);
RETVAL_FALSE;
if (FAILURE == _php_fbird_exec(INTERNAL_FUNCTION_PARAM_PASSTHRU, ib_query, NULL, 0)) {
zend_throw_exception(fbird_query_exception_ce, "Failed to execute statement", 0);
RETURN_THROWS();
}

/* Return a ResultSet wrapping the same query resource */
object_init_ex(return_value, fbird_resultset_ce);
fbird_resultset_obj *rs = Z_FBIRD_RESULTSET_P(return_value);
if (Z_TYPE(retval) == IS_RESOURCE) {
rs->query_res = Z_RES(retval);
GC_ADDREF(rs->query_res);
zval_ptr_dtor(&retval);
} else {
/* Non-SELECT: reuse the prepared statement resource for fetch (returns false) */
/* Issue #296: 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) {
fbird_setup_resultset_object(return_value, Z_RES_P(return_value));
} else if (Z_TYPE_P(return_value) != IS_RESOURCE) {
/* Non-SELECT (DML): return a ResultSet wrapping the statement resource
* (fetch will return false — standard behavior for DML results) */
object_init_ex(return_value, fbird_resultset_ce);
fbird_resultset_obj *rs = Z_FBIRD_RESULTSET_P(return_value);
rs->query_res = intern->query_res;
GC_ADDREF(rs->query_res);
zval_ptr_dtor(&retval);
}
}

Expand Down Expand Up @@ -549,27 +556,20 @@ PHP_METHOD(FirebirdConnection, prepare)
RETURN_THROWS();
}

/* Call fbird_prepare($conn_res, $tr_res, $sql) */
zval prep_args[3], retval;
ZVAL_RES(&prep_args[0], conn->conn_res);
GC_ADDREF(conn->conn_res);
ZVAL_RES(&prep_args[1], tr_res);
GC_ADDREF(tr_res);
ZVAL_STRINGL(&prep_args[2], sql, sql_len);
fbird_call_fn("fbird_prepare", prep_args, 3, &retval);
for (int i = 0; i < 3; i++) zval_ptr_dtor(&prep_args[i]);

if (Z_TYPE(retval) != IS_RESOURCE) {
zval_ptr_dtor(&retval);
/* Issue #297: Call _php_fbird_prepare() directly instead of through
* fbird_prepare()/call_user_function. This avoids the overhead and
* segfault risk of call_user_function in OOP method context. */
fbird_db_link *link = (fbird_db_link *)conn->conn_res->ptr;
fbird_transaction *trans = (fbird_transaction *)tr_res->ptr;
fbird_query *ib_query = NULL;

if (FAILURE == _php_fbird_prepare(&ib_query, link, trans, tr_res, sql)) {
zend_throw_exception(fbird_query_exception_ce, "Failed to prepare statement", 0);
RETURN_THROWS();
}

object_init_ex(return_value, fbird_statement_ce);
fbird_statement_obj *stmt = Z_FBIRD_STATEMENT_P(return_value);
stmt->query_res = Z_RES(retval);
GC_ADDREF(stmt->query_res);
zval_ptr_dtor(&retval);
/* Wrap the le_query resource in a Firebird\Statement object */
fbird_setup_statement_object(return_value, ib_query->res);
}

/* -----------------------------------------------------------------------
Expand Down Expand Up @@ -1298,6 +1298,17 @@ zend_resource *fbird_resultset_get_resource(zend_object *obj)
return intern ? intern->query_res : NULL;
}

/* -----------------------------------------------------------------------
* fbird_statement_get_resource() — extract zend_resource* from a
* Firebird\Statement internal object. Returns NULL if no resource set.
* Used by the dual-accept bridge in fbird_execute(), fbird_free_query(), etc.
* --------------------------------------------------------------------- */
zend_resource *fbird_statement_get_resource(zend_object *obj)
{
fbird_statement_obj *intern = fbird_statement_from_obj(obj);
return intern ? intern->query_res : NULL;
}

/* -----------------------------------------------------------------------
* fbird_setup_resultset_object() — glue from procedural fbird_query() /
* fbird_execute() to Firebird\ResultSet
Expand All @@ -1317,6 +1328,23 @@ void fbird_setup_resultset_object(zval *return_value, zend_resource *res)
intern->query_res = res;
}

/* -----------------------------------------------------------------------
* fbird_setup_statement_object() — glue from procedural fbird_prepare() /
* fbird_prepare_ex() to Firebird\Statement
*
* Same pattern as fbird_setup_resultset_object(): wraps the le_query
* resource in a typed Firebird\Statement object. The resource stays in
* EG(regular_list); we store a weak reference.
* --------------------------------------------------------------------- */
void fbird_setup_statement_object(zval *return_value, zend_resource *res)
{
zval_ptr_dtor(return_value);
object_init_ex(return_value, fbird_statement_ce);
fbird_statement_obj *intern = fbird_statement_from_obj(Z_OBJ_P(return_value));
GC_ADDREF(res);
intern->query_res = res;
}

/* -----------------------------------------------------------------------
* Registration entry point called from PHP_MINIT_FUNCTION(fbird)
* --------------------------------------------------------------------- */
Expand Down
12 changes: 12 additions & 0 deletions fbird_classes.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,24 @@ void fbird_setup_transaction_object(zval *return_value, zend_resource *res);
*/
zend_resource *fbird_resultset_get_resource(zend_object *obj);

/**
* Extract the zend_resource* from a Firebird\Statement object.
* Returns NULL if obj is not a Firebird\Statement or has no resource.
*/
zend_resource *fbird_statement_get_resource(zend_object *obj);

/**
* Wrap a le_query zend_resource* in a Firebird\ResultSet object.
* Stores the resource as a weak reference (EG(regular_list) owns it).
*/
void fbird_setup_resultset_object(zval *return_value, zend_resource *res);

/**
* Wrap a le_query zend_resource* in a Firebird\Statement object.
* Same weak-reference pattern as fbird_setup_resultset_object().
*/
void fbird_setup_statement_object(zval *return_value, zend_resource *res);

/**
* Extract the zend_resource* from a Firebird\Blob object (M3 Phase G bridge).
* Returns NULL if no resource is set (OOP-native path).
Expand Down
1 change: 1 addition & 0 deletions fbird_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ zend_resource *_php_fbird_connect_link(
ib_link->dialect = largs[DLECT] ? (unsigned short)largs[DLECT] : SQL_DIALECT_CURRENT;
ib_link->tr_list = NULL;
ib_link->event_head = NULL;
ib_link->is_persistent = persistent;

ib_link->fbc_connection = connection_ptr;

Expand Down
Loading
Loading