Fix numeric parameters silently sent as NULL after a character-typed bind (#300) - #302
Conversation
A parameter's sqlvar is rewritten on every bind to match the C type being transferred (OdbcConvert::setHeadSqlVar), but the driver described the parameter from that same live sqlvar. Once a character transfer had turned it into SQL_TEXT nothing restored it, so every later describe reported CHAR, the next bind selected a string converter again, and the parameter stayed text for the life of the statement -- SQLDescribeParam reported CHAR as well. Since SQL_C_DEFAULT resolves to SQL_C_CHAR for SQL_NUMERIC, SQL_DECIMAL and SQL_BIGINT, an application that binds its NULLs with SQL_C_DEFAULT and its values with the value's own C type poisoned the parameter on its first NULL: every row after it reached the server as NULL, with SQL_SUCCESS and a correct row count. SQL_INTEGER, SQL_SMALLINT and SQL_DOUBLE escaped only because their SQL_C_DEFAULT never resolves to a character C type, and SQLFreeStmt(SQL_RESET_PARAMS) did not help because the state was in the sqlvar, not in the descriptors. Describe INPUT parameters from the prepare-time snapshot (orgSqlProperties) instead, as getColumnDisplaySize() already did, so a bind always sees the type the statement was prepared with whatever the previous transfer did to the sqlvar. OUTPUT columns keep reading the live sqlvar, which is never rewritten.
|
How this relates to #292, which touches the same code path. This PR carries one commit: describe INPUT parameters from the prepare-time snapshot. That is the whole fix for #300, and it stands on master by itself — the five tests here fail on master and pass with it, and CI is green. #292 now carries that same commit plus two more, because a second defect sits next to it and is only testable there:
It is not in this PR because on master its payoff is invisible: clearing the flag only uncovers the truncation #292 fixes (master then writes
So the two PRs are independent and can merge in either order: whichever lands first makes the shared commit a no-op in the other. |
|
@irodushka when you have a moment: this one is a single commit on current master, stands on its own, and its four tests fail on master and pass here. The suggested merge order for an rc2 now lives in #312. |
|
Tested on Linux with the The two C programs from #300, PR build: Every row is the expected one, including NULL-first and the explicit Your Python matrix from #300, through unixODBC (
So the fix holds on both driver managers, and the Thank you, sincerely. In one day you took a report that only said "BIGINT, Linux, cause unknown", reproduced it on a second platform, widened it to the two numeric types it was really about, found the exact lines, and shipped a fix with regression tests that pin every shape in your matrix. Anyone loading numeric data into Firebird through ODBC with NULLs in it, from DuckDB, from an ADBC bridge, from a plain C program, will have you to thank for their rows arriving intact and never know it. I will note the fix in the compatibility record I keep, with your name on it, and re-run against the release that carries it when it ships. (Where this comes from: adbcBridge, an ADBC driver over ODBC that runs Firebird in its compatibility matrix on Linux and Windows, https://github.com/singhpratech/adbcbridge. Its Firebird row and upstream tracker now record this fix.) |
Fixes #300.
The problem
A parameter's sqlvar is rewritten on every bind to match the C type being transferred (
OdbcConvert::setHeadSqlVar, called at the top ofgetAdressFunction), but the driver described the parameter from that same live sqlvar. That closes a loop:setHeadSqlVarwrites sqlvarSQL_C_DEFAULTSQL_INT64(correct)transferStringToAllowedTypeSQL_TEXTSQL_C_SLONGSQL_TEXTconvLongToStringSQL_TEXTagainSQL_C_DEFAULTresolves toSQL_C_CHARforSQL_NUMERIC,SQL_DECIMALandSQL_BIGINT, so an application that binds its NULLs withSQL_C_DEFAULTand its values with the value's own C type retyped the parameter on its first NULL and never got it back. Every later row on that parameter was written as text into a numeric column, and — because the sqlvar's null flag was still set from the NULL row — reached the server as NULL, withSQL_SUCCESSand a correct row count.SQLFreeStmt(SQL_RESET_PARAMS)did not help: the state was in the sqlvar, not in the descriptors.SQLDescribeParamreportedSQL_CHARfor the parameter as well.SQL_INTEGER,SQL_SMALLINTandSQL_DOUBLEescaped only because theirSQL_C_DEFAULTnever resolves to a character C type.The fix
Describe INPUT parameters from the prepare-time snapshot (
orgSqlProperties) rather than the live sqlvar, which is whatgetColumnDisplaySize()already did. A bind then always sees the type the statement was prepared with, andsetHeadSqlVarshapes the sqlvar back to it. OUTPUT columns keep reading the live sqlvar, which is never rewritten.getPrecisionis deliberately untouched here — this fix does not need it, and #292 already changes it.Tests
Five tests in
tests/test_param_conversions.cpp. Four fail on master and pass with the fix;NullAsDefaultThenIntegerRebindpasses both ways and is the control that documents whySQL_INTEGERwas unaffected.Verified on Windows against Firebird 5.0.3 with the Microsoft driver manager, using the reproduction matrix from the issue (24 cells over
NUMERIC(9,3),DECIMAL(18,2),BIGINT,INTEGER,SMALLINT,DOUBLE PRECISION): 6 lost cells on master, 0 with this branch. The full suite passes in all three charset configurations.Still open
Two neighbouring defects on the numeric-to-string parameter path are not addressed here, and both are visible with a single binding whose indicator toggles NULL then value on a
VARCHAR/CHARparameter:ODBCCONVERT_CHECKNULLnever clears the target null flag on the not-null path, unlike its two sibling macros. For a sqlda target that flag survives between executes, so a value after a NULL is sent as NULL.6for60.They belong with #292, which owns that code path; #292 as it stands fixes neither. Both now sit on that PR, with tests.