uql-orm 0.79.0 → 0.81.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.map +2 -2
- package/dist/cockroachdb/cockroachDialect.js +5 -1
- package/dist/dialect/abstractDialect.d.ts +1 -31
- package/dist/dialect/abstractDialect.js +3 -27
- package/dist/dialect/abstractSqlDialect.d.ts +25 -56
- package/dist/dialect/abstractSqlDialect.js +78 -145
- package/dist/dialect/aliases.d.ts +5 -0
- package/dist/dialect/aliases.js +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
- package/dist/dialect/operators.d.ts +66 -0
- package/dist/dialect/operators.js +129 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
- package/dist/dialect/pgLikeSqlDialect.js +16 -3
- package/dist/dialect/vectorSqlDialect.d.ts +2 -0
- package/dist/dialect/vectorSqlDialect.js +4 -0
- package/dist/entity/decorator/entity.d.ts +6 -1
- package/dist/entity/decorator/entity.js +12 -1
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +6 -1
- package/dist/entity/metadata/definition.js +19 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +24 -0
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/codegen/entityTypes.js +1 -2
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
- package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
- package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
- package/dist/migrate/ddl/pgIndexDdl.js +8 -6
- package/dist/migrate/ddl/tableDdl.d.ts +5 -2
- package/dist/migrate/ddl/tableDdl.js +13 -7
- package/dist/migrate/drift/driftDetector.d.ts +4 -5
- package/dist/migrate/drift/driftDetector.js +21 -21
- package/dist/migrate/generator/definitionToNode.d.ts +1 -1
- package/dist/migrate/generator/definitionToNode.js +9 -20
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
- package/dist/migrate/index.d.ts +2 -1
- package/dist/migrate/index.js +1 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
- package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
- package/dist/migrate/introspection/mongoIntrospector.js +7 -3
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
- package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
- package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
- package/dist/migrate/introspection/postgresIntrospector.js +70 -43
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
- package/dist/migrate/migrator.d.ts +33 -1
- package/dist/migrate/migrator.js +111 -43
- package/dist/migrate/schemaChange.d.ts +18 -0
- package/dist/migrate/schemaChange.js +37 -0
- package/dist/migrate/schemaGenerator.d.ts +25 -15
- package/dist/migrate/schemaGenerator.js +130 -182
- package/dist/migrate/storage/databaseStorage.d.ts +4 -0
- package/dist/migrate/storage/databaseStorage.js +14 -8
- package/dist/migrate/triggerSql.d.ts +24 -0
- package/dist/migrate/triggerSql.js +229 -0
- package/dist/mongo/mongoDialect.d.ts +0 -21
- package/dist/mongo/mongoDialect.js +105 -100
- package/dist/mongo/mongodbQuerier.js +17 -1
- package/dist/mssql/mssqlDialect.d.ts +18 -7
- package/dist/mssql/mssqlDialect.js +77 -33
- package/dist/mssql/mssqlQuerier.js +2 -2
- package/dist/schema/canonicalType.d.ts +6 -1
- package/dist/schema/canonicalType.js +14 -0
- package/dist/schema/indexDifferences.d.ts +22 -6
- package/dist/schema/indexDifferences.js +23 -8
- package/dist/schema/matchByKey.d.ts +10 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.d.ts +6 -2
- package/dist/schema/schemaAST.js +7 -3
- package/dist/schema/schemaASTBuilder.d.ts +4 -8
- package/dist/schema/schemaASTBuilder.js +20 -29
- package/dist/schema/schemaASTDiffer.d.ts +2 -3
- package/dist/schema/schemaASTDiffer.js +15 -36
- package/dist/schema/types.d.ts +14 -15
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +12 -3
- package/dist/type/dialect.d.ts +69 -9
- package/dist/type/entity.d.ts +96 -3
- package/dist/type/migration.d.ts +43 -44
- package/dist/type/query.d.ts +13 -4
- package/dist/type/queryWhere.d.ts +4 -2
- package/dist/util/ddlExpression.util.d.ts +5 -1
- package/dist/util/ddlExpression.util.js +6 -2
- package/dist/util/field.util.d.ts +9 -1
- package/dist/util/field.util.js +14 -2
- package/dist/util/fieldOption.util.d.ts +2 -2
- package/dist/util/fieldOption.util.js +2 -2
- package/dist/util/raw.d.ts +9 -1
- package/dist/util/raw.js +36 -11
- package/dist/util/sql.util.d.ts +12 -0
- package/dist/util/sql.util.js +24 -3
- package/dist/util/uqlError.d.ts +2 -0
- package/dist/util/uqlError.js +4 -0
- package/package.json +4 -4
- package/skills/uql-orm/SKILL.md +11 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
|
|
4
|
+
import { GROUP_OPS, groupClauses, isGroupOp } from '../dialect/operators.js';
|
|
4
5
|
import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
5
6
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
7
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
@@ -15,7 +16,6 @@ const EMPTY_SORT_PLAN = { sort: {}, stages: [], fields: [] };
|
|
|
15
16
|
const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
|
|
16
17
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
17
18
|
export const mongoDialectFeatures = {
|
|
18
|
-
ifNotExists: false,
|
|
19
19
|
indexIfNotExists: false,
|
|
20
20
|
schemas: false, // the connection picks the database, and a collection name takes no dot
|
|
21
21
|
dropTableCascade: false,
|
|
@@ -39,29 +39,93 @@ const HEX_24 = /^[0-9a-f]{24}$/i;
|
|
|
39
39
|
function declaredTypeName(type) {
|
|
40
40
|
return typeof type === 'function' ? type.name : String(type);
|
|
41
41
|
}
|
|
42
|
+
const ID_KEY = '_id';
|
|
43
|
+
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
44
|
+
const MAX_NUM_CANDIDATES = 10_000;
|
|
45
|
+
/** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
|
|
46
|
+
function andExpr(filter, expr) {
|
|
47
|
+
filter['$expr'] = filter['$expr'] ? { $and: [filter['$expr'], expr] } : expr;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
|
|
51
|
+
* in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
|
|
52
|
+
* empty array in place of any other value.
|
|
53
|
+
*/
|
|
54
|
+
function arraySize(path, size) {
|
|
55
|
+
const value = `$${path}`;
|
|
56
|
+
const count = { $size: { $cond: [{ $isArray: value }, value, []] } };
|
|
57
|
+
return { $and: [{ $isArray: value }, compareCount(count, size)] };
|
|
58
|
+
}
|
|
59
|
+
/** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
|
|
60
|
+
function compareCount(count, size) {
|
|
61
|
+
if (typeof size === 'number') {
|
|
62
|
+
return { $eq: [count, size] };
|
|
63
|
+
}
|
|
64
|
+
const comparisons = Object.entries(size)
|
|
65
|
+
.filter(([, bound]) => bound !== undefined)
|
|
66
|
+
.flatMap(([op, bound]) => op === '$between' && Array.isArray(bound)
|
|
67
|
+
? [{ $gte: [count, bound[0]] }, { $lte: [count, bound[1]] }]
|
|
68
|
+
: [{ [op]: [count, bound] }]);
|
|
69
|
+
if (!comparisons.length) {
|
|
70
|
+
throw new UqlUsageError('$size needs at least one comparison');
|
|
71
|
+
}
|
|
72
|
+
return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
|
|
73
|
+
}
|
|
74
|
+
/** String operators -> { pattern: (v) => regex, caseInsensitive } */
|
|
75
|
+
const REGEX_OP_MAP = new Map([
|
|
76
|
+
['$startsWith', { wrap: (v) => `^${v}`, ci: false }],
|
|
77
|
+
['$istartsWith', { wrap: (v) => `^${v}`, ci: true }],
|
|
78
|
+
['$endsWith', { wrap: (v) => `${v}$`, ci: false }],
|
|
79
|
+
['$iendsWith', { wrap: (v) => `${v}$`, ci: true }],
|
|
80
|
+
['$includes', { wrap: (v) => String(v), ci: false }],
|
|
81
|
+
['$iincludes', { wrap: (v) => String(v), ci: true }],
|
|
82
|
+
['$like', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: false }],
|
|
83
|
+
['$ilike', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: true }],
|
|
84
|
+
]);
|
|
85
|
+
/** MongoDB native operators - pass through as-is. */
|
|
86
|
+
const NATIVE_OPS = new Set([
|
|
87
|
+
'$all',
|
|
88
|
+
'$size',
|
|
89
|
+
'$elemMatch',
|
|
90
|
+
'$eq',
|
|
91
|
+
'$ne',
|
|
92
|
+
'$lt',
|
|
93
|
+
'$lte',
|
|
94
|
+
'$gt',
|
|
95
|
+
'$gte',
|
|
96
|
+
'$in',
|
|
97
|
+
'$nin',
|
|
98
|
+
'$regex',
|
|
99
|
+
'$not',
|
|
100
|
+
]);
|
|
101
|
+
/** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
|
|
102
|
+
function countOf(ref) {
|
|
103
|
+
return { $cond: [isNullExpr(ref), 0, 1] };
|
|
104
|
+
}
|
|
105
|
+
/** Whether `ref` is null or missing: an expression compares a missing field as neither. */
|
|
106
|
+
function isNullExpr(ref) {
|
|
107
|
+
return { $eq: [{ $ifNull: [ref, null] }, null] };
|
|
108
|
+
}
|
|
42
109
|
export class MongoDialect extends AbstractDialect {
|
|
43
110
|
features = mongoDialectFeatures;
|
|
44
111
|
dialectName = 'mongodb';
|
|
45
112
|
// The MongoDB driver reports the exact `_id` of every inserted document (`insertedIds`).
|
|
46
113
|
insertIdSource = 'returning';
|
|
47
|
-
static ID_KEY = '_id';
|
|
48
|
-
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
49
|
-
static MAX_NUM_CANDIDATES = 10_000;
|
|
50
114
|
/**
|
|
51
115
|
* MongoDB stores the primary key as `_id`; everything else resolves as usual. Projections, sorts,
|
|
52
116
|
* `$group` refs and `$where` keys all map through here, so no read path can address a property name
|
|
53
117
|
* the document does not use.
|
|
54
118
|
*/
|
|
55
119
|
columnOf(meta, key) {
|
|
56
|
-
if (key ===
|
|
57
|
-
return
|
|
120
|
+
if (key === ID_KEY) {
|
|
121
|
+
return ID_KEY;
|
|
58
122
|
}
|
|
59
123
|
if (meta.fields[key]?.isId) {
|
|
60
124
|
// A document has one `_id`. A composite key is a different document shape - a sub-document,
|
|
61
125
|
// whose field order decides equality - rather than a translation, so it is refused here, which
|
|
62
126
|
// every read reaches, and in `getPersistables`, which every write reaches.
|
|
63
127
|
assertSoleId(meta, 'MongoDB');
|
|
64
|
-
return
|
|
128
|
+
return ID_KEY;
|
|
65
129
|
}
|
|
66
130
|
return super.columnOf(meta, key);
|
|
67
131
|
}
|
|
@@ -99,7 +163,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
99
163
|
for (const [rawKey, rawVal] of Object.entries(where)) {
|
|
100
164
|
let key = rawKey;
|
|
101
165
|
let val = rawVal;
|
|
102
|
-
if (
|
|
166
|
+
if (isGroupOp(key)) {
|
|
103
167
|
this.appendLogicalOperator(filter, entity, key, val, lookups);
|
|
104
168
|
}
|
|
105
169
|
else if (key === '$text') {
|
|
@@ -126,7 +190,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
126
190
|
}
|
|
127
191
|
const isReference = !!meta.fields[key]?.references;
|
|
128
192
|
key = this.pathOf(meta, key);
|
|
129
|
-
if ((key ===
|
|
193
|
+
if ((key === ID_KEY || isReference) && !isOperatorObject(val)) {
|
|
130
194
|
val = this.toWireId(val);
|
|
131
195
|
}
|
|
132
196
|
if (!isOperatorObject(val)) {
|
|
@@ -139,7 +203,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
139
203
|
filter[key] = this.transformOperators(val);
|
|
140
204
|
}
|
|
141
205
|
else {
|
|
142
|
-
|
|
206
|
+
andExpr(filter, arraySize(key, size));
|
|
143
207
|
if (hasKeys(ops)) {
|
|
144
208
|
filter[key] = this.transformOperators(ops);
|
|
145
209
|
}
|
|
@@ -153,8 +217,8 @@ export class MongoDialect extends AbstractDialect {
|
|
|
153
217
|
* wrapped in one `$and`), dropping empty clauses, since MongoDB refuses an empty operator.
|
|
154
218
|
*/
|
|
155
219
|
appendLogicalOperator(filter, entity, key, val, lookups) {
|
|
156
|
-
const { join, negate } =
|
|
157
|
-
const parts =
|
|
220
|
+
const { join, negate } = GROUP_OPS[key];
|
|
221
|
+
const parts = groupClauses(key, val)
|
|
158
222
|
.map((filterIt) => {
|
|
159
223
|
this.assertNoRaw(filterIt);
|
|
160
224
|
return this.renderFilter(entity, filterIt, lookups);
|
|
@@ -187,23 +251,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
187
251
|
filter[`${temp}.0`] = { $exists: true };
|
|
188
252
|
}
|
|
189
253
|
else {
|
|
190
|
-
|
|
254
|
+
andExpr(filter, compareCount(this.tally(temp), sizeVal));
|
|
191
255
|
}
|
|
192
256
|
}
|
|
193
|
-
/** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
|
|
194
|
-
static andExpr(filter, expr) {
|
|
195
|
-
filter['$expr'] = filter['$expr'] ? { $and: [filter['$expr'], expr] } : expr;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
|
|
199
|
-
* in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
|
|
200
|
-
* empty array in place of any other value.
|
|
201
|
-
*/
|
|
202
|
-
static arraySize(path, size) {
|
|
203
|
-
const value = `$${path}`;
|
|
204
|
-
const count = { $size: { $cond: [{ $isArray: value }, value, []] } };
|
|
205
|
-
return { $and: [{ $isArray: value }, MongoDialect.compareCount(count, size)] };
|
|
206
|
-
}
|
|
207
257
|
/**
|
|
208
258
|
* The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
|
|
209
259
|
* the target, or for a many-to-many from inside its junction's rows. The caller's filter bypass is not
|
|
@@ -226,7 +276,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
226
276
|
$lookup: {
|
|
227
277
|
from,
|
|
228
278
|
localField: junction.target,
|
|
229
|
-
foreignField:
|
|
279
|
+
foreignField: ID_KEY,
|
|
230
280
|
pipeline: [...targetMatch, { $limit: 1 }],
|
|
231
281
|
as: REL_NESTED_KEY,
|
|
232
282
|
},
|
|
@@ -254,28 +304,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
254
304
|
return {
|
|
255
305
|
lookup: {
|
|
256
306
|
from: this.resolveTableName(throughMeta),
|
|
257
|
-
localField:
|
|
307
|
+
localField: ID_KEY,
|
|
258
308
|
foreignField: this.columnOf(throughMeta, parentJoin.joined),
|
|
259
309
|
},
|
|
260
310
|
scope: hasKeys(scope) ? [{ $match: scope }] : [],
|
|
261
311
|
target: this.columnOf(throughMeta, targetColumn),
|
|
262
312
|
};
|
|
263
313
|
}
|
|
264
|
-
/** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
|
|
265
|
-
static compareCount(count, size) {
|
|
266
|
-
if (typeof size === 'number') {
|
|
267
|
-
return { $eq: [count, size] };
|
|
268
|
-
}
|
|
269
|
-
const comparisons = Object.entries(size)
|
|
270
|
-
.filter(([, bound]) => bound !== undefined)
|
|
271
|
-
.flatMap(([op, bound]) => op === '$between' && Array.isArray(bound)
|
|
272
|
-
? [{ $gte: [count, bound[0]] }, { $lte: [count, bound[1]] }]
|
|
273
|
-
: [{ [op]: [count, bound] }]);
|
|
274
|
-
if (!comparisons.length) {
|
|
275
|
-
throw new UqlUsageError('$size needs at least one comparison');
|
|
276
|
-
}
|
|
277
|
-
return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
|
|
278
|
-
}
|
|
279
314
|
/** Whether a query subtracts `key` from the projection, via `$exclude` or a negative `$select`. */
|
|
280
315
|
subtractsKey(key, select, exclude) {
|
|
281
316
|
const at = (map) => map?.[key];
|
|
@@ -294,38 +329,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
294
329
|
*/
|
|
295
330
|
assertKnownPathRoot(meta, key) {
|
|
296
331
|
const root = key.includes('.') ? key.slice(0, key.indexOf('.')) : key;
|
|
297
|
-
if (root ===
|
|
332
|
+
if (root === ID_KEY || meta.fields[root]) {
|
|
298
333
|
return;
|
|
299
334
|
}
|
|
300
335
|
throw new UqlUsageError(`path ${key} does not exist in ${entityName(meta)}`);
|
|
301
336
|
}
|
|
302
|
-
/** String operators -> { pattern: (v) => regex, caseInsensitive } */
|
|
303
|
-
static REGEX_OP_MAP = new Map([
|
|
304
|
-
['$startsWith', { wrap: (v) => `^${v}`, ci: false }],
|
|
305
|
-
['$istartsWith', { wrap: (v) => `^${v}`, ci: true }],
|
|
306
|
-
['$endsWith', { wrap: (v) => `${v}$`, ci: false }],
|
|
307
|
-
['$iendsWith', { wrap: (v) => `${v}$`, ci: true }],
|
|
308
|
-
['$includes', { wrap: (v) => String(v), ci: false }],
|
|
309
|
-
['$iincludes', { wrap: (v) => String(v), ci: true }],
|
|
310
|
-
['$like', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: false }],
|
|
311
|
-
['$ilike', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: true }],
|
|
312
|
-
]);
|
|
313
|
-
/** MongoDB native operators - pass through as-is. */
|
|
314
|
-
static NATIVE_OPS = new Set([
|
|
315
|
-
'$all',
|
|
316
|
-
'$size',
|
|
317
|
-
'$elemMatch',
|
|
318
|
-
'$eq',
|
|
319
|
-
'$ne',
|
|
320
|
-
'$lt',
|
|
321
|
-
'$lte',
|
|
322
|
-
'$gt',
|
|
323
|
-
'$gte',
|
|
324
|
-
'$in',
|
|
325
|
-
'$nin',
|
|
326
|
-
'$regex',
|
|
327
|
-
'$not',
|
|
328
|
-
]);
|
|
329
337
|
/**
|
|
330
338
|
* Transform UQL operators to MongoDB operators.
|
|
331
339
|
*/
|
|
@@ -339,6 +347,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
339
347
|
result[op] = this.held(val);
|
|
340
348
|
continue;
|
|
341
349
|
}
|
|
350
|
+
// `$not` wraps a condition too, so a uql-only operator inside it (`$isNull`, `$startsWith`) is mapped.
|
|
351
|
+
if (op === '$not' && isOperatorObject(val)) {
|
|
352
|
+
result[op] = this.transformOperators(val);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
342
355
|
// An object or an array is matched by what it holds, as the SQL engines read it, where native `$all`
|
|
343
356
|
// compares the whole element. MongoDB takes `$elemMatch` there only when every value is one.
|
|
344
357
|
if (op === '$all' && Array.isArray(val) && val.some((value) => Array.isArray(value) || isOperatorMap(value))) {
|
|
@@ -346,12 +359,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
346
359
|
continue;
|
|
347
360
|
}
|
|
348
361
|
// Native MongoDB operators - pass through directly
|
|
349
|
-
if (
|
|
362
|
+
if (NATIVE_OPS.has(op)) {
|
|
350
363
|
result[op] = val;
|
|
351
364
|
continue;
|
|
352
365
|
}
|
|
353
366
|
// String/pattern -> regex operators (8 variants including $like/$ilike)
|
|
354
|
-
const regexEntry =
|
|
367
|
+
const regexEntry = REGEX_OP_MAP.get(op);
|
|
355
368
|
if (regexEntry) {
|
|
356
369
|
result['$regex'] = regexEntry.wrap(val);
|
|
357
370
|
if (regexEntry.ci)
|
|
@@ -443,7 +456,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
443
456
|
// `_id: 0` - the one inclusion/exclusion mix MongoDB allows - or `$exclude: { id: true }` would
|
|
444
457
|
// have no effect at all.
|
|
445
458
|
if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), selectMap, exclude)) {
|
|
446
|
-
projection[
|
|
459
|
+
projection[ID_KEY] = 0;
|
|
447
460
|
}
|
|
448
461
|
return projection;
|
|
449
462
|
}
|
|
@@ -669,7 +682,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
669
682
|
$lookup: {
|
|
670
683
|
from,
|
|
671
684
|
localField: `${temp}.${junction.target}`,
|
|
672
|
-
foreignField:
|
|
685
|
+
foreignField: ID_KEY,
|
|
673
686
|
...pipeline,
|
|
674
687
|
as: relKey,
|
|
675
688
|
},
|
|
@@ -846,7 +859,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
846
859
|
const relationProjection = this.pipelineProjection(join.entity, join.query);
|
|
847
860
|
// MongoDB returns `_id` unless a projection subtracts it, so dropping the key from the map is
|
|
848
861
|
// how a joined document keeps its own id, as it does on the SQL dialects.
|
|
849
|
-
delete relationProjection?.[
|
|
862
|
+
delete relationProjection?.[ID_KEY];
|
|
850
863
|
const lookupPipeline = [
|
|
851
864
|
...(hasKeys(relationFilter) ? [{ $match: relationFilter }] : []),
|
|
852
865
|
...this.lookupStages(join.meta, joins, join),
|
|
@@ -879,9 +892,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
879
892
|
// rows of every key that agrees on it. The other branch is refused by `columnOf` below, whose
|
|
880
893
|
// key *is* an id column; this one names a plain foreign key, so it has to say so itself.
|
|
881
894
|
assertSoleId(relMeta, 'MongoDB');
|
|
882
|
-
return { localField: this.columnOf(meta, relOpts.references[0].local), foreignField:
|
|
895
|
+
return { localField: this.columnOf(meta, relOpts.references[0].local), foreignField: ID_KEY };
|
|
883
896
|
}
|
|
884
|
-
return { localField:
|
|
897
|
+
return { localField: ID_KEY, foreignField: this.columnOf(relMeta, relOpts.references[0].foreign) };
|
|
885
898
|
}
|
|
886
899
|
/** `[column, key]` for the fields whose stored name differs from their property name, memoized per entity. */
|
|
887
900
|
renamedColumns(meta) {
|
|
@@ -914,7 +927,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
914
927
|
return doc;
|
|
915
928
|
}
|
|
916
929
|
const res = doc;
|
|
917
|
-
const _id =
|
|
930
|
+
const _id = ID_KEY;
|
|
918
931
|
// `!== undefined`, not truthiness: `0` is a key MongoDB accepts and a truthy test dropped it.
|
|
919
932
|
if (res[_id] !== undefined) {
|
|
920
933
|
const idKey = soleIdOf(meta, 'MongoDB');
|
|
@@ -1079,7 +1092,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1079
1092
|
// Nothing named the key, so the database is being asked to mint one.
|
|
1080
1093
|
this.assertMintableKey(meta, fieldOf(meta, idKey));
|
|
1081
1094
|
}
|
|
1082
|
-
const doc = named ? { [
|
|
1095
|
+
const doc = named ? { [ID_KEY]: this.toWireId(it[idKey]) } : {};
|
|
1083
1096
|
for (const key of filterFieldKeys(meta, it, callbackKey)) {
|
|
1084
1097
|
if (key === idKey)
|
|
1085
1098
|
continue;
|
|
@@ -1166,7 +1179,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1166
1179
|
}
|
|
1167
1180
|
else if (entry.op === '$count') {
|
|
1168
1181
|
// COUNT(field) counts the non-null values, as SQL does.
|
|
1169
|
-
accumulators[entry.alias] = { $sum: read(
|
|
1182
|
+
accumulators[entry.alias] = { $sum: read(countOf(ref), 0) };
|
|
1170
1183
|
}
|
|
1171
1184
|
else {
|
|
1172
1185
|
// `$sum`, `$avg`, `$min` and `$max` are MongoDB accumulators of the same name, and skip a null.
|
|
@@ -1174,20 +1187,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1174
1187
|
}
|
|
1175
1188
|
if (entry.op === '$sum' && !entry.distinct) {
|
|
1176
1189
|
const counted = `${SUM_COUNT_ALIAS}_${entry.alias}`;
|
|
1177
|
-
accumulators[counted] = { $sum: read(
|
|
1190
|
+
accumulators[counted] = { $sum: read(countOf(ref), 0) };
|
|
1178
1191
|
columns[entry.alias] = { $cond: [{ $eq: [`$${counted}`, 0] }, null, `$${entry.alias}`] };
|
|
1179
1192
|
}
|
|
1180
1193
|
}
|
|
1181
1194
|
return { groupId, accumulators, columns, named };
|
|
1182
1195
|
}
|
|
1183
|
-
/** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
|
|
1184
|
-
static countOf(ref) {
|
|
1185
|
-
return { $cond: [MongoDialect.isNullExpr(ref), 0, 1] };
|
|
1186
|
-
}
|
|
1187
|
-
/** Whether `ref` is null or missing: an expression compares a missing field as neither. */
|
|
1188
|
-
static isNullExpr(ref) {
|
|
1189
|
-
return { $eq: [{ $ifNull: [ref, null] }, null] };
|
|
1190
|
-
}
|
|
1191
1196
|
/** The field a grouped path reads, on the document or on the joined one its lookup unwound. */
|
|
1192
1197
|
groupedPath(meta, joins, path, named) {
|
|
1193
1198
|
const { key, join } = groupPathField(joins, path);
|
|
@@ -1209,9 +1214,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1209
1214
|
const terms = getKeys(where)
|
|
1210
1215
|
.filter((key) => where[key] !== undefined)
|
|
1211
1216
|
.map((key) => {
|
|
1212
|
-
if (
|
|
1213
|
-
const { join, negate } =
|
|
1214
|
-
const clauses =
|
|
1217
|
+
if (isGroupOp(key)) {
|
|
1218
|
+
const { join, negate } = GROUP_OPS[key];
|
|
1219
|
+
const clauses = groupClauses(key, where[key]).map((clause) => {
|
|
1215
1220
|
if (clause instanceof QueryRaw) {
|
|
1216
1221
|
throw new UqlUsageError('raw SQL is not supported in an aggregate $where on MongoDB');
|
|
1217
1222
|
}
|
|
@@ -1225,19 +1230,19 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1225
1230
|
const val = where[key];
|
|
1226
1231
|
named.push(key);
|
|
1227
1232
|
const path = this.pathOf(meta, key);
|
|
1228
|
-
const wire = (value) => path ===
|
|
1233
|
+
const wire = (value) => path === ID_KEY || meta.fields[key]?.references ? this.toWireId(value) : value;
|
|
1229
1234
|
return this.fieldExpression(`$${path}`, val, wire);
|
|
1230
1235
|
});
|
|
1231
1236
|
return terms.length === 1 ? terms[0] : { $and: terms };
|
|
1232
1237
|
}
|
|
1233
1238
|
/** One field's condition as an expression: a value it equals, a list it is in, or a map of comparisons. */
|
|
1234
1239
|
fieldExpression(ref, val, wire) {
|
|
1235
|
-
const equals = (value) => (value === null ?
|
|
1240
|
+
const equals = (value) => (value === null ? isNullExpr(ref) : { $eq: [ref, wire(value)] });
|
|
1236
1241
|
if (!isOperatorMap(val)) {
|
|
1237
1242
|
return Array.isArray(val) ? { $in: [ref, wire(val)] } : equals(val);
|
|
1238
1243
|
}
|
|
1239
1244
|
// A null or missing field compares below every value in an expression, where SQL leaves it unmatched.
|
|
1240
|
-
const present = { $not: [
|
|
1245
|
+
const present = { $not: [isNullExpr(ref)] };
|
|
1241
1246
|
const terms = Object.entries(val).map(([op, operand]) => {
|
|
1242
1247
|
switch (op) {
|
|
1243
1248
|
case '$eq':
|
|
@@ -1259,9 +1264,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1259
1264
|
return { $and: [{ $gte: [ref, wire(min)] }, { $lte: [ref, wire(max)] }] };
|
|
1260
1265
|
}
|
|
1261
1266
|
case '$isNull':
|
|
1262
|
-
return operand ?
|
|
1267
|
+
return operand ? isNullExpr(ref) : present;
|
|
1263
1268
|
case '$isNotNull':
|
|
1264
|
-
return operand ? present :
|
|
1269
|
+
return operand ? present : isNullExpr(ref);
|
|
1265
1270
|
default:
|
|
1266
1271
|
throw new UqlUsageError(`aggregate $where operator '${op}' is not supported on MongoDB`);
|
|
1267
1272
|
}
|
|
@@ -1331,7 +1336,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1331
1336
|
queryVector: [...search.$vector],
|
|
1332
1337
|
// `$candidates` is the caller's own budget; the fallback is 10x the limit, which is what Atlas
|
|
1333
1338
|
// suggests as a floor. Either way it is clamped: Atlas rejects a stage asking for more.
|
|
1334
|
-
numCandidates: Math.min(candidates ?? limit * 10,
|
|
1339
|
+
numCandidates: Math.min(candidates ?? limit * 10, MAX_NUM_CANDIDATES),
|
|
1335
1340
|
limit,
|
|
1336
1341
|
};
|
|
1337
1342
|
// Pre-filter: merge $where into $vectorSearch.filter
|
|
@@ -2,7 +2,8 @@ import { AGGREGATE_VALUE_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { fieldOf, getMeta, namesKey, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, textSortOf, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { clone, getKeys, getSoftDeleteValue, hasKeys, hasTriggers, populatesRelations, textSortOf, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
6
7
|
/**
|
|
7
8
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
9
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -11,6 +12,16 @@ import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, textSo
|
|
|
11
12
|
function asksForNoRows(q) {
|
|
12
13
|
return q.$limit === 0;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* MongoDB has no triggers, so a write to an entity declaring one - a stamp included - would skip it
|
|
17
|
+
* silently. Refused instead, as a query naming SQL is.
|
|
18
|
+
*/
|
|
19
|
+
function refuseTriggers(entity) {
|
|
20
|
+
if (hasTriggers(getMeta(entity))) {
|
|
21
|
+
throw new UqlUsageError(`'${entity.name}' declares triggers, which MongoDB has none of: a write here would skip them. ` +
|
|
22
|
+
'Keep the entity on a SQL engine, or drop its triggers and stamps.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
14
25
|
export class MongodbQuerier extends AbstractQuerier {
|
|
15
26
|
dialect;
|
|
16
27
|
conn;
|
|
@@ -173,6 +184,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
173
184
|
return this.timed('estimatedCount', undefined, async () => this.execute((session) => this.collection(entity).estimatedDocumentCount({ session })));
|
|
174
185
|
}
|
|
175
186
|
async internalInsertMany(entity, rows) {
|
|
187
|
+
refuseTriggers(entity);
|
|
176
188
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
177
189
|
const meta = getMeta(entity);
|
|
178
190
|
const persistables = this.dialect.getPersistables(meta, rows, 'onInsert');
|
|
@@ -186,6 +198,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
186
198
|
});
|
|
187
199
|
}
|
|
188
200
|
async internalUpdateMany(entity, qm, payload, opts) {
|
|
201
|
+
refuseTriggers(entity);
|
|
189
202
|
return this.timed('internalUpdateMany', undefined, async () => {
|
|
190
203
|
const persistable = this.dialect.getPersistable(getMeta(entity), payload, 'onUpdate');
|
|
191
204
|
const filter = this.dialect.where(entity, qm.$where, opts);
|
|
@@ -217,6 +230,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
217
230
|
return this.dialect.where(entity, where);
|
|
218
231
|
}
|
|
219
232
|
async internalUpsertOne(entity, conflictPaths, payload) {
|
|
233
|
+
refuseTriggers(entity);
|
|
220
234
|
return this.timed('upsertOne', undefined, async () => {
|
|
221
235
|
payload = clone(payload);
|
|
222
236
|
const meta = getMeta(entity);
|
|
@@ -237,6 +251,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
237
251
|
});
|
|
238
252
|
}
|
|
239
253
|
async internalUpsertMany(entity, conflictPaths, payload) {
|
|
254
|
+
refuseTriggers(entity);
|
|
240
255
|
return this.timed('upsertMany', undefined, async () => {
|
|
241
256
|
if (!payload?.length) {
|
|
242
257
|
return { changes: 0 };
|
|
@@ -271,6 +286,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
271
286
|
});
|
|
272
287
|
}
|
|
273
288
|
async internalDeleteMany(entity, qm, opts = {}) {
|
|
289
|
+
refuseTriggers(entity);
|
|
274
290
|
return this.timed('internalDeleteMany', undefined, async () => {
|
|
275
291
|
const meta = getMeta(entity);
|
|
276
292
|
// Soft-delete (stamp) unless `hardDelete` is requested or the entity has no soft-delete field.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
2
2
|
import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
|
|
3
3
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
4
|
-
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
4
|
+
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
5
5
|
/** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
|
|
6
6
|
export declare class MsSqlDialect extends MergeSqlDialect {
|
|
7
7
|
#private;
|
|
@@ -23,6 +23,8 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
23
23
|
readonly maxBindValues = 2100;
|
|
24
24
|
/** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
|
|
25
25
|
readonly returningPosition = "after-target";
|
|
26
|
+
/** `OUTPUT` reads the written row off the `INSERTED` pseudo-table. */
|
|
27
|
+
protected readonly returnedRowPrefix = "INSERTED.";
|
|
26
28
|
readonly insertIdSource: InsertIdSource;
|
|
27
29
|
/** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
|
|
28
30
|
protected readonly mergeTargetHint = " WITH (HOLDLOCK)";
|
|
@@ -39,10 +41,6 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
39
41
|
pager(ctx: QueryContext, opts: QueryPager & {
|
|
40
42
|
$distinct?: boolean;
|
|
41
43
|
}, sorted?: boolean): void;
|
|
42
|
-
/**
|
|
43
|
-
* `SET IDENTITY_INSERT` around an insert that states a key the engine would generate, which it otherwise
|
|
44
|
-
* refuses; turned off in the same batch, since one table per session may hold it.
|
|
45
|
-
*/
|
|
46
44
|
insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
|
|
47
45
|
/** The table to toggle, or nothing when no record writes a key the engine would have generated. */
|
|
48
46
|
private identityInsertTarget;
|
|
@@ -65,9 +63,20 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
65
63
|
};
|
|
66
64
|
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
67
65
|
placeholder(index: number): string;
|
|
68
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it, and
|
|
68
|
+
* always `INTO` a table variable: SQL Server refuses an `OUTPUT` straight back to the client on a table
|
|
69
|
+
* carrying a trigger, and through a table it works whether one is there or not.
|
|
70
|
+
*/
|
|
69
71
|
returningId<E>(meta: EntityMeta<E>): string;
|
|
70
|
-
|
|
72
|
+
upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* A statement writing rows, `write`, wrapped in what SQL Server needs around one. The table variable its
|
|
75
|
+
* `OUTPUT` fills, typed as the id column is, and the read handing that back, uncounted so the batch
|
|
76
|
+
* reports the rows written alone; none on a composite key, which has no id to hand back. And `SET
|
|
77
|
+
* IDENTITY_INSERT` where a row states a key the engine would generate, turned off in the same batch.
|
|
78
|
+
*/
|
|
79
|
+
private writeRows;
|
|
71
80
|
protected mergeReturning(expression: string): string;
|
|
72
81
|
/** A row lock is a hint on the table here, which `rowLocks.placement` says instead of a trailing clause. */
|
|
73
82
|
protected lockHint<E>(q: Query<E>): string;
|
|
@@ -98,6 +107,8 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
98
107
|
createSchemaSql(schema: string): string;
|
|
99
108
|
/** The estimate the engine already keeps per partition, live without a stats refresh. */
|
|
100
109
|
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
110
|
+
/** Null-safe on every version, `EXCEPT` comparing NULLs as equal: `IS DISTINCT FROM` needs 2022. */
|
|
111
|
+
neExpr(field: string, ph: string): string;
|
|
101
112
|
protected numericCast(expr: string): string;
|
|
102
113
|
/**
|
|
103
114
|
* `OPENJSON` at the path's parent, matching its last segment as a key, in either reading: `JSON_VALUE`
|