Skip to content

Commit c0edf0c

Browse files
authored
FIx: Add opt-in batch error continuation for sqlsrv and PDO next-result APIs (#1600)
* Fix #1599: Remove SQLCancel from core_sqlsrv_next_result catch block When SQLMoreResults returns SQL_ERROR for a mid-batch statement failure (e.g. divide-by-zero with XACT_ABORT OFF), the statement handle remains valid. The old code called SQLCancel() in the catch block, which aborted the entire remaining batch. This change: 1. Removes SQLCancel from the catch block so the handle stays alive. 2. Adds a throw_on_errors parameter (default true) to core_sqlsrv_next_result to scope the non-throwing behaviour: - throw_on_errors=true (flush loops, closeCursor, param binding): uses the throwing core::SQLMoreResults wrapper — SQL_ERROR exits the loop via exception. Same behaviour as before. - throw_on_errors=false (sqlsrv_next_result / PDO::nextRowset): calls SQLMoreResults directly, reports the error through the normal error handler, clears any pending PDO exception, and falls through to new_result_set(). The batch remains navigable. 3. Tests both extensions (ERRMODE_WARNING and ERRMODE_EXCEPTION for PDO) and verifies re-execute after a batch error (flush loop). Fixes #1599 * Implement opt-in batch error continuation with legacy default Add optional batch error continuation controls for both sqlsrv and PDO next-result APIs, preserving legacy fail-on-error behavior by default while allowing users to opt-in to continue-after-error semantics. Changes: - Add batch_error_continue boolean flag to shared connection struct - Initialize flag to false to preserve backward-compatible behavior - Expose BatchErrorContinue option for sqlsrv_connect() - Expose SQLSRV_ATTR_BATCH_ERROR_CONTINUE attribute for PDO - Gate both sqlsrv_next_result() and PDOStatement::nextRowset() on flag - Updated tests to validate both default and opt-in behaviors Design: - Default (batch_error_continue=false): sqlsrv_next_result/nextRowset return false on mid-batch SQL errors, matching pre-fix semantics - Opt-in (batch_error_continue=true): continue past mid-batch errors while reporting them via sqlsrv_errors()/errorInfo() Impact: - Existing applications unaffected; no behavior changes without opt-in - Users must explicitly set the flag to enable new behavior - Full backward compatibility maintained * Harden opt-in batch error continuation without breaking defaults - Split non-throwing next-result behavior into explicit modes (silent internal drain vs opt-in user-facing diagnostics) - Preserve default re-execute flush semantics (no surfaced diagnostics) - Gate sqlsrv_next_result/PDO nextRowset reporting strictly on opt-in - Guard zend_clear_exception behind pending-exception check - Reduce test flakiness by asserting diagnostic presence instead of fixed SQLSTATE - Add coverage for default silent re-execute and PDO constructor opt-in path - Clarify connection-scope behavior in code and changelog (issue #1599) * Fix batch error tests: use RAISERROR instead of SELECT 1/0 SELECT 1/0 with ANSI_WARNINGS ON (the SQL Server default) produces SQL_SUCCESS_WITH_INFO (a NULL result set + warning), not SQL_ERROR. This caused the tests to fail across all CI environments because nextRowset() returned true (valid result set) instead of the expected false (error). RAISERROR('...', 16, 1) always produces SQL_ERROR from SQLMoreResults regardless of ANSI_WARNINGS or ARITHABORT settings, making it a reliable cross-environment trigger for the mid-batch error path that this feature is designed to handle. * Fix PDO flush loops to use silent drain when batch_error_continue is enabled The PDO execute and param_hook flush loops used throw_on_errors=true (the default), which means a mid-batch SQL_ERROR from a prior partial navigation would throw, call SQLCancel, and fail the re-execute. The sqlsrv driver already handled this correctly with silent drain (throw_on_errors=false). Apply the same conditional logic to PDO: when batch_error_continue is enabled, flush silently; otherwise preserve the existing throwing behavior. Also adds PDO re-execute tests (Tests 6 and 7) to validate that re-executing after partial batch navigation works correctly for both opt-in and default connections. * Fix PDO 'Invalid cursor state' after SQL_ERROR in opt-in batch continuation After SQLMoreResults returns SQL_ERROR for a non-result-producing statement (e.g. RAISERROR), there is no active ODBC cursor. The PDO nextRowset code unconditionally called SQLNumResultCols/SQLRowCount after core_sqlsrv_next_result, which failed with 'Invalid cursor state' (SQLSTATE 24000). Fix: 1. In core_sqlsrv_next_result: when report_errors=true and r==SQL_ERROR, clean up the previous result set and return early WITHOUT creating a new result set object. The batch remains navigable. 2. In pdo_sqlsrv_stmt_next_rowset: check current_results != NULL before calling SQLNumResultCols/SQLRowCount. On error markers (NULL results), set column_count=0 and row_count=0. 3. In test: suppress ERRMODE_WARNING output with @ operator so expected output stays deterministic across environments. * Address review nits: extract free_current_results() helper, remove dead assignments 1. Extract the ~12-line current_results teardown block into a free_current_results() helper method on sqlsrv_stmt. Used in the destructor, new_result_set(), and the SQL_ERROR opt-in path. 2. Remove dead stmt->column_count = 0 / stmt->row_count = 0 assignments that were immediately overwritten by clean_up_results_metadata() (which sets them to ACTIVE_NUM_COLS_INVALID / ACTIVE_NUM_ROWS_INVALID).
1 parent 3a8d8ee commit c0edf0c

11 files changed

Lines changed: 569 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Updated PECL release packages. Here is the list of updates:
1414
- Support for Red Hat 9 and 10
1515
- Support for Alpine 3.20, 3.21, 3.22, and 3.23
1616
- Support for macOS 15 and 26
17+
- Added opt-in batch error continuation controls for sqlsrv and PDO next-result APIs ([#1599](https://github.com/microsoft/msphpsql/issues/1599))
18+
- Applies only when set at connection scope (`BatchErrorContinue` or `PDO::SQLSRV_ATTR_BATCH_ERROR_CONTINUE`)
19+
- In opt-in mode, next-result APIs may return success while reporting `SQL_ERROR` diagnostics, allowing callers to continue traversing rowsets
1720

1821
### Removed
1922
- Support for PHP 8.1 and 8.2
@@ -35,6 +38,7 @@ Updated PECL release packages. Here is the list of updates:
3538
- Fixed critical memory safety bugs in encoding conversion - NULL pointer dereference and uninitialized pointer return ([PR #1555](https://github.com/microsoft/msphpsql/pull/1555))
3639
- Removed lingering error2 reference from failure block in CI pipeline ([PR #1568](https://github.com/microsoft/msphpsql/pull/1568))
3740
- Fixed PHP 8.5 compatibility issues in tests and CI pipeline ([PR #1569](https://github.com/microsoft/msphpsql/pull/1569))
41+
- Preserved legacy next-result error semantics by default while allowing opt-in continuation after mid-batch statement errors
3842

3943
### Limitations
4044
- No support for inout / output params when using sql_variant type

source/pdo_sqlsrv/pdo_dbh.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,11 @@ bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
13011301
}
13021302
break;
13031303

1304+
case SQLSRV_ATTR_BATCH_ERROR_CONTINUE:
1305+
// Connection-level option: applies to all statements created by this DBH.
1306+
driver_dbh->batch_error_continue = zend_is_true(val);
1307+
break;
1308+
13041309
#if PHP_VERSION_ID >= 70200
13051310
case PDO_ATTR_DEFAULT_STR_PARAM:
13061311
{
@@ -1525,6 +1530,13 @@ int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_
15251530
break;
15261531
}
15271532

1533+
case SQLSRV_ATTR_BATCH_ERROR_CONTINUE:
1534+
{
1535+
// Connection-level option value.
1536+
ZVAL_BOOL(return_value, driver_dbh->batch_error_continue);
1537+
break;
1538+
}
1539+
15281540
#if PHP_VERSION_ID >= 70200
15291541
case PDO_ATTR_DEFAULT_STR_PARAM:
15301542
{

source/pdo_sqlsrv/pdo_init.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ namespace {
304304
{ "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS },
305305
{ "SQLSRV_ATTR_DECIMAL_PLACES" , SQLSRV_ATTR_DECIMAL_PLACES },
306306
{ "SQLSRV_ATTR_DATA_CLASSIFICATION" , SQLSRV_ATTR_DATA_CLASSIFICATION },
307+
{ "SQLSRV_ATTR_BATCH_ERROR_CONTINUE", SQLSRV_ATTR_BATCH_ERROR_CONTINUE },
307308

308309
// used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int,
309310
// PDO::PARAM_STR uses the size of the string in the variable

source/pdo_sqlsrv/pdo_stmt.cpp

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,12 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt )
549549

550550
while( driver_stmt->past_next_result_end == false ) {
551551

552-
core_sqlsrv_next_result( driver_stmt, false );
552+
// When batch_error_continue is enabled, use silent drain
553+
// (throw_on_errors=false) so remaining mid-batch errors
554+
// don't prevent re-execution. Without opt-in, preserve
555+
// legacy throwing behavior.
556+
core_sqlsrv_next_result( driver_stmt, false,
557+
!driver_stmt->conn->batch_error_continue );
553558
}
554559
}
555560

@@ -1217,21 +1222,32 @@ int pdo_sqlsrv_stmt_next_rowset( _Inout_ pdo_stmt_t *stmt )
12171222

12181223
SQLSRV_ASSERT( driver_stmt != NULL, "pdo_sqlsrv_stmt_next_rowset: driver_data object was null" );
12191224

1220-
core_sqlsrv_next_result( static_cast<sqlsrv_stmt*>( stmt->driver_data ) );
1225+
core_sqlsrv_next_result( static_cast<sqlsrv_stmt*>( stmt->driver_data ), true,
1226+
!static_cast<sqlsrv_stmt*>( stmt->driver_data )->conn->batch_error_continue,
1227+
static_cast<sqlsrv_stmt*>( stmt->driver_data )->conn->batch_error_continue );
12211228

12221229
if( driver_stmt->past_next_result_end == true ) {
12231230
// Clean up remaining metadata since new_result_set() was not called
12241231
driver_stmt->clean_up_results_metadata();
12251232
return 0;
12261233
}
12271234

1228-
stmt->column_count = core::SQLNumResultCols( driver_stmt );
1229-
1230-
// return the row count regardless if there are any rows or not
1231-
stmt->row_count = core::SQLRowCount( driver_stmt );
1232-
1233-
driver_stmt->column_count = static_cast<short>(stmt->column_count);
1234-
driver_stmt->row_count = static_cast<long>(stmt->row_count);
1235+
// When positioned on an error marker (opt-in batch error continuation
1236+
// after SQL_ERROR from a non-result-producing statement like RAISERROR),
1237+
// there is no active ODBC cursor. Skip SQLNumResultCols/SQLRowCount
1238+
// which would fail with "Invalid cursor state".
1239+
if( driver_stmt->current_results != NULL ) {
1240+
stmt->column_count = core::SQLNumResultCols( driver_stmt );
1241+
stmt->row_count = core::SQLRowCount( driver_stmt );
1242+
driver_stmt->column_count = static_cast<short>(stmt->column_count);
1243+
driver_stmt->row_count = static_cast<long>(stmt->row_count);
1244+
}
1245+
else {
1246+
stmt->column_count = 0;
1247+
stmt->row_count = 0;
1248+
driver_stmt->column_count = 0;
1249+
driver_stmt->row_count = 0;
1250+
}
12351251
}
12361252
catch( core::CoreException& ) {
12371253

@@ -1305,7 +1321,10 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt,
13051321

13061322
while( driver_stmt->past_next_result_end == false ) {
13071323

1308-
core_sqlsrv_next_result( driver_stmt, false );
1324+
// Match execute flush: silent drain when opt-in is
1325+
// enabled, legacy throw otherwise.
1326+
core_sqlsrv_next_result( driver_stmt, false,
1327+
!driver_stmt->conn->batch_error_continue );
13091328
}
13101329
}
13111330

source/pdo_sqlsrv/php_pdo_sqlsrv_int.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ enum PDO_SQLSRV_ATTR {
9090
SQLSRV_ATTR_FETCHES_DATETIME_TYPE,
9191
SQLSRV_ATTR_FORMAT_DECIMALS,
9292
SQLSRV_ATTR_DECIMAL_PLACES,
93-
SQLSRV_ATTR_DATA_CLASSIFICATION
93+
SQLSRV_ATTR_DATA_CLASSIFICATION,
94+
SQLSRV_ATTR_BATCH_ERROR_CONTINUE
9495
};
9596

9697
// valid set of values for TransactionIsolation connection option

source/shared/core_sqlsrv.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,7 @@ struct sqlsrv_conn : public sqlsrv_context {
11351135

11361136
col_encryption_option ce_option; // holds the details of what are required to enable column encryption
11371137
ODBC_DRIVER driver_version; // version of ODBC driver
1138+
bool batch_error_continue; // opt-in to continue past mid-batch SQLMoreResults errors
11381139

11391140
ACCESSTOKEN* azure_ad_access_token; // non-owning; managed by token cache
11401141

@@ -1144,6 +1145,7 @@ struct sqlsrv_conn : public sqlsrv_context {
11441145
{
11451146
server_version = SERVER_VERSION_UNKNOWN;
11461147
driver_version = ODBC_DRIVER::VER_UNKNOWN;
1148+
batch_error_continue = false;
11471149
azure_ad_access_token = nullptr;
11481150
}
11491151

@@ -1696,6 +1698,9 @@ struct sqlsrv_stmt : public sqlsrv_context {
16961698
// free sensitivity classification metadata
16971699
void clean_up_sensitivity_metadata();
16981700

1701+
// free the current result set object (if any)
1702+
void free_current_results();
1703+
16991704
// free resultset metadata
17001705
void clean_up_results_metadata();
17011706

@@ -1805,7 +1810,8 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i
18051810
_Outref_result_bytebuffer_maybenull_(*field_length) void*& field_value, _Inout_ SQLLEN* field_length, _In_ bool cache_field,
18061811
_Out_ SQLSRV_PHPTYPE *sqlsrv_php_type_out);
18071812
bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt );
1808-
void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_output_params = true, _In_ bool throw_on_errors = true );
1813+
void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_output_params = true, _In_ bool throw_on_errors = true,
1814+
_In_ bool report_errors = false );
18091815
void core_sqlsrv_set_scrollable( _Inout_ sqlsrv_stmt* stmt, _In_ unsigned long cursor_type );
18101816
void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z );
18111817
bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt, _In_opt_ bool get_all = false);

source/shared/core_stmt.cpp

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//---------------------------------------------------------------------------------------------------------------------------------
1919

2020
#include "core_sqlsrv.h"
21+
#include "zend_exceptions.h"
2122

2223
#include <sstream>
2324
#include <vector>
@@ -153,12 +154,7 @@ sqlsrv_stmt::~sqlsrv_stmt( void )
153154
close_active_stream( this );
154155
}
155156

156-
// delete any current results
157-
if( current_results ) {
158-
current_results->~sqlsrv_result_set();
159-
efree( current_results );
160-
current_results = NULL;
161-
}
157+
free_current_results();
162158

163159
// delete sensivity data
164160
clean_up_sensitivity_metadata();
@@ -183,6 +179,16 @@ void sqlsrv_stmt::free_param_data( void )
183179
zend_hash_clean( Z_ARRVAL( field_cache ));
184180
}
185181

182+
// free the current result set object without creating a new one.
183+
void sqlsrv_stmt::free_current_results( void )
184+
{
185+
if( current_results ) {
186+
current_results->~sqlsrv_result_set();
187+
efree( current_results );
188+
current_results = NULL;
189+
}
190+
}
191+
186192

187193
// to be called whenever a new result set is created, such as after an
188194
// execute or next_result. Resets the state variables.
@@ -197,12 +203,7 @@ void sqlsrv_stmt::new_result_set( void )
197203
this->column_count = ACTIVE_NUM_COLS_INVALID;
198204
this->row_count = ACTIVE_NUM_ROWS_INVALID;
199205

200-
// delete any current results
201-
if( current_results ) {
202-
current_results->~sqlsrv_result_set();
203-
efree( current_results );
204-
current_results = NULL;
205-
}
206+
free_current_results();
206207

207208
// delete sensivity data
208209
clean_up_sensitivity_metadata();
@@ -963,7 +964,7 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt )
963964
// Returns
964965
// Nothing, exception thrown if problem occurs
965966

966-
void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_output_params, _In_ bool throw_on_errors )
967+
void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_output_params, _In_ bool throw_on_errors, _In_ bool report_errors )
967968
{
968969
try {
969970

@@ -982,11 +983,59 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp
982983
zend_hash_clean( Z_ARRVAL( stmt->col_cache ));
983984

984985
SQLRETURN r;
986+
985987
if( throw_on_errors ) {
988+
// Use the throwing wrapper for callers that require fail-fast
989+
// behavior on SQL_ERROR.
986990
r = core::SQLMoreResults( stmt );
987991
}
988992
else {
989-
r = SQLMoreResults( stmt->handle() );
993+
// Non-throwing paths are split into two semantics:
994+
// 1. Silent internal drain (report_errors=false): do not throw and
995+
// do not push diagnostics to the PHP error queues.
996+
// 2. User-facing opt-in continuation (report_errors=true): report
997+
// SQL_ERROR / SQL_SUCCESS_WITH_INFO without throwing so callers
998+
// can continue to subsequent rowsets.
999+
r = ::SQLMoreResults( stmt->handle() );
1000+
1001+
SQLSRV_ASSERT( r != SQL_INVALID_HANDLE, "Invalid handle returned from SQLMoreResults." );
1002+
1003+
if( report_errors ) {
1004+
if( r == SQL_ERROR ) {
1005+
// In opt-in continuation mode we surface diagnostics and
1006+
// keep rowset navigation alive. Depending on server/session
1007+
// state, SQL_ERROR can represent either a recoverable
1008+
// mid-batch statement failure or a fatal transport-level
1009+
// condition.
1010+
call_error_handler( stmt, SQLSRV_ERROR_ODBC, /*warning*/0 );
1011+
1012+
// The PDO error handler may set a pending zend exception
1013+
// in ERRMODE_EXCEPTION. Clear it so next-result traversal
1014+
// can continue and the error remains observable via
1015+
// errorInfo()/sqlsrv_errors().
1016+
if( EG(exception) != NULL ) {
1017+
zend_clear_exception();
1018+
}
1019+
1020+
// SQL_ERROR from a non-result-producing statement (e.g.
1021+
// RAISERROR) leaves no active ODBC cursor. Clean up the
1022+
// previous result set but do NOT create a new one — calling
1023+
// SQLNumResultCols/SQLRowCount would fail with "Invalid
1024+
// cursor state". The batch remains navigable via subsequent
1025+
// next-result calls.
1026+
stmt->free_current_results();
1027+
stmt->fetch_called = false;
1028+
stmt->has_rows = false;
1029+
stmt->past_fetch_end = false;
1030+
stmt->last_field_index = -1;
1031+
stmt->clean_up_sensitivity_metadata();
1032+
stmt->clean_up_results_metadata();
1033+
return;
1034+
}
1035+
else if( r == SQL_SUCCESS_WITH_INFO ) {
1036+
call_error_handler( stmt, SQLSRV_ERROR_ODBC, /*warning*/1 );
1037+
}
1038+
}
9901039
}
9911040

9921041
if( r == SQL_NO_DATA ) {
@@ -1005,7 +1054,15 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp
10051054
}
10061055
catch( core::CoreException& e ) {
10071056

1008-
SQLCancel( stmt->handle() );
1057+
// For internal callers (throw_on_errors=true — flush loops,
1058+
// closeCursor, param binding) we still call SQLCancel to clean up
1059+
// the ODBC handle on error. For user-facing callers
1060+
// (throw_on_errors=false — sqlsrv_next_result / PDO::nextRowset)
1061+
// we must NOT cancel because the handle is still valid and the
1062+
// batch remains navigable after a mid-batch statement failure.
1063+
if( throw_on_errors ) {
1064+
SQLCancel( stmt->handle() );
1065+
}
10091066
throw e;
10101067
}
10111068
}

source/sqlsrv/conn.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ struct decimal_places_func
7272
}
7373
};
7474

75+
struct batch_error_continue_func
76+
{
77+
static void func(connection_option const* /*option*/, _In_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/)
78+
{
79+
conn->batch_error_continue = zend_is_true(value);
80+
}
81+
};
82+
7583

7684
struct conn_char_set_func {
7785

@@ -231,6 +239,7 @@ const char DecimalPlaces[] = "DecimalPlaces";
231239
const char FormatDecimals[] = "FormatDecimals";
232240
const char DateAsString[] = "ReturnDatesAsStrings";
233241
const char Driver[] = "Driver";
242+
const char BatchErrorContinue[] = "BatchErrorContinue";
234243
const char Encrypt[] = "Encrypt";
235244
const char Failover_Partner[] = "Failover_Partner";
236245
const char KeyStoreAuthentication[] = "KeyStoreAuthentication";
@@ -256,6 +265,7 @@ const char HostNameInCertificate[] = "HostNameInCertificate";
256265
enum SS_CONN_OPTIONS {
257266

258267
SS_CONN_OPTION_DATE_AS_STRING = SQLSRV_CONN_OPTION_DRIVER_SPECIFIC,
268+
SS_CONN_OPTION_BATCH_ERROR_CONTINUE,
259269
SS_CONN_OPTION_FORMAT_DECIMALS,
260270
SS_CONN_OPTION_DECIMAL_PLACES,
261271
};
@@ -577,6 +587,15 @@ const connection_option SS_CONN_OPTS[] = {
577587
CONN_ATTR_BOOL,
578588
date_as_string_func::func
579589
},
590+
{
591+
SSConnOptionNames::BatchErrorContinue,
592+
sizeof( SSConnOptionNames::BatchErrorContinue ),
593+
SS_CONN_OPTION_BATCH_ERROR_CONTINUE,
594+
SSConnOptionNames::BatchErrorContinue,
595+
sizeof( SSConnOptionNames::BatchErrorContinue ),
596+
CONN_ATTR_BOOL,
597+
batch_error_continue_func::func
598+
},
580599
{
581600
SSConnOptionNames::FormatDecimals,
582601
sizeof( SSConnOptionNames::FormatDecimals),

source/sqlsrv/stmt.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ PHP_FUNCTION( sqlsrv_next_result )
569569

570570
try {
571571

572-
core_sqlsrv_next_result( stmt, true );
572+
core_sqlsrv_next_result( stmt, true, !stmt->conn->batch_error_continue, stmt->conn->batch_error_continue );
573573

574574
if( stmt->past_next_result_end ) {
575575
// Clean up remaining metadata since new_result_set() was not called

0 commit comments

Comments
 (0)