uql-orm 0.51.0 → 0.53.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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +5 -5
  4. package/dist/bunSql/bunSql.util.d.ts +33 -10
  5. package/dist/bunSql/bunSql.util.js +57 -42
  6. package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
  7. package/dist/bunSql/bunSqlQuerier.js +17 -8
  8. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
  9. package/dist/bunSql/bunSqlQuerierPool.js +38 -25
  10. package/dist/bunSql/index.d.ts +1 -3
  11. package/dist/bunSql/index.js +0 -3
  12. package/dist/dialect/abstractDialect.d.ts +5 -5
  13. package/dist/dialect/abstractDialect.js +7 -6
  14. package/dist/dialect/abstractSqlDialect.d.ts +59 -6
  15. package/dist/dialect/abstractSqlDialect.js +90 -32
  16. package/dist/dialect/aliases.d.ts +2 -0
  17. package/dist/dialect/aliases.js +2 -0
  18. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  19. package/dist/dialect/mergeSqlDialect.js +89 -0
  20. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -1
  21. package/dist/dialect/mysqlLikeSqlDialect.js +3 -3
  22. package/dist/dialect/pgLikeSqlDialect.js +3 -2
  23. package/dist/dialect/vectorSqlDialect.js +2 -1
  24. package/dist/http/handler.js +3 -12
  25. package/dist/http/query.js +4 -1
  26. package/dist/migrate/builder/expressions.js +5 -0
  27. package/dist/migrate/ddl/index.d.ts +8 -0
  28. package/dist/migrate/ddl/index.js +11 -0
  29. package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
  30. package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
  31. package/dist/migrate/ddl/tableDdl.d.ts +34 -0
  32. package/dist/migrate/ddl/tableDdl.js +71 -0
  33. package/dist/migrate/introspection/index.d.ts +2 -0
  34. package/dist/migrate/introspection/index.js +2 -0
  35. package/dist/migrate/introspection/mssqlIntrospector.d.ts +60 -0
  36. package/dist/migrate/introspection/mssqlIntrospector.js +205 -0
  37. package/dist/migrate/introspection/registry.d.ts +3 -0
  38. package/dist/migrate/introspection/registry.js +28 -0
  39. package/dist/migrate/migrator.js +2 -21
  40. package/dist/migrate/schemaGenerator.d.ts +8 -10
  41. package/dist/migrate/schemaGenerator.js +29 -74
  42. package/dist/mongo/mongoDialect.js +12 -14
  43. package/dist/mssql/index.d.ts +3 -0
  44. package/dist/mssql/index.js +3 -0
  45. package/dist/mssql/mssqlDialect.d.ts +155 -0
  46. package/dist/mssql/mssqlDialect.js +341 -0
  47. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  48. package/dist/mssql/mssqlQuerier.js +137 -0
  49. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  50. package/dist/mssql/mssqlQuerierPool.js +32 -0
  51. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  52. package/dist/mssql/mssqlWireTypes.js +44 -0
  53. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  54. package/dist/pglite/pgliteQuerier.js +7 -2
  55. package/dist/postgres/pgCursorStream.d.ts +20 -0
  56. package/dist/postgres/pgCursorStream.js +49 -0
  57. package/dist/postgres/pgDialect.d.ts +1 -1
  58. package/dist/postgres/pgDialect.js +1 -1
  59. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  60. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  61. package/dist/querier/abstractQuerier.js +13 -11
  62. package/dist/querier/abstractSqlQuerier.js +3 -3
  63. package/dist/schema/canonicalType.js +96 -113
  64. package/dist/sqlite/sqliteDialect.js +3 -2
  65. package/dist/type/dialect.d.ts +24 -5
  66. package/dist/type/migratorDialect.d.ts +1 -1
  67. package/dist/type/migratorDialect.js +1 -0
  68. package/dist/type/query.d.ts +1 -1
  69. package/dist/type/queryWhere.d.ts +7 -19
  70. package/dist/type/vector.d.ts +3 -1
  71. package/dist/util/dialect.util.d.ts +10 -7
  72. package/dist/util/dialect.util.js +16 -29
  73. package/dist/util/object.util.d.ts +2 -0
  74. package/dist/util/object.util.js +4 -0
  75. package/package.json +13 -3
  76. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  77. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  78. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  79. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  80. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  81. package/dist/bunSql/bunSqliteDialect.js +0 -6
@@ -0,0 +1,155 @@
1
+ import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
2
+ import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
+ /**
4
+ * Microsoft SQL Server 2017 and up - the floor `STRING_AGG` sets, every other construct here being
5
+ * 2016 or older.
6
+ *
7
+ * Identifiers are `"`-quoted rather than bracketed: `escapeIdChar` is one character that doubles to
8
+ * escape itself, `"` is the ANSI spelling, and `tedious` enables `QUOTED_IDENTIFIER` by default.
9
+ * Brackets would buy nothing and cost the shared dialect spec, which reads that one character.
10
+ */
11
+ export declare class MsSqlDialect extends MergeSqlDialect {
12
+ #private;
13
+ protected readonly featureDefaults: DialectFeatures;
14
+ readonly dialectName = "mssql";
15
+ readonly autoIncrementSuffix = "IDENTITY(1,1)";
16
+ readonly tableOptions = "";
17
+ readonly beginTransactionCommand = "BEGIN TRANSACTION";
18
+ readonly commitTransactionCommand = "COMMIT TRANSACTION";
19
+ readonly rollbackTransactionCommand = "ROLLBACK TRANSACTION";
20
+ /**
21
+ * The level rides the `BEGIN` rather than preceding it as its own statement: a `SET TRANSACTION
22
+ * ISOLATION LEVEL` sent on its own would go to whichever pooled connection served it, not the one
23
+ * the transaction opens on, and would then stick to that connection for unrelated later queries.
24
+ * `MsSqlQuerier` reads the level back off this command and hands it to the driver.
25
+ */
26
+ readonly isolationLevelStrategy = "inline";
27
+ readonly dropIndexSyntax = "on-table";
28
+ readonly booleanLiteral = "integer";
29
+ /** [The hard server limit](https://github.com/yiisoft/yii2/issues/10371), not a driver preference. */
30
+ readonly maxBindValues = 2100;
31
+ /** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
32
+ readonly returningPosition = "after-target";
33
+ readonly insertIdSource: InsertIdSource;
34
+ /** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
35
+ protected readonly mergeTargetHint = " WITH (HOLDLOCK)";
36
+ protected readonly statementTerminator = ";";
37
+ /**
38
+ * `TOP (0)`, the only way this engine says "no rows": `FETCH NEXT 0 ROWS ONLY` is rejected
39
+ * outright ("the number of rows provided for a FETCH clause must be greater then zero").
40
+ */
41
+ protected selectModifier<E>(q: Query<E>): string;
42
+ /**
43
+ * `TOP (0)` is the whole page when nothing is wanted: the engine refuses `TOP` alongside an
44
+ * `OFFSET`, and a skip into an empty result changes nothing, so no clause follows it.
45
+ */
46
+ pager(ctx: QueryContext, opts: QueryPager & {
47
+ $distinct?: boolean;
48
+ }, sorted?: boolean): void;
49
+ /**
50
+ * `SET IDENTITY_INSERT` around the insert, where the payload states a key the engine would
51
+ * otherwise generate: writing one is refused outright ("cannot insert explicit value for identity
52
+ * column ... when IDENTITY_INSERT is set to OFF") rather than ignored.
53
+ *
54
+ * Emitted only for that case, because the setting is per-session and only one table may hold it at
55
+ * a time, so it is turned back off in the same batch it was turned on.
56
+ */
57
+ insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
58
+ /** The table to toggle, or nothing when no record writes a key the engine would have generated. */
59
+ private identityInsertTarget;
60
+ /**
61
+ * A `DECIMAL` read back as the exact text it was written as, where the entity declared the field a
62
+ * `String`. `tedious` decodes the type to a JS number before anything here can see it, so the
63
+ * digits past 2^53 are gone at the wire unless the column is converted before it crosses - the same
64
+ * reason MariaDB reads a vector column through `VEC_ToText`. 41 characters covers `DECIMAL(38, s)`
65
+ * with room for the sign and the point.
66
+ */
67
+ protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
68
+ /** Named parameters, which `tedious` binds by name rather than by position. */
69
+ placeholder(index: number): string;
70
+ /** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
71
+ returningId<E>(meta: EntityMeta<E>): string;
72
+ protected returningIdExpression<E>(meta: EntityMeta<E>): string;
73
+ protected mergeReturning(expression: string): string;
74
+ /**
75
+ * A row lock is a hint on the table here, not a clause at the end of the statement, so
76
+ * {@link lockHint} emits it and this only keeps the guard - see the base declaration.
77
+ */
78
+ protected appendLock<E>(_ctx: QueryContext, entity: Type<E>, q: Query<E>): void;
79
+ protected lockHint<E>(q: Query<E>): string;
80
+ /**
81
+ * `N'...'`, always. A bare literal is `VARCHAR`, whose codepage silently destroys anything outside
82
+ * it, and every string column this dialect creates is `NVARCHAR`. Bound parameters need nothing -
83
+ * `tedious` binds a JS string as `NVarChar` already - so this reaches only inlined literals.
84
+ */
85
+ escape(value: unknown): string;
86
+ /**
87
+ * `REGEXP_LIKE` is SQL Server 2025 at database compatibility level 170; a server below that rejects
88
+ * it itself, neither the version nor the compatibility level being knowable here - the same terms
89
+ * `uuidv7()` is emitted on. It is a predicate rather than a value, so it stands alone.
90
+ */
91
+ protected regexCondition(operand: string, placeholder: string): string;
92
+ /**
93
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
94
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
95
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
96
+ */
97
+ readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
98
+ /**
99
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
100
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
101
+ */
102
+ protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
103
+ /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
104
+ createSchemaSql(schema: string): string;
105
+ /** The estimate the engine already keeps per partition, live without a stats refresh. */
106
+ estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
107
+ protected numericCast(expr: string): string;
108
+ /**
109
+ * `JSON_VALUE` returns `NVARCHAR(4000)` and, in the lax mode that is the default, answers NULL
110
+ * rather than erroring for anything longer - so a long string read through it disappears without a
111
+ * word. `OPENJSON` has no such bound, so the path is split and its last segment matched as a key.
112
+ */
113
+ protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
114
+ /**
115
+ * The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
116
+ * an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
117
+ * `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
118
+ * text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
119
+ */
120
+ protected getJsonPathJsonbExpr(escapedColumn: string, jsonPathStr: string): string;
121
+ /**
122
+ * A value being *compared* against a JSON path, which reads back as the text `JSON_VALUE` yields:
123
+ * `'true'` for a boolean, `'12'` for a number. So only a boolean needs re-spelling; a number or a
124
+ * string already binds as the text it will be compared with.
125
+ *
126
+ * There is no "parse this text as JSON" cast to bind through the way `CAST(? AS JSON)` and
127
+ * `json(?)` serve the other families - `JSON_QUERY` marks text as JSON but answers NULL for a
128
+ * scalar - which is why reading and writing need the two different binders here.
129
+ */
130
+ protected jsonScalarParam(ctx: QueryContext, value: unknown): string;
131
+ /**
132
+ * A value being *written* to a JSON path, which takes the type the driver sent: a `BIT` becomes a
133
+ * JSON boolean, a number a JSON number, a string a JSON string. `normalizeValue` has already
134
+ * flattened a boolean to 1/0 for this engine's columns, so the cast is what restores it.
135
+ */
136
+ protected jsonWriteParam(ctx: QueryContext, value: unknown): string;
137
+ /** An exploded element compares as text here, so `$elemMatch` always expands per field. */
138
+ protected readonly jsonContainmentIsPartial = false;
139
+ protected jsonElemFrom(jsonField: string, _fields: readonly string[], alias: string): string;
140
+ /** `JSON_VALUE`'s 4000-character bound applies to an element's field, unlike a whole column. */
141
+ protected jsonElemRef(alias: string, field?: string): string;
142
+ protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
143
+ protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
144
+ /** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
145
+ protected jsonSet(ctx: QueryContext, expr: string, set: Record<string, unknown>, _field?: FieldOptions): string;
146
+ /** `'append '` prefixing the path extends the array there, creating it where there is none. */
147
+ protected jsonPush(ctx: QueryContext, expr: string, push: Record<string, unknown>): string;
148
+ /** Assigning NULL to a path deletes it in lax mode, which is the default. */
149
+ protected jsonUnset(_ctx: QueryContext, expr: string, unset: readonly string[]): string;
150
+ /**
151
+ * The surviving elements are re-aggregated into an array and written back whole - there is no
152
+ * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
153
+ */
154
+ protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
155
+ }
@@ -0,0 +1,341 @@
1
+ import { COUNT_ALIAS, JSON_ELEM_ALIAS_PREFIX } from '../dialect/aliases.js';
2
+ import { jsonPath } from '../dialect/jsonSql.js';
3
+ import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
4
+ import { getMeta } from '../entity/index.js';
5
+ import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
6
+ import { QueryRaw } from '../type/index.js';
7
+ import { parseQueryLock } from '../type/index.js';
8
+ import { isAutoIncrement } from '../util/field.util.js';
9
+ import { assertNonNegativeInteger } from '../util/index.js';
10
+ import { escapeSingleQuotes } from '../util/sqlLiteral.js';
11
+ /**
12
+ * Microsoft SQL Server 2017 and up - the floor `STRING_AGG` sets, every other construct here being
13
+ * 2016 or older.
14
+ *
15
+ * Identifiers are `"`-quoted rather than bracketed: `escapeIdChar` is one character that doubles to
16
+ * escape itself, `"` is the ANSI spelling, and `tedious` enables `QUOTED_IDENTIFIER` by default.
17
+ * Brackets would buy nothing and cost the shared dialect spec, which reads that one character.
18
+ */
19
+ export class MsSqlDialect extends MergeSqlDialect {
20
+ featureDefaults = {
21
+ explicitJsonCast: false,
22
+ nativeArrays: false,
23
+ supportsJsonb: false,
24
+ // Neither object takes an `IF NOT EXISTS`; both need a `sys` catalogue lookup around them, which
25
+ // the generator does not emit.
26
+ ifNotExists: false,
27
+ indexIfNotExists: false,
28
+ schemas: true,
29
+ dropTableCascade: false,
30
+ foreignKeyAlter: true,
31
+ primaryKeyAlter: true,
32
+ generatedColumnAdd: true,
33
+ // Extended properties are out-of-band metadata with their own procedures, not comments.
34
+ commentSyntax: 'none',
35
+ vectorIndexRequiresNotNull: false,
36
+ vectorSupportsLength: true,
37
+ supportsTimestamptz: false,
38
+ stringSizing: 'varchar',
39
+ supportsUnsigned: false,
40
+ serverSideCursors: false,
41
+ };
42
+ dialectName = 'mssql';
43
+ /** `OPENJSON`'s own output columns, which every JSON operator below reads through. */
44
+ #elem = {
45
+ value: this.escapeId('value'),
46
+ key: this.escapeId('key'),
47
+ type: this.escapeId('type'),
48
+ };
49
+ autoIncrementSuffix = 'IDENTITY(1,1)';
50
+ tableOptions = '';
51
+ beginTransactionCommand = 'BEGIN TRANSACTION';
52
+ commitTransactionCommand = 'COMMIT TRANSACTION';
53
+ rollbackTransactionCommand = 'ROLLBACK TRANSACTION';
54
+ /**
55
+ * The level rides the `BEGIN` rather than preceding it as its own statement: a `SET TRANSACTION
56
+ * ISOLATION LEVEL` sent on its own would go to whichever pooled connection served it, not the one
57
+ * the transaction opens on, and would then stick to that connection for unrelated later queries.
58
+ * `MsSqlQuerier` reads the level back off this command and hands it to the driver.
59
+ */
60
+ isolationLevelStrategy = 'inline';
61
+ dropIndexSyntax = 'on-table';
62
+ booleanLiteral = 'integer';
63
+ /** [The hard server limit](https://github.com/yiisoft/yii2/issues/10371), not a driver preference. */
64
+ maxBindValues = 2100;
65
+ /** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
66
+ returningPosition = 'after-target';
67
+ insertIdSource = 'returning';
68
+ /** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
69
+ mergeTargetHint = ' WITH (HOLDLOCK)';
70
+ statementTerminator = ';';
71
+ /**
72
+ * `TOP (0)`, the only way this engine says "no rows": `FETCH NEXT 0 ROWS ONLY` is rejected
73
+ * outright ("the number of rows provided for a FETCH clause must be greater then zero").
74
+ */
75
+ selectModifier(q) {
76
+ return q.$limit === 0 ? 'TOP (0) ' : '';
77
+ }
78
+ /**
79
+ * `TOP (0)` is the whole page when nothing is wanted: the engine refuses `TOP` alongside an
80
+ * `OFFSET`, and a skip into an empty result changes nothing, so no clause follows it.
81
+ */
82
+ pager(ctx, opts, sorted = false) {
83
+ if (opts.$limit === 0) {
84
+ assertNonNegativeInteger(opts.$skip ?? 0, '$skip');
85
+ return;
86
+ }
87
+ super.pager(ctx, opts, sorted);
88
+ }
89
+ /**
90
+ * `SET IDENTITY_INSERT` around the insert, where the payload states a key the engine would
91
+ * otherwise generate: writing one is refused outright ("cannot insert explicit value for identity
92
+ * column ... when IDENTITY_INSERT is set to OFF") rather than ignored.
93
+ *
94
+ * Emitted only for that case, because the setting is per-session and only one table may hold it at
95
+ * a time, so it is turned back off in the same batch it was turned on.
96
+ */
97
+ insert(ctx, entity, payload, opts) {
98
+ const table = this.identityInsertTarget(entity, payload);
99
+ if (table) {
100
+ ctx.append(`SET IDENTITY_INSERT ${table} ON; `);
101
+ }
102
+ super.insert(ctx, entity, payload, opts);
103
+ if (table) {
104
+ ctx.append(`; SET IDENTITY_INSERT ${table} OFF`);
105
+ }
106
+ }
107
+ /** The table to toggle, or nothing when no record writes a key the engine would have generated. */
108
+ identityInsertTarget(entity, payload) {
109
+ const meta = getMeta(entity);
110
+ const [idKey] = meta.ids;
111
+ const field = meta.ids.length === 1 ? meta.fields[idKey] : undefined;
112
+ if (!field || !isAutoIncrement(field, true)) {
113
+ return undefined;
114
+ }
115
+ const records = Array.isArray(payload) ? payload : [payload];
116
+ const stated = records.some((record) => record[idKey] !== undefined);
117
+ return stated ? this.escapedTableName(meta) : undefined;
118
+ }
119
+ /**
120
+ * A `DECIMAL` read back as the exact text it was written as, where the entity declared the field a
121
+ * `String`. `tedious` decodes the type to a JS number before anything here can see it, so the
122
+ * digits past 2^53 are gone at the wire unless the column is converted before it crosses - the same
123
+ * reason MariaDB reads a vector column through `VEC_ToText`. 41 characters covers `DECIMAL(38, s)`
124
+ * with room for the sign and the point.
125
+ */
126
+ selectFieldExpr(escapedColumn, field) {
127
+ const exactDecimal = field.type === String && fieldOptionsToCanonical(field).category === 'decimal';
128
+ return exactDecimal ? `CONVERT(NVARCHAR(41), ${escapedColumn})` : escapedColumn;
129
+ }
130
+ /** Named parameters, which `tedious` binds by name rather than by position. */
131
+ placeholder(index) {
132
+ return `@p${index}`;
133
+ }
134
+ /** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
135
+ returningId(meta) {
136
+ const expression = this.returningIdExpression(meta);
137
+ return expression ? `OUTPUT ${expression}` : '';
138
+ }
139
+ returningIdExpression(meta) {
140
+ const [idKey] = meta.ids;
141
+ return meta.ids.length === 1 ? `INSERTED.${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
142
+ }
143
+ mergeReturning(expression) {
144
+ return `OUTPUT ${expression}`;
145
+ }
146
+ /**
147
+ * A row lock is a hint on the table here, not a clause at the end of the statement, so
148
+ * {@link lockHint} emits it and this only keeps the guard - see the base declaration.
149
+ */
150
+ appendLock(_ctx, entity, q) {
151
+ this.assertLockSupported(entity, q);
152
+ }
153
+ lockHint(q) {
154
+ const wait = parseQueryLock(q.$lock);
155
+ if (!wait) {
156
+ return '';
157
+ }
158
+ // `READPAST` skips locked rows and `NOWAIT` raises instead of waiting - what `SKIP LOCKED` and
159
+ // `NOWAIT` mean elsewhere. `ROWLOCK` asks the engine not to escalate to a page or the table.
160
+ const extra = wait === 'skip' ? ', READPAST' : wait === 'nowait' ? ', NOWAIT' : '';
161
+ return ` WITH (UPDLOCK, ROWLOCK${extra})`;
162
+ }
163
+ /**
164
+ * `N'...'`, always. A bare literal is `VARCHAR`, whose codepage silently destroys anything outside
165
+ * it, and every string column this dialect creates is `NVARCHAR`. Bound parameters need nothing -
166
+ * `tedious` binds a JS string as `NVarChar` already - so this reaches only inlined literals.
167
+ */
168
+ escape(value) {
169
+ if (typeof value === 'string') {
170
+ return `N'${escapeSingleQuotes(value)}'`;
171
+ }
172
+ if (value instanceof Uint8Array) {
173
+ return `0x${Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
174
+ }
175
+ return super.escape(value);
176
+ }
177
+ /**
178
+ * `REGEXP_LIKE` is SQL Server 2025 at database compatibility level 170; a server below that rejects
179
+ * it itself, neither the version nor the compatibility level being knowable here - the same terms
180
+ * `uuidv7()` is emitted on. It is a predicate rather than a value, so it stands alone.
181
+ */
182
+ regexCondition(operand, placeholder) {
183
+ return `REGEXP_LIKE(${operand}, ${placeholder})`;
184
+ }
185
+ /**
186
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
187
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
188
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
189
+ */
190
+ vectorMetrics = new Map([
191
+ ['cosine', { fn: 'VECTOR_DISTANCE', metricArg: 'cosine' }],
192
+ ['l2', { fn: 'VECTOR_DISTANCE', metricArg: 'euclidean' }],
193
+ ['inner', { fn: 'VECTOR_DISTANCE', metricArg: 'dot' }],
194
+ ]);
195
+ /**
196
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
197
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
198
+ */
199
+ appendVectorValue(ctx, value) {
200
+ ctx.append('CAST(');
201
+ super.appendVectorValue(ctx, value);
202
+ ctx.append(` AS VECTOR(${value.length}))`);
203
+ }
204
+ /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
205
+ createSchemaSql(schema) {
206
+ const literal = escapeSingleQuotes(schema);
207
+ const quoted = escapeSingleQuotes(this.escapeId(schema, true));
208
+ return `IF SCHEMA_ID(N'${literal}') IS NULL EXEC(N'CREATE SCHEMA ${quoted}')`;
209
+ }
210
+ /** The estimate the engine already keeps per partition, live without a stats refresh. */
211
+ estimatedCount(ctx, entity) {
212
+ const meta = getMeta(entity);
213
+ ctx.append(`SELECT SUM(p.rows) ${this.escapeId(COUNT_ALIAS, true)} FROM sys.partitions p` +
214
+ ` JOIN sys.objects o ON o.object_id = p.object_id` +
215
+ ` JOIN sys.schemas s ON s.schema_id = o.schema_id` +
216
+ ` WHERE p.index_id IN (0, 1) AND o.name = `);
217
+ ctx.addValue(this.resolveTableAlias(meta));
218
+ ctx.append(' AND s.name = ');
219
+ ctx.addValue(this.resolveSchema(meta) ?? 'dbo');
220
+ }
221
+ numericCast(expr) {
222
+ return `TRY_CAST(${expr} AS FLOAT)`;
223
+ }
224
+ /**
225
+ * `JSON_VALUE` returns `NVARCHAR(4000)` and, in the lax mode that is the default, answers NULL
226
+ * rather than erroring for anything longer - so a long string read through it disappears without a
227
+ * word. `OPENJSON` has no such bound, so the path is split and its last segment matched as a key.
228
+ */
229
+ getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
230
+ const dot = jsonPathStr.lastIndexOf('.');
231
+ const parent = dot === -1 ? '$' : `$.${jsonPathStr.slice(0, dot).split('.').map(escapeSingleQuotes).join('.')}`;
232
+ const leaf = escapeSingleQuotes(jsonPathStr.slice(dot + 1));
233
+ return `(SELECT ${this.#elem.value} FROM OPENJSON(${escapedColumn}, '${parent}') WHERE ${this.#elem.key} = N'${leaf}')`;
234
+ }
235
+ /**
236
+ * The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
237
+ * an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
238
+ * `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
239
+ * text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
240
+ */
241
+ getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
242
+ return this.getJsonPathScalarExpr(escapedColumn, jsonPathStr);
243
+ }
244
+ /**
245
+ * A value being *compared* against a JSON path, which reads back as the text `JSON_VALUE` yields:
246
+ * `'true'` for a boolean, `'12'` for a number. So only a boolean needs re-spelling; a number or a
247
+ * string already binds as the text it will be compared with.
248
+ *
249
+ * There is no "parse this text as JSON" cast to bind through the way `CAST(? AS JSON)` and
250
+ * `json(?)` serve the other families - `JSON_QUERY` marks text as JSON but answers NULL for a
251
+ * scalar - which is why reading and writing need the two different binders here.
252
+ */
253
+ jsonScalarParam(ctx, value) {
254
+ return (this.#jsonCompound(ctx, value) ??
255
+ this.addValue(ctx.values, typeof value === 'boolean' ? JSON.stringify(value) : value));
256
+ }
257
+ /**
258
+ * A value being *written* to a JSON path, which takes the type the driver sent: a `BIT` becomes a
259
+ * JSON boolean, a number a JSON number, a string a JSON string. `normalizeValue` has already
260
+ * flattened a boolean to 1/0 for this engine's columns, so the cast is what restores it.
261
+ */
262
+ jsonWriteParam(ctx, value) {
263
+ const compound = this.#jsonCompound(ctx, value);
264
+ if (compound) {
265
+ return compound;
266
+ }
267
+ const placeholder = this.addValue(ctx.values, value);
268
+ return typeof value === 'boolean' ? `CAST(${placeholder} AS BIT)` : placeholder;
269
+ }
270
+ /**
271
+ * An object or array bound as JSON, which is the half both binders spell the same way, or
272
+ * `undefined` for a scalar - where they diverge.
273
+ */
274
+ #jsonCompound(ctx, value) {
275
+ if (value === null || typeof value !== 'object' || value instanceof QueryRaw) {
276
+ return undefined;
277
+ }
278
+ ctx.pushValue(JSON.stringify(value));
279
+ return `JSON_QUERY(${this.placeholder(ctx.values.length)})`;
280
+ }
281
+ /** An exploded element compares as text here, so `$elemMatch` always expands per field. */
282
+ jsonContainmentIsPartial = false;
283
+ jsonElemFrom(jsonField, _fields, alias) {
284
+ return `OPENJSON(${jsonField}) ${alias}`;
285
+ }
286
+ /** `JSON_VALUE`'s 4000-character bound applies to an element's field, unlike a whole column. */
287
+ jsonElemRef(alias, field) {
288
+ return field === undefined
289
+ ? `${alias}.${this.#elem.value}`
290
+ : `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
291
+ }
292
+ jsonAll(ctx, jsonField, value) {
293
+ const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
294
+ const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
295
+ return `(${conditions.join(' AND ')})`;
296
+ }
297
+ jsonSize(ctx, jsonField, value) {
298
+ const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
299
+ return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
300
+ }
301
+ /** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
302
+ jsonSet(ctx, expr, set, _field) {
303
+ for (const [key, value] of Object.entries(set)) {
304
+ if (value === null) {
305
+ throw new TypeError(`mssql cannot $set '${key}' to null: JSON_MODIFY deletes the key instead. Use $unset, or store a JSON null through a whole-column write.`);
306
+ }
307
+ }
308
+ return Object.entries(set).reduce((acc, [key, value]) => `JSON_MODIFY(${acc}, ${jsonPath(key)}, ${this.jsonWriteParam(ctx, value)})`, `COALESCE(${expr}, '{}')`);
309
+ }
310
+ /** `'append '` prefixing the path extends the array there, creating it where there is none. */
311
+ jsonPush(ctx, expr, push) {
312
+ return Object.entries(push).reduce((acc, [key, value]) => `JSON_MODIFY(${acc}, 'append ${jsonPath(key).slice(1, -1)}', ${this.jsonWriteParam(ctx, value)})`, `COALESCE(${expr}, '{}')`);
313
+ }
314
+ /** Assigning NULL to a path deletes it in lax mode, which is the default. */
315
+ jsonUnset(_ctx, expr, unset) {
316
+ return unset.reduce((acc, key) => `JSON_MODIFY(${acc}, ${jsonPath(key)}, NULL)`, expr);
317
+ }
318
+ /**
319
+ * The surviving elements are re-aggregated into an array and written back whole - there is no
320
+ * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
321
+ */
322
+ jsonPullKey(ctx, expr, escapedCol, key, value) {
323
+ const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
324
+ const val = `${alias}.${this.#elem.value}`;
325
+ // `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
326
+ // re-encoded from its reported `type` before the array is put back together - concatenated raw,
327
+ // the result is text the engine then refuses to parse as JSON.
328
+ const encoded = `CASE ${alias}.${this.#elem.type}` +
329
+ ` WHEN 0 THEN 'null'` +
330
+ ` WHEN 1 THEN '"' + STRING_ESCAPE(${val}, 'json') + '"'` +
331
+ ` ELSE ${val} END`;
332
+ // `IS NULL OR` because a JSON null element reads back as SQL NULL, and `<>` against one is
333
+ // unknown rather than true - which silently dropped every null from the array it rebuilt.
334
+ const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM OPENJSON(${escapedCol}, ${jsonPath(key)}) ${alias}` +
335
+ ` WHERE ${val} IS NULL OR ${val} <> ${this.jsonScalarParam(ctx, value)}`;
336
+ // `JSON_MODIFY` creates a path it does not find, so a `$pull` against an absent key would add an
337
+ // empty array where the other engines leave the document alone.
338
+ const path = jsonPath(key);
339
+ return `CASE WHEN JSON_QUERY(${escapedCol}, ${path}) IS NULL THEN ${expr} ELSE JSON_MODIFY(${expr}, ${path}, JSON_QUERY(COALESCE((${kept}), '[]'))) END`;
340
+ }
341
+ }
@@ -0,0 +1,23 @@
1
+ import type { ConnectionPool } from 'mssql';
2
+ import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
3
+ import type { ExtraOptions, QueryUpdateResult } from '../type/index.js';
4
+ import type { MsSqlDialect } from './mssqlDialect.js';
5
+ /**
6
+ * A connection is a `ConnectionPool` handle here rather than a checked-out socket: `mssql` owns its
7
+ * own pool and hands out `Request`s, so what UQL holds is the pool plus, once a transaction opens,
8
+ * the `Transaction` every later request has to be bound to.
9
+ */
10
+ export declare class MsSqlQuerier extends AbstractPoolQuerier<ConnectionPool> {
11
+ #private;
12
+ constructor(connect: () => Promise<ConnectionPool>, dialect: MsSqlDialect, extra?: ExtraOptions);
13
+ internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
14
+ internalRun(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
15
+ /**
16
+ * `tedious` streams by event, not by async iterator, so rows are handed over as they arrive rather
17
+ * than collected first - buffering the whole result set would make this `all()` with extra steps.
18
+ * The `query` promise is awaited at the end so its rejection surfaces rather than going unhandled.
19
+ */
20
+ internalStream<T>(query: string, values?: unknown[]): AsyncGenerator<Awaited<T>, void, unknown>;
21
+ /** The pool owns the socket; releasing a querier only drops this one's claim on it. */
22
+ protected releaseConn(_conn: ConnectionPool, _discard: boolean): Promise<void>;
23
+ }
@@ -0,0 +1,137 @@
1
+ import { ISOLATION_LEVEL } from 'mssql';
2
+ import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
3
+ import { decodeWireTypes } from './mssqlWireTypes.js';
4
+ /**
5
+ * A connection is a `ConnectionPool` handle here rather than a checked-out socket: `mssql` owns its
6
+ * own pool and hands out `Request`s, so what UQL holds is the pool plus, once a transaction opens,
7
+ * the `Transaction` every later request has to be bound to.
8
+ */
9
+ export class MsSqlQuerier extends AbstractPoolQuerier {
10
+ #transaction;
11
+ constructor(connect, dialect, extra) {
12
+ super(dialect, connect, extra);
13
+ }
14
+ /**
15
+ * Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `tedious` infers
16
+ * a type from the JS value, which is why a `Date` and a `Uint8Array` reach it unconverted - the
17
+ * inference is right for both, and wrong only for a bare `null`, which it calls `NVarChar`.
18
+ */
19
+ #request(values) {
20
+ const request = this.#transaction ? this.#transaction.request() : this.getConn().request();
21
+ values?.forEach((value, index) => request.input(`p${index + 1}`, value));
22
+ return request;
23
+ }
24
+ async internalAll(query, values) {
25
+ const res = (await this.#request(values).query(query));
26
+ return decodeWireTypes(res.recordset, res.recordset?.columns);
27
+ }
28
+ async internalRun(query, values) {
29
+ if (await this.#driveTransaction(query)) {
30
+ return { changes: 0 };
31
+ }
32
+ const res = (await this.#request(values).query(query));
33
+ return this.buildUpdateResult({
34
+ // `rowsAffected` carries one entry per statement, and a `MERGE` upsert emits its `OUTPUT`
35
+ // alongside the write, so the counts are summed rather than read at [0].
36
+ changes: res.rowsAffected.reduce((total, count) => total + count, 0),
37
+ rows: decodeWireTypes(res.recordset, res.recordset?.columns),
38
+ });
39
+ }
40
+ /**
41
+ * `tedious` streams by event, not by async iterator, so rows are handed over as they arrive rather
42
+ * than collected first - buffering the whole result set would make this `all()` with extra steps.
43
+ * The `query` promise is awaited at the end so its rejection surfaces rather than going unhandled.
44
+ */
45
+ async *internalStream(query, values) {
46
+ const request = this.#request(values);
47
+ request.stream = true;
48
+ let pending = [];
49
+ let done = false;
50
+ let failure;
51
+ let wake;
52
+ const arrived = () => {
53
+ wake?.();
54
+ wake = undefined;
55
+ };
56
+ request.on('row', (row) => {
57
+ pending.push(row);
58
+ arrived();
59
+ });
60
+ request.on('error', (err) => {
61
+ failure ??= err;
62
+ arrived();
63
+ });
64
+ request.on('done', () => {
65
+ done = true;
66
+ arrived();
67
+ });
68
+ const completed = request.query(query).catch((err) => {
69
+ failure ??= err instanceof Error ? err : new Error(String(err));
70
+ arrived();
71
+ });
72
+ try {
73
+ while (true) {
74
+ if (pending.length) {
75
+ const batch = pending;
76
+ pending = [];
77
+ yield* batch;
78
+ continue;
79
+ }
80
+ if (failure)
81
+ throw failure;
82
+ if (done)
83
+ return;
84
+ await new Promise((resolve) => {
85
+ wake = resolve;
86
+ });
87
+ }
88
+ }
89
+ finally {
90
+ // `completed` is awaited rather than left floating so a late rejection is handled; its own
91
+ // `catch` has already recorded it, and the loop above is what raises one to the caller.
92
+ request.cancel();
93
+ await completed;
94
+ }
95
+ }
96
+ /**
97
+ * `mssql` owns its pool and hands out a `Request` per call, so a `BEGIN TRANSACTION` sent as text
98
+ * would open one on a connection the next call may not be given. Its `Transaction` object is the
99
+ * only thing that pins them together, so the three commands the dialect names are driven through
100
+ * it here instead of being sent. Compared against the dialect's own strings rather than literals,
101
+ * so renaming one cannot silently turn it back into text.
102
+ */
103
+ async #driveTransaction(query) {
104
+ const { beginTransactionCommand, commitTransactionCommand, rollbackTransactionCommand } = this.dialect;
105
+ if (query.startsWith(beginTransactionCommand)) {
106
+ this.#transaction = this.getConn().transaction();
107
+ await this.#transaction.begin(isolationLevelOf(query.slice(beginTransactionCommand.length)));
108
+ return true;
109
+ }
110
+ if (query !== commitTransactionCommand && query !== rollbackTransactionCommand) {
111
+ return false;
112
+ }
113
+ const transaction = this.#transaction;
114
+ this.#transaction = undefined;
115
+ await (query === commitTransactionCommand ? transaction?.commit() : transaction?.rollback());
116
+ return true;
117
+ }
118
+ /** The pool owns the socket; releasing a querier only drops this one's claim on it. */
119
+ async releaseConn(_conn, _discard) {
120
+ const transaction = this.#transaction;
121
+ this.#transaction = undefined;
122
+ // A querier handed back mid-transaction would otherwise leave it open on a pooled connection.
123
+ await transaction?.rollback().catch(() => undefined);
124
+ }
125
+ }
126
+ /**
127
+ * The driver constant for the level the dialect spelled into its `BEGIN`, so it applies to the
128
+ * connection the transaction actually opens on. Undefined leaves the server's own default.
129
+ */
130
+ function isolationLevelOf(suffix) {
131
+ const level = suffix
132
+ .replace(/^\s*ISOLATION LEVEL\s*/i, '')
133
+ .trim()
134
+ .toUpperCase()
135
+ .replaceAll(' ', '_');
136
+ return level ? ISOLATION_LEVEL[level] : undefined;
137
+ }