uql-orm 0.68.1 → 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 +6 -6
- 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 +42 -4
- package/dist/dialect/abstractSqlDialect.js +164 -48
- package/dist/dialect/aliases.d.ts +15 -4
- package/dist/dialect/aliases.js +15 -4
- 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 +44 -2
- package/dist/entity/decorator/members.js +0 -5
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/entity/metadata/definition.js +10 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/postgresIntrospector.js +7 -1
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +49 -19
- package/dist/mongo/mongoDialect.js +238 -81
- package/dist/mongo/mongodbQuerier.d.ts +2 -4
- package/dist/mongo/mongodbQuerier.js +16 -14
- 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 +35 -2
- package/dist/type/entity.d.ts +120 -4
- package/dist/type/migration.d.ts +7 -0
- package/dist/type/query.d.ts +7 -10
- package/dist/type/queryAggregate.d.ts +77 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/queryRaw.d.ts +19 -1
- package/dist/type/queryRaw.js +18 -0
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/util/dialect.util.d.ts +6 -2
- package/dist/util/dialect.util.js +21 -7
- package/dist/util/field.util.d.ts +15 -1
- package/dist/util/field.util.js +18 -1
- package/dist/util/object.util.d.ts +1 -4
- package/dist/util/object.util.js +0 -3
- package/dist/util/raw.d.ts +2 -2
- package/dist/util/raw.js +29 -2
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import {
|
|
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
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
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';
|
|
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)) {
|
|
@@ -165,7 +169,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
165
169
|
appendRelationLookup(filter, meta, relKey, val, lookups) {
|
|
166
170
|
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
167
171
|
const sizeVal = parseRelationSize(val);
|
|
168
|
-
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count:
|
|
172
|
+
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: AGGREGATE_VALUE_ALIAS }];
|
|
169
173
|
const where = (sizeVal === undefined ? val : {});
|
|
170
174
|
lookups.temps.push(temp);
|
|
171
175
|
lookups.stages.push(this.relationLookup(meta, meta.relations[relKey], where, temp, tail));
|
|
@@ -423,6 +427,15 @@ 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) => {
|
|
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.
|
|
432
|
+
const field = meta.fields[key];
|
|
433
|
+
if (field?.computed && !aggregateOf(field)) {
|
|
434
|
+
if (selectMap && key in selectMap) {
|
|
435
|
+
assertReadable(meta, key);
|
|
436
|
+
}
|
|
437
|
+
return acc;
|
|
438
|
+
}
|
|
426
439
|
acc[this.columnOf(meta, key)] = 1;
|
|
427
440
|
return acc;
|
|
428
441
|
}, {});
|
|
@@ -498,18 +511,70 @@ export class MongoDialect extends AbstractDialect {
|
|
|
498
511
|
continue;
|
|
499
512
|
}
|
|
500
513
|
const temp = sortCountField(key);
|
|
501
|
-
stages.push(this.
|
|
514
|
+
stages.push(...this.aggregateStages(meta, { relation: key, op: '$count' }, `${REL_TEMP_PREFIX}${temp}`, temp));
|
|
502
515
|
fields.push(temp);
|
|
503
516
|
}
|
|
504
517
|
return { stages, fields };
|
|
505
518
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
return this.
|
|
519
|
+
/** Whether a read answers with a relation aggregate, which only the pipeline can build. */
|
|
520
|
+
readsAggregates(entity, q) {
|
|
521
|
+
return this.aggregateKeys(entity, q).length > 0;
|
|
522
|
+
}
|
|
523
|
+
/** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
|
|
524
|
+
aggregateKeys(entity, q) {
|
|
525
|
+
const meta = getMeta(entity);
|
|
526
|
+
const projected = normalizeScalarFieldSelection(meta, asSelectMap(q.$select), q.$exclude);
|
|
527
|
+
return [...projected, ...Object.keys(q.$sort ?? {})].filter((key) => aggregateOf(meta.fields[key]));
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
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.
|
|
533
|
+
*/
|
|
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;
|
|
539
|
+
}
|
|
540
|
+
lookups.temps.push(temp);
|
|
541
|
+
lookups.stages.push(...this.aggregateStages(meta, spec, temp, this.columnOf(meta, key)));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* One relation aggregate on the document under `field`: the correlated lookup that reads the related
|
|
545
|
+
* rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
|
|
546
|
+
* the `$addFields` reading that back, `0` or `null` where the lookup matched nothing.
|
|
547
|
+
*
|
|
548
|
+
* Every aggregate MongoDB answers is built here: a `$count` a query asks for, an ordering by one, and
|
|
549
|
+
* a field a `computed` declares, which is the same spec the SQL dialects render as one subquery.
|
|
550
|
+
*/
|
|
551
|
+
aggregateStages(meta, spec, temp, field) {
|
|
552
|
+
const relOpts = relationOf(meta, spec.relation);
|
|
553
|
+
const query = spec.query ?? {};
|
|
554
|
+
const tail = [
|
|
555
|
+
...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query.$sort) }] : []),
|
|
556
|
+
...this.pagerStages(query),
|
|
557
|
+
spec.field
|
|
558
|
+
? {
|
|
559
|
+
$group: {
|
|
560
|
+
_id: null,
|
|
561
|
+
[AGGREGATE_VALUE_ALIAS]: { [spec.op]: `$${this.columnOf(getMeta(relOpts.entity()), spec.field)}` },
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
: { $count: AGGREGATE_VALUE_ALIAS },
|
|
565
|
+
];
|
|
566
|
+
return [
|
|
567
|
+
this.relationLookup(meta, relOpts, query.$where ?? {}, temp, tail),
|
|
568
|
+
{ $addFields: { [field]: this.tally(temp, spec.op) } },
|
|
569
|
+
];
|
|
509
570
|
}
|
|
510
|
-
/**
|
|
511
|
-
|
|
512
|
-
|
|
571
|
+
/**
|
|
572
|
+
* The value a lookup left in `temp`, which holds no row at all where nothing matched: `0` for the
|
|
573
|
+
* aggregates that count something, and `null` for the ones with no value to report.
|
|
574
|
+
*/
|
|
575
|
+
tally(temp, op = '$count') {
|
|
576
|
+
const empty = op === '$count' || op === '$sum' ? 0 : null;
|
|
577
|
+
return { $ifNull: [{ $arrayElemAt: [`$${temp}.${AGGREGATE_VALUE_ALIAS}`, 0] }, empty] };
|
|
513
578
|
}
|
|
514
579
|
/**
|
|
515
580
|
* The lookups reading each to-many a query populates, and the tally of each `$count`, onto the fields
|
|
@@ -523,12 +588,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
523
588
|
for (const relKey of getRelationRequestSummary(meta, q.$populate).toManyKeys) {
|
|
524
589
|
stages.push(...this.toManyLookup(meta, relKey, parseRelationAtKey(relKey, q.$populate).query, temps));
|
|
525
590
|
}
|
|
526
|
-
for (const { relKey,
|
|
591
|
+
for (const { relKey, where } of countedRelations(meta, q.$count)) {
|
|
527
592
|
const temp = `${REL_TEMP_PREFIX}count_${relKey}`;
|
|
528
593
|
temps.push(temp);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
});
|
|
594
|
+
const spec = { relation: relKey, op: '$count', query: { $where: where } };
|
|
595
|
+
stages.push(...this.aggregateStages(meta, spec, temp, `${COUNT_RESULT_KEY}.${relKey}`));
|
|
532
596
|
}
|
|
533
597
|
return temps.length ? [...stages, { $unset: temps }] : stages;
|
|
534
598
|
}
|
|
@@ -575,7 +639,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
575
639
|
return false;
|
|
576
640
|
}
|
|
577
641
|
const meta = getMeta(entity);
|
|
578
|
-
return someKey(sort, (key) =>
|
|
642
|
+
return someKey(sort, (key) => !!meta.relations[key]);
|
|
579
643
|
}
|
|
580
644
|
/**
|
|
581
645
|
* Aggregate results are keyed by `$group`/`$select` alias rather than by column, so an aggregate
|
|
@@ -595,18 +659,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
595
659
|
pathOf(meta, key) {
|
|
596
660
|
const dot = key.indexOf('.');
|
|
597
661
|
if (dot < 0) {
|
|
662
|
+
assertReadable(meta, key);
|
|
598
663
|
return this.columnOf(meta, key);
|
|
599
664
|
}
|
|
600
665
|
return this.columnOf(meta, key.slice(0, dot)) + key.slice(dot);
|
|
601
666
|
}
|
|
602
667
|
aggregationPipeline(entity, q, opts) {
|
|
603
|
-
// Lookups that a relation condition needs come first, then the match that reads them, then the
|
|
604
|
-
// temporary fields are dropped so they never reach the caller.
|
|
605
|
-
const { stages, filter, unset } = this.whereWithRelations(entity, q.$where, opts);
|
|
606
668
|
return [
|
|
607
|
-
...
|
|
608
|
-
...(hasKeys(filter) ? [{ $match: filter }] : []),
|
|
609
|
-
...(unset.length ? [{ $unset: unset }] : []),
|
|
669
|
+
...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
|
|
610
670
|
...this.readStages(entity, q, {
|
|
611
671
|
sort: this.sort(entity, q.$sort, q.$populate),
|
|
612
672
|
pager: this.pagerStages(q),
|
|
@@ -955,33 +1015,19 @@ export class MongoDialect extends AbstractDialect {
|
|
|
955
1015
|
* Build MongoDB aggregation pipeline stages from a QueryAggregate.
|
|
956
1016
|
*/
|
|
957
1017
|
buildAggregateStages(entity, q, opts) {
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
pipeline.push({ $group: { _id: hasKeys(groupId) ? groupId : null, ...groupAccumulators } });
|
|
969
|
-
// Project stage - rename _id fields back to their original names, and reduce collected distinct
|
|
970
|
-
// sets. Needed whenever there are group keys OR any distinct alias.
|
|
971
|
-
if (hasKeys(groupId) || distinctReducers.size) {
|
|
972
|
-
const project = { _id: 0 };
|
|
973
|
-
for (const alias of Object.keys(groupId)) {
|
|
974
|
-
project[alias] = `$_id.${alias}`;
|
|
975
|
-
}
|
|
976
|
-
for (const alias of Object.keys(groupAccumulators)) {
|
|
977
|
-
const reduceOp = distinctReducers.get(alias);
|
|
978
|
-
project[alias] = reduceOp ? { [reduceOp]: `$${alias}` } : 1;
|
|
979
|
-
}
|
|
980
|
-
pipeline.push({ $project: project });
|
|
981
|
-
}
|
|
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
|
+
];
|
|
982
1028
|
// Everything the pipeline emits, which is all `$having` and `$sort` may name. The `$project`
|
|
983
1029
|
// above has already dropped the rest, so an unchecked key matched nothing or ordered by nothing.
|
|
984
|
-
const emitted = new Set(
|
|
1030
|
+
const emitted = new Set(Object.keys(columns));
|
|
985
1031
|
// $match stage for HAVING (post-group filtering)
|
|
986
1032
|
if (q.$having) {
|
|
987
1033
|
assertAggregateColumns(q.$having, emitted, '$having');
|
|
@@ -1002,39 +1048,139 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1002
1048
|
return pipeline;
|
|
1003
1049
|
}
|
|
1004
1050
|
/**
|
|
1005
|
-
* Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage
|
|
1006
|
-
* `
|
|
1007
|
-
*
|
|
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.
|
|
1008
1055
|
*/
|
|
1009
|
-
buildGroupSpec(meta, groupEntries) {
|
|
1056
|
+
buildGroupSpec(meta, groupEntries, joins) {
|
|
1010
1057
|
if (!groupEntries.length) {
|
|
1011
1058
|
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
1012
1059
|
}
|
|
1013
1060
|
const groupId = {};
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1061
|
+
const accumulators = {};
|
|
1062
|
+
const columns = {};
|
|
1063
|
+
const named = [];
|
|
1016
1064
|
for (const entry of groupEntries) {
|
|
1017
1065
|
// Aliases stay as the caller wrote them ($project maps them back); the *refs* address columns.
|
|
1018
|
-
const ref = entry.kind === 'key' ? `$${this.columnOf(meta, entry.alias)}` : `$${this.columnOf(meta, entry.fieldRef)}`;
|
|
1019
1066
|
if (entry.kind === 'key') {
|
|
1020
|
-
groupId[entry.alias] =
|
|
1067
|
+
groupId[entry.alias] = `$${this.groupedPath(meta, joins, entry.path, named)}`;
|
|
1068
|
+
columns[entry.alias] = `$_id.${entry.alias}`;
|
|
1069
|
+
continue;
|
|
1021
1070
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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}` };
|
|
1026
1080
|
}
|
|
1027
1081
|
else if (entry.op === '$count') {
|
|
1028
1082
|
// COUNT(*) counts every row; COUNT(field) counts non-null values, matching SQL.
|
|
1029
|
-
|
|
1030
|
-
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) };
|
|
1031
1084
|
}
|
|
1032
1085
|
else {
|
|
1033
|
-
// `$sum`, `$avg`, `$min` and `$max` are MongoDB accumulators of the same name.
|
|
1034
|
-
|
|
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 };
|
|
1135
|
+
}
|
|
1136
|
+
if (key.startsWith('$')) {
|
|
1137
|
+
throw new TypeError(`aggregate $where operator '${key}' is not supported on MongoDB`);
|
|
1035
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);
|
|
1036
1152
|
}
|
|
1037
|
-
|
|
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 };
|
|
1038
1184
|
}
|
|
1039
1185
|
buildHavingFilter(having) {
|
|
1040
1186
|
const filter = {};
|
|
@@ -1113,3 +1259,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1113
1259
|
function sortDirection(value) {
|
|
1114
1260
|
return value === 'desc' || value === -1 ? -1 : 1;
|
|
1115
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* A `computed` field writing SQL is refused wherever a query names it, since no document engine
|
|
1264
|
+
* evaluates SQL and answering with the property name would hand back `undefined` for every row. One
|
|
1265
|
+
* computing a relation aggregate is read: `appendAggregateField` builds it.
|
|
1266
|
+
*/
|
|
1267
|
+
function assertReadable(meta, key) {
|
|
1268
|
+
const field = meta.fields[key];
|
|
1269
|
+
if (field?.computed && !aggregateOf(field)) {
|
|
1270
|
+
throw new TypeError(`cannot read '${meta.entity.name}.${key}' on MongoDB: a 'computed' field writing SQL is not something a document engine evaluates`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
@@ -18,8 +18,8 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
18
18
|
*/
|
|
19
19
|
private readCursor;
|
|
20
20
|
/**
|
|
21
|
-
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
22
|
-
*
|
|
21
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter or
|
|
22
|
+
* order by a relation, one to build a relation aggregate, and the grouping `$distinct` is.
|
|
23
23
|
*/
|
|
24
24
|
private readsThroughPipeline;
|
|
25
25
|
private buildScalarProjection;
|
|
@@ -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';
|
|
@@ -68,15 +68,16 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
68
68
|
: this.buildFindCursor(entity, q, opts);
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
72
|
-
*
|
|
71
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter or
|
|
72
|
+
* order by a relation, one to build a relation aggregate, and the grouping `$distinct` is.
|
|
73
73
|
*/
|
|
74
74
|
readsThroughPipeline(entity, q) {
|
|
75
75
|
return (!!q.$distinct ||
|
|
76
76
|
hasKeys(q.$count) ||
|
|
77
77
|
populatesRelations(getMeta(entity), q.$populate) ||
|
|
78
78
|
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
79
|
-
this.dialect.sortsRelations(entity, q.$sort)
|
|
79
|
+
this.dialect.sortsRelations(entity, q.$sort) ||
|
|
80
|
+
this.dialect.readsAggregates(entity, q));
|
|
80
81
|
}
|
|
81
82
|
buildScalarProjection(entity, q) {
|
|
82
83
|
return this.dialect.select(entity, q.$select, q.$exclude);
|
|
@@ -142,10 +143,10 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
142
143
|
const [founds, counted] = await Promise.all([
|
|
143
144
|
this.internalFindMany(entity, q, opts),
|
|
144
145
|
this.execute((session) => this.collection(entity)
|
|
145
|
-
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count:
|
|
146
|
+
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count: AGGREGATE_VALUE_ALIAS }], { session })
|
|
146
147
|
.toArray()),
|
|
147
148
|
]);
|
|
148
|
-
return [founds, counted[0]?.[
|
|
149
|
+
return [founds, counted[0]?.[AGGREGATE_VALUE_ALIAS] ?? 0];
|
|
149
150
|
}
|
|
150
151
|
/** The pipeline `countDocuments` runs, spelled out so a relation condition gets its lookups and a page its stages. */
|
|
151
152
|
async internalCount(entity, q, opts) {
|
|
@@ -153,10 +154,15 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
153
154
|
return 0;
|
|
154
155
|
}
|
|
155
156
|
return this.timed('internalCount', undefined, async () => {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
166
|
});
|
|
161
167
|
}
|
|
162
168
|
/**
|
|
@@ -166,10 +172,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
166
172
|
async estimatedCount(entity) {
|
|
167
173
|
return this.timed('estimatedCount', undefined, async () => this.execute((session) => this.collection(entity).estimatedDocumentCount({ session })));
|
|
168
174
|
}
|
|
169
|
-
/** A `find` filter cannot host the `$lookup` a relation condition needs, so such a write names its rows by id. */
|
|
170
|
-
settlesWrite(entity, q) {
|
|
171
|
-
return super.settlesWrite(entity, q) || this.dialect.constrainsRelations(entity, q.$where);
|
|
172
|
-
}
|
|
173
175
|
async internalInsertMany(entity, rows) {
|
|
174
176
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
175
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
|
}
|