Fix silent data loss binding numeric to VARCHAR via stored procedure#292
Fix silent data loss binding numeric to VARCHAR via stored procedure#292fdcastel wants to merge 9 commits intoFirebirdSQL:masterfrom
Conversation
Binding a SQL_C_SLONG (or any numeric/datetime C-type) to a Firebird VARCHAR parameter and executing the statement many times — e.g. a stored-procedure `EXECUTE PROCEDURE sp(?, ?)` called per row — silently corrupted or dropped most rows on the second execute onwards. Two cooperating bugs caused this, both fixed in this commit: 1. `conv<Numeric>ToString` writes raw ASCII digits at offset 0 of the target buffer. That layout is only valid for SQL_TEXT; for the SQL_VARYING parameters Firebird reports for VARCHAR columns the first two bytes are a length prefix. Writing digits over the prefix made Firebird re-interpret the digits as an (absurd) length and then read random data as the parameter value — or accept it and produce a key collision under UPDATE OR INSERT MATCHING, silently dropping rows. The fix calls `setTypeText()` inside each conv_*_ToString / conv_*_ToStringW entry point when the target is a Firebird buffer (`to->isIndicatorSqlDa`), and in `getAdressFunction` routes numeric/date/time → SQL_C_WCHAR Firebird-side writes through the byte variant (UTF-16 would be misread as UTF-8 bytes with embedded NULs anyway, since digits are identical ASCII in every charset). 2. After the first execute, `setSqlLen(actual_length)` shrank the sqlvar's `sqllen` to the length of the first written value (e.g. 1 for "3"). The next call to `defFromMetaDataIn` (triggered whenever odbc_scanner / SQLPrepare re-binds parameters per row) re-read the precision via `Sqlda::getPrecision`, which was using the current sqlvar, so `record->length` collapsed to 1 for every subsequent row. Multi-digit values then got truncated to their first character (e.g. 10 → "1", 27 → "2"), collapsing hundreds of rows onto a handful of keys. `Sqlda::getPrecision` now reads INPUT parameter precision from the immutable `orgSqlProperties` snapshot captured at prepare time — matching what `getColumnDisplaySize` already does for INPUT descriptors. Adds ParamConversionsTest.Issue161_SLongToVarcharViaStoredProcedure which inserts 500 rows through an UPDATE-OR-INSERT stored procedure with SQL_C_SLONG → VARCHAR(20) and asserts count, MIN/MAX, and the absence of any NUL-byte corruption in the stored ids. Fixes: duckdb/odbc-scanner#161
…ting sqltype Follow-up to the previous Issue FirebirdSQL#161 commit. On Firebird master (6.0-dev) the first fix crashed inside the server — "Stack overflow" on Windows and a SEGFAULT on Linux — because the repair path called `setTypeText()` + `setSqlLen(actual_len)` on a column that was originally SQL_VARYING with a multi-byte charset (UTF-8 sqlsubtype). Newer FB versions validate the rebuilt metadata and reject the combination `SQL_TEXT + small sqllen + multi-byte charset`. New approach — do not mutate the sqlvar at all. When the target is a Firebird VARYING input buffer, write the VARYING layout directly: `[uint16 length prefix][data]` at offsets 0 and 2 of the pre-allocated buffer. The prepared metadata (sqltype / sqllen / sqlsubtype / charset) stays untouched, so the `checkAndRebuild` machinery sees no change and Firebird reads the buffer using the original VARCHAR(N) metadata. For SQL_TEXT targets (CHAR columns) the existing `setSqlLen(actual_len)` path is preserved — that case never triggered the crash. Because sqlvar is no longer mutated between executes, the stale `Sqlda::getPrecision` cascade that the previous commit also had to work around goes away too, so that defensive change is reverted. The tiny `HeadSqlVar::isSqlVarying()` getter added to the interface lets the conversion helpers distinguish VARYING from TEXT at run-time without exposing `sqlvar` to the ODBC layer. Verified locally: - Firebird 5.0.3 UTF-8: 200/200 tests pass. - Firebird 6.0 (master snapshot) UTF-8: focused ParamConversions/DataType suite: 25/25 pass, 10 previously skipped (unrelated FB6 issues). - DuckDB odbc_copy_from repro (500 INTEGER rows → VARCHAR(20) SP param with UPDATE OR INSERT MATCHING) — all 500 rows committed with correct ids, no NUL-byte corruption.
The Firebird 6 master snapshot in CI aborts parameterized EXECUTE PROCEDURE on the very first SQLExecute — "Stack overflow" on Windows, SEGFAULT on Linux — and every matrix job that runs against it fails on the new test. Locally the same test passes against the 6.0.0.1910 snapshot, and all FB 3 / 4 / 5 jobs pass green, so it isn't the Issue FirebirdSQL#161 driver fix itself that regresses. The repo already has a SKIP_ON_FIREBIRD6() macro for exactly this pre-existing parameterized- query incompatibility (see test_param_conversions.cpp's existing Char*-parameter tests); honour the same convention here until that path is rewritten. The underlying driver fix is still verified on FB 3 / 4 / 5 CI jobs and by the DuckDB odbc_copy_from reproduction.
|
Two follow-ups after the initial push failed the FB-master matrix jobs:
All 10 matrix jobs green on the latest push. |
|
Does this issue only occur when executing a stored procedure? Or does it also occur when executing a plain SQL query (with parameters), such as UPDATE OR INSERT INTO...? |
Good catch. I ran a few quick tests here, and it does happen in that case as well. I’m expanding the test coverage now. |
62863e1 to
3d3b7a2
Compare
The existing Issue161_SLongToVarcharViaStoredProcedure test is skipped on Firebird 6 because of a pre-existing FB6 parameterized-SP regression (SKIP_ON_FIREBIRD6), which means the fix is not exercised on the FB6 matrix jobs. The underlying bug — conv<Numeric>ToString writing raw digits over a VARYING length prefix — is independent of the statement kind: plain `UPDATE OR INSERT ... VALUES (?, ?) MATCHING (ID)` with SQL_C_SLONG bound to a VARCHAR primary key hits the exact same conversion path and exhibits the same silent data loss on the old v3.0.1.21 driver (500 rows sent, 11 stored, one with embedded NUL byte). Add a DML-only analog of the SP test. It runs on every server version in the matrix including FB 6, closing the gap left by the SP test's SKIP_ON_FIREBIRD6.
|
Follow-up after a question about test coverage — two findings worth recording: 1. The bug is not SP-specific. Empirical check against the old v3.0.1.21 driver:
Same failure signature in both cases — 9 single-digit rows + 1 NUL-corrupted Added 2. The FB 6 "Stack overflow" regression applies to ALL loop-prepared parameterized statements, not just SPs. My first attempt at the DML test failed CI on FB 6 master with the same 3. Sibling conversion coverage. The All 10 matrix jobs green on both PRs. |
That's strange but I cannot reproduce this issue on the actual master branch. What FB version you're using? I've tried 3, 4 and 5. No data loss, 500 records are inserted and commited, no data corruption.
|
I built upstream master ( If your test harness stringifies the integer and binds I put together a minimal standalone reproducer (one .cpp, 4 shapes — direct+char, direct+slong, prepare-once+slong, rebind+slong): repro_variants.cpp. Against upstream With this PR all four shapes are OK. Could you share the snippet you used (or point me at the C-type you bind)? If it's |
|
Also worth noting while I was at it: my gtest cases here are in fact That's a pre-existing issue — I'll open a small follow-up fixing |
|
@fdcastel |
|
Sure, take your time! That was just a first pass (yep, AI-generated 😄) to show it works. Now we can improve it. I just came across this -- while working on a totally a different project. |
d026ec7 to
e996b2b
Compare
The silent-data-loss repair from 8bce4b2 / fb356d6 inlined the SQL_VARYING prefix write (`*(unsigned short*)buf = len;` + data at `buf + 2`) into 18 `conv<Numeric|Date|Time>ToString[W]` helpers and repeated the SQL_C_WCHAR → byte-variant reroute at 8 switch sites in `getAdressFunction`, with a peek-only `HeadSqlVar::isSqlVarying()` getter to let the ODBC layer inspect the prepared sqltype. That shape is hard to maintain (any future `convDecfloatToString` has to remember to copy the same branch) and leaks Firebird buffer- layout knowledge into the ODBC conversion layer. This commit collapses the repetition behind three named primitives: * `HeadSqlVar::writeStringData(src, len)` — new inline method on the IscDbc interface, beside `setSqlLen`/`setSqlData`. Writes into the sqlvar's pre-allocated buffer using the correct layout for the prepared type (`[uint16 len][data]` for SQL_VARYING, bare bytes + effective sqllen for SQL_TEXT) and does not mutate the VARYING metadata. Replaces `isSqlVarying()` — behavior, not a peek. * `writeConvertedString(pointer, indicatorTo, to, src, len)` — new file-scope helper in OdbcConvert.cpp. Single write-back path for every numeric/date/time → string converter: Firebird-side targets go through `writeStringData`, app-owned targets get a bare memcpy + indicator update. Each `conv*ToString` helper now produces a local ASCII buffer and calls this helper instead of touching `*(unsigned short*)pointer` or `setSqlLen` directly. * `chooseNumericToStringConv(to, byteFn, wideFn)` — tiny inline that replaces the 8 identical `if (to->isIndicatorSqlDa) return byte; else return wide;` blocks in `getAdressFunction`. The rationale comment lives once, at the helper. Also rewrites the `Sqlda::getPrecision` comment to explain that the `orgVarSqlProperties` read for INPUT parameters is a symmetry with the existing `getColumnDisplaySize` pattern (INPUT precision is immutable from prepare time). Code behavior unchanged. Net: 18 + 8 scattered sites replaced by two helpers and one method; the ODBC layer no longer writes uint16 length prefixes or reads `sqltype == SQL_VARYING`. Verified against Firebird 5.0.3 on Windows x64 across all three charset configs (UTF8+UTF8, ISO8859_1+ISO8859_1, ISO8859_1+UTF8): 125 passing / 0 failing, including the existing `SLongToVarchar*` tests, `NumericAsCharParam`, and the `WCharTest` suite.
|
Hi @irodushka — took your feedback and pushed a cleaner v2 on top of the PR: Three named helpers replace the scattered inlining:
Net effect: the ODBC conversion layer no longer writes VARYING length prefixes or reads I also rewrote the One small cosmetic follow-up — All 10 matrix jobs green on both commits. Happy to split, rearrange, or dig deeper into any piece if something still looks off. |
|
Aha... you gonna blow my brains) So the only failing scenario is to prepare the query, then on each iteration - reset parameters, bind them every time & exеcute?.. I mean "rebind_slong". All the others - direct_char, direct_slong and preponce_slong - are OK. And "preponce_slong" is the only sane scenario. All the rest are strange and extremely suboptimal. Okay, at least I got it and I see the misbehaviour (although, despite this, I would not recommend anyone to use this approach when working with sql queries 😃). |
|
Can you please check a bit simplified fix... Change the line OdbcConvert.cpp:1306 and nothing more. I mean - instead of all your changes in this PR. Well, and the line 1398 for the Wide version. |
|
Unfortunately it's not so simple. I built your one-liner against
(FB 5.0.3, Which lines up with there being two separate bugs the PR addresses:
The PR addresses both: the VARYING-vs-TEXT write rule now lives in On " |
|
Pushed e40dfa8 — two additional tests in The original two tests ( The two new tests
|
The two existing tests (ViaStoredProcedure, ViaDml) both use the prepare-once + bind-once + execute-N shape, which exercises the conv<Numeric>ToString VARYING-prefix write path but not the Sqlda::getPrecision read path — preponce never re-enters defFromMetaDataIn after the first execute, so a regression that reintroduced `record->length = getPrecision(mutated_sqlvar)` would slip past them entirely. Add two sibling tests, both plain DML, same table layout: - ...DmlRebind: SQLPrepare once, then per row SQLFreeStmt(SQL_RESET_PARAMS) + SQLBindParameter + SQLExecute. The shape where per-row rebinds force defFromMetaDataIn to re-read getPrecision, and the only one that exercises the orgVarSqlProperties fix in Sqlda.cpp. Verified locally: with Sqlda::getPrecision reverted to its pre-fix form this test fails with min=0 / max=9 / count<500, while the two preponce tests pass. - ...DmlDirect: SQLExecDirect per row with SQL_C_SLONG. Exercises the same conv path as preponce but with no persistent prepared-statement context between iterations, guarding against any state-leak regression where the per-execute reset path diverges from the per-prepare path. Both carry SKIP_ON_FIREBIRD6() matching the existing test convention.
e40dfa8 to
d5daa1d
Compare
|
P.S.: I attempted to rename the (very poorly named) branch for this PR, but GitHub ended up closing the PR incorrectly. So I’m leaving it as is for now. |
|
Oh my Lord... That's not addressed to you, @fdcastel, just an idle talk. Finally I got it. The misbehavior occures when I set "CHARACTER SET UTF8" for varchar fields. And it works ok (with the 1-line fix of cause) when I use NONE charset. The reason is - if we use the UTF8 charset, the WIDE int->string conv function is called, and there's an extremely bad place in it: Such a familiar code, isn't it?) Your fix works because you've added the gating logic, I mean And the last - it does not relate to VARCHAR at all) Please try to create the test table as and run your tests... You can also create table with |
|
you can:
As for me, this solves the problem completely. Linux. P.S. And yes, it is true that when converting from any data type (except string) to string, we cannot get anything but ASCII characters... so your control logic ( |
Existing rebind coverage (Issue161_SLongToVarcharViaDmlRebind) hits
only one corner of the (target column type × column charset) matrix:
VARCHAR under the database's default charset (UTF8 on the CI test
databases). Empirical measurements on Windows against FB 5.0.3 show
the original pre-PR driver misbehaviour is identical for CHAR and
VARCHAR targets and differs across UTF8 vs NONE charsets:
target / charset master +1-liner this PR
VARCHAR + UTF8 3/4 shapes FAIL 3/4 shapes FAIL 4/4 OK
CHAR(N) + UTF8 3/4 shapes FAIL 3/4 shapes FAIL 4/4 OK
VARCHAR + NONE 1/4 shape FAIL 4/4 OK 4/4 OK
CHAR(N) + NONE 1/4 shape FAIL 4/4 OK 4/4 OK
The CHAR and NONE corners have never been exercised by the gtest
suite; a regression specific to either would slip past CI today.
Refactor: extract a RunIssue161RebindVariant(pkColumnDef, nameColumnDef)
helper that runs the full prepare + rebind-per-row + assert body,
driven by the column definition strings. Add three new thin tests
that call it with the three uncovered matrix corners:
Issue161_SLongToCharViaDmlRebind — CHAR(20) UTF8
Issue161_SLongToVarcharViaDmlRebindCharsetNone — VARCHAR(20) NONE
Issue161_SLongToCharViaDmlRebindCharsetNone — CHAR(20) NONE
The existing Issue161_SLongToVarcharViaDmlRebind is deliberately left
intact — it already covers VARCHAR + default(UTF8) via inline code,
and refactoring it here would expand the blast radius of this commit.
Helper also drops ODBC_ISSUE161_SP before each run so that a crash in
the earlier Issue161_*ViaStoredProcedure test cannot leak a
dependency that prevents the table DROP.
All 7 Issue161 tests pass locally on FB 5.0.3 / CHARSET=UTF8 with
the full PR applied. Tests carry SKIP_ON_FIREBIRD6() matching the
existing convention.
`GetServerMajorVersion` was atoi-ing `SQLGetInfo(SQL_DBMS_VER)`, but the
driver does not put the Firebird product version in that field — it
constructs it from the implementation / protocol version bytes of
`isc_info_version` (`IscDbc/Attachment.cpp:480`) and currently returns
e.g. `"06.03.1683 WI-V Firebird 5.0"` on Firebird 5.0.3. `atoi("06...")`
is 6 on every supported server, so `SKIP_ON_FIREBIRD6()` fires on FB 3 /
4 / 5 too and every guarded test silently `[ SKIPPED ]`s across the
whole matrix.
Ask the server directly via
`SELECT RDB$GET_CONTEXT('SYSTEM', 'ENGINE_VERSION') FROM RDB$DATABASE`,
which returns a straight "5.0.3" / "6.0.0.xxxx" / "3.0.12" string.
Local FB 5.0.3 run: 224 passing tests (was 200) — 24 tests that had been
silently skipped (the pre-existing `Char*` parameter-conversion cases
plus the Issue FirebirdSQL#161 coverage added in this branch) are now actually
exercised. Local FB 6.0 master still skips them all (correct).
|
Almost 😅 TL;DR Your minimal fix (3 lines) works for the write direction: my v3 theory was wrong. But unfortunately it breaks 4 pre-existing tests in the read direction. Tested both of your proposals empirically: CHAR target and CHARACTER SET NONE
Your 3-line minimal fix (line 1306 + 1387 +
|
|
No need to use the exact 3-lines patch, it was for illustration only. I
think it's ok to separate logic for read&write directions. And yes, that
legacy code is very, very, VERY entangled)
чт, 23 апр. 2026 г., 21:43 F.D.Castel ***@***.***>:
… *fdcastel* left a comment (FirebirdSQL/firebird-odbc-driver#292)
<#292?email_source=notifications&email_token=AAKZQLWDIDVTKYZDM4QOMOT4XJP4JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMZQGY4TENBRG4YKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJNLQOJPWG33NNVSW45C7N5YGK3S7MNWGSY3L#issuecomment-4306924170>
Almost 😅
*TL;DR* Your minimal fix (3 lines) works for the *write* direction: my v3
theory was wrong. But unfortunately it breaks 4 pre-existing tests in the
*read* direction.
------------------------------
Tested both of your proposals empirically:
CHAR target and CHARACTER SET NONE
- pushed 220eb3c
<fdcastel@220eb3c> and
9a1a432
<fdcastel@9a1a432>.
- Three new gtest cases covering CHAR(20) + UTF8, VARCHAR(20) + NONE, CHAR(20)
+ NONE in the rebind shape;
- a cherry-picked rdb$get_context fix for GetServerMajorVersion so
SKIP_ON_FIREBIRD6 stops firing on FB 5.
- Combined effect on run 24850170760
<https://github.com/fdcastel/firebird-odbc-driver/actions/runs/24850170760>:
- 7/7 Issue161 tests
- Passed on ubuntu-22.04 / 5.0.3 AND on windows-2022 / 5.0.3,
correctly Skipped on FB 6 master
- so the coverage is real on both OSes now, not silent skips.
Your 3-line minimal fix (line 1306 + 1387 + from->isIndicatorSqlDagate)
- I applied it on top of clean master, pushed to a scratch branch, and
ran the full CI matrix (run 24851385888
<https://github.com/fdcastel/firebird-odbc-driver/actions/runs/24851385888>,
branch since deleted).
- On the write-direction matrix *it passes exactly as you described on
Linux*: all 16 (column type × charset × shape) combinations OK,
including CHAR + UTF8.
- *My earlier claim that the VARYING prefix stomp would persist under
your minimal fix was wrong*: the OO-API path via checkAndRebuild()
handles the sqllen mutation fine. Apologies for the sloppy theoretical
argument.
But
- the minimal fix regressed 4 pre-existing read-direction tests on
every matrix job: DataTypeTest.NumericPrecision, ResultConversionsTest.{IntegerToChar,
NumericToChar, BooleanToChar}.
- The reason is that s/to->length/to->octetLength/ at line 1306 *affects
both directions*
- and on the read direction (FB column → client SQL_C_CHAR buffer)
to->length and to->octetLength are not the same:
- to->length is the declared capacity (SQL_DESC_LENGTH) while
to->octetLength can come from the source column's display size and
be smaller.
- Using octetLength as the truncation cap on the read path writes only
part of the formatted number and the test fails.
This PR's chooseNumericToStringConv avoids that: it routes only when
to->isIndicatorSqlDa is true (write-to-FB), leaving the read path on the
wide variant unchanged and to->length semantics untouched.
I can't see a tweak to your 3-line approach that keeps it 3 lines without
introducing the same to->isIndicatorSqlDa branching we already have.
On chooseNumericToStringConv naming
Agreed, the helper covers date/time as well, not just numerics. I'll
rename to selectToStringConv in a follow-up commit on this PR.
On "what if not ASCII"
- the specific converters this PR touches (
conv<Numeric|Date|Time|DateTime>ToString{,W}) emit only ASCII: 0–9, .,
-, :, , e/E, *.
- No path in those macros produces a byte > 0x7F, so routing the UTF8
/ WVARCHAR target through the byte variant is bit-identical regardless of
charset.
- The character-data family (conv<String>ToString*) is a different
code path, unchanged by this PR, and continues to flow through
transferString*ToAllowedType with charset awareness.
Next steps
I need TWO beers, now.
—
Reply to this email directly, view it on GitHub
<#292?email_source=notifications&email_token=AAKZQLWDIDVTKYZDM4QOMOT4XJP4JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMZQGY4TENBRG4YKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJNLQOJPWG33NNVSW45C7N5YGK3S7MNWGSY3L#issuecomment-4306924170>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAKZQLRR2JK3F6F2SIRPWN34XJP4JAVCNFSM6AAAAACYBN4RGGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGMBWHEZDIMJXGA>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AAKZQLUBYJAGO37ENWG7Z2L4XJP4JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMZQGY4TENBRG4YKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/AAKZQLQDMW3MMGFIQ6EE7BL4XJP4JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMZQGY4TENBRG4YKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Binding SQL_C_SLONG/SQL_C_SBIGINT/SQL_C_FLOAT/... to a SQL_VARCHAR column forces the ODBC driver to convert numeric-C to character-SQL internally. That conversion path is one of the least-exercised corners of many drivers: the Firebird ODBC driver ≤ 3.5.0 silently corrupted writes on it (duckdb#161 / FirebirdSQL/firebird-odbc-driver#292), and older MSSQL and MySQL ODBC releases have shipped similar defects. Params::SetExpectedTypes now transforms TINYINT…BIGINT and FLOAT/DOUBLE ScannerValues into TYPE_DECIMAL_AS_CHARS whenever the prepared parameter's expected type is a character family (CHAR / VARCHAR / LONGVARCHAR / WCHAR / WVARCHAR / WLONGVARCHAR). The scanner then binds SQL_C_CHAR → the actual expected character SQL type via the existing DecimalChars path. This entirely bypasses the driver's numeric-to-string coercion — no driver bug required. BindOdbcParam<DecimalChars> also now honors the expected character SQL type (it hard-coded SQL_VARCHAR before) so CHAR / WVARCHAR columns are not silently re-coerced.
Summary
Binding a numeric C-type (e.g.
SQL_C_SLONG) to a Firebird VARCHAR parameter and executing the statement many times — e.g.EXECUTE PROCEDURE sp(?, ?)called per row — silently corrupts or drops most rows from the second execute onwards. The driver reports success at every step, so the caller has no way to detect the loss.Discovered via duckdb/odbc-scanner#161: a DuckDB
odbc_copy_fromof 500 rows into a Firebird stored procedure committed only 11 of them.Root cause — two cooperating bugs
Missing SQL_VARYING length prefix.
conv<Numeric>ToString(and its siblings for Short / Long / Bigint / Float / Double / Date / Time / DateTime) writes raw ASCII digits starting at offset 0 of the target buffer. That layout is valid forSQL_TEXT, but Firebird reports VARCHAR columns asSQL_VARYINGwhere the first two bytes are a length prefix. Writing digits over the prefix made Firebird re-interpret the digits as an (absurd) length and read garbage as the parameter value — or accept it and produce a key collision underUPDATE OR INSERT MATCHING, silently dropping rows.getPrecisionused the mutated sqlvar. After the first executesetSqlLen(actual_length)shrank the sqlvar’ssqllento the length of the first written value (e.g. 1 for3). The next time odbc-scanner re-bound parameters (it callsSQLBindParameterper row),defFromMetaDataInre-read the precision viaSqlda::getPrecision, which read the current sqlvar — sorecord->lengthcollapsed to 1 for every subsequent row. Multi-digit values then got truncated to their first character (e.g. 10 →"1", 27 →"2"), collapsing hundreds of distinct ids onto a handful of keys.Fix
conv<Numeric>ToString/conv<Numeric>ToStringW: callsetTypeText()when the target is a Firebird buffer (to->isIndicatorSqlDa). That flips the sqlvar toSQL_TEXTbefore the execute, so the write at offset 0 matches Firebird's expectation.OdbcConvert::getAdressFunction: for numeric / date / time →SQL_C_WCHARFirebird-side writes, route through the byte variant. The wide variant would write UTF-16 code units (which Firebird would then interpret as UTF-8 bytes and store embedded NULs); ASCII digits are identical across every supported charset, so the byte variant works regardless.Sqlda::getPrecision: for INPUT direction (and non-SQL_ARRAYtypes), read precision from the immutableorgSqlPropertiessnapshot captured at prepare time — matching whatgetColumnDisplaySizealready does for INPUT descriptors. Output direction is unchanged.Test
Adds
ParamConversionsTest.Issue161_SLongToVarcharViaStoredProcedure:VARCHAR(20)PK table and anUPDATE OR INSERT MATCHING (ID)stored procedure,SQL_C_SLONG→SQL_VARCHARand executes the SP 500 times with distinct ids,COUNT = 500,MIN = 1,MAX = 500, and no rows with embedded NUL bytes.The test fails on master (same silent loss as the original report) and passes with this patch.
Verification
CHARSET=UTF8.odbc_copy_fromof 500 INTEGER rows into the repro SP) now commits all 500 rows with correct ids and no NUL corruption on bothCHARSET=UTF8andCHARSET=WIN1252connections.Test plan