uql-orm 0.56.0 → 0.58.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/README.md +7 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +196 -33
- package/dist/dialect/abstractSqlDialect.js +410 -203
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +23 -48
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +26 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +54 -42
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +2 -2
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
- package/dist/migrate/ddl/pgIndexDdl.js +29 -12
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +4 -4
- package/dist/migrate/schemaGenerator.d.ts +8 -8
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +146 -104
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -106
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +35 -129
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +4 -17
- package/dist/querier/abstractSqlQuerier.js +40 -50
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +32 -3
- package/dist/schema/schemaASTDiffer.js +5 -5
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +38 -7
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +3 -3
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +110 -69
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +41 -72
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +43 -34
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +4 -4
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/relationQuery.util.d.ts +12 -65
- package/dist/util/relationQuery.util.js +27 -81
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -3,8 +3,9 @@ import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
|
3
3
|
import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
|
+
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
6
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
9
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
9
10
|
export const mongoDialectFeatures = {
|
|
10
11
|
explicitJsonCast: false,
|
|
@@ -72,7 +73,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
72
73
|
whereWithRelations(entity, where = {}, opts = {}) {
|
|
73
74
|
const meta = getMeta(entity);
|
|
74
75
|
const lookups = { stages: [], temps: [] };
|
|
75
|
-
const filter = this.renderFilter(entity, this.scopedWhere(meta, where, opts),
|
|
76
|
+
const filter = this.renderFilter(entity, this.scopedWhere(meta, where, opts), lookups);
|
|
76
77
|
return { stages: lookups.stages, filter, unset: lookups.temps };
|
|
77
78
|
}
|
|
78
79
|
/** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
|
|
@@ -91,14 +92,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
91
92
|
* recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
|
|
92
93
|
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
93
94
|
*/
|
|
94
|
-
renderFilter(entity, where = {},
|
|
95
|
+
renderFilter(entity, where = {}, lookups) {
|
|
95
96
|
const meta = getMeta(entity);
|
|
96
97
|
const filter = {};
|
|
97
98
|
for (const [rawKey, rawVal] of Object.entries(where)) {
|
|
98
99
|
let key = rawKey;
|
|
99
100
|
let val = rawVal;
|
|
100
101
|
if (MongoDialect.isGroupOp(key)) {
|
|
101
|
-
this.appendLogicalOperator(filter, entity, key, val,
|
|
102
|
+
this.appendLogicalOperator(filter, entity, key, val, lookups);
|
|
102
103
|
}
|
|
103
104
|
else if (key === '$text') {
|
|
104
105
|
// MongoDB's text index declares which fields it covers, so `$fields` cannot narrow the search
|
|
@@ -110,7 +111,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
110
111
|
if (!lookups) {
|
|
111
112
|
throw new TypeError(`filtering by relation '${key}' is not supported here on MongoDB`);
|
|
112
113
|
}
|
|
113
|
-
Object.assign(filter, this.appendRelationLookup(meta, key, val,
|
|
114
|
+
Object.assign(filter, this.appendRelationLookup(meta, key, val, lookups));
|
|
114
115
|
}
|
|
115
116
|
else {
|
|
116
117
|
this.assertNoRaw(val);
|
|
@@ -140,12 +141,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
140
141
|
* rejects an empty `$and`/`$or`/`$nor` outright, where the SQL dialects contribute no term.
|
|
141
142
|
* Negations accumulate into the one `$nor`, since `NOT a AND NOT b` is `$nor: [a, b]`.
|
|
142
143
|
*/
|
|
143
|
-
appendLogicalOperator(filter, entity, key, val,
|
|
144
|
+
appendLogicalOperator(filter, entity, key, val, lookups) {
|
|
144
145
|
const { join, negate } = MongoDialect.GROUP_OPS[key];
|
|
145
146
|
const parts = MongoDialect.groupClauses(key, val)
|
|
146
147
|
.map((filterIt) => {
|
|
147
148
|
this.assertNoRaw(filterIt);
|
|
148
|
-
return this.renderFilter(entity, filterIt,
|
|
149
|
+
return this.renderFilter(entity, filterIt, lookups);
|
|
149
150
|
})
|
|
150
151
|
.filter((part) => Object.keys(part).length > 0);
|
|
151
152
|
if (!parts.length) {
|
|
@@ -164,80 +165,72 @@ export class MongoDialect extends AbstractDialect {
|
|
|
164
165
|
* `$size`. The target's (and, for ManyToMany, the junction's) own filters scope the lookup, so a
|
|
165
166
|
* relation subquery can no more read out-of-scope rows than a direct query on the target can.
|
|
166
167
|
*/
|
|
167
|
-
appendRelationLookup(meta, relKey, val,
|
|
168
|
-
const relOpts = meta.relations[relKey];
|
|
169
|
-
const relEntity = relOpts.entity();
|
|
170
|
-
const relMeta = getMeta(relEntity);
|
|
168
|
+
appendRelationLookup(meta, relKey, val, lookups) {
|
|
171
169
|
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
172
170
|
const sizeVal = parseRelationSize(val);
|
|
173
|
-
// `$count` for a size test, `$limit: 1` for existence: neither returns the matched documents.
|
|
174
171
|
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
|
|
175
|
-
|
|
176
|
-
// leave their own keys unmapped. The caller's filter bypass is deliberately *not* passed down:
|
|
177
|
-
// `withDeleted()` or `hardDelete` on the parent must not un-hide trashed rows of the target, the
|
|
178
|
-
// same rule the SQL dialects' relation subqueries follow.
|
|
179
|
-
const targetCondition = (sizeVal === undefined ? val : {});
|
|
180
|
-
const targetScope = this.renderFilter(relEntity, this.scopedWhere(relMeta, targetCondition), opts);
|
|
172
|
+
const where = (sizeVal === undefined ? val : {});
|
|
181
173
|
lookups.temps.push(temp);
|
|
182
|
-
lookups.stages.push(this.relationLookup(meta,
|
|
174
|
+
lookups.stages.push(this.relationLookup(meta, meta.relations[relKey], where, temp, tail));
|
|
183
175
|
return sizeVal === undefined
|
|
184
176
|
? { [`${temp}.0`]: { $exists: true } }
|
|
185
177
|
: { $expr: this.compareRelationCount(temp, sizeVal) };
|
|
186
178
|
}
|
|
187
179
|
/**
|
|
188
|
-
* The correlated `$lookup` for one relation, as `temp`:
|
|
189
|
-
*
|
|
190
|
-
*
|
|
180
|
+
* The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
|
|
181
|
+
* the target, or for a many-to-many from inside its junction's rows. The caller's filter bypass is not
|
|
182
|
+
* passed down, as on the SQL dialects. `tail` decides what the lookup leaves behind - a row to test for
|
|
183
|
+
* existence, or a `$count` - so a filter and an ordering build the same stage.
|
|
191
184
|
*/
|
|
192
|
-
relationLookup(meta, relOpts,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
185
|
+
relationLookup(meta, relOpts, where, temp, tail) {
|
|
186
|
+
const relEntity = relOpts.entity();
|
|
187
|
+
const relMeta = getMeta(relEntity);
|
|
188
|
+
const targetScope = this.renderFilter(relEntity, this.scopedWhere(relMeta, where));
|
|
189
|
+
const targetMatch = hasKeys(targetScope) ? [{ $match: targetScope }] : [];
|
|
190
|
+
const from = this.resolveTableName(relMeta);
|
|
191
|
+
if (!relOpts.through) {
|
|
192
|
+
return {
|
|
193
|
+
$lookup: { from, ...this.joinKeys(meta, relMeta, relOpts), pipeline: [...targetMatch, ...tail], as: temp },
|
|
202
194
|
};
|
|
195
|
+
}
|
|
196
|
+
const junction = this.junctionOf(meta, relOpts, relMeta, relOpts.through());
|
|
197
|
+
const target = {
|
|
198
|
+
$lookup: {
|
|
199
|
+
from,
|
|
200
|
+
localField: junction.target,
|
|
201
|
+
foreignField: MongoDialect.ID_KEY,
|
|
202
|
+
pipeline: [...targetMatch, { $limit: 1 }],
|
|
203
|
+
as: REL_NESTED_KEY,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
$lookup: {
|
|
208
|
+
...junction.lookup,
|
|
209
|
+
pipeline: [...junction.scope, target, { $match: { [`${REL_NESTED_KEY}.0`]: { $exists: true } } }, ...tail],
|
|
210
|
+
as: temp,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
203
213
|
}
|
|
204
214
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
215
|
+
* The junction a many-to-many reaches its targets through: the lookup keys matching a parent's rows of
|
|
216
|
+
* it, its own filters, since a soft-deleted link is not a link, and the field holding each target's id.
|
|
217
|
+
* Each end is one field matched against one `_id`, so both sides must be sole-keyed.
|
|
207
218
|
*/
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const throughMeta = getMeta(throughEntity);
|
|
211
|
-
const junctionScope = this.renderFilter(throughEntity, this.scopedWhere(throughMeta, {}), opts);
|
|
212
|
-
const nested = REL_NESTED_KEY;
|
|
213
|
-
// Both ends are one column here - each `$lookup` matches one field against `_id` - so both sides
|
|
214
|
-
// must be sole-keyed. Sliced rather than indexed positionally: `references[1]` is the parent's
|
|
215
|
-
// *second* column on a composite, a real column of the wrong side.
|
|
219
|
+
junctionOf(meta, relOpts, relMeta, through) {
|
|
220
|
+
const throughMeta = getMeta(through);
|
|
216
221
|
assertSoleId(meta, 'MongoDB');
|
|
217
222
|
assertSoleId(relMeta, 'MongoDB');
|
|
218
223
|
const [parentJoin] = parentJoins(relOpts, meta.ids.length);
|
|
219
224
|
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
225
|
+
const scope = this.renderFilter(through, this.scopedWhere(throughMeta, {}));
|
|
220
226
|
return {
|
|
221
|
-
|
|
227
|
+
lookup: {
|
|
222
228
|
from: this.resolveTableName(throughMeta),
|
|
223
229
|
localField: MongoDialect.ID_KEY,
|
|
224
230
|
foreignField: this.columnOf(throughMeta, parentJoin.joined),
|
|
225
|
-
pipeline: [
|
|
226
|
-
...(hasKeys(junctionScope) ? [{ $match: junctionScope }] : []),
|
|
227
|
-
{
|
|
228
|
-
$lookup: {
|
|
229
|
-
from: this.resolveTableName(relMeta),
|
|
230
|
-
localField: this.columnOf(throughMeta, targetColumn),
|
|
231
|
-
foreignField: MongoDialect.ID_KEY,
|
|
232
|
-
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), { $limit: 1 }],
|
|
233
|
-
as: nested,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
{ $match: { [`${nested}.0`]: { $exists: true } } },
|
|
237
|
-
...tail,
|
|
238
|
-
],
|
|
239
|
-
as: temp,
|
|
240
231
|
},
|
|
232
|
+
scope: hasKeys(scope) ? [{ $match: scope }] : [],
|
|
233
|
+
target: this.columnOf(throughMeta, targetColumn),
|
|
241
234
|
};
|
|
242
235
|
}
|
|
243
236
|
/**
|
|
@@ -245,7 +238,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
245
238
|
* the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
|
|
246
239
|
*/
|
|
247
240
|
compareRelationCount(temp, sizeVal) {
|
|
248
|
-
const count =
|
|
241
|
+
const count = this.tally(temp);
|
|
249
242
|
if (typeof sizeVal === 'number') {
|
|
250
243
|
return { $eq: [count, sizeVal] };
|
|
251
244
|
}
|
|
@@ -295,7 +288,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
295
288
|
mapTableNameRow(row) {
|
|
296
289
|
return row.table_name;
|
|
297
290
|
}
|
|
298
|
-
/** String operators
|
|
291
|
+
/** String operators -> { pattern: (v) => regex, caseInsensitive } */
|
|
299
292
|
static REGEX_OP_MAP = new Map([
|
|
300
293
|
['$startsWith', { wrap: (v) => `^${v}`, ci: false }],
|
|
301
294
|
['$istartsWith', { wrap: (v) => `^${v}`, ci: true }],
|
|
@@ -340,7 +333,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
340
333
|
result[op] = val;
|
|
341
334
|
continue;
|
|
342
335
|
}
|
|
343
|
-
// String/pattern
|
|
336
|
+
// String/pattern -> regex operators (8 variants including $like/$ilike)
|
|
344
337
|
const regexEntry = MongoDialect.REGEX_OP_MAP.get(op);
|
|
345
338
|
if (regexEntry) {
|
|
346
339
|
result['$regex'] = regexEntry.wrap(val);
|
|
@@ -468,7 +461,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
468
461
|
* per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
|
|
469
462
|
* orders by. A parent with no related row gets no lookup result at all, which is a zero.
|
|
470
463
|
*/
|
|
471
|
-
sortCountStages(entity, sort
|
|
464
|
+
sortCountStages(entity, sort) {
|
|
472
465
|
const meta = getMeta(entity);
|
|
473
466
|
const stages = [];
|
|
474
467
|
const fields = [];
|
|
@@ -477,18 +470,78 @@ export class MongoDialect extends AbstractDialect {
|
|
|
477
470
|
if (!relOpts || parseSortByCount(value) === undefined) {
|
|
478
471
|
continue;
|
|
479
472
|
}
|
|
480
|
-
const relEntity = relOpts.entity();
|
|
481
|
-
const relMeta = getMeta(relEntity);
|
|
482
473
|
const temp = sortCountField(key);
|
|
483
|
-
|
|
484
|
-
const tail = [{ $count: COUNT_ALIAS }];
|
|
485
|
-
stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts), {
|
|
486
|
-
$addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] } },
|
|
487
|
-
});
|
|
474
|
+
stages.push(this.tallyLookup(meta, relOpts, {}, temp), { $addFields: { [temp]: this.tally(temp) } });
|
|
488
475
|
fields.push(temp);
|
|
489
476
|
}
|
|
490
477
|
return { stages, fields };
|
|
491
478
|
}
|
|
479
|
+
/** The correlated lookup counting a relation's rows, which `where` narrows, into `temp`. */
|
|
480
|
+
tallyLookup(meta, relOpts, where, temp) {
|
|
481
|
+
return this.relationLookup(meta, relOpts, where, temp, [{ $count: COUNT_ALIAS }]);
|
|
482
|
+
}
|
|
483
|
+
/** The tally a lookup left in `temp`, which holds no row at all where nothing matched: a zero. */
|
|
484
|
+
tally(temp) {
|
|
485
|
+
return { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] };
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* The lookups reading each to-many a query populates, and the tally of each `$count`, onto the fields
|
|
489
|
+
* its rows answer under, and the fields they parked a junction's pairings or a tally on taken back out.
|
|
490
|
+
* [The design](../../../../architecture/relations-in-one-statement.md).
|
|
491
|
+
*/
|
|
492
|
+
relationReadStages(entity, q) {
|
|
493
|
+
const meta = getMeta(entity);
|
|
494
|
+
const stages = [];
|
|
495
|
+
const temps = [];
|
|
496
|
+
for (const relKey of getRelationRequestSummary(meta, q.$populate).toManyKeys) {
|
|
497
|
+
stages.push(...this.toManyLookup(meta, relKey, parseRelationAtKey(relKey, q.$populate).query, temps));
|
|
498
|
+
}
|
|
499
|
+
for (const { relKey, relation, where } of countedRelations(meta, q.$count)) {
|
|
500
|
+
const temp = `${REL_TEMP_PREFIX}count_${relKey}`;
|
|
501
|
+
temps.push(temp);
|
|
502
|
+
stages.push(this.tallyLookup(meta, relation, where, temp), {
|
|
503
|
+
$addFields: { [`${COUNT_RESULT_KEY}.${relKey}`]: this.tally(temp) },
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return temps.length ? [...stages, { $unset: temps }] : stages;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* A to-many's rows as a lookup running their own read, whose filters, ordering and page apply per
|
|
510
|
+
* parent inside it. A many-to-many reads its targets, each once, by the ids its junction pairs the
|
|
511
|
+
* parent with, parked in a temporary field of the parent's.
|
|
512
|
+
*/
|
|
513
|
+
toManyLookup(meta, relKey, query, temps) {
|
|
514
|
+
const relOpts = relationOf(meta, relKey);
|
|
515
|
+
const relEntity = relOpts.entity();
|
|
516
|
+
const relMeta = getMeta(relEntity);
|
|
517
|
+
const read = this.aggregationPipeline(relEntity, query);
|
|
518
|
+
const pipeline = read.length ? { pipeline: read } : {};
|
|
519
|
+
const from = this.resolveTableName(relMeta);
|
|
520
|
+
if (!relOpts.through) {
|
|
521
|
+
return [{ $lookup: { from, ...this.joinKeys(meta, relMeta, relOpts), ...pipeline, as: relKey } }];
|
|
522
|
+
}
|
|
523
|
+
const junction = this.junctionOf(meta, relOpts, relMeta, relOpts.through());
|
|
524
|
+
const temp = `${REL_TEMP_PREFIX}${relKey}`;
|
|
525
|
+
temps.push(temp);
|
|
526
|
+
return [
|
|
527
|
+
{
|
|
528
|
+
$lookup: {
|
|
529
|
+
...junction.lookup,
|
|
530
|
+
pipeline: [...junction.scope, { $project: { [junction.target]: 1 } }],
|
|
531
|
+
as: temp,
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
$lookup: {
|
|
536
|
+
from,
|
|
537
|
+
localField: `${temp}.${junction.target}`,
|
|
538
|
+
foreignField: MongoDialect.ID_KEY,
|
|
539
|
+
...pipeline,
|
|
540
|
+
as: relKey,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
];
|
|
544
|
+
}
|
|
492
545
|
/** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
|
|
493
546
|
sortsRelations(entity, sort) {
|
|
494
547
|
if (!sort) {
|
|
@@ -498,7 +551,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
498
551
|
return someKey(sort, (key) => Boolean(meta.relations[key]));
|
|
499
552
|
}
|
|
500
553
|
/**
|
|
501
|
-
* Aggregate results are keyed by `$group`/`$
|
|
554
|
+
* Aggregate results are keyed by `$group`/`$select` alias rather than by column, so an aggregate
|
|
502
555
|
* `$sort` addresses those aliases as-is - the same reason the SQL dialects sort by alias there.
|
|
503
556
|
*/
|
|
504
557
|
aliasSort(sort) {
|
|
@@ -527,7 +580,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
527
580
|
...stages,
|
|
528
581
|
...(hasKeys(filter) ? [{ $match: filter }] : []),
|
|
529
582
|
...(unset.length ? [{ $unset: unset }] : []),
|
|
530
|
-
...this.readStages(entity, q,
|
|
583
|
+
...this.readStages(entity, q, {
|
|
531
584
|
sort: this.sort(entity, q.$sort, q.$populate),
|
|
532
585
|
pager: [
|
|
533
586
|
...(q.$skip === undefined ? [] : [{ $skip: assertNonNegativeInteger(q.$skip, '$skip') }]),
|
|
@@ -544,13 +597,15 @@ export class MongoDialect extends AbstractDialect {
|
|
|
544
597
|
* Shared by the plain pipeline and the `$vectorSearch` one, which each used to spell the order out
|
|
545
598
|
* for themselves and each got a different part of it wrong.
|
|
546
599
|
*/
|
|
547
|
-
readStages(entity, q,
|
|
600
|
+
readStages(entity, q, extra = {}) {
|
|
548
601
|
const meta = getMeta(entity);
|
|
549
602
|
const joins = resolveQueryJoins(meta, q);
|
|
550
603
|
// The tally an ordering by a relation's size reads, and the field it parks it on: both belong
|
|
551
604
|
// with the lookups, since the `$sort` right after them is what they exist for.
|
|
552
|
-
const counted = this.sortCountStages(entity, q.$sort
|
|
553
|
-
const lookups = [...this.lookupStages(meta, joins
|
|
605
|
+
const counted = this.sortCountStages(entity, q.$sort);
|
|
606
|
+
const lookups = [...this.lookupStages(meta, joins), ...counted.stages];
|
|
607
|
+
// Each to-many and each `$count`, which neither drop nor reorder a row, so they read the page alone.
|
|
608
|
+
const related = this.relationReadStages(entity, q);
|
|
554
609
|
const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
|
|
555
610
|
const pager = extra.pager ?? [];
|
|
556
611
|
// Merged into the query's own projection rather than standing in for one: a query that asked
|
|
@@ -578,7 +633,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
578
633
|
// ordering and the page have to run after it to address the set the caller actually receives.
|
|
579
634
|
const dedup = q.$distinct ? this.distinctStages(projected) : [];
|
|
580
635
|
if (dedup.length) {
|
|
581
|
-
return [...lookups, ...project, ...dedup, ...sort, ...pager];
|
|
636
|
+
return [...lookups, ...related, ...project, ...dedup, ...sort, ...pager];
|
|
582
637
|
}
|
|
583
638
|
// A `$required` relation drops parents when it unwinds, and an ordering may read a field only a
|
|
584
639
|
// lookup produces: either one puts the lookups first, as an INNER JOIN does. Otherwise paging
|
|
@@ -587,6 +642,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
587
642
|
lookups.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
|
|
588
643
|
return [
|
|
589
644
|
...(lookupsFirst ? [...lookups, ...sort, ...pager] : [...sort, ...pager, ...lookups]),
|
|
645
|
+
...related,
|
|
590
646
|
...unset,
|
|
591
647
|
...project,
|
|
592
648
|
];
|
|
@@ -610,42 +666,28 @@ export class MongoDialect extends AbstractDialect {
|
|
|
610
666
|
}
|
|
611
667
|
/**
|
|
612
668
|
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
669
|
+
* each populated relation and the tallies. It goes last, after the lookups have read the join keys -
|
|
670
|
+
* projecting any earlier is what used to leave `$populate` empty, and is why the pipeline emitted no
|
|
671
|
+
* projection at all and returned every column.
|
|
616
672
|
*/
|
|
617
673
|
pipelineProjection(entity, q) {
|
|
618
674
|
if (!q.$select && !q.$exclude) {
|
|
619
675
|
return undefined;
|
|
620
676
|
}
|
|
621
677
|
const projection = this.select(entity, q.$select, q.$exclude);
|
|
622
|
-
const
|
|
623
|
-
for (const relKey of summary.joinableKeys) {
|
|
678
|
+
for (const relKey of getRelationRequestSummary(getMeta(entity), q.$populate).requestedKeys) {
|
|
624
679
|
projection[relKey] = 1;
|
|
625
680
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
delete projection[MongoDialect.ID_KEY];
|
|
681
|
+
if (q.$count) {
|
|
682
|
+
projection[COUNT_RESULT_KEY] = 1;
|
|
629
683
|
}
|
|
630
684
|
return projection;
|
|
631
685
|
}
|
|
632
|
-
/**
|
|
633
|
-
* `$lookup`/`$unwind` stages for the joinable relations a query populates. Shared by the plain
|
|
634
|
-
* aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
|
|
635
|
-
*/
|
|
636
|
-
relationStages(entity, q, opts) {
|
|
637
|
-
// The whole query, not `$populate` alone: an ordering by a related field needs that relation
|
|
638
|
-
// looked up just as much as selecting it does. A `$lookup` does put a field on the document
|
|
639
|
-
// where a SQL join is invisible, so the ones only the ordering asked for are unset again by
|
|
640
|
-
// {@link readStages} before the caller sees the row.
|
|
641
|
-
const meta = getMeta(entity);
|
|
642
|
-
return this.lookupStages(meta, resolveQueryJoins(meta, q), undefined, opts);
|
|
643
|
-
}
|
|
644
686
|
/**
|
|
645
687
|
* The `$lookup`/`$unwind` pair for each relation joined below `parent`, its own relations nested
|
|
646
688
|
* inside its pipeline and resolved before the projection that reads them.
|
|
647
689
|
*/
|
|
648
|
-
lookupStages(parentMeta, joins, parent
|
|
690
|
+
lookupStages(parentMeta, joins, parent) {
|
|
649
691
|
const pipeline = [];
|
|
650
692
|
// Every join at this level hangs off `parent`, so its metadata is `parentMeta` - no branch, and
|
|
651
693
|
// no union of two unrelated entity types to resolve the join column through.
|
|
@@ -654,19 +696,19 @@ export class MongoDialect extends AbstractDialect {
|
|
|
654
696
|
continue;
|
|
655
697
|
}
|
|
656
698
|
// Unconditional, not gated by an explicit relation-level `$where`: the related entity's own
|
|
657
|
-
// filters (in particular `security: true` ones)
|
|
658
|
-
//
|
|
659
|
-
const relationFilter = this.where(join.entity, join.query.$where ?? {}
|
|
699
|
+
// filters (in particular `security: true` ones) apply even to a bare `$populate: { rel: true }`,
|
|
700
|
+
// and the caller's bypass never reaches them, exactly like the SQL dialects' JOIN ON-clause filters.
|
|
701
|
+
const relationFilter = this.where(join.entity, join.query.$where ?? {});
|
|
660
702
|
// The relation's own projection runs inside the lookup, where its keys resolve against the
|
|
661
703
|
// related entity. Left out, `$populate: { rel: { $select } }` returned all of `rel`'s columns.
|
|
662
704
|
const relationProjection = this.pipelineProjection(join.entity, join.query);
|
|
663
705
|
// MongoDB returns `_id` unless a projection subtracts it, so dropping the key from the map is
|
|
664
|
-
// how a joined document keeps its own id
|
|
665
|
-
// to-many fill needs.
|
|
706
|
+
// how a joined document keeps its own id, as it does on the SQL dialects.
|
|
666
707
|
delete relationProjection?.[MongoDialect.ID_KEY];
|
|
667
708
|
const lookupPipeline = [
|
|
668
709
|
...(hasKeys(relationFilter) ? [{ $match: relationFilter }] : []),
|
|
669
|
-
...this.lookupStages(join.meta, joins, join
|
|
710
|
+
...this.lookupStages(join.meta, joins, join),
|
|
711
|
+
...this.relationReadStages(join.entity, join.query),
|
|
670
712
|
...(relationProjection ? [{ $project: relationProjection }] : []),
|
|
671
713
|
];
|
|
672
714
|
pipeline.push({
|
|
@@ -916,7 +958,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
916
958
|
}
|
|
917
959
|
}
|
|
918
960
|
// $group stage
|
|
919
|
-
const { groupId, groupAccumulators, distinctReducers } = this.buildGroupSpec(getMeta(entity), parseGroupMap(q.$group, q.$
|
|
961
|
+
const { groupId, groupAccumulators, distinctReducers } = this.buildGroupSpec(getMeta(entity), parseGroupMap(q.$group, q.$select));
|
|
920
962
|
pipeline.push({ $group: { _id: hasKeys(groupId) ? groupId : null, ...groupAccumulators } });
|
|
921
963
|
// Project stage - rename _id fields back to their original names, and reduce collected distinct
|
|
922
964
|
// sets. Needed whenever there are group keys OR any distinct alias.
|
|
@@ -966,7 +1008,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
966
1008
|
*/
|
|
967
1009
|
buildGroupSpec(meta, groupEntries) {
|
|
968
1010
|
if (!groupEntries.length) {
|
|
969
|
-
throw new TypeError('aggregate requires at least one $group column or $
|
|
1011
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
970
1012
|
}
|
|
971
1013
|
const groupId = {};
|
|
972
1014
|
const groupAccumulators = {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Document, MongoClient } from 'mongodb';
|
|
2
2
|
import { AbstractQuerier } from '../querier/index.js';
|
|
3
3
|
import type { EntityData, ExtraOptions, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
4
|
-
import { type ParentPartition } from '../util/index.js';
|
|
5
4
|
import type { MongoDialect } from './mongoDialect.js';
|
|
6
5
|
export declare class MongodbQuerier extends AbstractQuerier {
|
|
7
6
|
readonly dialect: MongoDialect;
|
|
@@ -11,27 +10,21 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
11
10
|
constructor(dialect: MongoDialect, conn: MongoClient, extra?: ExtraOptions | undefined);
|
|
12
11
|
private execute;
|
|
13
12
|
protected internalFindMany<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
13
|
+
protected internalFindManyStream<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncGenerator<E, void, unknown>;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* rather than N concurrent ones.
|
|
19
|
-
*
|
|
20
|
-
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
21
|
-
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
22
|
-
* [The design](../../../../architecture/populate-limits.md).
|
|
15
|
+
* The cursor a read runs on: the aggregation pipeline for a clause only a stage can express - a
|
|
16
|
+
* lookup, a grouping, a vector search - and the plain `find` cursor for everything else. One routing
|
|
17
|
+
* for a read and a stream alike, so both load the same relations.
|
|
23
18
|
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
private readCursor;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
22
|
+
* or order by a relation, and the grouping `$distinct` is.
|
|
23
|
+
*/
|
|
24
|
+
private readsThroughPipeline;
|
|
30
25
|
private buildScalarProjection;
|
|
31
26
|
/** Build a MongoDB FindCursor with filter, projection, sort, skip, and limit from the query. */
|
|
32
27
|
private buildFindCursor;
|
|
33
|
-
/** Execute an aggregation pipeline and normalize `_id` → `id`. */
|
|
34
|
-
private runPipeline;
|
|
35
28
|
/**
|
|
36
29
|
* Build an aggregation pipeline for vector similarity search.
|
|
37
30
|
* `$vectorSearch` is always the first stage; `$where` is merged into its `filter`.
|