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.
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/dialect/abstractDialect.d.ts +5 -5
- package/dist/dialect/abstractDialect.js +7 -6
- package/dist/dialect/abstractSqlDialect.d.ts +2 -3
- package/dist/dialect/abstractSqlDialect.js +5 -8
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
- package/dist/dialect/pgLikeSqlDialect.js +0 -2
- package/dist/dialect/vectorSqlDialect.js +2 -1
- package/dist/http/handler.js +3 -12
- package/dist/http/query.js +4 -1
- package/dist/migrate/ddl/index.d.ts +8 -0
- package/dist/migrate/ddl/index.js +11 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
- package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
- package/dist/migrate/ddl/tableDdl.d.ts +34 -0
- package/dist/migrate/ddl/tableDdl.js +71 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +6 -9
- package/dist/migrate/introspection/mssqlIntrospector.js +40 -33
- package/dist/migrate/schemaGenerator.d.ts +8 -10
- package/dist/migrate/schemaGenerator.js +29 -74
- package/dist/mongo/mongoDialect.js +9 -14
- package/dist/mssql/mssqlDialect.d.ts +12 -1
- package/dist/mssql/mssqlDialect.js +20 -7
- package/dist/querier/abstractQuerier.js +13 -11
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/schema/canonicalType.js +2 -2
- package/dist/sqlite/sqliteDialect.js +0 -2
- package/dist/type/dialect.d.ts +0 -8
- package/dist/type/query.d.ts +1 -1
- package/dist/type/queryWhere.d.ts +7 -19
- package/dist/type/vector.d.ts +3 -1
- package/dist/util/dialect.util.d.ts +10 -7
- package/dist/util/dialect.util.js +16 -29
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- 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,
|
|
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.
|
|
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.
|
|
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 =
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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,
|
|
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
|
|
8
|
-
* table. An entity declares its id optional, which puts `undefined` inside
|
|
9
|
-
* HTTP layer reaches these methods with parsed JSON regardless, so the guard
|
|
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 `
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
217
|
-
mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, '
|
|
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';
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -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
|
package/dist/type/query.d.ts
CHANGED
|
@@ -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 `
|
|
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 {
|
|
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
|
|
42
|
-
[K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ?
|
|
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> = (
|
|
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 {};
|
package/dist/type/vector.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
247
|
-
*
|
|
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
|
|
251
|
-
if (
|
|
252
|
-
return {
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 =
|
|
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>;
|
package/dist/util/object.util.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
154
|
+
"mssql": "^12",
|
|
155
155
|
"mysql2": "^3.24.4",
|
|
156
156
|
"pg": "^8.23.0",
|
|
157
157
|
"pg-query-stream": "^4.17.0",
|