zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Databases

SQL ServerSupported

Install @zmdb/mssql for T-SQL compilation, modeled migration DDL, structural execution and catalog introspection. Its optional mssql peer remains application-selected; the package does not open, close or configure the pool.

Database-selection workflow#

The six official database packages use the same selection workflow. The package reference owns current install and peer ranges; the SQL Server package README includes the standalone TypeScript setup and full capability table.

StepSQL Server selection
Installyarn add @zmdb/mssql@1.0.0-beta.2 mssql@^12.7.0
ConfigureSupply an application-owned mssql client to mssqlDriver(client); the application closes it.
CompilecreateQueryCompiler(mssql) from @zmdb/sql produces SQL and a separate parameter array.
Migratemssql.migrations.emitUp(operation) and mssql.migrations.connection(driver) supply database-specific DDL and runner behavior; @zmdb/migrations owns up/down.
Introspectmssql.introspector.snapshot(driver) reads the real catalog.
Executedriver.execute(query) runs the compiled query; driver.transaction(...) pins transaction work.
CapabilitiesRead mssql.capabilities and the package capability table; client-specific requirements still apply.
Refusalsunordered pagination and unmodeled row-level-security declarations; see the detailed boundaries below.
Testing evidenceThe installed SQL Server consumer and the common six-database qualification prove their recorded package, client and server inputs.

A hosted-service connection guide is a recipe using one of these owners or an explicitly supplied structural adapter. Protocol compatibility alone does not create another official package or transfer the recorded server qualification to that service.

import sql from 'mssql';
import { createQueryCompiler, trustedTable } from '@zmdb/sql';
import { mssql, mssqlDriver } from '@zmdb/mssql';

const pool = await sql.connect(process.env.DATABASE_URL!);
const driver = mssqlDriver(pool);
const query = createQueryCompiler(mssql).selectFrom(trustedTable('users')).where('email', '=', 'a@b.com').compile();

const rows = await driver.execute(query);

The compiler keeps parameters positional. The adapter maps array element zero to p1, element one to p2, and so on; mssql receives those names without the leading @.

SQL contract#

ConstructEmitted SQL / behaviorCaveat
identifiers[name]; a closing ] is escaped as ]]each qualified-name segment is quoted separately
placeholders@p1, @p2, …the adapter binds p1, p2, … from the positional array
parameter budgetrepository IN lists use the dialect's 2,000-parameter ceilingSQL Server's batch limit is 2,100
paginationOFFSET … ROWS FETCH NEXT … ROWS ONLY after an explicit ORDER BYunordered pagination is refused
insert/update rowsOUTPUT INSERTED.… in the verb-specific middle of the statementenabled triggers can require an OUTPUT … INTO shape
deleted rowsOUTPUT DELETED.…the public builder does not request pre-update rows
upsertone terminated MERGE … WITH (HOLDLOCK) statementan explicit conflict target is required
auto-incrementINT IDENTITY(1,1)there is no abstract uuid type
booleansBIT; not() emits bitwise ~
timestampDATETIMEOFFSET(3)preserves a JavaScript Date instant at millisecond precision
text / JSON storageNVARCHAR(MAX)JSON is text storage, not a native JSON column
string concatenationCONCAT(column, @pN)CONCAT(NULL, 'x') returns 'x'
column migrationsADD; `ALTER COLUMN … NULL\NOT NULL; DROP COLUMN`altering a type must carry nullability
referential RESTRICTNO ACTIONT-SQL has no RESTRICT spelling

A paginated SQL Server select without .orderBy(...) is refused at compile(). The compiler does not invent ORDER BY (SELECT NULL), because that would make the query legal without making its pages reproducible.

returning() maps to the correct OUTPUT pseudo-table for insert, update and delete. SQL Server rejects OUTPUT without INTO when an enabled trigger exists for that DML action. zmdb cannot inspect target-table triggers, and OUTPUT … INTO would require a table variable and another statement, so triggered tables must use a hand-written path.

Upsert locking#

SQL Server upserts compile to MERGE with an explicit conflict target:

MERGE [users] WITH (HOLDLOCK) AS tgt
USING (VALUES (@p1, @p2)) AS src ([email], [role])
ON tgt.[email] = src.[email]
WHEN MATCHED THEN UPDATE SET [role] = src.[role]
WHEN NOT MATCHED THEN INSERT ([email], [role])
VALUES (src.[email], src.[role]);

HOLDLOCK closes the absent-key race between concurrent upserts by taking serializable range locks on the target. That correctness has a cost: hot-key workloads can block longer or deadlock. SQL Server error 1205 is classified as retryable metadata, but a transaction is retried only when the caller opts into the transaction retry policy. Keep external side effects out of a retrying callback.

Types and migrations#

All ten SqlType members have an explicit SQL Server mapping. varchar uses NVARCHAR(n) with Length<n> and NVARCHAR(MAX) without one. timestamp uses DATETIMEOFFSET(3), preserving the instant and JavaScript Date millisecond precision.

There is no uuid member in SqlType, so the dialect does not invent a UNIQUEIDENTIFIER mapping. Use Sql<'varchar'> & Length<36> for an application-generated GUID, or a custom migration when the native type is required.

Generated migrations cover table creation and removal, add/drop/alter column, named foreign keys, indexes including filtered indexes, sequences and persisted computed columns.

Refusals and boundaries#

Requested constructCurrent result / alternative
pagination without ORDER BYrefused; add .orderBy(...)
composite-key populaterefused; SQL Server does not support row-value IN
upsert without a conflict targetrefused; MERGE needs an explicit join predicate
OUTPUT on an enabled-trigger targetthe server can reject it; use a hand-written OUTPUT … INTO path
materialized viewrefused; SQL Server indexed views need a different declaration shape
row-level-security policyrefused; predicate functions and security policies are not represented
full-text searchrefused; the schema cannot declare the required catalog and index
schema introspectionreads columns, defaults, identity, keys, foreign keys, indexes, computed columns and sequences
stored-routine calls or RoutineDef DDLrefused; SQL Server's CREATE/ALTER and EXEC shapes are not modeled
database extensions and extension-backed typerefused; no PostgreSQL-style extension contract is assumed
vector or spatial extension operatorrefused; those closed operators are available only on the exact postgres dialect
expression indexrefused; add a generated column and index that instead
explicit index method other than btreerefused; SQL Server-specific index method/options are not modeled
index operator classrefused; operator classes are a PostgreSQL-only contract
hand-built type alteration without nullabilityrefused; SQL Server must restate NULL or NOT NULL
altering an existing primary keyrefused; the snapshot does not carry the existing SQL Server constraint name
reversing a dropped tablerefused; the drop operation no longer carries the removed columns

Measured coverage#

The always-on suite covers SQL Server SQL/DDL expectations and refusals, a captured SQL Server 2022 catalog, named-parameter binding, transaction pinning, and the 2,000-parameter and 1205 metadata.

A mandatory CI job and packed external consumer run migrations, CRUD, transaction rollback, type round-trips, catalog introspection and clean drift against SQL Server 2022. The developer-only suite visibly skips without ZMDB_MSSQL_URL; required lanes make the connection mandatory.

---

See also: Query Compiler · Writing a Driver · Raw SQL