mssql-odbc: declare and bound data-at-execution parameters from ParameterType - #494
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Deferred sequencing and split UTF-16 padding still contain correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Aligns ODBC data-at-execution parameters with their declared SQL type, size, and encoding.
Changes:
- Selects buffered or streamed delivery from
ParameterType. - Enforces
ColumnSizeand incrementally transcodes streamed character data. - Adds unit, end-to-end, and parity coverage.
File summaries
| File | Description |
|---|---|
mssql-tds/src/message/parameters/rpc_parameters.rs |
Supports narrowed streamed declarations. |
mssql-odbc/tests/e2e/tests/param_cross_conversions_test.cpp |
Tests buffered cross-family conversion. |
mssql-odbc/tests/e2e/tests/param_char_conversions_test.cpp |
Tests character bounds and declarations. |
mssql-odbc/tests/e2e/tests/param_binary_conversions_test.cpp |
Tests binary bounds and declarations. |
mssql-odbc/tests/e2e/tests/execute_test.cpp |
Tests mixed DAE sequencing and transcoding. |
mssql-odbc/src/handles/stmt.rs |
Extends deferred DAE state. |
mssql-odbc/src/conversion/param_convert.rs |
Adds planning, limits, buffering, and transcoding. |
mssql-odbc/src/api/put_data.rs |
Processes bounded streamed and buffered chunks. |
mssql-odbc/src/api/param_data.rs |
Completes deferred and streamed execution. |
mssql-odbc/src/api/execute.rs |
Defers prepared buffered execution. |
mssql-odbc/src/api/exec_direct.rs |
Defers direct buffered execution. |
mssql-odbc/src/api/exec_common.rs |
Builds and reconstructs DAE parameters. |
mssql-odbc/src/api/cancel.rs |
Updates DAE test setup. |
.github/instructions/mssql-odbc.instructions.md |
Records parameter-order divergence. |
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
fdedbeb to
81f06bd
Compare
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Automated review from an unattended run — these findings have not been checked by a human first, and this is a comment, not an approval.
Reviewed the full diff against merge base 4382290 (13 files, ~2.6k insertions), plus the surrounding bound_param_to_rpc / conversion_matrix / cancel_streamed_write code the new paths depend on. Ran cargo nextest run -p mssqlodbc --lib: 1308 passed, 1 skipped, so the cargo btest checklist box holds locally. gh pr checks 494 shows no failing required check (4 still pending), so nothing here contradicts CI.
The overall shape reads well: keying stream-vs-buffer off IsPartialLenType rather than ColumnSize, keeping the PLP body while narrowing only the @params declaration, and only narrowing when the bound is actually enforceable are all the right calls, and the reasoning is documented where a future reader will need it. Both prior Copilot threads are genuinely addressed. One of the two, though, is only half-fixed.
1. A split pad unit still fails 22001 — the fix covers the first chunk but not the byte that completes it
DaeLengthLimit::fit now accepts a trailing partial unit that is a prefix of the pad, but it does not record that it consumed part of a unit. already only advances by consumed, so the next call re-derives split from an even max and lands the overflow window on a byte offset that is one off the application's code-unit grid. The whole-unit comparison then reads [0x00, 0x20] instead of [0x20, 0x00] and reports truncation on an overflow that is entirely blanks — exactly the failure mode the previous round was meant to remove.
Verified in the worktree with a throwaway test (removed afterwards):
let wide = dae_length_limit(SQL_C_WCHAR, SQL_WVARCHAR, 1).unwrap().unwrap();
// Application stream: 'a' + two UTF-16 blanks, against nvarchar(1).
let (kept, consumed) = wide.fit(&[b'a', 0, b' '], 0).unwrap();
assert_eq!((kept, consumed), (&[b'a', 0][..], 2));
// The 0x00 that completes the split blank, then a whole blank.
let second = wide.fit(&[0, b' ', 0], consumed);
assert!(second.is_ok(), "overflow is entirely blanks but got {second:?}");panicked at param_convert.rs: overflow is entirely blanks but got Err(StringTruncation)
The same misalignment shows up whenever a chunk boundary leaves the retained/overflow split at an odd byte offset, so it is not limited to the pad-prefix case: fit(&[b'a', 0, b'b'], 0) then fit(&[0, b' '], _) then fit(&[0], _) walks into it too. dae_limit_fit_accepts_a_pad_unit_split_across_chunks passes because it only ever calls fit once for the split unit.
Two ways out, both cheap: carry the partial pad byte count in the limit's running state so the next fit starts on the right boundary, or take msodbcsql's own route and mask the odd byte off the measured length (cbValue &= ~1, sqlccmd.cpp:10931) so a trailing half-unit is never classified at all. Whichever you pick, please add a two-call test — the current one cannot fail on this.
2. SQLPutData's buffered branch no longer claims the sequence
The old buffered branch checked the client out before appending, with a comment naming the hazard: a concurrent SQLParamData snapshots the accumulator, and an append that lands after that snapshot is silently discarded while SQLPutData still reports success. The new will_buffer branch appends under the statement lock but never checks the client out, so a SQLParamData that closes and advances between sql_put_data_safe's validation lock and its append lock now has this call append to the next parameter's progress.buffer — with put_data_called set on it — instead of failing. Your reply on the resolved param_data.rs thread says both halves now serialize on the same thing; the SQLPutData half opted out of it in the same change.
The window is narrow and only an application driving one statement handle from two threads can reach it, so this is a suggestion rather than a blocker — but the guard was deliberate, and restoring the checkout in the buffered branch costs one lookup.
Smaller notes, no action needed
unwind_daecallingcancel_streamed_writeon a deferred sequence is safe —tds_client.rs:1913documents the no-active-write case as a no-op — so the new22001/OOM aborts on a buffered parameter do not touch the connection. Worth a word inpark_deferred_dae's doc so nobody re-derives it.dae_planno longer checks family agreement, so a cross-family char/binary pairing would now stream (withDaeSource::Utf8→DaeTarget::Rawrunning the bytes through a lossy UTF-8 round trip). It is unreachable today becauseis_supported_conversionrejects those pairings at bind, so this is only about the deleted defence, not a live bug.dae_plan_buffers_what_cannot_be_plp_framedwould be a natural place to pin that it stays unreachable.
Nice work on the deferred/streaming split and on documenting why bind order was kept over the sort — that trade-off is the kind of thing that gets silently re-litigated later without the note. Finding 1 is worth another pass before this merges.
Saurabh Singh (saurabh500)
left a comment
There was a problem hiding this comment.
Disclosure: this review was produced by an unattended review sweep. The findings below were not checked by a human before posting — please weigh them accordingly.
Verdict: COMMENT — two blocking correctness bugs on the deferred data-at-execution path, plus one suggestion. Otherwise this is an exceptionally thorough, well-documented change, and its core conversion logic checks out.
This PR is the squash superseding #488. I read all three prior-discussion endpoints on both #494 and #488, ran the mssqlodbc lib suite (1308 passed), mutation-tested utf16_units_of_utf8_byte (the split-character and narrow-buffer tests do fail when the continuation cost is changed, so they genuinely guard it), and checked the Stream/Buffer split and per-SQLPutData length enforcement against the msodbcsql reference (ValidatePutDataLength called per SQLPutData at sqlccmd.cpp:3906; PLP streaming vs CacheNonBLOBDAEParam at sqlccmd.cpp:3916-3990) — the design faithfully mirrors it.
The three findings below are the three suppressed (never-threaded) Copilot comments from #488's final review (5103122097). They were never posted as threads, got no response, and I confirmed all three still stand against this squash's code by reading it — so they are not re-files of answered threads. I verified each mechanism myself rather than forwarding the bot.
Blocking (2) — details inline:
param_data.rs:493— a mixed buffered+streamed prepared execute leaves the statement unprepared and skips the no-row result drain.param_data.rs:521— a failed deferred RPC open leaves the statement stuck in "Need Data" on a torn-down sequence.
Suggestion (1) — details inline:
param_convert.rs:210— malformed UTF-8 under-counts againstColumnSizeon the streamed path, accepting a value the buffered path rejects with22001.
Nit: none.
Cross-check I could not complete: I did not resolve AB#47590 in Azure DevOps and treated the AB# reference as satisfying the linked-work-item requirement.
81f06bd to
039a0e4
Compare
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-odbc/src/api/exec_common.rsmssql-odbc/src/api/exec_direct.rsmssql-odbc/src/api/param_data.rsmssql-odbc/src/api/put_data.rsmssql-odbc/src/conversion/param_convert.rsmssql-odbc/src/handles/stmt.rs🔗 Quick Links |
Saurabh Singh (saurabh500)
left a comment
There was a problem hiding this comment.
Disclosure: this review was produced by an unattended review sweep running as saurabh500, and has not been checked by a human before posting.
Verdict: COMMENT. My three earlier threads are all addressed in substance — I re-read each against the code in 039a0e44 and replied inside the existing threads (I am not resolving them, per this sweep's rules). One new Suggestion on the even-length mask that closed the earlier padding-split thread. No blocking issues.
Blocking: none.
Suggestion (1): The even-length mask in DaeLengthLimit::fit drops a straddling SQL_C_WCHAR byte instead of carrying it, so a value split mid-unit across SQLPutData calls misaligns and reassembles to the wrong bytes with no diagnostic. It is the other half of David-Engel's still-open padding-split thread — the mask is the response to that thread — so I've left the detail, an empirical repro, the parity question I could not settle against msodbcsql, and a fix direction inline on mssql-odbc/src/conversion/param_convert.rs at the mask. I ranked it Suggestion rather than Blocking only because reachability needs an odd-length (irregular) WCHAR chunk and I could not prove retail msodbcsql preserves such a split; if it does, it rises to data loss. It is not caught by the existing pad-split test, which binds SQL_C_CHAR and never reaches the mask.
Nit: none.
On the three prior threads (replies inline, not resolved):
- Data-at-exec
restore_planon theNeedDataarm — fixed correctly;restore_planis symmetric totake_prepared/take_orphanedand the plan goes back on the sequence so the closingtake_daerecovers it. The extra fix to thedae-vanished early return is right too. - Failed deferred-RPC-open teardown — fixed; both the
Errand the documented-unreachableCompletearms nowtake_dae()before failing, with thedebug_assert!(parked.is_none()). - Malformed-UTF-8 undercount on the streamed path — documented, not fixed, which is the disposition we agreed on; the note on
utf16_units_of_utf8_bytestates the miscount, why byte-wise classification can't distinguish the cases, and that the effect is a late22001rather than corruption. Fine to defer under AB#47590.
There was a problem hiding this comment.
🟡 Changes recommended
Deferred-close concurrency can skip parameters, and bounded split UTF-16 chunks can lose data.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
mssql-odbc/src/api/param_data.rs:190
- The checkout does not actually serialize two deferred closes: it is returned at line 214 before this mutex guard is released. If two
SQLParamDatacalls both pass the earlier close validation, the second acquires this lock after the first, checks out the now-returned client, and captures/advances the next parameter without validating that parameter. Keep the validated cursor generation through this block (or revalidateput_data_calledfor the same cursor while holding this lock) before capturing and advancing.
let client = match stmt_state
.dae
.as_mut()
.and_then(|dae| dae.checkout_client())
- Files reviewed: 13/13 changed files
- Comments generated: 3
- Review effort level: Balanced
028da64 to
4ba272b
Compare
Saurabh Singh (saurabh500)
left a comment
There was a problem hiding this comment.
Posted by an unattended review sweep — I have not read this PR live; the thread replies below were generated and posted automatically.
Verdict: not ready to merge as-is. One blocking correctness issue remains — the SQL_C_WCHAR odd-split mask in DaeLengthLimit::fit (mssql-odbc/src/conversion/param_convert.rs:337). Everything else I flagged in the earlier round is either addressed or explicitly deferred, and the force-push to 4ba272b6 preserved those fixes. It's still a draft, so treat this as pre-merge feedback rather than a gate.
Blocking
param_convert.rs:337—fitdrops a straddlingSQL_C_WCHARbyte instead of carrying it, silently corrupting a bounded same-family WCHAR data-at-execution parameter that an app splits at an odd byte acrossSQLPutDatacalls. The parity question I left open last round is now settled against msodbcsql (it streams the odd byte, it does not floor it), which moves this from Suggestion to Blocking. Source trace and the fix shape are inline in that thread.
Suggestion
param_convert.rs:269— malformed-UTF-8 under-count on the streamed path (late22001vs the materialized path's early one). Documented-not-fixed under AB#47590; fine to defer. Detail inline in that thread.
Nit
- None.
The two earlier Blocking items on param_data.rs (the NeedData/restore_plan restore and the failed-open take_dae teardown) are addressed and survived the squash; confirmations are inline in their threads. I did not resolve any thread (sweep discipline).
56d381f to
ca634f6
Compare
Saurabh Singh (saurabh500)
left a comment
There was a problem hiding this comment.
🤖 Automated review from an unattended PR-review sweep. Event is COMMENT only — I won't approve, request changes, merge, enable auto-merge, or resolve any thread (including my own). Reviewed at head ca634f67.
Verdict: The blocker I raised last round is resolved. The odd-SQL_C_WCHAR-split corruption is now fixed by carrying the half-unit across SQLPutData calls in DaeProgress::unit_carry, and the guarding test is the exact two-call non-padding shape I asked for — I re-ran it and mutation-checked it (removing the carry line makes it fail [97,0,0,99] vs [97,0,98,0,99,0]). The fit comment/test that conflated "measured without the odd byte" with "the byte is dropped" is also corrected — it now says plainly that msodbcsql floors the measurement, not the payload. No Blocking findings from me this round. Two non-blocking Suggestions on the new carry machinery's failure/memory handling. This is still a draft, so this is not a merge sign-off — just where the code lands at ca634f67.
Blocking: none.
Suggestion:
unit_carryisn't rolled back on the two retriableSQLPutDatafailures, unlike the sibling transcodercarryright beside it. Silent WCHAR corruption, but only in the two-thread + app-retries-after-HY010 window — so Suggestion, not Blocking. Inline onput_data.rs.- Redundant full-chunk
clone()on the untranscoded passthrough path, and the reworkedtry_reservenow guards a vector the non-transcoded carry path never fills. Inline onput_data.rs.
Nit: none.
Two of my other open threads are untouched by this force-push and stand as last assessed: the failed-open teardown on param_data.rs (addressed), and the malformed-UTF-8 undercount on param_convert.rs (documented, deferred under AB#47590). I'm leaving every thread unresolved per the sweep rule.
…eterType SQLBindParameter's ParameterType and ColumnSize were ignored for data-at-execution parameters: every value streamed as a `max` type whatever the application declared, and no length bound was enforced. Declaration follows ParameterType. `dae_plan` decides stream-vs-buffer from the SQL type's PLP-ability, mirroring msodbcsql's IsPartialLenType (sqlcprot.h:1421), which keys on the SQL type alone rather than on ColumnSize. A fixed-width target is now collected and converted whole through the materializing path instead of being forced into a `max` stream it cannot represent. ColumnSize bounds the value. `dae_streamed_declaration` narrows the @params declaration and `dae_length_limit` enforces the accumulated total in SQLPutData, applied before the streamed/buffered split and against the application buffer -- where msodbcsql runs ValidatePutDataLength (sqlccmd.cpp:4571), so 22001 lands on the call that overflows rather than at close. Trailing pad units are trimmed rather than rejected, including one split across two calls, and trimmed padding does not consume the declaration's budget. A narrow buffer is measured in UTF-16 units, the unit the materialized path uses, so the two paths agree on what fits. The declaration is only narrowed when that bound can be enforced: a streamed parameter never reaches a close-time conversion, so promising a length nothing checks would leave the overflow to the server. Transcoding is streamed, not deferred. `DaeTranscode` converts each chunk on the way out, carrying a trailing partial character into the next call -- the shape of msodbcsql's ConvertLongData (sqlccnvt.cpp:841) with its cbTruncatedCharsInConvBuf carry and end-of-value flush (sqlccmd.cpp:5999-6001). Narrow encoding reuses encode_narrow, so the streamed and materialized paths agree on the wire bytes. A mixed sequence collects only what it must. Parameters are offered in bind order, as msodbcsql offers them, and the RPC opens as soon as no parameter still to be visited needs collecting -- so a PLP-capable value bound after a fixed-width one streams rather than being held whole. text/ntext/image keep their `max` substitution, tracked under AB#47592. AB#47590
ca634f6 to
519c24b
Compare
|
Copilot resolve the merge conflicts in this pull request |
…parameter-type-squashed # Conflicts: # mssql-odbc/src/conversion/param_convert.rs # mssql-odbc/tests/e2e/tests/param_conversions_test.cpp Co-authored-by: shiwanigupta0809 <60127942+shiwanigupta0809@users.noreply.github.com>
Resolved by merging |
|
Copilot resolve the merge conflicts in this pull request |
…parameter-type-squashed # Conflicts: # mssql-odbc/src/api/exec_common.rs # mssql-odbc/src/api/exec_direct.rs # mssql-odbc/src/api/execute.rs # mssql-odbc/src/api/param_data.rs Co-authored-by: shiwanigupta0809 <60127942+shiwanigupta0809@users.noreply.github.com>
Resolved by merging the current |
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Automated review — generated by GitHub Copilot on behalf of David Engel (@David-Engel). This is not an approval and does not satisfy the human review requirement. Findings may be incomplete or wrong; push back on anything that looks off.
Summary
Reviewed at head 1241563c against merge base e72e799e (14 files, ~3.1k insertions). This is the fifth automated pass on this PR; the earlier rounds' threads (the fit pad-split misalignment, the SQLPutData buffered-branch claim, the NeedData/restore_plan restore, the failed-open teardown, the odd-SQL_C_WCHAR-split carry) are all resolved and I re-checked each against the current code rather than re-filing them. The design still reads well: keying stream-vs-buffer off IsPartialLenType, narrowing only the @params declaration while the body stays PLP, and narrowing only when the bound is enforceable are the right calls, and the unit_carry fix is correct — fit_chunk joins the held-back byte to the front of the next chunk, and advance() resets DaeProgress wholesale so nothing leaks between parameters.
One blocking item: the branch does not compile under --cfg fuzzing, which is what the two red Linux jobs are reporting. Two non-blocking suggestions.
Verification
- Read the full diff plus the surrounding unchanged code the new paths depend on:
build_named_params,park_dae_client/park_deferred_dae,unwind_dae,take_dae,sql_family,convert_character_sql/trim_blank_overflow,variable_length/fixed_length,parameter_column_size_is_valid, and theconversion_matrixrows that decide which pairings can reachdae_planat all. cargo nextest run -p mssqlodbc --lib --no-fail-fast→ 1472 passed, 0 failed, so thecargo btestchecklist box holds on this platform.RUSTFLAGS="--cfg fuzzing" cargo check -p mssqlodbc --lib→ fails, reproducing the ADO failure exactly (see Blocking 1).- Traced the deferred sequencing by hand for
[buffered, streamed, streamed],[streamed, buffered], and the single-buffered case; cursor/token alignment against the TDS layer'sNeedDataholds in all three, andbuffered_phase_complete's!rest.is_empty()guard is what keeps the last-parameter case onrun_deferred_execute. - Checked one thing I expected to be a regression and it is not:
SQL_LONGVARCHARwithColumnSize = 0would giveDaeBound::Utf16Units(0)and22001on the first byte, butparameter_column_size_is_validbounds thelongtypes at1..=SQL_PREC_TEXTIMAGE, so bind rejects it withHY104first.varchar/varbinarycorrectly keep0as themaxspelling viavariable_length→ no limit. No finding.
Blocking
1. mssql-odbc/src/fuzz_support.rs:37 — orphaned import of the deleted transcode_dae_bytes breaks the --cfg fuzzing build
This PR removes transcode_dae_bytes from param_convert.rs, and the merge of origin/main at 2e948940 brought in #511's fuzz targets, which still import and call it. fuzz_support is behind #[cfg(fuzzing)] (lib.rs:17), so cargo bfmt / cargo bclippy / cargo btest never compile it — which is why the checklist boxes are green locally and the Linux jobs are red.
Reproduced in this worktree:
$ RUSTFLAGS="--cfg fuzzing" cargo check -p mssqlodbc --lib
error[E0432]: unresolved import `crate::conversion::param_convert::transcode_dae_bytes`
--> mssql-odbc\src\fuzz_support.rs:37:60
|
37 | use crate::conversion::param_convert::{bound_param_to_rpc, transcode_dae_bytes};
| ^^^^^^^^^^^^^^^^^^^ no `transcode_dae_bytes` in `conversion::param_convert`
error: could not compile `mssqlodbc` (lib) due to 1 previous error
Same error in build 174072, task Build in Container, on both Build Linux and Build Linux ARM.
The natural fix is to re-point fuzz_transcode_dae_bytes (fuzz_support.rs:89) at the successor API rather than deleting the target — and it gets better coverage than the old one did, because the interesting new surface is exactly the state carried across calls:
let transcode = DaeTranscode::new(c_type, sql_type, collation);
let mut carry = Vec::new();
// Split the input so the carry path is exercised, not just a single whole value.
let (head, tail) = input.split_at(input.len() / 2);
let _ = transcode.push(&mut carry, head);
let _ = transcode.push(&mut carry, tail);
let _ = transcode.finish(&mut carry);DaeLengthLimit::fit_chunk is worth folding in for the same reason — it is the other new function that carries bytes between calls, and it is the one that returns 22001. Whatever you pick, please run RUSTFLAGS="--cfg fuzzing" cargo check -p mssqlodbc --lib before the next push; the standard checklist commands cannot see this.
Suggestion
1. mssql-odbc/src/api/put_data.rs — the untranscoded stream now copies every chunk, and the comment above it says it does not
// A transcoded stream converts on the way out, holding back a character
// that this chunk ended part-way through. An untranscoded one is already
// the wire's bytes, so it is forwarded borrowed.It isn't forwarded borrowed. Both arms end in an owned buffer:
limit: None→fitted_owned = chunk.to_vec()limit: Some(_)→fit_chunkreturnskept.to_vec(), including itsunit <= 1fast path- then
None => Cow::Owned(fitted_owned)
On e72e799e the same path was client.write_streamed_chunk(chunk) straight off the from_raw_parts slice, with no allocation at all. So the canonical SQL_C_BINARY → varbinary(max) LOB case — the one data-at-execution exists for — picks up one full-chunk allocation and memcpy per SQLPutData that it did not have before. The earlier thread on the redundant second clone() fixed the double copy; this is the remaining single one.
It's avoidable without disturbing the carry logic: hoist let chunk: &[u8] above the block (it borrows data_ptr, not stmt_state, so it already outlives the guard) and take Cow::Borrowed(chunk) when limit.is_none() && transcode.is_none(). Having fit_chunk return Cow<'a, [u8]> — borrowed on the unit <= 1 fast path and whenever nothing was trimmed — would cover the bounded-but-not-overflowing case too. Non-blocking: it is an allocation-shape regression on a path whose cost is dominated by the socket write, not a correctness problem. Either fix it or correct the comment, but please don't leave the comment claiming the property it lost.
2. param_data.rs / exec_common.rs — the new fractional_truncated DAE plumbing is unreachable, and its test builds a binding dae_plan refuses
1241563c threads ConvOk::Truncated out of buffered_dae_to_rpc through rebuild_deferred_params, DaeState::fractional_truncated, open_deferred_rpc and run_deferred_execute so a deferred close can report 01S07. As far as I can tell nothing can ever set it:
ConvOk::Truncatedon the parameter-build side is produced only bydecimal_from_numeric(param_convert.rs:1418,:1424);decimal_from_textconverts its ownTruncatedintoErr(StringTruncation)at:1316.decimal_from_numericis reached only from(AppValue::Numeric(_), SqlFamily::Decimal)at:250, andAppValue::Numericis produced only forSQL_C_NUMERIC(param_buffer.rs:191).dae_planrejects any C type outside{SQL_C_CHAR, SQL_C_WCHAR, SQL_C_BINARY}withUnsupportedCType(param_convert.rs:616), andbuild_named_paramsturns that intoHYC00atSQLExecute. So aDaeParamwhosebinding.c_typeisSQL_C_NUMERICcannot exist outside a test.
rebuild_deferred_params_reports_fractional_truncation sets param.binding.c_type = SQL_C_NUMERIC directly on a hand-built DaeParam, which is the state dae_plan exists to prevent. The test does exercise the propagation, so it is not vacuous — but it pins a configuration production cannot reach, and a reader will come away believing 01S07 can now surface from a DAE close.
Both dispositions are defensible; what I'd like is for the code to say which one it is. Either drop the plumbing and restore a version of the comment the commit removed (which was correct for a different reason), or keep it as a guard against a future C type widening and add an assertion that pins today's unreachability — assert!(dae_plan(SQL_C_NUMERIC, SQL_DECIMAL).is_err()) next to the new test would do it, and would fail loudly the day the gate widens.
Nit
MixedDataAtExecutionStreamsThePlpParameter(execute_test.cpp) asserts bind order and the round-tripped values, but nothing in it distinguishes "streamed" from "buffered" — the same assertions pass if both parameters are collected. The name promises more than the test checks. The unit testbuffered_phase_completes_only_when_remaining_params_streamis what actually guards the decision; a pointer to it in the comment would stop someone trusting the e2e name later.DaeLengthLimit::finishtakes&selfand never reads it; it isstd::mem::take(carry). Fine as a symmetry withDaeTranscode::finish, just noting it.
CI
gh pr checks 494 on 1241563c: mssql-rs Pull request validation (Build Stage Build Linux) and (Build Stage Build Linux ARM) are failing, both on the Build in Container task with Bash exited with code '101'. That is Blocking 1 above, not a flake — I reproduced the identical E0432 locally. Everything else is passing or still pending.
The cargo bfmt / cargo bclippy / cargo btest checklist boxes are not contradicted by CI: none of those three commands set --cfg fuzzing, so they genuinely pass on a tree that this job cannot build.
Nice work on the deferred/streaming split and on documenting the bind-order trade-off and the measurement-vs-payload distinction — those are exactly the notes that stop a later change from silently re-litigating them. Fixing the fuzz-support import should be the only thing standing between this and a human review.
ttk (Theekshna)
left a comment
There was a problem hiding this comment.
Reviewed at head 1241563c against merge base with origin/main (e72e799e, 14 files, +3137/-666). Unattended sweep — comment only, not an approval.
| Category | Count |
|---|---|
| Blocking | 1 |
| Suggestion | 0 |
| Nit | 0 |
Scope note: David-Engel's automated reviewer already posted a five-round review at this exact SHA covering the same finding below plus two Suggestions and two Nits on allocation-shape and dead-code paths. I independently reproduced the Blocking item from CI logs rather than trusting that review, and I'm not re-filing its Suggestions/Nits without independently re-deriving them, which the run budget didn't allow this pass — a human should still read that review.
msodbcsql parity (check 1): Spot-checked one of the PR's own citations against the reference tree: ValidatePutDataLength's cbValue &= ~((SQLULEN)1) is at sqlccmd.cpp:10958 in this checkout (PR cites :10931, a ~27-line drift, immaterial) and confirmed it operates on a local SQLLEN cbValue copy, not *pcbValue — the PR's "masks a local copy, not the payload" claim holds exactly as described. No parity gap found in what I checked; did not audit the full DAE table exhaustively given the CI failure below took priority.
Evidence audit (check 6): Re-derived the sqlccmd.cpp masking claim above (holds). Did not re-run the e2e suite (no live SQL Server in this environment) so the "41/41 suites pass against retail 18.6" and "28 DAE e2e tests" claims are unverified by me — flagging as a question, not a finding: worth confirming those e2e runs happened on this exact head rather than an earlier commit in the stack, since two behavior-changing commits (the fit/carry fixes) landed after #488 was superseded.
AI slop / test sufficiency / divergence docs (checks 2, 3, 5): No slop patterns found in a targeted scan of the diff. text/ntext/image max-substitution divergence is tracked at AB#47592 as stated. Test-sufficiency gap is subsumed by the Blocking item below — the fuzz harness is the only path this PR leaves untested, and it doesn't build.
[Blocking] mssql-odbc/src/fuzz_support.rs:37 — orphaned import breaks the --cfg fuzzing build, and both Linux CI jobs are currently red because of it
transcode_dae_bytes was removed from param_convert.rs in favor of the new DaeTranscode struct, but fuzz_support.rs:37's use crate::conversion::param_convert::{bound_param_to_rpc, transcode_dae_bytes}; and fuzz_support.rs:111's call site were never updated. Confirmed directly from the ADO build log (build 174072, Build in Container task, both Build Linux and Build Linux ARM jobs, log line ~3147):
error[E0432]: unresolved import `crate::conversion::param_convert::transcode_dae_bytes`
--> mssql-odbc/src/fuzz_support.rs:37:60
|
37 | use crate::conversion::param_convert::{bound_param_to_rpc, transcode_dae_bytes};
| ^^^^^^^^^^^^^^^^^^^ no `transcode_dae_bytes` in `conversion::param_convert`
error: could not compile `mssqlodbc` (lib) due to 1 previous error
fuzz_support is #[cfg(fuzzing)]-gated (lib.rs:17), which is why cargo bfmt/cargo bclippy/cargo btest — none of which set --cfg fuzzing — genuinely pass locally and the checklist boxes aren't lying. But the ADO pipeline's Linux jobs do build under that cfg, and both are failing on this exact head SHA as of this review. This blocks merge regardless of approvals, since it's a required, currently-failing check with a concrete, reproducible cause rather than an infra flake.
Description
SQLBindParameter'sParameterTypeandColumnSizewere ignored for data-at-execution parameters: every value streamed as amaxtype whatever the application declared, and no length bound was enforced. This aligns the DAE path with msodbcsql on both counts (AB#47590).Declaration follows
ParameterType.dae_plandecides stream-vs-buffer from the SQL type's PLP-ability, mirroring msodbcsql'sIsPartialLenType(sqlcprot.h:1421), which keys on the SQL type alone rather than onColumnSize. A fixed-width target is now collected and converted whole through the materializing path instead of being forced into amaxstream it cannot represent.ColumnSizebounds the value.dae_streamed_declarationnarrows the@paramsdeclaration anddae_length_limitenforces the accumulated total inSQLPutData— applied before the streamed/buffered split and against the application buffer, where msodbcsql runsValidatePutDataLength(sqlccmd.cpp:4571), so22001lands on the call that overflows rather than at close. Trailing pad units are trimmed rather than rejected, and trimmed padding does not consume the declaration's budget.Transcoding is streamed, not deferred.
DaeTranscodeconverts each chunk on the way out, carrying a trailing partial character into the next call — the shape of msodbcsql'sConvertLongData(sqlccnvt.cpp:841) with itscbTruncatedCharsInConvBufcarry and end-of-value flush (sqlccmd.cpp:5999-6001). Narrow encoding reusesencode_narrow, so the streamed and materialized paths agree on the wire bytes.text/ntext/imagekeep theirmaxsubstitution, tracked under AB#47592.Please look closely at
1. A mixed sequence opens its RPC partway through, so a LOB still streams.
A fixed-width target has no PLP form, so its value must be collected before the RPC can declare it. Rather than deferring the whole sequence — which would hold a
varbinary(max)bound alongside it entirely in memory — the RPC opens as soon as no parameter still to be visited needs collecting, and everything after that streams.Parameters are offered in bind order, the same as msodbcsql. An earlier revision of this change sorted buffered parameters first so that a LOB bound before a fixed-width parameter could also stream; that was reverted. It optimized an unusual bind order at the cost of changing the order
SQLParamDatahands tokens back, which an application that counts iterations instead of comparing tokens would silently mispair. Not worth it — the remaining case (LOB bound first) simply buffers, exactly as it did before this PR.2. Two bugs that only exist in combination with #444, so neither branch shows them alone.
buffered_dae_to_rpcmust repoint bothstrlen_or_ind_ptrandoctet_length_ptr. mssql-odbc: buffer and transcode data-at-execution parameters with mismatched wideness #444 split the indicator from the length; repointing only the first leftSQL_DATA_AT_EXECon the synthesized binding.try_reserveOOM guard added in mssql-odbc: buffer and transcode data-at-execution parameters with mismatched wideness #444 is preserved but re-sited onto whichever accumulator actually receives the bytes —bufferwhen the value converts at close,carrywhen it converts on the way out,unit_carrywhen an untranscoded stream realigns a unit split across chunks.3. Narrow buffers are measured in UTF-16 units, not bytes.
convert_character_sqlderives its limit fromsql_type+ColumnSizealone and measures the source in UTF-16 units, so the C type never enters the unit. MeasuringSQL_C_CHARin bytes would have rejectedéagainstvarchar(1)— two bytes, one unit — that the same binding materialized accepts. The unit is an approximation of collation bytes on both paths; making it exact is AB#47584.4. The declaration is only narrowed when the bound can be enforced.
A streamed parameter never reaches a close-time conversion, so a bound
dae_length_limitcannot measure is enforced nowhere. Narrowing anyway would have the client declarevarchar(n)and then stream past it, leaving the overflow to the server instead of the22001the declaration implies.5. A
SQL_C_WCHARcode unit split across two chunks is carried, not dropped.A chunk can end part-way through a UTF-16 code unit. That stray byte is held back and joined to the front of the next chunk (
fit_chunk/DaeProgress::unit_carry), so"abc"sent as[61 00 62]+[00 63 00]reassembles intact;DaeLengthLimit::finishreleases a trailing half unit at close. Measuring the byte instead would put every later chunk one byte off the application's code-unit grid, so a following[0x20, 0x00]blank would read as half of one unit plus half of the next and report22001on padding — so the measurement is still floored to whole units, matching msodbcsql'scbValue &= ~(SQLULEN)1(sqlccmd.cpp:10931), which masks a local copy governing the length check and runs after the bytes have already been pushed.An earlier revision of this change dropped the byte and justified it as parity, citing that same mask. That was wrong on both counts: the mask floors the measurement, not the payload, and retail streams the odd byte (
ConvertLongData's same-wideness branch forwards the full oddcbSrc). Dropping it silently turned"abc"intoU+0061 U+6300with no diagnostic. Caught in review by Saurabh Singh (@saurabh500); the reading came from source rather than a run, and the corrected distinction is now spelled out atfitand indae_limit_fit_measures_whole_units_only.6. A cross-family data-at-execution parameter now succeeds where
mainreturnsHYC00.Binding
SQL_C_CHARtoSQL_INTEGERwith a data-at-execution indicator fails onmainwithHYC00atSQLExecute. Here the chunks are collected and converted by the same code the materialized path uses, so"41"againstSELECT ? + 1returns42— exactly as the same binding already behaves when the value is passed in one buffer.This moves toward msodbcsql, not away: measured against retail 18.6, its
SQLPutDataaccepts the chunk and returns42as well. BothCrossFamilyDataAtExecutionConvertsToIntegertests therefore run on the parity leg rather than opting out.Worth flagging because a comment in the tree claimed msodbcsql rejects this with
HY019. ItsValidatePutDataLengthdoes have anIsFixedSqlTypearm raisingIDS_HY_019, butSQL_INTEGERdoes not reach it at runtime — the claim came from reading the source rather than running it. Probing both drivers disproved it, and the stale claim is corrected here, in both test files anddocs/parameters_plan.md.Known gaps
DaeTranscode::push's output still allocates infallibly. Thetry_reserveguard covers the input side, but bounding the encoded output means threadingtry_reservethroughencode_narrowandencoding_rs, which is beyond this change.Malformed UTF-8 under-counts against
ColumnSizeon the streamed path.utf16_units_of_utf8_byteclassifies each byte alone, so an orphan continuation byte costs 0 where the materialized path repairs it to oneU+FFFDand charges a unit —[0x80, b'a']againstvarchar(1)fits when streamed and is22001when buffered. Closing it needs UTF-8 decoder state carried acrossSQLPutDatacalls, since a leading continuation byte is otherwise indistinguishable from the tail of a legitimate split character — the case this design exists to support. Attempted and reverted rather than half-done; documented at the miscount. The value is still bounded server-side, so the effect is a late22001, not data loss.A value that ends part-way through a unit is treated differently by the two close paths.
DaeLengthLimit::finishreleases a trailing half unit, but the transcoded arm then decodes it throughdecode_utf16le, whosechunks_exact(2)discards a lone byte, while the passthrough arm writes it raw. Measured, not inferred:DaeTranscode::finishon a carry of[0x62]returns empty. Only reachable on malformed final input — a well-formed value ends on a unit boundary — and unlike the mid-stream case above it needs a deliberate contract (drop,U+FFFD, or22001) applied to both arms rather than falling out of which plan the parameter took. Raised by Saurabh Singh (@saurabh500); left for a separate change, and I have not measured which arm matches retail.These are deliberate, not oversights.
Related Issues
https://sqlclientdrivers.visualstudio.com/DefaultCollection/mssql-rs/_workitems/edit/47590
AB#47590
Checklist
cargo bfmtpassescargo bclippypasses (workspace +mssql-tds+mssql-py-core)cargo btestpasses (1398mssqlodbc, 2124mssql-tds)Validation
Supersedes #488, which carries the review history for these changes across seven commits.