Skip to content

fix: pass max lengths to tedious as a number - #1907

Merged
dhensby merged 2 commits into
tediousjs:masterfrom
dhensby:fix/tedious-max-length
Oct 4, 2026
Merged

dhensby merged 2 commits into
tediousjs:masterfrom
dhensby:fix/tedious-max-length

Conversation

@dhensby

@dhensby dhensby commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Since tedious 20.3.0, any length given as the word 'max' produces a malformed TDS stream, and the server rejects the request with:

The incoming tabular data stream (TDS) protocol stream is incorrect. The stream ended unexpectedly.

node-mssql passes the string 'max' straight through to tedious, which only compares lengths numerically. In 20.3's PLP write path, the header is chosen with length <= maximumLength and the terminator with length > maximumLength. A string fails both checks, so the value is written without its terminator.

Affected with tedious ≥ 20.3.0 (reproduced on 20.3.0–20.3.4; 20.2.3 and earlier are fine):

  • sql.NVarChar('max') and sql.VarBinary('max') input/output parameters
  • bulk-load Table columns with length: 'max' (NVarChar, VarBinary)
  • TVP columns with no length, e.g. table.columns.add('c', sql.NVarChar): tedious writes TVP columns with the length it's given and doesn't resolve a missing one, so undefined fails both comparisons the same way (NVarChar, VarBinary)

Numeric sql.MAX is unaffected.

Reported upstream as tediousjs/tedious#1810 (introduced by tediousjs/tedious#1774). This PR keeps node-mssql working regardless of when that lands.

This is what is failing the tedious 20 CI jobs on #1906. That PR's brace-expansion bump is unrelated; CI installs tedious@^20 without pinning a version and now picks up 20.3.x.

Fix

Convert 'max' to 65535 where node-mssql hands lengths to tedious: parameters, bulk-load columns and TVP columns. A TVP NVarChar/VarChar/VarBinary column with no length is also sent as 65535, which is how tedious ≤ 20.2 treated it. The generated SQL declarations still emit max, and size validation (assertSafeTypeSize) is unchanged.

msnodesqlv8: output parameter initial values

Registering the new tests in the msnodesqlv8 suite, so both suites stay in step, exposed a separate long-standing msnodesqlv8 bug, fixed in its own commit (fix: set output parameter initial values in msnodesqlv8 queries). query()/batch() bound an output parameter's initial value but only wrote a set @name=? marker for inputs. So:

  • the output started as NULL (its initial value was ignored);
  • because values are bound by position, an output with an initial value declared before an input shifted the input onto the output's value.

Separately, an initial value of 0, '' or false was skipped altogether by both query() and execute() (a truthiness check), so those outputs started as NULL too. tedious sends them.

The fix writes a marker for every value that is bound, and treats only null (what output() stores when no value is given) as "no initial value", in query()/batch() and execute() alike. msnodesqlv8 can't run locally; I checked the generated SQL with a stubbed driver (markers and values now line up in every case), and Windows CI is the real check.

Tests

  • New parameters with length as max test covering nvarchar/varchar/varbinary inputs and an nvarchar(max) output. Only the bulk path was covered before.
  • New new Table with string columns without a length TVP test (adds a MSSQLTestMaxType table type and __testMaxType procedure to the test fixtures). It goes through a stored procedure because msnodesqlv8 can't take a TVP in an ad-hoc query.
  • New output parameters with a falsy initial value test (0, '' and false outputs through query(), and a 0 output through execute()).
  • All three max tests (parameters with length as max, bulk insert with length as max, new Table with string columns without a length) are now registered in both the tedious and msnodesqlv8 suites. Other tests that the msnodesqlv8 suite is missing are tracked separately.
  • Locally, without the fix, all three new/existing max tests fail on tedious 20.3.4. With it, the tedious suite passes on 20.3.4, 20.2.3 and the locked 19.2.2. Lint and unit tests pass.

🤖 Generated with Claude Code

A length of 'max' (e.g. `sql.NVarChar('max')` or a Table column with
`length: 'max'`) was handed to tedious as the string. tedious only
compares a length numerically, and since 20.3.0 it decides whether to
write the PLP header and the PLP terminator with opposite comparisons,
so a word fails both and the value is sent without its terminator. The
server rejects the request with "The incoming tabular data stream (TDS)
protocol stream is incorrect".

A TVP column without a length (e.g. `sql.NVarChar` with no length)
fails the same way, because tedious writes TVP columns with the length
they are given rather than resolving a missing one.

Map 'max' to 65535 at the tedious boundary for parameters, bulk load
columns and TVP columns, and send a missing TVP length as 65535 for
NVarChar, VarChar and VarBinary, as tedious wrote it before 20.3.0;
the SQL declarations still emit `max`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The msnodesqlv8 fix still discards defined but falsy output parameter initial values.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes tedious max-length serialization and msnodesqlv8 output-parameter binding.

Changes:

  • Converts 'max' and missing TVP lengths to numeric max lengths for tedious.
  • Aligns msnodesqlv8 output markers with bound values.
  • Adds integration coverage and SQL fixtures.
File Description
lib/​tedious/​request.js Normalizes lengths passed to tedious.
lib/​msnodesqlv8/​request.js Adds output-value assignment markers.
test/​common/​tests.js Tests max-length parameters and TVPs.
test/​tedious/​tedious.js Registers tedious tests.
test/​msnodesqlv8/​msnodesqlv8.js Registers msnodesqlv8 tests.
test/​prepare.sql Creates the TVP test fixtures.
test/​cleanup.sql Removes the TVP test fixtures.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/msnodesqlv8/request.js Outdated
query() and batch() on msnodesqlv8 bound an output parameter's initial
value but only wrote a `set @name=?` marker for inputs, so the value was
never assigned and the output started as NULL. Values are bound by
position, so an output with an initial value declared before an input
also shifted the input onto the output's value.

An initial value of 0, '' or false was skipped altogether, by query()
and by execute(), so those outputs started as NULL too.

Write the marker for every value that is bound, and treat only a null
initial value as no value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dhensby
dhensby force-pushed the fix/tedious-max-length branch from efcba06 to a05796a Compare October 4, 2026 22:59
@dhensby
dhensby merged commit 67187a6 into tediousjs:master Oct 4, 2026
47 checks passed
@dhensby
dhensby deleted the fix/tedious-max-length branch October 4, 2026 23:50
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 12.7.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants