uql-orm 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) 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/dialect/abstractDialect.d.ts +5 -5
  4. package/dist/dialect/abstractDialect.js +7 -6
  5. package/dist/dialect/abstractSqlDialect.d.ts +2 -3
  6. package/dist/dialect/abstractSqlDialect.js +5 -8
  7. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -1
  8. package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
  9. package/dist/dialect/pgLikeSqlDialect.js +0 -2
  10. package/dist/dialect/vectorSqlDialect.js +2 -1
  11. package/dist/http/handler.js +3 -12
  12. package/dist/http/query.js +4 -1
  13. package/dist/migrate/ddl/index.d.ts +8 -0
  14. package/dist/migrate/ddl/index.js +11 -0
  15. package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
  16. package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
  17. package/dist/migrate/ddl/tableDdl.d.ts +34 -0
  18. package/dist/migrate/ddl/tableDdl.js +71 -0
  19. package/dist/migrate/introspection/mssqlIntrospector.d.ts +6 -9
  20. package/dist/migrate/introspection/mssqlIntrospector.js +40 -33
  21. package/dist/migrate/schemaGenerator.d.ts +8 -10
  22. package/dist/migrate/schemaGenerator.js +29 -74
  23. package/dist/mongo/mongoDialect.js +9 -14
  24. package/dist/mssql/mssqlDialect.d.ts +12 -1
  25. package/dist/mssql/mssqlDialect.js +20 -7
  26. package/dist/querier/abstractQuerier.js +13 -11
  27. package/dist/querier/abstractSqlQuerier.js +3 -3
  28. package/dist/schema/canonicalType.js +2 -2
  29. package/dist/sqlite/sqliteDialect.js +0 -2
  30. package/dist/type/dialect.d.ts +0 -8
  31. package/dist/type/query.d.ts +1 -1
  32. package/dist/type/queryWhere.d.ts +7 -19
  33. package/dist/type/vector.d.ts +3 -1
  34. package/dist/util/dialect.util.d.ts +10 -7
  35. package/dist/util/dialect.util.js +16 -29
  36. package/dist/util/object.util.d.ts +2 -0
  37. package/dist/util/object.util.js +4 -0
  38. package/package.json +2 -2
@@ -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,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.
@@ -89,6 +89,17 @@ export declare class MsSqlDialect extends MergeSqlDialect {
89
89
  * `uuidv7()` is emitted on. It is a predicate rather than a value, so it stands alone.
90
90
  */
91
91
  protected regexCondition(operand: string, placeholder: string): string;
92
+ /**
93
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
94
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
95
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
96
+ */
97
+ readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
98
+ /**
99
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
100
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
101
+ */
102
+ protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
92
103
  /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
93
104
  createSchemaSql(schema: string): string;
94
105
  /** 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';
@@ -188,6 +182,25 @@ export class MsSqlDialect extends MergeSqlDialect {
188
182
  regexCondition(operand, placeholder) {
189
183
  return `REGEXP_LIKE(${operand}, ${placeholder})`;
190
184
  }
185
+ /**
186
+ * `VECTOR_DISTANCE('cosine', a, b)`, one function taking the metric by name; `dot` is the negated
187
+ * inner product, pgvector's `<#>` convention. Exact search, as on sqlite-vec: 2025's DiskANN index
188
+ * is a preview feature, and only `VECTOR_SEARCH` reads it, never an `ORDER BY VECTOR_DISTANCE`.
189
+ */
190
+ vectorMetrics = new Map([
191
+ ['cosine', { fn: 'VECTOR_DISTANCE', metricArg: 'cosine' }],
192
+ ['l2', { fn: 'VECTOR_DISTANCE', metricArg: 'euclidean' }],
193
+ ['inner', { fn: 'VECTOR_DISTANCE', metricArg: 'dot' }],
194
+ ]);
195
+ /**
196
+ * `VECTOR_DISTANCE` refuses the `nvarchar` a vector binds as, so it is cast - to the value's own
197
+ * length, which is its dimension. A write would convert implicitly, and shares the cast anyway.
198
+ */
199
+ appendVectorValue(ctx, value) {
200
+ ctx.append('CAST(');
201
+ super.appendVectorValue(ctx, value);
202
+ ctx.append(` AS VECTOR(${value.length}))`);
203
+ }
191
204
  /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
192
205
  createSchemaSql(schema) {
193
206
  const literal = escapeSingleQuotes(schema);
@@ -1,12 +1,13 @@
1
1
  import { assertSoleId, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
2
- import { asSelectMap, augmentWhere, childrenOf, clone, dataKeyed, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
2
+ import { asSelectMap, childrenOf, clone, dataKeyed, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
4
  import { fillRelationCounts, withIdForCounts } from './relationCount.js';
5
5
  /**
6
6
  * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
7
- * `{ $where: id }`, and a nullish `$where` is *no filter*, so an unchecked one addresses the whole
8
- * table. An entity declares its id optional, which puts `undefined` inside `IdValue<E>`, and the
9
- * HTTP layer reaches these methods with parsed JSON regardless, so the guard belongs at runtime.
7
+ * `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
8
+ * addresses the whole table. An entity declares its id optional, which puts `undefined` inside
9
+ * `IdValue<E>`, and the HTTP layer reaches these methods with parsed JSON regardless, so the guard
10
+ * belongs at runtime.
10
11
  *
11
12
  * Its callers are all `async` so this surfaces as a rejection on every one of them: a guard that
12
13
  * threw synchronously from some and rejected from others would escape a caller's `.catch()`.
@@ -16,7 +17,7 @@ function assertIdValue(entity, id) {
16
17
  throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
17
18
  }
18
19
  if (isScalarId(id)) {
19
- // One value names one column, which `buildQueryWhereAsMap` refuses on a composite.
20
+ // One value names one column, which `whereIds` refuses on a composite.
20
21
  return;
21
22
  }
22
23
  // Every key, or the `$where` names only some of the columns and addresses each row that agrees on
@@ -119,7 +120,7 @@ export class AbstractQuerier {
119
120
  }
120
121
  async findOneById(entity, id, q = {}, opts) {
121
122
  assertIdValue(entity, id);
122
- return this.findOne(entity, { ...q, $where: augmentWhere(getMeta(entity), q.$where, id) }, opts);
123
+ return this.findOne(entity, { ...q, $where: { ...q.$where, ...whereIds(getMeta(entity), id) } }, opts);
123
124
  }
124
125
  async findOne(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
125
126
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
@@ -194,21 +195,21 @@ export class AbstractQuerier {
194
195
  }
195
196
  async updateOneById(entity, id, payload, opts) {
196
197
  assertIdValue(entity, id);
197
- return this.updateMany(entity, { $where: id }, payload, opts);
198
+ return this.updateMany(entity, { $where: whereIds(getMeta(entity), id) }, payload, opts);
198
199
  }
199
200
  async updateMany(entity, q, payload, opts) {
200
201
  return this.hooked(entity, 'Update', [payload], () => this.internalUpdateMany(entity, q, payload, opts));
201
202
  }
202
203
  async restoreOneById(entity, id) {
203
204
  assertIdValue(entity, id);
204
- return this.restoreMany(entity, { $where: id });
205
+ return this.restoreMany(entity, { $where: whereIds(getMeta(entity), id) });
205
206
  }
206
207
  async restoreMany(entity, q) {
207
208
  const meta = getMeta(entity);
208
209
  if (!meta.softDelete) {
209
210
  throw new TypeError(`'${entity.name}' has not enabled 'softDelete'`);
210
211
  }
211
- const $where = augmentWhere(meta, q.$where, { [meta.softDelete]: { $ne: null } });
212
+ const $where = { ...q.$where, [meta.softDelete]: { $ne: null } };
212
213
  return this.updateMany(entity, { ...q, $where }, { [meta.softDelete]: null }, {
213
214
  filters: { softDelete: false },
214
215
  });
@@ -234,7 +235,7 @@ export class AbstractQuerier {
234
235
  }
235
236
  async deleteOneById(entity, id, opts) {
236
237
  assertIdValue(entity, id);
237
- return this.deleteMany(entity, { $where: id }, opts);
238
+ return this.deleteMany(entity, { $where: whereIds(getMeta(entity), id) }, opts);
238
239
  }
239
240
  async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
240
241
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
@@ -248,7 +249,8 @@ export class AbstractQuerier {
248
249
  let target = q;
249
250
  if (doomed) {
250
251
  const meta = getMeta(entity);
251
- target = { $where: doomed.map((it) => idOf(meta, it)) };
252
+ const ids = doomed.map((it) => idOf(meta, it));
253
+ target = { $where: whereIds(meta, ids) };
252
254
  }
253
255
  await this.emitHook(entity, 'beforeDelete', doomed ?? []);
254
256
  const changes = await this.internalDeleteMany(entity, target, opts);
@@ -1,7 +1,7 @@
1
1
  import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
2
2
  import { decodeColumn } from '../dialect/hydrateColumn.js';
3
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';
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
  /**
@@ -418,7 +418,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
418
418
  if (!ids.length) {
419
419
  return 0;
420
420
  }
421
- target = { $where: ids };
421
+ target = { $where: whereIds(getMeta(entity), ids) };
422
422
  }
423
423
  const ctx = this.dialect.createContext();
424
424
  this.dialect.update(ctx, entity, target, payload, opts);
@@ -507,7 +507,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
507
507
  // outright by any schema that declares the constraint without `ON DELETE CASCADE`.
508
508
  await this.deleteRelations(entity, ids, opts);
509
509
  const deleteCtx = this.dialect.createContext();
510
- this.dialect.delete(deleteCtx, entity, { $where: ids }, opts);
510
+ this.dialect.delete(deleteCtx, entity, { $where: whereIds(meta, ids) }, opts);
511
511
  const { changes = 0 } = await this.run(deleteCtx.sql, deleteCtx.values);
512
512
  return changes;
513
513
  }
@@ -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
  /**
@@ -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
@@ -143,7 +143,7 @@ type ToOneRelationKey<E> = {
143
143
  type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
144
144
  /**
145
145
  * 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
146
+ * like `QueryWhere`), relation sort via nested objects, and vector similarity search on
147
147
  * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:
148
148
  * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank
149
149
  * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.
@@ -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 {};
@@ -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 } 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`. */
@@ -90,13 +90,16 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
90
90
  export declare function hasVectorNear(where: unknown): boolean;
91
91
  /** Type guard: checks whether an update payload value is a JSON operator object. */
92
92
  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
93
  /**
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.
94
+ * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
95
+ * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
98
96
  */
99
- export declare function buildQueryWhereAsMap<E>(meta: EntityMeta<E>, filter?: QueryWhere<E>): QueryWhereMap<E>;
97
+ export declare function whereIds<E>(meta: EntityMeta<E>, ids: EntityId<E> | EntityId<E>[]): QueryWhere<E>;
98
+ /**
99
+ * Refuses a `$where` that is not a map. Untyped JS and parsed JSON can still pass an id or a list of
100
+ * them, and a scalar read as a map has no keys: the statement would address every row.
101
+ */
102
+ export declare function assertWhere<E>(meta: EntityMeta<E>, where: unknown): void;
100
103
  /** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
101
104
  export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']): QueryOptions['filters'];
102
105
  /**
@@ -112,7 +115,7 @@ export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']
112
115
  * (`{}`) resolved to "no restriction" and adds nothing - the escape hatch for trusted cross-tenant
113
116
  * work (e.g. a maintenance job running under a `system` context).
114
117
  */
115
- export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhereMap<E>, opts?: QueryOptions): QueryWhereMap<E>;
118
+ export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhere<E>, opts?: QueryOptions): QueryWhere<E>;
116
119
  /**
117
120
  * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
118
121
  */
@@ -3,7 +3,7 @@ import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { VECTOR_INDEX_TYPES } from '../type/vector.js';
5
5
  import { isDatabaseWritten } from './field.util.js';
6
- import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, someKey } from './object.util.js';
6
+ import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, isWhereMap, someKey } from './object.util.js';
7
7
  export function filterFieldKeys(meta, payload, callbackKey) {
8
8
  return getKeys(payload).filter((key) => {
9
9
  const fieldOpts = meta.fields[key];
@@ -234,37 +234,24 @@ const JSON_UPDATE_OPS = [
234
234
  export function isJsonUpdateOp(value) {
235
235
  return value !== null && typeof value === 'object' && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
236
236
  }
237
- export function augmentWhere(meta, target = {}, source = {}) {
238
- const targetComparison = buildQueryWhereAsMap(meta, target);
239
- const sourceComparison = buildQueryWhereAsMap(meta, source);
240
- return {
241
- ...targetComparison,
242
- ...sourceComparison,
243
- };
244
- }
245
237
  /**
246
- * Normalizes any `$where` shape (id, id[], raw, or map) to a `QueryWhereMap`. Read-only: for a map
247
- * input it returns that same object by reference (no copy), so callers must not mutate the result -
248
- * {@link applyFilters} and {@link augmentWhere} return new objects instead.
238
+ * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
239
+ * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
249
240
  */
250
- export function buildQueryWhereAsMap(meta, filter = {}) {
251
- if (filter instanceof QueryRaw) {
252
- return { $and: [filter] };
253
- }
254
- if (Array.isArray(filter)) {
255
- // A list of bare ids is an `IN` over the one key column; a list of anything else is a list of
256
- // `$where`s, which is an OR - and that is how a composite's id objects name a settled set of rows.
257
- return filter.every(isScalarId)
258
- ? { [soleIdOf(meta, 'addressing by a bare id value')]: filter }
259
- : { $or: filter };
241
+ export function whereIds(meta, ids) {
242
+ if (Array.isArray(ids) ? ids.every(isScalarId) : isScalarId(ids)) {
243
+ return { [soleIdOf(meta, 'addressing by a bare id value')]: ids };
260
244
  }
261
- if (isScalarId(filter)) {
262
- // A scalar can only name one column, so on a composite it would address every row agreeing on
263
- // that one. A composite is addressed by a map, which falls through below as the `$where` it
264
- // already is - an id object and a where map are the same shape by design.
265
- return { [soleIdOf(meta, 'addressing by a bare id value')]: filter };
245
+ return (Array.isArray(ids) ? { $or: ids } : ids);
246
+ }
247
+ /**
248
+ * Refuses a `$where` that is not a map. Untyped JS and parsed JSON can still pass an id or a list of
249
+ * them, and a scalar read as a map has no keys: the statement would address every row.
250
+ */
251
+ export function assertWhere(meta, where) {
252
+ if (!isWhereMap(where)) {
253
+ throw new TypeError(`$where on '${entityName(meta)}' must be a map of conditions, such as { id: 1 }`);
266
254
  }
267
- return filter;
268
255
  }
269
256
  /** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
270
257
  export function withoutSoftDeleteFilter(filters) {
@@ -314,7 +301,7 @@ export function applyFilters(meta, whereMap, opts) {
314
301
  }
315
302
  continue;
316
303
  }
317
- const conditionMap = buildQueryWhereAsMap(meta, condition);
304
+ const conditionMap = condition;
318
305
  if (!hasKeys(conditionMap)) {
319
306
  continue; // resolved to "no restriction" (e.g. a trusted system context) - nothing to merge
320
307
  }
@@ -35,3 +35,5 @@ export declare function getFieldKeys<E>(fields: {
35
35
  * is what a `$where` map and a composite key's id object both are; an array is a list of either.
36
36
  */
37
37
  export declare function isScalarId(value: unknown): boolean;
38
+ /** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */
39
+ export declare function isWhereMap(value: unknown): value is Record<string, unknown>;
@@ -81,3 +81,7 @@ export function isScalarId(value) {
81
81
  const proto = Object.getPrototypeOf(value);
82
82
  return proto !== Object.prototype && proto !== null;
83
83
  }
84
+ /** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */
85
+ export function isWhereMap(value) {
86
+ return !Array.isArray(value) && !isScalarId(value);
87
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.52.0",
6
+ "version": "0.53.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -151,7 +151,7 @@
151
151
  "express": "^5.2.1",
152
152
  "mariadb": "^3.5.4",
153
153
  "mongodb": "^7.6.0",
154
- "mssql": "^11",
154
+ "mssql": "^12",
155
155
  "mysql2": "^3.24.4",
156
156
  "pg": "^8.23.0",
157
157
  "pg-query-stream": "^4.17.0",