uql-orm 0.51.0 → 0.52.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/bunSql/bunSql.util.d.ts +33 -10
  3. package/dist/bunSql/bunSql.util.js +57 -42
  4. package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
  5. package/dist/bunSql/bunSqlQuerier.js +17 -8
  6. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
  7. package/dist/bunSql/bunSqlQuerierPool.js +38 -25
  8. package/dist/bunSql/index.d.ts +1 -3
  9. package/dist/bunSql/index.js +0 -3
  10. package/dist/dialect/abstractSqlDialect.d.ts +58 -4
  11. package/dist/dialect/abstractSqlDialect.js +85 -24
  12. package/dist/dialect/aliases.d.ts +2 -0
  13. package/dist/dialect/aliases.js +2 -0
  14. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  15. package/dist/dialect/mergeSqlDialect.js +89 -0
  16. package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
  17. package/dist/dialect/pgLikeSqlDialect.js +4 -1
  18. package/dist/migrate/builder/expressions.js +5 -0
  19. package/dist/migrate/introspection/index.d.ts +2 -0
  20. package/dist/migrate/introspection/index.js +2 -0
  21. package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
  22. package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
  23. package/dist/migrate/introspection/registry.d.ts +3 -0
  24. package/dist/migrate/introspection/registry.js +28 -0
  25. package/dist/migrate/migrator.js +2 -21
  26. package/dist/mongo/mongoDialect.js +4 -1
  27. package/dist/mssql/index.d.ts +3 -0
  28. package/dist/mssql/index.js +3 -0
  29. package/dist/mssql/mssqlDialect.d.ts +144 -0
  30. package/dist/mssql/mssqlDialect.js +328 -0
  31. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  32. package/dist/mssql/mssqlQuerier.js +137 -0
  33. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  34. package/dist/mssql/mssqlQuerierPool.js +32 -0
  35. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  36. package/dist/mssql/mssqlWireTypes.js +44 -0
  37. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  38. package/dist/pglite/pgliteQuerier.js +7 -2
  39. package/dist/postgres/pgCursorStream.d.ts +20 -0
  40. package/dist/postgres/pgCursorStream.js +49 -0
  41. package/dist/postgres/pgDialect.d.ts +1 -1
  42. package/dist/postgres/pgDialect.js +1 -1
  43. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  44. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  45. package/dist/schema/canonicalType.js +96 -113
  46. package/dist/sqlite/sqliteDialect.js +4 -1
  47. package/dist/type/dialect.d.ts +31 -4
  48. package/dist/type/migratorDialect.d.ts +1 -1
  49. package/dist/type/migratorDialect.js +1 -0
  50. package/package.json +13 -3
  51. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  52. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  53. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  54. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  55. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  56. package/dist/bunSql/bunSqliteDialect.js +0 -6
@@ -168,8 +168,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
168
168
  opts = { ...opts, prefix };
169
169
  }
170
170
  this.where(ctx, entity, q.$where, opts);
171
- this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
172
- this.pager(ctx, q);
171
+ const sorted = this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
172
+ this.pager(ctx, q, sorted);
173
173
  }
174
174
  selectFields(ctx, entity, select, opts = {}, exclude) {
175
175
  const meta = getMeta(entity);
@@ -227,6 +227,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
227
227
  }
228
228
  });
229
229
  }
230
+ /**
231
+ * What follows `SELECT` before the projection. Empty everywhere but SQL Server, whose `FETCH` will
232
+ * not take a zero and which spells "no rows" as `TOP (0)` instead.
233
+ */
234
+ selectModifier(_q) {
235
+ return '';
236
+ }
230
237
  /**
231
238
  * The expression a scalar field is read through, the plain column by default. MariaDB reads a
232
239
  * vector column back with `VEC_ToText`, since selecting it raw yields its binary form.
@@ -248,6 +255,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
248
255
  const { alias, ref } = this.tableRef(meta);
249
256
  const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
250
257
  ctx.append(q.$distinct ? 'SELECT DISTINCT ' : 'SELECT ');
258
+ ctx.append(this.selectModifier(q));
251
259
  this.selectFields(ctx, entity, q.$select, { prefix }, q.$exclude);
252
260
  // Add related fields BEFORE FROM clause
253
261
  this.selectRelationFields(ctx, joins);
@@ -261,7 +269,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
261
269
  if (totalAlias) {
262
270
  ctx.append(`, ${this.totalOverExpr} ${this.escapeId(totalAlias, true)}`);
263
271
  }
264
- ctx.append(` FROM ${ref}`);
272
+ ctx.append(` FROM ${ref}${this.lockHint(q)}`);
265
273
  // Add JOINs AFTER FROM clause
266
274
  this.selectRelationJoins(ctx, meta, alias, joins);
267
275
  }
@@ -582,7 +590,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
582
590
  case '$ne':
583
591
  return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, this.addValue(ctx.values, val));
584
592
  case '$regex':
585
- return `${operand} ${this.regexpOp} ${this.addValue(ctx.values, val)}`;
593
+ return this.regexCondition(operand, this.addValue(ctx.values, val));
586
594
  case '$in':
587
595
  case '$nin': {
588
596
  if (!Array.isArray(val)) {
@@ -643,7 +651,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
643
651
  return `${jsonField} IS NOT NULL`;
644
652
  return this.neExpr(comparand(value), this.jsonOperand(ctx, value, asJson));
645
653
  case '$regex':
646
- return `${jsonField} ${this.regexpOp} ${this.addValue(ctx.values, value)}`;
654
+ return this.regexCondition(jsonField, this.addValue(ctx.values, value));
647
655
  case '$in':
648
656
  case '$nin':
649
657
  return this.jsonInNin(ctx, jsonField, comparand, op, value, asJson);
@@ -737,15 +745,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
737
745
  return this.addValue(ctx.values, value);
738
746
  }
739
747
  ctx.pushValue(JSON.stringify(value));
740
- return this.jsonCast('?');
748
+ // The placeholder for the value just pushed, so a named or numbered one is spelled correctly.
749
+ return this.jsonCast(this.placeholder(ctx.values.length));
741
750
  }
742
751
  /** {@link resolveOperandField}, appended. */
743
752
  getComparisonKey(ctx, entity, key, opts = {}) {
744
753
  ctx.append(this.resolveOperandField(ctx, entity, key, opts));
745
754
  }
755
+ /** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
756
+ * engines that refuse to page an unordered statement. */
746
757
  sort(ctx, entity, sort, opts = {}) {
747
758
  if (!hasKeys(sort)) {
748
- return;
759
+ return false;
749
760
  }
750
761
  // Collected before anything is appended so an unorderable key is reported instead of half a
751
762
  // clause, and because a vector distance is the primary ordering wherever it appears in the map.
@@ -756,6 +767,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
756
767
  if (terms.length) {
757
768
  ctx.append(` ORDER BY ${terms.join(', ')}`);
758
769
  }
770
+ return terms.length > 0;
759
771
  }
760
772
  /**
761
773
  * Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
@@ -815,7 +827,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
815
827
  const json = this.resolveJsonDotPath(meta, key, prefix);
816
828
  return json ? this.jsonPathExpr(json.column, json.jsonPath, 'text') : this.escapeId(key);
817
829
  }
818
- pager(ctx, opts) {
830
+ /**
831
+ * `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
832
+ * {@link MergeSqlDialect} needs: SQL Server refuses to page a statement that has none.
833
+ */
834
+ pager(ctx, opts, _sorted = false) {
819
835
  // `!== undefined`, not truthiness: `$limit: 0` asks for no rows, where "unset" means every row.
820
836
  if (opts.$limit !== undefined) {
821
837
  ctx.append(` LIMIT ${assertNonNegativeInteger(opts.$limit, '$limit')}`);
@@ -847,6 +863,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
847
863
  throw new TypeError(`${this.dialectName} cannot narrow a row lock to one table, so $lock cannot be combined with a joined relation`);
848
864
  }
849
865
  }
866
+ /**
867
+ * The lock as a hint on the table itself, for the engine that has no trailing `FOR UPDATE`. Empty
868
+ * everywhere else, which is where {@link appendLock} does the work instead - the two are the same
869
+ * lock spelled at opposite ends of the statement, so exactly one of them ever emits.
870
+ */
871
+ lockHint(_q) {
872
+ return '';
873
+ }
850
874
  /**
851
875
  * The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
852
876
  * joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
@@ -946,8 +970,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
946
970
  if (q.$having) {
947
971
  this.having(ctx, q.$having, emittedColumns);
948
972
  }
949
- this.aggregateSort(ctx, q.$sort, emittedColumns);
950
- this.pager(ctx, q);
973
+ const sorted = this.aggregateSort(ctx, q.$sort, emittedColumns);
974
+ this.pager(ctx, q, sorted);
951
975
  }
952
976
  /**
953
977
  * ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
@@ -956,13 +980,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
956
980
  */
957
981
  aggregateSort(ctx, sort, emittedColumns) {
958
982
  if (!hasKeys(sort))
959
- return;
983
+ return false;
960
984
  ctx.append(' ORDER BY ');
961
985
  Object.entries(sort).forEach(([key, dir], index) => {
962
986
  if (index > 0)
963
987
  ctx.append(', ');
964
988
  ctx.append(this.aggregateRef(emittedColumns, key, '$sort') + this.resolveSortDirection(dir));
965
989
  });
990
+ return true;
966
991
  }
967
992
  /** The SQL referencing one of an aggregate's emitted columns, rejecting any other name. */
968
993
  aggregateRef(emittedColumns, key, clause) {
@@ -1021,22 +1046,48 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1021
1046
  this.appendLock(ctx, entity, q, joins);
1022
1047
  }
1023
1048
  insert(ctx, entity, payload, opts) {
1024
- this.appendInsertValues(ctx, entity, payload);
1025
1049
  // Every engine whose ids come back from the statement itself wants the same clause, so it is
1026
- // appended once here instead of in an identical `insert` override per dialect. `returningId` is
1050
+ // built once here instead of in an identical `insert` override per dialect. `returningId` is
1027
1051
  // empty on a composite key, which has no id to ask for.
1028
- if (this.insertIdSource === 'returning') {
1029
- const returning = this.returningId(getMeta(entity));
1030
- if (returning) {
1031
- ctx.append(` ${returning}`);
1032
- }
1052
+ const returning = this.insertIdSource === 'returning' ? this.returningId(getMeta(entity)) : '';
1053
+ if (returning && this.returningPosition === 'after-target') {
1054
+ this.appendInsertValues(ctx, entity, payload, returning);
1055
+ return;
1056
+ }
1057
+ this.appendInsertValues(ctx, entity, payload);
1058
+ if (returning) {
1059
+ ctx.append(` ${returning}`);
1033
1060
  }
1034
1061
  }
1062
+ /**
1063
+ * Where the clause reporting an insert's generated ids goes. `suffix` is `RETURNING ...` at the end
1064
+ * of the statement, which every engine here but one spells that way; SQL Server's `OUTPUT` has no
1065
+ * trailing form and sits between the column list and `VALUES`.
1066
+ *
1067
+ * A knob rather than a pair of hooks: one concept decides where the string {@link returningId}
1068
+ * already built ends up, so the two ends cannot disagree.
1069
+ */
1070
+ returningPosition = 'suffix';
1035
1071
  /**
1036
1072
  * `INSERT INTO ... VALUES (...)` and nothing more. The upsert builders extend this rather than
1037
1073
  * {@link insert}: their own clause has to come before the `RETURNING`, not after it.
1038
1074
  */
1039
- appendInsertValues(ctx, entity, payload) {
1075
+ appendInsertValues(ctx, entity, payload,
1076
+ /** Spliced between the column list and `VALUES`; see {@link returningPosition}. */
1077
+ afterTarget = '') {
1078
+ const shape = this.insertShape(entity, payload);
1079
+ const tableName = this.escapedTableName(getMeta(entity));
1080
+ ctx.append(`INSERT INTO ${tableName} (${shape.columns.join(', ')})${afterTarget ? ` ${afterTarget}` : ''} VALUES `);
1081
+ this.appendValueRows(ctx, shape);
1082
+ }
1083
+ /**
1084
+ * The columns an insert writes and the records it writes them from, resolved once.
1085
+ *
1086
+ * Split out of {@link appendInsertValues} because a `MERGE` needs the same rows as a `VALUES` row
1087
+ * source rather than as an `INSERT`, and both have to apply `onInsert` defaults and the
1088
+ * JSON/vector binding rules identically.
1089
+ */
1090
+ insertShape(entity, payload) {
1040
1091
  const meta = getMeta(entity);
1041
1092
  const payloads = fillOnFields(meta, payload, 'onInsert');
1042
1093
  const keys = getInsertFieldKeys(meta, payloads);
@@ -1053,12 +1104,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1053
1104
  columns[i] = this.escapedColumnName(meta, key);
1054
1105
  kinds[i] = this.persistKind(field);
1055
1106
  }
1056
- const tableName = this.escapedTableName(meta);
1057
- ctx.append(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (`);
1107
+ return { meta, payloads, keys, fields, columns, kinds };
1108
+ }
1109
+ /** `(a, b), (c, d)` - the row constructor an INSERT and a MERGE source both write. */
1110
+ appendValueRows(ctx, { payloads, keys, fields, kinds }) {
1111
+ const width = keys.length;
1058
1112
  for (let r = 0; r < payloads.length; r++) {
1059
- if (r > 0) {
1060
- ctx.append('), (');
1061
- }
1113
+ ctx.append(r > 0 ? '), (' : '(');
1062
1114
  const record = payloads[r];
1063
1115
  for (let i = 0; i < width; i++) {
1064
1116
  if (i > 0) {
@@ -1677,6 +1729,15 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1677
1729
  get regexpOp() {
1678
1730
  return 'REGEXP';
1679
1731
  }
1732
+ /**
1733
+ * The `$regex` predicate. An infix operator on the MySQL family (`REGEXP`) and the Postgres one
1734
+ * (`~`), but a function on Oracle and SQL Server 2025 (`REGEXP_LIKE(col, ?)`) - which is why this
1735
+ * is a method rather than the operator token alone. An engine with no regex at all overrides it to
1736
+ * throw, the way {@link appendTextSearch} already does.
1737
+ */
1738
+ regexCondition(operand, placeholder) {
1739
+ return `${operand} ${this.regexpOp} ${placeholder}`;
1740
+ }
1680
1741
  get likeFn() {
1681
1742
  return 'LIKE';
1682
1743
  }
@@ -32,6 +32,8 @@ export declare const REL_NESTED_KEY = "_uql_target";
32
32
  * syntax and keeps `VALUES(col)`.
33
33
  */
34
34
  export declare const UPSERT_NEW_ROW_ALIAS = "_uql_new";
35
+ /** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
36
+ export declare const UPSERT_SOURCE_ALIAS = "_uql_src";
35
37
  /**
36
38
  * Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
37
39
  * `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
@@ -32,6 +32,8 @@ export const REL_NESTED_KEY = '_uql_target';
32
32
  * syntax and keeps `VALUES(col)`.
33
33
  */
34
34
  export const UPSERT_NEW_ROW_ALIAS = '_uql_new';
35
+ /** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
36
+ export const UPSERT_SOURCE_ALIAS = '_uql_src';
35
37
  /**
36
38
  * Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
37
39
  * `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
@@ -0,0 +1,45 @@
1
+ import type { QueryConflictPaths, QueryContext, QueryPager, Type } from '../type/index.js';
2
+ import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
+ /**
4
+ * Shared SQL between SQL Server and Oracle: the two engines that spell paging and upsert the way the
5
+ * standard does, where the Postgres and MySQL families each predate it.
6
+ *
7
+ * A family base rather than a pair of knobs, the way {@link PgLikeSqlDialect} and
8
+ * {@link MysqlLikeSqlDialect} already are - `pager` and `upsert` are both plain overrides, so nothing
9
+ * in the core has to learn that a second spelling exists.
10
+ */
11
+ export declare abstract class MergeSqlDialect extends AbstractSqlDialect {
12
+ readonly escapeIdChar = "\"";
13
+ /**
14
+ * `OFFSET ... ROWS FETCH NEXT ... ROWS ONLY`, and an `ORDER BY` where the statement has none.
15
+ *
16
+ * SQL Server refuses to page an unordered statement. A constant `ORDER BY` costs one clause the
17
+ * optimizer discards and keeps `$limit` and `$skip` meaning the same thing here as everywhere
18
+ * else; the alternative, `TOP (n)` in the select list, needs a second hook and still leaves a
19
+ * `$skip` with no `$sort` unanswerable.
20
+ */
21
+ pager(ctx: QueryContext, opts: QueryPager & {
22
+ $distinct?: boolean;
23
+ }, sorted?: boolean): void;
24
+ /**
25
+ * `MERGE`, which both engines take in place of the `ON CONFLICT`/`ON DUPLICATE KEY` the other
26
+ * families have. The rows go in as a `VALUES` row source rather than an `INSERT`, built by
27
+ * {@link AbstractSqlDialect.insertShape} so both shapes apply `onInsert` defaults identically.
28
+ *
29
+ * Every value binds before the assignments are rendered, so a `?`-placeholder engine needs none of
30
+ * the scratch-context reordering `ON CONFLICT` does - the source is read positionally, in order.
31
+ */
32
+ upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
33
+ /** `<target>.<col> = <source>.<col>` for every conflict key, which is what makes a row "the same". */
34
+ private mergeOn;
35
+ /**
36
+ * A lock hint on the merge target. `MERGE` takes an update key lock but releases it before the
37
+ * insert, so two concurrent upserts of the same key race into a duplicate-key error; `HOLDLOCK`
38
+ * holds it across both. Empty on Oracle, which does not have the hint and does not need it.
39
+ */
40
+ protected readonly mergeTargetHint: string;
41
+ /** How the merge reports the row it wrote. Oracle has no such clause on a `MERGE` and omits it. */
42
+ protected mergeReturning(expression: string): string;
43
+ /** `MERGE` must be terminated on SQL Server; nothing else here cares. */
44
+ protected readonly statementTerminator: string;
45
+ }
@@ -0,0 +1,89 @@
1
+ import { getMeta } from '../entity/index.js';
2
+ import { assertNonNegativeInteger, getKeys } from '../util/index.js';
3
+ import { AbstractSqlDialect } from './abstractSqlDialect.js';
4
+ import { UPSERT_SOURCE_ALIAS } from './aliases.js';
5
+ /**
6
+ * Shared SQL between SQL Server and Oracle: the two engines that spell paging and upsert the way the
7
+ * standard does, where the Postgres and MySQL families each predate it.
8
+ *
9
+ * A family base rather than a pair of knobs, the way {@link PgLikeSqlDialect} and
10
+ * {@link MysqlLikeSqlDialect} already are - `pager` and `upsert` are both plain overrides, so nothing
11
+ * in the core has to learn that a second spelling exists.
12
+ */
13
+ export class MergeSqlDialect extends AbstractSqlDialect {
14
+ escapeIdChar = '"';
15
+ /**
16
+ * `OFFSET ... ROWS FETCH NEXT ... ROWS ONLY`, and an `ORDER BY` where the statement has none.
17
+ *
18
+ * SQL Server refuses to page an unordered statement. A constant `ORDER BY` costs one clause the
19
+ * optimizer discards and keeps `$limit` and `$skip` meaning the same thing here as everywhere
20
+ * else; the alternative, `TOP (n)` in the select list, needs a second hook and still leaves a
21
+ * `$skip` with no `$sort` unanswerable.
22
+ */
23
+ pager(ctx, opts, sorted = false) {
24
+ if (opts.$limit === undefined && opts.$skip === undefined) {
25
+ return;
26
+ }
27
+ if (!sorted) {
28
+ // A `SELECT DISTINCT` may only order by something it projects, so the constant cannot be used
29
+ // there; the first projected column is the one term always available.
30
+ ctx.append(` ORDER BY ${opts.$distinct ? '1' : '(SELECT NULL)'}`);
31
+ }
32
+ ctx.append(` OFFSET ${assertNonNegativeInteger(opts.$skip ?? 0, '$skip')} ROWS`);
33
+ if (opts.$limit !== undefined) {
34
+ ctx.append(` FETCH NEXT ${assertNonNegativeInteger(opts.$limit, '$limit')} ROWS ONLY`);
35
+ }
36
+ }
37
+ /**
38
+ * `MERGE`, which both engines take in place of the `ON CONFLICT`/`ON DUPLICATE KEY` the other
39
+ * families have. The rows go in as a `VALUES` row source rather than an `INSERT`, built by
40
+ * {@link AbstractSqlDialect.insertShape} so both shapes apply `onInsert` defaults identically.
41
+ *
42
+ * Every value binds before the assignments are rendered, so a `?`-placeholder engine needs none of
43
+ * the scratch-context reordering `ON CONFLICT` does - the source is read positionally, in order.
44
+ */
45
+ upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
46
+ const meta = getMeta(entity);
47
+ const table = this.escapedTableName(meta);
48
+ const source = this.escapeId(UPSERT_SOURCE_ALIAS, true);
49
+ // Before the row source, which is what fills the payload's `onInsert` fields: a column that
50
+ // exists only there - the generated key, `createdAt` - must not join the update set, or a row
51
+ // that already existed has both rewritten.
52
+ const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, (col) => `${source}.${col}`);
53
+ const shape = this.insertShape(entity, payload);
54
+ const columns = shape.columns.join(', ');
55
+ ctx.append(`MERGE INTO ${table}${this.mergeTargetHint} USING (VALUES `);
56
+ this.appendValueRows(ctx, shape);
57
+ ctx.append(`) AS ${source} (${columns}) ON ${this.mergeOn(meta, conflictPaths, table, source)}`);
58
+ if (update) {
59
+ ctx.append(` WHEN MATCHED THEN UPDATE SET ${update}`);
60
+ }
61
+ ctx.append(` WHEN NOT MATCHED THEN INSERT (${columns}) VALUES (${shape.columns.map((col) => `${source}.${col}`).join(', ')})`);
62
+ const returning = [this.returningIdExpression(meta), extraReturning].filter(Boolean).join(', ');
63
+ if (returning) {
64
+ ctx.append(` ${this.mergeReturning(returning)}`);
65
+ }
66
+ ctx.append(this.statementTerminator);
67
+ }
68
+ /** `<target>.<col> = <source>.<col>` for every conflict key, which is what makes a row "the same". */
69
+ mergeOn(meta, conflictPaths, table, source) {
70
+ return getKeys(conflictPaths)
71
+ .map((key) => {
72
+ const column = this.escapeId(this.resolveColumnName(key, meta.fields[key]));
73
+ return `${table}.${column} = ${source}.${column}`;
74
+ })
75
+ .join(' AND ');
76
+ }
77
+ /**
78
+ * A lock hint on the merge target. `MERGE` takes an update key lock but releases it before the
79
+ * insert, so two concurrent upserts of the same key race into a duplicate-key error; `HOLDLOCK`
80
+ * holds it across both. Empty on Oracle, which does not have the hint and does not need it.
81
+ */
82
+ mergeTargetHint = '';
83
+ /** How the merge reports the row it wrote. Oracle has no such clause on a `MERGE` and omits it. */
84
+ mergeReturning(expression) {
85
+ return `RETURNING ${expression}`;
86
+ }
87
+ /** `MERGE` must be terminated on SQL Server; nothing else here cares. */
88
+ statementTerminator = '';
89
+ }
@@ -37,7 +37,10 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
37
37
  vectorIndexRequiresNotNull: false,
38
38
  vectorSupportsLength: false,
39
39
  supportsTimestamptz: false,
40
- defaultStringAsText: false,
40
+ stringSizing: 'varchar',
41
+ supportsUnsigned: true,
42
+ multipleCascadePaths: true,
43
+ serverSideCursors: false,
41
44
  };
42
45
  /**
43
46
  * `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
@@ -37,7 +37,10 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
37
37
  vectorIndexRequiresNotNull: false,
38
38
  vectorSupportsLength: true,
39
39
  supportsTimestamptz: true,
40
- defaultStringAsText: true,
40
+ stringSizing: 'bounded-text',
41
+ supportsUnsigned: false,
42
+ multipleCascadePaths: true,
43
+ serverSideCursors: true,
41
44
  };
42
45
  escapeIdChar = '"';
43
46
  // Shared default for both dialects. CockroachDB docs flag sequential PKs as a hotspotting risk
@@ -26,6 +26,11 @@ export const DIALECT_DEFAULTS = {
26
26
  mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES },
27
27
  mariadb: { expressions: { ...MYSQL, uuidv7: 'UUID_v7()' }, wrapTypes: MYSQL_LARGE_TYPES },
28
28
  sqlite: { expressions: ANSI },
29
+ // `SYSUTCDATETIME()` over `CURRENT_TIMESTAMP`, which is local time in the server's zone. No
30
+ // `uuidv7`: `NEWSEQUENTIALID()` is an ordered v4 GUID, so it carries no readable timestamp and
31
+ // does not sort the way a v7 does elsewhere - a `uuidv7()` default is refused rather than served
32
+ // something that only looks like one.
33
+ mssql: { expressions: { ...ANSI, now: 'SYSUTCDATETIME()', uuid: 'NEWID()' } },
29
34
  };
30
35
  /**
31
36
  * A DDL default that is SQL rather than a literal. A class, not a plain object, so a JSON default
@@ -1,5 +1,7 @@
1
1
  export * from './abstractSqlSchemaIntrospector.js';
2
2
  export * from './mongoIntrospector.js';
3
+ export * from './mssqlIntrospector.js';
3
4
  export { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntrospector.js';
4
5
  export * from './postgresIntrospector.js';
5
6
  export * from './sqliteIntrospector.js';
7
+ export { introspectorFor } from './registry.js';
@@ -1,5 +1,7 @@
1
1
  export * from './abstractSqlSchemaIntrospector.js';
2
2
  export * from './mongoIntrospector.js';
3
+ export * from './mssqlIntrospector.js';
3
4
  export { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntrospector.js';
4
5
  export * from './postgresIntrospector.js';
5
6
  export * from './sqliteIntrospector.js';
7
+ export { introspectorFor } from './registry.js';
@@ -0,0 +1,63 @@
1
+ import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
+ import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
+ /**
4
+ * SQL Server schema introspector.
5
+ *
6
+ * `INFORMATION_SCHEMA` answers columns and foreign keys, but not indexes: it has no view for them at
7
+ * all, and its `CONSTRAINT_COLUMN_USAGE` conflates a unique index with a unique constraint. Those
8
+ * come from `sys.indexes` instead, which is also the only place the filtered-index predicate and the
9
+ * included columns are readable.
10
+ */
11
+ export declare class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
12
+ protected readonly defaultSchemaExpr = "SCHEMA_NAME()";
13
+ protected getTableNamesQuery(): string;
14
+ protected tableExistsQuery(): string;
15
+ protected parseTableExistsResult(results: {
16
+ count?: number;
17
+ }[]): boolean;
18
+ /**
19
+ * `is_identity` and the default come from `sys`, not `INFORMATION_SCHEMA`: the latter has no
20
+ * identity flag, and reports a default already wrapped in the parentheses the engine reprints it
21
+ * with, which is the same text either way but only nameable as a constraint through `sys`.
22
+ */
23
+ protected getColumnsQuery(_tableName: string): string;
24
+ /** `is_primary_key` is excluded: the key is read separately, the way every other engine reads it. */
25
+ protected getIndexesQuery(_tableName: string): string;
26
+ protected getForeignKeysQuery(_tableName: string): string;
27
+ protected getPrimaryKeyQuery(_tableName: string): string;
28
+ /**
29
+ * `max_length` is in bytes, so an `NVARCHAR` column reports twice its declared length and `MAX`
30
+ * reports `-1`. Read as characters, every Unicode column would drift to double its width the first
31
+ * time it was diffed against the entity that declared it.
32
+ */
33
+ protected mapColumnsResult(read: TableRowReader, tableName: string, results: MsSqlColumnRow[]): Promise<ColumnSchema[]>;
34
+ protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: {
35
+ index_name: string;
36
+ columns: string;
37
+ is_unique: boolean;
38
+ }[]): Promise<IndexSchema[]>;
39
+ protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
40
+ constraint_name: string;
41
+ columns: string;
42
+ referenced_table: string;
43
+ referenced_columns: string;
44
+ delete_rule: string;
45
+ update_rule: string;
46
+ }[]): Promise<ForeignKeySchema[]>;
47
+ /**
48
+ * A default is stored wrapped in at least one layer of parentheses - `((0))` for a number,
49
+ * `(N'x')` for a string - because the engine reprints it from its own parse tree.
50
+ */
51
+ protected parseDefaultValue(defaultValue: string | null): unknown;
52
+ }
53
+ type MsSqlColumnRow = {
54
+ column_name: string;
55
+ data_type: string;
56
+ max_length: number | null;
57
+ numeric_precision: number | null;
58
+ numeric_scale: number | null;
59
+ is_nullable: boolean;
60
+ is_identity: boolean;
61
+ column_default: string | null;
62
+ };
63
+ export {};