opensearch-project / opensearch-project/sql-odbc
SQLError/SQLErrorW returns the same connection error on every call and never SQL_NO_DATA, so unixODBC's SQLDriverConnect(W) spins forever on any failed connect (closed port, useSSL mismatch, bad auth=…)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 10
- Forks
- 25
- PR merge metrics
- No merged PRs in 30d
Description
What happens
Through unixODBC, a SQLDriverConnect/SQLDriverConnectW that cannot succeed never returns: a closed port, useSSL=1 against a plaintext cluster, an unknown auth= value — the call sits at 100 % CPU until the process is killed (I gave up at 11 minutes on an unresolvable host). responseTimeout= makes no difference, because the network part does finish (the refused connect() is diagnosed within about two seconds); it is what happens afterwards that never ends.
Why
After a failed connect, unixODBC's SQLDriverConnectW collects the driver's diagnostics before dropping the connection. If the driver exports SQLErrorW, the driver manager prefers it over SQLGetDiagRecW (DriverManager/SQLDriverConnectW.c, 2.3.12, the CHECK_SQLERRORW( connection ) branch) and calls it in a do … while ( SQL_SUCCEEDED( ret )) loop — SQLError has no record number, so by the ODBC 2 contract each call returns the next error and the driver clears it; the loop ends at SQL_NO_DATA.
This driver exports SQLErrorW, and its OPENSEARCHAPI_ConnectError (src/sqlodbc/environ.c) returns the connection's error on every call without ever clearing it: CC_get_error() is read, the sqlstate mapped, and the function returns SQL_SUCCESS — nothing resets conn->__error_number/message. So the driver manager loops forever. The driver's own log shows it: strace of the hung process is 31,948 write()s in 20 seconds to the mylog file, all of them
environ.c[OPENSEARCHAPI_ConnectError]212: entering hdbc=0x… <513>
environ.c[OPENSEARCHAPI_ConnectError]229: CC_get_error: status = 202, msg = #Connection error: [OpenSearch][SQL ODBC Driver][SQL Plugin] Connection error: SQL plugin is not available at url: http://127.0.0.1:19999, please install the SQL plugin to use this driver.#
(15,974 pairs), and a gdb sample of the process has only libodbc.so.2 frames under SQLDriverConnectW — the driver manager is the one looping. SQLGetDiagRecW is fine (it checks RecNumber and answers SQL_NO_DATA for record 2), which is why the same failure returns promptly through a driver manager that uses SQLGetDiagRec, and why an application that reaches the driver directly sees a normal 08001.
Environment
- Driver built from
mainat dcd08dc (2026-08-07), Linux x86_64,SQL_DRIVER_VER1.6.0.0; unixODBC 2.3.12 - Any unreachable target; OpenSearch 3.8.0 was on 19200, the closed port used below is 19999
Reproduction 1 — the hang, through unixODBC (gcc -O1 -Wall connect_hang.c -lodbc):
/* OpenSearch SQL ODBC: a connect that cannot succeed never returns.
* Build: gcc -O1 -Wall connect_hang.c -lodbc Run: ./connect_hang '<connection string>' (wrap in `timeout 60`) */
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, char** argv) {
setvbuf(stdout, NULL, _IONBF, 0);
SQLHENV env; SQLHDBC dbc; SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
const char* c = argv[1]; size_t n = strlen(c); SQLWCHAR w[1024]; for (size_t i = 0; i <= n; i++) w[i] = (SQLWCHAR)(unsigned char)c[i];
time_t t0 = time(NULL); printf("SQLDriverConnectW ... ");
SQLRETURN rc = SQLDriverConnectW(dbc, NULL, w, (SQLSMALLINT)n, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
printf("returned rc=%d after %ld s\n", (int)rc, (long)(time(NULL) - t0));
SQLCHAR st[6], msg[1024]; SQLINTEGER nat; SQLSMALLINT len, i = 1, k = 0;
while (SQLGetDiagRec(SQL_HANDLE_DBC, dbc, i++, st, &nat, msg, sizeof msg, &len) == SQL_SUCCESS) { k++; printf(" %s (%d): %s\n", st, (int)nat, msg); }
printf(" diagnostic records: %d\n", k); return 0;
}
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19999;auth=NONE;useSSL=0;'
SQLDriverConnectW ... exit=124
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19999;auth=NONE;useSSL=0;responseTimeout=3;'
SQLDriverConnectW ... exit=124
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19200;auth=NONE;useSSL=1;responseTimeout=3;'
SQLDriverConnectW ... exit=124
(exit=124 is timeout killing the process after 45 s; SQLDriverConnectW never returned.)
Reproduction 2 — the driver called directly, no driver manager (gcc -O1 -Wall diagrec_loop.c -ldl): after the same failed connect, SQLGetDiagRecW answers records 1 then SQL_NO_DATA, while SQLErrorW answers the same record on call 1, 2 and 3:
/* Calls the OpenSearch SQL ODBC driver directly (dlopen, no driver manager) and asks
* SQLGetDiagRecW for records 1, 2, 3 after a failed connect.
* Build: gcc -O1 -Wall diagrec_loop.c -ldl -o diagrec_loop Run: ./diagrec_loop /path/libsqlodbc.so */
#include <sql.h>
#include <sqlext.h>
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char** argv) {
void* so = dlopen(argv[1], RTLD_NOW); if (!so) { printf("dlopen: %s\n", dlerror()); return 1; }
SQLRETURN (*AllocHandle)(SQLSMALLINT, SQLHANDLE, SQLHANDLE*) = dlsym(so, "SQLAllocHandle");
SQLRETURN (*SetEnvAttr)(SQLHENV, SQLINTEGER, SQLPOINTER, SQLINTEGER) = dlsym(so, "SQLSetEnvAttr");
SQLRETURN (*DriverConnectW)(SQLHDBC, SQLHWND, SQLWCHAR*, SQLSMALLINT, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*, SQLUSMALLINT) = dlsym(so, "SQLDriverConnectW");
SQLRETURN (*GetDiagRecW)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*) = dlsym(so, "SQLGetDiagRecW");
SQLHENV env; SQLHDBC dbc; AllocHandle(SQL_HANDLE_ENV, NULL, &env);
SetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); AllocHandle(SQL_HANDLE_DBC, env, &dbc);
const char* c = "host=127.0.0.1;port=19999;auth=NONE;useSSL=0;"; size_t n = strlen(c); SQLWCHAR w[256]; for (size_t i = 0; i <= n; i++) w[i] = (SQLWCHAR)c[i];
SQLRETURN rc = DriverConnectW(dbc, NULL, w, (SQLSMALLINT)n, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
printf("driver SQLDriverConnectW -> rc=%d\n", (int)rc);
for (SQLSMALLINT rec = 1; rec <= 4; rec++) {
SQLWCHAR st[6] = {0}, msg[512] = {0}; SQLINTEGER nat = 0; SQLSMALLINT len = 0;
rc = GetDiagRecW(SQL_HANDLE_DBC, dbc, rec, st, &nat, msg, 512, &len);
char s[6], m[120]; for (int i = 0; i < 5; i++) s[i] = (char)st[i]; s[5] = 0; for (int i = 0; i < 119; i++) m[i] = (char)msg[i]; m[119] = 0;
printf("SQLGetDiagRecW(RecNumber=%d) -> rc=%d%s state=%s native=%d msg=%.100s\n", rec, (int)rc, rc == SQL_NO_DATA ? " (SQL_NO_DATA)" : "", s, (int)nat, m);
}
SQLRETURN (*ErrorW)(SQLHENV, SQLHDBC, SQLHSTMT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*) = dlsym(so, "SQLErrorW");
printf("driver exports SQLErrorW: %s\n", ErrorW ? "yes" : "no");
for (int k = 1; k <= 3 && ErrorW; k++) {
SQLWCHAR st[6] = {0}, msg[512] = {0}; SQLINTEGER nat = 0; SQLSMALLINT len = 0;
rc = ErrorW(SQL_NULL_HENV, dbc, SQL_NULL_HSTMT, st, &nat, msg, 512, &len);
char s[6]; for (int i = 0; i < 5; i++) s[i] = (char)st[i]; s[5] = 0;
printf("SQLErrorW call %d -> rc=%d%s state=%s len=%d\n", k, (int)rc, rc == SQL_NO_DATA ? " (SQL_NO_DATA)" : "", s, (int)len);
}
return 0;
}
$ ./diagrec_loop …/libsqlodbc.so
driver SQLDriverConnectW -> rc=-1
SQLGetDiagRecW(RecNumber=1) -> rc=0 state=08001 native=202 msg=Connection error: [OpenSearch][SQL ODBC Driver][SQL Plugin] Connection error: SQL plugin is not avai
SQLGetDiagRecW(RecNumber=2) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
SQLGetDiagRecW(RecNumber=3) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
SQLGetDiagRecW(RecNumber=4) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
driver exports SQLErrorW: yes
SQLErrorW call 1 -> rc=0 state=08001 len=187
SQLErrorW call 2 -> rc=0 state=08001 len=187
SQLErrorW call 3 -> rc=0 state=08001 len=187
Suggested fix
Stop exporting SQLError/SQLErrorW so that driver managers use SQLGetDiagRec(W), which is already correct here — that is what psqlodbc, which this code descends from, does: nm -D psqlodbcw.so (16.00) exports neither symbol, and its PGAPI_ConnectError has the same no-clear tail as this one, so the ODBC 2 entry points are what makes the difference. (Alternatively, clear the connection error once SQLError has returned it.) The same pattern presumably applies to the statement- and environment-level SQLError paths, though the connect one is the one that hangs every application at its first mistake.
Two side observations from the same trace, not bugs in themselves: the connect first tries 169.254.169.254:80 (the EC2 instance-metadata endpoint — the AWS SDK credential chain) even with auth=NONE, which costs about two seconds on a non-EC2 host before the real connect is attempted; and an empty host= fails with SQL_ERROR and zero diagnostic records on both entry points.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/sqlodbc/environ.c at OPENSEARCHAPI_ConnectError, then compare its SQLErrorW behavior with SQLGetDiagRecW and the unixODBC DriverManager/SQLDriverConnectW.c CHECK_SQLERRORW branch. Reproduce with the provided connect_hang.c or diagrec_loop.c examples; done means a failed SQLDriverConnectW returns promptly and repeated error retrieval reaches SQL_NO_DATA.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, sql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100