Resolve SQL_C_DEFAULT bound columns at fetch time - #460
Conversation
There was a problem hiding this comment.
Pull request overview
Enables deferred SQL_C_DEFAULT resolution for bound fetch columns. However, the shared resolver introduces undocumented fetch-side parity deviations for wide-character and GUID columns.
Changes:
- Accept and retain
SQL_C_DEFAULTinSQLBindCol. - Resolve bindings from result metadata and ODBC version during fetch.
- Add unit/e2e coverage and update documentation.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
CHANGELOG.md |
Records the behavior change. |
mssql-odbc/docs/typed-columnar-fetch-plan.md |
Removes the previous limitation. |
mssql-odbc/src/api/bind_col.rs |
Defers default target resolution. |
mssql-odbc/src/api/fetch_scroll.rs |
Resolves default types per fetch. |
mssql-odbc/tests/e2e/tests/fetch_scroll_test.cpp |
Tests integer and character rowset bindings. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-odbc/src/api/fetch_scroll.rs🔗 Quick Links |
Extract the per-fetch resolution into resolve_default_bindings, with the two deviations from msodbcsql's Sql2CDefault stated where the mapping is applied: wide types resolve to SQL_C_WCHAR and SQL_GUID to SQL_C_GUID, where msodbcsql resolves both to its ANSI SQL_C_CHAR. The reference behaviour is measured against msodbcsql18 rather than inferred, and the GUID case is called out as the one that also narrows the rowset stride. Add unit coverage for both deviations and for a binding with no matching result column, and an e2e test that pins the wide indicator in UTF-16 bytes and the GUID stride at sizeof(SQLGUID). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9778c7a-00a8-4446-a5d8-1d645cc659f8
The test asserts behaviour that deliberately differs from msodbcsql, so comparing it always reports a divergence and fails the parity gate. Use the existing SKIP_IF_COMPARING_MSODBCSQL() escape hatch: the assertions still run against mssql-odbc on both platforms, and the reference leg has nothing to diverge. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9778c7a-00a8-4446-a5d8-1d645cc659f8
ttk (Theekshna)
left a comment
There was a problem hiding this comment.
This unattended review found no issues in the proposed change.
Covered deferred SQL_C_DEFAULT resolution, per-result-set rebinding, ODBC-version mappings, buffer strides, lock ordering, unit/e2e test sufficiency, msodbcsql parity and documented divergences, CI/checklist consistency, and PR-description currency.
| Category | Count |
|---|---|
| Blocking | 0 |
| Suggestion | 0 |
| Nit | 0 |
ttk (Theekshna)
left a comment
There was a problem hiding this comment.
Summary
The design matches the reference: msodbcsql also keeps the descriptor unresolved and calls Sql2CDefault at data-fetch time (sqlcdata.h:254-257), and its BindOffset stride rule (sqlcfunc.cpp:2176-2284) matches element_stride — fixed types stride by sizeof(C type), char/binary by cbValueMax, with SQL_C_DEFAULT resolved before the stride is computed. Lock ordering follows the existing bind_param.rs:129-138 precedent. Tests are non-vacuous: removing the resolve_default_bindings call fails default_binding_resolves_from_current_result_metadata, and the e2e GUID test is well built — four slots for a rowset of two makes a BufferLength-driven stride fail an assertion inside the array rather than smash the stack.
No blocking findings. Two suggestions and three nits, the latter all on files outside the diff.
Nit — the registry entry still scopes the deviation to SQLBindParameter
.github/instructions/mssql-odbc.instructions.md:79 reads "SQL_C_DEFAULT in SQLBindParameter resolves the wide character SQL types to SQL_C_WCHAR, and SQL_GUID to SQL_C_GUID". The PR description says these deviations are registered there, but that file isn't in the diff, so the canonical registry now understates the scope. A few words.
(It also names only "the wide character SQL types" and SQL_GUID — no SQL_SS_XML — which corroborates the inline comment on the new table row.)
Nit — SQLGetData(SQL_C_DEFAULT) still answers HYC00
get_data.rs:378-393 rejects it, where msodbcsql resolves it in the same GetColData that serves both paths. Pre-existing and unchanged here, so a legitimate deferral, but the driver now answers the same placeholder two different ways depending on how the column is read. I couldn't find it tracked anywhere; worth a work item while odbc_sql_type + resolve_default_c_type are freshly wired up next door.
Nit — one more unregistered stride divergence, now reachable by default
Under 3.8 both drivers resolve time/datetimeoffset identically (rgbTRANSTYPE380, sqlcmisc.cpp:220-221), but msodbcsql's BindOffset switch has no case for SQL_C_SS_TIME2/SQL_C_SS_TIMESTAMPOFFSET, so they fall to default: dwOffset = lpbindinfo->cbValueMax (sqlcfunc.cpp:2280-2283) while element_stride returns 12 and 20. Pre-existing for explicit binds, but this PR makes it reachable without the application naming the type. Ours is the safer direction — msodbcsql with BufferLength 0 strides 0 and stacks every row in slot 0 — so I'd register it rather than change behaviour.
Not verified: no e2e run (no live server), no reproduction of the msodbcsql18 measurements, no cross-platform build.
SQL_SS_XML was listed as a deviation, but msodbcsql maps it to SQL_C_WCHAR too: an xml column bound SQL_C_DEFAULT against msodbcsql18 comes back as UTF-16 with indicator 30, not narrow bytes. It is also unreachable here, since odbc_sql_type reports xml and json columns as SQL_WLONGVARCHAR. Drop it from the table and the doc comment. The odbc_version argument was unobservable: hardcoding OdbcVersion::Odbc2 left all 1,170 tests passing, so a regression to a constant would have been silent. Add a fetch-level test that drives SQL_SS_TIME2 at 3.0 and 3.8 and distinguishes the resolved target by its per-row diagnostic. Verified it fails when the version is hardcoded. Also correct the claim that a binding past the end of the result set is reported as an unsupported target: the fill loop skips it before delivery, matching msodbcsql. Only a SQL type with no default reaches that path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9778c7a-00a8-4446-a5d8-1d645cc659f8
The deviation registry scoped SQL_C_DEFAULT resolution to SQLBindParameter, which understates it now that SQLFetchScroll resolves bound columns through the same resolver. Widen it, and record that SQL_SS_XML is not part of the deviation in either direction. Register the time/datetimeoffset stride divergence. Both drivers resolve these identically at ODBC 3.8, but msodbcsql's BindOffset has no case for the SS structs and strides by BufferLength: measured, a two-row rowset bound SQL_C_DEFAULT at BufferLength 40 puts msodbcsql's second row at offset 40 and ours at 12, indicator 12 in both. Ours is the safer direction, so the behaviour is kept and recorded rather than matched. Also record that SQLGetData still refuses SQL_C_DEFAULT, so the same placeholder is answered two ways depending on how a column is read. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9778c7a-00a8-4446-a5d8-1d645cc659f8
|
Thanks — all three nits were real, and I acted on all three in dab4f5c.
Fixed. It now says the deviation applies in both directions and names I also added the
Registered, and measured first rather than taken on the source read — registering an unverified cell is exactly what went wrong with Row 1 lands at 40, not 12, and the indicator is 12 in both drivers — so the resolved type agrees and only the stride differs, exactly as you described. Kept our behaviour for the reason you gave:
Confirmed at I have not filed the work item. Creating one is outside the actions I am authorised to take on this run, so I have deliberately left it rather than quietly skipping it — it needs a human to file, and the note in the plan doc is there so it is not lost in a review thread. Happy to be pointed at it if you would rather it were tracked before this merges. All three are documentation-only; no behaviour changed. Unit suite still 1,183 passing, fmt and clippy clean.
Worth noting the two measurements above were reproduced independently on my side against a live SQL Server and a retail msodbcsql18, and the |
ttk (Theekshna)
left a comment
There was a problem hiding this comment.
Unattended re-review: head moved (418e12bc to dab4f5c5) since my last marked review at that sha (findings=0). In between, a separate review (submitted 09:50 UTC, same sha, no auto-review marker so it didn't drive skip-logic but I treated its findings as mine to check) raised 2 suggestions + 3 nits. All five are now addressed by the two commits since:
0d4ed9c6: dropsSQL_SS_XMLfrom the deviation table/doc comment (msodbcsql maps it toSQL_C_WCHARtoo, confirmed by measurement, and it's unreachable here sinceodbc_sql_typereports xml/json asSQL_WLONGVARCHAR); adds a fetch-level test (fetch_time_column_row_state) that drives the ODBC-version-dependentSQL_SS_TIME2resolution at 3.0 and 3.8 and distinguishes the result by diagnostic code, closing the observability gap the prior review flagged (hardcoding the version left all tests passing); corrects the "binding past end of result set" claim to match what the fill loop actually does.dab4f5c5: widens theSQL_C_DEFAULTdeviation registry entry from "in SQLBindParameter" to coverSQLFetchScrolltoo; registers thetime/datetimeoffsetstride divergence (measured: msodbcsql strides byBufferLength, this driver bysizeof) with a stated rationale for keeping rather than matching; registers thatSQLGetData(SQL_C_DEFAULT)still answersHYC00while fetch-bound columns now resolve.
I checked each fix against the actual diff rather than the commit message alone (typed-columnar-fetch-plan.md, mssql-odbc.instructions.md, the new test in fetch_scroll.rs) — all match what was claimed, with the msodbcsql citations (sqlcmisc.cpp:179/218/220-221, sqlcfunc.cpp:2280-2283) consistent with what I'd expect from prior verification of this file (path+line, not quoted).
Test sufficiency: the new fetch-level test is exactly the mutation-informed gap the prior review asked for (a direct resolve_default_bindings call wouldn't have caught the wiring bug). No further gap found in this increment.
msodbcsql parity: covered above — the corrections are accurate and the newly-registered divergences are honest about which direction is safer and why.
Divergences documented: yes, this increment is entirely divergence-documentation work, done at the two locations the repo uses for it (.github/instructions/mssql-odbc.instructions.md and the design doc).
PR description/registry currency: consistent as of this head.
Verbose slop: none in the new doc/comment text — it's measurement-backed, not restating the diff.
CI: no failing checks found via gh pr checks 460.
| Category | Count |
|---|---|
| Blocking | 0 |
| Suggestion | 0 |
| Nit | 0 |
Vahid (Vahid-b)
left a comment
There was a problem hiding this comment.
Summary
SQLBindCol stops rejecting SQL_C_DEFAULT and each fetch resolves the placeholder from the current result set's column types, through the resolver already shared with SQLBindParameter. The design matches the reference driver's shape and every parity claim in the description checks out against the msodbcsql source: rgbTRANSTYPE / rgbTRANSTYPE380 at sqlcmisc.cpp:153 and :192 both read SQL_C_CHAR, // SQL_GUID, both read SQL_C_WCHAR, // SQL_XML_MAPPED, and BindOffset (sqlcfunc.cpp:2176-2284) has a SQL_C_GUID case at :2242 but none for SQL_C_SS_TIME2 / SQL_C_SS_TIMESTAMPOFFSET, so those fall to default: dwOffset = lpbindinfo->cbValueMax; fFixed = FALSE; at :2280-2283. All three newly-registered divergences are accurate.
No blocking findings. Two suggestions and four nits, inline; most are documentation precision rather than code.
Incidental win worth naming: before this PR set_desc_field.rs's set_type already accepted SQL_C_DEFAULT on an ARD record via is_valid_c_type, while SQLBindCol refused it — contradicting that function's own doc comment that "a descriptor-set type can never diverge from what direct binding accepts". This closes that.
Evidence
Measured on this head (dab4f5c5), Linux, cargo nextest run -p mssqlodbc --lib:
- Baseline: 1167 passed / 0 failed / 1 skipped.
- Mutation — drop
.saturating_sub(1)from the ordinal lookup atfetch_scroll.rs:539: 3 failures (default_binding_resolves_from_current_result_metadata,default_bindings_resolve_wide_and_guid_columns_to_typed_targets,a_default_binding_follows_the_declared_odbc_version). Guarded. - Mutation — replace the env read at
:557-564with a hardcodedOdbcVersion::Odbc3_80: exactly 1 failure,a_default_binding_follows_the_declared_odbc_version. Independently reproduces the wiring gap raised earlier and confirms the new test closes it.
Coverage
| Row | Status |
|---|---|
| Primary logic | Examined — resolve_default_bindings, fetch_scroll_safe, sql_bind_col_safe |
| Siblings | Examined — deliver_bound, deliver_fixed_bound, deliver_encoded_string, deliver_bound_plp; the SQLFetch delegation; all four parent_env() lock sites |
| Callers & implementers | Examined — every .bindings consumer (only fetch_scroll and handles/stmt), all RowWriter methods on BoundRowWriter |
| Diagnostics | Examined — RowIssue mapping, poison early-return ordering relative to free_errors |
| Tests | Examined, plus the two mutations above |
| Build, packaging, pipelines | Not applicable — no such files in the diff |
| Docs & comments | Examined — msodbcsql citations verified line by line against a local checkout |
| Description & work items | Examined — 6/6 files map to description bullets, no riders; AB#47481 linked; Changed is the right heading |
| CI evidence | Examined — all checks green, coverage comment present at 98% diff |
Not verified: no live SQL Server, so the e2e test and the msodbcsql18 probe values in the description are not reproduced; Linux host only, no Windows or macOS build; mssql-python cross-repo leg read as green rather than run.
BufferLength is ignored for a fixed-width target, which is safe when the application named that type and so accepted its width contract. A SQL_C_DEFAULT binding names nothing, so resolving a uniqueidentifier column to SQL_C_GUID wrote sizeof(SQLGUID) into whatever slot the application declared -- 16 bytes into 4 -- where msodbcsql resolves to SQL_C_CHAR and truncates inside BufferLength. Deferred resolution is what made that reachable without the application naming the C type. Leave such a binding unresolved instead, so the row fails with HYC00 rather than overrunning the slot. BufferLength 0 is exempt: it is the documented idiom for a fixed-width target and claims nothing about width. Also correct three doc claims: the GUID safety argument holds only for BufferLength >= sizeof(SQLGUID); an unresolvable column reports an unsupported target only for rows carrying a value, since NULL short-circuits to SQL_NULL_DATA; and register that varbinary/image columns resolve to the still-unimplemented SQL_C_BINARY (AB#47239). Pin the per-row return code alongside the SQLSTATE in the ODBC-version test, record why the environment version is read per fetch, and add an e2e test driving two result sets of different column types through one SQL_C_DEFAULT binding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b9778c7a-00a8-4446-a5d8-1d645cc659f8
|
Re-reviewed on this head, after #436 merged. Read the new width guard, Blocking: none. Suggestions: 1. SuggestionThis PR closes a real hole #436 left open, and the rebase is the moment to claim it — with a test — rather than let it look like an incidental side effect. The branch is The semantic part is the one worth acting on.
The descriptor path gets there because That is precisely the state #436's own summary says cannot occur:
Two things follow, and neither is a criticism of the change as written: 1. This is a stronger justification than the one the description gives. The current framing is "no known consumer binds 2. The descriptor route deserves a test, because it reaches Worth checking while you are in there, since the same rebase decides it: the new width guard reads |
…-backed bindings PR #436 moves the fetch's binding table from StmtState to the ARD, read under the DESC lock after the STMT lock is released, and reintroduces the SQLBindCol SQL_C_DEFAULT rejection this branch removed. Delete that rejection again, and reconcile resolution with the new lock scope: the column SQL types are snapshotted under the STMT lock and carried out, then applied to the ARD-derived bindings once the descriptor lock is dropped. resolve_default_bindings now takes the pre-resolved types rather than ColumnMetadata, so it no longer needs statement state. Tests move from StmtState::set_binding to SQLBindCol and read the stored placeholder back through the ARD. Both mutation guards were re-verified against the merged code: hardcoding the ODBC version still fails a_default_binding_follows_the_declared_odbc_version, and removing the narrow-buffer guard still fails both overflow tests.
Shiwani Gupta (shiwanigupta0809)
left a comment
There was a problem hiding this comment.
Reviewed — see #460 (comment)
SQLBindCol is not the only door: set_type accepts SQL_C_DEFAULT on an application descriptor and ColumnBinding::from_record keys bound-ness off SQL_DESC_DATA_PTR rather than the concise type, so a SQLSetDescField sequence reaches the fill loop too. Before this branch the two disagreed -- HY003 from SQLBindCol, HYC00 per row from the descriptor route -- which is the bind/descriptor equivalence AB#47437 is built on. Add a unit test that binds entirely through SQLSetDescField and resolves identically. It sets SQL_DESC_INDICATOR_PTR and SQL_DESC_OCTET_LENGTH_PTR separately, since SQLBindCol's single StrLen_or_Ind argument feeds both and the length is reported through the latter. Record that justification in the plan doc, and note on the width guard that SQL_DESC_OCTET_LENGTH is independently writable after a bind, which is why the check runs per fetch on the same snapshot the fill loop uses.
|
Thanks — the descriptor-route point was the valuable one, and I acted on both parts in 2122e20. First, a timing note: the rebase you flagged as outstanding is done. #436 merged at 17:20 UTC and I reconciled in 09a2085 a few minutes before your review posted, so
Agreed, and added — Writing it surfaced something worth recording: the descriptor API exposes
Taken. The bind/descriptor equivalence framing is now in the plan doc with the mechanism spelled out, replacing the speculative "no known consumer" reasoning.
Good catch, and you are right that the code is correct but the comment was not carrying its weight. 1,237 unit tests pass, fmt and clippy clean. |
ttk (Theekshna)
left a comment
There was a problem hiding this comment.
Unattended re-review: head moved from dab4f5c5 (my last marked review, findings=0) to 2122e20d via two new commits — f90e2419 (refuse a SQL_C_DEFAULT fixed-width target wider than the bound BufferLength) and 2122e20d (cover the descriptor/SQLSetDescField route into the same resolution path) — plus merge commits pulling in unrelated #436/#453/#463 work with no conflicts in this PR's own files.
Reviewed both new commits in full against mssql-odbc/src/api/fetch_scroll.rs and the two docs they touch.
| Severity | Count |
|---|---|
| Blocking | 0 |
| Suggestion | 0 |
| Nit | 0 |
What I checked:
- The new narrow-buffer guard in
resolve_default_bindingsis generic overelement_stride, not GUID-specific despite the PR description's framing — it correctly also coversSQL_C_TYPE_DATE/TIME/TIMESTAMPandSQL_C_SS_TIME2/SQL_C_SS_TIMESTAMPOFFSETat ODBC 3.8, since all route through the samefixed_width > 0 && buffer_length > 0 && buffer_length < fixed_widthcheck. Only the GUID case is unit-tested directly, but since the guard's correctness depends only on the arithmetic comparison (not on which C type triggers it), one concrete instantiation is adequate coverage; I don't think a test per fixed-width type is needed here. BufferLength == 0exemption verified correct and tested (a_default_binding_too_narrow_for_its_fixed_target_stays_unresolved) — matches the documented idiom for fixed-width targets.- New overrun test (
a_narrow_default_guid_binding_never_writes_past_the_declared_buffer) asserts bytes pastBufferLengthare untouched using an oversized backing array — good regression shape, confirms the guard stops the write rather than just returning early. - Descriptor-route fix: verified
set_type/is_valid_c_type/canonical_c_typeandColumnBinding::from_recordin the current tree — the bind/descriptor disagreement (HY003viaSQLBindColvsHYC00per-row viaSQLSetDescField) is real and the fix is the right one (key both paths through the sameresolve_default_bindings). New unit test drives the descriptor path standalone and asserts identical resolution. - msodbcsql parity: N/A for these two commits specifically (no new parity claims beyond what was already reviewed and documented at
dab4f5c5; GUID/wide-char deviations were already verified againstC:\REPOS\Drivers\ODBC_2in the earlier review of this PR). - Divergences: both new behaviors (narrow-buffer refusal, descriptor-route equivalence) are documented in
.github/instructions/mssql-odbc.instructions.mdandmssql-odbc/docs/typed-columnar-fetch-plan.md, consistent with the code. - PR description: matches current diff and testing claims; linked to
AB#47481.cargo check -p mssqlodbc --lib --tests --offlinecompiles clean; fullnextestrun not possible in this sandbox (pre-existing local toolchain codegen limitation incrc32fast, unrelated to this PR) — CI (build 171912) was still pending on all build/test legs at review time, no failures observed. - No AI slop found in the new doc comments — they explain why (lock ordering, snapshot-vs-bind-time consistency, ARD as source of truth), not restating the diff.
No re-filed findings — nothing outstanding from prior threads on this PR to re-check; all were already resolved before this push.
Summary
SQL_C_DEFAULTinSQLBindColinstead of rejecting it withHY003.resolve_default_bindings.BufferLength, so deferred resolution cannot overrun a bound slot.AB#47481
Why this is needed
Since AB#47437 (#436) the ARD is the fetch's single source of truth, and
SQLBindColand an equivalentSQLSetDescFieldsequence are meant to be indistinguishable.SQL_C_DEFAULTwas the exception:SQLBindCol(1, SQL_C_DEFAULT, buf, n, &ind)SQL_ERROR,HY003SQLSetDescField(ard, 1, SQL_DESC_CONCISE_TYPE, SQL_C_DEFAULT)+SQL_DESC_DATA_PTRHYC00per rowThe descriptor route gets there because
set_typetakes thekind.is_application()branch,canonical_c_typeleavesSQL_C_DEFAULTalone, andis_valid_c_typelists it — whileColumnBinding::from_recordkeys "bound" off a non-nullSQL_DESC_DATA_PTRrather than the concise type. Both doors now resolve identically, which restores the equivalence #436 was built to guarantee.Parity deviations
Resolution reuses
type_rules::resolve_default_c_type, shared withSQLBindParameter, so the driver gives one answer to whatSQL_C_DEFAULTmeans regardless of direction. That resolver carries two deliberate deviations from msodbcsql'sSql2CDefault, accepted in #323 and registered inparameters_plan.md,mssql-odbc.instructions.md, and AB#47365. This PR makes them apply on the fetch path too, so they are restated where the mapping is used, and the registry entry — which previously scoped them toSQLBindParameter— is widened to cover both directions:SQL_WCHAR,SQL_WVARCHAR,SQL_WLONGVARCHARSQL_C_WCHARSQL_C_CHARSQL_GUIDSQL_C_GUIDSQL_C_CHARBoth cells are measured against msodbcsql18, not inferred from the
Sql2CDefaulttables. Binding annvarcharand auniqueidentifiercolumn withSQL_C_DEFAULTandBufferLength64 returns the wide value as the three narrow bytes6F 6E 65with indicator3, and the GUID as the 36-character text form with indicator36.SQL_SS_XMLis deliberately absent from that table: msodbcsql maps it toSQL_C_WCHARas well (anxmlcolumn boundSQL_C_DEFAULTreturns UTF-16 with indicator30), so there is no deviation, and it is unreachable on this path becausedescribe_col.rsreports xml and json columns asSQL_WLONGVARCHAR.The wide deviation exists because this driver's
SQL_C_CHARis UTF-8 by design; msodbcsql's narrow default reads the client code page, which has no equivalent here. TheSQL_GUIDdeviation is the one that also changes the bound rowset layout: a fixed-width target strides by its C type rather than byBufferLength, so the stride becomessizeof(SQLGUID). A slot at least 16 bytes wide — including the 36-character text form msodbcsql would fill — takes the narrower stride and stays inside the application's array.Narrow-buffer guard
BufferLengthis ignored for a fixed-width target, which is safe when the application named that type and so accepted its width contract. ASQL_C_DEFAULTbinding names nothing, so resolving auniqueidentifiercolumn toSQL_C_GUIDwould write 16 bytes into whatever slot the application declared — 4 bytes, say — where msodbcsql resolves toSQL_C_CHARand truncates insideBufferLength. Deferred resolution is what makes that reachable without the application naming the C type.Such a binding is therefore left unresolved and the row fails with
HYC00rather than writing. The check is generic overelement_striderather than GUID-specific:SQL_C_TYPE_DATE/TIME/TIMESTAMPand, at ODBC 3.8,SQL_C_SS_TIME2/SQL_C_SS_TIMESTAMPOFFSETall route through the samefixed_width > 0 && buffer_length > 0 && buffer_length < fixed_widthcomparison. GUID is the motivating case and the one unit-tested directly, since the guard's correctness depends on the arithmetic rather than on which C type triggers it.BufferLength0 is exempt: it is the documented idiom for a fixed-width target and carries no width claim. The width isSQL_DESC_OCTET_LENGTHon the ARD record, which an application can rewrite after binding without touching the type — checking it per fetch against the same snapshot the fill loop uses is what keeps a slot narrowed after the bind from slipping through. Whether these should instead report01004and deliver a truncated value, matching msodbcsql more closely, is open and untracked.Other newly-reachable cases, registered not changed
SQL_SS_TIME2andSQL_SS_TIMESTAMPOFFSETdefault toSQL_C_BINARYbelow ODBC 3.8 and to theirSQL_C_SS_*types at 3.8. That 3.8 mapping makes a stride-only divergence reachable: both drivers resolve identically, but msodbcsql'sBindOffsetstrides byBufferLengthwhereelement_strideuses 12 / 20. Measured atBufferLength40, msodbcsql puts row 1 at byte offset 40 and this driver at 12, indicator 12 in both. Ours is the safer direction —BufferLength0 would make msodbcsql stride 0 and stack every row in slot 0.varbinary/imagecolumn resolves toSQL_C_BINARY, which bound delivery does not implement yet (AB#47239), so it fails per row withHYC00. msodbcsql resolves identically and delivers the bytes.A column whose SQL type has no default keeps
SQL_C_DEFAULTand is reported as an unsupported target for any row carrying a value; a NULL row still reportsSQL_NULL_DATAthrough its indicator, because nothing needs converting. A binding whose ordinal is past the end of the result set also stays unresolved, but never reaches delivery — the fill loop skips it, matching msodbcsql.Known asymmetry, pre-existing and untouched:
SQLGetDatastill refusesSQL_C_DEFAULTwithHYC00, so the same placeholder is answered two ways depending on how a column is read. Recorded intyped-columnar-fetch-plan.md; not currently tracked by a work item.Testing
cargo nextest run -p mssqlodbc: 1,237 passed, 1 skipped.cargo clippy --workspace --all-features --all-targets -- -D warningscargo fmt -- --checkelement_stride; a binding whose column is absent from the result set staying unresolved;a_default_binding_follows_the_declared_odbc_version, a fetch-level test driving aSQL_SS_TIME2column at ODBC 3.0 and 3.8 that pins both the per-rowSqlReturnand its SQLSTATE (HYC00vs07006); two tests for the narrow-buffer guard, one of which uses a 64-byte backing array behind a declaredBufferLengthof 4 so a regression surfaces as bytes written past the declared width rather than as a crash; anda_default_binding_set_through_the_descriptor_api_also_resolves, which binds entirely throughSQLSetDescFieldto cover the second door into the resolver. Each guard was confirmed to fail with its guard removed.DefaultTargetResolvesWideAndGuidToTypedTargetspins the wide indicator in UTF-16 bytes and the GUID stride atsizeof(SQLGUID), binding four GUID slots with aBufferLengthof two so aBufferLength-driven stride fails an assertion inside the array rather than running off the end. It asserts a deliberate deviation, so it carriesSKIP_IF_COMPARING_MSODBCSQL().DefaultTargetResolvesPerResultSetdrives two result sets of different column types through oneSQL_C_DEFAULTbinding. It deliberately usesintand narrowvarchar, which both drivers resolve identically, so it runs and compares on the msodbcsql leg too.Breaking changes / migration notes
None. The narrow-buffer guard only affects
SQL_C_DEFAULTbindings that would previously have overrun the application's declared buffer.