uql-orm 0.77.0 → 0.77.1
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 +4 -4
- package/dist/d1/d1Querier.js +2 -1
- package/dist/d1/d1SqliteDialect.js +2 -1
- package/dist/dialect/abstractDialect.d.ts +7 -1
- package/dist/dialect/abstractDialect.js +17 -5
- package/dist/dialect/abstractSqlDialect.d.ts +4 -4
- package/dist/dialect/abstractSqlDialect.js +30 -26
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -3
- package/dist/dialect/pgLikeSqlDialect.js +1 -3
- package/dist/dialect/queryJoins.js +6 -5
- package/dist/dialect/vectorSqlDialect.js +2 -1
- package/dist/http/query.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/maria/mariaDialect.js +2 -2
- package/dist/mongo/mongoDialect.d.ts +0 -6
- package/dist/mongo/mongoDialect.js +21 -29
- package/dist/mongo/mongodbQuerier.js +0 -1
- package/dist/mssql/mssqlDialect.d.ts +1 -5
- package/dist/mssql/mssqlDialect.js +4 -11
- package/dist/querier/abstractQuerier.d.ts +8 -1
- package/dist/querier/abstractQuerier.js +32 -14
- package/dist/querier/abstractSqlQuerier.d.ts +0 -5
- package/dist/querier/abstractSqlQuerier.js +4 -17
- package/dist/querier/queryError.d.ts +1 -27
- package/dist/querier/queryError.js +3 -28
- package/dist/sqlite/sqliteDialect.js +0 -2
- package/dist/type/dialect.d.ts +18 -6
- package/dist/type/queryAggregate.js +2 -1
- package/dist/type/queryLock.js +2 -1
- package/dist/type/queryWhere.d.ts +7 -6
- package/dist/type/utility.d.ts +8 -0
- package/dist/util/dialect.util.js +14 -13
- package/dist/util/relationQuery.util.js +4 -3
- package/dist/util/uqlError.d.ts +39 -0
- package/dist/util/uqlError.js +35 -0
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +7 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
2
|
import { getKeys, getRelationRequestSummary, hasKeys, isRecord, isToManyRelation, isVectorSearch, parseRelationAtKey, parseRelationSize, parseSortByCount, } from '../util/index.js';
|
|
3
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
3
4
|
export const NO_JOINS = new Map();
|
|
4
5
|
/**
|
|
5
6
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
@@ -49,7 +50,7 @@ export function groupPathField(joins, path) {
|
|
|
49
50
|
}
|
|
50
51
|
const join = joins.get(path.slice(0, -1).join('.'));
|
|
51
52
|
if (!join) {
|
|
52
|
-
throw new
|
|
53
|
+
throw new UqlUsageError(`cannot $group by '${path.join('.')}': only a to-one relation's field groups, since a to-many multiplies the rows it joins`);
|
|
53
54
|
}
|
|
54
55
|
return { key, join };
|
|
55
56
|
}
|
|
@@ -149,14 +150,14 @@ function addPathJoins(joins, claimAlias, meta, map, required, parent) {
|
|
|
149
150
|
/** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
|
|
150
151
|
export function resolveSortableJoin(relation, path, value, joins, unjoinable) {
|
|
151
152
|
if (isToManyRelation(relation)) {
|
|
152
|
-
throw new
|
|
153
|
+
throw new UqlUsageError(`cannot $sort by '${path}': a parent has many of them, so there is no single value to order by. Sort the relation's own rows inside $populate instead.`);
|
|
153
154
|
}
|
|
154
155
|
if (!isSortMap(value)) {
|
|
155
|
-
throw new
|
|
156
|
+
throw new UqlUsageError(`$sort by relation '${path}' expects a map of its fields, got ${String(value)}`);
|
|
156
157
|
}
|
|
157
158
|
const join = joins.get(path);
|
|
158
159
|
if (!join) {
|
|
159
|
-
throw new
|
|
160
|
+
throw new UqlUsageError(unjoinable);
|
|
160
161
|
}
|
|
161
162
|
return { join, sort: value };
|
|
162
163
|
}
|
|
@@ -177,7 +178,7 @@ export function relationSortTerms(relKey, path, value) {
|
|
|
177
178
|
return [];
|
|
178
179
|
}
|
|
179
180
|
if (search.$project !== undefined) {
|
|
180
|
-
throw new
|
|
181
|
+
throw new UqlUsageError(`cannot $project the distance of relation '${path}': it ranks the parent, and no one row answers under it`);
|
|
181
182
|
}
|
|
182
183
|
return [{ spec: { relation: relKey, op: '$min', field, search }, direction: undefined }];
|
|
183
184
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unsupportedVectorMetric } from '../type/vector.js';
|
|
2
2
|
import { findVectorIndex, findVectorSort, vectorCandidates, vectorDistanceOf } from '../util/dialect.util.js';
|
|
3
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
3
4
|
import { AbstractDialect } from './abstractDialect.js';
|
|
4
5
|
import { encodeFloat32s } from './vectorCast.js';
|
|
5
6
|
/**
|
|
@@ -70,7 +71,7 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
70
71
|
*/
|
|
71
72
|
appendVectorDistance(ctx, meta, key, search, prefix) {
|
|
72
73
|
if (this.vectorMetrics.size === 0) {
|
|
73
|
-
throw new
|
|
74
|
+
throw new UqlUsageError(`${this.dialectName} does not support vector similarity search. Use raw() for vector queries.`);
|
|
74
75
|
}
|
|
75
76
|
const { colName, distance, field } = this.resolveVectorDistance(meta, key, search);
|
|
76
77
|
const metric = this.vectorMetrics.get(distance);
|
package/dist/http/query.js
CHANGED
|
@@ -5,6 +5,8 @@ import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUER
|
|
|
5
5
|
import { RAW_VALUE } from '../type/queryRaw.js';
|
|
6
6
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
7
7
|
import { getKeys, isWhereMap } from '../util/object.util.js';
|
|
8
|
+
// the error class alone, from its own leaf module: `queryError.ts` carries every driver's code map
|
|
9
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
8
10
|
/**
|
|
9
11
|
* Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar
|
|
10
12
|
* flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't
|
|
@@ -35,7 +37,7 @@ export function parseQueryParams(params = {}) {
|
|
|
35
37
|
const query = {};
|
|
36
38
|
for (const key of getKeys(params)) {
|
|
37
39
|
if (REJECTED_QUERY_KEYS.has(key)) {
|
|
38
|
-
throw
|
|
40
|
+
throw new UqlUsageError(`'${key}' is not supported over HTTP`);
|
|
39
41
|
}
|
|
40
42
|
if (ALLOWED_QUERY_KEYS.has(key)) {
|
|
41
43
|
query[key] = params[key];
|
|
@@ -54,7 +56,7 @@ export function parseQueryParams(params = {}) {
|
|
|
54
56
|
}
|
|
55
57
|
query['$where'] ??= {};
|
|
56
58
|
if (!isWhereMap(query['$where'])) {
|
|
57
|
-
throw
|
|
59
|
+
throw new UqlUsageError("'$where' must be a JSON object");
|
|
58
60
|
}
|
|
59
61
|
// A query string carries every value as text, so what decodes a clause is the shape its group
|
|
60
62
|
// declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
2
2
|
import { jsonPath } from '../dialect/jsonSql.js';
|
|
3
|
-
import { MYSQL_FEATURES, MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
3
|
+
import { MYSQL_FEATURES, MYSQL_ROW_LOCKS, MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
4
4
|
import { getMeta } from '../entity/index.js';
|
|
5
5
|
import { columnFamily } from '../util/field.util.js';
|
|
6
6
|
export class MariaDialect extends MysqlLikeSqlDialect {
|
|
@@ -18,7 +18,7 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
18
18
|
vectorBytes: true,
|
|
19
19
|
vectorIndexRequiresNotNull: true,
|
|
20
20
|
indexIfNotExists: true,
|
|
21
|
-
|
|
21
|
+
rowLocks: { ...MYSQL_ROW_LOCKS, of: false },
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* A derived table here reads no column of the statement around it, so the aggregate reads the
|
|
@@ -95,12 +95,6 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
95
95
|
private static compareCount;
|
|
96
96
|
/** Whether a query subtracts `key` from the projection, via `$exclude` or a negative `$select`. */
|
|
97
97
|
private subtractsKey;
|
|
98
|
-
/**
|
|
99
|
-
* MongoDB has no row-level lock to map `$lock` onto: its concurrency control is the transaction
|
|
100
|
-
* plus atomic document updates. Rejected rather than ignored, like `raw()` below, since a dropped
|
|
101
|
-
* lock silently removes the mutual exclusion the caller asked for.
|
|
102
|
-
*/
|
|
103
|
-
assertNoLock<E>(q: Query<E>): void;
|
|
104
98
|
/** `raw()` renders SQL, so it has no MongoDB equivalent - say so instead of emitting `{}`. */
|
|
105
99
|
private assertNoRaw;
|
|
106
100
|
/**
|
|
@@ -6,6 +6,7 @@ import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/
|
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
8
8
|
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
|
|
9
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
9
10
|
import { decodeBigIntsExcept } from '../util/wideNumber.js';
|
|
10
11
|
import { textLanguage } from './textLanguage.js';
|
|
11
12
|
import { vectorDistanceExpr } from './vectorDistance.js';
|
|
@@ -30,6 +31,7 @@ export const mongoDialectFeatures = {
|
|
|
30
31
|
supportsUnsigned: false,
|
|
31
32
|
serverSideCursors: false,
|
|
32
33
|
correlatedWrites: false,
|
|
34
|
+
rowLocks: false, // its concurrency control is the transaction plus atomic document updates
|
|
33
35
|
};
|
|
34
36
|
/** What `toWireId` converts: the hex spelling of an `ObjectId`, and nothing looser. */
|
|
35
37
|
const HEX_24 = /^[0-9a-f]{24}$/i;
|
|
@@ -109,7 +111,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
109
111
|
else if (meta.relations[key]) {
|
|
110
112
|
this.assertNoRaw(val);
|
|
111
113
|
if (!lookups) {
|
|
112
|
-
throw new
|
|
114
|
+
throw new UqlUsageError(`filtering by relation '${key}' is not supported here on MongoDB`);
|
|
113
115
|
}
|
|
114
116
|
this.appendRelationLookup(filter, meta, key, val, lookups);
|
|
115
117
|
}
|
|
@@ -118,7 +120,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
118
120
|
this.assertKnownPathRoot(meta, key);
|
|
119
121
|
if (aggregateOf(meta.fields[key])) {
|
|
120
122
|
if (!lookups) {
|
|
121
|
-
throw new
|
|
123
|
+
throw new UqlUsageError(`filtering by relation aggregate '${key}' is not supported here on MongoDB`);
|
|
122
124
|
}
|
|
123
125
|
this.appendAggregateField(meta, key, lookups);
|
|
124
126
|
}
|
|
@@ -270,7 +272,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
270
272
|
? [{ $gte: [count, bound[0]] }, { $lte: [count, bound[1]] }]
|
|
271
273
|
: [{ [op]: [count, bound] }]);
|
|
272
274
|
if (!comparisons.length) {
|
|
273
|
-
throw new
|
|
275
|
+
throw new UqlUsageError('$size needs at least one comparison');
|
|
274
276
|
}
|
|
275
277
|
return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
|
|
276
278
|
}
|
|
@@ -279,20 +281,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
279
281
|
const at = (map) => map?.[key];
|
|
280
282
|
return at(exclude) === true || at(select) === false;
|
|
281
283
|
}
|
|
282
|
-
/**
|
|
283
|
-
* MongoDB has no row-level lock to map `$lock` onto: its concurrency control is the transaction
|
|
284
|
-
* plus atomic document updates. Rejected rather than ignored, like `raw()` below, since a dropped
|
|
285
|
-
* lock silently removes the mutual exclusion the caller asked for.
|
|
286
|
-
*/
|
|
287
|
-
assertNoLock(q) {
|
|
288
|
-
if (q.$lock) {
|
|
289
|
-
throw new TypeError('$lock (row-level locking) is not supported on MongoDB');
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
284
|
/** `raw()` renders SQL, so it has no MongoDB equivalent - say so instead of emitting `{}`. */
|
|
293
285
|
assertNoRaw(value) {
|
|
294
286
|
if (value instanceof QueryRaw) {
|
|
295
|
-
throw new
|
|
287
|
+
throw new UqlUsageError('raw() in $where is not supported on MongoDB');
|
|
296
288
|
}
|
|
297
289
|
}
|
|
298
290
|
/**
|
|
@@ -305,7 +297,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
305
297
|
if (root === MongoDialect.ID_KEY || meta.fields[root]) {
|
|
306
298
|
return;
|
|
307
299
|
}
|
|
308
|
-
throw new
|
|
300
|
+
throw new UqlUsageError(`path ${key} does not exist in ${entityName(meta)}`);
|
|
309
301
|
}
|
|
310
302
|
/** String operators -> { pattern: (v) => regex, caseInsensitive } */
|
|
311
303
|
static REGEX_OP_MAP = new Map([
|
|
@@ -383,7 +375,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
383
375
|
case '$near':
|
|
384
376
|
// Atlas offers only a similarity threshold, on the index's own scale, which UQL neither emits nor
|
|
385
377
|
// reads: converting a distance would mean guessing the metric, so this refuses.
|
|
386
|
-
throw new
|
|
378
|
+
throw new UqlUsageError('$near is not supported on MongoDB: Atlas scores by index-defined similarity, not distance. ' +
|
|
387
379
|
"Project the score with $sort's $project and filter on it instead.");
|
|
388
380
|
default:
|
|
389
381
|
result[op] = val;
|
|
@@ -430,7 +422,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
430
422
|
return {};
|
|
431
423
|
}
|
|
432
424
|
if (Array.isArray(select)) {
|
|
433
|
-
throw new
|
|
425
|
+
throw new UqlUsageError('raw $select is not supported on MongoDB');
|
|
434
426
|
}
|
|
435
427
|
const selectMap = asSelectMap(select);
|
|
436
428
|
// Projected by column, not by field key; `normalizeId` maps them back on the way out.
|
|
@@ -497,7 +489,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
497
489
|
const relation = meta.relations[key];
|
|
498
490
|
if (key === '$text') {
|
|
499
491
|
if (path) {
|
|
500
|
-
throw new
|
|
492
|
+
throw new UqlUsageError(`$sort by $text is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
|
|
501
493
|
}
|
|
502
494
|
const { order, project } = textSortOf(sort);
|
|
503
495
|
out[project ?? TEXT_SCORE_ALIAS] = sortDirection(order);
|
|
@@ -508,7 +500,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
508
500
|
// so one reaching it is a second. `sortDirection` would read the operator object as "ascending"
|
|
509
501
|
// and order by the raw vector column instead, a silent answer where the caller asked for a rank.
|
|
510
502
|
if (isVectorSearch(value)) {
|
|
511
|
-
throw new
|
|
503
|
+
throw new UqlUsageError(`cannot $sort by a second vector '${key}' on MongoDB: $vectorSearch ranks by one`);
|
|
512
504
|
}
|
|
513
505
|
const docPath = path + this.pathOf(meta, key);
|
|
514
506
|
const nulls = sortNulls(value);
|
|
@@ -530,7 +522,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
530
522
|
// own pipeline has: a nested one is built inside its parent's `$lookup`, where there is no
|
|
531
523
|
// parent document left to hang it off.
|
|
532
524
|
if (path) {
|
|
533
|
-
throw new
|
|
525
|
+
throw new UqlUsageError(`$sort by '${relPath}.${spec.field ?? '$count'}' is only supported on the queried entity`);
|
|
534
526
|
}
|
|
535
527
|
if (spec.search) {
|
|
536
528
|
nearest[sortAggregateField(spec)] = 1;
|
|
@@ -773,10 +765,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
773
765
|
// that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
|
|
774
766
|
// the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
|
|
775
767
|
if (q.$distinct && aggregated.fields.length) {
|
|
776
|
-
throw new
|
|
768
|
+
throw new UqlUsageError(`cannot $sort by a relation's aggregate with $distinct: the grouping keeps only the columns it projects`);
|
|
777
769
|
}
|
|
778
770
|
if (q.$distinct && sortOnly.length) {
|
|
779
|
-
throw new
|
|
771
|
+
throw new UqlUsageError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
|
|
780
772
|
}
|
|
781
773
|
// `$distinct` inverts the usual order twice over: the projection decides which columns make two
|
|
782
774
|
// rows the same, so it has to run *before* the grouping, and the grouping collapses rows, so the
|
|
@@ -1141,7 +1133,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1141
1133
|
*/
|
|
1142
1134
|
buildGroupSpec(meta, groupEntries, joins) {
|
|
1143
1135
|
if (!groupEntries.length) {
|
|
1144
|
-
throw new
|
|
1136
|
+
throw new UqlUsageError('aggregate requires at least one $group column or $select function');
|
|
1145
1137
|
}
|
|
1146
1138
|
const groupId = {};
|
|
1147
1139
|
const accumulators = {};
|
|
@@ -1204,7 +1196,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1204
1196
|
return this.columnOf(meta, key);
|
|
1205
1197
|
}
|
|
1206
1198
|
if (aggregateOf(join.meta.fields[key])) {
|
|
1207
|
-
throw new
|
|
1199
|
+
throw new UqlUsageError(`cannot $group by '${path.join('.')}' on MongoDB: a joined row's relation aggregate is not read`);
|
|
1208
1200
|
}
|
|
1209
1201
|
return `${join.path}.${this.columnOf(join.meta, key)}`;
|
|
1210
1202
|
}
|
|
@@ -1221,14 +1213,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1221
1213
|
const { join, negate } = MongoDialect.GROUP_OPS[key];
|
|
1222
1214
|
const clauses = MongoDialect.groupClauses(key, where[key]).map((clause) => {
|
|
1223
1215
|
if (clause instanceof QueryRaw) {
|
|
1224
|
-
throw new
|
|
1216
|
+
throw new UqlUsageError('raw SQL is not supported in an aggregate $where on MongoDB');
|
|
1225
1217
|
}
|
|
1226
1218
|
return this.whereExpression(meta, clause, named);
|
|
1227
1219
|
});
|
|
1228
1220
|
return negate ? { $not: [{ [join]: clauses }] } : { [join]: clauses };
|
|
1229
1221
|
}
|
|
1230
1222
|
if (key.startsWith('$')) {
|
|
1231
|
-
throw new
|
|
1223
|
+
throw new UqlUsageError(`aggregate $where operator '${key}' is not supported on MongoDB`);
|
|
1232
1224
|
}
|
|
1233
1225
|
const val = where[key];
|
|
1234
1226
|
named.push(key);
|
|
@@ -1271,7 +1263,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1271
1263
|
case '$isNotNull':
|
|
1272
1264
|
return operand ? present : MongoDialect.isNullExpr(ref);
|
|
1273
1265
|
default:
|
|
1274
|
-
throw new
|
|
1266
|
+
throw new UqlUsageError(`aggregate $where operator '${op}' is not supported on MongoDB`);
|
|
1275
1267
|
}
|
|
1276
1268
|
});
|
|
1277
1269
|
return terms.length === 1 ? terms[0] : { $and: terms };
|
|
@@ -1326,12 +1318,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1326
1318
|
const meta = getMeta(entity);
|
|
1327
1319
|
const field = meta.fields[key];
|
|
1328
1320
|
if (!field) {
|
|
1329
|
-
throw new
|
|
1321
|
+
throw new UqlUsageError(`Field '${key}' not found in entity '${meta.name}'`);
|
|
1330
1322
|
}
|
|
1331
1323
|
const colName = this.resolveColumnName(key, field);
|
|
1332
1324
|
const indexName = this.vectorSearchIndexName(findVectorIndex(meta, key)?.name, colName);
|
|
1333
1325
|
if (!limit) {
|
|
1334
|
-
throw new
|
|
1326
|
+
throw new UqlUsageError(`$vectorSearch requires $limit (vector sort on '${key}' of '${meta.name}')`);
|
|
1335
1327
|
}
|
|
1336
1328
|
const stage = {
|
|
1337
1329
|
index: indexName,
|
|
@@ -58,7 +58,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
58
58
|
* for a read and a stream alike, so both load the same relations.
|
|
59
59
|
*/
|
|
60
60
|
readCursor(entity, q, opts) {
|
|
61
|
-
this.dialect.assertNoLock(q);
|
|
62
61
|
const vectorSort = this.dialect.extractVectorSort(q.$sort);
|
|
63
62
|
const pipeline = vectorSort
|
|
64
63
|
? this.buildVectorPipeline(entity, q, vectorSort, opts)
|
|
@@ -69,11 +69,7 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
69
69
|
returningId<E>(meta: EntityMeta<E>): string;
|
|
70
70
|
protected returningIdExpression<E>(meta: EntityMeta<E>): string;
|
|
71
71
|
protected mergeReturning(expression: string): string;
|
|
72
|
-
/**
|
|
73
|
-
* A row lock is a hint on the table here, not a clause at the end of the statement, so
|
|
74
|
-
* {@link lockHint} emits it and this only keeps the guard - see the base declaration.
|
|
75
|
-
*/
|
|
76
|
-
protected appendLock<E>(_ctx: QueryContext, entity: Type<E>, q: Query<E>): void;
|
|
72
|
+
/** A row lock is a hint on the table here, which `rowLocks.placement` says instead of a trailing clause. */
|
|
77
73
|
protected lockHint<E>(q: Query<E>): string;
|
|
78
74
|
/**
|
|
79
75
|
* `N'...'`, always. A bare literal is `VARCHAR`, whose codepage silently destroys anything outside
|
|
@@ -10,6 +10,7 @@ import { parseQueryLock } from '../type/index.js';
|
|
|
10
10
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
11
11
|
import { assertNonNegativeInteger } from '../util/index.js';
|
|
12
12
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
13
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
13
14
|
/** What SQL Server has. */
|
|
14
15
|
const MSSQL_FEATURES = {
|
|
15
16
|
// Neither object takes an `IF NOT EXISTS`; both need a `sys` catalogue lookup around them, which
|
|
@@ -31,10 +32,8 @@ const MSSQL_FEATURES = {
|
|
|
31
32
|
supportsUnsigned: false,
|
|
32
33
|
serverSideCursors: false,
|
|
33
34
|
correlatedWrites: true,
|
|
34
|
-
rowLocks: true,
|
|
35
|
-
rowLockWithWindow: true,
|
|
35
|
+
rowLocks: { of: true, withWindow: true, placement: 'tableHint' },
|
|
36
36
|
nullsOrdering: 'case',
|
|
37
|
-
rowLockOf: true,
|
|
38
37
|
textScoreIndexes: false,
|
|
39
38
|
orderedUpsertReturning: false,
|
|
40
39
|
orderedJsonAggregates: true,
|
|
@@ -164,13 +163,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
164
163
|
mergeReturning(expression) {
|
|
165
164
|
return `OUTPUT ${expression}`;
|
|
166
165
|
}
|
|
167
|
-
/**
|
|
168
|
-
* A row lock is a hint on the table here, not a clause at the end of the statement, so
|
|
169
|
-
* {@link lockHint} emits it and this only keeps the guard - see the base declaration.
|
|
170
|
-
*/
|
|
171
|
-
appendLock(_ctx, entity, q) {
|
|
172
|
-
this.assertLockSupported(entity, q);
|
|
173
|
-
}
|
|
166
|
+
/** A row lock is a hint on the table here, which `rowLocks.placement` says instead of a trailing clause. */
|
|
174
167
|
lockHint(q) {
|
|
175
168
|
const wait = parseQueryLock(q.$lock);
|
|
176
169
|
if (!wait) {
|
|
@@ -324,7 +317,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
324
317
|
jsonSet(ctx, expr, set, _field) {
|
|
325
318
|
for (const [key, value] of Object.entries(set)) {
|
|
326
319
|
if (value === null) {
|
|
327
|
-
throw new
|
|
320
|
+
throw new UqlUsageError(`mssql cannot $set '${key}' to null: JSON_MODIFY deletes the key instead. Use $unset, or store a JSON null through a whole-column write.`);
|
|
328
321
|
}
|
|
329
322
|
}
|
|
330
323
|
return Object.entries(set).reduce((acc, [key, value]) => `JSON_MODIFY(${acc}, ${jsonPath(key)}, ${this.jsonWriteParam(ctx, value)})`, `COALESCE(${expr}, '{}')`);
|
|
@@ -18,7 +18,14 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
18
18
|
protected readonly logger: LoggerWrapper;
|
|
19
19
|
abstract readonly dialect: AbstractDialect;
|
|
20
20
|
constructor(extra?: ExtraOptions | undefined);
|
|
21
|
-
|
|
21
|
+
/** What every read is checked for before it runs, whichever backend runs it. */
|
|
22
|
+
protected validateReadQuery<E extends object>(entity: Type<E>, q: Query<E>): void;
|
|
23
|
+
/**
|
|
24
|
+
* Refuses a `$lock` the engine cannot take, then one outside a transaction, where the lock would
|
|
25
|
+
* drop as the statement commits: only the querier knows whether one is open. Here rather than in
|
|
26
|
+
* each backend's read, so the rule reaches a find, a stream and a paged count alike.
|
|
27
|
+
*/
|
|
28
|
+
private assertLockable;
|
|
22
29
|
private validateProjectionQueryRecursive;
|
|
23
30
|
/** `[entity, query, opts]` from either call form, `(entity, q, opts)` or `({ $entity, ...q }, opts)`. */
|
|
24
31
|
protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, namesKey, relationOf } from '../entity/index.js';
|
|
2
|
+
import { parseQueryLock } from '../type/index.js';
|
|
2
3
|
import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, hasKeys, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
|
-
import {
|
|
4
|
+
import { UqlOptimisticLockError, UqlUsageError } from '../util/uqlError.js';
|
|
5
|
+
import { enrichError } from './queryError.js';
|
|
4
6
|
/**
|
|
5
7
|
* Refuses a nullish id, which would reduce to no filter at all, and a composite id missing a column,
|
|
6
8
|
* which would address every row agreeing on the rest. Callers are `async`, so it always rejects.
|
|
7
9
|
*/
|
|
8
10
|
function assertIdValue(entity, id) {
|
|
9
11
|
if (id === undefined || id === null) {
|
|
10
|
-
throw new
|
|
12
|
+
throw new UqlUsageError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
|
|
11
13
|
}
|
|
12
14
|
if (isScalarId(id)) {
|
|
13
15
|
// One value names one column, which `whereIds` refuses on a composite.
|
|
@@ -20,7 +22,7 @@ function assertIdValue(entity, id) {
|
|
|
20
22
|
const { ids } = getMeta(entity);
|
|
21
23
|
const missing = ids.filter((key) => given[key] == null);
|
|
22
24
|
if (missing.length) {
|
|
23
|
-
throw new
|
|
25
|
+
throw new UqlUsageError(`'${entity.name}' is addressed by an object carrying every key of its primary key (${ids.join(', ')}); missing ${missing.join(', ')}.`);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
/** The column a write matches a parent's sole key against, on the junction or the child. */
|
|
@@ -36,7 +38,7 @@ function assertNamesRows(entity, method, q, opts) {
|
|
|
36
38
|
if (opts?.unfiltered || hasKeys(q?.$where) || q?.$limit !== undefined) {
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
|
-
throw new
|
|
41
|
+
throw new UqlUsageError(`'${method}' over '${entity.name}' names no rows, so it would address every one: pass '{ unfiltered: true }' to mean it`);
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* An optimistic lock as one update applies it: the version the payload carried, the one that replaces
|
|
@@ -46,7 +48,7 @@ function assertNamesRows(entity, method, q, opts) {
|
|
|
46
48
|
function lockVersion(meta, key, q, row) {
|
|
47
49
|
const expected = row[key];
|
|
48
50
|
if (typeof expected !== 'number' && typeof expected !== 'bigint') {
|
|
49
|
-
throw new
|
|
51
|
+
throw new UqlUsageError(`an update of '${entityName(meta)}' carries no '${key}': a versioned row is written against the version it was read at`);
|
|
50
52
|
}
|
|
51
53
|
const next = typeof expected === 'bigint' ? expected + 1n : expected + 1;
|
|
52
54
|
// Spread, as every other added predicate here is: one flat `AND`, and a caller already filtering on
|
|
@@ -60,7 +62,7 @@ function lockVersion(meta, key, q, row) {
|
|
|
60
62
|
*/
|
|
61
63
|
function assertUnversioned(meta, what) {
|
|
62
64
|
if (meta.version) {
|
|
63
|
-
throw new
|
|
65
|
+
throw new UqlUsageError(`cannot ${what} the versioned '${entityName(meta)}': it carries no '${meta.version}' to match, so update it by id`);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -73,7 +75,7 @@ function assertLockableUpdate(meta, q, settles) {
|
|
|
73
75
|
const where = q.$where;
|
|
74
76
|
const namesOneRow = meta.ids.every((key) => where?.[key] !== undefined && isScalarId(where[key]));
|
|
75
77
|
if (!namesOneRow || settles) {
|
|
76
|
-
throw new
|
|
78
|
+
throw new UqlUsageError(`cannot update '${entityName(meta)}' this way: a versioned row is matched and written in one statement, so it is named by its ${meta.ids.map((id) => `'${id}'`).join(', ')}, takes no '$sort', '$limit' or '$skip', writes no relation, and filters by none`);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
/**
|
|
@@ -116,15 +118,31 @@ export class AbstractQuerier {
|
|
|
116
118
|
this.extra = extra;
|
|
117
119
|
this.logger = queryLoggerFor(extra);
|
|
118
120
|
}
|
|
119
|
-
|
|
121
|
+
/** What every read is checked for before it runs, whichever backend runs it. */
|
|
122
|
+
validateReadQuery(entity, q) {
|
|
123
|
+
this.assertLockable(entity, q);
|
|
120
124
|
this.validateProjectionQueryRecursive(entity, q, entityName(getMeta(entity)));
|
|
121
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Refuses a `$lock` the engine cannot take, then one outside a transaction, where the lock would
|
|
128
|
+
* drop as the statement commits: only the querier knows whether one is open. Here rather than in
|
|
129
|
+
* each backend's read, so the rule reaches a find, a stream and a paged count alike.
|
|
130
|
+
*/
|
|
131
|
+
assertLockable(entity, q) {
|
|
132
|
+
if (!parseQueryLock(q.$lock)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.dialect.assertLockSupported(entity, q);
|
|
136
|
+
if (!this.hasOpenTransaction) {
|
|
137
|
+
throw new UqlUsageError('$lock requires an open transaction');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
122
140
|
validateProjectionQueryRecursive(entity, q, path) {
|
|
123
141
|
const meta = getMeta(entity);
|
|
124
142
|
if (q.$select && q.$exclude) {
|
|
125
143
|
for (const [key, value] of Object.entries(q.$select)) {
|
|
126
144
|
if (key in meta.fields && value) {
|
|
127
|
-
throw new
|
|
145
|
+
throw new UqlUsageError(`Cannot combine $select and $exclude when $select includes positive scalar fields (${key}) at ${path}. Use either $select (whitelist) or $exclude (subtractive) in a single query.`);
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
}
|
|
@@ -144,7 +162,7 @@ export class AbstractQuerier {
|
|
|
144
162
|
}
|
|
145
163
|
const q = entityOrQuery;
|
|
146
164
|
if (!q.$entity) {
|
|
147
|
-
throw new
|
|
165
|
+
throw new UqlUsageError('$entity is required when using query-object syntax');
|
|
148
166
|
}
|
|
149
167
|
const { $entity, ...query } = q;
|
|
150
168
|
return [$entity, query, maybeQueryOrOpts];
|
|
@@ -160,7 +178,7 @@ export class AbstractQuerier {
|
|
|
160
178
|
}
|
|
161
179
|
async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
162
180
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
163
|
-
this.
|
|
181
|
+
this.validateReadQuery(entity, q);
|
|
164
182
|
const founds = await this.internalFindMany(entity, q, opts);
|
|
165
183
|
// Guarded here rather than only inside: awaiting a call that returns at once still costs every read
|
|
166
184
|
// a promise and a turn of the microtask queue, and most reads hook nothing.
|
|
@@ -171,12 +189,12 @@ export class AbstractQuerier {
|
|
|
171
189
|
}
|
|
172
190
|
findManyStream(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
173
191
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
174
|
-
this.
|
|
192
|
+
this.validateReadQuery(entity, q);
|
|
175
193
|
return this.internalFindManyStream(entity, q, opts);
|
|
176
194
|
}
|
|
177
195
|
async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
178
196
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
179
|
-
this.
|
|
197
|
+
this.validateReadQuery(entity, q);
|
|
180
198
|
const [founds, count] = await this.internalFindManyAndCount(entity, q, opts);
|
|
181
199
|
if (this.listensForLoad(entity, q.$populate)) {
|
|
182
200
|
await this.emitLoaded(entity, founds, q.$populate);
|
|
@@ -309,7 +327,7 @@ export class AbstractQuerier {
|
|
|
309
327
|
async restoreMany(entity, q) {
|
|
310
328
|
const meta = getMeta(entity);
|
|
311
329
|
if (!meta.softDelete) {
|
|
312
|
-
throw new
|
|
330
|
+
throw new UqlUsageError(`'${entity.name}' has not enabled 'softDelete'`);
|
|
313
331
|
}
|
|
314
332
|
const $where = { ...q.$where, [meta.softDelete]: { $ne: null } };
|
|
315
333
|
// No version: a restore only undoes the stamp a delete left, which takes none either, and two of
|
|
@@ -38,11 +38,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
38
38
|
private query;
|
|
39
39
|
/** Runs a statement the dialect builds. */
|
|
40
40
|
private exec;
|
|
41
|
-
/**
|
|
42
|
-
* Refuses a `$lock` the engine lacks, then one outside a transaction, where the lock would drop as the
|
|
43
|
-
* statement commits; only the querier knows whether one is open.
|
|
44
|
-
*/
|
|
45
|
-
protected assertLockable<E>(entity: Type<E>, q: Query<E>): void;
|
|
46
41
|
/**
|
|
47
42
|
* Runs the `SET`s tuning an ANN index for the query on its connection, refusing where they would apply to
|
|
48
43
|
* nothing: a `SET LOCAL` outside a transaction.
|
|
@@ -3,6 +3,7 @@ import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
|
3
3
|
import { getMeta, namesKey } from '../entity/index.js';
|
|
4
4
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
5
5
|
import { buildUpdateResult, clone, getInsertFieldKeys, insertShapeOf, isAutoIncrement, isRecord, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, } from '../util/index.js';
|
|
6
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
6
7
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
7
8
|
import { streamViaCursor } from './cursorStream.js';
|
|
8
9
|
import { enrichError } from './queryError.js';
|
|
@@ -117,19 +118,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
117
118
|
build(ctx);
|
|
118
119
|
return this.run(ctx.sql, ctx.values);
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Refuses a `$lock` the engine lacks, then one outside a transaction, where the lock would drop as the
|
|
122
|
-
* statement commits; only the querier knows whether one is open.
|
|
123
|
-
*/
|
|
124
|
-
assertLockable(entity, q) {
|
|
125
|
-
if (!q.$lock) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
this.dialect.assertLockSupported(entity, q);
|
|
129
|
-
if (!this.hasOpenTransaction) {
|
|
130
|
-
throw new TypeError('$lock requires an open transaction');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
121
|
/**
|
|
134
122
|
* Runs the `SET`s tuning an ANN index for the query on its connection, refusing where they would apply to
|
|
135
123
|
* nothing: a `SET LOCAL` outside a transaction.
|
|
@@ -143,7 +131,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
143
131
|
return;
|
|
144
132
|
}
|
|
145
133
|
if (this.dialect.features.vectorTuningNeedsTransaction && !this.hasOpenTransaction) {
|
|
146
|
-
throw new
|
|
134
|
+
throw new UqlUsageError(`$candidates requires an open transaction on ${this.dialect.dialectName}; run the query inside pool.transaction(...)`);
|
|
147
135
|
}
|
|
148
136
|
for (const statement of statements) {
|
|
149
137
|
await this.internalRun(statement);
|
|
@@ -164,7 +152,8 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
164
152
|
* so those count apart.
|
|
165
153
|
*/
|
|
166
154
|
async internalFindManyAndCount(entity, q, opts) {
|
|
167
|
-
|
|
155
|
+
const { rowLocks } = this.dialect.features;
|
|
156
|
+
if (q.$distinct || (q.$lock && !(rowLocks && rowLocks.withWindow))) {
|
|
168
157
|
return Promise.all([this.internalFindMany(entity, q, opts), this.countUnpaged(entity, q, opts)]);
|
|
169
158
|
}
|
|
170
159
|
const rows = await this.selectRows(entity, q, opts, TOTAL_ALIAS);
|
|
@@ -175,7 +164,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
175
164
|
return [this.hydrateRows(entity, rows), total];
|
|
176
165
|
}
|
|
177
166
|
async selectRows(entity, q, opts, totalAlias) {
|
|
178
|
-
this.assertLockable(entity, q);
|
|
179
167
|
// Guarded rather than awaited unconditionally, here and in the stream below: an `await` on this
|
|
180
168
|
// path defers a microtask on every read, which reorders the two statements `findManyAndCount`
|
|
181
169
|
// issues concurrently. Keep the guard at any new call site.
|
|
@@ -190,7 +178,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
190
178
|
return founds;
|
|
191
179
|
}
|
|
192
180
|
async *internalFindManyStream(entity, q, opts) {
|
|
193
|
-
this.assertLockable(entity, q);
|
|
194
181
|
// Guarded for the reason `selectRows` above spells out.
|
|
195
182
|
if (q.$candidates !== undefined) {
|
|
196
183
|
await this.applyVectorTuning(entity, q);
|