uql-orm 0.52.0 → 0.54.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 (64) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +5 -5
  3. package/dist/cockroachdb/cockroachDialect.d.ts +2 -5
  4. package/dist/cockroachdb/cockroachDialect.js +2 -5
  5. package/dist/dialect/abstractDialect.d.ts +5 -5
  6. package/dist/dialect/abstractDialect.js +7 -6
  7. package/dist/dialect/abstractSqlDialect.d.ts +8 -3
  8. package/dist/dialect/abstractSqlDialect.js +14 -11
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -4
  10. package/dist/dialect/mysqlLikeSqlDialect.js +0 -6
  11. package/dist/dialect/pgLikeSqlDialect.js +0 -2
  12. package/dist/dialect/vectorSqlDialect.js +2 -1
  13. package/dist/entity/decorator/members.d.ts +3 -10
  14. package/dist/entity/metadata/definition.js +0 -4
  15. package/dist/http/handler.js +3 -12
  16. package/dist/http/query.js +4 -1
  17. package/dist/migrate/builder/migrationBuilder.js +0 -4
  18. package/dist/migrate/ddl/index.d.ts +8 -0
  19. package/dist/migrate/ddl/index.js +11 -0
  20. package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
  21. package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
  22. package/dist/migrate/ddl/tableDdl.d.ts +34 -0
  23. package/dist/migrate/ddl/tableDdl.js +71 -0
  24. package/dist/migrate/introspection/mssqlIntrospector.d.ts +6 -9
  25. package/dist/migrate/introspection/mssqlIntrospector.js +40 -33
  26. package/dist/migrate/migrator.d.ts +3 -7
  27. package/dist/migrate/migrator.js +3 -7
  28. package/dist/migrate/schemaGenerator.d.ts +8 -23
  29. package/dist/migrate/schemaGenerator.js +29 -87
  30. package/dist/migrate/storage/databaseStorage.d.ts +2 -2
  31. package/dist/migrate/storage/databaseStorage.js +2 -2
  32. package/dist/mongo/mongoDialect.js +9 -14
  33. package/dist/mongo/mongodbQuerier.d.ts +3 -5
  34. package/dist/mongo/mongodbQuerier.js +18 -19
  35. package/dist/mssql/mssqlDialect.d.ts +14 -1
  36. package/dist/mssql/mssqlDialect.js +22 -7
  37. package/dist/querier/abstractQuerier.d.ts +17 -22
  38. package/dist/querier/abstractQuerier.js +92 -67
  39. package/dist/querier/abstractSqlQuerier.d.ts +9 -9
  40. package/dist/querier/abstractSqlQuerier.js +71 -87
  41. package/dist/schema/canonicalType.js +2 -2
  42. package/dist/schema/schemaASTBuilder.js +7 -7
  43. package/dist/sqlite/sqliteDialect.js +0 -2
  44. package/dist/type/dialect.d.ts +0 -8
  45. package/dist/type/entity.d.ts +6 -11
  46. package/dist/type/migration.d.ts +0 -3
  47. package/dist/type/query.d.ts +13 -13
  48. package/dist/type/query.js +0 -6
  49. package/dist/type/queryWhere.d.ts +7 -19
  50. package/dist/type/universalQuerier.d.ts +3 -3
  51. package/dist/type/vector.d.ts +3 -1
  52. package/dist/util/dialect.util.d.ts +12 -8
  53. package/dist/util/dialect.util.js +17 -29
  54. package/dist/util/field.util.d.ts +4 -16
  55. package/dist/util/field.util.js +6 -19
  56. package/dist/util/fieldOption.util.d.ts +1 -4
  57. package/dist/util/fieldOption.util.js +0 -2
  58. package/dist/util/logger.d.ts +3 -3
  59. package/dist/util/logger.js +3 -0
  60. package/dist/util/object.util.d.ts +2 -0
  61. package/dist/util/object.util.js +4 -0
  62. package/dist/util/raw.d.ts +3 -10
  63. package/dist/util/sql.util.js +2 -2
  64. package/package.json +2 -2
@@ -4,10 +4,11 @@ import { areTypesEqual, canonicalToSql, engineType, fieldOptionsToCanonical, isV
4
4
  import { indexSignature } from '../schema/indexDifferences.js';
5
5
  import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
6
6
  import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
7
- import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/index.js';
7
+ import { isAutoIncrement, qualifyName } from '../util/index.js';
8
8
  import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
9
9
  import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
10
- import { indexDdlFor } from './ddl/index.js';
10
+ import { indexDdlFor, tableDdlFor } from './ddl/index.js';
11
+ import { sizedType } from './ddl/tableDdl.js';
11
12
  import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, tableDefinitionToNode, } from './generator/definitionToNode.js';
12
13
  import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
13
14
  /**
@@ -19,10 +20,13 @@ export class SqlSchemaGenerator {
19
20
  defaultForeignKeyAction;
20
21
  /** `CREATE INDEX` for this dialect: the migrator's, so a runtime import carries none of it. */
21
22
  indexDdl;
23
+ /** The `ALTER TABLE` statements this dialect spells its own way, the migrator's for the same reason. */
24
+ tableDdl;
22
25
  constructor(dialect, defaultForeignKeyAction = 'NO ACTION') {
23
26
  this.dialect = dialect;
24
27
  this.defaultForeignKeyAction = defaultForeignKeyAction;
25
28
  this.indexDdl = indexDdlFor(dialect);
29
+ this.tableDdl = tableDdlFor(dialect);
26
30
  }
27
31
  get namingStrategy() {
28
32
  return this.dialect.namingStrategy;
@@ -46,9 +50,6 @@ export class SqlSchemaGenerator {
46
50
  escapeId(identifier) {
47
51
  return this.dialect.escapeId(identifier);
48
52
  }
49
- /**
50
- * Primary key type for auto-increment integer IDs
51
- */
52
53
  /**
53
54
  * How an auto-increment key of `type` is spelled: the type as any other column renders it, plus what
54
55
  * the engine appends to make it generated.
@@ -138,7 +139,6 @@ export class SqlSchemaGenerator {
138
139
  }
139
140
  generateAlterTable(diff) {
140
141
  const statements = [];
141
- const tableName = this.escapeId(diff.tableName);
142
142
  // Before the columns, because a key column being added cannot be part of the old key, and after
143
143
  // it is dropped the table is free to take the new one below.
144
144
  if (diff.primaryKey?.from.length) {
@@ -154,7 +154,7 @@ export class SqlSchemaGenerator {
154
154
  if (diff.columnsToAdd?.length) {
155
155
  for (const column of diff.columnsToAdd) {
156
156
  this.assertColumnAddable(diff.tableName, column);
157
- statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.generateColumnDefinitionFromSchema(column)};`);
157
+ statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
158
158
  statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
159
159
  }
160
160
  }
@@ -169,7 +169,7 @@ export class SqlSchemaGenerator {
169
169
  // Drop columns
170
170
  if (diff.columnsToDrop?.length) {
171
171
  for (const columnName of diff.columnsToDrop) {
172
- statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(columnName)};`);
172
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
173
173
  }
174
174
  }
175
175
  // Add indexes
@@ -206,7 +206,6 @@ export class SqlSchemaGenerator {
206
206
  }
207
207
  generateAlterTableDown(diff) {
208
208
  const statements = [];
209
- const tableName = this.escapeId(diff.tableName);
210
209
  // Constraints first, mirroring the up direction: the up added them last, so the down drops them
211
210
  // first, and a column it is about to drop is then free of anything naming it.
212
211
  statements.push(...this.dropForeignKeyStatements(diff.tableName, [
@@ -222,7 +221,7 @@ export class SqlSchemaGenerator {
222
221
  // Reverse column additions by dropping them
223
222
  if (diff.columnsToAdd?.length) {
224
223
  for (const column of diff.columnsToAdd) {
225
- statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(column.name)};`);
224
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
226
225
  }
227
226
  }
228
227
  // Reverse column alterations by restoring original schema
@@ -273,53 +272,35 @@ export class SqlSchemaGenerator {
273
272
  * {@link renderColumn}, which is the part that must not be written twice.
274
273
  */
275
274
  generateColumnDefinitionFromSchema(column) {
276
- let type = column.type;
277
- if (!type.includes('(')) {
278
- if (column.precision !== undefined) {
279
- type += column.scale === undefined ? `(${column.precision})` : `(${column.precision}, ${column.scale})`;
280
- }
281
- else if (column.length !== undefined) {
282
- type += `(${column.length})`;
283
- }
284
- }
285
- return this.renderColumn({ ...column, type });
275
+ return this.renderColumn({ ...column, type: sizedType(column) });
286
276
  }
287
277
  /**
288
- * The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
289
- * and the `ColumnNode` path below - because the clause order and the "no NOT NULL on a primary key"
290
- * rules are the same everywhere, and having them written twice is how the two paths drifted.
278
+ * The one place a column definition is spelled, so the `ColumnSchema` and `ColumnNode` paths cannot
279
+ * drift. A key column states `NOT NULL` rather than leave it to the key: SQLite lets a key column hold
280
+ * NULL otherwise, and SQL Server adds no key over a nullable column. `UNIQUE` is left to the key. An
281
+ * enum's `CHECK` comes last, the only place MariaDB takes it.
291
282
  */
292
283
  renderColumn(column) {
293
- let def = `${this.escapeId(column.name)} ${column.type}`;
294
- // Before the constraints, which every engine here accepts and is where each documents it. The
295
- // clause takes the place of a `DEFAULT`, which is mutually exclusive with it; everything else -
296
- // `NOT NULL`, `UNIQUE`, an enum `CHECK`, a comment - a generated column carries like any other.
297
- if (column.generatedAs) {
298
- def += ` GENERATED ALWAYS AS (${column.generatedAs}) STORED`;
299
- }
300
- if (!column.nullable && !column.isPrimaryKey) {
284
+ const type = column.generatedAs
285
+ ? this.tableDdl.storedGeneratedColumn(column.type, column.generatedAs)
286
+ : column.type;
287
+ let def = `${this.escapeId(column.name)} ${type}`;
288
+ if (!column.nullable) {
301
289
  def += ' NOT NULL';
302
290
  }
303
291
  if (column.isUnique && !column.isPrimaryKey) {
304
292
  def += ' UNIQUE';
305
293
  }
294
+ def += this.tableDdl.defaultClause(column);
295
+ if (column.comment) {
296
+ def += this.generateColumnComment(column.comment);
297
+ }
306
298
  if (column.enum?.length) {
307
299
  const values = column.enum.map((value) => this.dialect.escape(value)).join(', ');
308
300
  def += ` CHECK (${this.escapeId(column.name)} IN (${values}))`;
309
301
  }
310
- def += this.defaultClause(column);
311
- if (column.comment) {
312
- def += this.generateColumnComment(column.comment);
313
- }
314
302
  return def;
315
303
  }
316
- /** ` DEFAULT <sql>`, or nothing where the column declares none. Empty rather than `DEFAULT NULL`
317
- * so an absent default stays absent - `defaultValue: null` is the way to ask for one. */
318
- defaultClause(column) {
319
- return column.defaultValue === undefined
320
- ? ''
321
- : ` DEFAULT ${formatDefaultValue(column.defaultValue, this.dialect, column.type)}`;
322
- }
323
304
  getSqlType(field) {
324
305
  // A foreign key takes the type of the key it points at. A `referencedKey` the target does not
325
306
  // have falls through to this column's own options, as the AST builder does with the same case.
@@ -338,35 +319,9 @@ export class SqlSchemaGenerator {
338
319
  }
339
320
  return this.canonicalTypeToSql(canonical);
340
321
  }
341
- /**
342
- * Generate ALTER COLUMN statements (database-specific)
343
- */
322
+ /** The statements that alter `column` in place, as this dialect spells them. */
344
323
  generateAlterColumnStatements(tableName, column, newDefinition) {
345
- const table = this.escapeId(tableName);
346
- const colName = this.escapeId(column.name);
347
- if (this.dialect.alterColumnSyntax === 'none') {
348
- throw new TypeError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
349
- `This database does not support ALTER COLUMN.`);
350
- }
351
- if (this.dialect.alterColumnStrategy === 'separate-clauses') {
352
- const statements = [];
353
- // Separate ALTER COLUMN clauses for different changes (Postgres)
354
- statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} TYPE ${column.type};`);
355
- if (column.nullable) {
356
- statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} DROP NOT NULL;`);
357
- }
358
- else {
359
- statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} SET NOT NULL;`);
360
- }
361
- if (column.defaultValue !== undefined) {
362
- statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} SET${this.defaultClause(column)};`);
363
- }
364
- else {
365
- statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} DROP DEFAULT;`);
366
- }
367
- return statements;
368
- }
369
- return [`ALTER TABLE ${table} ${this.dialect.alterColumnSyntax} ${newDefinition};`];
324
+ return this.tableDdl.alterColumn(tableName, column, newDefinition);
370
325
  }
371
326
  /** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
372
327
  generateColumnComment(comment) {
@@ -403,9 +358,6 @@ export class SqlSchemaGenerator {
403
358
  const tableRef = this.dialect.escapeQualifiedId(tableName, schema);
404
359
  return [`COMMENT ON COLUMN ${tableRef}.${this.escapeId(column.name)} IS ${this.dialect.escape(column.comment)};`];
405
360
  }
406
- /**
407
- * Compare an entity with a database table node and return the differences.
408
- */
409
361
  /**
410
362
  * How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
411
363
  *
@@ -489,13 +441,6 @@ export class SqlSchemaGenerator {
489
441
  foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
490
442
  };
491
443
  }
492
- /**
493
- * What the shared differ needs from a dialect: a type as this engine would actually store it.
494
- *
495
- * `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
496
- * paper can be one column in the database. Round-tripping through the engine's own spelling is what
497
- * stops every such column reporting an alteration on every sync.
498
- */
499
444
  /**
500
445
  * Indexes the entity declares that the table does not already have, in any shape.
501
446
  *
@@ -627,10 +572,7 @@ export class SqlSchemaGenerator {
627
572
  return this.generateCreateTableFromNode(tableNode, options);
628
573
  }
629
574
  generateRenameTableSql(oldName, newName) {
630
- if (this.dialect.renameTableSyntax === 'rename-table') {
631
- return `RENAME TABLE ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
632
- }
633
- return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
575
+ return this.tableDdl.renameTable(oldName, newName);
634
576
  }
635
577
  /**
636
578
  * `ADD COLUMN`, plus the constraint and index the column declares.
@@ -643,7 +585,7 @@ export class SqlSchemaGenerator {
643
585
  generateAddColumnSql(tableName, column) {
644
586
  this.assertColumnAddable(tableName, column);
645
587
  const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
646
- const statements = [`ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`];
588
+ const statements = [this.tableDdl.addColumn(tableName, colSql)];
647
589
  const foreignKey = columnForeignKey(column);
648
590
  if (foreignKey) {
649
591
  statements.push(...this.addForeignKeyStatements(tableName, [foreignKey]));
@@ -660,10 +602,10 @@ export class SqlSchemaGenerator {
660
602
  return this.generateAlterColumnStatements(tableName, { ...this.columnNodeToSchema(node), name: columnName }, this.generateColumnFromNode(node)).join('\n');
661
603
  }
662
604
  generateDropColumnSql(tableName, columnName) {
663
- return `ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)};`;
605
+ return this.tableDdl.dropColumn(tableName, columnName).join('\n');
664
606
  }
665
607
  generateRenameColumnSql(tableName, oldName, newName) {
666
- return `ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
608
+ return this.tableDdl.renameColumn(tableName, oldName, newName);
667
609
  }
668
610
  /**
669
611
  * `CONSTRAINT <name> FOREIGN KEY (...) REFERENCES ... ON DELETE ... ON UPDATE ...`.
@@ -1,10 +1,10 @@
1
1
  import type { MigrationStorage, QuerierPool, SqlQuerier } from '../../type/index.js';
2
+ /** Where executed migrations are recorded when the config does not name a table. */
3
+ export declare const DEFAULT_MIGRATIONS_TABLE = "uql_migrations";
2
4
  /**
3
5
  * Stores migration state in a database table.
4
6
  * Uses the querier's dialect for escaping and placeholders.
5
7
  */
6
- /** Where executed migrations are recorded when the config does not name a table. */
7
- export declare const DEFAULT_MIGRATIONS_TABLE = "uql_migrations";
8
8
  export declare class DatabaseMigrationStorage implements MigrationStorage {
9
9
  private readonly pool;
10
10
  private readonly tableName;
@@ -1,10 +1,10 @@
1
1
  import { withSqlQuerierForMigrations } from '../acquireQuerierForMigrations.js';
2
+ /** Where executed migrations are recorded when the config does not name a table. */
3
+ export const DEFAULT_MIGRATIONS_TABLE = 'uql_migrations';
2
4
  /**
3
5
  * Stores migration state in a database table.
4
6
  * Uses the querier's dialect for escaping and placeholders.
5
7
  */
6
- /** Where executed migrations are recorded when the config does not name a table. */
7
- export const DEFAULT_MIGRATIONS_TABLE = 'uql_migrations';
8
8
  export class DatabaseMigrationStorage {
9
9
  pool;
10
10
  tableName;
@@ -4,7 +4,7 @@ import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '..
4
4
  import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
5
5
  import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
6
6
  import { QueryRaw } from '../type/queryRaw.js';
7
- import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, columnFamily, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
7
+ import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
8
8
  /** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
9
9
  export const mongoDialectFeatures = {
10
10
  explicitJsonCast: false,
@@ -14,7 +14,6 @@ export const mongoDialectFeatures = {
14
14
  indexIfNotExists: false,
15
15
  schemas: false, // the connection picks the database, and a collection name takes no dot
16
16
  dropTableCascade: false,
17
- renameColumn: false,
18
17
  foreignKeyAlter: false,
19
18
  primaryKeyAlter: false,
20
19
  generatedColumnAdd: false,
@@ -24,7 +23,6 @@ export const mongoDialectFeatures = {
24
23
  supportsTimestamptz: false,
25
24
  stringSizing: 'bounded-text',
26
25
  supportsUnsigned: false,
27
- multipleCascadePaths: true,
28
26
  serverSideCursors: false,
29
27
  };
30
28
  /** What `toWireId` converts: the hex spelling of an `ObjectId`, and nothing looser. */
@@ -70,7 +68,7 @@ export class MongoDialect extends AbstractDialect {
70
68
  where(entity, where = {}, opts = {}) {
71
69
  const meta = getMeta(entity);
72
70
  // Filters are applied once, here at the scope entry point; recursion uses `renderFilter`.
73
- return this.renderFilter(entity, this.scopedWhereMap(meta, where, opts));
71
+ return this.renderFilter(entity, this.scopedWhere(meta, where, opts));
74
72
  }
75
73
  /**
76
74
  * A `$where` that may constrain relations, split into the `$lookup` stages it needs and the `$match`
@@ -82,7 +80,7 @@ export class MongoDialect extends AbstractDialect {
82
80
  whereWithRelations(entity, where = {}, opts = {}) {
83
81
  const meta = getMeta(entity);
84
82
  const lookups = { stages: [], temps: [] };
85
- const filter = this.renderFilter(entity, this.scopedWhereMap(meta, where, opts), opts, lookups);
83
+ const filter = this.renderFilter(entity, this.scopedWhere(meta, where, opts), opts, lookups);
86
84
  return { stages: lookups.stages, filter, unset: lookups.temps };
87
85
  }
88
86
  /** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
@@ -91,9 +89,9 @@ export class MongoDialect extends AbstractDialect {
91
89
  return false;
92
90
  }
93
91
  const meta = getMeta(entity);
94
- const whereMap = buildQueryWhereAsMap(meta, where);
92
+ const whereMap = where;
95
93
  return someKey(whereMap, (key) => MongoDialect.isGroupOp(key)
96
- ? (whereMap[key] ?? []).some((it) => this.constrainsRelations(entity, it))
94
+ ? (whereMap[key] ?? []).some((it) => !(it instanceof QueryRaw) && this.constrainsRelations(entity, it))
97
95
  : Boolean(meta.relations[key]));
98
96
  }
99
97
  /**
@@ -103,9 +101,8 @@ export class MongoDialect extends AbstractDialect {
103
101
  */
104
102
  renderFilter(entity, where = {}, opts, lookups) {
105
103
  const meta = getMeta(entity);
106
- const whereMap = buildQueryWhereAsMap(meta, where);
107
104
  const filter = {};
108
- for (const [rawKey, rawVal] of Object.entries(whereMap)) {
105
+ for (const [rawKey, rawVal] of Object.entries(where)) {
109
106
  let key = rawKey;
110
107
  let val = rawVal;
111
108
  if (MongoDialect.isGroupOp(key)) {
@@ -155,8 +152,6 @@ export class MongoDialect extends AbstractDialect {
155
152
  const { join, negate } = MongoDialect.GROUP_OPS[key];
156
153
  const parts = MongoDialect.groupClauses(key, val)
157
154
  .map((filterIt) => {
158
- // A `QueryRaw` here would recurse forever: `buildQueryWhereAsMap` re-wraps it as
159
- // `{ $and: [raw] }`, which lands back on this branch.
160
155
  this.assertNoRaw(filterIt);
161
156
  return this.renderFilter(entity, filterIt, opts, lookups);
162
157
  })
@@ -190,7 +185,7 @@ export class MongoDialect extends AbstractDialect {
190
185
  // `withDeleted()` or `hardDelete` on the parent must not un-hide trashed rows of the target, the
191
186
  // same rule the SQL dialects' relation subqueries follow.
192
187
  const targetCondition = (sizeVal === undefined ? val : {});
193
- const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, targetCondition), opts);
188
+ const targetScope = this.renderFilter(relEntity, this.scopedWhere(relMeta, targetCondition), opts);
194
189
  lookups.temps.push(temp);
195
190
  lookups.stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts));
196
191
  return sizeVal === undefined
@@ -221,7 +216,7 @@ export class MongoDialect extends AbstractDialect {
221
216
  junctionLookup(meta, relOpts, relMeta, targetScope, temp, tail, opts) {
222
217
  const throughEntity = relOpts.through();
223
218
  const throughMeta = getMeta(throughEntity);
224
- const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
219
+ const junctionScope = this.renderFilter(throughEntity, this.scopedWhere(throughMeta, {}), opts);
225
220
  const nested = REL_NESTED_KEY;
226
221
  // Both ends are one column here - each `$lookup` matches one field against `_id` - so both sides
227
222
  // must be sole-keyed. Sliced rather than indexed positionally: `references[1]` is the parent's
@@ -493,7 +488,7 @@ export class MongoDialect extends AbstractDialect {
493
488
  const relEntity = relOpts.entity();
494
489
  const relMeta = getMeta(relEntity);
495
490
  const temp = sortCountField(key);
496
- const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, {}), opts);
491
+ const targetScope = this.renderFilter(relEntity, this.scopedWhere(relMeta, {}), opts);
497
492
  const tail = [{ $count: COUNT_ALIAS }];
498
493
  stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts), {
499
494
  $addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] } },
@@ -1,6 +1,6 @@
1
1
  import type { Document, MongoClient } from 'mongodb';
2
2
  import { AbstractQuerier } from '../querier/index.js';
3
- import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
+ import type { EntityData, ExtraOptions, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
4
4
  import { type ParentPartition } from '../util/index.js';
5
5
  import type { MongoDialect } from './mongoDialect.js';
6
6
  export declare class MongodbQuerier extends AbstractQuerier {
@@ -56,7 +56,7 @@ export declare class MongodbQuerier extends AbstractQuerier {
56
56
  * the floor, and skipped the relation-sort rejection {@link MongoDialect.sort} raises.
57
57
  */
58
58
  private settleIds;
59
- internalInsertMany<E extends Document>(entity: Type<E>, payloads: EntityData<E>[]): Promise<IdValue<E>[]>;
59
+ internalInsertMany<E extends Document>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
60
60
  internalUpdateMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
61
61
  /**
62
62
  * `_id` is immutable, so a key the payload names can only be written on the insert branch of an
@@ -65,18 +65,16 @@ export declare class MongodbQuerier extends AbstractQuerier {
65
65
  private upsertUpdate;
66
66
  private buildConflictFilter;
67
67
  protected internalUpsertOne<E extends Document>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<{
68
- firstId: PrimaryKey | undefined;
68
+ ids: (PrimaryKey | undefined)[];
69
69
  changes: number;
70
70
  created: boolean;
71
71
  }>;
72
72
  protected internalUpsertMany<E extends Document>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<{
73
73
  changes: number;
74
74
  ids?: undefined;
75
- firstId?: undefined;
76
75
  } | {
77
76
  changes: number;
78
77
  ids: (PrimaryKey | undefined)[];
79
- firstId: PrimaryKey | undefined;
80
78
  }>;
81
79
  protected internalDeleteMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
82
80
  get hasOpenTransaction(): boolean;
@@ -1,6 +1,6 @@
1
1
  import { COUNT_ALIAS } from '../dialect/aliases.js';
2
2
  import { hasRequiredJoin } from '../dialect/queryJoins.js';
3
- import { getMeta, idOf, soleIdOf } from '../entity/index.js';
3
+ import { getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
4
4
  import { AbstractQuerier, enrichError } from '../querier/index.js';
5
5
  import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, queryChildrenOf, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
6
6
  /**
@@ -259,22 +259,17 @@ export class MongodbQuerier extends AbstractQuerier {
259
259
  // are named the same way every other driver names them.
260
260
  return (this.dialect.normalizeIds(meta, founds) || []).map((found) => idOf(meta, found));
261
261
  }
262
- async internalInsertMany(entity, payloads) {
262
+ async internalInsertMany(entity, rows) {
263
263
  return this.timed('internalInsertMany', undefined, async () => {
264
- if (!payloads?.length) {
265
- return [];
266
- }
267
- payloads = clone(payloads);
268
264
  const meta = getMeta(entity);
269
- const persistables = this.dialect.getPersistables(meta, payloads, 'onInsert');
265
+ const persistables = this.dialect.getPersistables(meta, rows, 'onInsert');
270
266
  const { insertedIds } = await this.execute((session) => this.collection(entity).insertMany(persistables, { session }));
271
267
  const ids = Object.values(insertedIds).map((id) => this.dialect.fromWireId(id));
272
268
  const idKey = soleIdOf(meta, 'insert');
273
- for (const [index, it] of payloads.entries()) {
274
- it[idKey] = ids[index];
269
+ for (let index = 0; index < rows.length; index++) {
270
+ rows[index][idKey] = ids[index];
275
271
  }
276
- await this.insertRelations(entity, payloads);
277
- return ids;
272
+ await this.insertRelations(entity, rows);
278
273
  });
279
274
  }
280
275
  async internalUpdateMany(entity, qm, payload, opts) {
@@ -332,10 +327,11 @@ export class MongodbQuerier extends AbstractQuerier {
332
327
  includeResultMetadata: true,
333
328
  session,
334
329
  }));
335
- const firstId = this.dialect.fromWireId(res?.value?._id);
330
+ // Read off the document as written, which carries its `_id` on either branch.
331
+ const id = this.dialect.fromWireId(res?.value?._id);
336
332
  // `updatedExisting` is false when a new document was inserted (upserted).
337
333
  const created = res?.lastErrorObject?.['updatedExisting'] === false;
338
- return { firstId, changes: firstId ? 1 : 0, created };
334
+ return { ids: [id], changes: id === undefined ? 0 : 1, created };
339
335
  });
340
336
  }
341
337
  async internalUpsertMany(entity, conflictPaths, payload) {
@@ -343,8 +339,10 @@ export class MongodbQuerier extends AbstractQuerier {
343
339
  if (!payload?.length) {
344
340
  return { changes: 0 };
345
341
  }
346
- payload = clone(payload);
347
342
  const meta = getMeta(entity);
343
+ // Asked before `getPersistable` fills an `onInsert` key into rows it may only update.
344
+ const unnamed = payload.map((row) => !namesKey(meta, row));
345
+ payload = clone(payload);
348
346
  const operations = payload.map((item) => {
349
347
  const persistable = this.dialect.getPersistable(meta, item, 'onInsert');
350
348
  const filter = this.buildConflictFilter(entity, conflictPaths, item);
@@ -359,14 +357,15 @@ export class MongodbQuerier extends AbstractQuerier {
359
357
  });
360
358
  const res = await this.execute((session) => this.collection(entity).bulkWrite(operations, { session }));
361
359
  const changes = (res.upsertedCount ?? 0) + (res.modifiedCount ?? 0);
362
- // `upsertedIds` only covers newly-inserted documents, keyed by operation index: a matched and
363
- // updated document's `_id` is not in the response. Read by that index rather than flattened,
364
- // so each id lands on the row it belongs to and the gaps stay gaps.
365
- const ids = payload.map((_, index) => {
360
+ // `upsertedIds` names only the documents inserted, keyed by operation index, so each lands on
361
+ // its own row; an updated document's `_id` is read back by the conflict fields instead.
362
+ const reported = payload.map((_, index) => {
366
363
  const id = res.upsertedIds[index];
367
364
  return id === undefined ? undefined : this.dialect.fromWireId(id);
368
365
  });
369
- return { changes, ids, firstId: ids.find((id) => id !== undefined) };
366
+ const unplaced = reported.some((id, index) => id === undefined && unnamed[index]);
367
+ const found = unplaced ? await this.idsByConflict(entity, conflictPaths, payload) : [];
368
+ return { changes, ids: reported.map((id, index) => id ?? found[index]) };
370
369
  });
371
370
  }
372
371
  async internalDeleteMany(entity, qm, opts = {}) {
@@ -1,5 +1,5 @@
1
1
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
2
- import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type } from '../type/index.js';
2
+ import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
3
  /**
4
4
  * Microsoft SQL Server 2017 and up - the floor `STRING_AGG` sets, every other construct here being
5
5
  * 2016 or older.
@@ -30,6 +30,8 @@ export declare class MsSqlDialect extends MergeSqlDialect {
30
30
  readonly maxBindValues = 2100;
31
31
  /** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
32
32
  readonly returningPosition = "after-target";
33
+ /** Microsoft documents no row order for a `MERGE ... OUTPUT`. */
34
+ readonly upsertReturningOrdered = false;
33
35
  readonly insertIdSource: InsertIdSource;
34
36
  /** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
35
37
  protected readonly mergeTargetHint = " WITH (HOLDLOCK)";
@@ -89,6 +91,17 @@ export declare class MsSqlDialect extends MergeSqlDialect {
89
91
  * `uuidv7()` is emitted on. It is a predicate rather than a value, so it stands alone.
90
92
  */
91
93
  protected regexCondition(operand: string, placeholder: string): string;
94
+ /**
95
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
96
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
97
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
98
+ */
99
+ readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
100
+ /**
101
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
102
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
103
+ */
104
+ protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
92
105
  /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
93
106
  createSchemaSql(schema: string): string;
94
107
  /** The estimate the engine already keeps per partition, live without a stats refresh. */
@@ -27,22 +27,16 @@ export class MsSqlDialect extends MergeSqlDialect {
27
27
  indexIfNotExists: false,
28
28
  schemas: true,
29
29
  dropTableCascade: false,
30
- // `sp_rename` is a stored procedure, not DDL, so a rename is refused by name rather than emitted
31
- // as an `ALTER TABLE` the parser rejects.
32
- renameColumn: false,
33
30
  foreignKeyAlter: true,
34
31
  primaryKeyAlter: true,
35
32
  generatedColumnAdd: true,
36
33
  // Extended properties are out-of-band metadata with their own procedures, not comments.
37
34
  commentSyntax: 'none',
38
35
  vectorIndexRequiresNotNull: false,
39
- vectorSupportsLength: false,
36
+ vectorSupportsLength: true,
40
37
  supportsTimestamptz: false,
41
38
  stringSizing: 'varchar',
42
39
  supportsUnsigned: false,
43
- // Error 1785: the constraint is refused at create time, so any diamond-shaped schema would fail
44
- // to build at all rather than misbehave on write.
45
- multipleCascadePaths: false,
46
40
  serverSideCursors: false,
47
41
  };
48
42
  dialectName = 'mssql';
@@ -70,6 +64,8 @@ export class MsSqlDialect extends MergeSqlDialect {
70
64
  maxBindValues = 2100;
71
65
  /** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
72
66
  returningPosition = 'after-target';
67
+ /** Microsoft documents no row order for a `MERGE ... OUTPUT`. */
68
+ upsertReturningOrdered = false;
73
69
  insertIdSource = 'returning';
74
70
  /** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
75
71
  mergeTargetHint = ' WITH (HOLDLOCK)';
@@ -188,6 +184,25 @@ export class MsSqlDialect extends MergeSqlDialect {
188
184
  regexCondition(operand, placeholder) {
189
185
  return `REGEXP_LIKE(${operand}, ${placeholder})`;
190
186
  }
187
+ /**
188
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
189
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
190
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
191
+ */
192
+ vectorMetrics = new Map([
193
+ ['cosine', { fn: 'VECTOR_DISTANCE', metricArg: 'cosine' }],
194
+ ['l2', { fn: 'VECTOR_DISTANCE', metricArg: 'euclidean' }],
195
+ ['inner', { fn: 'VECTOR_DISTANCE', metricArg: 'dot' }],
196
+ ]);
197
+ /**
198
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
199
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
200
+ */
201
+ appendVectorValue(ctx, value) {
202
+ ctx.append('CAST(');
203
+ super.appendVectorValue(ctx, value);
204
+ ctx.append(` AS VECTOR(${value.length}))`);
205
+ }
191
206
  /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
192
207
  createSchemaSql(schema) {
193
208
  const literal = escapeSingleQuotes(schema);