@atscript/db-mongo 0.1.151 → 0.1.153
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/agg.cjs +6 -4
- package/dist/agg.mjs +6 -4
- package/dist/index.cjs +388 -227
- package/dist/index.d.cts +37 -1
- package/dist/index.d.mts +37 -1
- package/dist/index.mjs +389 -228
- package/dist/{mongo-accumulator-BQBMx4GC.cjs → mongo-accumulator-BW-RNFg0.cjs} +136 -34
- package/dist/{mongo-accumulator-DYhg8usT.mjs → mongo-accumulator-DMPFOscc.mjs} +119 -35
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_accumulator = require("./mongo-accumulator-
|
|
2
|
+
const require_mongo_accumulator = require("./mongo-accumulator-BW-RNFg0.cjs");
|
|
3
3
|
let _atscript_db = require("@atscript/db");
|
|
4
4
|
let mongodb = require("mongodb");
|
|
5
5
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
@@ -395,6 +395,262 @@ function mongoIndexKey(type, name) {
|
|
|
395
395
|
return `${INDEX_PREFIX}${type}__${name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 117 - type.length - 2)}`;
|
|
396
396
|
}
|
|
397
397
|
//#endregion
|
|
398
|
+
//#region src/lib/mongo-view-pipeline.ts
|
|
399
|
+
/** Where a joined table's document lands (`$lookup.as`, then unwound). */
|
|
400
|
+
function joinedAs(table) {
|
|
401
|
+
return `${JOINED_PREFIX}${table}`;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Builds the aggregation pipeline of a managed MongoDB view (created with
|
|
405
|
+
* `viewOn: <entry collection>`):
|
|
406
|
+
*
|
|
407
|
+
* 1. per join, in declaration order: `$lookup` + `$unwind` (inner joins drop
|
|
408
|
+
* unmatched documents, left joins keep them — `preserveNullAndEmptyArrays`);
|
|
409
|
+
* 2. `@db.view.filter` → `$match`;
|
|
410
|
+
* 3. aggregates → `$group` (+ `$addFields` turning `countDistinct` sets into
|
|
411
|
+
* their sizes, + `@db.view.having` → `$match`) + `$project`, otherwise a
|
|
412
|
+
* flat `$project`. A conditional aggregate's predicate reads the same
|
|
413
|
+
* pipeline paths (SQL null semantics — `queryNodeToExpr`).
|
|
414
|
+
*
|
|
415
|
+
* All field paths are PHYSICAL document paths (`@db.column` renames), joined
|
|
416
|
+
* tables live under `__joined_<scope>` — the physical collection, or the
|
|
417
|
+
* `@db.alias` type name for an aliased join (`__joined_Manager`), whose
|
|
418
|
+
* `$lookup.from` stays the physical collection (since 0.1.141).
|
|
419
|
+
* `variant` (since 0.1.153) renders a pruned read variant of the view
|
|
420
|
+
* (`AtscriptDbView.readPlan`) instead of its full definition.
|
|
421
|
+
* The entry collection and a `$lookup.from` may be views. A column whose source may be missing
|
|
422
|
+
* (`nullable` mapping) reads as null; every other column is a plain path, so
|
|
423
|
+
* a `$match` / `$sort` on the view can still push down to an index.
|
|
424
|
+
* @since 0.1.136
|
|
425
|
+
*/
|
|
426
|
+
function buildViewPipeline(view, variant) {
|
|
427
|
+
const plan = variant?.plan ?? view.viewPlan;
|
|
428
|
+
const columns = variant?.columns ?? view.getViewColumnMappings();
|
|
429
|
+
const pipeline = [];
|
|
430
|
+
const prefixes = new Map([[plan.entryTable, ""]]);
|
|
431
|
+
const pathOf = (ref) => {
|
|
432
|
+
const { table, source } = view.resolveRefSource(ref);
|
|
433
|
+
const prefix = prefixes.get(table);
|
|
434
|
+
if (prefix === void 0) throw new Error(`View "${view.tableName}": "${table}" is not joined before it is referenced`);
|
|
435
|
+
return prefix + source.column;
|
|
436
|
+
};
|
|
437
|
+
for (const join of plan.joins) {
|
|
438
|
+
pipeline.push(buildLookup(view, join, prefixes));
|
|
439
|
+
pipeline.push({ $unwind: {
|
|
440
|
+
path: `$${joinedAs(join.scope)}`,
|
|
441
|
+
preserveNullAndEmptyArrays: join.kind === "left"
|
|
442
|
+
} });
|
|
443
|
+
prefixes.set(join.scope, `${joinedAs(join.scope)}.`);
|
|
444
|
+
}
|
|
445
|
+
if (plan.filter) pipeline.push({ $match: viewMatch(plan.filter, pathOf) });
|
|
446
|
+
/** A column's source field as an aggregation operand. */
|
|
447
|
+
const colSourceField = (col) => `$${prefixes.get(col.sourceTable) ?? ""}${col.sourceColumn}`;
|
|
448
|
+
const colValue = (col) => col.nullable ? require_mongo_accumulator.orNull(colSourceField(col)) : colSourceField(col);
|
|
449
|
+
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
450
|
+
const rendered = /* @__PURE__ */ new Map();
|
|
451
|
+
const computedExpr = (col, leaf) => {
|
|
452
|
+
let out = rendered.get(col.viewPath);
|
|
453
|
+
if (out === void 0) {
|
|
454
|
+
out = require_mongo_accumulator.exprToMongo(col.expr, (path) => {
|
|
455
|
+
const c = byPath.get(path);
|
|
456
|
+
return c.expr === void 0 ? { $toDouble: leaf(c) } : computedExpr(c, leaf);
|
|
457
|
+
});
|
|
458
|
+
rendered.set(col.viewPath, out);
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
};
|
|
462
|
+
if (columns.some((c) => c.aggFn)) {
|
|
463
|
+
const group = { _id: {} };
|
|
464
|
+
const project = { _id: 0 };
|
|
465
|
+
const distinctSizes = {};
|
|
466
|
+
const computed = {};
|
|
467
|
+
for (const col of columns) if (col.expr !== void 0) {
|
|
468
|
+
computed[col.viewColumn] = computedExpr(col, (c) => c.aggFn ? `$${c.viewColumn}` : `$_id.${c.viewColumn}`);
|
|
469
|
+
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
470
|
+
} else if (col.aggFn) {
|
|
471
|
+
const predicate = col.aggFilter ? require_mongo_accumulator.queryNodeToExpr(col.aggFilter, (ref) => `$${pathOf(ref)}`) : void 0;
|
|
472
|
+
group[col.viewColumn] = require_mongo_accumulator.buildAccumulator(col.aggFn, col.aggField === "*" ? "*" : colSourceField(col), predicate, col.viewColumn);
|
|
473
|
+
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
474
|
+
if (col.aggFn === "countDistinct") distinctSizes[col.viewColumn] = require_mongo_accumulator.distinctCountExpr(`$${col.viewColumn}`);
|
|
475
|
+
} else {
|
|
476
|
+
group._id[col.viewColumn] = colValue(col);
|
|
477
|
+
project[col.viewColumn] = `$_id.${col.viewColumn}`;
|
|
478
|
+
}
|
|
479
|
+
pipeline.push({ $group: group });
|
|
480
|
+
if (Object.keys(distinctSizes).length > 0) pipeline.push({ $addFields: distinctSizes });
|
|
481
|
+
if (Object.keys(computed).length > 0) pipeline.push({ $addFields: computed });
|
|
482
|
+
if (plan.having) pipeline.push({ $match: havingMatch(plan.having, byPath) });
|
|
483
|
+
pipeline.push({ $project: project });
|
|
484
|
+
} else {
|
|
485
|
+
const project = { _id: 0 };
|
|
486
|
+
for (const col of columns) project[col.viewColumn] = col.expr === void 0 ? colValue(col) : computedExpr(col, colValue);
|
|
487
|
+
pipeline.push({ $project: project });
|
|
488
|
+
}
|
|
489
|
+
return pipeline;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* One join's `$lookup`.
|
|
493
|
+
*
|
|
494
|
+
* The simple `localField` / `foreignField` form is used whenever it is exact:
|
|
495
|
+
* a single `=` between a field of the joined table and a field of an outer
|
|
496
|
+
* table (the entry or an earlier join), with the joined field REQUIRED — the
|
|
497
|
+
* simple form matches a null / missing local value to null / missing foreign
|
|
498
|
+
* values, which a required foreign field can never hold. It is kept (rather
|
|
499
|
+
* than always using the pipeline form) because it can use an index on the
|
|
500
|
+
* foreign field on every MongoDB version; the pipeline form's `$expr` match
|
|
501
|
+
* can't before MongoDB 5.0. Everything else uses the pipeline form (`let` +
|
|
502
|
+
* `$match: { $expr }`, SQL null semantics).
|
|
503
|
+
*/
|
|
504
|
+
function buildLookup(view, join, outer) {
|
|
505
|
+
const scope = join.scope;
|
|
506
|
+
const as = joinedAs(scope);
|
|
507
|
+
const simple = join.first ? void 0 : simpleJoinFields(view, join, outer);
|
|
508
|
+
if (simple) return { $lookup: {
|
|
509
|
+
from: join.targetTable,
|
|
510
|
+
...simple,
|
|
511
|
+
as
|
|
512
|
+
} };
|
|
513
|
+
const letVars = {};
|
|
514
|
+
const varByPath = /* @__PURE__ */ new Map();
|
|
515
|
+
const pathOf = (ref) => {
|
|
516
|
+
const { table, source } = view.resolveRefSource(ref);
|
|
517
|
+
if (table === scope) return `$${source.column}`;
|
|
518
|
+
const prefix = outer.get(table);
|
|
519
|
+
if (prefix === void 0) throw new Error(`View "${view.tableName}": the join on "${scope}" references "${table}", which is not joined before it`);
|
|
520
|
+
const outerPath = prefix + source.column;
|
|
521
|
+
let varName = varByPath.get(outerPath);
|
|
522
|
+
if (!varName) {
|
|
523
|
+
varName = `v${varByPath.size}`;
|
|
524
|
+
varByPath.set(outerPath, varName);
|
|
525
|
+
letVars[varName] = `$${outerPath}`;
|
|
526
|
+
}
|
|
527
|
+
return `$$${varName}`;
|
|
528
|
+
};
|
|
529
|
+
const pipeline = [{ $match: { $expr: require_mongo_accumulator.queryNodeToExpr(join.condition, pathOf) } }];
|
|
530
|
+
if (join.first) {
|
|
531
|
+
const sort = {};
|
|
532
|
+
for (const { ref, desc } of join.first.order) sort[view.resolveRefSource(ref).source.column] = desc ? -1 : 1;
|
|
533
|
+
pipeline.push({ $sort: sort }, { $limit: 1 });
|
|
534
|
+
}
|
|
535
|
+
return { $lookup: {
|
|
536
|
+
from: join.targetTable,
|
|
537
|
+
...varByPath.size > 0 ? { let: letVars } : {},
|
|
538
|
+
pipeline,
|
|
539
|
+
as
|
|
540
|
+
} };
|
|
541
|
+
}
|
|
542
|
+
/** `{ localField, foreignField }` when the simple `$lookup` form is exact, else `undefined`. */
|
|
543
|
+
function simpleJoinFields(view, join, outer) {
|
|
544
|
+
let node = join.condition;
|
|
545
|
+
if ("$and" in node) {
|
|
546
|
+
const children = node.$and;
|
|
547
|
+
if (children.length !== 1) return void 0;
|
|
548
|
+
node = children[0];
|
|
549
|
+
}
|
|
550
|
+
if ("$or" in node || "$not" in node || "$and" in node) return void 0;
|
|
551
|
+
const comp = node;
|
|
552
|
+
if (comp.op !== "$eq" || !(0, _atscript_db.isFieldRef)(comp.right)) return void 0;
|
|
553
|
+
const scope = join.scope;
|
|
554
|
+
const left = view.resolveRefSource(comp.left);
|
|
555
|
+
const right = view.resolveRefSource(comp.right);
|
|
556
|
+
let target;
|
|
557
|
+
let local;
|
|
558
|
+
if (left.table === scope && right.table !== scope) {
|
|
559
|
+
target = left;
|
|
560
|
+
local = right;
|
|
561
|
+
} else if (right.table === scope && left.table !== scope) {
|
|
562
|
+
target = right;
|
|
563
|
+
local = left;
|
|
564
|
+
} else return;
|
|
565
|
+
const localPrefix = outer.get(local.table);
|
|
566
|
+
if (localPrefix === void 0) return void 0;
|
|
567
|
+
const foreign = target.source;
|
|
568
|
+
if (!(!foreign.optional && foreign.designType !== "union" && foreign.designType !== "unknown")) return void 0;
|
|
569
|
+
return {
|
|
570
|
+
localField: localPrefix + local.source.column,
|
|
571
|
+
foreignField: foreign.column
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* A view predicate (`@db.view.filter`, `@db.view.having`) as a `$match`
|
|
576
|
+
* over pipeline paths: the shared query translation (`translateQueryTree` →
|
|
577
|
+
* `buildMongoFilter`), so operators mean what they mean in queries (`exists`
|
|
578
|
+
* = holds a value, `matches` takes `/re/flags`); a field-to-field comparison
|
|
579
|
+
* becomes `$expr`.
|
|
580
|
+
*/
|
|
581
|
+
function viewMatch(node, pathOf) {
|
|
582
|
+
return require_mongo_accumulator.buildMongoFilter((0, _atscript_db.translateQueryTree)(node, pathOf), { fieldOperands: true });
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* `@db.view.having` as a `$match` after `$group`: refs name view fields —
|
|
586
|
+
* aggregates are top-level, dimensions under `_id`.
|
|
587
|
+
*/
|
|
588
|
+
function havingMatch(node, colMap) {
|
|
589
|
+
return viewMatch(node, (ref) => {
|
|
590
|
+
const col = ref.type ? void 0 : colMap.get(ref.field);
|
|
591
|
+
if (!col) return ref.field;
|
|
592
|
+
return col.aggFn || col.expr !== void 0 ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/lib/mongo-view-read.ts
|
|
597
|
+
/** Rendered prefixes per read variant (variants are memoised by their view). */
|
|
598
|
+
const prefixes = /* @__PURE__ */ new WeakMap();
|
|
599
|
+
/**
|
|
600
|
+
* The pruned read of `view` for `query`, or `undefined` to read the stored
|
|
601
|
+
* view: nothing droppable, an external view, or an entry that is itself a
|
|
602
|
+
* view (its collation and pipeline would be read twice).
|
|
603
|
+
*/
|
|
604
|
+
function mongoViewRead(view, query, kind) {
|
|
605
|
+
const variant = view.readPlan((0, _atscript_db.queryReadColumns)(query, kind));
|
|
606
|
+
if (!variant || (0, _atscript_db.isViewType)(variant.plan.entryType())) return void 0;
|
|
607
|
+
let prefix = prefixes.get(variant);
|
|
608
|
+
if (!prefix) {
|
|
609
|
+
prefix = buildViewPipeline(view, variant);
|
|
610
|
+
prefixes.set(variant, prefix);
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
entry: variant.plan.entryTable,
|
|
614
|
+
prefix,
|
|
615
|
+
dropped: variant.droppedColumns
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function isPlainObject$1(value) {
|
|
619
|
+
if (value === null || typeof value !== "object") return false;
|
|
620
|
+
const proto = Object.getPrototypeOf(value);
|
|
621
|
+
return proto === Object.prototype || proto === null;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Whether any stage may read one of the `dropped` view columns: a field path
|
|
625
|
+
* string (`"$col"`, `"col.sub"`, a `localField`), an object key naming it, or
|
|
626
|
+
* a whole-document variable (`$$ROOT` / `$$CURRENT`). Literals that happen to
|
|
627
|
+
* equal a column name match too — that only falls back to the stored view.
|
|
628
|
+
*
|
|
629
|
+
* MongoDB never rejects a missing field, so this scan — not an engine error —
|
|
630
|
+
* is what keeps a column reference the read-column collector did not report
|
|
631
|
+
* from silently reading `null`.
|
|
632
|
+
*/
|
|
633
|
+
function stagesReadAny(stages, dropped) {
|
|
634
|
+
if (dropped.length === 0) return false;
|
|
635
|
+
const names = (raw) => {
|
|
636
|
+
if (raw.startsWith("$$")) {
|
|
637
|
+
const variable = raw.slice(2).split(".")[0];
|
|
638
|
+
return variable === "ROOT" || variable === "CURRENT";
|
|
639
|
+
}
|
|
640
|
+
const path = raw.startsWith("$") ? raw.slice(1) : raw;
|
|
641
|
+
if (!path) return false;
|
|
642
|
+
return dropped.some((col) => path === col || path.startsWith(`${col}.`) || col.startsWith(`${path}.`));
|
|
643
|
+
};
|
|
644
|
+
const visit = (value) => {
|
|
645
|
+
if (typeof value === "string") return names(value);
|
|
646
|
+
if (Array.isArray(value)) return value.some(visit);
|
|
647
|
+
if (!isPlainObject$1(value)) return false;
|
|
648
|
+
for (const [key, child] of Object.entries(value)) if (names(key) || visit(child)) return true;
|
|
649
|
+
return false;
|
|
650
|
+
};
|
|
651
|
+
return visit(stages);
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
398
654
|
//#region src/lib/mongo-relations.ts
|
|
399
655
|
/** Join key of a row's primary key values (`fields` = the row's own names for them). */
|
|
400
656
|
function buildPKKey(fields, doc) {
|
|
@@ -619,21 +875,23 @@ function buildLookupInnerPipeline(target, withRel, relation, requiredFields) {
|
|
|
619
875
|
const flatRel = withRel;
|
|
620
876
|
const nested = withRel.controls || {};
|
|
621
877
|
const sort = nested.$sort || flatRel.$sort;
|
|
878
|
+
const nulls = nested.$nulls || flatRel.$nulls;
|
|
622
879
|
const limit = nested.$limit ?? flatRel.$limit;
|
|
623
880
|
const skip = nested.$skip ?? flatRel.$skip;
|
|
624
881
|
const select = nested.$select || flatRel.$select;
|
|
625
882
|
const statics = (0, _atscript_db.relationStaticFilter)(relation, withRel.name);
|
|
626
883
|
const controls = {};
|
|
627
884
|
if (sort) controls.$sort = sort;
|
|
885
|
+
if (nulls) controls.$nulls = nulls;
|
|
628
886
|
if (select) controls.$select = select;
|
|
629
887
|
const translated = target._translateForAdapter({
|
|
630
888
|
filter: (0, _atscript_db.andFilters)(statics.target, withRel.filter),
|
|
631
889
|
controls
|
|
632
890
|
});
|
|
633
|
-
const page =
|
|
634
|
-
if (translated.controls?.$sort) page.push({ $sort: translated.controls.$sort });
|
|
891
|
+
const { stages: page, cleanup } = require_mongo_accumulator.sortStages(translated.controls);
|
|
635
892
|
if (skip) page.push({ $skip: skip });
|
|
636
893
|
if (limit !== null && limit !== void 0) page.push({ $limit: limit });
|
|
894
|
+
if (cleanup) page.push(cleanup);
|
|
637
895
|
const projection = translated.controls?.$select?.asProjection;
|
|
638
896
|
if (projection) {
|
|
639
897
|
const project = { ...projection };
|
|
@@ -902,13 +1160,22 @@ function resolveGeoKeyPath(host, indexName) {
|
|
|
902
1160
|
}]);
|
|
903
1161
|
return field;
|
|
904
1162
|
}
|
|
905
|
-
/**
|
|
1163
|
+
/**
|
|
1164
|
+
* Appends a `$project` stage, keeping the computed distance in inclusion mode.
|
|
1165
|
+
* The distance key goes on a COPY: `asProjection` (and `dedupeProjection`)
|
|
1166
|
+
* hand back the caller's own `$select` map when nothing needs rewriting — an
|
|
1167
|
+
* object the caller may share across queries (moost-db memoizes, and in dev /
|
|
1168
|
+
* test deep-freezes, the projection of a read without `$select`).
|
|
1169
|
+
*/
|
|
906
1170
|
function pushGeoProjection(stages, controls) {
|
|
907
1171
|
const projection = controls?.$select?.asProjection;
|
|
908
1172
|
if (!projection) return;
|
|
909
1173
|
const deduped = dedupeProjection(projection);
|
|
910
|
-
|
|
911
|
-
stages.push({ $project:
|
|
1174
|
+
const isInclusion = Object.values(deduped).some((v) => v === 1);
|
|
1175
|
+
stages.push({ $project: isInclusion ? {
|
|
1176
|
+
...deduped,
|
|
1177
|
+
[DISTANCE_FIELD]: 1
|
|
1178
|
+
} : deduped });
|
|
912
1179
|
}
|
|
913
1180
|
/** Renames the internal distance field to the public `$distance` pseudo-field. */
|
|
914
1181
|
function renameDistance(row) {
|
|
@@ -1218,11 +1485,13 @@ async function runSearchPipeline(host, stage, query, label, { threshold, classic
|
|
|
1218
1485
|
pipeline.push({ $match: { _score: { $gte: threshold } } });
|
|
1219
1486
|
} else if (classicText) pipeline.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
1220
1487
|
pipeline.push(...filterStages ?? require_mongo_accumulator.mongoFilterStages(query.filter, filterOptionsOf(host)));
|
|
1221
|
-
|
|
1488
|
+
const { stages: sorted, cleanup } = require_mongo_accumulator.sortStages(controls);
|
|
1489
|
+
if (sorted.length > 0) pipeline.push(...sorted);
|
|
1222
1490
|
else if (classicText) pipeline.push({ $sort: { _score: -1 } });
|
|
1223
1491
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
1224
1492
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
1225
1493
|
else pipeline.push({ $limit: 1e3 });
|
|
1494
|
+
if (cleanup) pipeline.push(cleanup);
|
|
1226
1495
|
if (controls.$select) {
|
|
1227
1496
|
const projection = controls.$select.asProjection;
|
|
1228
1497
|
if (projection) pipeline.push({ $project: dedupeProjection(projection) });
|
|
@@ -1247,11 +1516,11 @@ async function runSearchWithCountPipeline(host, stage, query, label, { threshold
|
|
|
1247
1516
|
preStages.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
|
|
1248
1517
|
preStages.push({ $match: { _score: { $gte: threshold } } });
|
|
1249
1518
|
} else if (classicText) preStages.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
1250
|
-
const dataStages =
|
|
1251
|
-
if (
|
|
1252
|
-
else if (classicText) dataStages.push({ $sort: { _score: -1 } });
|
|
1519
|
+
const { stages: dataStages, cleanup } = require_mongo_accumulator.sortStages(controls);
|
|
1520
|
+
if (dataStages.length === 0 && classicText) dataStages.push({ $sort: { _score: -1 } });
|
|
1253
1521
|
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
1254
1522
|
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
1523
|
+
if (cleanup) dataStages.push(cleanup);
|
|
1255
1524
|
if (controls.$select) {
|
|
1256
1525
|
const projection = controls.$select.asProjection;
|
|
1257
1526
|
if (projection) dataStages.push({ $project: dedupeProjection(projection) });
|
|
@@ -1301,202 +1570,6 @@ async function runSearchWithMeta(host, stage, dataStages, label) {
|
|
|
1301
1570
|
};
|
|
1302
1571
|
}
|
|
1303
1572
|
//#endregion
|
|
1304
|
-
//#region src/lib/mongo-view-pipeline.ts
|
|
1305
|
-
/** Where a joined table's document lands (`$lookup.as`, then unwound). */
|
|
1306
|
-
function joinedAs(table) {
|
|
1307
|
-
return `${JOINED_PREFIX}${table}`;
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Builds the aggregation pipeline of a managed MongoDB view (created with
|
|
1311
|
-
* `viewOn: <entry collection>`):
|
|
1312
|
-
*
|
|
1313
|
-
* 1. per join, in declaration order: `$lookup` + `$unwind` (inner joins drop
|
|
1314
|
-
* unmatched documents, left joins keep them — `preserveNullAndEmptyArrays`);
|
|
1315
|
-
* 2. `@db.view.filter` → `$match`;
|
|
1316
|
-
* 3. aggregates → `$group` (+ `$addFields` turning `countDistinct` sets into
|
|
1317
|
-
* their sizes, + `@db.view.having` → `$match`) + `$project`, otherwise a
|
|
1318
|
-
* flat `$project`. A conditional aggregate's predicate reads the same
|
|
1319
|
-
* pipeline paths (SQL null semantics — `queryNodeToExpr`).
|
|
1320
|
-
*
|
|
1321
|
-
* All field paths are PHYSICAL document paths (`@db.column` renames), joined
|
|
1322
|
-
* tables live under `__joined_<scope>` — the physical collection, or the
|
|
1323
|
-
* `@db.alias` type name for an aliased join (`__joined_Manager`), whose
|
|
1324
|
-
* `$lookup.from` stays the physical collection (since 0.1.141).
|
|
1325
|
-
* The entry collection and a `$lookup.from` may be views. A column whose source may be missing
|
|
1326
|
-
* (`nullable` mapping) reads as null; every other column is a plain path, so
|
|
1327
|
-
* a `$match` / `$sort` on the view can still push down to an index.
|
|
1328
|
-
* @since 0.1.136
|
|
1329
|
-
*/
|
|
1330
|
-
function buildViewPipeline(view) {
|
|
1331
|
-
const plan = view.viewPlan;
|
|
1332
|
-
const columns = view.getViewColumnMappings();
|
|
1333
|
-
const pipeline = [];
|
|
1334
|
-
const prefixes = new Map([[plan.entryTable, ""]]);
|
|
1335
|
-
const pathOf = (ref) => {
|
|
1336
|
-
const { table, source } = view.resolveRefSource(ref);
|
|
1337
|
-
const prefix = prefixes.get(table);
|
|
1338
|
-
if (prefix === void 0) throw new Error(`View "${view.tableName}": "${table}" is not joined before it is referenced`);
|
|
1339
|
-
return prefix + source.column;
|
|
1340
|
-
};
|
|
1341
|
-
for (const join of plan.joins) {
|
|
1342
|
-
pipeline.push(buildLookup(view, join, prefixes));
|
|
1343
|
-
pipeline.push({ $unwind: {
|
|
1344
|
-
path: `$${joinedAs(join.scope)}`,
|
|
1345
|
-
preserveNullAndEmptyArrays: join.kind === "left"
|
|
1346
|
-
} });
|
|
1347
|
-
prefixes.set(join.scope, `${joinedAs(join.scope)}.`);
|
|
1348
|
-
}
|
|
1349
|
-
if (plan.filter) pipeline.push({ $match: viewMatch(plan.filter, pathOf) });
|
|
1350
|
-
/** A column's source field as an aggregation operand. */
|
|
1351
|
-
const colSourceField = (col) => `$${prefixes.get(col.sourceTable) ?? ""}${col.sourceColumn}`;
|
|
1352
|
-
const colValue = (col) => col.nullable ? require_mongo_accumulator.orNull(colSourceField(col)) : colSourceField(col);
|
|
1353
|
-
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1354
|
-
const rendered = /* @__PURE__ */ new Map();
|
|
1355
|
-
const computedExpr = (col, leaf) => {
|
|
1356
|
-
let out = rendered.get(col.viewPath);
|
|
1357
|
-
if (out === void 0) {
|
|
1358
|
-
out = require_mongo_accumulator.exprToMongo(col.expr, (path) => {
|
|
1359
|
-
const c = byPath.get(path);
|
|
1360
|
-
return c.expr === void 0 ? { $toDouble: leaf(c) } : computedExpr(c, leaf);
|
|
1361
|
-
});
|
|
1362
|
-
rendered.set(col.viewPath, out);
|
|
1363
|
-
}
|
|
1364
|
-
return out;
|
|
1365
|
-
};
|
|
1366
|
-
if (columns.some((c) => c.aggFn)) {
|
|
1367
|
-
const group = { _id: {} };
|
|
1368
|
-
const project = { _id: 0 };
|
|
1369
|
-
const distinctSizes = {};
|
|
1370
|
-
const computed = {};
|
|
1371
|
-
for (const col of columns) if (col.expr !== void 0) {
|
|
1372
|
-
computed[col.viewColumn] = computedExpr(col, (c) => c.aggFn ? `$${c.viewColumn}` : `$_id.${c.viewColumn}`);
|
|
1373
|
-
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1374
|
-
} else if (col.aggFn) {
|
|
1375
|
-
const predicate = col.aggFilter ? require_mongo_accumulator.queryNodeToExpr(col.aggFilter, (ref) => `$${pathOf(ref)}`) : void 0;
|
|
1376
|
-
group[col.viewColumn] = require_mongo_accumulator.buildAccumulator(col.aggFn, col.aggField === "*" ? "*" : colSourceField(col), predicate, col.viewColumn);
|
|
1377
|
-
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1378
|
-
if (col.aggFn === "countDistinct") distinctSizes[col.viewColumn] = require_mongo_accumulator.distinctCountExpr(`$${col.viewColumn}`);
|
|
1379
|
-
} else {
|
|
1380
|
-
group._id[col.viewColumn] = colValue(col);
|
|
1381
|
-
project[col.viewColumn] = `$_id.${col.viewColumn}`;
|
|
1382
|
-
}
|
|
1383
|
-
pipeline.push({ $group: group });
|
|
1384
|
-
if (Object.keys(distinctSizes).length > 0) pipeline.push({ $addFields: distinctSizes });
|
|
1385
|
-
if (Object.keys(computed).length > 0) pipeline.push({ $addFields: computed });
|
|
1386
|
-
if (plan.having) pipeline.push({ $match: havingMatch(plan.having, byPath) });
|
|
1387
|
-
pipeline.push({ $project: project });
|
|
1388
|
-
} else {
|
|
1389
|
-
const project = { _id: 0 };
|
|
1390
|
-
for (const col of columns) project[col.viewColumn] = col.expr === void 0 ? colValue(col) : computedExpr(col, colValue);
|
|
1391
|
-
pipeline.push({ $project: project });
|
|
1392
|
-
}
|
|
1393
|
-
return pipeline;
|
|
1394
|
-
}
|
|
1395
|
-
/**
|
|
1396
|
-
* One join's `$lookup`.
|
|
1397
|
-
*
|
|
1398
|
-
* The simple `localField` / `foreignField` form is used whenever it is exact:
|
|
1399
|
-
* a single `=` between a field of the joined table and a field of an outer
|
|
1400
|
-
* table (the entry or an earlier join), with the joined field REQUIRED — the
|
|
1401
|
-
* simple form matches a null / missing local value to null / missing foreign
|
|
1402
|
-
* values, which a required foreign field can never hold. It is kept (rather
|
|
1403
|
-
* than always using the pipeline form) because it can use an index on the
|
|
1404
|
-
* foreign field on every MongoDB version; the pipeline form's `$expr` match
|
|
1405
|
-
* can't before MongoDB 5.0. Everything else uses the pipeline form (`let` +
|
|
1406
|
-
* `$match: { $expr }`, SQL null semantics).
|
|
1407
|
-
*/
|
|
1408
|
-
function buildLookup(view, join, outer) {
|
|
1409
|
-
const scope = join.scope;
|
|
1410
|
-
const as = joinedAs(scope);
|
|
1411
|
-
const simple = join.first ? void 0 : simpleJoinFields(view, join, outer);
|
|
1412
|
-
if (simple) return { $lookup: {
|
|
1413
|
-
from: join.targetTable,
|
|
1414
|
-
...simple,
|
|
1415
|
-
as
|
|
1416
|
-
} };
|
|
1417
|
-
const letVars = {};
|
|
1418
|
-
const varByPath = /* @__PURE__ */ new Map();
|
|
1419
|
-
const pathOf = (ref) => {
|
|
1420
|
-
const { table, source } = view.resolveRefSource(ref);
|
|
1421
|
-
if (table === scope) return `$${source.column}`;
|
|
1422
|
-
const prefix = outer.get(table);
|
|
1423
|
-
if (prefix === void 0) throw new Error(`View "${view.tableName}": the join on "${scope}" references "${table}", which is not joined before it`);
|
|
1424
|
-
const outerPath = prefix + source.column;
|
|
1425
|
-
let varName = varByPath.get(outerPath);
|
|
1426
|
-
if (!varName) {
|
|
1427
|
-
varName = `v${varByPath.size}`;
|
|
1428
|
-
varByPath.set(outerPath, varName);
|
|
1429
|
-
letVars[varName] = `$${outerPath}`;
|
|
1430
|
-
}
|
|
1431
|
-
return `$$${varName}`;
|
|
1432
|
-
};
|
|
1433
|
-
const pipeline = [{ $match: { $expr: require_mongo_accumulator.queryNodeToExpr(join.condition, pathOf) } }];
|
|
1434
|
-
if (join.first) {
|
|
1435
|
-
const sort = {};
|
|
1436
|
-
for (const { ref, desc } of join.first.order) sort[view.resolveRefSource(ref).source.column] = desc ? -1 : 1;
|
|
1437
|
-
pipeline.push({ $sort: sort }, { $limit: 1 });
|
|
1438
|
-
}
|
|
1439
|
-
return { $lookup: {
|
|
1440
|
-
from: join.targetTable,
|
|
1441
|
-
...varByPath.size > 0 ? { let: letVars } : {},
|
|
1442
|
-
pipeline,
|
|
1443
|
-
as
|
|
1444
|
-
} };
|
|
1445
|
-
}
|
|
1446
|
-
/** `{ localField, foreignField }` when the simple `$lookup` form is exact, else `undefined`. */
|
|
1447
|
-
function simpleJoinFields(view, join, outer) {
|
|
1448
|
-
let node = join.condition;
|
|
1449
|
-
if ("$and" in node) {
|
|
1450
|
-
const children = node.$and;
|
|
1451
|
-
if (children.length !== 1) return void 0;
|
|
1452
|
-
node = children[0];
|
|
1453
|
-
}
|
|
1454
|
-
if ("$or" in node || "$not" in node || "$and" in node) return void 0;
|
|
1455
|
-
const comp = node;
|
|
1456
|
-
if (comp.op !== "$eq" || !(0, _atscript_db.isFieldRef)(comp.right)) return void 0;
|
|
1457
|
-
const scope = join.scope;
|
|
1458
|
-
const left = view.resolveRefSource(comp.left);
|
|
1459
|
-
const right = view.resolveRefSource(comp.right);
|
|
1460
|
-
let target;
|
|
1461
|
-
let local;
|
|
1462
|
-
if (left.table === scope && right.table !== scope) {
|
|
1463
|
-
target = left;
|
|
1464
|
-
local = right;
|
|
1465
|
-
} else if (right.table === scope && left.table !== scope) {
|
|
1466
|
-
target = right;
|
|
1467
|
-
local = left;
|
|
1468
|
-
} else return;
|
|
1469
|
-
const localPrefix = outer.get(local.table);
|
|
1470
|
-
if (localPrefix === void 0) return void 0;
|
|
1471
|
-
const foreign = target.source;
|
|
1472
|
-
if (!(!foreign.optional && foreign.designType !== "union" && foreign.designType !== "unknown")) return void 0;
|
|
1473
|
-
return {
|
|
1474
|
-
localField: localPrefix + local.source.column,
|
|
1475
|
-
foreignField: foreign.column
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
/**
|
|
1479
|
-
* A view predicate (`@db.view.filter`, `@db.view.having`) as a `$match`
|
|
1480
|
-
* over pipeline paths: the shared query translation (`translateQueryTree` →
|
|
1481
|
-
* `buildMongoFilter`), so operators mean what they mean in queries (`exists`
|
|
1482
|
-
* = holds a value, `matches` takes `/re/flags`); a field-to-field comparison
|
|
1483
|
-
* becomes `$expr`.
|
|
1484
|
-
*/
|
|
1485
|
-
function viewMatch(node, pathOf) {
|
|
1486
|
-
return require_mongo_accumulator.buildMongoFilter((0, _atscript_db.translateQueryTree)(node, pathOf), { fieldOperands: true });
|
|
1487
|
-
}
|
|
1488
|
-
/**
|
|
1489
|
-
* `@db.view.having` as a `$match` after `$group`: refs name view fields —
|
|
1490
|
-
* aggregates are top-level, dimensions under `_id`.
|
|
1491
|
-
*/
|
|
1492
|
-
function havingMatch(node, colMap) {
|
|
1493
|
-
return viewMatch(node, (ref) => {
|
|
1494
|
-
const col = ref.type ? void 0 : colMap.get(ref.field);
|
|
1495
|
-
if (!col) return ref.field;
|
|
1496
|
-
return col.aggFn || col.expr !== void 0 ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
//#endregion
|
|
1500
1573
|
//#region src/lib/mongo-schema-sync.ts
|
|
1501
1574
|
const DESTRUCTIVE_OPTION_KEYS = new Set([
|
|
1502
1575
|
"capped",
|
|
@@ -2086,6 +2159,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2086
2159
|
this.db = db;
|
|
2087
2160
|
this.client = client;
|
|
2088
2161
|
this.options = options;
|
|
2162
|
+
this.viewJoinPruning = options.viewJoinPruning !== false;
|
|
2089
2163
|
}
|
|
2090
2164
|
get _client() {
|
|
2091
2165
|
return this.client;
|
|
@@ -2178,15 +2252,17 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2178
2252
|
return (applied ?? await run(stages))[0];
|
|
2179
2253
|
};
|
|
2180
2254
|
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
2255
|
+
const run = (pipeline, label) => {
|
|
2256
|
+
const pruned = searchStage ? void 0 : this._viewRead(query, "aggregate", pipeline);
|
|
2257
|
+
if (pruned) return this._readPipeline(query, pipeline, label, pruned, aggregateOptions(pipeline));
|
|
2258
|
+
this._log(label, pipeline);
|
|
2259
|
+
return wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
2260
|
+
};
|
|
2181
2261
|
if (query.controls?.$count) {
|
|
2182
|
-
const
|
|
2183
|
-
this._log("aggregate (count)", pipeline);
|
|
2184
|
-
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
2262
|
+
const result = await run(buildCountPipeline(query, searchStage, this._predicateFilterOpts), "aggregate (count)");
|
|
2185
2263
|
return result.length > 0 ? result : [{ count: await emptyGroup(true) ? 1 : 0 }];
|
|
2186
2264
|
}
|
|
2187
|
-
const
|
|
2188
|
-
this._log("aggregate", pipeline);
|
|
2189
|
-
const rows = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
2265
|
+
const rows = await run(buildAggregatePipeline(query, searchStage, this._predicateFilterOpts), "aggregate");
|
|
2190
2266
|
const empty = rows.length === 0 ? await emptyGroup(false) : void 0;
|
|
2191
2267
|
return empty ? [empty] : rows;
|
|
2192
2268
|
}
|
|
@@ -2236,6 +2312,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2236
2312
|
supportsAggregateExpressions() {
|
|
2237
2313
|
return true;
|
|
2238
2314
|
}
|
|
2315
|
+
/**
|
|
2316
|
+
* `$nulls` placement (since 0.1.153): a placed `$sort` / `$rowOrder` key
|
|
2317
|
+
* gets a null-or-missing flag ordered before it (`mongo-sort.ts`) — reads
|
|
2318
|
+
* then run as a pipeline with a blocking sort instead of `find().sort()`.
|
|
2319
|
+
*/
|
|
2320
|
+
supportsNullsPlacement() {
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2239
2323
|
/** Computed view columns and first-row joins. */
|
|
2240
2324
|
viewCapabilities() {
|
|
2241
2325
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -2457,7 +2541,8 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2457
2541
|
if (this._hasExplicitId) return {
|
|
2458
2542
|
addPrimaryKeys: ["_id"],
|
|
2459
2543
|
removePrimaryKeys: meta.originalMetaIdFields.filter((f) => f !== "_id"),
|
|
2460
|
-
addUniqueFields: uniqueFields.length
|
|
2544
|
+
addUniqueFields: uniqueFields.length === 1 ? uniqueFields : void 0,
|
|
2545
|
+
addUniqueKeys: uniqueFields.length > 1 ? [[...uniqueFields]] : void 0
|
|
2461
2546
|
};
|
|
2462
2547
|
const effectiveUnique = uniqueFields.filter((f) => meta.primaryKeys.length <= 1 || !meta.primaryKeys.includes(f));
|
|
2463
2548
|
effectiveUnique.push("_id");
|
|
@@ -2631,8 +2716,8 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2631
2716
|
data: pageStages(query.controls),
|
|
2632
2717
|
meta: [{ $count: "count" }]
|
|
2633
2718
|
} }];
|
|
2634
|
-
this.
|
|
2635
|
-
const result = await
|
|
2719
|
+
const pruned = this._viewRead(query, "rows", pipeline);
|
|
2720
|
+
const result = await this._readPipeline(query, pipeline, "aggregate (findManyWithCount)", pruned, placedSortOptions(query.controls));
|
|
2636
2721
|
return {
|
|
2637
2722
|
data: result[0]?.data || [],
|
|
2638
2723
|
count: result[0]?.meta[0]?.count || 0
|
|
@@ -2896,7 +2981,12 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2896
2981
|
}
|
|
2897
2982
|
}
|
|
2898
2983
|
async findOne(query) {
|
|
2899
|
-
|
|
2984
|
+
const pruned = this._prunedFind(query, "findOne", 1);
|
|
2985
|
+
if (pruned) {
|
|
2986
|
+
const [row] = await pruned;
|
|
2987
|
+
return row ?? null;
|
|
2988
|
+
}
|
|
2989
|
+
if ((0, _atscript_db.containsRelationFilter)(query.filter) || require_mongo_accumulator.hasNullsPlacement(query.controls)) {
|
|
2900
2990
|
const [row] = await this._aggregateFind(query, "findOne", 1);
|
|
2901
2991
|
return row ?? null;
|
|
2902
2992
|
}
|
|
@@ -2910,7 +3000,9 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2910
3000
|
}));
|
|
2911
3001
|
}
|
|
2912
3002
|
async findMany(query) {
|
|
2913
|
-
|
|
3003
|
+
const pruned = this._prunedFind(query, "findMany");
|
|
3004
|
+
if (pruned) return pruned;
|
|
3005
|
+
if ((0, _atscript_db.containsRelationFilter)(query.filter) || require_mongo_accumulator.hasNullsPlacement(query.controls)) return this._aggregateFind(query, "findMany");
|
|
2914
3006
|
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2915
3007
|
const opts = this._buildFindOptions(query.controls);
|
|
2916
3008
|
this._log("findMany", filter, opts);
|
|
@@ -2921,10 +3013,11 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2921
3013
|
}).toArray());
|
|
2922
3014
|
}
|
|
2923
3015
|
async count(query) {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3016
|
+
const stages = () => [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), { $count: "count" }];
|
|
3017
|
+
const pruned = this._viewRead(query, "count", stages);
|
|
3018
|
+
if (pruned || (0, _atscript_db.containsRelationFilter)(query.filter)) {
|
|
3019
|
+
const pipeline = pruned?.pipeline ?? stages();
|
|
3020
|
+
return (await this._readPipeline(query, pipeline, "aggregate (count)", pruned))[0]?.count ?? 0;
|
|
2928
3021
|
}
|
|
2929
3022
|
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2930
3023
|
if (this._estimatesCount() && Object.keys(filter).length === 0) {
|
|
@@ -3129,14 +3222,71 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
3129
3222
|
fieldCounters.set(physical, next + 1);
|
|
3130
3223
|
}
|
|
3131
3224
|
}
|
|
3132
|
-
/**
|
|
3225
|
+
/**
|
|
3226
|
+
* The pruned read of this adapter's managed view for `query` followed by
|
|
3227
|
+
* `stages` (`mongoViewRead`, since 0.1.153): the view pipeline without the
|
|
3228
|
+
* joins the read does not need, on the entry collection — or `undefined`
|
|
3229
|
+
* to read the stored view: pruning off, not a view, nothing droppable, an
|
|
3230
|
+
* operation-wide collation (it would govern the view's join keys too), or
|
|
3231
|
+
* stages that may read a dropped column ({@link stagesReadAny}).
|
|
3232
|
+
*/
|
|
3233
|
+
_viewRead(query, kind, stages) {
|
|
3234
|
+
if (!this.viewJoinPruning || !this._table?.isView) return void 0;
|
|
3235
|
+
if (!(0, _atscript_db.containsRelationFilter)(query.filter) && this._getCollationOpts(query)) return void 0;
|
|
3236
|
+
const read = mongoViewRead(this._table, query, kind);
|
|
3237
|
+
if (!read) return void 0;
|
|
3238
|
+
const own = typeof stages === "function" ? stages() : stages;
|
|
3239
|
+
if (stagesReadAny(own, read.dropped)) return void 0;
|
|
3240
|
+
return {
|
|
3241
|
+
collection: this.db.collection(read.entry),
|
|
3242
|
+
pipeline: [...read.prefix, ...own]
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Runs a read pipeline — through the pruned view read `pruned` when there
|
|
3247
|
+
* is one, on the bound collection otherwise. A view's own default
|
|
3248
|
+
* collation is the simple one, so the pruned read pins it: the entry
|
|
3249
|
+
* collection's default must not apply.
|
|
3250
|
+
*/
|
|
3251
|
+
_readPipeline(query, stages, label, pruned, options) {
|
|
3252
|
+
if (pruned) {
|
|
3253
|
+
this._log(`${label} (pruned view)`, pruned.pipeline);
|
|
3254
|
+
return wrapInvalidQuery(() => pruned.collection.aggregate(pruned.pipeline, {
|
|
3255
|
+
...options,
|
|
3256
|
+
collation: { locale: "simple" },
|
|
3257
|
+
...this._getSessionOpts()
|
|
3258
|
+
}).toArray());
|
|
3259
|
+
}
|
|
3260
|
+
this._log(label, stages);
|
|
3261
|
+
return wrapInvalidQuery(() => this.collection.aggregate(stages, {
|
|
3262
|
+
...options,
|
|
3263
|
+
...this._readOpts(query)
|
|
3264
|
+
}).toArray());
|
|
3265
|
+
}
|
|
3266
|
+
/** A find over the pruned view read, or `undefined` when the stored view is read. */
|
|
3267
|
+
_prunedFind(query, label, limit) {
|
|
3268
|
+
const pruned = this._viewRead(query, "rows", () => [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), ...pageStages(limit ? {
|
|
3269
|
+
...query.controls,
|
|
3270
|
+
$limit: limit
|
|
3271
|
+
} : query.controls)]);
|
|
3272
|
+
return pruned && this._readPipeline(query, pruned.pipeline, `aggregate (${label})`, pruned, placedSortOptions(query.controls));
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* A find as an aggregation pipeline: its filter holds relational
|
|
3276
|
+
* predicates, or its `$sort` places NULL (`$nulls` — `find().sort()` cannot
|
|
3277
|
+
* express it; the placed sort is a blocking sort, so it may spill to disk).
|
|
3278
|
+
*/
|
|
3133
3279
|
async _aggregateFind(query, label, limit) {
|
|
3134
3280
|
const pipeline = [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), ...pageStages(limit ? {
|
|
3135
3281
|
...query.controls,
|
|
3136
3282
|
$limit: limit
|
|
3137
3283
|
} : query.controls)];
|
|
3138
3284
|
this._log(`aggregate (${label})`, pipeline);
|
|
3139
|
-
|
|
3285
|
+
const opts = {
|
|
3286
|
+
...placedSortOptions(query.controls),
|
|
3287
|
+
...this._readOpts(query)
|
|
3288
|
+
};
|
|
3289
|
+
return wrapInvalidQuery(() => this.collection.aggregate(pipeline, opts).toArray());
|
|
3140
3290
|
}
|
|
3141
3291
|
_buildFindOptions(controls) {
|
|
3142
3292
|
const opts = {};
|
|
@@ -3249,13 +3399,24 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
3249
3399
|
return node.fields;
|
|
3250
3400
|
}
|
|
3251
3401
|
};
|
|
3252
|
-
/**
|
|
3402
|
+
/**
|
|
3403
|
+
* `allowDiskUse` for a read whose `$sort` places NULL (`$nulls`): the null
|
|
3404
|
+
* flags make it a blocking sort no index serves, so it may need to spill.
|
|
3405
|
+
*/
|
|
3406
|
+
function placedSortOptions(controls) {
|
|
3407
|
+
return require_mongo_accumulator.hasNullsPlacement(controls) ? { allowDiskUse: true } : void 0;
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* `$sort` → `$skip` → `$limit` → `$project` stages of a read's controls (a
|
|
3411
|
+
* `$nulls` placement adds its flags before the `$sort` and drops them after
|
|
3412
|
+
* `$limit` — see {@link sortStages}).
|
|
3413
|
+
*/
|
|
3253
3414
|
function pageStages(controls) {
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
if (controls.$sort) stages.push({ $sort: controls.$sort });
|
|
3415
|
+
if (!controls) return [];
|
|
3416
|
+
const { stages, cleanup } = require_mongo_accumulator.sortStages(controls);
|
|
3257
3417
|
if (controls.$skip) stages.push({ $skip: controls.$skip });
|
|
3258
3418
|
if (controls.$limit) stages.push({ $limit: controls.$limit });
|
|
3419
|
+
if (cleanup) stages.push(cleanup);
|
|
3259
3420
|
if (controls.$select) {
|
|
3260
3421
|
const projection = controls.$select.asProjection;
|
|
3261
3422
|
if (projection) stages.push({ $project: dedupeProjection(projection) });
|