turbine-orm 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +82 -18
  2. package/dist/adapters/cockroachdb.js +4 -9
  3. package/dist/cjs/adapters/cockroachdb.js +4 -9
  4. package/dist/cjs/cli/index.js +6 -3
  5. package/dist/cjs/cli/observe-ui.js +12 -2
  6. package/dist/cjs/cli/observe.js +6 -7
  7. package/dist/cjs/cli/studio.js +6 -7
  8. package/dist/cjs/client.js +33 -20
  9. package/dist/cjs/dialect.js +116 -0
  10. package/dist/cjs/errors.js +19 -1
  11. package/dist/cjs/generate.js +4 -0
  12. package/dist/cjs/index.js +3 -2
  13. package/dist/cjs/introspect.js +20 -0
  14. package/dist/cjs/mssql.js +1338 -0
  15. package/dist/cjs/mysql.js +1052 -0
  16. package/dist/cjs/query/builder.js +293 -73
  17. package/dist/cjs/sqlite.js +849 -0
  18. package/dist/cjs/typed-sql.js +9 -7
  19. package/dist/cli/index.js +6 -3
  20. package/dist/cli/observe-ui.d.ts +1 -1
  21. package/dist/cli/observe-ui.js +12 -2
  22. package/dist/cli/observe.js +7 -8
  23. package/dist/cli/studio.js +7 -8
  24. package/dist/client.d.ts +23 -1
  25. package/dist/client.js +34 -21
  26. package/dist/dialect.d.ts +258 -1
  27. package/dist/dialect.js +83 -0
  28. package/dist/errors.d.ts +12 -0
  29. package/dist/errors.js +17 -0
  30. package/dist/generate.js +4 -0
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.js +1 -1
  33. package/dist/introspect.d.ts +18 -0
  34. package/dist/introspect.js +19 -0
  35. package/dist/mssql.d.ts +233 -0
  36. package/dist/mssql.js +1298 -0
  37. package/dist/mysql.d.ts +174 -0
  38. package/dist/mysql.js +1012 -0
  39. package/dist/query/builder.d.ts +97 -0
  40. package/dist/query/builder.js +294 -74
  41. package/dist/query/index.d.ts +1 -1
  42. package/dist/sqlite.d.ts +144 -0
  43. package/dist/sqlite.js +842 -0
  44. package/dist/typed-sql.d.ts +7 -5
  45. package/dist/typed-sql.js +9 -7
  46. package/package.json +30 -1
@@ -0,0 +1,1338 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm/mssql — Microsoft SQL Server engine (driver-injected, optional peer)
4
+ *
5
+ * Binds Turbine to SQL Server 2016+ via the `mssql` driver (which wraps
6
+ * `tedious`). `mssql` is **not** a root dependency — it is an **optional peer**:
7
+ * `npm i turbine-orm` pulls nothing extra, and only consumers who
8
+ * `import 'turbine-orm/mssql'` install `mssql` themselves. The factory loads it
9
+ * through a dynamic `import('mssql')` so importing this module never crashes when
10
+ * `mssql` is absent for a consumer who does not use it. Turbine's root runtime
11
+ * dependency stays exactly `pg`.
12
+ *
13
+ * ## The three hard SQL Server realities this engine solves
14
+ *
15
+ * 1. **No `RETURNING`.** `INSERT`/`UPDATE`/`DELETE` cannot trail a `RETURNING`
16
+ * clause; SQL Server returns affected rows via `OUTPUT INSERTED.*` /
17
+ * `OUTPUT DELETED.*` injected MID-statement (between the column list and
18
+ * `VALUES`, or between `SET …` and `WHERE …`). `mssqlDialect.resultStrategy =
19
+ * 'output'`: the statement returns its own rows in ONE round-trip (executed
20
+ * exactly like the PostgreSQL `'returning'` path). Upsert becomes a
21
+ * `MERGE … WHEN MATCHED … WHEN NOT MATCHED … OUTPUT INSERTED.* ;` (a MERGE
22
+ * must end with `;`). This is the first shipped engine to exercise the
23
+ * Phase-0 `'output'` result strategy.
24
+ * 2. **No `json_agg`.** SQL Server has no JSON aggregate function; the idiomatic
25
+ * single-query nested-relation path is `(SELECT child cols … FOR JSON PATH)`,
26
+ * whose object shape is expressed by the child SELECT's column ALIASES rather
27
+ * than an explicit `JSON_OBJECT(...)`. That does NOT map onto
28
+ * `buildJsonObject`/`buildJsonArrayAgg`, so `mssqlDialect` defines the
29
+ * additive `Dialect.buildRelationSubquery` override (the sanctioned Phase-3
30
+ * seam extension) and owns the whole correlated subquery. To-many wraps
31
+ * `ISNULL((… FOR JSON PATH), '[]')` (FOR JSON over zero rows is NULL, not
32
+ * `[]`); to-one adds `, WITHOUT_ARRAY_WRAPPER` and lets NULL be the no-row
33
+ * value. Nested relations are embedded with `JSON_QUERY(...)` so they stay
34
+ * real JSON instead of being escaped as a string. `INCLUDE_NULL_VALUES`
35
+ * keeps NULL columns present (matching PostgreSQL `json_build_object`).
36
+ * 3. **No `LIMIT`.** Paging is `ORDER BY … OFFSET n ROWS FETCH NEXT m ROWS ONLY`,
37
+ * which requires an ORDER BY — a stable `ORDER BY (SELECT NULL)` is injected
38
+ * when the query has none (`Dialect.buildLimitOffset`).
39
+ *
40
+ * ## Named `@pN` placeholders (no positional `?`)
41
+ *
42
+ * `mssqlDialect.paramPlaceholder = (i) => '@p' + i`. The driver shim binds via
43
+ * `request.input('p' + i, value)`, so binding is by NAME and independent of where
44
+ * each placeholder lands in the SQL text — exactly the guarantee PostgreSQL's
45
+ * numbered `$N` gives. (SQL Server is naturally named-param friendly, sidestepping
46
+ * the positional-`?` mis-bind bug the SQLite/MySQL phases hit.)
47
+ *
48
+ * ## Capabilities & limits (vs PostgreSQL)
49
+ *
50
+ * - **Single query nested relations preserved** via `FOR JSON PATH` (SQL Server
51
+ * 2016+). Ordered/limited to-many uses `ORDER BY … OFFSET/FETCH` inside the FOR
52
+ * JSON subquery (no inner-subquery rewrite needed — FOR JSON aggregates AFTER
53
+ * the row selection).
54
+ * - **Result strategy `'output'`:** create/update/delete/upsert return their rows
55
+ * from the same statement. `createMany` returns the inserted rows via
56
+ * `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows / 2100 params
57
+ * per statement — exceeding either throws a clear `ValidationError`; chunk
58
+ * yourself or use single `create`s).
59
+ * - **MERGE concurrency caveat:** `MERGE` is the upsert primitive; under high
60
+ * concurrency a `MERGE` can still race (it is NOT a substitute for a unique
61
+ * constraint). Keep the conflict target backed by a real `UNIQUE`/`PK` index,
62
+ * and rely on the typed `UniqueConstraintError` (2627/2601 → E008) for the
63
+ * loser of a race.
64
+ * - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
65
+ * LISTEN/NOTIFY (`$listen`/`$notify`), RLS `sessionContext` (sp_set_session_context
66
+ * exists but is connection-scoped, not transaction-local, so it is not wired —
67
+ * throws rather than silently leaking context across pooled connections).
68
+ * - **Advisory-lock migration locking** is available in principle via
69
+ * `sp_getapplock`/`sp_releaseapplock` (`supportsAdvisoryLock = true`); the
70
+ * migrate CLI is still PostgreSQL-only, so this flag documents intent for a
71
+ * future adapter.
72
+ * - **Case-insensitive matching** uses `LOWER(col) LIKE LOWER(ref)` — deterministic
73
+ * regardless of the column's collation (note this can defeat an index unless a
74
+ * computed/persisted `LOWER()` index exists).
75
+ * - **bignum:** the shim applies the same safe-int policy Turbine uses for Postgres
76
+ * `int8` (number when it fits in 2^53, decimal string otherwise) WITHOUT mutating
77
+ * any global driver state. `DECIMAL`/`NUMERIC`/`MONEY` come back as strings;
78
+ * `BIT` binds/returns booleans.
79
+ * - **`DISTINCT ON`** is PostgreSQL-only and is not translated — avoid `distinct`
80
+ * on SQL Server.
81
+ *
82
+ * ## Example
83
+ *
84
+ * ```ts
85
+ * import { turbineMssql } from 'turbine-orm/mssql';
86
+ * import { SCHEMA } from './generated/turbine/metadata.js';
87
+ *
88
+ * const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA);
89
+ * const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
90
+ * await db.disconnect();
91
+ * ```
92
+ */
93
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
94
+ if (k2 === undefined) k2 = k;
95
+ var desc = Object.getOwnPropertyDescriptor(m, k);
96
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
97
+ desc = { enumerable: true, get: function() { return m[k]; } };
98
+ }
99
+ Object.defineProperty(o, k2, desc);
100
+ }) : (function(o, m, k, k2) {
101
+ if (k2 === undefined) k2 = k;
102
+ o[k2] = m[k];
103
+ }));
104
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
105
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
106
+ }) : function(o, v) {
107
+ o["default"] = v;
108
+ });
109
+ var __importStar = (this && this.__importStar) || (function () {
110
+ var ownKeys = function(o) {
111
+ ownKeys = Object.getOwnPropertyNames || function (o) {
112
+ var ar = [];
113
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
114
+ return ar;
115
+ };
116
+ return ownKeys(o);
117
+ };
118
+ return function (mod) {
119
+ if (mod && mod.__esModule) return mod;
120
+ var result = {};
121
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
122
+ __setModuleDefault(result, mod);
123
+ return result;
124
+ };
125
+ })();
126
+ Object.defineProperty(exports, "__esModule", { value: true });
127
+ exports.mssqlDialect = exports.MssqlPool = void 0;
128
+ exports.mssqlTypeToTs = mssqlTypeToTs;
129
+ exports.introspectMssqlWith = introspectMssqlWith;
130
+ exports.introspectMssql = introspectMssql;
131
+ exports.turbineMssql = turbineMssql;
132
+ const client_js_1 = require("./client.js");
133
+ const dialect_js_1 = require("./dialect.js");
134
+ const errors_js_1 = require("./errors.js");
135
+ const schema_js_1 = require("./schema.js");
136
+ // ---------------------------------------------------------------------------
137
+ // SQL Server / connection limits
138
+ // ---------------------------------------------------------------------------
139
+ /** Max bound parameters per SQL Server statement. */
140
+ const MSSQL_MAX_PARAMS = 2100;
141
+ /** Max rows per multi-row INSERT … VALUES statement. */
142
+ const MSSQL_MAX_INSERT_ROWS = 1000;
143
+ /** Canonical key sort matching the query builder's `sortedEntries`/collect order. */
144
+ function sortedRelEntries(obj) {
145
+ return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Value coercion (params in)
149
+ // ---------------------------------------------------------------------------
150
+ /**
151
+ * Coerce an arbitrary JS value into something `mssql` can bind. `BIT` accepts JS
152
+ * booleans directly; `undefined`/`null` → NULL; `bigint` follows the safe-int
153
+ * policy (number when it fits in 2^53, else a decimal string — no precision
154
+ * loss); `Date`/`Uint8Array` pass through; any remaining object/array → JSON text
155
+ * (matches how Turbine already pre-serializes JSON filter / `IN`-list params).
156
+ */
157
+ function toMssqlParam(value) {
158
+ if (value === undefined || value === null)
159
+ return null;
160
+ switch (typeof value) {
161
+ case 'boolean':
162
+ case 'number':
163
+ case 'string':
164
+ return value;
165
+ case 'bigint':
166
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER)
167
+ ? Number(value)
168
+ : value.toString();
169
+ }
170
+ if (value instanceof Date)
171
+ return value;
172
+ if (value instanceof Uint8Array)
173
+ return value; // Buffer is a Uint8Array subclass
174
+ return JSON.stringify(value);
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Result shaping + error translation
178
+ // ---------------------------------------------------------------------------
179
+ /** Shape an `mssql` result into a `pg.QueryResult`-like object. */
180
+ function shapeResult(result) {
181
+ const rows = result.recordset ?? [];
182
+ coerceBigIntColumns(rows, result.recordset?.columns);
183
+ const affected = result.rowsAffected?.[0];
184
+ return { rows, rowCount: typeof affected === 'number' ? affected : rows.length };
185
+ }
186
+ /**
187
+ * Coerce top-level `BIGINT` columns from strings to numbers, mirroring the
188
+ * Postgres `int8` policy (the TurbineClient registers a pg type parser that
189
+ * returns bigint as number within the JS safe-integer range). The `mssql`/tedious
190
+ * driver returns BIGINT as a string to avoid precision loss, so a BIGINT IDENTITY
191
+ * `id` would otherwise surface as `'1'` instead of `1`. Values outside the safe
192
+ * range are left as strings (same as the Postgres path). Nested relations are
193
+ * unaffected — `FOR JSON PATH` already renders BIGINT as a JSON number.
194
+ */
195
+ function coerceBigIntColumns(rows, columns) {
196
+ if (!columns || rows.length === 0)
197
+ return;
198
+ const bigIntCols = [];
199
+ for (const [name, col] of Object.entries(columns)) {
200
+ const t = col?.type;
201
+ const decl = String(t?.declaration ?? t?.name ?? '').toLowerCase();
202
+ if (decl === 'bigint')
203
+ bigIntCols.push(name);
204
+ }
205
+ if (bigIntCols.length === 0)
206
+ return;
207
+ for (const row of rows) {
208
+ for (const c of bigIntCols) {
209
+ const v = row[c];
210
+ if (typeof v === 'string' && /^-?\d+$/.test(v)) {
211
+ const n = Number(v);
212
+ if (Number.isSafeInteger(n))
213
+ row[c] = n;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ const EMPTY_RESULT = { rows: [], rowCount: 0 };
219
+ /**
220
+ * Augment a `mssql` driver error with the Postgres-shaped `.code` (SQLSTATE) and
221
+ * detail fields that `wrapPgError` understands, so SQL Server constraint failures
222
+ * surface as the same typed Turbine errors as Postgres (E008/E009/E010/E011) and
223
+ * deadlock / lock-timeout become retryable (E012/E013). The original error (with
224
+ * its real message) is preserved as the wrapped error's `.cause` downstream.
225
+ * Returns the value unchanged when it is not a recognizable mssql error.
226
+ */
227
+ function augmentMssqlError(err) {
228
+ if (!err || typeof err !== 'object')
229
+ return err;
230
+ const e = err;
231
+ if (typeof e.number !== 'number')
232
+ return err;
233
+ const target = e;
234
+ const msg = e.message ?? '';
235
+ switch (e.number) {
236
+ // 2627 = unique constraint / PK violation; 2601 = unique index violation.
237
+ case 2627:
238
+ case 2601: {
239
+ target.code = '23505';
240
+ // "...constraint 'UQ_users_email'..." or "...index 'IX_users_email'..."
241
+ const m = /(?:constraint|index)\s+'([^']+)'/i.exec(msg) ?? /'([^']+)'/.exec(msg);
242
+ if (m?.[1])
243
+ target.constraint = m[1];
244
+ return err;
245
+ }
246
+ // 547 = FOREIGN KEY / CHECK constraint conflict (message distinguishes them).
247
+ case 547:
248
+ target.code = /CHECK constraint/i.test(msg) ? '23514' : '23503';
249
+ return err;
250
+ // 515 = cannot insert NULL into a non-nullable column.
251
+ case 515: {
252
+ target.code = '23502';
253
+ const c = /column '([^']+)'/i.exec(msg);
254
+ if (c?.[1])
255
+ target.column = c[1];
256
+ return err;
257
+ }
258
+ // 1205 = transaction chosen as deadlock victim (retryable → pg 40P01).
259
+ case 1205:
260
+ target.code = '40P01';
261
+ return err;
262
+ // 1222 = lock request time out (retryable → pg 40001 serialization failure).
263
+ case 1222:
264
+ target.code = '40001';
265
+ return err;
266
+ default:
267
+ return err;
268
+ }
269
+ }
270
+ function normalizeQueryArgs(arg, values) {
271
+ if (typeof arg === 'string')
272
+ return { text: arg, params: values ?? [] };
273
+ return { text: arg.text, params: arg.values ?? values ?? [] };
274
+ }
275
+ /** Bind positional `params[]` to `@p1`, `@p2`, … inputs and run the statement. */
276
+ async function runRequest(request, text, params) {
277
+ for (let i = 0; i < params.length; i++) {
278
+ request.input(`p${i + 1}`, toMssqlParam(params[i]));
279
+ }
280
+ try {
281
+ const result = await request.query(text);
282
+ return shapeResult(result);
283
+ }
284
+ catch (err) {
285
+ throw augmentMssqlError(err);
286
+ }
287
+ }
288
+ /** Map a SQL-standard isolation-level name to the mssql `ISOLATION_LEVEL` constant. */
289
+ function mapIsolationLevel(sqlNS, level) {
290
+ if (!level)
291
+ return undefined;
292
+ return sqlNS.ISOLATION_LEVEL[level.trim().toUpperCase().replace(/\s+/g, '_')];
293
+ }
294
+ /**
295
+ * A transaction-scoped {@link PgCompatPoolClient}. SQL Server transactions are
296
+ * driven through the `mssql` `Transaction` API, NOT raw `BEGIN`/`COMMIT` SQL (the
297
+ * driver owns the connection a transaction is pinned to). This client intercepts
298
+ * the dialect's transaction-control statements and routes them to that API, while
299
+ * regular queries run on a `Request` bound to the active transaction (or the pool
300
+ * before BEGIN). Mirrors the MySQL shim's `runOnConnection` intent.
301
+ */
302
+ class MssqlTxClient {
303
+ pool;
304
+ sqlNS;
305
+ tx = null;
306
+ pendingIsolation = null;
307
+ constructor(pool, sqlNS) {
308
+ this.pool = pool;
309
+ this.sqlNS = sqlNS;
310
+ }
311
+ // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
312
+ async query(text, values) {
313
+ const { text: rawSql, params } = normalizeQueryArgs(text, values);
314
+ const sql = rawSql.trim();
315
+ // RELEASE SAVEPOINT has no SQL Server equivalent (savepoints auto-persist until
316
+ // the outer commit/rollback) — releaseSavepointStatement() returns '' → no-op.
317
+ if (sql === '')
318
+ return EMPTY_RESULT;
319
+ // The dialect composes `SET TRANSACTION ISOLATION LEVEL …; BEGIN TRANSACTION`
320
+ // (SQL Server cannot take an inline isolation level on BEGIN). Split and run
321
+ // the parts in order — same pattern as the MySQL shim. Gated on the exact
322
+ // transaction-control prefix so no builder/user SQL is ever split.
323
+ if (/^SET TRANSACTION ISOLATION LEVEL /i.test(sql) && sql.includes('; ')) {
324
+ let last = EMPTY_RESULT;
325
+ for (const part of sql.split('; ')) {
326
+ const p = part.trim();
327
+ if (p)
328
+ last = await this.query(p, []);
329
+ }
330
+ return last;
331
+ }
332
+ if (/^SET TRANSACTION ISOLATION LEVEL /i.test(sql)) {
333
+ this.pendingIsolation = sql.replace(/^SET TRANSACTION ISOLATION LEVEL /i, '').trim();
334
+ return EMPTY_RESULT;
335
+ }
336
+ if (/^BEGIN TRAN(SACTION)?\b/i.test(sql)) {
337
+ this.tx = new this.sqlNS.Transaction(this.pool);
338
+ await this.tx.begin(mapIsolationLevel(this.sqlNS, this.pendingIsolation));
339
+ this.pendingIsolation = null;
340
+ return EMPTY_RESULT;
341
+ }
342
+ if (/^COMMIT\b/i.test(sql)) {
343
+ if (this.tx)
344
+ await this.tx.commit();
345
+ this.tx = null;
346
+ return EMPTY_RESULT;
347
+ }
348
+ // ROLLBACK TRANSACTION <name> = rollback to a savepoint; bare ROLLBACK = abort.
349
+ const spRollback = /^ROLLBACK TRAN(?:SACTION)?\s+(\S+)/i.exec(sql);
350
+ if (spRollback) {
351
+ const req = new this.sqlNS.Request(this.tx ?? this.pool);
352
+ await req.batch(`ROLLBACK TRANSACTION ${spRollback[1]}`);
353
+ return EMPTY_RESULT;
354
+ }
355
+ if (/^ROLLBACK\b/i.test(sql)) {
356
+ if (this.tx)
357
+ await this.tx.rollback();
358
+ this.tx = null;
359
+ return EMPTY_RESULT;
360
+ }
361
+ if (/^SAVE TRAN(SACTION)?\b/i.test(sql)) {
362
+ const req = new this.sqlNS.Request(this.tx ?? this.pool);
363
+ await req.batch(sql);
364
+ return EMPTY_RESULT;
365
+ }
366
+ const request = new this.sqlNS.Request(this.tx ?? this.pool);
367
+ return runRequest(request, rawSql, params);
368
+ }
369
+ release() {
370
+ // The transaction's connection is owned by the mssql Transaction object and is
371
+ // returned to the pool on commit/rollback; nothing to release here.
372
+ }
373
+ }
374
+ /**
375
+ * A {@link PgCompatPool} backed by an `mssql` ConnectionPool. Non-transaction
376
+ * queries run on a fresh pooled `Request`; `connect()` returns a
377
+ * {@link MssqlTxClient} that drives a single transaction through the mssql
378
+ * `Transaction` API so `BEGIN`/`COMMIT`/`ROLLBACK`/savepoints all run on the same
379
+ * physical connection.
380
+ */
381
+ class MssqlPool {
382
+ /** The underlying `mssql` ConnectionPool — exposed as an escape hatch (seed / DDL / advanced ops). */
383
+ pool;
384
+ sqlNS;
385
+ closed = false;
386
+ constructor(pool, sqlNS) {
387
+ this.pool = pool;
388
+ this.sqlNS = sqlNS;
389
+ }
390
+ // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
391
+ async query(text, values) {
392
+ const { text: sql, params } = normalizeQueryArgs(text, values);
393
+ return runRequest(this.pool.request(), sql, params);
394
+ }
395
+ async connect() {
396
+ return new MssqlTxClient(this.pool, this.sqlNS);
397
+ }
398
+ async end() {
399
+ if (this.closed)
400
+ return;
401
+ this.closed = true;
402
+ await this.pool.close();
403
+ }
404
+ }
405
+ exports.MssqlPool = MssqlPool;
406
+ // ---------------------------------------------------------------------------
407
+ // Type mapping (SQL Server data type → TypeScript)
408
+ // ---------------------------------------------------------------------------
409
+ /**
410
+ * Map a SQL Server column type to a TypeScript type. `dialectType` is the
411
+ * `INFORMATION_SCHEMA.COLUMNS.DATA_TYPE` (lowercase, e.g. `bigint`, `nvarchar`,
412
+ * `datetime2`, `bit`, `uniqueidentifier`).
413
+ */
414
+ function mssqlTypeToTs(dialectType, nullable) {
415
+ const t = dialectType.toLowerCase();
416
+ let base;
417
+ if (t === 'bit')
418
+ base = 'boolean';
419
+ else if (/^(tinyint|smallint|int|bigint)$/.test(t))
420
+ base = 'number';
421
+ else if (/^(decimal|numeric|money|smallmoney)$/.test(t))
422
+ base = 'string';
423
+ else if (/^(float|real)$/.test(t))
424
+ base = 'number';
425
+ else if (/^(datetime|datetime2|smalldatetime|date|datetimeoffset)$/.test(t))
426
+ base = 'Date';
427
+ else if (t === 'uniqueidentifier')
428
+ base = 'string';
429
+ else if (/(varbinary|binary|image|rowversion|timestamp)$/.test(t))
430
+ base = 'Uint8Array';
431
+ else if (/(char|text|xml|time)/.test(t))
432
+ base = 'string';
433
+ else
434
+ base = 'unknown';
435
+ return nullable ? `${base} | null` : base;
436
+ }
437
+ /** Is a SQL Server declared type a date/time type (so values coerce back to `Date`)? */
438
+ function isMssqlDateType(dialectType) {
439
+ const t = dialectType.toLowerCase();
440
+ return (0, schema_js_1.isDateType)(t) || /^(datetime|datetime2|smalldatetime|date|datetimeoffset)$/.test(t);
441
+ }
442
+ /** Map a schema-builder (Postgres-flavored) column type to SQL Server DDL. */
443
+ function mssqlColumnType(type, maxLength) {
444
+ const t = type.toUpperCase();
445
+ if (/BIGSERIAL/.test(t))
446
+ return 'BIGINT IDENTITY(1,1)';
447
+ if (/SERIAL/.test(t))
448
+ return 'INT IDENTITY(1,1)';
449
+ if (/BIGINT|INT8/.test(t))
450
+ return 'BIGINT';
451
+ if (/SMALLINT|INT2/.test(t))
452
+ return 'SMALLINT';
453
+ if (/INTEGER|INT4|\bINT\b/.test(t))
454
+ return 'INT';
455
+ if (/BOOL/.test(t))
456
+ return 'BIT';
457
+ if (/DOUBLE|FLOAT8/.test(t))
458
+ return 'FLOAT';
459
+ if (/REAL|FLOAT4|FLOAT/.test(t))
460
+ return 'REAL';
461
+ if (/NUMERIC|DECIMAL|MONEY/.test(t))
462
+ return 'DECIMAL(38,18)';
463
+ if (/JSONB|JSON/.test(t))
464
+ return 'NVARCHAR(MAX)';
465
+ if (/UUID/.test(t))
466
+ return 'UNIQUEIDENTIFIER';
467
+ if (/TIMESTAMPTZ/.test(t))
468
+ return 'DATETIMEOFFSET';
469
+ if (/TIMESTAMP|DATETIME/.test(t))
470
+ return 'DATETIME2';
471
+ if (/\bDATE\b/.test(t))
472
+ return 'DATE';
473
+ if (/\bTIME\b/.test(t))
474
+ return 'TIME';
475
+ if (/BYTEA|BLOB/.test(t))
476
+ return 'VARBINARY(MAX)';
477
+ if (/VARCHAR/.test(t))
478
+ return maxLength != null ? `NVARCHAR(${maxLength})` : 'NVARCHAR(255)';
479
+ if (/CHAR/.test(t))
480
+ return maxLength != null ? `NCHAR(${maxLength})` : 'NCHAR(255)';
481
+ if (/TEXT|CLOB/.test(t))
482
+ return 'NVARCHAR(MAX)';
483
+ if (/ENUM/.test(t))
484
+ return 'NVARCHAR(255)';
485
+ return type;
486
+ }
487
+ // ---------------------------------------------------------------------------
488
+ // mssqlDialect — the full Dialect contract for SQL Server 2016+
489
+ // ---------------------------------------------------------------------------
490
+ /**
491
+ * SQL Server 2016+ implementation of the {@link Dialect} contract. Bracket
492
+ * identifier quoting (`[…]`), named `@pN` placeholders, the `FOR JSON PATH`
493
+ * nested-relation override (no `json_agg`), no `RETURNING`
494
+ * (`resultStrategy = 'output'` via `OUTPUT INSERTED.*` / `MERGE`), `OFFSET/FETCH`
495
+ * paging, and the Postgres-only capabilities disabled (vector / LISTEN-NOTIFY /
496
+ * RLS).
497
+ */
498
+ exports.mssqlDialect = {
499
+ ...dialect_js_1.postgresDialect,
500
+ name: 'mssql',
501
+ // No RETURNING → the statement emits its rows via OUTPUT INSERTED.* in ONE
502
+ // round-trip (Phase-0 'output' strategy, executed like 'returning').
503
+ resultStrategy: 'output',
504
+ supportsReturning: false,
505
+ supportsILike: false,
506
+ supportsVector: false,
507
+ supportsListenNotify: false,
508
+ supportsRLS: false,
509
+ // sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
510
+ supportsAdvisoryLock: true,
511
+ // FOR JSON over zero rows is NULL → coalesced in the relation override.
512
+ aggSupportsInlineOrderBy: false,
513
+ jsonPathSupport: 'limited',
514
+ emptyJsonArrayLiteral: "'[]'",
515
+ nullJsonLiteral: 'NULL',
516
+ // Named `@pN` placeholders bound by name via request.input('p'+i, value).
517
+ paramPlaceholder(index) {
518
+ return `@p${index}`;
519
+ },
520
+ // Bracket-quote identifiers; escape a literal ']' as ']]' per T-SQL rules.
521
+ quoteIdentifier(name) {
522
+ return `[${name.replace(/]/g, ']]')}]`;
523
+ },
524
+ // SQL Server aggregate casts: COUNT → INT, AVG/float → FLOAT.
525
+ castAggregate(expr, target) {
526
+ return `CAST(${expr} AS ${target === 'int' ? 'INT' : 'FLOAT'})`;
527
+ },
528
+ // No array params. OPENJSON expands a single JSON-array param into a row set,
529
+ // keeping ONE placeholder (so the SQL cache stays valid regardless of list
530
+ // length) and handling the empty list (OPENJSON('[]') → zero rows). SQL Server
531
+ // implicitly converts the nvarchar `value` to the column's type, so this works
532
+ // for numbers AND strings. OPENJSON requires SQL Server 2016+.
533
+ buildInClause(expr, paramRef, negated) {
534
+ return `${expr} ${negated ? 'NOT IN' : 'IN'} (SELECT [value] FROM OPENJSON(${paramRef}))`;
535
+ },
536
+ inClauseParam(values) {
537
+ return JSON.stringify(values ?? []);
538
+ },
539
+ // OUTPUT replaces RETURNING — injected mid-statement by the statement builders,
540
+ // never as a trailing clause.
541
+ buildReturningClause() {
542
+ return '';
543
+ },
544
+ buildInsertStatement(input) {
545
+ const out = input.returning ? ` OUTPUT INSERTED.${input.returning}` : '';
546
+ return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
547
+ },
548
+ buildBulkInsertStatement(input) {
549
+ // No UNNEST in SQL Server — emit multi-row VALUES with flattened, named `@pN`
550
+ // placeholders. Enforce the engine's 1000-row / 2100-param statement limits.
551
+ const rowCount = input.rowValues.length;
552
+ const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
553
+ if (rowCount > MSSQL_MAX_INSERT_ROWS) {
554
+ throw new errors_js_1.ValidationError(`[turbine] SQL Server INSERT … VALUES is limited to ${MSSQL_MAX_INSERT_ROWS} rows per statement (got ${rowCount}). ` +
555
+ 'Chunk the data or use individual create() calls.');
556
+ }
557
+ if (paramCount > MSSQL_MAX_PARAMS) {
558
+ throw new errors_js_1.ValidationError(`[turbine] SQL Server is limited to ${MSSQL_MAX_PARAMS} bound parameters per statement (got ${paramCount}). ` +
559
+ 'Reduce the batch size.');
560
+ }
561
+ let n = 0;
562
+ const placeholders = input.rowValues
563
+ .map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
564
+ .join(', ');
565
+ const out = input.returning ? ` OUTPUT INSERTED.${input.returning}` : '';
566
+ // skipDuplicates has no single-statement equivalent here; ignored (documented).
567
+ return {
568
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES ${placeholders}`,
569
+ params: input.rowValues.flat(),
570
+ };
571
+ },
572
+ buildUpsertStatement(input) {
573
+ // MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
574
+ // CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint —
575
+ // keep the conflict columns backed by one and rely on UniqueConstraintError
576
+ // (2627/2601 → E008) for a concurrent loser.
577
+ const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
578
+ const insertCols = input.insertColumns.join(', ');
579
+ const sourceVals = input.insertColumns.map((c) => `S.${c}`).join(', ');
580
+ const out = input.returning ? ` OUTPUT INSERTED.${input.returning}` : '';
581
+ return (`MERGE INTO ${input.table} AS T ` +
582
+ `USING (VALUES (${input.valuePlaceholders.join(', ')})) AS S (${insertCols}) ` +
583
+ `ON (${on}) ` +
584
+ `WHEN MATCHED THEN UPDATE SET ${input.updateSetClauses.join(', ')} ` +
585
+ `WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${sourceVals})` +
586
+ `${out};`);
587
+ },
588
+ // UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
589
+ // WHERE) — a trailing clause would be invalid T-SQL.
590
+ buildUpdateStatement(input) {
591
+ const out = input.returning ? ` OUTPUT INSERTED.${input.returning}` : '';
592
+ return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
593
+ },
594
+ buildDeleteStatement(input) {
595
+ const out = input.returning ? ` OUTPUT DELETED.${input.returning}` : '';
596
+ return `DELETE FROM ${input.table}${out}${input.whereSql}`;
597
+ },
598
+ // SQL Server has no LIMIT — emit OFFSET/FETCH, injecting a stable ORDER BY when
599
+ // the outer query has none (OFFSET/FETCH requires an ORDER BY).
600
+ buildLimitOffset(input) {
601
+ const { limitPlaceholder, offsetPlaceholder, hasOrderBy } = input;
602
+ if (limitPlaceholder === undefined && offsetPlaceholder === undefined)
603
+ return '';
604
+ const order = hasOrderBy ? '' : ' ORDER BY (SELECT NULL)';
605
+ const offset = offsetPlaceholder ?? '0';
606
+ let clause = `${order} OFFSET ${offset} ROWS`;
607
+ if (limitPlaceholder !== undefined)
608
+ clause += ` FETCH NEXT ${limitPlaceholder} ROWS ONLY`;
609
+ return clause;
610
+ },
611
+ // The crown jewel: nested relations via FOR JSON PATH (no json_agg). See the
612
+ // module docstring + RelationSubqueryContext for the param-push ordering
613
+ // contract (mirrors collectRelationSubqueryParams so the SQL cache stays valid).
614
+ buildRelationSubquery(ctx) {
615
+ return buildForJsonSubquery(this, ctx);
616
+ },
617
+ buildInsensitiveLike(column, paramRef) {
618
+ // Deterministic regardless of collation. NOTE: LOWER(col) can defeat an index
619
+ // unless a computed/persisted LOWER() index exists.
620
+ return `LOWER(${column}) LIKE LOWER(${paramRef})`;
621
+ },
622
+ // SQL Server has no JSON_CONTAINS. Emulate "the JSON array column contains the
623
+ // scalar value" via OPENJSON (documented `limited`: object-containment and deep
624
+ // paths are not supported — use a generated column + index for those).
625
+ buildJsonContains(column, paramRef) {
626
+ return `EXISTS (SELECT 1 FROM OPENJSON(${column}) WHERE [value] = ${paramRef})`;
627
+ },
628
+ buildJsonPathExtract(column, pathParamRef) {
629
+ return `JSON_VALUE(${column}, ${pathParamRef})`;
630
+ },
631
+ // ---- Type mapping -------------------------------------------------------
632
+ typeToTypeScript(dialectType, nullable) {
633
+ return mssqlTypeToTs(dialectType, nullable);
634
+ },
635
+ // No native array columns; bulk insert uses multi-row VALUES.
636
+ arrayType: undefined,
637
+ // ---- DDL ----------------------------------------------------------------
638
+ buildColumnType(input) {
639
+ return mssqlColumnType(input.type, input.maxLength);
640
+ },
641
+ buildColumnDefinition(input) {
642
+ const isSerial = /serial/i.test(input.type);
643
+ const parts = [input.name, this.buildColumnType(input)];
644
+ if (input.primaryKey)
645
+ parts.push('PRIMARY KEY');
646
+ else if (input.unique)
647
+ parts.push('UNIQUE');
648
+ // IDENTITY columns are implicitly NOT NULL; PK is implicitly NOT NULL too.
649
+ if (input.notNull && !isSerial && !input.primaryKey)
650
+ parts.push('NOT NULL');
651
+ if (input.defaultValue != null)
652
+ parts.push(`DEFAULT ${input.defaultValue}`);
653
+ if (input.references)
654
+ parts.push(`REFERENCES ${input.references.table}(${input.references.column})`);
655
+ return parts.join(' ');
656
+ },
657
+ buildCreateIndexStatement(input) {
658
+ return `CREATE INDEX ${input.name} ON ${input.table}(${input.columns.join(', ')});`;
659
+ },
660
+ buildCreateTableStatement(input) {
661
+ const body = input.definitions.map((d) => ` ${d}`).join(',\n');
662
+ return `CREATE TABLE ${input.table} (\n${body}\n);`;
663
+ },
664
+ // ---- Migration tracking -------------------------------------------------
665
+ // SelectApplied / UpdateChecksum / DeleteApplied inherit from postgresDialect:
666
+ // they call `this.paramPlaceholder(n)` (→ `@pN`) and emit standard SQL valid on
667
+ // SQL Server. The tracking-table DDL and the conflict-free INSERT need T-SQL forms.
668
+ buildMigrationTrackingTable(table) {
669
+ return `
670
+ IF OBJECT_ID(N'${table}', N'U') IS NULL
671
+ CREATE TABLE ${table} (
672
+ id BIGINT IDENTITY(1,1) PRIMARY KEY,
673
+ name NVARCHAR(255) NOT NULL UNIQUE,
674
+ checksum NVARCHAR(255) NOT NULL,
675
+ applied_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
676
+ );
677
+ `;
678
+ },
679
+ buildMigrationInsertApplied(table) {
680
+ // INSERT-if-absent (SQL Server has no ON CONFLICT DO NOTHING).
681
+ return (`IF NOT EXISTS (SELECT 1 FROM ${table} WHERE name = ${this.paramPlaceholder(1)}) ` +
682
+ `INSERT INTO ${table} (name, checksum) VALUES (${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)})`);
683
+ },
684
+ // ---- Transaction control ------------------------------------------------
685
+ beginStatement(isolationLevel) {
686
+ // SQL Server cannot take an inline isolation level on BEGIN TRANSACTION. The
687
+ // driver shim (MssqlTxClient) splits this exact compound and runs the parts in
688
+ // order on the same transaction.
689
+ return isolationLevel
690
+ ? `SET TRANSACTION ISOLATION LEVEL ${isolationLevel}; BEGIN TRANSACTION`
691
+ : 'BEGIN TRANSACTION';
692
+ },
693
+ commitStatement() {
694
+ return 'COMMIT TRANSACTION';
695
+ },
696
+ rollbackStatement() {
697
+ return 'ROLLBACK TRANSACTION';
698
+ },
699
+ savepointStatement(name) {
700
+ return `SAVE TRANSACTION ${name}`;
701
+ },
702
+ releaseSavepointStatement() {
703
+ // SQL Server has no RELEASE SAVEPOINT — savepoints persist until the outer
704
+ // commit/rollback. The shim treats the empty statement as a no-op.
705
+ return '';
706
+ },
707
+ rollbackToSavepointStatement(name) {
708
+ return `ROLLBACK TRANSACTION ${name}`;
709
+ },
710
+ buildSetSessionConfig() {
711
+ // Never reached: supportsRLS=false makes $transaction throw before calling
712
+ // this. Guarded here too so a misuse fails loudly. SQL Server's
713
+ // sp_set_session_context is CONNECTION-scoped (not transaction-local like
714
+ // Postgres set_config), so wiring it safely behind pooling is future work.
715
+ throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', 'mssql', 'SQL Server sp_set_session_context is connection-scoped, not transaction-local; not wired.');
716
+ },
717
+ // ---- Streaming ----------------------------------------------------------
718
+ /**
719
+ * The {@link StreamableConnection} seam only exposes `query()`, not the `mssql`
720
+ * `request.stream` event API, so this fetches once and yields the rows in
721
+ * `batchSize` chunks. True server-side streaming would require direct `mssql`
722
+ * access (documented limitation).
723
+ */
724
+ async *openStream(connection, sql, params, batchSize) {
725
+ const result = await connection.query(sql, params);
726
+ for (let i = 0; i < result.rows.length; i += batchSize) {
727
+ yield result.rows.slice(i, i + batchSize);
728
+ }
729
+ },
730
+ // ---- Introspection ------------------------------------------------------
731
+ introspector: {
732
+ async introspect(options) {
733
+ return introspectMssql(options);
734
+ },
735
+ },
736
+ };
737
+ // ---------------------------------------------------------------------------
738
+ // FOR JSON PATH relation-subquery generator (the buildRelationSubquery override)
739
+ // ---------------------------------------------------------------------------
740
+ /**
741
+ * Generate a correlated `FOR JSON PATH` subquery for one relation. SQL Server has
742
+ * no `json_agg`, so the object shape is expressed by the child SELECT's column
743
+ * ALIASES; to-many wraps `ISNULL((… FOR JSON PATH), '[]')` and to-one adds
744
+ * `, WITHOUT_ARRAY_WRAPPER`. `INCLUDE_NULL_VALUES` keeps NULL columns present so
745
+ * the parsed tree matches PostgreSQL's `json_build_object`. Nested relations are
746
+ * embedded with `JSON_QUERY(...)` so they stay real JSON instead of being escaped
747
+ * as a string.
748
+ *
749
+ * **Param-push order** strictly mirrors `collectRelationSubqueryParams`:
750
+ * - manyToMany / to-many with limit|orderBy: `where` → `limit` → nested;
751
+ * - to-one / unordered-unlimited to-many: nested → `where` (no limit).
752
+ */
753
+ function buildForJsonSubquery(dialect, ctx) {
754
+ const { relDef, spec, params, parentRef, alias, targetTable, targetMeta, targetColumns, depth, path } = ctx;
755
+ const q = (name) => dialect.quoteIdentifier(name);
756
+ const qTarget = q(targetTable);
757
+ const qParent = q(parentRef);
758
+ const orderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
759
+ const hasOrder = orderEntries.length > 0;
760
+ const hasLimit = spec !== true && spec.limit !== undefined;
761
+ /** `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys. */
762
+ const colSelect = (a) => targetColumns.map((col) => {
763
+ const field = targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
764
+ return `${a}.${q(col)} AS ${q(field)}`;
765
+ });
766
+ /** Build nested relations as `JSON_QUERY((<subquery>)) AS [<name>]` columns (pushes their params). */
767
+ const buildNested = (parentAlias) => {
768
+ if (spec === true || !spec.with)
769
+ return [];
770
+ const cols = [];
771
+ for (const [nestedRelName, nestedSpec] of sortedRelEntries(spec.with)) {
772
+ const nestedRelDef = targetMeta.relations[nestedRelName];
773
+ if (!nestedRelDef) {
774
+ throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
775
+ `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
776
+ }
777
+ const sub = ctx.recurse(nestedRelDef, nestedSpec, parentAlias, depth + 1, [...path, relDef.name]);
778
+ cols.push(`JSON_QUERY((${sub})) AS ${q(nestedRelName)}`);
779
+ }
780
+ return cols;
781
+ };
782
+ /** ORDER BY + OFFSET/FETCH paging clause for a to-many FOR JSON subquery. */
783
+ const buildPaging = (a, limitPlaceholder) => {
784
+ if (hasOrder) {
785
+ const orderBy = orderEntries
786
+ .map(([k, dir]) => {
787
+ const col = (0, schema_js_1.camelToSnake)(k);
788
+ if (!targetMeta.allColumns.includes(col)) {
789
+ throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
790
+ }
791
+ const safeDir = String(dir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
792
+ return `${a}.${q(col)} ${safeDir}`;
793
+ })
794
+ .join(', ');
795
+ // FOR JSON permits ORDER BY; OFFSET/FETCH is only needed to apply a LIMIT.
796
+ return limitPlaceholder !== undefined
797
+ ? ` ORDER BY ${orderBy} OFFSET 0 ROWS FETCH NEXT ${limitPlaceholder} ROWS ONLY`
798
+ : ` ORDER BY ${orderBy}`;
799
+ }
800
+ if (limitPlaceholder !== undefined) {
801
+ return ` ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT ${limitPlaceholder} ROWS ONLY`;
802
+ }
803
+ return '';
804
+ };
805
+ // ----- manyToMany: JOIN target through the junction table -----------------
806
+ if (relDef.type === 'manyToMany') {
807
+ return buildForJsonManyToMany(dialect, ctx, { colSelect, buildNested, buildPaging, hasLimit });
808
+ }
809
+ const isToOne = relDef.type === 'belongsTo' || relDef.type === 'hasOne';
810
+ const correlation = isToOne
811
+ ? dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey)
812
+ : dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
813
+ // ----- to-one (belongsTo / hasOne): single object, no paging --------------
814
+ if (isToOne) {
815
+ // Non-wrap order: nested → where (matches collectRelationSubqueryParams).
816
+ const nestedCols = buildNested(alias);
817
+ const extra = ctx.buildWhere(alias);
818
+ const where = extra ? `${correlation} AND ${extra}` : correlation;
819
+ const cols = [...colSelect(alias), ...nestedCols].join(', ');
820
+ // TOP 1 guarantees a single object for WITHOUT_ARRAY_WRAPPER; NULL when no row.
821
+ return `SELECT TOP 1 ${cols} FROM ${qTarget} ${alias} WHERE ${where} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES`;
822
+ }
823
+ // ----- hasMany ------------------------------------------------------------
824
+ const willWrap = hasLimit || hasOrder;
825
+ let nestedCols;
826
+ let extra;
827
+ let limitPlaceholder;
828
+ if (willWrap) {
829
+ // Wrap order: where → limit → nested.
830
+ extra = ctx.buildWhere(alias);
831
+ if (hasLimit) {
832
+ params.push(Number(spec.limit));
833
+ limitPlaceholder = dialect.paramPlaceholder(params.length);
834
+ }
835
+ nestedCols = buildNested(alias);
836
+ }
837
+ else {
838
+ // Non-wrap order: nested → where (no limit param).
839
+ nestedCols = buildNested(alias);
840
+ extra = ctx.buildWhere(alias);
841
+ }
842
+ const where = extra ? `${correlation} AND ${extra}` : correlation;
843
+ const cols = [...colSelect(alias), ...nestedCols].join(', ');
844
+ const paging = buildPaging(alias, limitPlaceholder);
845
+ return `SELECT ISNULL((SELECT ${cols} FROM ${qTarget} ${alias} WHERE ${where}${paging} FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`;
846
+ }
847
+ /** `FOR JSON PATH` subquery for a manyToMany relation (JOIN target through junction). */
848
+ function buildForJsonManyToMany(dialect, ctx, h) {
849
+ const { relDef, spec, params, parentRef, alias, targetTable, targetMeta } = ctx;
850
+ const q = (name) => dialect.quoteIdentifier(name);
851
+ if (!relDef.through) {
852
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
853
+ }
854
+ const qTarget = q(targetTable);
855
+ const qJunction = q(relDef.through.table);
856
+ const qParent = q(parentRef);
857
+ const jalias = `${alias}j`;
858
+ // JOIN: junction.targetKey = target.<PK>.
859
+ const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
860
+ if (targetMeta.primaryKey.length === 0) {
861
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
862
+ 'cannot determine the join column.');
863
+ }
864
+ const targetPk = targetMeta.primaryKey;
865
+ if (targetKeys.length !== targetPk.length) {
866
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
867
+ `but target "${targetTable}" primary key has ${targetPk.length}.`);
868
+ }
869
+ const joinOn = targetKeys.map((jcol, i) => `${jalias}.${q(jcol)} = ${alias}.${q(targetPk[i])}`).join(' AND ');
870
+ // Correlation: junction.sourceKey = parent.<referenceKey>.
871
+ const sourceKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.sourceKey);
872
+ const refKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.referenceKey);
873
+ if (sourceKeys.length !== refKeys.length) {
874
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
875
+ `but referenceKey has ${refKeys.length}.`);
876
+ }
877
+ const correlation = sourceKeys.map((jcol, i) => `${jalias}.${q(jcol)} = ${qParent}.${q(refKeys[i])}`).join(' AND ');
878
+ // Param order mirrors collectRelationSubqueryParams: where → limit → nested.
879
+ const extra = ctx.buildWhere(alias);
880
+ let limitPlaceholder;
881
+ if (h.hasLimit) {
882
+ params.push(Number(spec.limit));
883
+ limitPlaceholder = dialect.paramPlaceholder(params.length);
884
+ }
885
+ const nestedCols = h.buildNested(alias);
886
+ const where = extra ? `${correlation} AND ${extra}` : correlation;
887
+ const cols = [...h.colSelect(alias), ...nestedCols].join(', ');
888
+ const paging = h.buildPaging(alias, limitPlaceholder);
889
+ return (`SELECT ISNULL((SELECT ${cols} FROM ${qTarget} ${alias} ` +
890
+ `JOIN ${qJunction} ${jalias} ON ${joinOn} WHERE ${where}${paging} ` +
891
+ `FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`);
892
+ }
893
+ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
894
+ /**
895
+ * Derive belongsTo + hasMany relations (and conservatively-detected manyToMany
896
+ * junctions) from a flat foreign-key list. Mirrors the PostgreSQL / MySQL / SQLite
897
+ * introspectors so the produced {@link SchemaMetadata} has an identical relation
898
+ * shape across engines.
899
+ */
900
+ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
901
+ const tableSet = new Set(tableNames);
902
+ const fkCounts = new Map();
903
+ for (const fk of foreignKeys) {
904
+ const key = `${fk.sourceTable}->${fk.targetTable}`;
905
+ fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
906
+ }
907
+ const relationsByTable = new Map();
908
+ for (const fk of foreignKeys) {
909
+ if (!tableSet.has(fk.targetTable))
910
+ continue;
911
+ const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
912
+ const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
913
+ const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
914
+ const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
915
+ ? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
916
+ : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
917
+ if (!relationsByTable.has(fk.sourceTable))
918
+ relationsByTable.set(fk.sourceTable, {});
919
+ relationsByTable.get(fk.sourceTable)[belongsToName] = {
920
+ type: 'belongsTo',
921
+ name: belongsToName,
922
+ from: fk.sourceTable,
923
+ to: fk.targetTable,
924
+ foreignKey,
925
+ referenceKey,
926
+ };
927
+ const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
928
+ ? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
929
+ : (0, schema_js_1.snakeToCamel)(fk.sourceTable);
930
+ if (!relationsByTable.has(fk.targetTable))
931
+ relationsByTable.set(fk.targetTable, {});
932
+ relationsByTable.get(fk.targetTable)[hasManyName] = {
933
+ type: 'hasMany',
934
+ name: hasManyName,
935
+ from: fk.targetTable,
936
+ to: fk.sourceTable,
937
+ foreignKey,
938
+ referenceKey,
939
+ };
940
+ }
941
+ // Conservative many-to-many auto-detection (additive): a table J is a pure
942
+ // junction iff PK is exactly two columns, exactly two single-column FKs whose
943
+ // source columns ARE the PK, two distinct target tables, and no payload columns.
944
+ for (const tableName of tableNames) {
945
+ const pk = pkByTable.get(tableName) ?? [];
946
+ if (pk.length !== 2)
947
+ continue;
948
+ const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
949
+ if (tableFks.length !== 2)
950
+ continue;
951
+ if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
952
+ continue;
953
+ const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
954
+ const pkSet = new Set(pk);
955
+ if (!fkCols.every((c) => pkSet.has(c)))
956
+ continue;
957
+ if (new Set(fkCols).size !== 2)
958
+ continue;
959
+ const [fkA, fkB] = tableFks;
960
+ if (fkA.targetTable === fkB.targetTable)
961
+ continue;
962
+ const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
963
+ if (jCols.length !== 2)
964
+ continue;
965
+ const addM2M = (self, other) => {
966
+ const sourceTbl = self.targetTable;
967
+ const targetTbl = other.targetTable;
968
+ const relName = (0, schema_js_1.snakeToCamel)(targetTbl);
969
+ if (!relationsByTable.has(sourceTbl))
970
+ relationsByTable.set(sourceTbl, {});
971
+ const existing = relationsByTable.get(sourceTbl);
972
+ if (existing[relName])
973
+ return;
974
+ existing[relName] = {
975
+ type: 'manyToMany',
976
+ name: relName,
977
+ from: sourceTbl,
978
+ to: targetTbl,
979
+ referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
980
+ foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
981
+ through: {
982
+ table: tableName,
983
+ sourceKey: self.sourceColumns[0],
984
+ targetKey: other.sourceColumns[0],
985
+ },
986
+ };
987
+ };
988
+ addM2M(fkA, fkB);
989
+ addM2M(fkB, fkA);
990
+ }
991
+ return relationsByTable;
992
+ }
993
+ /**
994
+ * Introspect a SQL Server database into the same {@link SchemaMetadata} shape the
995
+ * Postgres catalog introspector produces, using a caller-supplied query executor
996
+ * (so tests can dogfood an already-open `mssql` pool/connection). Reads
997
+ * `INFORMATION_SCHEMA.*` plus `sys.identity_columns` / `sys.foreign_keys` /
998
+ * `sys.indexes`.
999
+ *
1000
+ * @param exec Runs a parameterized (`@p1`, `@p2`, …) query and returns rows.
1001
+ * @param schema The SQL Server schema to introspect (default `dbo`).
1002
+ */
1003
+ async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
1004
+ // ----- Tables -----
1005
+ let tableNames = (await exec("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
1006
+ if (options.include?.length) {
1007
+ const inc = new Set(options.include);
1008
+ tableNames = tableNames.filter((t) => inc.has(t));
1009
+ }
1010
+ if (options.exclude?.length) {
1011
+ const exc = new Set(options.exclude);
1012
+ tableNames = tableNames.filter((t) => !exc.has(t));
1013
+ }
1014
+ const tableSet = new Set(tableNames);
1015
+ // ----- Identity columns (mark hasDefault) -----
1016
+ const identityRows = await exec(`SELECT t.name AS TABLE_NAME, c.name AS COLUMN_NAME
1017
+ FROM sys.identity_columns c
1018
+ JOIN sys.tables t ON t.object_id = c.object_id
1019
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
1020
+ WHERE s.name = @p1`, [schema]);
1021
+ const identityCols = new Set(identityRows.map((r) => `${r.TABLE_NAME}.${r.COLUMN_NAME}`));
1022
+ // ----- Columns -----
1023
+ const columnRows = await exec(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, CHARACTER_MAXIMUM_LENGTH
1024
+ FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]);
1025
+ // ----- Primary keys (ordered) -----
1026
+ const pkRows = await exec(`SELECT kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION
1027
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
1028
+ JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
1029
+ ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
1030
+ WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p1
1031
+ ORDER BY kcu.TABLE_NAME, kcu.ORDINAL_POSITION`, [schema]);
1032
+ const pkByTable = new Map();
1033
+ for (const r of pkRows) {
1034
+ const t = String(r.TABLE_NAME);
1035
+ if (!tableSet.has(t))
1036
+ continue;
1037
+ if (!pkByTable.has(t))
1038
+ pkByTable.set(t, []);
1039
+ pkByTable.get(t).push(String(r.COLUMN_NAME));
1040
+ }
1041
+ const pkColSet = new Set();
1042
+ for (const [t, cols] of pkByTable)
1043
+ for (const c of cols)
1044
+ pkColSet.add(`${t}.${c}`);
1045
+ // ----- Assemble columns -----
1046
+ const columnsByTable = new Map();
1047
+ for (const c of columnRows) {
1048
+ const tableName = String(c.TABLE_NAME);
1049
+ if (!tableSet.has(tableName))
1050
+ continue;
1051
+ const colName = String(c.COLUMN_NAME);
1052
+ const dataType = String(c.DATA_TYPE).toLowerCase();
1053
+ const isPk = pkColSet.has(`${tableName}.${colName}`);
1054
+ const nullable = String(c.IS_NULLABLE).toUpperCase() === 'YES' && !isPk;
1055
+ const maxLen = c.CHARACTER_MAXIMUM_LENGTH != null ? num(c.CHARACTER_MAXIMUM_LENGTH) : undefined;
1056
+ const col = {
1057
+ name: colName,
1058
+ field: (0, schema_js_1.snakeToCamel)(colName),
1059
+ dialectType: dataType,
1060
+ pgType: dataType,
1061
+ tsType: mssqlTypeToTs(dataType, nullable),
1062
+ nullable,
1063
+ hasDefault: c.COLUMN_DEFAULT !== null || identityCols.has(`${tableName}.${colName}`),
1064
+ isArray: false,
1065
+ arrayType: undefined,
1066
+ pgArrayType: 'text[]',
1067
+ };
1068
+ if (maxLen !== undefined && maxLen >= 0)
1069
+ col.maxLength = maxLen;
1070
+ if (!columnsByTable.has(tableName))
1071
+ columnsByTable.set(tableName, []);
1072
+ columnsByTable.get(tableName).push(col);
1073
+ }
1074
+ // ----- Foreign keys (grouped by constraint for composite FKs) -----
1075
+ const fkRows = await exec(`SELECT fk.name AS CONSTRAINT_NAME,
1076
+ OBJECT_NAME(fkc.parent_object_id) AS TABLE_NAME,
1077
+ cps.name AS COLUMN_NAME,
1078
+ OBJECT_NAME(fkc.referenced_object_id) AS REFERENCED_TABLE_NAME,
1079
+ cpt.name AS REFERENCED_COLUMN_NAME,
1080
+ fkc.constraint_column_id AS ORDINAL_POSITION
1081
+ FROM sys.foreign_keys fk
1082
+ JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
1083
+ JOIN sys.columns cps ON cps.object_id = fkc.parent_object_id AND cps.column_id = fkc.parent_column_id
1084
+ JOIN sys.columns cpt ON cpt.object_id = fkc.referenced_object_id AND cpt.column_id = fkc.referenced_column_id
1085
+ JOIN sys.schemas s ON s.schema_id = fk.schema_id
1086
+ WHERE s.name = @p1
1087
+ ORDER BY fk.name, fkc.constraint_column_id`, [schema]);
1088
+ const fkByConstraint = new Map();
1089
+ for (const r of fkRows) {
1090
+ const sourceTable = String(r.TABLE_NAME);
1091
+ const targetTable = String(r.REFERENCED_TABLE_NAME);
1092
+ if (!tableSet.has(sourceTable))
1093
+ continue;
1094
+ const key = String(r.CONSTRAINT_NAME);
1095
+ let entry = fkByConstraint.get(key);
1096
+ if (!entry) {
1097
+ entry = { sourceTable, sourceColumns: [], targetTable, targetColumns: [], constraintName: key };
1098
+ fkByConstraint.set(key, entry);
1099
+ }
1100
+ entry.sourceColumns.push(String(r.COLUMN_NAME));
1101
+ entry.targetColumns.push(String(r.REFERENCED_COLUMN_NAME));
1102
+ }
1103
+ const foreignKeys = [...fkByConstraint.values()].filter((fk) => tableSet.has(fk.targetTable));
1104
+ // ----- Indexes + unique constraints -----
1105
+ const idxRows = await exec(`SELECT t.name AS TABLE_NAME, i.name AS INDEX_NAME, i.is_unique AS IS_UNIQUE,
1106
+ c.name AS COLUMN_NAME, ic.key_ordinal AS SEQ
1107
+ FROM sys.indexes i
1108
+ JOIN sys.tables t ON t.object_id = i.object_id
1109
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
1110
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
1111
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
1112
+ WHERE s.name = @p1 AND i.is_primary_key = 0 AND ic.is_included_column = 0 AND i.name IS NOT NULL
1113
+ ORDER BY t.name, i.name, ic.key_ordinal`, [schema]);
1114
+ const indexGroups = new Map();
1115
+ for (const r of idxRows) {
1116
+ const t = String(r.TABLE_NAME);
1117
+ if (!tableSet.has(t))
1118
+ continue;
1119
+ const name = String(r.INDEX_NAME);
1120
+ const key = `${t}.${name}`;
1121
+ let g = indexGroups.get(key);
1122
+ if (!g) {
1123
+ g = { table: t, name, unique: num(r.IS_UNIQUE) === 1, columns: [] };
1124
+ indexGroups.set(key, g);
1125
+ }
1126
+ g.columns.push(String(r.COLUMN_NAME));
1127
+ }
1128
+ const indexesByTable = new Map();
1129
+ const uniqueByTable = new Map();
1130
+ for (const g of indexGroups.values()) {
1131
+ if (!indexesByTable.has(g.table))
1132
+ indexesByTable.set(g.table, []);
1133
+ indexesByTable.get(g.table).push({
1134
+ name: g.name,
1135
+ columns: g.columns,
1136
+ unique: g.unique,
1137
+ definition: `${g.unique ? 'UNIQUE ' : ''}INDEX ${g.name} ON ${g.table}(${g.columns.join(', ')})`,
1138
+ });
1139
+ if (g.unique && g.columns.length > 0) {
1140
+ if (!uniqueByTable.has(g.table))
1141
+ uniqueByTable.set(g.table, []);
1142
+ uniqueByTable.get(g.table).push(g.columns);
1143
+ }
1144
+ }
1145
+ // ----- Relations -----
1146
+ const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable);
1147
+ // ----- Assemble TableMetadata -----
1148
+ const tables = {};
1149
+ for (const tableName of tableNames) {
1150
+ const columns = columnsByTable.get(tableName) ?? [];
1151
+ const columnMap = {};
1152
+ const reverseColumnMap = {};
1153
+ const dateColumns = new Set();
1154
+ const dialectTypes = {};
1155
+ const pgTypes = {};
1156
+ const allColumns = [];
1157
+ for (const col of columns) {
1158
+ columnMap[col.field] = col.name;
1159
+ reverseColumnMap[col.name] = col.field;
1160
+ allColumns.push(col.name);
1161
+ dialectTypes[col.name] = col.dialectType ?? col.pgType;
1162
+ pgTypes[col.name] = col.pgType;
1163
+ if (isMssqlDateType(col.dialectType ?? col.pgType))
1164
+ dateColumns.add(col.name);
1165
+ }
1166
+ tables[tableName] = {
1167
+ name: tableName,
1168
+ columns,
1169
+ columnMap,
1170
+ reverseColumnMap,
1171
+ dateColumns,
1172
+ dialectTypes,
1173
+ pgTypes,
1174
+ allColumns,
1175
+ primaryKey: pkByTable.get(tableName) ?? [],
1176
+ uniqueColumns: uniqueByTable.get(tableName) ?? [],
1177
+ relations: relationsByTable.get(tableName) ?? {},
1178
+ indexes: indexesByTable.get(tableName) ?? [],
1179
+ };
1180
+ }
1181
+ // SQL Server has no first-class enum type — enums are CHECK constraints; left empty.
1182
+ return { tables, enums: {} };
1183
+ }
1184
+ /**
1185
+ * Open a short-lived `mssql` connection from `options.connectionString`, introspect
1186
+ * the database (schema = `options.schema` or `dbo`), and close it. Wraps
1187
+ * {@link introspectMssqlWith} for the {@link DialectIntrospector} seam used by
1188
+ * `introspect()` / `npx turbine generate`.
1189
+ */
1190
+ async function introspectMssql(options) {
1191
+ const sqlNS = await loadMssql();
1192
+ const config = parseMssqlConfig(options.connectionString);
1193
+ const pool = await new sqlNS.ConnectionPool(config).connect();
1194
+ try {
1195
+ const mp = new MssqlPool(pool, sqlNS);
1196
+ const exec = async (sql, params) => (await mp.query(sql, params)).rows;
1197
+ return introspectMssqlWith(exec, options.schema ?? 'dbo', {
1198
+ include: options.include,
1199
+ exclude: options.exclude,
1200
+ });
1201
+ }
1202
+ finally {
1203
+ await pool.close();
1204
+ }
1205
+ }
1206
+ /** Parse a `mssql://user:pass@host:port/db` connection string into mssql config. */
1207
+ function parseMssqlConfig(connectionString) {
1208
+ try {
1209
+ const u = new URL(connectionString);
1210
+ const config = {
1211
+ server: u.hostname || 'localhost',
1212
+ port: u.port ? Number(u.port) : 1433,
1213
+ // Sensible local/dev defaults; pass an explicit config object to override.
1214
+ options: { encrypt: false, trustServerCertificate: true },
1215
+ };
1216
+ if (u.username)
1217
+ config.user = decodeURIComponent(u.username);
1218
+ if (u.password)
1219
+ config.password = decodeURIComponent(u.password);
1220
+ const db = u.pathname.replace(/^\//, '');
1221
+ if (db)
1222
+ config.database = decodeURIComponent(db);
1223
+ return config;
1224
+ }
1225
+ catch {
1226
+ throw new errors_js_1.ConnectionError(`[turbine] Invalid MSSQL connection string: "${connectionString}"`);
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Dynamically load the `mssql` module. Kept out of the static import graph so
1231
+ * `import 'turbine-orm/mssql'` never throws when `mssql` is absent for a consumer
1232
+ * that does not use the factory.
1233
+ */
1234
+ async function loadMssql() {
1235
+ let mod;
1236
+ try {
1237
+ // `mssql` ships no bundled type declarations (it needs @types/mssql, which
1238
+ // Turbine deliberately does not depend on) — the structural MssqlModule above
1239
+ // is our typed surface. Import through a widened specifier so tsc treats the
1240
+ // typeless module as `any` instead of erroring (TS7016).
1241
+ const specifier = 'mssql';
1242
+ mod = (await Promise.resolve(`${specifier}`).then(s => __importStar(require(s))));
1243
+ }
1244
+ catch (err) {
1245
+ throw new errors_js_1.ConnectionError("[turbine] turbine-orm/mssql requires the optional peer dependency 'mssql'. Install it: npm i mssql. " +
1246
+ `(${err.message})`);
1247
+ }
1248
+ // `mssql` is a CommonJS module; the namespace may be on `default` under ESM interop.
1249
+ const ns = (mod.default ?? mod);
1250
+ if (typeof ns.ConnectionPool !== 'function' || typeof ns.Request !== 'function') {
1251
+ throw new errors_js_1.ConnectionError("[turbine] Loaded 'mssql' but it is missing ConnectionPool/Request exports.");
1252
+ }
1253
+ return ns;
1254
+ }
1255
+ /**
1256
+ * Fail fast on unsupported servers: SQL Server 2016+ is required (`FOR JSON PATH`,
1257
+ * `OPENJSON`, and the relation engine need it). ProductMajorVersion 13 = 2016.
1258
+ */
1259
+ function assertSupportedVersion(majorVersion) {
1260
+ if (majorVersion > 0 && majorVersion < 13) {
1261
+ throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/mssql requires SQL Server 2016+ (FOR JSON / OPENJSON); got major version ${majorVersion}.`);
1262
+ }
1263
+ }
1264
+ function isMssqlPool(x) {
1265
+ return (!!x &&
1266
+ typeof x.request === 'function' &&
1267
+ typeof x.close === 'function');
1268
+ }
1269
+ /**
1270
+ * Create a {@link TurbineClient} bound to SQL Server 2016+ via `mssql`.
1271
+ *
1272
+ * Pass one of:
1273
+ * - a connection string (`'mssql://sa:pass@host:1433/db'`),
1274
+ * - an `mssql` config object (`{ server, user, password, database, options }`),
1275
+ * - an existing `MssqlPool` (injection — you own its lifecycle, `disconnect()` is
1276
+ * a no-op).
1277
+ *
1278
+ * When Turbine builds the pool (string/config), it probes
1279
+ * `SERVERPROPERTY('ProductMajorVersion')` to reject SQL Server < 2016, and
1280
+ * `disconnect()` closes the pool it created.
1281
+ *
1282
+ * @example
1283
+ * ```ts
1284
+ * import { turbineMssql } from 'turbine-orm/mssql';
1285
+ * const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA);
1286
+ * ```
1287
+ */
1288
+ async function turbineMssql(target, schema, options = {}) {
1289
+ let pool;
1290
+ let owns = false;
1291
+ if (target instanceof MssqlPool) {
1292
+ pool = target;
1293
+ }
1294
+ else {
1295
+ const sqlNS = await loadMssql();
1296
+ if (isMssqlPool(target)) {
1297
+ pool = new MssqlPool(target, sqlNS);
1298
+ }
1299
+ else {
1300
+ const config = typeof target === 'string' ? parseMssqlConfig(target) : target;
1301
+ const rawPool = await new sqlNS.ConnectionPool(config).connect();
1302
+ pool = new MssqlPool(rawPool, sqlNS);
1303
+ owns = true;
1304
+ }
1305
+ }
1306
+ // Probe the server version (fail fast on SQL Server < 2016).
1307
+ try {
1308
+ const rows = (await pool.query("SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS v")).rows;
1309
+ assertSupportedVersion(Number(rows[0]?.v ?? 0));
1310
+ }
1311
+ catch (err) {
1312
+ if (err instanceof errors_js_1.ConnectionError)
1313
+ throw err;
1314
+ // A non-version probe failure (permissions, etc.) should not block startup.
1315
+ }
1316
+ const client = new client_js_1.TurbineClient({
1317
+ pool,
1318
+ dialect: exports.mssqlDialect,
1319
+ preparedStatements: false,
1320
+ logging: options.logging,
1321
+ defaultLimit: options.defaultLimit,
1322
+ warnOnUnlimited: options.warnOnUnlimited,
1323
+ }, schema);
1324
+ if (owns) {
1325
+ // Turbine built this pool, so disconnect()/end() must close it. External pools
1326
+ // (injection) stay the caller's responsibility (disconnect() no-op), consistent
1327
+ // with turbineHttp / turbineMysql / turbineSqlite.
1328
+ const baseDisconnect = client.disconnect.bind(client);
1329
+ const close = async () => {
1330
+ await baseDisconnect();
1331
+ await pool.end();
1332
+ };
1333
+ const patch = client;
1334
+ patch.disconnect = close;
1335
+ patch.end = close;
1336
+ }
1337
+ return client;
1338
+ }