@atscript/db-mongo 0.1.145 → 0.1.147
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 +20 -17
- package/dist/agg.d.cts +7 -4
- package/dist/agg.d.mts +7 -4
- package/dist/agg.mjs +20 -17
- package/dist/index.cjs +472 -177
- package/dist/index.d.cts +90 -22
- package/dist/index.d.mts +90 -22
- package/dist/index.mjs +469 -179
- package/dist/mongo-accumulator-BJcQs2OV.cjs +605 -0
- package/dist/mongo-accumulator-Y_xVYo3C.mjs +534 -0
- package/dist/mongo-filter-CGd9ryOD.d.cts +123 -0
- package/dist/mongo-filter-CGd9ryOD.d.mts +123 -0
- package/package.json +9 -9
- package/dist/mongo-accumulator-BalE9LkU.cjs +0 -265
- package/dist/mongo-accumulator-Dc0w4t0l.mjs +0 -236
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-BJcQs2OV.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");
|
|
@@ -374,47 +374,67 @@ function mongoIndexKey(type, name) {
|
|
|
374
374
|
}
|
|
375
375
|
//#endregion
|
|
376
376
|
//#region src/lib/mongo-relations.ts
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const val = doc[primaryKeys[0]];
|
|
380
|
-
return val == null ? "" : `${val}`;
|
|
381
|
-
}
|
|
377
|
+
/** Join key of a row's primary key values (`fields` = the row's own names for them). */
|
|
378
|
+
function buildPKKey(fields, doc) {
|
|
382
379
|
let key = "";
|
|
383
|
-
for (let i = 0; i <
|
|
380
|
+
for (let i = 0; i < fields.length; i++) {
|
|
384
381
|
if (i > 0) key += "\0";
|
|
385
|
-
const val = doc[
|
|
382
|
+
const val = doc[fields[i]];
|
|
386
383
|
key += val == null ? "" : `${val}`;
|
|
387
384
|
}
|
|
388
385
|
return key;
|
|
389
386
|
}
|
|
387
|
+
function resolveReadable(tableResolver, type) {
|
|
388
|
+
if (!type || !tableResolver) return;
|
|
389
|
+
return tableResolver(type);
|
|
390
|
+
}
|
|
391
|
+
/** The collection a readable reads (a view's name for a `@db.view`). */
|
|
392
|
+
function collectionOf(readable) {
|
|
393
|
+
return readable.getAdapter().resolveTableName(false);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Native `$with` loading: one aggregation over the source collection
|
|
397
|
+
* (re-selected by primary key) with a `$lookup` per relation.
|
|
398
|
+
*
|
|
399
|
+
* Every name is physical (since 0.1.147): join fields go through each
|
|
400
|
+
* side's `physicalPath`, and the `$with` sub-query (filter — relational
|
|
401
|
+
* predicates included —, `$sort`, `$select`) through the related table's
|
|
402
|
+
* own translation. The relation's `@db.rel.filter` is part of the lookup
|
|
403
|
+
* (target conditions ANDed into the sub-filter, junction conditions on the
|
|
404
|
+
* junction rows). Loaded documents come back as logical rows (renames,
|
|
405
|
+
* value formatters, decryption) before nested `$with` loads on them.
|
|
406
|
+
*/
|
|
390
407
|
async function loadRelationsImpl(host, rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
391
408
|
if (rows.length === 0 || withRelations.length === 0) return;
|
|
392
|
-
const
|
|
409
|
+
const source = host._table;
|
|
410
|
+
const primaryKeys = source.primaryKeys;
|
|
393
411
|
const relMeta = [];
|
|
394
412
|
for (const withRel of withRelations) {
|
|
395
413
|
if (withRel.name.includes(".")) continue;
|
|
396
414
|
const relation = relations.get(withRel.name);
|
|
397
415
|
if (!relation) throw new Error(`Unknown relation "${withRel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`);
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
400
|
-
relMeta.push({
|
|
401
|
-
name: withRel.name,
|
|
402
|
-
isArray: lookupResult.isArray,
|
|
403
|
-
relation,
|
|
404
|
-
nestedWith: extractNestedWith(withRel),
|
|
405
|
-
stages: lookupResult.stages
|
|
406
|
-
});
|
|
416
|
+
const lookup = buildRelationLookup(source, withRel, relation, foreignKeys, tableResolver);
|
|
417
|
+
if (lookup) relMeta.push(lookup);
|
|
407
418
|
}
|
|
408
419
|
if (relMeta.length === 0) return;
|
|
409
420
|
const pkMatchFilter = buildPKMatchFilter(rows, primaryKeys);
|
|
410
421
|
if (pkMatchFilter) {
|
|
411
|
-
const
|
|
422
|
+
const { filter } = source._translateForAdapter({ filter: pkMatchFilter });
|
|
423
|
+
const sourceMeta = source.getMetadata();
|
|
424
|
+
const physicalKeys = primaryKeys.map((pk) => sourceMeta.physicalPath(pk));
|
|
425
|
+
const project = {};
|
|
426
|
+
for (const key of physicalKeys) project[key] = 1;
|
|
427
|
+
for (const meta of relMeta) project[meta.name] = 1;
|
|
428
|
+
const pipeline = [{ $match: require_mongo_accumulator.buildMongoFilter(filter) }];
|
|
412
429
|
for (const meta of relMeta) pipeline.push(...meta.stages);
|
|
413
|
-
|
|
430
|
+
pipeline.push({ $project: project });
|
|
431
|
+
const results = await host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
|
|
432
|
+
await Promise.all(relMeta.map((meta) => toLogicalRows(results, meta)));
|
|
433
|
+
mergeRelationResults(rows, results, primaryKeys, physicalKeys, relMeta);
|
|
414
434
|
} else for (const row of rows) for (const meta of relMeta) row[meta.name] = meta.isArray ? [] : null;
|
|
415
|
-
await loadNestedRelations(rows, relMeta
|
|
435
|
+
await loadNestedRelations(rows, relMeta);
|
|
416
436
|
}
|
|
417
|
-
/** Builds a $match filter to re-select source rows by PK. */
|
|
437
|
+
/** Builds a $match filter to re-select source rows by PK (logical names and values). */
|
|
418
438
|
function buildPKMatchFilter(rows, primaryKeys) {
|
|
419
439
|
if (primaryKeys.length === 1) {
|
|
420
440
|
const pk = primaryKeys[0];
|
|
@@ -448,137 +468,164 @@ function buildPKMatchFilter(rows, primaryKeys) {
|
|
|
448
468
|
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
449
469
|
}
|
|
450
470
|
/** Dispatches to the correct $lookup builder based on relation direction. */
|
|
451
|
-
function buildRelationLookup(
|
|
471
|
+
function buildRelationLookup(source, withRel, relation, foreignKeys, tableResolver) {
|
|
472
|
+
const target = resolveReadable(tableResolver, relation.targetType());
|
|
473
|
+
if (!target) return;
|
|
474
|
+
let built;
|
|
452
475
|
switch (relation.direction) {
|
|
453
|
-
case "to":
|
|
454
|
-
|
|
455
|
-
|
|
476
|
+
case "to":
|
|
477
|
+
built = buildToLookup(source, target, withRel, relation, foreignKeys);
|
|
478
|
+
break;
|
|
479
|
+
case "from":
|
|
480
|
+
built = buildFromLookup(source, target, withRel, relation);
|
|
481
|
+
break;
|
|
482
|
+
case "via":
|
|
483
|
+
built = buildViaLookup(source, target, withRel, relation, tableResolver);
|
|
484
|
+
break;
|
|
456
485
|
default: return;
|
|
457
486
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
exprMatch: { $eq: [`$${remoteFields[0]}`, `$$${varPrefix}0`] }
|
|
466
|
-
};
|
|
467
|
-
const andClauses = [];
|
|
468
|
-
for (let i = 0; i < remoteFields.length; i++) andClauses.push({ $eq: [`$${remoteFields[i]}`, `$$${varPrefix}${i}`] });
|
|
469
|
-
return {
|
|
470
|
-
letVars,
|
|
471
|
-
exprMatch: { $and: andClauses }
|
|
487
|
+
return built && {
|
|
488
|
+
name: withRel.name,
|
|
489
|
+
isArray: built.isArray,
|
|
490
|
+
target,
|
|
491
|
+
readControls: built.readControls,
|
|
492
|
+
nestedWith: extractNestedWith(withRel),
|
|
493
|
+
stages: built.stages
|
|
472
494
|
};
|
|
473
495
|
}
|
|
496
|
+
/** Pairs of physical join fields: `outer` on the looking-up side, `inner` on the looked-up one. */
|
|
497
|
+
function joinPairs(outerMeta, outerFields, innerMeta, innerFields) {
|
|
498
|
+
return outerFields.map((field, i) => ({
|
|
499
|
+
outer: outerMeta.physicalPath(field),
|
|
500
|
+
inner: innerMeta.physicalPath(innerFields[i])
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
474
503
|
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */
|
|
475
|
-
function buildToLookup(withRel, relation, foreignKeys) {
|
|
504
|
+
function buildToLookup(source, target, withRel, relation, foreignKeys) {
|
|
476
505
|
const fk = findFKForRelation(relation, foreignKeys);
|
|
477
|
-
|
|
478
|
-
const innerPipeline = buildLookupInnerPipeline(withRel, fk.targetFields);
|
|
479
|
-
const { letVars, exprMatch } = buildLookupJoin(fk.localFields, fk.targetFields, "fk_");
|
|
480
|
-
return {
|
|
481
|
-
stages: [{ $lookup: {
|
|
482
|
-
from: fk.targetTable,
|
|
483
|
-
let: letVars,
|
|
484
|
-
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
485
|
-
as: withRel.name
|
|
486
|
-
} }, { $unwind: {
|
|
487
|
-
path: `$${withRel.name}`,
|
|
488
|
-
preserveNullAndEmptyArrays: true
|
|
489
|
-
} }],
|
|
490
|
-
isArray: false
|
|
491
|
-
};
|
|
506
|
+
return fk ? directLookup(source, target, withRel, relation, fk.localFields, fk.targetFields, "fk_", false) : void 0;
|
|
492
507
|
}
|
|
493
508
|
/** $lookup for FROM relations (FK is on target → this table). */
|
|
494
|
-
function buildFromLookup(
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
509
|
+
function buildFromLookup(source, target, withRel, relation) {
|
|
510
|
+
const remoteFK = findRemoteFK(target, source.tableName, relation.alias);
|
|
511
|
+
return remoteFK ? directLookup(source, target, withRel, relation, remoteFK.targetFields, remoteFK.fields, "pk_", relation.isArray) : void 0;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* The correlated `$lookup` of a TO / FROM relation: `sourceFields` of this
|
|
515
|
+
* table paired with `targetFields` of the target, unwound when single-valued.
|
|
516
|
+
*/
|
|
517
|
+
function directLookup(source, target, withRel, relation, sourceFields, targetFields, prefix, isArray) {
|
|
518
|
+
const pairs = joinPairs(source.getMetadata(), sourceFields, target.getMetadata(), targetFields);
|
|
519
|
+
const inner = buildLookupInnerPipeline(target, withRel, relation, pairs.map((p) => p.inner));
|
|
520
|
+
const join = require_mongo_accumulator.correlate(prefix, pairs);
|
|
504
521
|
const stages = [{ $lookup: {
|
|
505
|
-
from:
|
|
506
|
-
let:
|
|
507
|
-
pipeline: [
|
|
522
|
+
from: collectionOf(target),
|
|
523
|
+
let: join.let,
|
|
524
|
+
pipeline: [
|
|
525
|
+
...join.stages,
|
|
526
|
+
...inner.filter,
|
|
527
|
+
...inner.page
|
|
528
|
+
],
|
|
508
529
|
as: withRel.name
|
|
509
530
|
} }];
|
|
510
|
-
if (!
|
|
531
|
+
if (!isArray) stages.push({ $unwind: {
|
|
511
532
|
path: `$${withRel.name}`,
|
|
512
533
|
preserveNullAndEmptyArrays: true
|
|
513
534
|
} });
|
|
514
535
|
return {
|
|
515
536
|
stages,
|
|
516
|
-
isArray
|
|
537
|
+
isArray,
|
|
538
|
+
readControls: inner.readControls
|
|
517
539
|
};
|
|
518
540
|
}
|
|
519
541
|
/** $lookup for VIA relations (M:N through junction table). Always array. */
|
|
520
|
-
function buildViaLookup(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const junctionMeta = tableResolver(junctionType);
|
|
525
|
-
if (!junctionMeta) return;
|
|
526
|
-
const junctionTableName = (0, _atscript_db.tableNameOf)(junctionType);
|
|
527
|
-
const targetTableName = (0, _atscript_db.tableNameOf)(relation.targetType());
|
|
528
|
-
const fkToThis = findRemoteFK(junctionMeta, host._table.tableName);
|
|
542
|
+
function buildViaLookup(source, target, withRel, relation, tableResolver) {
|
|
543
|
+
const junction = resolveReadable(tableResolver, relation.viaType?.());
|
|
544
|
+
if (!junction) return;
|
|
545
|
+
const fkToThis = findRemoteFK(junction, source.tableName);
|
|
529
546
|
if (!fkToThis) return;
|
|
530
|
-
const fkToTarget = findRemoteFK(
|
|
547
|
+
const fkToTarget = findRemoteFK(junction, (0, _atscript_db.tableNameOf)(relation.targetType()));
|
|
531
548
|
if (!fkToTarget) return;
|
|
532
|
-
const
|
|
533
|
-
const
|
|
549
|
+
const sourceMeta = source.getMetadata();
|
|
550
|
+
const junctionMeta = junction.getMetadata();
|
|
551
|
+
const targetMeta = target.getMetadata();
|
|
552
|
+
const toSource = require_mongo_accumulator.correlate("pk_", joinPairs(sourceMeta, fkToThis.targetFields, junctionMeta, fkToThis.fields));
|
|
553
|
+
const toTargetPairs = joinPairs(junctionMeta, fkToTarget.fields, targetMeta, fkToTarget.targetFields);
|
|
554
|
+
const toTarget = require_mongo_accumulator.correlate("fk_", toTargetPairs);
|
|
555
|
+
const inner = buildLookupInnerPipeline(target, withRel, relation, toTargetPairs.map((p) => p.inner));
|
|
556
|
+
const junctionFilter = (0, _atscript_db.relationStaticFilter)(relation, withRel.name).junction;
|
|
557
|
+
const junctionStages = junctionFilter ? filterStages(junction, junction._translateForAdapter({ filter: junctionFilter }).filter) : [];
|
|
534
558
|
return {
|
|
535
559
|
stages: [{ $lookup: {
|
|
536
|
-
from:
|
|
537
|
-
let:
|
|
560
|
+
from: collectionOf(junction),
|
|
561
|
+
let: toSource.let,
|
|
538
562
|
pipeline: [
|
|
539
|
-
|
|
563
|
+
...toSource.stages,
|
|
564
|
+
...junctionStages,
|
|
540
565
|
{ $lookup: {
|
|
541
|
-
from:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
pipeline: innerPipeline,
|
|
566
|
+
from: collectionOf(target),
|
|
567
|
+
let: toTarget.let,
|
|
568
|
+
pipeline: [...toTarget.stages, ...inner.filter],
|
|
545
569
|
as: "__target"
|
|
546
570
|
} },
|
|
547
571
|
{ $unwind: {
|
|
548
572
|
path: "$__target",
|
|
549
573
|
preserveNullAndEmptyArrays: false
|
|
550
574
|
} },
|
|
551
|
-
{ $replaceRoot: { newRoot: "$__target" } }
|
|
575
|
+
{ $replaceRoot: { newRoot: "$__target" } },
|
|
576
|
+
...inner.page
|
|
552
577
|
],
|
|
553
578
|
as: withRel.name
|
|
554
579
|
} }],
|
|
555
|
-
isArray: true
|
|
580
|
+
isArray: true,
|
|
581
|
+
readControls: inner.readControls
|
|
556
582
|
};
|
|
557
583
|
}
|
|
558
|
-
/**
|
|
559
|
-
function
|
|
560
|
-
|
|
584
|
+
/** Stages applying a translated filter on `readable`'s collection (none when empty). */
|
|
585
|
+
function filterStages(readable, filter) {
|
|
586
|
+
if (!filter || Object.keys(filter).length === 0) return [];
|
|
587
|
+
return require_mongo_accumulator.mongoFilterStages(filter, { collation: require_mongo_accumulator.collationOfAdapter(readable.getAdapter()) });
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* The `$with` sub-query on the related table: its filter AND the target part
|
|
591
|
+
* of the relation's `@db.rel.filter`, plus `$sort` / `$skip` / `$limit` /
|
|
592
|
+
* `$select` — translated by the TARGET readable (physical names, value
|
|
593
|
+
* formatters, relational predicates resolved). `requiredFields` (physical
|
|
594
|
+
* join keys) are always projected.
|
|
595
|
+
*/
|
|
596
|
+
function buildLookupInnerPipeline(target, withRel, relation, requiredFields) {
|
|
561
597
|
const flatRel = withRel;
|
|
562
598
|
const nested = withRel.controls || {};
|
|
563
|
-
const filter = withRel.filter;
|
|
564
599
|
const sort = nested.$sort || flatRel.$sort;
|
|
565
600
|
const limit = nested.$limit ?? flatRel.$limit;
|
|
566
601
|
const skip = nested.$skip ?? flatRel.$skip;
|
|
567
602
|
const select = nested.$select || flatRel.$select;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
if (
|
|
572
|
-
const
|
|
603
|
+
const statics = (0, _atscript_db.relationStaticFilter)(relation, withRel.name);
|
|
604
|
+
const controls = {};
|
|
605
|
+
if (sort) controls.$sort = sort;
|
|
606
|
+
if (select) controls.$select = select;
|
|
607
|
+
const translated = target._translateForAdapter({
|
|
608
|
+
filter: (0, _atscript_db.andFilters)(statics.target, withRel.filter),
|
|
609
|
+
controls
|
|
610
|
+
});
|
|
611
|
+
const page = [];
|
|
612
|
+
if (translated.controls?.$sort) page.push({ $sort: translated.controls.$sort });
|
|
613
|
+
if (skip) page.push({ $skip: skip });
|
|
614
|
+
if (limit !== null && limit !== void 0) page.push({ $limit: limit });
|
|
615
|
+
const projection = translated.controls?.$select?.asProjection;
|
|
573
616
|
if (projection) {
|
|
574
617
|
const project = { ...projection };
|
|
575
618
|
if (Object.values(project).some((flag) => flag === 1 || flag === true)) {
|
|
576
619
|
for (const f of requiredFields) project[f] = 1;
|
|
577
620
|
if (!("_id" in projection) && !requiredFields.includes("_id")) project._id = 0;
|
|
578
621
|
} else for (const f of requiredFields) delete project[f];
|
|
579
|
-
if (Object.keys(project).length > 0)
|
|
622
|
+
if (Object.keys(project).length > 0) page.push({ $project: dedupeProjection(project) });
|
|
580
623
|
}
|
|
581
|
-
return
|
|
624
|
+
return {
|
|
625
|
+
filter: filterStages(target, translated.filter),
|
|
626
|
+
page,
|
|
627
|
+
readControls: select ? { $select: select } : {}
|
|
628
|
+
};
|
|
582
629
|
}
|
|
583
630
|
/** Extracts nested $with from a WithRelation's controls. */
|
|
584
631
|
function extractNestedWith(withRel) {
|
|
@@ -587,15 +634,10 @@ function extractNestedWith(withRel) {
|
|
|
587
634
|
return nestedWith && nestedWith.length > 0 ? nestedWith : void 0;
|
|
588
635
|
}
|
|
589
636
|
/** Post-processes nested $with by delegating to the target table's own relation loading. */
|
|
590
|
-
async function loadNestedRelations(rows, relMeta
|
|
591
|
-
if (!tableResolver) return;
|
|
637
|
+
async function loadNestedRelations(rows, relMeta) {
|
|
592
638
|
const tasks = [];
|
|
593
639
|
for (const meta of relMeta) {
|
|
594
640
|
if (!meta.nestedWith || meta.nestedWith.length === 0) continue;
|
|
595
|
-
const targetType = meta.relation.targetType();
|
|
596
|
-
if (!targetType) continue;
|
|
597
|
-
const targetTable = tableResolver(targetType);
|
|
598
|
-
if (!targetTable) continue;
|
|
599
641
|
const subRows = [];
|
|
600
642
|
for (const row of rows) {
|
|
601
643
|
const val = row[meta.name];
|
|
@@ -603,21 +645,44 @@ async function loadNestedRelations(rows, relMeta, tableResolver) {
|
|
|
603
645
|
else if (val && typeof val === "object") subRows.push(val);
|
|
604
646
|
}
|
|
605
647
|
if (subRows.length === 0) continue;
|
|
606
|
-
tasks.push(
|
|
648
|
+
tasks.push(meta.target.loadRelations(subRows, meta.nestedWith));
|
|
607
649
|
}
|
|
608
650
|
await Promise.all(tasks);
|
|
609
651
|
}
|
|
610
|
-
/**
|
|
611
|
-
|
|
652
|
+
/**
|
|
653
|
+
* Replaces one relation's loaded documents on every aggregation result with
|
|
654
|
+
* the related table's LOGICAL rows (one conversion call for all of them).
|
|
655
|
+
*/
|
|
656
|
+
async function toLogicalRows(results, meta) {
|
|
657
|
+
const physical = [];
|
|
658
|
+
const spans = [];
|
|
659
|
+
for (const doc of results) {
|
|
660
|
+
const value = doc[meta.name];
|
|
661
|
+
const list = Array.isArray(value) ? value : value && typeof value === "object" ? [value] : [];
|
|
662
|
+
spans.push([
|
|
663
|
+
doc,
|
|
664
|
+
physical.length,
|
|
665
|
+
list.length
|
|
666
|
+
]);
|
|
667
|
+
physical.push(...list);
|
|
668
|
+
}
|
|
669
|
+
const logical = physical.length > 0 ? await meta.target._rowsFromAdapter(physical, meta.readControls) : [];
|
|
670
|
+
for (const [doc, start, length] of spans) doc[meta.name] = meta.isArray ? logical.slice(start, start + length) : length > 0 ? logical[start] : null;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Merges aggregation results back onto the original rows by PK — the
|
|
674
|
+
* results carry the PHYSICAL key fields (`physicalKeys`), the rows the
|
|
675
|
+
* logical ones (`primaryKeys`).
|
|
676
|
+
*/
|
|
677
|
+
function mergeRelationResults(rows, results, primaryKeys, physicalKeys, relMeta) {
|
|
612
678
|
const resultIndex = /* @__PURE__ */ new Map();
|
|
613
|
-
for (const doc of results) resultIndex.set(buildPKKey(
|
|
679
|
+
for (const doc of results) resultIndex.set(buildPKKey(physicalKeys, doc), doc);
|
|
614
680
|
for (const row of rows) {
|
|
615
681
|
const enriched = resultIndex.get(buildPKKey(primaryKeys, row));
|
|
616
|
-
for (const meta of relMeta)
|
|
617
|
-
const value = enriched[meta.name];
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
} else row[meta.name] = meta.isArray ? [] : null;
|
|
682
|
+
for (const meta of relMeta) {
|
|
683
|
+
const value = enriched?.[meta.name];
|
|
684
|
+
row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
685
|
+
}
|
|
621
686
|
}
|
|
622
687
|
}
|
|
623
688
|
/** Finds FK entry for a TO relation from this table's foreignKeys map. */
|
|
@@ -644,6 +709,10 @@ function findRemoteFK(target, thisTableName, alias) {
|
|
|
644
709
|
}
|
|
645
710
|
//#endregion
|
|
646
711
|
//#region src/lib/mongo-search.ts
|
|
712
|
+
/** Renders a host's filter with relational predicates — its 'nocase' fields per field. */
|
|
713
|
+
function filterOptionsOf(host) {
|
|
714
|
+
return { collation: require_mongo_accumulator.collationOfAdapter(host) };
|
|
715
|
+
}
|
|
647
716
|
/**
|
|
648
717
|
* Returns available search indexes as generic metadata for UI. The text
|
|
649
718
|
* default is the `DEFAULT` entry (an alias of the index `search()` answers
|
|
@@ -736,7 +805,7 @@ const DISTANCE_FIELD = "__atscript_distance";
|
|
|
736
805
|
/** Distance-ranked geo search via a leading `$geoNear` aggregation stage. */
|
|
737
806
|
async function geoSearchImpl(host, point, query, indexName) {
|
|
738
807
|
const controls = query.controls || {};
|
|
739
|
-
const pipeline =
|
|
808
|
+
const pipeline = buildGeoNearStages(host, point, query, indexName);
|
|
740
809
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
741
810
|
pipeline.push({ $limit: controls.$limit || 1e3 });
|
|
742
811
|
pushGeoProjection(pipeline, query.controls);
|
|
@@ -750,7 +819,7 @@ async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
|
750
819
|
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
751
820
|
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
752
821
|
pushGeoProjection(dataStages, query.controls);
|
|
753
|
-
const pipeline = [
|
|
822
|
+
const pipeline = [...buildGeoNearStages(host, point, query, indexName), { $facet: {
|
|
754
823
|
data: dataStages,
|
|
755
824
|
meta: [{ $count: "count" }]
|
|
756
825
|
} }];
|
|
@@ -761,9 +830,15 @@ async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
|
761
830
|
count: result[0]?.meta?.[0]?.count || 0
|
|
762
831
|
};
|
|
763
832
|
}
|
|
764
|
-
/**
|
|
765
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Builds the leading `$geoNear` stage; the filter rides in its `query`
|
|
835
|
+
* option. A filter with relational predicates keeps its predicate-free
|
|
836
|
+
* conjuncts there and applies the rest (predicate `$lookup`s + `$match`)
|
|
837
|
+
* right after — exact, since `$geoNear` has no result cut-off of its own.
|
|
838
|
+
*/
|
|
839
|
+
function buildGeoNearStages(host, point, query, indexName) {
|
|
766
840
|
const controls = query.controls || {};
|
|
841
|
+
const plan = (0, _atscript_db.containsRelationFilter)(query.filter) ? require_mongo_accumulator.buildMongoQuery(query.filter, filterOptionsOf(host)) : void 0;
|
|
767
842
|
const geoNear = {
|
|
768
843
|
near: {
|
|
769
844
|
type: "Point",
|
|
@@ -772,11 +847,14 @@ function buildGeoNearStage(host, point, query, indexName) {
|
|
|
772
847
|
distanceField: DISTANCE_FIELD,
|
|
773
848
|
spherical: true,
|
|
774
849
|
key: resolveGeoKeyPath(host, indexName),
|
|
775
|
-
query: require_mongo_accumulator.buildMongoFilter(query.filter)
|
|
850
|
+
query: plan ? plan.pre ?? {} : require_mongo_accumulator.buildMongoFilter(query.filter)
|
|
776
851
|
};
|
|
777
852
|
if (typeof controls.$maxDistance === "number") geoNear.maxDistance = controls.$maxDistance;
|
|
778
853
|
if (typeof controls.$minDistance === "number") geoNear.minDistance = controls.$minDistance;
|
|
779
|
-
return { $geoNear: geoNear }
|
|
854
|
+
return plan ? [{ $geoNear: geoNear }, ...require_mongo_accumulator.planStages({
|
|
855
|
+
...plan,
|
|
856
|
+
pre: void 0
|
|
857
|
+
})] : [{ $geoNear: geoNear }];
|
|
780
858
|
}
|
|
781
859
|
/** Resolves the physical field path of the targeted geo index. */
|
|
782
860
|
function resolveGeoKeyPath(host, indexName) {
|
|
@@ -985,14 +1063,13 @@ function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
|
985
1063
|
}
|
|
986
1064
|
/** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
|
|
987
1065
|
async function runSearchPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
988
|
-
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
989
1066
|
const controls = query.controls || {};
|
|
990
1067
|
const pipeline = [stage];
|
|
991
1068
|
if (threshold !== void 0) {
|
|
992
1069
|
pipeline.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
|
|
993
1070
|
pipeline.push({ $match: { _score: { $gte: threshold } } });
|
|
994
1071
|
} else if (classicText) pipeline.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
995
|
-
pipeline.push(
|
|
1072
|
+
pipeline.push(...require_mongo_accumulator.mongoFilterStages(query.filter, filterOptionsOf(host)));
|
|
996
1073
|
if (controls.$sort) pipeline.push({ $sort: controls.$sort });
|
|
997
1074
|
else if (classicText) pipeline.push({ $sort: { _score: -1 } });
|
|
998
1075
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
@@ -1007,7 +1084,6 @@ async function runSearchPipeline(host, stage, query, label, threshold, classicTe
|
|
|
1007
1084
|
}
|
|
1008
1085
|
/** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
|
|
1009
1086
|
async function runSearchWithCountPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
1010
|
-
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
1011
1087
|
const controls = query.controls || {};
|
|
1012
1088
|
const preStages = [];
|
|
1013
1089
|
if (threshold !== void 0) {
|
|
@@ -1026,7 +1102,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold,
|
|
|
1026
1102
|
const pipeline = [
|
|
1027
1103
|
stage,
|
|
1028
1104
|
...preStages,
|
|
1029
|
-
|
|
1105
|
+
...require_mongo_accumulator.mongoFilterStages(query.filter, filterOptionsOf(host)),
|
|
1030
1106
|
{ $facet: {
|
|
1031
1107
|
data: dataStages,
|
|
1032
1108
|
meta: [{ $count: "count" }]
|
|
@@ -1089,11 +1165,28 @@ function buildViewPipeline(view) {
|
|
|
1089
1165
|
/** A column's source field as an aggregation operand. */
|
|
1090
1166
|
const colSourceField = (col) => `$${prefixes.get(col.sourceTable) ?? ""}${col.sourceColumn}`;
|
|
1091
1167
|
const colValue = (col) => col.nullable ? require_mongo_accumulator.orNull(colSourceField(col)) : colSourceField(col);
|
|
1168
|
+
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1169
|
+
const rendered = /* @__PURE__ */ new Map();
|
|
1170
|
+
const computedExpr = (col, leaf) => {
|
|
1171
|
+
let out = rendered.get(col.viewPath);
|
|
1172
|
+
if (out === void 0) {
|
|
1173
|
+
out = require_mongo_accumulator.exprToMongo(col.expr, (path) => {
|
|
1174
|
+
const c = byPath.get(path);
|
|
1175
|
+
return c.expr === void 0 ? { $toDouble: leaf(c) } : computedExpr(c, leaf);
|
|
1176
|
+
});
|
|
1177
|
+
rendered.set(col.viewPath, out);
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
};
|
|
1092
1181
|
if (columns.some((c) => c.aggFn)) {
|
|
1093
1182
|
const group = { _id: {} };
|
|
1094
1183
|
const project = { _id: 0 };
|
|
1095
1184
|
const distinctSizes = {};
|
|
1096
|
-
|
|
1185
|
+
const computed = {};
|
|
1186
|
+
for (const col of columns) if (col.expr !== void 0) {
|
|
1187
|
+
computed[col.viewColumn] = computedExpr(col, (c) => c.aggFn ? `$${c.viewColumn}` : `$_id.${c.viewColumn}`);
|
|
1188
|
+
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1189
|
+
} else if (col.aggFn) {
|
|
1097
1190
|
const predicate = col.aggFilter ? require_mongo_accumulator.queryNodeToExpr(col.aggFilter, (ref) => `$${pathOf(ref)}`) : void 0;
|
|
1098
1191
|
group[col.viewColumn] = require_mongo_accumulator.buildAccumulator(col.aggFn, col.aggField === "*" ? "*" : colSourceField(col), predicate, col.viewColumn);
|
|
1099
1192
|
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
@@ -1104,11 +1197,12 @@ function buildViewPipeline(view) {
|
|
|
1104
1197
|
}
|
|
1105
1198
|
pipeline.push({ $group: group });
|
|
1106
1199
|
if (Object.keys(distinctSizes).length > 0) pipeline.push({ $addFields: distinctSizes });
|
|
1107
|
-
if (
|
|
1200
|
+
if (Object.keys(computed).length > 0) pipeline.push({ $addFields: computed });
|
|
1201
|
+
if (plan.having) pipeline.push({ $match: havingMatch(plan.having, byPath) });
|
|
1108
1202
|
pipeline.push({ $project: project });
|
|
1109
1203
|
} else {
|
|
1110
1204
|
const project = { _id: 0 };
|
|
1111
|
-
for (const col of columns) project[col.viewColumn] = colValue(col);
|
|
1205
|
+
for (const col of columns) project[col.viewColumn] = col.expr === void 0 ? colValue(col) : computedExpr(col, colValue);
|
|
1112
1206
|
pipeline.push({ $project: project });
|
|
1113
1207
|
}
|
|
1114
1208
|
return pipeline;
|
|
@@ -1129,7 +1223,7 @@ function buildViewPipeline(view) {
|
|
|
1129
1223
|
function buildLookup(view, join, outer) {
|
|
1130
1224
|
const scope = join.scope;
|
|
1131
1225
|
const as = joinedAs(scope);
|
|
1132
|
-
const simple = simpleJoinFields(view, join, outer);
|
|
1226
|
+
const simple = join.first ? void 0 : simpleJoinFields(view, join, outer);
|
|
1133
1227
|
if (simple) return { $lookup: {
|
|
1134
1228
|
from: join.targetTable,
|
|
1135
1229
|
...simple,
|
|
@@ -1151,11 +1245,16 @@ function buildLookup(view, join, outer) {
|
|
|
1151
1245
|
}
|
|
1152
1246
|
return `$$${varName}`;
|
|
1153
1247
|
};
|
|
1154
|
-
const
|
|
1248
|
+
const pipeline = [{ $match: { $expr: require_mongo_accumulator.queryNodeToExpr(join.condition, pathOf) } }];
|
|
1249
|
+
if (join.first) {
|
|
1250
|
+
const sort = {};
|
|
1251
|
+
for (const { ref, desc } of join.first.order) sort[view.resolveRefSource(ref).source.column] = desc ? -1 : 1;
|
|
1252
|
+
pipeline.push({ $sort: sort }, { $limit: 1 });
|
|
1253
|
+
}
|
|
1155
1254
|
return { $lookup: {
|
|
1156
1255
|
from: join.targetTable,
|
|
1157
1256
|
...varByPath.size > 0 ? { let: letVars } : {},
|
|
1158
|
-
pipeline
|
|
1257
|
+
pipeline,
|
|
1159
1258
|
as
|
|
1160
1259
|
} };
|
|
1161
1260
|
}
|
|
@@ -1205,12 +1304,11 @@ function viewMatch(node, pathOf) {
|
|
|
1205
1304
|
* `@db.view.having` as a `$match` after `$group`: refs name view fields —
|
|
1206
1305
|
* aggregates are top-level, dimensions under `_id`.
|
|
1207
1306
|
*/
|
|
1208
|
-
function havingMatch(node,
|
|
1209
|
-
const colMap = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1307
|
+
function havingMatch(node, colMap) {
|
|
1210
1308
|
return viewMatch(node, (ref) => {
|
|
1211
1309
|
const col = ref.type ? void 0 : colMap.get(ref.field);
|
|
1212
1310
|
if (!col) return ref.field;
|
|
1213
|
-
return col.aggFn ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1311
|
+
return col.aggFn || col.expr !== void 0 ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1214
1312
|
});
|
|
1215
1313
|
}
|
|
1216
1314
|
//#endregion
|
|
@@ -1700,6 +1798,11 @@ function objectIdFromStorage(value) {
|
|
|
1700
1798
|
if (Array.isArray(value)) return value.map(objectIdFromStorage);
|
|
1701
1799
|
return value;
|
|
1702
1800
|
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Documents per `_id $in` write when a mutation filter holds relational
|
|
1803
|
+
* predicates (see {@link MongoAdapter.supportsRelationFilters}).
|
|
1804
|
+
*/
|
|
1805
|
+
const REL_WRITE_BATCH = 1e3;
|
|
1703
1806
|
var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
1704
1807
|
db;
|
|
1705
1808
|
client;
|
|
@@ -1743,6 +1846,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1743
1846
|
return this.client ?? this.db;
|
|
1744
1847
|
}
|
|
1745
1848
|
/**
|
|
1849
|
+
* Same store = same client (or database handle) AND the same database: a
|
|
1850
|
+
* `$lookup` only reads collections of the pipeline's own database, and one
|
|
1851
|
+
* client may serve several (since 0.1.147).
|
|
1852
|
+
*/
|
|
1853
|
+
sharesStoreWith(other) {
|
|
1854
|
+
return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1746
1857
|
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
1747
1858
|
* Shared across all adapter instances for the same client so topology is probed once.
|
|
1748
1859
|
*/
|
|
@@ -1797,19 +1908,22 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1797
1908
|
if (!this._collection) this._collection = this.db.collection(this.resolveTableName(false));
|
|
1798
1909
|
return this._collection;
|
|
1799
1910
|
}
|
|
1800
|
-
aggregatePipeline(pipeline) {
|
|
1801
|
-
return this.collection.aggregate(pipeline,
|
|
1911
|
+
aggregatePipeline(pipeline, options) {
|
|
1912
|
+
return this.collection.aggregate(pipeline, {
|
|
1913
|
+
...options,
|
|
1914
|
+
...this._getSessionOpts()
|
|
1915
|
+
});
|
|
1802
1916
|
}
|
|
1803
1917
|
async aggregate(query) {
|
|
1804
1918
|
const { buildAggregatePipeline, buildCountPipeline } = await Promise.resolve().then(() => require("./agg.cjs"));
|
|
1805
1919
|
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1806
1920
|
if (query.controls?.$count) {
|
|
1807
|
-
const pipeline = buildCountPipeline(query, searchStage);
|
|
1921
|
+
const pipeline = buildCountPipeline(query, searchStage, this._predicateFilterOpts);
|
|
1808
1922
|
this._log("aggregate (count)", pipeline);
|
|
1809
1923
|
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1810
1924
|
return result.length > 0 ? result : [{ count: 0 }];
|
|
1811
1925
|
}
|
|
1812
|
-
const pipeline = buildAggregatePipeline(query, searchStage);
|
|
1926
|
+
const pipeline = buildAggregatePipeline(query, searchStage, this._predicateFilterOpts);
|
|
1813
1927
|
this._log("aggregate", pipeline);
|
|
1814
1928
|
return wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1815
1929
|
}
|
|
@@ -1847,7 +1961,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1847
1961
|
supportsNativePatch() {
|
|
1848
1962
|
return true;
|
|
1849
1963
|
}
|
|
1850
|
-
/**
|
|
1964
|
+
/** Every unit, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
|
|
1851
1965
|
calendarBucketUnits() {
|
|
1852
1966
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
1853
1967
|
}
|
|
@@ -1855,6 +1969,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1855
1969
|
aggregateFns() {
|
|
1856
1970
|
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
1857
1971
|
}
|
|
1972
|
+
/** Computed view columns and first-row joins. */
|
|
1973
|
+
viewCapabilities() {
|
|
1974
|
+
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
1975
|
+
}
|
|
1858
1976
|
/**
|
|
1859
1977
|
* See BaseDbAdapter.viewRenderRevision. 2 = 0.1.137 (null-guarded field filters, nested document paths).
|
|
1860
1978
|
* BUMP whenever `buildViewPipeline` output changes for an unchanged view definition.
|
|
@@ -1912,6 +2030,58 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1912
2030
|
supportsNativeRelations() {
|
|
1913
2031
|
return true;
|
|
1914
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Relational predicates (`{ nav: { $some | $none: … } }`) render as
|
|
2035
|
+
* correlated `$lookup` stages, so a read whose filter holds one runs as an
|
|
2036
|
+
* aggregation pipeline (find / count / findManyWithCount / grouped
|
|
2037
|
+
* aggregate / search / geo); predicate-free reads keep their plain
|
|
2038
|
+
* `find` / `countDocuments` path.
|
|
2039
|
+
*
|
|
2040
|
+
* Writes (`updateMany` / `replaceMany` / `deleteMany` and the single-row
|
|
2041
|
+
* variants, whose scope may carry a predicate) first resolve the matching
|
|
2042
|
+
* `_id`s through that pipeline, then write by `_id` in batches of
|
|
2043
|
+
* {@link REL_WRITE_BATCH} — re-checking the predicate-free part of the
|
|
2044
|
+
* filter. Inside an active transaction both steps share its session and
|
|
2045
|
+
* are atomic; without one there is a window between resolving and writing
|
|
2046
|
+
* in which a related document can change, so a written document may no
|
|
2047
|
+
* longer satisfy the predicate (or a newly matching one is missed).
|
|
2048
|
+
*
|
|
2049
|
+
* Collation: such a pipeline runs without an operation-wide `collation`
|
|
2050
|
+
* (it would govern the join keys and every related field too); each
|
|
2051
|
+
* table's `'nocase'` fields are compared case-insensitively explicitly
|
|
2052
|
+
* ({@link TMongoFilterOptions.collation}) — on reads and on these writes.
|
|
2053
|
+
*
|
|
2054
|
+
* @since 0.1.147
|
|
2055
|
+
*/
|
|
2056
|
+
supportsRelationFilters(_mode) {
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* The `@db.column.collate` of a field (physical or logical path), or
|
|
2061
|
+
* `undefined` for a byte-wise one — how predicate pipelines render this
|
|
2062
|
+
* table's `'nocase'` comparisons ({@link TMongoFilterOptions.collation}).
|
|
2063
|
+
*
|
|
2064
|
+
* @since 0.1.147
|
|
2065
|
+
*/
|
|
2066
|
+
fieldCollation(field) {
|
|
2067
|
+
this._table.flatMap;
|
|
2068
|
+
return this._collateFields?.get(field);
|
|
2069
|
+
}
|
|
2070
|
+
/** Filter-rendering options of a pipeline with relational predicates on this table. */
|
|
2071
|
+
get _predicateFilterOpts() {
|
|
2072
|
+
return { collation: require_mongo_accumulator.collationOfAdapter(this) };
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Operation options of a read: the request collation for a predicate-free
|
|
2076
|
+
* filter ({@link _getCollationOpts}); none with relational predicates —
|
|
2077
|
+
* those pipelines render collation per field ({@link _predicateFilterOpts}).
|
|
2078
|
+
*/
|
|
2079
|
+
_readOpts(query) {
|
|
2080
|
+
return (0, _atscript_db.containsRelationFilter)(query.filter) ? this._getSessionOpts() : {
|
|
2081
|
+
...this._getCollationOpts(query),
|
|
2082
|
+
...this._getSessionOpts()
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
1915
2085
|
async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
1916
2086
|
return loadRelationsImpl(this, rows, withRelations, relations, foreignKeys, tableResolver);
|
|
1917
2087
|
}
|
|
@@ -1924,7 +2094,11 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1924
2094
|
};
|
|
1925
2095
|
}
|
|
1926
2096
|
async nativePatch(filter, patch, ops, expectedVersion) {
|
|
1927
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "nativePatch");
|
|
2097
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "nativePatch");
|
|
2098
|
+
if (!mongoFilter) return {
|
|
2099
|
+
matchedCount: 0,
|
|
2100
|
+
modifiedCount: 0
|
|
2101
|
+
};
|
|
1928
2102
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1929
2103
|
const effectiveOps = versionColumn !== void 0 ? {
|
|
1930
2104
|
...ops,
|
|
@@ -1962,10 +2136,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1962
2136
|
}
|
|
1963
2137
|
onFieldScanned(field, _type, metadata) {
|
|
1964
2138
|
if (field === "_id") this._hasExplicitId = true;
|
|
1965
|
-
if (field !== "_id" && metadata.has("meta.id"))
|
|
1966
|
-
this._addMongoIndexField("unique", "__pk", field);
|
|
1967
|
-
this._pendingUniqueFields.push(field);
|
|
1968
|
-
}
|
|
2139
|
+
if (field !== "_id" && metadata.has("meta.id")) this._pendingUniqueFields.push(field);
|
|
1969
2140
|
for (const index of metadata.get("db.mongo.search.text") || []) this._pendingSearchFields.push({
|
|
1970
2141
|
indexName: index.indexName,
|
|
1971
2142
|
field,
|
|
@@ -2032,6 +2203,8 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2032
2203
|
};
|
|
2033
2204
|
}
|
|
2034
2205
|
onAfterFlatten() {
|
|
2206
|
+
const meta = this._table.getMetadata();
|
|
2207
|
+
for (const field of this._pendingUniqueFields) this._addMongoIndexField("unique", "__pk", meta.documentPath(field));
|
|
2035
2208
|
for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
|
|
2036
2209
|
this._pendingSearchFields = [];
|
|
2037
2210
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
@@ -2121,25 +2294,12 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2121
2294
|
return geoSearchWithCountImpl(this, point, query, indexName);
|
|
2122
2295
|
}
|
|
2123
2296
|
async findManyWithCount(query) {
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
const dataStages = [];
|
|
2127
|
-
if (controls.$sort) dataStages.push({ $sort: controls.$sort });
|
|
2128
|
-
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
2129
|
-
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
2130
|
-
if (controls.$select) {
|
|
2131
|
-
const projection = controls.$select.asProjection;
|
|
2132
|
-
if (projection) dataStages.push({ $project: dedupeProjection(projection) });
|
|
2133
|
-
}
|
|
2134
|
-
const pipeline = [{ $match: filter }, { $facet: {
|
|
2135
|
-
data: dataStages,
|
|
2297
|
+
const pipeline = [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), { $facet: {
|
|
2298
|
+
data: pageStages(query.controls),
|
|
2136
2299
|
meta: [{ $count: "count" }]
|
|
2137
2300
|
} }];
|
|
2138
2301
|
this._log("aggregate (findManyWithCount)", pipeline);
|
|
2139
|
-
const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline,
|
|
2140
|
-
...this._getCollationOpts(query),
|
|
2141
|
-
...this._getSessionOpts()
|
|
2142
|
-
}).toArray());
|
|
2302
|
+
const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray());
|
|
2143
2303
|
return {
|
|
2144
2304
|
data: result[0]?.data || [],
|
|
2145
2305
|
count: result[0]?.meta[0]?.count || 0
|
|
@@ -2169,14 +2329,94 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2169
2329
|
* is supplied for a non-versioned table — fail loud at the adapter boundary
|
|
2170
2330
|
* rather than silently dropping the CAS predicate.
|
|
2171
2331
|
*/
|
|
2172
|
-
_buildCasFilter(filter, expectedVersion, op) {
|
|
2332
|
+
async _buildCasFilter(filter, expectedVersion, op) {
|
|
2173
2333
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2174
2334
|
if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error(`${op}: expectedVersion requires versionColumn`);
|
|
2175
|
-
const mongoFilter =
|
|
2335
|
+
const mongoFilter = await this._writeFilterOne(filter);
|
|
2336
|
+
if (!mongoFilter) return;
|
|
2176
2337
|
if (expectedVersion !== void 0 && versionColumn !== void 0) mongoFilter[versionColumn] = expectedVersion;
|
|
2177
2338
|
return mongoFilter;
|
|
2178
2339
|
}
|
|
2179
2340
|
/**
|
|
2341
|
+
* The id-resolving pipeline of a write filter holding relational
|
|
2342
|
+
* predicates: the matching `_id`s (in the active transaction's session,
|
|
2343
|
+
* if any), each table's `'nocase'` fields compared like on reads
|
|
2344
|
+
* ({@link _predicateFilterOpts}). `pre` is the predicate-free part, which
|
|
2345
|
+
* every `_id`-based write re-checks — so a document that stopped matching
|
|
2346
|
+
* it in between is not written.
|
|
2347
|
+
*/
|
|
2348
|
+
_predicateWritePlan(filter) {
|
|
2349
|
+
const plan = require_mongo_accumulator.buildMongoQuery(filter, this._predicateFilterOpts);
|
|
2350
|
+
return {
|
|
2351
|
+
pipeline: require_mongo_accumulator.planStages(plan),
|
|
2352
|
+
pre: plan.pre
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
/** A write filter's `_id` filter (re-checking `pre`). */
|
|
2356
|
+
static _byIds(ids, pre) {
|
|
2357
|
+
const byId = ids.length === 1 ? { _id: ids[0] } : { _id: { $in: ids } };
|
|
2358
|
+
return pre ? { $and: [byId, pre] } : byId;
|
|
2359
|
+
}
|
|
2360
|
+
/** Single-document write over a predicate filter: the first match's `_id` filter (`undefined`: none). */
|
|
2361
|
+
async _predicateWriteFilterOne(filter) {
|
|
2362
|
+
const { pipeline, pre } = this._predicateWritePlan(filter);
|
|
2363
|
+
pipeline.push({ $limit: 1 }, { $project: { _id: 1 } });
|
|
2364
|
+
this._log("aggregate (write id)", pipeline);
|
|
2365
|
+
const [doc] = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
2366
|
+
return doc ? MongoAdapter._byIds([doc._id], pre) : void 0;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Multi-document write over `filter`: a predicate-free filter is written
|
|
2370
|
+
* as is; one with predicates calls `write` with one `_id`-based filter per
|
|
2371
|
+
* batch of {@link REL_WRITE_BATCH} matching ids, streamed from the cursor
|
|
2372
|
+
* (never all ids in memory at once).
|
|
2373
|
+
*
|
|
2374
|
+
* The ids come sorted by `_id`, so writes made while the cursor is open
|
|
2375
|
+
* can never feed back into it (otherwise a document an update moves within
|
|
2376
|
+
* the index the cursor scans could be returned — and written — twice):
|
|
2377
|
+
* after the `$lookup`s that `$sort` consumes every match before the first
|
|
2378
|
+
* batch returns, and an `_id` order read from the `_id` index is stable
|
|
2379
|
+
* (`_id` never changes).
|
|
2380
|
+
*/
|
|
2381
|
+
async _forEachMatching(filter, write) {
|
|
2382
|
+
if (!(0, _atscript_db.containsRelationFilter)(filter)) return write(require_mongo_accumulator.buildMongoFilter(filter));
|
|
2383
|
+
const { pipeline, pre } = this._predicateWritePlan(filter);
|
|
2384
|
+
pipeline.push({ $project: { _id: 1 } }, { $sort: { _id: 1 } });
|
|
2385
|
+
this._log("aggregate (write ids)", pipeline);
|
|
2386
|
+
const cursor = this.aggregatePipeline(pipeline, { allowDiskUse: true }).batchSize(REL_WRITE_BATCH);
|
|
2387
|
+
try {
|
|
2388
|
+
const next = () => wrapInvalidQuery(() => cursor.next());
|
|
2389
|
+
let ids = [];
|
|
2390
|
+
for (let doc = await next(); doc; doc = await next()) {
|
|
2391
|
+
ids.push(doc._id);
|
|
2392
|
+
if (ids.length === REL_WRITE_BATCH) {
|
|
2393
|
+
await write(MongoAdapter._byIds(ids, pre));
|
|
2394
|
+
ids = [];
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
if (ids.length > 0) await write(MongoAdapter._byIds(ids, pre));
|
|
2398
|
+
} finally {
|
|
2399
|
+
await cursor.close();
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/** Runs an update-shaped write over `filter` ({@link _forEachMatching}), counts summed. */
|
|
2403
|
+
async _updateMatching(filter, write) {
|
|
2404
|
+
const total = {
|
|
2405
|
+
matchedCount: 0,
|
|
2406
|
+
modifiedCount: 0
|
|
2407
|
+
};
|
|
2408
|
+
await this._forEachMatching(filter, async (mongoFilter) => {
|
|
2409
|
+
const result = await write(mongoFilter);
|
|
2410
|
+
total.matchedCount += result.matchedCount;
|
|
2411
|
+
total.modifiedCount += result.modifiedCount;
|
|
2412
|
+
});
|
|
2413
|
+
return total;
|
|
2414
|
+
}
|
|
2415
|
+
/** The filter of a single-document write — its predicates resolved to one `_id`. */
|
|
2416
|
+
_writeFilterOne(filter) {
|
|
2417
|
+
return (0, _atscript_db.containsRelationFilter)(filter) ? this._predicateWriteFilterOne(filter) : require_mongo_accumulator.buildMongoFilter(filter);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2180
2420
|
* Wraps an async operation to catch MongoDB duplicate key errors
|
|
2181
2421
|
* (code 11000) and rethrow as structured `DbError` — every write path
|
|
2182
2422
|
* (insert, replace, update, patch, their `*Many` variants) goes through it,
|
|
@@ -2233,6 +2473,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2233
2473
|
};
|
|
2234
2474
|
}
|
|
2235
2475
|
async findOne(query) {
|
|
2476
|
+
if ((0, _atscript_db.containsRelationFilter)(query.filter)) {
|
|
2477
|
+
const [row] = await this._aggregateFind(query, "findOne", 1);
|
|
2478
|
+
return row ?? null;
|
|
2479
|
+
}
|
|
2236
2480
|
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2237
2481
|
const opts = this._buildFindOptions(query.controls);
|
|
2238
2482
|
this._log("findOne", filter, opts);
|
|
@@ -2243,6 +2487,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2243
2487
|
}));
|
|
2244
2488
|
}
|
|
2245
2489
|
async findMany(query) {
|
|
2490
|
+
if ((0, _atscript_db.containsRelationFilter)(query.filter)) return this._aggregateFind(query, "findMany");
|
|
2246
2491
|
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2247
2492
|
const opts = this._buildFindOptions(query.controls);
|
|
2248
2493
|
this._log("findMany", filter, opts);
|
|
@@ -2253,6 +2498,11 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2253
2498
|
}).toArray());
|
|
2254
2499
|
}
|
|
2255
2500
|
async count(query) {
|
|
2501
|
+
if ((0, _atscript_db.containsRelationFilter)(query.filter)) {
|
|
2502
|
+
const pipeline = [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), { $count: "count" }];
|
|
2503
|
+
this._log("aggregate (count)", pipeline);
|
|
2504
|
+
return (await wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray()))[0]?.count ?? 0;
|
|
2505
|
+
}
|
|
2256
2506
|
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2257
2507
|
this._log("countDocuments", filter);
|
|
2258
2508
|
return this.collection.countDocuments(filter, {
|
|
@@ -2261,13 +2511,21 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2261
2511
|
});
|
|
2262
2512
|
}
|
|
2263
2513
|
async updateOne(filter, data, ops, expectedVersion) {
|
|
2264
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "updateOne");
|
|
2514
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "updateOne");
|
|
2515
|
+
if (!mongoFilter) return {
|
|
2516
|
+
matchedCount: 0,
|
|
2517
|
+
modifiedCount: 0
|
|
2518
|
+
};
|
|
2265
2519
|
const updateDoc = buildMongoUpdateDoc(data, ops, this._table.versionColumnPhysical);
|
|
2266
2520
|
this._log("updateOne", mongoFilter, updateDoc);
|
|
2267
2521
|
return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2268
2522
|
}
|
|
2269
2523
|
async replaceOne(filter, data, expectedVersion) {
|
|
2270
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "replaceOne");
|
|
2524
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "replaceOne");
|
|
2525
|
+
if (!mongoFilter) return {
|
|
2526
|
+
matchedCount: 0,
|
|
2527
|
+
modifiedCount: 0
|
|
2528
|
+
};
|
|
2271
2529
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2272
2530
|
this._log("replaceOne", mongoFilter, data);
|
|
2273
2531
|
if (versionColumn !== void 0) {
|
|
@@ -2280,27 +2538,34 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2280
2538
|
return this._wrapUpdate(() => this.collection.replaceOne(mongoFilter, data, this._getSessionOpts()));
|
|
2281
2539
|
}
|
|
2282
2540
|
async deleteOne(filter) {
|
|
2283
|
-
const mongoFilter =
|
|
2541
|
+
const mongoFilter = await this._writeFilterOne(filter);
|
|
2542
|
+
if (!mongoFilter) return { deletedCount: 0 };
|
|
2284
2543
|
this._log("deleteOne", mongoFilter);
|
|
2285
2544
|
return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
2286
2545
|
}
|
|
2287
2546
|
async updateMany(filter, data, ops) {
|
|
2288
2547
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2289
|
-
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2290
2548
|
const updateDoc = buildMongoUpdateDoc(data, ops, versionColumn);
|
|
2291
|
-
this.
|
|
2292
|
-
|
|
2549
|
+
return this._updateMatching(filter, (mongoFilter) => {
|
|
2550
|
+
this._log("updateMany", mongoFilter, updateDoc);
|
|
2551
|
+
return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2552
|
+
});
|
|
2293
2553
|
}
|
|
2294
2554
|
async replaceMany(filter, data) {
|
|
2295
|
-
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2296
2555
|
const updateDoc = buildMongoUpdateDoc(data, void 0, this._table.versionColumnPhysical);
|
|
2297
|
-
this.
|
|
2298
|
-
|
|
2556
|
+
return this._updateMatching(filter, (mongoFilter) => {
|
|
2557
|
+
this._log("replaceMany", mongoFilter, updateDoc);
|
|
2558
|
+
return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2559
|
+
});
|
|
2299
2560
|
}
|
|
2300
2561
|
async deleteMany(filter) {
|
|
2301
|
-
|
|
2302
|
-
this.
|
|
2303
|
-
|
|
2562
|
+
let deletedCount = 0;
|
|
2563
|
+
await this._forEachMatching(filter, async (mongoFilter) => {
|
|
2564
|
+
this._log("deleteMany", mongoFilter);
|
|
2565
|
+
const result = await this.collection.deleteMany(mongoFilter, this._getSessionOpts());
|
|
2566
|
+
deletedCount += result.deletedCount;
|
|
2567
|
+
});
|
|
2568
|
+
return { deletedCount };
|
|
2304
2569
|
}
|
|
2305
2570
|
clearCollectionCache() {
|
|
2306
2571
|
this._collection = void 0;
|
|
@@ -2427,6 +2692,15 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2427
2692
|
fieldCounters.set(physical, next + 1);
|
|
2428
2693
|
}
|
|
2429
2694
|
}
|
|
2695
|
+
/** A find whose filter holds relational predicates, as an aggregation pipeline. */
|
|
2696
|
+
async _aggregateFind(query, label, limit) {
|
|
2697
|
+
const pipeline = [...require_mongo_accumulator.mongoFilterStages(query.filter, this._predicateFilterOpts), ...pageStages(limit ? {
|
|
2698
|
+
...query.controls,
|
|
2699
|
+
$limit: limit
|
|
2700
|
+
} : query.controls)];
|
|
2701
|
+
this._log(`aggregate (${label})`, pipeline);
|
|
2702
|
+
return wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray());
|
|
2703
|
+
}
|
|
2430
2704
|
_buildFindOptions(controls) {
|
|
2431
2705
|
const opts = {};
|
|
2432
2706
|
if (!controls) return opts;
|
|
@@ -2443,6 +2717,9 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2443
2717
|
* Returns MongoDB collation options if any filter field has a non-binary collation.
|
|
2444
2718
|
* Uses pre-computed insights when available, falls back to computing them on demand.
|
|
2445
2719
|
* Maps: nocase → strength 2 (case-insensitive), unicode → strength 1 (case+accent-insensitive).
|
|
2720
|
+
* Predicate-free filters only — a filter with relational predicates never gets an
|
|
2721
|
+
* operation-wide collation ({@link _readOpts}), so insights are never computed over
|
|
2722
|
+
* resolved predicate operands.
|
|
2446
2723
|
*/
|
|
2447
2724
|
_getCollationOpts(query) {
|
|
2448
2725
|
if (!this._collateFields) return;
|
|
@@ -2545,6 +2822,19 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2545
2822
|
return node.fields;
|
|
2546
2823
|
}
|
|
2547
2824
|
};
|
|
2825
|
+
/** `$sort` → `$skip` → `$limit` → `$project` stages of a read's controls. */
|
|
2826
|
+
function pageStages(controls) {
|
|
2827
|
+
const stages = [];
|
|
2828
|
+
if (!controls) return stages;
|
|
2829
|
+
if (controls.$sort) stages.push({ $sort: controls.$sort });
|
|
2830
|
+
if (controls.$skip) stages.push({ $skip: controls.$skip });
|
|
2831
|
+
if (controls.$limit) stages.push({ $limit: controls.$limit });
|
|
2832
|
+
if (controls.$select) {
|
|
2833
|
+
const projection = controls.$select.asProjection;
|
|
2834
|
+
if (projection) stages.push({ $project: dedupeProjection(projection) });
|
|
2835
|
+
}
|
|
2836
|
+
return stages;
|
|
2837
|
+
}
|
|
2548
2838
|
/**
|
|
2549
2839
|
* Normalizes a declared `fuzzy` arg (`0-2`) into the query-time metadata Atlas
|
|
2550
2840
|
* accepts. Atlas only honors an edit distance of `1` or `2`; `0`/undefined means
|
|
@@ -2610,7 +2900,12 @@ function createAdapter(connection, _options) {
|
|
|
2610
2900
|
exports.CollectionPatcher = CollectionPatcher;
|
|
2611
2901
|
exports.INDEX_PREFIX = INDEX_PREFIX;
|
|
2612
2902
|
exports.MongoAdapter = MongoAdapter;
|
|
2903
|
+
exports.REL_FILTER_TEMP_PREFIX = require_mongo_accumulator.REL_FILTER_TEMP_PREFIX;
|
|
2613
2904
|
exports.buildMongoFilter = require_mongo_accumulator.buildMongoFilter;
|
|
2905
|
+
exports.buildMongoQuery = require_mongo_accumulator.buildMongoQuery;
|
|
2906
|
+
exports.collationOfAdapter = require_mongo_accumulator.collationOfAdapter;
|
|
2614
2907
|
exports.createAdapter = createAdapter;
|
|
2908
|
+
exports.mongoFilterStages = require_mongo_accumulator.mongoFilterStages;
|
|
2615
2909
|
exports.mongoIndexKey = mongoIndexKey;
|
|
2910
|
+
exports.planStages = require_mongo_accumulator.planStages;
|
|
2616
2911
|
exports.validateMongoIdPlugin = validateMongoIdPlugin;
|