uql-orm 0.69.0 → 0.70.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/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/type/clientQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.js +2 -2
- package/dist/dialect/abstractDialect.d.ts +8 -2
- package/dist/dialect/abstractDialect.js +17 -1
- package/dist/dialect/abstractSqlDialect.d.ts +16 -5
- package/dist/dialect/abstractSqlDialect.js +82 -44
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +10 -7
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/dialect/queryJoins.d.ts +8 -1
- package/dist/dialect/queryJoins.js +33 -10
- package/dist/entity/decorator/members.d.ts +25 -11
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +31 -27
- package/dist/mongo/mongoDialect.js +172 -112
- package/dist/mongo/mongodbQuerier.d.ts +0 -2
- package/dist/mongo/mongodbQuerier.js +12 -11
- package/dist/mssql/mssqlDialect.js +3 -2
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +16 -11
- package/dist/querier/abstractQuerier.js +28 -8
- package/dist/querier/abstractQuerierPool.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/sqlite/sqliteDialect.js +1 -0
- package/dist/turso/tursoDialect.d.ts +1 -1
- package/dist/turso/tursoDialect.js +6 -2
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +31 -2
- package/dist/type/migration.d.ts +7 -0
- package/dist/type/query.d.ts +6 -0
- package/dist/type/queryAggregate.d.ts +77 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/util/dialect.util.d.ts +6 -2
- package/dist/util/dialect.util.js +19 -5
- package/package.json +1 -1
|
@@ -33,20 +33,15 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
33
33
|
columnOf<E>(meta: EntityMeta<E>, key: string): string;
|
|
34
34
|
where<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions): Filter<E>;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The stages every pipeline starts with: the `$lookup` each relation condition needs, into a temporary
|
|
37
|
+
* field so `$or` keeps its meaning, the `$match` reading them, and the temporaries taken back out. Each
|
|
38
|
+
* relation aggregate the `$where` or `named` reads is left on the document, once, under its column.
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
-
readonly stages: MongoAggregationPipelineEntry<Document>[];
|
|
41
|
-
readonly filter: Filter<E>;
|
|
42
|
-
readonly unset: string[];
|
|
43
|
-
};
|
|
44
|
-
/** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
|
|
45
|
-
constrainsRelations<E extends Document>(entity: Type<E>, where: QueryWhere<E> | undefined): boolean;
|
|
40
|
+
matchStages<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions, named?: readonly string[]): MongoAggregationPipelineEntry<Document>[];
|
|
46
41
|
/**
|
|
47
42
|
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
48
|
-
* recursion).
|
|
49
|
-
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
43
|
+
* recursion). A relation, or a relation aggregate, needs `$lookup` stages, so it is only accepted
|
|
44
|
+
* when `lookups` is given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
50
45
|
*/
|
|
51
46
|
protected renderFilter<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, lookups?: RelationLookups): Filter<E>;
|
|
52
47
|
/**
|
|
@@ -141,20 +136,14 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
141
136
|
};
|
|
142
137
|
/** Whether a read answers with a relation aggregate, which only the pipeline can build. */
|
|
143
138
|
readsAggregates<E extends Document>(entity: Type<E>, q: Query<E>): boolean;
|
|
144
|
-
/**
|
|
145
|
-
* The relation aggregates one query reads: the ones its projection carries, plus any its `$where` or
|
|
146
|
-
* `$sort` names, which a read materializes whether or not it answers with them.
|
|
147
|
-
*/
|
|
139
|
+
/** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
|
|
148
140
|
private aggregateKeys;
|
|
149
141
|
/**
|
|
150
|
-
* The
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* The same spec the SQL dialects render as a correlated subquery: a relation aggregate is data, so a
|
|
155
|
-
* document engine builds it out of stages rather than being refused a language it does not speak.
|
|
142
|
+
* The relation aggregate `key` computes, put on the document under its column by the stages
|
|
143
|
+
* {@link aggregateStages} builds from the same spec SQL renders as a subquery. Once however many
|
|
144
|
+
* clauses read it; a key computing none adds nothing.
|
|
156
145
|
*/
|
|
157
|
-
private
|
|
146
|
+
private appendAggregateField;
|
|
158
147
|
/**
|
|
159
148
|
* One relation aggregate on the document under `field`: the correlated lookup that reads the related
|
|
160
149
|
* rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
|
|
@@ -258,13 +247,28 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
258
247
|
/**
|
|
259
248
|
* Build MongoDB aggregation pipeline stages from a QueryAggregate.
|
|
260
249
|
*/
|
|
261
|
-
buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions):
|
|
250
|
+
buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): MongoAggregationPipelineEntry<Document>[];
|
|
262
251
|
/**
|
|
263
|
-
* Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage
|
|
264
|
-
* `
|
|
265
|
-
*
|
|
252
|
+
* Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage, and the
|
|
253
|
+
* `columns` its `$project` reads each result from: a group key out of `_id`, a DISTINCT set by its
|
|
254
|
+
* size, and a `$sum` as null where it read no value, as SQL answers it. `named` is every field it reads,
|
|
255
|
+
* filters included, so each relation aggregate among them is put on the document first.
|
|
266
256
|
*/
|
|
267
257
|
private buildGroupSpec;
|
|
258
|
+
/** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
|
|
259
|
+
private static countOf;
|
|
260
|
+
/** Whether `ref` is null or missing: an expression compares a missing field as neither. */
|
|
261
|
+
private static isNullExpr;
|
|
262
|
+
/** The field a grouped path reads, on the document or on the joined one its lookup unwound. */
|
|
263
|
+
private groupedPath;
|
|
264
|
+
/**
|
|
265
|
+
* An aggregate's own `$where` as the expression a `$cond` tests, which a query filter is not: the
|
|
266
|
+
* comparisons and the logical operators translate, anything else is refused by name. `named` gathers
|
|
267
|
+
* the fields it reads, so a relation aggregate among them is on the document first.
|
|
268
|
+
*/
|
|
269
|
+
private whereExpression;
|
|
270
|
+
/** One field's condition as an expression: a value it equals, a list it is in, or a map of comparisons. */
|
|
271
|
+
private fieldExpression;
|
|
268
272
|
private buildHavingFilter;
|
|
269
273
|
/**
|
|
270
274
|
* Separate vector sort entries from regular sort entries.
|
|
@@ -275,7 +279,7 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
275
279
|
* Build a `$vectorSearch` aggregation pipeline stage.
|
|
276
280
|
* Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
|
|
277
281
|
*/
|
|
278
|
-
buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number):
|
|
282
|
+
buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number): MongoAggregationPipelineEntry<Document>;
|
|
279
283
|
}
|
|
280
284
|
export type MongoAggregationPipelineEntry<E extends Document> = {
|
|
281
285
|
$lookup?: MongoAggregationLookup;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
4
|
-
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
3
|
+
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, } from '../dialect/aliases.js';
|
|
4
|
+
import { groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
|
-
import { QueryRaw
|
|
8
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
7
|
+
import { QueryRaw } from '../type/queryRaw.js';
|
|
8
|
+
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
9
9
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
10
10
|
export const mongoDialectFeatures = {
|
|
11
11
|
ifNotExists: false,
|
|
@@ -22,6 +22,7 @@ export const mongoDialectFeatures = {
|
|
|
22
22
|
stringSizing: 'bounded-text',
|
|
23
23
|
supportsUnsigned: false,
|
|
24
24
|
serverSideCursors: false,
|
|
25
|
+
correlatedWrites: false,
|
|
25
26
|
};
|
|
26
27
|
/** What `toWireId` converts: the hex spelling of an `ObjectId`, and nothing looser. */
|
|
27
28
|
const HEX_24 = /^[0-9a-f]{24}$/i;
|
|
@@ -61,30 +62,27 @@ export class MongoDialect extends AbstractDialect {
|
|
|
61
62
|
return this.renderFilter(entity, this.scopedWhere(meta, where, opts));
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
65
|
+
* The stages every pipeline starts with: the `$lookup` each relation condition needs, into a temporary
|
|
66
|
+
* field so `$or` keeps its meaning, the `$match` reading them, and the temporaries taken back out. Each
|
|
67
|
+
* relation aggregate the `$where` or `named` reads is left on the document, once, under its column.
|
|
66
68
|
*/
|
|
67
|
-
|
|
69
|
+
matchStages(entity, where = {}, opts = {}, named = []) {
|
|
68
70
|
const meta = getMeta(entity);
|
|
69
71
|
const lookups = { stages: [], temps: [] };
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
/** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
|
|
74
|
-
constrainsRelations(entity, where) {
|
|
75
|
-
if (!where) {
|
|
76
|
-
return false;
|
|
72
|
+
for (const key of named) {
|
|
73
|
+
this.appendAggregateField(meta, key, lookups);
|
|
77
74
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
75
|
+
const filter = this.renderFilter(entity, this.scopedWhere(meta, where, opts), lookups);
|
|
76
|
+
return [
|
|
77
|
+
...lookups.stages,
|
|
78
|
+
...(hasKeys(filter) ? [{ $match: filter }] : []),
|
|
79
|
+
...(lookups.temps.length ? [{ $unset: lookups.temps }] : []),
|
|
80
|
+
];
|
|
83
81
|
}
|
|
84
82
|
/**
|
|
85
83
|
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
86
|
-
* recursion).
|
|
87
|
-
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
84
|
+
* recursion). A relation, or a relation aggregate, needs `$lookup` stages, so it is only accepted
|
|
85
|
+
* when `lookups` is given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
88
86
|
*/
|
|
89
87
|
renderFilter(entity, where = {}, lookups) {
|
|
90
88
|
const meta = getMeta(entity);
|
|
@@ -110,6 +108,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
110
108
|
else {
|
|
111
109
|
this.assertNoRaw(val);
|
|
112
110
|
this.assertKnownPathRoot(meta, key);
|
|
111
|
+
if (aggregateOf(meta.fields[key])) {
|
|
112
|
+
if (!lookups) {
|
|
113
|
+
throw new TypeError(`filtering by relation aggregate '${key}' is not supported here on MongoDB`);
|
|
114
|
+
}
|
|
115
|
+
this.appendAggregateField(meta, key, lookups);
|
|
116
|
+
}
|
|
113
117
|
const isReference = !!meta.fields[key]?.references;
|
|
114
118
|
key = this.pathOf(meta, key);
|
|
115
119
|
if ((key === MongoDialect.ID_KEY || isReference) && !isOperatorObject(val)) {
|
|
@@ -423,16 +427,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
423
427
|
const selectMap = asSelectMap(select);
|
|
424
428
|
// Projected by column, not by field key; `normalizeId` maps them back on the way out.
|
|
425
429
|
const projection = normalizeScalarFieldSelection(meta, selectMap, exclude).reduce((acc, key) => {
|
|
426
|
-
//
|
|
427
|
-
//
|
|
430
|
+
// A computed field writing SQL leaves the document nothing to project: refused asked for by
|
|
431
|
+
// name, skipped swept in with the rest. A relation aggregate is on it by now, like any column.
|
|
428
432
|
const field = meta.fields[key];
|
|
429
|
-
if (field?.computed) {
|
|
430
|
-
|
|
431
|
-
// is refused where it was asked for by name, and skipped where it was swept in with the rest.
|
|
432
|
-
if (aggregateOf(field)) {
|
|
433
|
-
acc[key] = 1;
|
|
434
|
-
}
|
|
435
|
-
else if (selectMap && key in selectMap) {
|
|
433
|
+
if (field?.computed && !aggregateOf(field)) {
|
|
434
|
+
if (selectMap && key in selectMap) {
|
|
436
435
|
assertReadable(meta, key);
|
|
437
436
|
}
|
|
438
437
|
return acc;
|
|
@@ -521,36 +520,25 @@ export class MongoDialect extends AbstractDialect {
|
|
|
521
520
|
readsAggregates(entity, q) {
|
|
522
521
|
return this.aggregateKeys(entity, q).length > 0;
|
|
523
522
|
}
|
|
524
|
-
/**
|
|
525
|
-
* The relation aggregates one query reads: the ones its projection carries, plus any its `$where` or
|
|
526
|
-
* `$sort` names, which a read materializes whether or not it answers with them.
|
|
527
|
-
*/
|
|
523
|
+
/** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
|
|
528
524
|
aggregateKeys(entity, q) {
|
|
529
525
|
const meta = getMeta(entity);
|
|
530
526
|
const projected = normalizeScalarFieldSelection(meta, asSelectMap(q.$select), q.$exclude);
|
|
531
|
-
|
|
532
|
-
return [...new Set([...projected, ...named])].flatMap((key) => {
|
|
533
|
-
const spec = aggregateOf(meta.fields[key]);
|
|
534
|
-
return spec ? [[key, spec]] : [];
|
|
535
|
-
});
|
|
527
|
+
return [...projected, ...Object.keys(q.$sort ?? {})].filter((key) => aggregateOf(meta.fields[key]));
|
|
536
528
|
}
|
|
537
529
|
/**
|
|
538
|
-
* The
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* The same spec the SQL dialects render as a correlated subquery: a relation aggregate is data, so a
|
|
543
|
-
* document engine builds it out of stages rather than being refused a language it does not speak.
|
|
530
|
+
* The relation aggregate `key` computes, put on the document under its column by the stages
|
|
531
|
+
* {@link aggregateStages} builds from the same spec SQL renders as a subquery. Once however many
|
|
532
|
+
* clauses read it; a key computing none adds nothing.
|
|
544
533
|
*/
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
stages.push(...this.aggregateStages(meta, spec, temp, key));
|
|
551
|
-
temps.push(temp);
|
|
534
|
+
appendAggregateField(meta, key, lookups) {
|
|
535
|
+
const spec = aggregateOf(meta.fields[key]);
|
|
536
|
+
const temp = `${REL_TEMP_PREFIX}${key}`;
|
|
537
|
+
if (!spec || lookups.temps.includes(temp)) {
|
|
538
|
+
return;
|
|
552
539
|
}
|
|
553
|
-
|
|
540
|
+
lookups.temps.push(temp);
|
|
541
|
+
lookups.stages.push(...this.aggregateStages(meta, spec, temp, this.columnOf(meta, key)));
|
|
554
542
|
}
|
|
555
543
|
/**
|
|
556
544
|
* One relation aggregate on the document under `field`: the correlated lookup that reads the related
|
|
@@ -651,7 +639,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
651
639
|
return false;
|
|
652
640
|
}
|
|
653
641
|
const meta = getMeta(entity);
|
|
654
|
-
return someKey(sort, (key) =>
|
|
642
|
+
return someKey(sort, (key) => !!meta.relations[key]);
|
|
655
643
|
}
|
|
656
644
|
/**
|
|
657
645
|
* Aggregate results are keyed by `$group`/`$select` alias rather than by column, so an aggregate
|
|
@@ -677,22 +665,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
677
665
|
return this.columnOf(meta, key.slice(0, dot)) + key.slice(dot);
|
|
678
666
|
}
|
|
679
667
|
aggregationPipeline(entity, q, opts) {
|
|
680
|
-
// Lookups that a relation condition needs come first, then the match that reads them, then the
|
|
681
|
-
// temporary fields are dropped so they never reach the caller.
|
|
682
|
-
// Every relation aggregate the query reads comes first: a `$match`, a `$sort` and the projection all
|
|
683
|
-
// name it as a field of the document, which is what these stages make true.
|
|
684
|
-
const aggregates = this.aggregateFieldStages(getMeta(entity), this.aggregateKeys(entity, q));
|
|
685
|
-
const { stages, filter, unset } = this.whereWithRelations(entity, q.$where, opts);
|
|
686
668
|
return [
|
|
687
|
-
...
|
|
688
|
-
...stages,
|
|
689
|
-
...(hasKeys(filter) ? [{ $match: filter }] : []),
|
|
690
|
-
...(unset.length ? [{ $unset: unset }] : []),
|
|
669
|
+
...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
|
|
691
670
|
...this.readStages(entity, q, {
|
|
692
671
|
sort: this.sort(entity, q.$sort, q.$populate),
|
|
693
672
|
pager: this.pagerStages(q),
|
|
694
673
|
}),
|
|
695
|
-
...(aggregates.temps.length ? [{ $unset: aggregates.temps }] : []),
|
|
696
674
|
];
|
|
697
675
|
}
|
|
698
676
|
/** The `$skip`/`$limit` stages of a page, each checked: `/http` hands a page over untyped. */
|
|
@@ -1037,33 +1015,19 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1037
1015
|
* Build MongoDB aggregation pipeline stages from a QueryAggregate.
|
|
1038
1016
|
*/
|
|
1039
1017
|
buildAggregateStages(entity, q, opts) {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
pipeline.push({ $group: { _id: hasKeys(groupId) ? groupId : null, ...groupAccumulators } });
|
|
1051
|
-
// Project stage - rename _id fields back to their original names, and reduce collected distinct
|
|
1052
|
-
// sets. Needed whenever there are group keys OR any distinct alias.
|
|
1053
|
-
if (hasKeys(groupId) || distinctReducers.size) {
|
|
1054
|
-
const project = { _id: 0 };
|
|
1055
|
-
for (const alias of Object.keys(groupId)) {
|
|
1056
|
-
project[alias] = `$_id.${alias}`;
|
|
1057
|
-
}
|
|
1058
|
-
for (const alias of Object.keys(groupAccumulators)) {
|
|
1059
|
-
const reduceOp = distinctReducers.get(alias);
|
|
1060
|
-
project[alias] = reduceOp ? { [reduceOp]: `$${alias}` } : 1;
|
|
1061
|
-
}
|
|
1062
|
-
pipeline.push({ $project: project });
|
|
1063
|
-
}
|
|
1018
|
+
const meta = getMeta(entity);
|
|
1019
|
+
const joins = resolveGroupJoins(meta, q.$group);
|
|
1020
|
+
const { groupId, accumulators, columns, named } = this.buildGroupSpec(meta, parseGroupMap(q.$group, q.$select), joins);
|
|
1021
|
+
const pipeline = [
|
|
1022
|
+
...this.matchStages(entity, q.$where, opts, named),
|
|
1023
|
+
...this.lookupStages(meta, joins),
|
|
1024
|
+
{ $group: { _id: hasKeys(groupId) ? groupId : null, ...accumulators } },
|
|
1025
|
+
// `$group` answers with `_id` even when grouping by nothing, and with what `columns` read to the end.
|
|
1026
|
+
{ $project: { _id: 0, ...columns } },
|
|
1027
|
+
];
|
|
1064
1028
|
// Everything the pipeline emits, which is all `$having` and `$sort` may name. The `$project`
|
|
1065
1029
|
// above has already dropped the rest, so an unchecked key matched nothing or ordered by nothing.
|
|
1066
|
-
const emitted = new Set(
|
|
1030
|
+
const emitted = new Set(Object.keys(columns));
|
|
1067
1031
|
// $match stage for HAVING (post-group filtering)
|
|
1068
1032
|
if (q.$having) {
|
|
1069
1033
|
assertAggregateColumns(q.$having, emitted, '$having');
|
|
@@ -1084,39 +1048,139 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1084
1048
|
return pipeline;
|
|
1085
1049
|
}
|
|
1086
1050
|
/**
|
|
1087
|
-
* Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage
|
|
1088
|
-
* `
|
|
1089
|
-
*
|
|
1051
|
+
* Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage, and the
|
|
1052
|
+
* `columns` its `$project` reads each result from: a group key out of `_id`, a DISTINCT set by its
|
|
1053
|
+
* size, and a `$sum` as null where it read no value, as SQL answers it. `named` is every field it reads,
|
|
1054
|
+
* filters included, so each relation aggregate among them is put on the document first.
|
|
1090
1055
|
*/
|
|
1091
|
-
buildGroupSpec(meta, groupEntries) {
|
|
1056
|
+
buildGroupSpec(meta, groupEntries, joins) {
|
|
1092
1057
|
if (!groupEntries.length) {
|
|
1093
1058
|
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
1094
1059
|
}
|
|
1095
1060
|
const groupId = {};
|
|
1096
|
-
const
|
|
1097
|
-
const
|
|
1061
|
+
const accumulators = {};
|
|
1062
|
+
const columns = {};
|
|
1063
|
+
const named = [];
|
|
1098
1064
|
for (const entry of groupEntries) {
|
|
1099
1065
|
// Aliases stay as the caller wrote them ($project maps them back); the *refs* address columns.
|
|
1100
|
-
const ref = entry.kind === 'key' ? `$${this.columnOf(meta, entry.alias)}` : `$${this.columnOf(meta, entry.fieldRef)}`;
|
|
1101
1066
|
if (entry.kind === 'key') {
|
|
1102
|
-
groupId[entry.alias] =
|
|
1067
|
+
groupId[entry.alias] = `$${this.groupedPath(meta, joins, entry.path, named)}`;
|
|
1068
|
+
columns[entry.alias] = `$_id.${entry.alias}`;
|
|
1069
|
+
continue;
|
|
1103
1070
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1071
|
+
named.push(entry.fieldRef);
|
|
1072
|
+
const ref = `$${this.columnOf(meta, entry.fieldRef)}`;
|
|
1073
|
+
const test = entry.where && this.whereExpression(meta, entry.where, named);
|
|
1074
|
+
// What the accumulator reads from a row its own `$where` passes, and from one it does not.
|
|
1075
|
+
const read = (passed, failed) => (test ? { $cond: [test, passed, failed] } : passed);
|
|
1076
|
+
columns[entry.alias] = 1;
|
|
1077
|
+
if (entry.distinct) {
|
|
1078
|
+
accumulators[entry.alias] = { $addToSet: read(ref, '$$REMOVE') };
|
|
1079
|
+
columns[entry.alias] = { $size: `$${entry.alias}` };
|
|
1108
1080
|
}
|
|
1109
1081
|
else if (entry.op === '$count') {
|
|
1110
1082
|
// COUNT(*) counts every row; COUNT(field) counts non-null values, matching SQL.
|
|
1111
|
-
|
|
1112
|
-
entry.fieldRef === '*' ? { $sum: 1 } : { $sum: { $cond: [{ $ne: [ref, null] }, 1, 0] } };
|
|
1083
|
+
accumulators[entry.alias] = { $sum: entry.fieldRef === '*' ? read(1, 0) : read(MongoDialect.countOf(ref), 0) };
|
|
1113
1084
|
}
|
|
1114
1085
|
else {
|
|
1115
|
-
// `$sum`, `$avg`, `$min` and `$max` are MongoDB accumulators of the same name.
|
|
1116
|
-
|
|
1086
|
+
// `$sum`, `$avg`, `$min` and `$max` are MongoDB accumulators of the same name, and skip a null.
|
|
1087
|
+
accumulators[entry.alias] = { [entry.op]: read(ref, null) };
|
|
1088
|
+
}
|
|
1089
|
+
if (entry.op === '$sum' && !entry.distinct) {
|
|
1090
|
+
const counted = `${SUM_COUNT_ALIAS}_${entry.alias}`;
|
|
1091
|
+
accumulators[counted] = { $sum: read(MongoDialect.countOf(ref), 0) };
|
|
1092
|
+
columns[entry.alias] = { $cond: [{ $eq: [`$${counted}`, 0] }, null, `$${entry.alias}`] };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { groupId, accumulators, columns, named };
|
|
1096
|
+
}
|
|
1097
|
+
/** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
|
|
1098
|
+
static countOf(ref) {
|
|
1099
|
+
return { $cond: [MongoDialect.isNullExpr(ref), 0, 1] };
|
|
1100
|
+
}
|
|
1101
|
+
/** Whether `ref` is null or missing: an expression compares a missing field as neither. */
|
|
1102
|
+
static isNullExpr(ref) {
|
|
1103
|
+
return { $eq: [{ $ifNull: [ref, null] }, null] };
|
|
1104
|
+
}
|
|
1105
|
+
/** The field a grouped path reads, on the document or on the joined one its lookup unwound. */
|
|
1106
|
+
groupedPath(meta, joins, path, named) {
|
|
1107
|
+
const { key, join } = groupPathField(joins, path);
|
|
1108
|
+
if (!join) {
|
|
1109
|
+
named.push(key);
|
|
1110
|
+
return this.columnOf(meta, key);
|
|
1111
|
+
}
|
|
1112
|
+
if (aggregateOf(join.meta.fields[key])) {
|
|
1113
|
+
throw new TypeError(`cannot $group by '${path.join('.')}' on MongoDB: a joined row's relation aggregate is not read`);
|
|
1114
|
+
}
|
|
1115
|
+
return `${join.path}.${this.columnOf(join.meta, key)}`;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* An aggregate's own `$where` as the expression a `$cond` tests, which a query filter is not: the
|
|
1119
|
+
* comparisons and the logical operators translate, anything else is refused by name. `named` gathers
|
|
1120
|
+
* the fields it reads, so a relation aggregate among them is on the document first.
|
|
1121
|
+
*/
|
|
1122
|
+
whereExpression(meta, where, named) {
|
|
1123
|
+
const terms = getKeys(where)
|
|
1124
|
+
.filter((key) => where[key] !== undefined)
|
|
1125
|
+
.map((key) => {
|
|
1126
|
+
if (MongoDialect.isGroupOp(key)) {
|
|
1127
|
+
const { join, negate } = MongoDialect.GROUP_OPS[key];
|
|
1128
|
+
const clauses = MongoDialect.groupClauses(key, where[key]).map((clause) => {
|
|
1129
|
+
if (clause instanceof QueryRaw) {
|
|
1130
|
+
throw new TypeError('raw SQL is not supported in an aggregate $where on MongoDB');
|
|
1131
|
+
}
|
|
1132
|
+
return this.whereExpression(meta, clause, named);
|
|
1133
|
+
});
|
|
1134
|
+
return negate ? { $not: [{ [join]: clauses }] } : { [join]: clauses };
|
|
1117
1135
|
}
|
|
1136
|
+
if (key.startsWith('$')) {
|
|
1137
|
+
throw new TypeError(`aggregate $where operator '${key}' is not supported on MongoDB`);
|
|
1138
|
+
}
|
|
1139
|
+
const val = where[key];
|
|
1140
|
+
named.push(key);
|
|
1141
|
+
const path = this.pathOf(meta, key);
|
|
1142
|
+
const wire = (value) => path === MongoDialect.ID_KEY || meta.fields[key]?.references ? this.toWireId(value) : value;
|
|
1143
|
+
return this.fieldExpression(`$${path}`, val, wire);
|
|
1144
|
+
});
|
|
1145
|
+
return terms.length === 1 ? terms[0] : { $and: terms };
|
|
1146
|
+
}
|
|
1147
|
+
/** One field's condition as an expression: a value it equals, a list it is in, or a map of comparisons. */
|
|
1148
|
+
fieldExpression(ref, val, wire) {
|
|
1149
|
+
const equals = (value) => (value === null ? MongoDialect.isNullExpr(ref) : { $eq: [ref, wire(value)] });
|
|
1150
|
+
if (!isOperatorMap(val)) {
|
|
1151
|
+
return Array.isArray(val) ? { $in: [ref, wire(val)] } : equals(val);
|
|
1118
1152
|
}
|
|
1119
|
-
|
|
1153
|
+
// A null or missing field compares below every value in an expression, where SQL leaves it unmatched.
|
|
1154
|
+
const present = { $not: [MongoDialect.isNullExpr(ref)] };
|
|
1155
|
+
const terms = Object.entries(val).map(([op, operand]) => {
|
|
1156
|
+
switch (op) {
|
|
1157
|
+
case '$eq':
|
|
1158
|
+
return equals(operand);
|
|
1159
|
+
case '$ne':
|
|
1160
|
+
return { $not: [equals(operand)] };
|
|
1161
|
+
case '$gt':
|
|
1162
|
+
case '$gte':
|
|
1163
|
+
return { [op]: [ref, wire(operand)] };
|
|
1164
|
+
case '$lt':
|
|
1165
|
+
case '$lte':
|
|
1166
|
+
return { $and: [present, { [op]: [ref, wire(operand)] }] };
|
|
1167
|
+
case '$in':
|
|
1168
|
+
return { $in: [ref, wire(operand)] };
|
|
1169
|
+
case '$nin':
|
|
1170
|
+
return { $not: [{ $in: [ref, wire(operand)] }] };
|
|
1171
|
+
case '$between': {
|
|
1172
|
+
const [min, max] = operand;
|
|
1173
|
+
return { $and: [{ $gte: [ref, wire(min)] }, { $lte: [ref, wire(max)] }] };
|
|
1174
|
+
}
|
|
1175
|
+
case '$isNull':
|
|
1176
|
+
return operand ? MongoDialect.isNullExpr(ref) : present;
|
|
1177
|
+
case '$isNotNull':
|
|
1178
|
+
return operand ? present : MongoDialect.isNullExpr(ref);
|
|
1179
|
+
default:
|
|
1180
|
+
throw new TypeError(`aggregate $where operator '${op}' is not supported on MongoDB`);
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
return terms.length === 1 ? terms[0] : { $and: terms };
|
|
1120
1184
|
}
|
|
1121
1185
|
buildHavingFilter(having) {
|
|
1122
1186
|
const filter = {};
|
|
@@ -1195,14 +1259,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1195
1259
|
function sortDirection(value) {
|
|
1196
1260
|
return value === 'desc' || value === -1 ? -1 : 1;
|
|
1197
1261
|
}
|
|
1198
|
-
/** The relation aggregate a field computes, where it computes one rather than writing SQL. */
|
|
1199
|
-
function aggregateOf(field) {
|
|
1200
|
-
return field?.computed instanceof RelationAggregate ? field.computed.spec : undefined;
|
|
1201
|
-
}
|
|
1202
1262
|
/**
|
|
1203
1263
|
* A `computed` field writing SQL is refused wherever a query names it, since no document engine
|
|
1204
1264
|
* evaluates SQL and answering with the property name would hand back `undefined` for every row. One
|
|
1205
|
-
* computing a relation aggregate is read: `
|
|
1265
|
+
* computing a relation aggregate is read: `appendAggregateField` builds it.
|
|
1206
1266
|
*/
|
|
1207
1267
|
function assertReadable(meta, key) {
|
|
1208
1268
|
const field = meta.fields[key];
|
|
@@ -44,8 +44,6 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
44
44
|
* filter - the reason {@link UniversalQuerier.estimatedCount} takes none either.
|
|
45
45
|
*/
|
|
46
46
|
estimatedCount<E extends Document>(entity: Type<E>): Promise<number>;
|
|
47
|
-
/** A `find` filter cannot host the `$lookup` a relation condition needs, so such a write names its rows by id. */
|
|
48
|
-
protected settlesWrite<E extends Document>(entity: Type<E>, q: QuerySearch<E>): boolean;
|
|
49
47
|
internalInsertMany<E extends Document>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
50
48
|
internalUpdateMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
51
49
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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';
|
|
@@ -143,10 +143,10 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
143
143
|
const [founds, counted] = await Promise.all([
|
|
144
144
|
this.internalFindMany(entity, q, opts),
|
|
145
145
|
this.execute((session) => this.collection(entity)
|
|
146
|
-
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count:
|
|
146
|
+
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count: AGGREGATE_VALUE_ALIAS }], { session })
|
|
147
147
|
.toArray()),
|
|
148
148
|
]);
|
|
149
|
-
return [founds, counted[0]?.[
|
|
149
|
+
return [founds, counted[0]?.[AGGREGATE_VALUE_ALIAS] ?? 0];
|
|
150
150
|
}
|
|
151
151
|
/** The pipeline `countDocuments` runs, spelled out so a relation condition gets its lookups and a page its stages. */
|
|
152
152
|
async internalCount(entity, q, opts) {
|
|
@@ -154,10 +154,15 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
154
154
|
return 0;
|
|
155
155
|
}
|
|
156
156
|
return this.timed('internalCount', undefined, async () => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const pipeline = [
|
|
158
|
+
...this.dialect.matchStages(entity, q.$where, opts),
|
|
159
|
+
...this.dialect.pagerStages(q),
|
|
160
|
+
{ $count: AGGREGATE_VALUE_ALIAS },
|
|
161
|
+
];
|
|
162
|
+
const [counted] = await this.execute((session) => this.collection(entity)
|
|
163
|
+
.aggregate(pipeline, { session })
|
|
164
|
+
.toArray());
|
|
165
|
+
return counted?.[AGGREGATE_VALUE_ALIAS] ?? 0;
|
|
161
166
|
});
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
@@ -167,10 +172,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
167
172
|
async estimatedCount(entity) {
|
|
168
173
|
return this.timed('estimatedCount', undefined, async () => this.execute((session) => this.collection(entity).estimatedDocumentCount({ session })));
|
|
169
174
|
}
|
|
170
|
-
/** A `find` filter cannot host the `$lookup` a relation condition needs, so such a write names its rows by id. */
|
|
171
|
-
settlesWrite(entity, q) {
|
|
172
|
-
return super.settlesWrite(entity, q) || this.dialect.constrainsRelations(entity, q.$where);
|
|
173
|
-
}
|
|
174
175
|
async internalInsertMany(entity, rows) {
|
|
175
176
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
176
177
|
const meta = getMeta(entity);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
2
|
-
import {
|
|
2
|
+
import { AGGREGATE_VALUE_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
|
|
3
3
|
import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
4
4
|
import { jsonArraySlotArgs, jsonPath, jsonSlotArgs } from '../dialect/jsonSql.js';
|
|
5
5
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
@@ -29,6 +29,7 @@ const MSSQL_FEATURES = {
|
|
|
29
29
|
stringSizing: 'varchar',
|
|
30
30
|
supportsUnsigned: false,
|
|
31
31
|
serverSideCursors: false,
|
|
32
|
+
correlatedWrites: true,
|
|
32
33
|
rowLocks: true,
|
|
33
34
|
rowLockWithWindow: true,
|
|
34
35
|
rowLockOf: true,
|
|
@@ -227,7 +228,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
227
228
|
/** The estimate the engine already keeps per partition, live without a stats refresh. */
|
|
228
229
|
estimatedCount(ctx, entity) {
|
|
229
230
|
const meta = getMeta(entity);
|
|
230
|
-
ctx.append(`SELECT SUM(p.rows) ${this.escapeId(
|
|
231
|
+
ctx.append(`SELECT SUM(p.rows) ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM sys.partitions p` +
|
|
231
232
|
` JOIN sys.objects o ON o.object_id = p.object_id` +
|
|
232
233
|
` JOIN sys.schemas s ON s.schema_id = o.schema_id` +
|
|
233
234
|
` WHERE p.index_id IN (0, 1) AND o.name = `);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AGGREGATE_VALUE_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { PG_FEATURES, PgLikeSqlDialect } from '../dialect/pgLikeSqlDialect.js';
|
|
3
3
|
import { getMeta } from '../entity/index.js';
|
|
4
4
|
/** PostgreSQL, under every driver: `pg`, Neon, PGlite, `bun:sql`. Adds pgvector and the `xmax` upsert `created`. */
|
|
@@ -18,7 +18,7 @@ export class PostgresDialect extends PgLikeSqlDialect {
|
|
|
18
18
|
* back raw it would read as a negative row count.
|
|
19
19
|
*/
|
|
20
20
|
estimatedCount(ctx, entity) {
|
|
21
|
-
ctx.append(`SELECT GREATEST(reltuples, 0)::bigint ${this.escapeId(
|
|
21
|
+
ctx.append(`SELECT GREATEST(reltuples, 0)::bigint ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM pg_class WHERE oid = to_regclass(`);
|
|
22
22
|
ctx.addValue(this.escapedTableName(getMeta(entity)));
|
|
23
23
|
ctx.append(')');
|
|
24
24
|
}
|