Using the tedious MSSQL client against mssqlite — connection options that matter (TLS, port, useColumnNames), Request API and events (row, infoMessage on the connection, done vs doneInProc), how tedious maps operations to the wire (sp_executesql RPC, sp_prepare handles, transaction manager), and e2e testing patterns. Use when writing server e2e tests or debugging client interop.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Using the tedious MSSQL client against mssqlite — connection options that matter (TLS, port, useColumnNames), Request API and events (row, infoMessage on the connection, done vs doneInProc), how tedious maps operations to the wire (sp_executesql RPC, sp_prepare handles, transaction manager), and e2e testing patterns. Use when writing server e2e tests or debugging client interop.
tedious Client Against mssqlite
tedious is the pure-JS MSSQL driver used for end-to-end tests
(packages/server/src/server.test.ts). It exercises the full stack the
same way real applications do.
tedious ≥ 16 defaults to encrypt: true. Start mssqlite with
tls: { key, cert }; required encryption is the server default whenever TLS
is configured. trustServerCertificate: true is appropriate only for an
explicitly trusted self-signed development certificate. For a plaintext
local server with no TLS configuration, set encrypt: false.
To validate a private CA, set trustServerCertificate: false and pass it as
cryptoCredentialsDetails: { ca }. serverName must match a certificate
DNS/IP subject alternative name; Node rejects an IP literal as an SNI name,
so set a DNS serverName even when the TCP target is an IP.
connection.connect(callback) — the callback fires after LOGINACK +
final DONE. connection.state.name === 'LoggedIn' confirms handshake.
Failed logins surface as ConnectionError from the ERROR token. The
connection callback reports code ELOGIN; listen to the connection's
errorMessage event to assert the underlying 18456 token number. Password-
authenticated mssqlite listeners require encrypt: true and required TLS.
How tedious talks to the server
tedious call
Wire
mssqlite handling
execSql(request) without params
SQL batch (0x01)
engine.executeBatchAsync
execSql(request) with params
RPC sp_executesql (by name)
engine.executeSqlAsync
callProcedure(request)
RPC by procedure name
system/user procedure dispatch, result sets + return status
Tedious 20 does not expose MARS/SMP, so it cannot be the MARS-capable client
ground truth. Use System.Data.SqlClient (or Microsoft.Data.SqlClient) with
MultipleActiveResultSets=True for the external interoperability smoke test.
The repository's server/mars.test.ts independently drives exact PRELOGIN,
SMP, and TDS bytes so automated tests do not depend on a .NET installation.
RPC parameter TYPE_INFO is semantically significant, not only a decoder hint:
the server maps it to the scoped variable's T-SQL declaration so mixed-type
predicates and assignments use SQL Server precedence. E2E tests should include
a typed character parameter compared with a numeric expression and assert both
the successful result and error 245 for invalid numeric text. Decimal RPC
parameters cast to integer must use their declared numeric source type so the
engine truncates toward zero; explicit bigint result metadata makes tedious
surface even a small value such as zero as a string.
row — with useColumnNames: true, columns is
Record<name, { value }>. Values are decoded per COLMETADATA TYPE_INFO:
IntN → number, NVarChar → string, DateTimeN/DateTime2N → JS
Date, BitN → boolean, FloatN → number.
Duplicate labels necessarily overwrite in this object form. Use
useColumnNames: false when labels may repeat; the row event then receives
an ordered column array and mssqlite preserves every positional value.
infoMessage fires on the Connection, not the Request — PRINT
output and other INFO tokens land there. errorMessage likewise.
The request callback's error is an MSSQL-shaped error with .number
(e.g. 208 invalid object, 2627 unique violation) — assert on it.
Multiple ERROR tokens produce an AggregateError whose .errors retain
token order; the Connection's errorMessage events interleave with Request
row events, so test mixed failing/successful batches through both streams.
done / doneInProc / doneProc events mirror the DONE token family;
counts from tokens carrying DONE_COUNT contribute to the Request callback's
rowCount. Trigger-body DML can therefore add to the originating count.
NOCOUNT makes the event argument undefined and excludes that completion
from the callback total; result rows and @@ROWCOUNT remain available.
execBulkLoad first sends its generated INSERT BULK table(columns...)
batch and waits for DONE, then streams COLMETADATA, ROW values, and DONE in
packet type 7. Its callback row count comes from the server's DONE_COUNT.
keepNulls, checkConstraints, and fireTriggers become INSERT BULK
options. Cancellation before the stream finishes uses an IGNORE EOM packet
without a separate Attention; cancellation after send uses Attention.
Unlike freebcp -b, tedious sends one type-7 request for the supplied row
iterable, so a server-side row failure rolls back that complete request.
Request.cancel() has two wire paths. While the request payload is still
streaming, tedious terminates it with IGNORE and expects one normal response;
after EOM it sends Attention, consumes the original request's response, then
reads a separate response until DONE_ATTN. If the server sends only one
DONE_ATTN message, the first reader consumes it and SentAttention eventually
fails with . A successful cancel callback receives .
Testing patterns
packages/differential uses one useColumnNames: false tedious capture
path against mssqlite and SQL Server 2025. It records every
columnMetadata, ordered row/result set, DONE/DONEINPROC/DONEPROC count,
stable error fields, transaction state, and a follow-up reuse probe.
The artifact also records tedious's post-decryption packet headers and
decoded response-token diagnostics per case. Packet data/payload diagnostics
stay disabled so LOGIN7 secrets are never persisted.
Intentional differences must name an exact JSON-pointer path and both values;
run the container-backed suite only with pnpm test:differential.
Listen on port 0 and read the assigned port
(await listen({ port: 0 })).
TLS e2e tests use a fixed localhost certificate and exercise tedious with its
current encryption default, private-CA validation, hostname mismatch,
plaintext rejection, and the higher-level mssql client.
One shared connection in beforeAll keeps tests fast; open a second
connection inside a test to check session isolation.
Multiple-database e2e tests should switch only the second connection with
USE, verify DB_NAME()/DB_ID() and local metadata there, then query the same
object through a three-part name on the still-master first connection.
Wrap Request in a promise helper collecting rows + rowCount (see
server.test.ts).
LIKE character-class e2e tests should include literal and typed RPC patterns,
case folding under the default collation, ESCAPE, and error 506 so SQLite's
native no-class LIKE cannot leak through one expression source.
Non-SC UTF-16 tests must inspect JavaScript length and charCodeAt, not only
visual output: tedious preserves lone surrogate units decoded from NCHAR,
SUBSTRING/LEFT/RIGHT/STUFF, and code-unit REVERSE TDS payloads.
Keep a table variable's declaration and all references in one SQL-batch
request; a later execSql call is a new batch and must receive error 1087.
TVF result metadata is available even for NULL/empty inputs. Remember that
tedious surfaces STRING_SPLIT's bigint ordinal as a string, while
GENERATE_SERIES over int literals retains IntN(4) and surfaces numbers.
APPLY e2e tests should cover both CROSS row elimination and OUTER NULL
extension; use explicit projected columns for rewritten TOP (1) sources.
PIVOT/UNPIVOT e2e tests should listen for columnMetadata as well as rows:
all-NULL generated PIVOT columns must retain the aggregate input TYPE_INFO,
while UNPIVOT's name column is NVARCHAR and its value column keeps the
common declared input type.
Gotchas discovered
tedious sends sp_executesql by name, not by ProcID — handle both.
RPC OptionFlags is 2 bytes on the wire (the MS-TDS example prose shows
a single byte but the byte count proves otherwise).
tedious validates TYPE_INFO strictly: NVARCHAR must carry a 5-byte
collation; PLP max types use the 0xFFFF length marker and 8-byte PLP
framing in rows.
BigInt columns (IntN(8)) are surfaced by tedious as strings.
LOGIN7 from tedious includes FeatureExt (UTF8 etc.); acking is optional
— mssqlite skips FEATUREEXTACK and tedious proceeds.
ETIMEOUT
ECANCEL
Advanced-grouping e2e tests must include a real NULL detail row and the
visually identical subtotal NULL, distinguished by GROUPING(). The latter
arrives as nullable-family IntN(1) metadata and a JavaScript number;
grouped and aggregate columns retain their declared source-derived widths.
Integer AVG must arrive as IntN(4) or IntN(8), not SQLite's FloatN result,
and COUNT_BIG remains IntN(8) even for zero. Tedious exposes both bigint
aggregate values as strings regardless of their runtime magnitude.
FOR JSON arrives as one column named
JSON_F52E2B61-18A1-11d1-B105-00805F49916B with NVarChar(max) metadata.
Test a value larger than 8 KiB so the server's PLP path is exercised;
tedious still surfaces the completed value as one JavaScript string.
Bulk-load e2e tests should include an AsyncIterable or thousands of rows,
an nvarchar(max)/varbinary(max) PLP value spanning packets, NULL/default
handling, a constraint rollback, and cancellation followed by another query
on the same connection. node-mssql Table + request.bulk exercises the
higher-level SqlBulkCopy-style API over the same tedious wire path.
MARS wire e2e tests should hold a multi-packet reader at its four-packet SMP
window, prove a sibling SID still completes, then cover a shared transaction,
sibling SQL error, session-local Attention, connection reuse, and FIN reply.
Separately run a .NET SqlClient reader plus concurrent command on one
MultipleActiveResultSets=True connection to catch implementation-specific
sequence/window assumptions.
Attention e2e tests should cancel a long interpreted loop after EOM, assert
ECANCEL and no row events, verify completed work remains inside an explicit
transaction, roll it back, then cancel immediately after execSql to cover
IGNORE. Run another query after each path to prove connection reuse.
User-function e2e tests should cover a scalar declared return type (BIGINT
arrives as IntN(8) and a string) and an inline TVF used as an ordinary FROM
source. CREATE FUNCTION definitions must be sent in their own request/batch.
System-procedure e2e tests should use Request.callProcedure() with named
parameters and collect every columnMetadata/row group: sp_help,
sp_helpdb, and sp_spaceused can emit multiple result sets. Procedure names
and sys. aliases are case-insensitive. Test sp_rename by querying the
renamed SQLite object afterward, not only by inspecting its return status.
Catalog-coverage tests should query INFORMATION_SCHEMA and sys.dm_exec_*
through ordinary execSql, asserting columnMetadata as well as values.
Open a second tedious connection to prove session rows are server-wide; the
querying connection should see its own request 0 as running/SELECT.
IDENTITY e2e tests should use a non-1 seed/increment, assert generated values
plus @@IDENTITY / SCOPE_IDENTITY() / IDENT_CURRENT, and verify explicit
input fails with tedious error number 544. Open a second connection when
checking session-local IDENTITY_INSERT behavior.
Windows-1252 scalar tests should assert both values (ASCII('€') = 128,
CHAR(128) = '€') and metadata: ASCII is IntN(4), while CHAR is
nullable VarChar(1), not inferred NVarChar.