The statement handle is still valid and available for processing the non-failing statements in the batch.
The following program can be used to demonstrate the observed behavior.
/*
* repro.c
*
* Show the return code from SQLMoreResults with a multi-statement batch.
*
* Usage:
* ./repro CONNECTION-STRING
*/
#include <stdio.h>
#include <stdlib.h>
#include <sql.h>
#include <sqlext.h>
/* Print all diagnostic records on a handle. */
static void print_diag(const char *fn, SQLSMALLINT handle_type, SQLHANDLE handle)
{
SQLSMALLINT i = 1;
SQLCHAR sqlstate[6], msg[1024];
SQLINTEGER native;
SQLSMALLINT msg_len;
SQLRETURN ret;
while ((ret = SQLGetDiagRec(handle_type, handle, i++,
sqlstate, &native, msg, sizeof(msg), &msg_len))
== SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO)
{
fprintf(stderr, " %s DIAG: SQLSTATE=%s native=%ld msg=%s\n",
fn, (char *)sqlstate, (long)native, (char *)msg);
}
}
/* Check return code; print diagnostics on unexpected failure. */
static void check(const char *fn, SQLRETURN ret,
SQLSMALLINT handle_type, SQLHANDLE handle,
SQLRETURN expected)
{
if (ret == expected)
return;
fprintf(stderr, "UNEXPECTED: %s returned %d (expected %d)\n",
fn, (int)ret, (int)expected);
print_diag(fn, handle_type, handle);
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <connection-string>\n", argv[0]);
return 1;
}
SQLRETURN ret;
SQLHENV henv = SQL_NULL_HENV;
SQLHDBC hdbc = SQL_NULL_HDBC;
SQLHSTMT hstmt = SQL_NULL_HSTMT;
int rc = EXIT_SUCCESS;
/* --- allocate environment ------------------------------------------ */
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
check("SQLAllocHandle(ENV)", ret, SQL_HANDLE_ENV, henv, SQL_SUCCESS);
ret = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION,
(SQLPOINTER)SQL_OV_ODBC3, 0);
check("SQLSetEnvAttr", ret, SQL_HANDLE_ENV, henv, SQL_SUCCESS);
/* --- connect -------------------------------------------------------- */
ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
check("SQLAllocHandle(DBC)", ret, SQL_HANDLE_DBC, hdbc, SQL_SUCCESS);
ret = SQLDriverConnect(hdbc, NULL,
(SQLCHAR *)argv[1], SQL_NTS,
NULL, 0, NULL,
SQL_DRIVER_NOPROMPT);
if (!SQL_SUCCEEDED(ret)) {
fprintf(stderr, "SQLDriverConnect failed\n");
print_diag("SQLDriverConnect", SQL_HANDLE_DBC, hdbc);
rc = EXIT_FAILURE;
goto cleanup;
}
printf("SQLDriverConnect succeeded ...\n");
/* --- execute statement --------------------------------------------- */
ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
check("SQLAllocHandle(STMT)", ret, SQL_HANDLE_STMT, hstmt, SQL_SUCCESS);
SQLCHAR *batch =
(SQLCHAR *)
"SET XACT_ABORT OFF; "
"SELECT 1 AS n; "
"SELECT 1/0 AS boom; "
"SELECT 2 AS n;";
ret = SQLExecDirect(hstmt, batch, SQL_NTS);
if (!SQL_SUCCEEDED(ret)) {
fprintf(stderr, "SQLExecDirect failed unexpectedly.\n");
print_diag("SQLExecDirect", SQL_HANDLE_STMT, hstmt);
goto cleanup;
}
printf("SQLExecDirect succeeded ...\n");
/* Consume result set 0 (SELECT 1). */
{
SQLINTEGER n;
SQLLEN ind;
SQLBindCol(hstmt, 1, SQL_C_LONG, &n, sizeof(n), &ind);
SQLFetch(hstmt);
if (n == 1)
printf("first SQLFetch succeeded ...\n");
else {
printf("first SQLFetch: expected 1, got %d\n", n);
rc = EXIT_FAILURE;
}
SQLFreeStmt(hstmt, SQL_UNBIND);
}
// Move to the next (failing) statement's results set.
ret = SQLMoreResults(hstmt);
switch (ret) {
case SQL_SUCCESS_WITH_INFO:
printf("first SQLMoreResults succeeded (returning SQL_SUCCESS_WITH_INFO as expected) ...\n");
break;
case SQL_ERROR:
printf("first SQLMoreResults: expected SQL_SUCCESS_WITH_INFO, got SQL_ERROR ...\n");
rc = EXIT_FAILURE;
break;
case SQL_SUCCESS:
printf("first SQLMoreResults: expected SQL_SUCCESS_WITH_INFO, got SQL_SUCCESS ...\n");
rc = EXIT_FAILURE;
break;
default:
printf("first SQLMoreResults: expected SQL_SUCCESS_WITH_INFO, got %d ...\n", ret);
rc = EXIT_FAILURE;
break;
}
print_diag("first SQLMoreResults", SQL_HANDLE_STMT, hstmt);
// Find out if the statement handle is still valid. Some drivers (Microsoft)
// mistakenly return SQL_ERROR for the first call to SQLMoreResults() when
// they really mean SQL_SUCCESS_WITH_INFO, because they leave the handle open
// for business. Others (FreeTDS) return SQL_SUCCESS for the previous call,
// but the statement handle is no longer alive.
ret = SQLMoreResults(hstmt);
switch (ret) {
case SQL_SUCCESS:
printf("second SQLMoreResults succeeded (returning SQL_SUCCESS as expected) ...\n");
break;
case SQL_SUCCESS_WITH_INFO:
printf("second SQLMoreResults: expected SQL_SUCCESS, got SQL_SUCCESS_WITH_INFO\n");
rc = EXIT_FAILURE;
break;
case SQL_ERROR:
printf("second SQLMoreResults: expected SQL_SUCCESS, got SQL_ERROR\n");
rc = EXIT_FAILURE;
break;
default:
printf("second SQLMoreResults: expected SQL_SUCCESS, got %d\n", ret);
rc = EXIT_FAILURE;
break;
}
print_diag("SQLMoreResults", SQL_HANDLE_STMT, hstmt);
/* Consume result set 2 (SELECT 2). */
{
SQLINTEGER n;
SQLLEN ind;
SQLBindCol(hstmt, 1, SQL_C_LONG, &n, sizeof(n), &ind);
if (!SQL_SUCCEEDED(SQLFetch(hstmt)) || n != 2) {
printf("second SQLFetch: statement handle is no longer valid ...\n");
rc = EXIT_FAILURE;
}
else
printf("second SQLFetch succeeded ...\n");
SQLFreeStmt(hstmt, SQL_UNBIND);
}
cleanup:
if (hstmt != SQL_NULL_HSTMT) SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
if (hdbc != SQL_NULL_HDBC) { SQLDisconnect(hdbc); SQLFreeHandle(SQL_HANDLE_DBC, hdbc); }
if (henv != SQL_NULL_HENV) SQLFreeHandle(SQL_HANDLE_ENV, henv);
return rc;
}
SQLDriverConnect succeeded ...
SQLExecDirect succeeded ...
first SQLFetch succeeded ...
first SQLMoreResults: expected SQL_SUCCESS_WITH_INFO, got SQL_ERROR ...
first SQLMoreResults DIAG: SQLSTATE=22012 native=8134 msg=[Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Divide by zero error encountered.
second SQLMoreResults succeeded (returning SQL_SUCCESS as expected) ...
second SQLFetch succeeded ...
Background
Here is what the spec says about the return value from
SQLMoreResultsfor multi-statement batches in which one or more of the statements have failed.Expected
For a multi-statement batch, the driver returns
SQL_SUCCESS_WITH_INFOfromSQLMoreResultswhen all of the following conditions are met:XACT_ABORTisOFF(the default), the handles are still valid and uncorrupted, etc.)The statement handle is still valid and available for processing the non-failing statements in the batch.
Observed
Under those conditions, the Microsoft ODBC driver returns
SQL_ERRORinstead ofSQL_SUCCESS_WITH_INFOeven though the statement handle is still longer valid and available for processing the remaining statements in the batch.Repro
The following program can be used to demonstrate the observed behavior.
Output
Environment
Footnotes
The design decision to have
SQL_ERRORrepresent different semantics depending on the statement position seems flawed to me, but that's a separate issue. ↩