refactor: resolve parameters once and serialize them through a write contract - #1774
Conversation
…contract Splits parameter handling into two phases. `resolveParameter` validates the value and determines the declaration facts (length, precision, scale, collation) once, before a request is sent; `writeTypeInfo` and `writeValue` serialize the resolved parameter into a `WritableTrackingBuffer`. Types can implement `resolve`, `writeTypeInfo` and `writeValue` natively; the helpers adapt everything else from the existing `validate` / `resolve*` / `generate*` methods, so types can be migrated one at a time. Int, NVarChar and VarBinary are migrated. `Request.validateParameters` now resolves the request's parameters and keeps the result; the RPC payload takes resolved parameters and only serializes. `Connection.execSql`, `callProcedure`, `prepare`, `unprepare`, `execute` and the Always Encrypted metadata request build their payloads from resolved parameters. Bulk load writes column metadata and row values through the same helpers. Two behaviour changes come with the shared resolution: - Lengths are resolved for every type that can resolve one, not only for type ids matching the legacy variable-length bit pattern (the fix proposed in #1771). - Errors thrown while writing a parameter's TYPE_INFO are wrapped in the same `InputError` as errors from writing its value (the RPC half of #1772). A new unit test serializes 40 parameter cases across every type, on TDS 7.4 and 7.2, with and without a collation, through the new payload and through an inline copy of the previous serialization, and asserts the bytes are identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
ReviewSolid refactor — the single One thing worth fixing before merge: bulk load isn't actually part of the #1771 fixThe PR says "#1771 can be closed once this lands," but #1771 explicitly scopes the fix to both This PR only changes the RPC path. if ((type.id & 0x30) === 0x20) {
if (column.length == null && type.resolveLength) {
column.length = type.resolveLength(column);
}
}So a bulk load column using a modern-id type with a resolvable length (e.g. the upcoming Recommend either applying the same unconditional- Minor notes, non-blocking
Not verifiedI wasn't able to run |
Constraint names on temporary tables are unique per database, so the
named constraint collided when two CI jobs ran this test against the
same Azure database at the same time ("There is already an object named
'chk_id' in the database").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
|
One Azure job on c6342d9 ( Cause, reproduced locally against SQL Server 2022 with two sessions: the test creates Fixed in the test by leaving the constraint unnamed, which gets a unique generated name; pushed as the next commit. Generated by Claude Code |
- `BulkLoad.addColumn` no longer gates length resolution on the legacy variable-length type id bit pattern, so the RPC and bulk load paths agree and #1771 is covered in full. - `resolveParameter` treats an explicitly specified length, precision or scale of 0 as specified instead of falling through to the type's resolver. Every existing resolver re-checked for an explicit value itself, so this changes no bytes for existing types; it removes the trap for a future type whose resolver does not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
|
Addressed the review:
Generated by Claude Code |
|
Nicely scoped refactor — the One behavior change that isn't called out in the "Behaviour changes" section, and doesn't look intentional:
Before this PR, every call site that invoked Five types key their out-of-range validation on if (options && options.useUTC) {
year = value.getUTCFullYear(); ...
} else {
year = value.getFullYear(); ...
}Since This isn't mentioned in the PR description's two enumerated behaviour changes, and none of the new tests exercise it — the date values used (e.g.
Everything else looked solid:
🤖 Generated with Claude Code |
|
Review I read through the full diff ( Code quality / design
Minor observations (non-blocking)
Bugs Test coverage Security / performance |
`resolveParameter` calls `type.validate(value, collation)` without the connection options, as every caller did before. Passing the options would activate the `useUTC`-dependent range checks in the date and time types, which have never run; that is a behaviour change to make on its own. A unit test pins the call shape. `Connection.resolveParameter` is renamed `resolveRequestParameter` so it is not misread as recursion into the free function it wraps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
|
Good catch on Also renamed Generated by Claude Code |
ReviewI read through the diff in full (data-type.ts, the Int/NVarChar/VarBinary write methods, request.ts, connection.ts, bulk-load.ts, rpcrequest-payload.ts, and the new tests) and traced the two documented behaviour changes against every existing Code quality / design
Correctness
Minor observations (not blocking)
Test coverage
Performance / security
Overall this is a well-scoped, well-tested refactor that keeps byte-for-byte compatibility while opening the door for streaming TVP support. Nice work. |
|
On the two minor notes from the latest review:
Generated by Claude Code |
…quest The Always Encrypted metadata request is built from raw parameters rather than a `Request` that went through `validateParameters`, so the parameters are resolved inline. A comment says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
ReviewThis is a large, carefully-scoped refactor, and it reads that way: the PR description states the exact behavior changes up front, and the new tests ( Correctness checks I did
Minor observations (not blockers)
Test coverageGood — byte-equivalence tests for the three natively-migrated types across null/empty/short/ Didn't find security concerns — this is wire-serialization code operating on already-validated/typed values, no injection surface here. I wasn't able to run the suite in this sandbox (no network access to install dependencies), so this is a static review; the PR description's own numbers (498 unit tests, full integration suite passing, lint/typecheck clean) cover that ground. |
|
The Corroboration on the same commit:
There's nothing to fix in the PR — re-running the single failed job clears it. This integration can't trigger a re-run (the API returns 403), so a maintainer re-run or the next push will resolve it. Keeping the PR watched until CI is green. Generated by Claude Code |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1774 +/- ##
==========================================
+ Coverage 81.27% 82.50% +1.22%
==========================================
Files 92 92
Lines 4967 5035 +68
Branches 938 961 +23
==========================================
+ Hits 4037 4154 +117
+ Misses 649 592 -57
- Partials 281 289 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c5d24ccc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ReviewThis is a well-executed refactor. The Correctness
Minor observations (non-blocking)
Test coverageExcellent — SecurityNo concerns — this is internal wire-format serialization logic with no new external input handling; validation behavior for user-supplied values is preserved or made stricter (never weaker). Nothing here blocks merging; the minor points above are observations for awareness rather than requested changes. |
Takes the bulk load serializer from #1779. The transform this branch had routed cells through writeValue is gone; the payload's loop routes them through writeValue instead, and COLMETADATA keeps going through writeTypeInfo. The wide-row unit test spies on writeValue now that VarBinary implements it natively. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
ReviewThis is a well-scoped, carefully executed refactor. The split between Correctness
Minor / worth a second look
Test coverageCoverage is thorough for the refactor's core claims (byte parity, explicit-zero handling, modern type-id length resolution, TYPE_INFO error attribution, by-reference passthrough for large values). I didn't find gaps beyond the error-containment point above, which isn't really testable as "bytes produced" — it'd need a test that deliberately makes a wrapper parameter's Not verifiedI reviewed the diff and affected source files in detail but did not execute the build/lint/test suite in this environment (sandbox restrictions on running Nice work overall — the contract is easy to reason about and the migration path for the remaining types (future work in #1773's series) looks straightforward to follow. |
Takes #1771, which removed the legacy-id gate on length resolution that this branch had already removed in resolveParameter and BulkLoad.addColumn; only its comment is new. Its three unit tests drove the old payload constructor and are covered by parameter-contract-test.ts and bulk-load-test.ts here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
The fireTriggers integration test randomizes its table name to survive
parallel runs against one database, but created its trigger under a
fixed name. Trigger names are scoped to the schema, so two Azure CI
jobs running the test at the same time collided on it ("There is
already an object named 'bulkLoadTest' in the database"). The trigger
is now named after the table, like the CHECK constraint fix before it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug
ReviewThis is a well-designed refactor that consolidates parameter resolution and serialization into a single contract, and I traced through the mechanics in detail before commenting. Overall the change is solid; comments below are mostly confirmations of subtle points and a couple of minor nits rather than blockers. Correctness
Minor nits (non-blocking)
Test coverageExcellent — the 40-case byte-parity matrix in SecurityNo concerns — this is an internal serialization refactor with no new untrusted-input parsing, no string-built SQL, and no changes to credential/encryption handling. The Always Encrypted metadata request path is updated consistently and its inline resolution is now explained with a comment. Nice work — the byte-for-byte test discipline here made this refactor genuinely easy to verify. |
ReviewI went through the diff against Strengths
Things worth a look
Other
Nice work — the byte-equivalence testing strategy here is a good pattern for wire-protocol refactors in general. |
|
🎉 This PR is included in version 20.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Problem
A parameter's handling is spread over five
DataTypemethods and two call sites that each combine them differently.validateruns inRequest.validateParameters;resolveLength/resolvePrecision/resolveScalerun inside the RPC payload while the request is being written;generateTypeInfo,generateParameterLengthandgenerateParameterDataproduce a list of small buffers per parameter. Bulk load repeats the resolution logic with slightly different rules inaddColumn. There is no single place that says what a parameter's declaration is, and no way for a type to write its bytes directly into the shared buffer that #1773 introduced.This is the second of the series described in #1773. The third (streaming table-valued parameters) needs parameters resolved before the request starts and types that write into a buffer; this PR provides both.
Change
DataTypegains three optional methods, anddata-type.tsthree helpers that adapt types which do not implement them:resolve(parameter, collation, options)→ParameterData: validate the value and determine length, precision, scale and collation.resolveParameterfalls back tovalidateand theresolve*methods; an explicitly specified fact wins, including an explicit0.writeTypeInfo(buffer, data, options): write the TYPE_INFO.writeTypeInfofalls back togenerateTypeInfo.writeValue(buffer, data, options): write the length prefix and data.writeValuefalls back togenerateParameterLengthandgenerateParameterData.Int,NVarCharandVarBinaryimplement the write methods natively; every other type goes through the adapters unchanged, so migration can continue one type at a time.Resolution happens once, up front:
Request.validateParameters(collation, options)resolves every parameter and keeps the result inrequest.resolvedParameters. It still writes the validated value back toparameter.value, whichmakeParamsParameterrelies on.RpcRequestPayloadtakesResolvedParameter[](name, output flag, type, resolved data) and only serializes. It writes each parameter's header, TYPE_INFO and value into oneWritableTrackingBufferand yields its chunks, so a large value written by reference stays by reference.Connection.execSql,callProcedure,prepare,unprepare,executeand the Always Encryptedsp_describe_parameter_encryptionrequest build their payloads from resolved parameters.executeresolves each parameter with the value supplied for that execution, as it validated before. The wrapper parameters these methods add (statement,params,handle,stmt,tsql) now pass throughvalidatelike every other parameter; their values are always well-formed strings and integers, so nothing observable changes.writeTypeInfoand each row's cells throughwriteValueinto the payload's buffer (perf: serialize bulk load rows with an async generator, coalesced into packet-sized chunks #1779). Its error handling is unchanged.validateis still called asvalidate(value, collation), without the connection options, as every caller did before. TheuseUTC-dependent range checks in the date and time types' validators have therefore never been active; enabling them is a behaviour change to make deliberately, not as a side effect here. A unit test pins the call shape.Behaviour changes
All fall out of sharing one resolution path; all are covered by tests.
BulkLoad.addColumn, not only for type ids matching(id & 0x30) === 0x20. refactor: resolve parameter lengths for all types that can resolve one #1771 made the same change onmasterin the meantime; this branch carries it throughresolveParameter, so the two agree.0for length, precision or scale is kept rather than treated as unspecified. Every existingresolve*implementation already re-checks for an explicit value, so no bytes change for existing types; this removes the trap for a future type whose resolver does not.InputErrornaming the parameter that an error from its value did. This is the RPC half of fix: wrap bulk load serialization errors in InputError #1772; the bulk load half is not included.Validation
test/unit/rpcrequest-payload-test.tsserializes 40 parameter cases across every input type (int, string, binary, decimal, date/time, GUID, TVP, output and unnamed parameters, null values,maxvalues over 8000 bytes) on TDS 7.4 and 7.2, with and without a collation, through the new payload and through an inline copy of the previous serialization algorithm, and asserts the bytes are identical. It also checks that a 1 MB value is passed through by reference and that a failing type surfaces asInputError.test/unit/parameter-contract-test.tscoversresolveParameter(explicit facts win, explicit zero kept, nativeresolvedelegation, modern-id lengths, validation errors, thevalidatecall shape) and byte equivalence of the three native types against their legacy methods across 19 value/length combinations.test/unit/bulk-load-test.tscovers modern-id length resolution inaddColumn, and its byte-equivalence test now runs the bulk load payload's cells throughwriteValue.should not leave any dangling sockets after connection timeout.checkConstraintstest no longer names its CHECK constraint, and thefireTriggerstest names its trigger after its randomized table instead of the fixedbulkLoadTest. Constraint and trigger names are scoped to the schema, so concurrent jobs collided on both.Measurements
Measured on this head against current
master, same machine, same run.benchmarks/parameters/scalar-params.jsis new (20 scalar parameters per request, resolved once per request asvalidateParametersdoes).RPC serialization only, median of 5 runs. On
mastereach parameter is validated and then serialized by the old payload; here each is resolved and then serialized by the new one.mastervarbinary(max)varbinary(max)Scalar requests gain about 8%; the large-value cases pass the buffer through by reference on both sides and the difference is inside run-to-run noise (min/max spread of 2x on both).
Bulk load, where this PR routes every cell through
writeValueand the three native types skip the per-cellgenerateParameterLength/generateParameterDataallocations. Serializer in isolation, 200,000 rows from an array, median of 5 runs:masterint,nvarchar(50),floatintEnd to end against SQL Server 2022, 50 bulk loads of 10,000 rows each:
masterintcolumn, arrayThe single-
intcase is server-bound on both sides. The wide-row serializer gain (4.4x) is the compounding #1779 anticipated: that PR removed the per-row stream plumbing, this one removes the per-cell allocations.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fxv5h4UMCGJEpjgCKcAxug