Skip to content
Open
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
2 changes: 1 addition & 1 deletion source/pdo_sqlsrv/pdo_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<sqlsrv_stmt*>( stmt->driver_data ) );
core_sqlsrv_next_result( static_cast<sqlsrv_stmt*>( stmt->driver_data ), true, false );

if( driver_stmt->past_next_result_end == true ) {
// Clean up remaining metadata since new_result_set() was not called
Expand Down
42 changes: 40 additions & 2 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//---------------------------------------------------------------------------------------------------------------------------------

#include "core_sqlsrv.h"
#include "zend_exceptions.h"

#include <sstream>
#include <vector>
Expand Down Expand Up @@ -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 ) {
Expand All @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion source/sqlsrv/stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 108 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_batch_error_continue.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");

$batch = "SET XACT_ABORT OFF; SELECT 1 AS n; SELECT 1/0 AS boom; SELECT 2 AS n;";

// ============================================================
// Test 1: ERRMODE_WARNING — error reported, batch continues
// ============================================================
echo "=== Test 1: ERRMODE_WARNING ===\n";

try {
$conn = connect("", array(), PDO::ERRMODE_WARNING);
$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 — 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
110 changes: 110 additions & 0 deletions test/functional/sqlsrv/sqlsrv_batch_error_continue.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon.inc");

$conn = connect();
if ($conn === false) {
fatalError("Could not connect.\n");
}

$batch = "SET XACT_ABORT OFF; SELECT 1 AS n; SELECT 1/0 AS boom; SELECT 2 AS n;";

// ============================================================
// Test 1: XACT_ABORT OFF — non-fatal error, batch continues
// ============================================================
echo "=== Test 1: XACT_ABORT OFF ===\n";

$stmt = sqlsrv_query($conn, $batch);
if ($stmt === false) {
fatalError("sqlsrv_query failed.\n");
}

// Result set 1: SELECT 1
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
echo "Result set 1: n={$row['n']}\n";

// Advance past failing SELECT 1/0 — returns true (batch not aborted)
$next = sqlsrv_next_result($stmt);
echo "next_result (failing): ";
var_dump($next);

// Error is available via sqlsrv_errors()
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($errors) {
echo "Error SQLSTATE: {$errors[0]['SQLSTATE']}\n";
}

// Advance to result set 3: SELECT 2
$next2 = sqlsrv_next_result($stmt);
echo "next_result (SELECT 2): ";
var_dump($next2);

$row2 = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
echo "Result set 3: n={$row2['n']}\n";

// End of results
$next3 = sqlsrv_next_result($stmt);
echo "next_result (end): ";
var_dump($next3);

sqlsrv_free_stmt($stmt);

// ============================================================
// Test 2: Re-execute after mid-batch error (flush loop)
// ============================================================
echo "\n=== Test 2: Re-execute after error ===\n";

$stmt2 = sqlsrv_prepare($conn, $batch);
if ($stmt2 === false) {
fatalError("sqlsrv_prepare failed.\n");
}

// First execution — consume only the first result set
sqlsrv_execute($stmt2);
$row = sqlsrv_fetch_array($stmt2, SQLSRV_FETCH_ASSOC);
echo "First execute, result set 1: n={$row['n']}\n";

// Re-execute — the internal flush loop must handle the remaining
// results (including the SQL_ERROR from SELECT 1/0) without failing.
$ok = sqlsrv_execute($stmt2);
echo "Re-execute: ";
var_dump($ok);

// Verify the new execution produces correct results
$row = sqlsrv_fetch_array($stmt2, SQLSRV_FETCH_ASSOC);
echo "Second execute, result set 1: n={$row['n']}\n";

sqlsrv_free_stmt($stmt2);
sqlsrv_close($conn);

echo "\nDone\n";
?>
--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
Loading