diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 396c8a0e4..cc51bdcd7 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -1217,7 +1217,7 @@ int pdo_sqlsrv_stmt_next_rowset( _Inout_ pdo_stmt_t *stmt ) SQLSRV_ASSERT( driver_stmt != NULL, "pdo_sqlsrv_stmt_next_rowset: driver_data object was null" ); - core_sqlsrv_next_result( static_cast( stmt->driver_data ) ); + core_sqlsrv_next_result( static_cast( stmt->driver_data ), true, false ); if( driver_stmt->past_next_result_end == true ) { // Clean up remaining metadata since new_result_set() was not called diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index bc8e940da..69bb72eab 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -18,6 +18,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- #include "core_sqlsrv.h" +#include "zend_exceptions.h" #include #include @@ -982,11 +983,40 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp zend_hash_clean( Z_ARRVAL( stmt->col_cache )); SQLRETURN r; + if( throw_on_errors ) { + // Use the throwing wrapper. This is the default path used by + // internal flush loops (re-execute, closeCursor, param binding) + // where SQL_ERROR should abort the loop via exception. r = core::SQLMoreResults( stmt ); } else { - r = SQLMoreResults( stmt->handle() ); + // User-facing path (sqlsrv_next_result / PDO::nextRowset). + // The ODBC driver may return SQL_ERROR instead of + // SQL_SUCCESS_WITH_INFO for a failed statement that is not the + // last in a non-aborted batch (e.g. divide-by-zero with + // XACT_ABORT OFF). In that case the statement handle is still + // valid and subsequent result sets remain reachable. We report + // the error through the normal handler but do NOT throw, so the + // caller sees success and the user can keep advancing. + r = ::SQLMoreResults( stmt->handle() ); + + SQLSRV_ASSERT( r != SQL_INVALID_HANDLE, "Invalid handle returned from SQLMoreResults." ); + + if( r == SQL_ERROR ) { + // Report the ODBC error so it is visible via + // sqlsrv_errors() / PDOStatement::errorInfo(). + call_error_handler( stmt, SQLSRV_ERROR_ODBC, /*warning*/0 ); + // The PDO error handler may set a pending zend exception + // when the error mode is PDO::ERRMODE_EXCEPTION. Clear it + // so execution continues to new_result_set() and the batch + // remains navigable. The error is still available via + // errorInfo(). + zend_clear_exception(); + } + else if( r == SQL_SUCCESS_WITH_INFO ) { + call_error_handler( stmt, SQLSRV_ERROR_ODBC, /*warning*/1 ); + } } if( r == SQL_NO_DATA ) { @@ -1005,7 +1035,15 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp } catch( core::CoreException& e ) { - SQLCancel( stmt->handle() ); + // For internal callers (throw_on_errors=true — flush loops, + // closeCursor, param binding) we still call SQLCancel to clean up + // the ODBC handle on error. For user-facing callers + // (throw_on_errors=false — sqlsrv_next_result / PDO::nextRowset) + // we must NOT cancel because the handle is still valid and the + // batch remains navigable after a mid-batch statement failure. + if( throw_on_errors ) { + SQLCancel( stmt->handle() ); + } throw e; } } diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 74cf86b7e..1d7077243 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -569,7 +569,7 @@ PHP_FUNCTION( sqlsrv_next_result ) try { - core_sqlsrv_next_result( stmt, true ); + core_sqlsrv_next_result( stmt, true, false ); if( stmt->past_next_result_end ) { // Clean up remaining metadata since new_result_set() was not called diff --git a/test/functional/pdo_sqlsrv/pdo_batch_error_continue.phpt b/test/functional/pdo_sqlsrv/pdo_batch_error_continue.phpt new file mode 100644 index 000000000..399c0af0e --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_batch_error_continue.phpt @@ -0,0 +1,108 @@ +--TEST-- +PDO nextRowset continues after mid-batch error with XACT_ABORT OFF +--DESCRIPTION-- +When a multi-statement batch contains a failing statement (e.g. divide-by-zero) +with XACT_ABORT OFF, nextRowset() should report the error but still allow +subsequent nextRowset() calls to reach the remaining result sets. Previously +the driver called SQLCancel() which destroyed the remaining batch. + +This test verifies both ERRMODE_WARNING and ERRMODE_EXCEPTION modes. +--SKIPIF-- + +--FILE-- +query($batch); + + // Result set 1 + $row = $stmt->fetch(PDO::FETCH_ASSOC); + echo "Result set 1: n={$row['n']}\n"; + + // Advance past failing SELECT 1/0 — returns true (batch not aborted) + $next = $stmt->nextRowset(); + echo "nextRowset (failing): "; + var_dump($next); + + $err = $stmt->errorInfo(); + echo "Error SQLSTATE: {$err[0]}\n"; + + // Advance to result set 3 + $next2 = $stmt->nextRowset(); + echo "nextRowset (SELECT 2): "; + var_dump($next2); + + $row2 = $stmt->fetch(PDO::FETCH_ASSOC); + echo "Result set 3: n={$row2['n']}\n"; + + $stmt = null; + $conn = null; +} catch (PDOException $e) { + echo "UNEXPECTED Exception: " . $e->getMessage() . "\n"; +} + +// ============================================================ +// Test 2: ERRMODE_EXCEPTION — exception cleared internally, +// batch remains navigable, error in errorInfo() +// ============================================================ +echo "\n=== Test 2: ERRMODE_EXCEPTION ===\n"; + +try { + $conn = connect("", array(), PDO::ERRMODE_EXCEPTION); + $stmt = $conn->query($batch); + + // Result set 1 + $row = $stmt->fetch(PDO::FETCH_ASSOC); + echo "Result set 1: n={$row['n']}\n"; + + // Advance past failing SELECT 1/0 — the driver clears the pending + // exception internally so that the batch remains navigable. + $next = $stmt->nextRowset(); + echo "nextRowset (failing): "; + var_dump($next); + + // Error info is still populated even though the exception was cleared. + $err = $stmt->errorInfo(); + echo "Error SQLSTATE: {$err[0]}\n"; + + // Advance to result set 3 + $next2 = $stmt->nextRowset(); + echo "nextRowset (SELECT 2): "; + var_dump($next2); + + $row2 = $stmt->fetch(PDO::FETCH_ASSOC); + echo "Result set 3: n={$row2['n']}\n"; + + $stmt = null; + $conn = null; +} catch (PDOException $e) { + echo "UNEXPECTED Exception: " . $e->getMessage() . "\n"; +} + +echo "\nDone\n"; +?> +--EXPECTF-- +=== Test 1: ERRMODE_WARNING === +Result set 1: n=1 +nextRowset (failing): bool(true) +Error SQLSTATE: 22012 +nextRowset (SELECT 2): bool(true) +Result set 3: n=2 + +=== Test 2: ERRMODE_EXCEPTION === +Result set 1: n=1 +nextRowset (failing): bool(true) +Error SQLSTATE: 22012 +nextRowset (SELECT 2): bool(true) +Result set 3: n=2 + +Done diff --git a/test/functional/sqlsrv/sqlsrv_batch_error_continue.phpt b/test/functional/sqlsrv/sqlsrv_batch_error_continue.phpt new file mode 100644 index 000000000..bad772a86 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_batch_error_continue.phpt @@ -0,0 +1,110 @@ +--TEST-- +sqlsrv_next_result continues after mid-batch error with XACT_ABORT OFF +--DESCRIPTION-- +When a multi-statement batch contains a failing statement (e.g. divide-by-zero) +with XACT_ABORT OFF, sqlsrv_next_result() should report the error but still +allow the user to advance to subsequent result sets. Previously the driver +called SQLCancel() which destroyed the remaining batch. + +This test also verifies: +- Error information is available via sqlsrv_errors() +- Re-executing the statement (flush loop) works after batch errors +--SKIPIF-- + +--FILE-- + +--EXPECT-- +=== Test 1: XACT_ABORT OFF === +Result set 1: n=1 +next_result (failing): bool(true) +Error SQLSTATE: 22012 +next_result (SELECT 2): bool(true) +Result set 3: n=2 +next_result (end): NULL + +=== Test 2: Re-execute after error === +First execute, result set 1: n=1 +Re-execute: bool(true) +Second execute, result set 1: n=1 + +Done