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
@@ -1,7 +1,7 @@
1
1
  import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
2
2
  import { decodeColumn } from '../dialect/hydrateColumn.js';
3
- import { getMeta, idOf, soleIdOf } from '../entity/index.js';
4
- import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
3
+ import { getMeta, idOf, namesKey } from '../entity/index.js';
4
+ import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { AbstractQuerier } from './abstractQuerier.js';
6
6
  import { enrichError } from './queryError.js';
7
7
  /**
@@ -22,50 +22,36 @@ function partitionBySuppliedId(payload, idKey) {
22
22
  return supplied.length && generated.length ? [supplied, generated] : [supplied.length ? supplied : generated];
23
23
  }
24
24
  /**
25
- * Rows grouped by the columns they carry, payload order kept within each group - or `undefined` when
26
- * they all carry the same ones, which is the batch as it stands and needs no grouping at all.
25
+ * Row indexes grouped by the columns their rows carry, payload order kept within each group.
27
26
  *
28
27
  * Deliberately finer than {@link partitionBySuppliedId}: an upsert's `DO UPDATE SET` is one
29
28
  * assignment list for the whole statement, so rows of different shapes cannot share one at all.
30
29
  */
31
30
  function groupByInsertShape(meta, payload) {
32
- const first = insertShapeOf(meta, payload[0]);
33
- let index = 1;
34
- while (index < payload.length && insertShapeOf(meta, payload[index]) === first) {
35
- index++;
36
- }
37
- if (index === payload.length) {
38
- return undefined;
39
- }
40
- const groups = new Map([[first, payload.slice(0, index)]]);
41
- for (; index < payload.length; index++) {
42
- const row = payload[index];
43
- const shape = insertShapeOf(meta, row);
31
+ const groups = new Map();
32
+ for (let index = 0; index < payload.length; index++) {
33
+ const shape = insertShapeOf(meta, payload[index]);
44
34
  const group = groups.get(shape);
45
35
  if (group) {
46
- group.push(row);
36
+ group.push(index);
47
37
  }
48
38
  else {
49
- groups.set(shape, [row]);
39
+ groups.set(shape, [index]);
50
40
  }
51
41
  }
52
42
  return [...groups.values()];
53
43
  }
54
44
  /**
55
- * How many rows one statement can carry within the dialect's bind budget. `DEFAULT` cells bind no
56
- * parameter, so fields-per-record is a safe upper bound. Every multi-row write splits on this: D1
57
- * allows 100 binds, which a couple of dozen rows reach.
45
+ * A group's row indexes split into statements within the dialect's bind budget, payload order kept.
46
+ * `DEFAULT` cells bind no parameter, so fields-per-record is a safe upper bound. Every multi-row
47
+ * write splits on this: D1 allows 100 binds, which a couple of dozen rows reach.
58
48
  */
59
- function bindBudgetChunkSize(meta, rows, maxBindValues) {
60
- const fieldsPerRecord = getInsertFieldKeys(meta, rows).length || 1;
61
- return Math.max(1, Math.floor(maxBindValues / fieldsPerRecord));
62
- }
63
- /** `rows` split into statement-sized slices, payload order kept. */
64
- function chunkByBindBudget(meta, rows, maxBindValues) {
65
- const size = bindBudgetChunkSize(meta, rows, maxBindValues);
49
+ function chunkByBindBudget(meta, payload, group, maxBindValues) {
50
+ const fieldsPerRecord = getInsertFieldKeys(meta, group.map((index) => payload[index])).length;
51
+ const size = Math.max(1, Math.floor(maxBindValues / (fieldsPerRecord || 1)));
66
52
  const chunks = [];
67
- for (let start = 0; start < rows.length; start += size) {
68
- chunks.push(rows.slice(start, start + size));
53
+ for (let start = 0; start < group.length; start += size) {
54
+ chunks.push(group.slice(start, start + size));
69
55
  }
70
56
  return chunks;
71
57
  }
@@ -186,17 +172,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
186
172
  this.dialect.findPerParent(ctx, entity, q, partition);
187
173
  return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
188
174
  }
189
- /**
190
- * One statement for both: the page carries its own unpaged total in an extra column. An empty page
191
- * has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
192
- * the end, or a filter nothing matched.
193
- *
194
- * A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
195
- * is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
196
- * engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
197
- * answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
198
- * too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
199
- */
200
175
  /**
201
176
  * How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
202
177
  * `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
@@ -212,6 +187,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
212
187
  }
213
188
  return undefined;
214
189
  }
190
+ /**
191
+ * One statement for both: the page carries its own unpaged total in an extra column. An empty page
192
+ * has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
193
+ * the end, or a filter nothing matched.
194
+ *
195
+ * A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
196
+ * is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
197
+ * engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
198
+ * answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
199
+ * too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
200
+ */
215
201
  async internalFindManyAndCount(entity, q, opts) {
216
202
  const separately = this.countedSeparately(entity, q, opts);
217
203
  if (separately) {
@@ -359,24 +345,15 @@ export class AbstractSqlQuerier extends AbstractQuerier {
359
345
  }
360
346
  return res;
361
347
  }
362
- async internalInsertMany(entity, payload) {
363
- if (!payload?.length) {
364
- return [];
365
- }
366
- payload = clone(payload);
348
+ async internalInsertMany(entity, rows) {
367
349
  const meta = getMeta(entity);
368
- // What comes back is one column's value, so a composite reports nothing here and `insertMany`
369
- // names those rows from the payload instead. `sole` is what keeps every id path off a key that
370
- // is several columns.
350
+ // What comes back is one column's value, so nothing is read back for a composite: its rows
351
+ // already carry every column of it. `sole` is what keeps every id path off such a key.
371
352
  const [idKey] = meta.ids;
372
353
  const sole = meta.ids.length === 1;
373
354
  const idField = sole ? meta.fields[idKey] : undefined;
374
355
  const generatedKey = !!idField && isAutoIncrement(idField, true);
375
- const payloadIds = new Array(payload.length);
376
- for (const group of partitionBySuppliedId(payload, idKey)) {
377
- // Per group, not per batch: the two carry different columns - one names the key, one does not -
378
- // so a budget taken over their union would under-fill the statement that is missing one.
379
- const chunkSize = bindBudgetChunkSize(meta, group.map((index) => payload[index]), this.dialect.maxBindValues);
356
+ for (const group of partitionBySuppliedId(rows, idKey)) {
380
357
  // RETURNING-based ids are exact per row. Header-derived ones (LAST_INSERT_ID / lastInsertRowid
381
358
  // arithmetic) are only sound when the key is database-generated and every row *in this
382
359
  // statement* left it to the database. That is a property of the statement, not of the batch:
@@ -385,28 +362,26 @@ export class AbstractSqlQuerier extends AbstractQuerier {
385
362
  // most one extra statement and keeps each of them inferable.
386
363
  const idsReliable = sole &&
387
364
  (this.dialect.insertIdSource === 'returning' ||
388
- (generatedKey && group.every((index) => payload[index][idKey] === undefined)));
365
+ (generatedKey && group.every((index) => rows[index][idKey] === undefined)));
389
366
  // Inferring multiple ids from the single header id (MySQL) assumes a known stride; a clustered
390
367
  // server may set `auto_increment_increment` > 1, so probe it (once, cached) before inferring.
391
368
  if (idsReliable && group.length > 1 && this.dialect.insertIdSource === 'firstId') {
392
369
  this.#insertIdIncrement ??= await this.loadInsertIdIncrement();
393
370
  }
394
- for (let start = 0; start < group.length; start += chunkSize) {
395
- const indexes = group.slice(start, start + chunkSize);
371
+ // Per group, not per batch: the two carry different columns - one names the key, one does not -
372
+ // so a budget taken over their union would under-fill the statement that is missing one.
373
+ for (const indexes of chunkByBindBudget(meta, rows, group, this.dialect.maxBindValues)) {
396
374
  const ctx = this.dialect.createContext();
397
- this.dialect.insert(ctx, entity, indexes.map((index) => payload[index]));
375
+ this.dialect.insert(ctx, entity, indexes.map((index) => rows[index]));
398
376
  const { ids = [] } = await this.run(ctx.sql, ctx.values);
399
- indexes.forEach((index, position) => {
400
- const it = payload[index];
401
- if (idsReliable) {
402
- it[idKey] ??= ids[position];
377
+ if (idsReliable) {
378
+ for (let position = 0; position < indexes.length; position++) {
379
+ rows[indexes[position]][idKey] ??= ids[position];
403
380
  }
404
- payloadIds[index] = sole ? it[idKey] : undefined;
405
- });
381
+ }
406
382
  }
407
383
  }
408
- await this.insertRelations(entity, payload);
409
- return payloadIds;
384
+ await this.insertRelations(entity, rows);
410
385
  }
411
386
  async internalUpdateMany(entity, q, payload, opts) {
412
387
  payload = clone(payload);
@@ -418,7 +393,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
418
393
  if (!ids.length) {
419
394
  return 0;
420
395
  }
421
- target = { $where: ids };
396
+ target = { $where: whereIds(getMeta(entity), ids) };
422
397
  }
423
398
  const ctx = this.dialect.createContext();
424
399
  this.dialect.update(ctx, entity, target, payload, opts);
@@ -446,8 +421,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
446
421
  // One statement per shape, each split again to stay inside the bind budget. Grouping first is
447
422
  // what makes the budget arithmetic right: every row of a group carries the same columns, so the
448
423
  // `DO UPDATE SET` resolves to non-binding `EXCLUDED` references rather than inlined values.
449
- const groups = groupByInsertShape(meta, payload);
450
- const statements = (groups ?? [payload]).flatMap((group) => chunkByBindBudget(meta, group, this.dialect.maxBindValues));
424
+ const statements = groupByInsertShape(meta, payload).flatMap((group) => chunkByBindBudget(meta, payload, group, this.dialect.maxBindValues));
451
425
  if (statements.length === 1) {
452
426
  return this.runUpsert(entity, conflictPaths, payload);
453
427
  }
@@ -459,31 +433,41 @@ export class AbstractSqlQuerier extends AbstractQuerier {
459
433
  // than one statement; `transaction` is re-entrant, so this is free inside a caller's own.
460
434
  return this.transaction(async () => {
461
435
  let changes = 0;
462
- const ids = [];
463
- for (const statement of statements) {
464
- const result = await this.runUpsert(entity, conflictPaths, statement);
465
- changes += result.changes ?? 0;
466
- if (result.ids) {
467
- ids.push(...result.ids);
436
+ // Placed by index, since grouping by shape reorders the rows. A statement reporting fewer ids
437
+ // than it wrote places none.
438
+ const ids = new Array(payload.length);
439
+ for (const indexes of statements) {
440
+ const { changes: written = 0, ids: reported } = await this.runUpsert(entity, conflictPaths, indexes.map((index) => payload[index]));
441
+ changes += written;
442
+ if (reported?.length === indexes.length) {
443
+ for (let position = 0; position < indexes.length; position++) {
444
+ ids[indexes[position]] = reported[position];
445
+ }
468
446
  }
469
447
  }
470
- // No `created`/`firstId`: both speak for a single statement, and there were several.
471
- return ids.length ? { changes, ids } : { changes };
448
+ // No `created`: it speaks for a single statement, and there were several.
449
+ return { changes, ids };
472
450
  });
473
451
  }
474
452
  async runUpsert(entity, conflictPaths, payload) {
453
+ const meta = getMeta(entity);
454
+ // Asked first: the statement fills an `onInsert` key into these rows whether it inserts them or not.
455
+ const unnamed = meta.ids.length === 1 && payload.some((row) => !namesKey(meta, row));
475
456
  const ctx = this.dialect.createContext();
476
457
  this.dialect.upsert(ctx, entity, conflictPaths, payload);
477
458
  const result = await this.run(ctx.sql, ctx.values);
478
- // On a `firstId` dialect (MySQL: no `RETURNING`), a multi-row upsert's `affectedRows` is a
479
- // per-row weighted sum (1=insert, 2=update, 0=no-op), not a row count, so `buildUpdateResult`'s
480
- // header-derived `ids`/`firstId`/`created` can't be trusted the moment more than one row is
481
- // involved (verified: a 1-insert-1-update batch reports `changes: 3`, which would otherwise
482
- // infer 3 sequential ids for only 2 real rows). A single-row batch (`upsertOne`) is unambiguous.
483
- if (this.dialect.insertIdSource !== 'returning' && payload.length > 1) {
484
- return { changes: result.changes };
485
- }
486
- return result;
459
+ const ordered = payload.length === 1 || (this.dialect.insertIdSource === 'returning' && this.dialect.upsertReturningOrdered);
460
+ if (ordered && result.ids?.length === payload.length) {
461
+ return result;
462
+ }
463
+ // The statement's ids name its rows only in order and for every one. A MySQL batch reports a
464
+ // weighted count (1=insert, 2=update) instead, CockroachDB and SQL Server answer out of order, and
465
+ // `DO NOTHING` skips rows, so there the ids are read back by the conflict columns.
466
+ const { changes } = result;
467
+ const created = payload.length === 1 ? result.created : undefined;
468
+ return unnamed
469
+ ? { changes, created, ids: await this.idsByConflict(entity, conflictPaths, payload) }
470
+ : { changes, created };
487
471
  }
488
472
  async internalDeleteMany(entity, q, opts) {
489
473
  const meta = getMeta(entity);
@@ -507,7 +491,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
507
491
  // outright by any schema that declares the constraint without `ON DELETE CASCADE`.
508
492
  await this.deleteRelations(entity, ids, opts);
509
493
  const deleteCtx = this.dialect.createContext();
510
- this.dialect.delete(deleteCtx, entity, { $where: ids }, opts);
494
+ this.dialect.delete(deleteCtx, entity, { $where: whereIds(meta, ids) }, opts);
511
495
  const { changes = 0 } = await this.run(deleteCtx.sql, deleteCtx.values);
512
496
  return changes;
513
497
  }
@@ -213,8 +213,8 @@ const ENGINE_TYPES = {
213
213
  },
214
214
  // SQLite uses affinity, so no size variants.
215
215
  sqlite: { scalars: withVectorType(SQLITE_SCALAR_MAP, 'TEXT') },
216
- // A 2025 server has a native `VECTOR`; below that the column is JSON text, which stays queryable.
217
- mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'NVARCHAR(MAX)'), sizes: MSSQL_SIZES },
216
+ // 2025 and up; below that the server refuses the type rather than storing it as text.
217
+ mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'VECTOR'), sizes: MSSQL_SIZES },
218
218
  mongodb: { scalars: withVectorType(MONGO_SCALAR_MAP, 'array') },
219
219
  };
220
220
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
9
  import { ddlText } from '../util/ddlExpression.util.js';
10
- import { computedExpression, isInlinedExpression } from '../util/field.util.js';
10
+ import { isInlinedExpression } from '../util/field.util.js';
11
11
  import { isSoleIdField } from '../util/field.util.js';
12
12
  import { isAutoIncrement } from '../util/field.util.js';
13
13
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
@@ -101,7 +101,7 @@ function addTableFromEntity(ctx, meta) {
101
101
  isPrimaryKey,
102
102
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
103
103
  isUnique: field.unique ?? false,
104
- generatedAs: ddlText(computedExpression(field), `the computed column '${columnName}'`),
104
+ generatedAs: ddlText(field.computed, `the computed column '${columnName}'`),
105
105
  comment: field.comment,
106
106
  enum: field.enum,
107
107
  table,
@@ -202,16 +202,16 @@ function addIndexesFromEntity(ctx, meta) {
202
202
  addCompositeIndex(ctx, table, meta, idxMeta);
203
203
  }
204
204
  }
205
- /**
206
- * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
207
- * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
208
- * subset, which is what diffing and introspection compare.
209
- */
210
205
  /** An `include` column is named like any other, so a naming strategy has to reach it too. */
211
206
  function resolveIncludeColumn(ctx, meta, column) {
212
207
  const field = meta.fields[column];
213
208
  return field ? ctx.resolveColumnName(column, field) : column;
214
209
  }
210
+ /**
211
+ * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
212
+ * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
213
+ * subset, which is what diffing and introspection compare.
214
+ */
215
215
  function addCompositeIndex(ctx, table, meta, idxMeta) {
216
216
  // An entry survives if it is an expression (nothing to resolve) or names a column that exists;
217
217
  // an index left with none is dropped, the same as one naming only unknown columns always was.
@@ -11,7 +11,6 @@ export class SqliteDialect extends AbstractSqlDialect {
11
11
  indexIfNotExists: true,
12
12
  schemas: false, // SQLite's namespaces are attached database files, not declared objects
13
13
  dropTableCascade: false,
14
- renameColumn: true,
15
14
  foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
16
15
  primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
17
16
  generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
@@ -21,7 +20,6 @@ export class SqliteDialect extends AbstractSqlDialect {
21
20
  supportsTimestamptz: false,
22
21
  stringSizing: 'text',
23
22
  supportsUnsigned: false,
24
- multipleCascadePaths: true,
25
23
  serverSideCursors: false,
26
24
  };
27
25
  dialectName = 'sqlite';
@@ -89,7 +89,6 @@ export interface EngineFeatures {
89
89
  */
90
90
  readonly schemas: boolean;
91
91
  readonly dropTableCascade: boolean;
92
- readonly renameColumn: boolean;
93
92
  readonly foreignKeyAlter: boolean;
94
93
  /**
95
94
  * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
@@ -140,13 +139,6 @@ export interface EngineFeatures {
140
139
  readonly stringSizing: 'text' | 'bounded-text' | 'varchar';
141
140
  /** Whether the engine has unsigned integers, so `@Field({ unsigned: true })` reaches the column. */
142
141
  readonly supportsUnsigned: boolean;
143
- /**
144
- * Whether one table can be reached by two cascading foreign-key paths. SQL Server refuses the
145
- * constraint outright ("may cause cycles or multiple cascade paths", error 1785) and Oracle
146
- * likewise, so a cascade is downgraded to `NO ACTION` there rather than emitting DDL the engine
147
- * rejects - which would otherwise fail on any diamond-shaped schema at create time.
148
- */
149
- readonly multipleCascadePaths: boolean;
150
142
  /**
151
143
  * Whether the engine has SQL-level cursors (`DECLARE`/`FETCH FORWARD`/`CLOSE`), which is how a
152
144
  * driver with no cursor API of its own still streams a result set instead of buffering it - see
@@ -359,11 +359,6 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
359
359
  * @example `@Field({ type: String, enum: ['draft', 'paid'] as const })`
360
360
  */
361
361
  readonly enum?: EnumValues;
362
- /**
363
- * @deprecated Renamed to {@link FieldOptions.computed}, which also takes `stored`. `npx uql-codemod`
364
- * rewrites it. Giving both throws.
365
- */
366
- readonly virtual?: QueryRaw;
367
362
  /**
368
363
  * An expression the database computes, rather than a value the caller writes. Never part of an
369
364
  * insert or update either way.
@@ -818,12 +813,6 @@ export type EntityMeta<E> = {
818
813
  /** The revision `getMeta` last finalized, which is what makes finalizing idempotent and re-entrant. */
819
814
  processedAt?: number;
820
815
  };
821
- /**
822
- * Configurable options for an entity (`@Entity()` / `defineEntity`).
823
- *
824
- * Optional `fields`, `relations`, `indexes`, and `hooks` register metadata in one call for
825
- * decorator-free setups. Omit them when using `@Field` / `@ManyToOne` / etc.
826
- */
827
816
  /**
828
817
  * A table-level `CHECK`. The expression is `raw` with no interpolation, like an index expression:
829
818
  * this is DDL, so there is no placeholder a bound value could go into.
@@ -843,6 +832,12 @@ export type EntityMembers = {
843
832
  readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
844
833
  readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
845
834
  };
835
+ /**
836
+ * Configurable options for an entity (`@Entity()` / `defineEntity`).
837
+ *
838
+ * Optional `fields`, `relations`, `indexes`, and `hooks` register metadata in one call for
839
+ * decorator-free setups. Omit them when using `@Field` / `@ManyToOne` / etc.
840
+ */
846
841
  export type EntityOptions<E = unknown> = {
847
842
  readonly name?: string;
848
843
  /**
@@ -372,9 +372,6 @@ export interface SchemaIntrospector {
372
372
  * the database side never reports it, and no migration can close the gap.
373
373
  */
374
374
  readonly indexFacets: ReadonlySet<IndexFacet>;
375
- /**
376
- * Introspect entire database schema and return SchemaAST.
377
- */
378
375
  /** The whole database, or just the tables named. Names nothing matches are left out. */
379
376
  introspect(tables?: readonly string[]): Promise<SchemaAST>;
380
377
  /**
@@ -59,17 +59,17 @@ export type QueryExclude<E> = QuerySelect<E>;
59
59
  export type QueryPopulate<E> = {
60
60
  [K in RelationKey<E>]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;
61
61
  };
62
+ /**
63
+ * The key a read carries its relation tallies under. One spelling for the type and the runtime that
64
+ * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.
65
+ */
66
+ export declare const COUNT_RESULT_KEY = "_count";
62
67
  /**
63
68
  * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow
64
69
  * which ones count. One statement per relation named here, batched over every parent at once, so it
65
70
  * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of
66
71
  * a relation of the same name that `$populate` filled with rows.
67
72
  */
68
- /**
69
- * The key a read carries its relation tallies under. One spelling for the type and the runtime that
70
- * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.
71
- */
72
- export declare const COUNT_RESULT_KEY = "_count";
73
73
  export type QueryCount<E> = {
74
74
  [K in ToManyRelationKey<E>]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;
75
75
  };
@@ -141,9 +141,16 @@ type ToOneRelationKey<E> = {
141
141
  }[RelationKey<E>];
142
142
  /** The relation names a parent holds many rows of, which a populated query fills with a list. */
143
143
  type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
144
+ /**
145
+ * Ordering parents by how many rows a to-many relation holds - "the ten users with the most posts".
146
+ * The tally is computed per parent as a correlated count, never by loading the rows.
147
+ */
148
+ export type QuerySortByCount = {
149
+ $count: QuerySortDirection;
150
+ };
144
151
  /**
145
152
  * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,
146
- * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on
153
+ * like `QueryWhere`), relation sort via nested objects, and vector similarity search on
147
154
  * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:
148
155
  * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank
149
156
  * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.
@@ -153,13 +160,6 @@ type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
153
160
  * against an intersection is repeated per constituent, which made this the single most expensive
154
161
  * type in the package to check.
155
162
  */
156
- /**
157
- * Ordering parents by how many rows a to-many relation holds - "the ten users with the most posts".
158
- * The tally is computed per parent as a correlated count, never by loading the rows.
159
- */
160
- export type QuerySortByCount = {
161
- $count: QuerySortDirection;
162
- };
163
163
  export type QuerySortMap<E, Vector extends boolean = true> = {
164
164
  [K in FieldKey<E> | JsonFieldPaths<E> | RelationKey<E>]?: K extends RelationKey<E> ? IsMany<E[K]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[K]>, false> : K extends FieldKey<E> ? Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection : QuerySortDirection;
165
165
  };
@@ -1,9 +1,3 @@
1
- /**
2
- * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow
3
- * which ones count. One statement per relation named here, batched over every parent at once, so it
4
- * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of
5
- * a relation of the same name that `$populate` filled with rows.
6
- */
7
1
  /**
8
2
  * The key a read carries its relation tallies under. One spelling for the type and the runtime that
9
3
  * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.
@@ -1,4 +1,4 @@
1
- import type { EntityId, FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
1
+ import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryRaw } from './queryRaw.js';
3
3
  import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
4
4
  import type { QueryVectorQuery } from './vector.js';
@@ -20,12 +20,6 @@ export type QueryTextSearchOptions<E> = {
20
20
  */
21
21
  $config?: string;
22
22
  };
23
- /**
24
- * comparison by fields.
25
- */
26
- export type QueryWhereFieldMap<E> = {
27
- [K in FieldKey<E>]?: QueryWhereFieldValue<E[K]>;
28
- };
29
23
  /**
30
24
  * Field comparison, JSON dot-path access, and relation filtering - all fully typed.
31
25
  * JSON dot-paths are restricted to real JSON fields, and typed payloads type each path's value
@@ -37,9 +31,12 @@ export type QueryWhereFieldMap<E> = {
37
31
  * {@link QuerySortMap} is: the sets are disjoint, and an assignability check against an
38
32
  * intersection is repeated per constituent, which every `$where` in a codebase pays. The root
39
33
  * operators stay a separate member - they are a fixed shape, not keyed off the entity.
34
+ *
35
+ * An object and nothing else: in a union with ids or lists, TypeScript reports a wrong value against
36
+ * the whole `$where` instead of the key holding it. Ids are `{ id: 1 }`, or the by-id methods.
40
37
  */
41
- export type QueryWhereMap<E> = QueryWhereRootOperator<E> & {
42
- [K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhereMap<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
38
+ export type QueryWhere<E> = QueryWhereRootOperator<E> & {
39
+ [K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhere<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
43
40
  };
44
41
  /**
45
42
  * Filter a to-many relation by its row count.
@@ -332,14 +329,5 @@ export type QueryWhereFieldValue<T> = T | (undefined extends T ? null : never) |
332
329
  /**
333
330
  * query filter array - the value every {@link QueryGroupOp} takes.
334
331
  */
335
- export type QueryWhereArray<E> = (QueryWhereMap<E> | QueryRaw)[];
336
- /**
337
- * query filter.
338
- */
339
- /**
340
- * `EntityId` rather than `IdValue`: a by-id method reduces to `$where: id`, and a composite key is
341
- * addressed by an object carrying every key. That object is a where map naming those columns, so the
342
- * two spellings meet here rather than needing a conversion.
343
- */
344
- export type QueryWhere<E> = EntityId<E> | EntityId<E>[] | QueryWhereMap<E> | QueryWhereArray<E> | QueryRaw;
332
+ export type QueryWhereArray<E> = (QueryWhere<E> | QueryRaw)[];
345
333
  export {};
@@ -126,7 +126,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
126
126
  * on MySQL/SQLite they are inferred from the driver header, which is only reliable for
127
127
  * auto-increment keys in batches without explicit IDs - otherwise those entries are
128
128
  * `undefined` rather than potentially wrong values. A composite key is never one the statement
129
- * reports, so those rows are named from the payload instead.
129
+ * reports, so those rows are named as written, `onInsert` columns included.
130
130
  * @param entity the entity to persist on
131
131
  * @param payload the data to be persisted
132
132
  * @return the IDs
@@ -137,7 +137,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
137
137
  * @param entity the entity to persist on
138
138
  * @param conflictPaths the keys to use for the unique search
139
139
  * @param payload the data to be persisted
140
- * @return operation metadata; see {@link QueryUpdateResult}
140
+ * @return the id and whether it was created; see {@link QueryUpsertOneResult}
141
141
  */
142
142
  upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpsertOneResult<E>>;
143
143
  /**
@@ -145,7 +145,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
145
145
  * @param entity the entity to persist on
146
146
  * @param conflictPaths the keys to use for the unique search
147
147
  * @param payload the data to be persisted
148
- * @return operation metadata; see {@link QueryUpdateResult}
148
+ * @return the ids, in payload order; see {@link QueryUpsertManyResult}
149
149
  */
150
150
  upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpsertManyResult<E>>;
151
151
  /**
@@ -58,13 +58,15 @@ export type WithDistance<E, K extends string = '_distance'> = E & Record<K, numb
58
58
  *
59
59
  * `opsSuffix` rides along on the operator form because pgvector's index operator class is named from
60
60
  * the same metric (`vector_cosine_ops`): keeping them together is what stops a dialect from having
61
- * the operator but not the class it indexes with.
61
+ * the operator but not the class it indexes with. `metricArg` is for the engine with one function
62
+ * taking the metric by name: SQL Server's `VECTOR_DISTANCE('cosine', a, b)`.
62
63
  */
63
64
  export type VectorMetric = {
64
65
  readonly op: string;
65
66
  readonly opsSuffix: string;
66
67
  } | {
67
68
  readonly fn: string;
69
+ readonly metricArg?: string;
68
70
  };
69
71
  /** The operator form, for the pgvector-family dialects whose index DDL also needs `opsSuffix`. */
70
72
  export type VectorOperatorMetric = Extract<VectorMetric, {
@@ -1,4 +1,4 @@
1
- import { type CascadeType, type EntityData, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
3
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
4
  /** Appends `record`'s not-yet-`seen` insertable keys (real, caller-written, defined value) to `keys`. */
@@ -35,7 +35,8 @@ export declare function getSoftDeleteValue(field: FieldOptions): string | number
35
35
  }[] | readonly number[] | Uint8Array<ArrayBufferLike> | {
36
36
  readonly __json?: never;
37
37
  };
38
- export declare function fillOnFields<E>(meta: EntityMeta<E>, payload: EntityData<E> | EntityData<E>[], callbackKey: CallbackKey): EntityData<E>[];
38
+ /** Fills each field `callbackKey` generates on `payload` in place, where the caller left it unset. */
39
+ export declare function fillOnFields<E, R extends EntityData<E> | UpdatePayload<E>>(meta: EntityMeta<E>, payload: R | R[], callbackKey: CallbackKey): R[];
39
40
  /**
40
41
  * The relation keys present in `payload` whose cascade configuration allows `action`. Only
41
42
  * `payload`'s keys are read, so any keys-bearing object works (an entity, an update payload,
@@ -90,13 +91,16 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
90
91
  export declare function hasVectorNear(where: unknown): boolean;
91
92
  /** Type guard: checks whether an update payload value is a JSON operator object. */
92
93
  export declare function isJsonUpdateOp(value: unknown): value is JsonUpdateOp;
93
- export declare function augmentWhere<E>(meta: EntityMeta<E>, target?: QueryWhere<E>, source?: QueryWhere<E>): QueryWhere<E>;
94
94
  /**
95
- * Normalizes any `$where` shape (id, id[], raw, or map) to a `QueryWhereMap`. Read-only: for a map
96
- * input it returns that same object by reference (no copy), so callers must not mutate the result -
97
- * {@link applyFilters} and {@link augmentWhere} return new objects instead.
95
+ * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
96
+ * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
98
97
  */
99
- export declare function buildQueryWhereAsMap<E>(meta: EntityMeta<E>, filter?: QueryWhere<E>): QueryWhereMap<E>;
98
+ export declare function whereIds<E>(meta: EntityMeta<E>, ids: EntityId<E> | EntityId<E>[]): QueryWhere<E>;
99
+ /**
100
+ * Refuses a `$where` that is not a map. Untyped JS and parsed JSON can still pass an id or a list of
101
+ * them, and a scalar read as a map has no keys: the statement would address every row.
102
+ */
103
+ export declare function assertWhere<E>(meta: EntityMeta<E>, where: unknown): void;
100
104
  /** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
101
105
  export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']): QueryOptions['filters'];
102
106
  /**
@@ -112,7 +116,7 @@ export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']
112
116
  * (`{}`) resolved to "no restriction" and adds nothing - the escape hatch for trusted cross-tenant
113
117
  * work (e.g. a maintenance job running under a `system` context).
114
118
  */
115
- export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhereMap<E>, opts?: QueryOptions): QueryWhereMap<E>;
119
+ export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhere<E>, opts?: QueryOptions): QueryWhere<E>;
116
120
  /**
117
121
  * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
118
122
  */