@atscript/db-mongo 0.1.146 → 0.1.148
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 +126 -18
- package/dist/agg.d.cts +40 -5
- package/dist/agg.d.mts +40 -5
- package/dist/agg.mjs +123 -19
- package/dist/index.cjs +599 -194
- package/dist/index.d.cts +118 -23
- package/dist/index.d.mts +118 -23
- package/dist/index.mjs +597 -197
- package/dist/mongo-accumulator-BKMdRf3s.cjs +611 -0
- package/dist/mongo-accumulator-BvJNCVoc.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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as queryNodeToExpr, i as
|
|
2
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace,
|
|
3
|
-
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
1
|
+
import { a as buildMongoQuery, c as planStages, d as orNull, f as queryNodeToExpr, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, o as collationOfAdapter, p as correlate, r as REL_FILTER_TEMP_PREFIX, s as mongoFilterStages, t as buildAccumulator } from "./mongo-accumulator-BvJNCVoc.mjs";
|
|
2
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, containsRelationFilter, createFailureCollector, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, isAtscriptDbView, isFieldRef, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, tableNameOf, translateQueryTree, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
3
|
+
import { MongoBulkWriteError, MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
4
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
5
5
|
//#region src/lib/path-utils.ts
|
|
6
6
|
function joinPath(prefix, segment) {
|
|
@@ -373,47 +373,67 @@ function mongoIndexKey(type, name) {
|
|
|
373
373
|
}
|
|
374
374
|
//#endregion
|
|
375
375
|
//#region src/lib/mongo-relations.ts
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const val = doc[primaryKeys[0]];
|
|
379
|
-
return val == null ? "" : `${val}`;
|
|
380
|
-
}
|
|
376
|
+
/** Join key of a row's primary key values (`fields` = the row's own names for them). */
|
|
377
|
+
function buildPKKey(fields, doc) {
|
|
381
378
|
let key = "";
|
|
382
|
-
for (let i = 0; i <
|
|
379
|
+
for (let i = 0; i < fields.length; i++) {
|
|
383
380
|
if (i > 0) key += "\0";
|
|
384
|
-
const val = doc[
|
|
381
|
+
const val = doc[fields[i]];
|
|
385
382
|
key += val == null ? "" : `${val}`;
|
|
386
383
|
}
|
|
387
384
|
return key;
|
|
388
385
|
}
|
|
386
|
+
function resolveReadable(tableResolver, type) {
|
|
387
|
+
if (!type || !tableResolver) return;
|
|
388
|
+
return tableResolver(type);
|
|
389
|
+
}
|
|
390
|
+
/** The collection a readable reads (a view's name for a `@db.view`). */
|
|
391
|
+
function collectionOf(readable) {
|
|
392
|
+
return readable.getAdapter().resolveTableName(false);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Native `$with` loading: one aggregation over the source collection
|
|
396
|
+
* (re-selected by primary key) with a `$lookup` per relation.
|
|
397
|
+
*
|
|
398
|
+
* Every name is physical (since 0.1.147): join fields go through each
|
|
399
|
+
* side's `physicalPath`, and the `$with` sub-query (filter — relational
|
|
400
|
+
* predicates included —, `$sort`, `$select`) through the related table's
|
|
401
|
+
* own translation. The relation's `@db.rel.filter` is part of the lookup
|
|
402
|
+
* (target conditions ANDed into the sub-filter, junction conditions on the
|
|
403
|
+
* junction rows). Loaded documents come back as logical rows (renames,
|
|
404
|
+
* value formatters, decryption) before nested `$with` loads on them.
|
|
405
|
+
*/
|
|
389
406
|
async function loadRelationsImpl(host, rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
390
407
|
if (rows.length === 0 || withRelations.length === 0) return;
|
|
391
|
-
const
|
|
408
|
+
const source = host._table;
|
|
409
|
+
const primaryKeys = source.primaryKeys;
|
|
392
410
|
const relMeta = [];
|
|
393
411
|
for (const withRel of withRelations) {
|
|
394
412
|
if (withRel.name.includes(".")) continue;
|
|
395
413
|
const relation = relations.get(withRel.name);
|
|
396
414
|
if (!relation) throw new Error(`Unknown relation "${withRel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`);
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
relMeta.push({
|
|
400
|
-
name: withRel.name,
|
|
401
|
-
isArray: lookupResult.isArray,
|
|
402
|
-
relation,
|
|
403
|
-
nestedWith: extractNestedWith(withRel),
|
|
404
|
-
stages: lookupResult.stages
|
|
405
|
-
});
|
|
415
|
+
const lookup = buildRelationLookup(source, withRel, relation, foreignKeys, tableResolver);
|
|
416
|
+
if (lookup) relMeta.push(lookup);
|
|
406
417
|
}
|
|
407
418
|
if (relMeta.length === 0) return;
|
|
408
419
|
const pkMatchFilter = buildPKMatchFilter(rows, primaryKeys);
|
|
409
420
|
if (pkMatchFilter) {
|
|
410
|
-
const
|
|
421
|
+
const { filter } = source._translateForAdapter({ filter: pkMatchFilter });
|
|
422
|
+
const sourceMeta = source.getMetadata();
|
|
423
|
+
const physicalKeys = primaryKeys.map((pk) => sourceMeta.physicalPath(pk));
|
|
424
|
+
const project = {};
|
|
425
|
+
for (const key of physicalKeys) project[key] = 1;
|
|
426
|
+
for (const meta of relMeta) project[meta.name] = 1;
|
|
427
|
+
const pipeline = [{ $match: buildMongoFilter(filter) }];
|
|
411
428
|
for (const meta of relMeta) pipeline.push(...meta.stages);
|
|
412
|
-
|
|
429
|
+
pipeline.push({ $project: project });
|
|
430
|
+
const results = await host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
|
|
431
|
+
await Promise.all(relMeta.map((meta) => toLogicalRows(results, meta)));
|
|
432
|
+
mergeRelationResults(rows, results, primaryKeys, physicalKeys, relMeta);
|
|
413
433
|
} else for (const row of rows) for (const meta of relMeta) row[meta.name] = meta.isArray ? [] : null;
|
|
414
|
-
await loadNestedRelations(rows, relMeta
|
|
434
|
+
await loadNestedRelations(rows, relMeta);
|
|
415
435
|
}
|
|
416
|
-
/** Builds a $match filter to re-select source rows by PK. */
|
|
436
|
+
/** Builds a $match filter to re-select source rows by PK (logical names and values). */
|
|
417
437
|
function buildPKMatchFilter(rows, primaryKeys) {
|
|
418
438
|
if (primaryKeys.length === 1) {
|
|
419
439
|
const pk = primaryKeys[0];
|
|
@@ -447,137 +467,164 @@ function buildPKMatchFilter(rows, primaryKeys) {
|
|
|
447
467
|
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
448
468
|
}
|
|
449
469
|
/** Dispatches to the correct $lookup builder based on relation direction. */
|
|
450
|
-
function buildRelationLookup(
|
|
470
|
+
function buildRelationLookup(source, withRel, relation, foreignKeys, tableResolver) {
|
|
471
|
+
const target = resolveReadable(tableResolver, relation.targetType());
|
|
472
|
+
if (!target) return;
|
|
473
|
+
let built;
|
|
451
474
|
switch (relation.direction) {
|
|
452
|
-
case "to":
|
|
453
|
-
|
|
454
|
-
|
|
475
|
+
case "to":
|
|
476
|
+
built = buildToLookup(source, target, withRel, relation, foreignKeys);
|
|
477
|
+
break;
|
|
478
|
+
case "from":
|
|
479
|
+
built = buildFromLookup(source, target, withRel, relation);
|
|
480
|
+
break;
|
|
481
|
+
case "via":
|
|
482
|
+
built = buildViaLookup(source, target, withRel, relation, tableResolver);
|
|
483
|
+
break;
|
|
455
484
|
default: return;
|
|
456
485
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
exprMatch: { $eq: [`$${remoteFields[0]}`, `$$${varPrefix}0`] }
|
|
465
|
-
};
|
|
466
|
-
const andClauses = [];
|
|
467
|
-
for (let i = 0; i < remoteFields.length; i++) andClauses.push({ $eq: [`$${remoteFields[i]}`, `$$${varPrefix}${i}`] });
|
|
468
|
-
return {
|
|
469
|
-
letVars,
|
|
470
|
-
exprMatch: { $and: andClauses }
|
|
486
|
+
return built && {
|
|
487
|
+
name: withRel.name,
|
|
488
|
+
isArray: built.isArray,
|
|
489
|
+
target,
|
|
490
|
+
readControls: built.readControls,
|
|
491
|
+
nestedWith: extractNestedWith(withRel),
|
|
492
|
+
stages: built.stages
|
|
471
493
|
};
|
|
472
494
|
}
|
|
495
|
+
/** Pairs of physical join fields: `outer` on the looking-up side, `inner` on the looked-up one. */
|
|
496
|
+
function joinPairs(outerMeta, outerFields, innerMeta, innerFields) {
|
|
497
|
+
return outerFields.map((field, i) => ({
|
|
498
|
+
outer: outerMeta.physicalPath(field),
|
|
499
|
+
inner: innerMeta.physicalPath(innerFields[i])
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
473
502
|
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */
|
|
474
|
-
function buildToLookup(withRel, relation, foreignKeys) {
|
|
503
|
+
function buildToLookup(source, target, withRel, relation, foreignKeys) {
|
|
475
504
|
const fk = findFKForRelation(relation, foreignKeys);
|
|
476
|
-
|
|
477
|
-
const innerPipeline = buildLookupInnerPipeline(withRel, fk.targetFields);
|
|
478
|
-
const { letVars, exprMatch } = buildLookupJoin(fk.localFields, fk.targetFields, "fk_");
|
|
479
|
-
return {
|
|
480
|
-
stages: [{ $lookup: {
|
|
481
|
-
from: fk.targetTable,
|
|
482
|
-
let: letVars,
|
|
483
|
-
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
484
|
-
as: withRel.name
|
|
485
|
-
} }, { $unwind: {
|
|
486
|
-
path: `$${withRel.name}`,
|
|
487
|
-
preserveNullAndEmptyArrays: true
|
|
488
|
-
} }],
|
|
489
|
-
isArray: false
|
|
490
|
-
};
|
|
505
|
+
return fk ? directLookup(source, target, withRel, relation, fk.localFields, fk.targetFields, "fk_", false) : void 0;
|
|
491
506
|
}
|
|
492
507
|
/** $lookup for FROM relations (FK is on target → this table). */
|
|
493
|
-
function buildFromLookup(
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
508
|
+
function buildFromLookup(source, target, withRel, relation) {
|
|
509
|
+
const remoteFK = findRemoteFK(target, source.tableName, relation.alias);
|
|
510
|
+
return remoteFK ? directLookup(source, target, withRel, relation, remoteFK.targetFields, remoteFK.fields, "pk_", relation.isArray) : void 0;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* The correlated `$lookup` of a TO / FROM relation: `sourceFields` of this
|
|
514
|
+
* table paired with `targetFields` of the target, unwound when single-valued.
|
|
515
|
+
*/
|
|
516
|
+
function directLookup(source, target, withRel, relation, sourceFields, targetFields, prefix, isArray) {
|
|
517
|
+
const pairs = joinPairs(source.getMetadata(), sourceFields, target.getMetadata(), targetFields);
|
|
518
|
+
const inner = buildLookupInnerPipeline(target, withRel, relation, pairs.map((p) => p.inner));
|
|
519
|
+
const join = correlate(prefix, pairs);
|
|
503
520
|
const stages = [{ $lookup: {
|
|
504
|
-
from:
|
|
505
|
-
let:
|
|
506
|
-
pipeline: [
|
|
521
|
+
from: collectionOf(target),
|
|
522
|
+
let: join.let,
|
|
523
|
+
pipeline: [
|
|
524
|
+
...join.stages,
|
|
525
|
+
...inner.filter,
|
|
526
|
+
...inner.page
|
|
527
|
+
],
|
|
507
528
|
as: withRel.name
|
|
508
529
|
} }];
|
|
509
|
-
if (!
|
|
530
|
+
if (!isArray) stages.push({ $unwind: {
|
|
510
531
|
path: `$${withRel.name}`,
|
|
511
532
|
preserveNullAndEmptyArrays: true
|
|
512
533
|
} });
|
|
513
534
|
return {
|
|
514
535
|
stages,
|
|
515
|
-
isArray
|
|
536
|
+
isArray,
|
|
537
|
+
readControls: inner.readControls
|
|
516
538
|
};
|
|
517
539
|
}
|
|
518
540
|
/** $lookup for VIA relations (M:N through junction table). Always array. */
|
|
519
|
-
function buildViaLookup(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const junctionMeta = tableResolver(junctionType);
|
|
524
|
-
if (!junctionMeta) return;
|
|
525
|
-
const junctionTableName = tableNameOf(junctionType);
|
|
526
|
-
const targetTableName = tableNameOf(relation.targetType());
|
|
527
|
-
const fkToThis = findRemoteFK(junctionMeta, host._table.tableName);
|
|
541
|
+
function buildViaLookup(source, target, withRel, relation, tableResolver) {
|
|
542
|
+
const junction = resolveReadable(tableResolver, relation.viaType?.());
|
|
543
|
+
if (!junction) return;
|
|
544
|
+
const fkToThis = findRemoteFK(junction, source.tableName);
|
|
528
545
|
if (!fkToThis) return;
|
|
529
|
-
const fkToTarget = findRemoteFK(
|
|
546
|
+
const fkToTarget = findRemoteFK(junction, tableNameOf(relation.targetType()));
|
|
530
547
|
if (!fkToTarget) return;
|
|
531
|
-
const
|
|
532
|
-
const
|
|
548
|
+
const sourceMeta = source.getMetadata();
|
|
549
|
+
const junctionMeta = junction.getMetadata();
|
|
550
|
+
const targetMeta = target.getMetadata();
|
|
551
|
+
const toSource = correlate("pk_", joinPairs(sourceMeta, fkToThis.targetFields, junctionMeta, fkToThis.fields));
|
|
552
|
+
const toTargetPairs = joinPairs(junctionMeta, fkToTarget.fields, targetMeta, fkToTarget.targetFields);
|
|
553
|
+
const toTarget = correlate("fk_", toTargetPairs);
|
|
554
|
+
const inner = buildLookupInnerPipeline(target, withRel, relation, toTargetPairs.map((p) => p.inner));
|
|
555
|
+
const junctionFilter = relationStaticFilter(relation, withRel.name).junction;
|
|
556
|
+
const junctionStages = junctionFilter ? filterStages(junction, junction._translateForAdapter({ filter: junctionFilter }).filter) : [];
|
|
533
557
|
return {
|
|
534
558
|
stages: [{ $lookup: {
|
|
535
|
-
from:
|
|
536
|
-
let:
|
|
559
|
+
from: collectionOf(junction),
|
|
560
|
+
let: toSource.let,
|
|
537
561
|
pipeline: [
|
|
538
|
-
|
|
562
|
+
...toSource.stages,
|
|
563
|
+
...junctionStages,
|
|
539
564
|
{ $lookup: {
|
|
540
|
-
from:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
pipeline: innerPipeline,
|
|
565
|
+
from: collectionOf(target),
|
|
566
|
+
let: toTarget.let,
|
|
567
|
+
pipeline: [...toTarget.stages, ...inner.filter],
|
|
544
568
|
as: "__target"
|
|
545
569
|
} },
|
|
546
570
|
{ $unwind: {
|
|
547
571
|
path: "$__target",
|
|
548
572
|
preserveNullAndEmptyArrays: false
|
|
549
573
|
} },
|
|
550
|
-
{ $replaceRoot: { newRoot: "$__target" } }
|
|
574
|
+
{ $replaceRoot: { newRoot: "$__target" } },
|
|
575
|
+
...inner.page
|
|
551
576
|
],
|
|
552
577
|
as: withRel.name
|
|
553
578
|
} }],
|
|
554
|
-
isArray: true
|
|
579
|
+
isArray: true,
|
|
580
|
+
readControls: inner.readControls
|
|
555
581
|
};
|
|
556
582
|
}
|
|
557
|
-
/**
|
|
558
|
-
function
|
|
559
|
-
|
|
583
|
+
/** Stages applying a translated filter on `readable`'s collection (none when empty). */
|
|
584
|
+
function filterStages(readable, filter) {
|
|
585
|
+
if (!filter || Object.keys(filter).length === 0) return [];
|
|
586
|
+
return mongoFilterStages(filter, { collation: collationOfAdapter(readable.getAdapter()) });
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* The `$with` sub-query on the related table: its filter AND the target part
|
|
590
|
+
* of the relation's `@db.rel.filter`, plus `$sort` / `$skip` / `$limit` /
|
|
591
|
+
* `$select` — translated by the TARGET readable (physical names, value
|
|
592
|
+
* formatters, relational predicates resolved). `requiredFields` (physical
|
|
593
|
+
* join keys) are always projected.
|
|
594
|
+
*/
|
|
595
|
+
function buildLookupInnerPipeline(target, withRel, relation, requiredFields) {
|
|
560
596
|
const flatRel = withRel;
|
|
561
597
|
const nested = withRel.controls || {};
|
|
562
|
-
const filter = withRel.filter;
|
|
563
598
|
const sort = nested.$sort || flatRel.$sort;
|
|
564
599
|
const limit = nested.$limit ?? flatRel.$limit;
|
|
565
600
|
const skip = nested.$skip ?? flatRel.$skip;
|
|
566
601
|
const select = nested.$select || flatRel.$select;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
if (
|
|
571
|
-
const
|
|
602
|
+
const statics = relationStaticFilter(relation, withRel.name);
|
|
603
|
+
const controls = {};
|
|
604
|
+
if (sort) controls.$sort = sort;
|
|
605
|
+
if (select) controls.$select = select;
|
|
606
|
+
const translated = target._translateForAdapter({
|
|
607
|
+
filter: andFilters(statics.target, withRel.filter),
|
|
608
|
+
controls
|
|
609
|
+
});
|
|
610
|
+
const page = [];
|
|
611
|
+
if (translated.controls?.$sort) page.push({ $sort: translated.controls.$sort });
|
|
612
|
+
if (skip) page.push({ $skip: skip });
|
|
613
|
+
if (limit !== null && limit !== void 0) page.push({ $limit: limit });
|
|
614
|
+
const projection = translated.controls?.$select?.asProjection;
|
|
572
615
|
if (projection) {
|
|
573
616
|
const project = { ...projection };
|
|
574
617
|
if (Object.values(project).some((flag) => flag === 1 || flag === true)) {
|
|
575
618
|
for (const f of requiredFields) project[f] = 1;
|
|
576
619
|
if (!("_id" in projection) && !requiredFields.includes("_id")) project._id = 0;
|
|
577
620
|
} else for (const f of requiredFields) delete project[f];
|
|
578
|
-
if (Object.keys(project).length > 0)
|
|
621
|
+
if (Object.keys(project).length > 0) page.push({ $project: dedupeProjection(project) });
|
|
579
622
|
}
|
|
580
|
-
return
|
|
623
|
+
return {
|
|
624
|
+
filter: filterStages(target, translated.filter),
|
|
625
|
+
page,
|
|
626
|
+
readControls: select ? { $select: select } : {}
|
|
627
|
+
};
|
|
581
628
|
}
|
|
582
629
|
/** Extracts nested $with from a WithRelation's controls. */
|
|
583
630
|
function extractNestedWith(withRel) {
|
|
@@ -586,15 +633,10 @@ function extractNestedWith(withRel) {
|
|
|
586
633
|
return nestedWith && nestedWith.length > 0 ? nestedWith : void 0;
|
|
587
634
|
}
|
|
588
635
|
/** Post-processes nested $with by delegating to the target table's own relation loading. */
|
|
589
|
-
async function loadNestedRelations(rows, relMeta
|
|
590
|
-
if (!tableResolver) return;
|
|
636
|
+
async function loadNestedRelations(rows, relMeta) {
|
|
591
637
|
const tasks = [];
|
|
592
638
|
for (const meta of relMeta) {
|
|
593
639
|
if (!meta.nestedWith || meta.nestedWith.length === 0) continue;
|
|
594
|
-
const targetType = meta.relation.targetType();
|
|
595
|
-
if (!targetType) continue;
|
|
596
|
-
const targetTable = tableResolver(targetType);
|
|
597
|
-
if (!targetTable) continue;
|
|
598
640
|
const subRows = [];
|
|
599
641
|
for (const row of rows) {
|
|
600
642
|
const val = row[meta.name];
|
|
@@ -602,21 +644,44 @@ async function loadNestedRelations(rows, relMeta, tableResolver) {
|
|
|
602
644
|
else if (val && typeof val === "object") subRows.push(val);
|
|
603
645
|
}
|
|
604
646
|
if (subRows.length === 0) continue;
|
|
605
|
-
tasks.push(
|
|
647
|
+
tasks.push(meta.target.loadRelations(subRows, meta.nestedWith));
|
|
606
648
|
}
|
|
607
649
|
await Promise.all(tasks);
|
|
608
650
|
}
|
|
609
|
-
/**
|
|
610
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Replaces one relation's loaded documents on every aggregation result with
|
|
653
|
+
* the related table's LOGICAL rows (one conversion call for all of them).
|
|
654
|
+
*/
|
|
655
|
+
async function toLogicalRows(results, meta) {
|
|
656
|
+
const physical = [];
|
|
657
|
+
const spans = [];
|
|
658
|
+
for (const doc of results) {
|
|
659
|
+
const value = doc[meta.name];
|
|
660
|
+
const list = Array.isArray(value) ? value : value && typeof value === "object" ? [value] : [];
|
|
661
|
+
spans.push([
|
|
662
|
+
doc,
|
|
663
|
+
physical.length,
|
|
664
|
+
list.length
|
|
665
|
+
]);
|
|
666
|
+
physical.push(...list);
|
|
667
|
+
}
|
|
668
|
+
const logical = physical.length > 0 ? await meta.target._rowsFromAdapter(physical, meta.readControls) : [];
|
|
669
|
+
for (const [doc, start, length] of spans) doc[meta.name] = meta.isArray ? logical.slice(start, start + length) : length > 0 ? logical[start] : null;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Merges aggregation results back onto the original rows by PK — the
|
|
673
|
+
* results carry the PHYSICAL key fields (`physicalKeys`), the rows the
|
|
674
|
+
* logical ones (`primaryKeys`).
|
|
675
|
+
*/
|
|
676
|
+
function mergeRelationResults(rows, results, primaryKeys, physicalKeys, relMeta) {
|
|
611
677
|
const resultIndex = /* @__PURE__ */ new Map();
|
|
612
|
-
for (const doc of results) resultIndex.set(buildPKKey(
|
|
678
|
+
for (const doc of results) resultIndex.set(buildPKKey(physicalKeys, doc), doc);
|
|
613
679
|
for (const row of rows) {
|
|
614
680
|
const enriched = resultIndex.get(buildPKKey(primaryKeys, row));
|
|
615
|
-
for (const meta of relMeta)
|
|
616
|
-
const value = enriched[meta.name];
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
} else row[meta.name] = meta.isArray ? [] : null;
|
|
681
|
+
for (const meta of relMeta) {
|
|
682
|
+
const value = enriched?.[meta.name];
|
|
683
|
+
row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
684
|
+
}
|
|
620
685
|
}
|
|
621
686
|
}
|
|
622
687
|
/** Finds FK entry for a TO relation from this table's foreignKeys map. */
|
|
@@ -643,6 +708,10 @@ function findRemoteFK(target, thisTableName, alias) {
|
|
|
643
708
|
}
|
|
644
709
|
//#endregion
|
|
645
710
|
//#region src/lib/mongo-search.ts
|
|
711
|
+
/** Renders a host's filter with relational predicates — its 'nocase' fields per field. */
|
|
712
|
+
function filterOptionsOf(host) {
|
|
713
|
+
return { collation: collationOfAdapter(host) };
|
|
714
|
+
}
|
|
646
715
|
/**
|
|
647
716
|
* Returns available search indexes as generic metadata for UI. The text
|
|
648
717
|
* default is the `DEFAULT` entry (an alias of the index `search()` answers
|
|
@@ -735,7 +804,7 @@ const DISTANCE_FIELD = "__atscript_distance";
|
|
|
735
804
|
/** Distance-ranked geo search via a leading `$geoNear` aggregation stage. */
|
|
736
805
|
async function geoSearchImpl(host, point, query, indexName) {
|
|
737
806
|
const controls = query.controls || {};
|
|
738
|
-
const pipeline =
|
|
807
|
+
const pipeline = buildGeoNearStages(host, point, query, indexName);
|
|
739
808
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
740
809
|
pipeline.push({ $limit: controls.$limit || 1e3 });
|
|
741
810
|
pushGeoProjection(pipeline, query.controls);
|
|
@@ -749,7 +818,7 @@ async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
|
749
818
|
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
750
819
|
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
751
820
|
pushGeoProjection(dataStages, query.controls);
|
|
752
|
-
const pipeline = [
|
|
821
|
+
const pipeline = [...buildGeoNearStages(host, point, query, indexName), { $facet: {
|
|
753
822
|
data: dataStages,
|
|
754
823
|
meta: [{ $count: "count" }]
|
|
755
824
|
} }];
|
|
@@ -760,9 +829,15 @@ async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
|
760
829
|
count: result[0]?.meta?.[0]?.count || 0
|
|
761
830
|
};
|
|
762
831
|
}
|
|
763
|
-
/**
|
|
764
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Builds the leading `$geoNear` stage; the filter rides in its `query`
|
|
834
|
+
* option. A filter with relational predicates keeps its predicate-free
|
|
835
|
+
* conjuncts there and applies the rest (predicate `$lookup`s + `$match`)
|
|
836
|
+
* right after — exact, since `$geoNear` has no result cut-off of its own.
|
|
837
|
+
*/
|
|
838
|
+
function buildGeoNearStages(host, point, query, indexName) {
|
|
765
839
|
const controls = query.controls || {};
|
|
840
|
+
const plan = containsRelationFilter(query.filter) ? buildMongoQuery(query.filter, filterOptionsOf(host)) : void 0;
|
|
766
841
|
const geoNear = {
|
|
767
842
|
near: {
|
|
768
843
|
type: "Point",
|
|
@@ -771,11 +846,14 @@ function buildGeoNearStage(host, point, query, indexName) {
|
|
|
771
846
|
distanceField: DISTANCE_FIELD,
|
|
772
847
|
spherical: true,
|
|
773
848
|
key: resolveGeoKeyPath(host, indexName),
|
|
774
|
-
query: buildMongoFilter(query.filter)
|
|
849
|
+
query: plan ? plan.pre ?? {} : buildMongoFilter(query.filter)
|
|
775
850
|
};
|
|
776
851
|
if (typeof controls.$maxDistance === "number") geoNear.maxDistance = controls.$maxDistance;
|
|
777
852
|
if (typeof controls.$minDistance === "number") geoNear.minDistance = controls.$minDistance;
|
|
778
|
-
return { $geoNear: geoNear }
|
|
853
|
+
return plan ? [{ $geoNear: geoNear }, ...planStages({
|
|
854
|
+
...plan,
|
|
855
|
+
pre: void 0
|
|
856
|
+
})] : [{ $geoNear: geoNear }];
|
|
779
857
|
}
|
|
780
858
|
/** Resolves the physical field path of the targeted geo index. */
|
|
781
859
|
function resolveGeoKeyPath(host, indexName) {
|
|
@@ -984,14 +1062,13 @@ function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
|
984
1062
|
}
|
|
985
1063
|
/** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
|
|
986
1064
|
async function runSearchPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
987
|
-
const filter = buildMongoFilter(query.filter);
|
|
988
1065
|
const controls = query.controls || {};
|
|
989
1066
|
const pipeline = [stage];
|
|
990
1067
|
if (threshold !== void 0) {
|
|
991
1068
|
pipeline.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
|
|
992
1069
|
pipeline.push({ $match: { _score: { $gte: threshold } } });
|
|
993
1070
|
} else if (classicText) pipeline.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
994
|
-
pipeline.push(
|
|
1071
|
+
pipeline.push(...mongoFilterStages(query.filter, filterOptionsOf(host)));
|
|
995
1072
|
if (controls.$sort) pipeline.push({ $sort: controls.$sort });
|
|
996
1073
|
else if (classicText) pipeline.push({ $sort: { _score: -1 } });
|
|
997
1074
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
@@ -1006,7 +1083,6 @@ async function runSearchPipeline(host, stage, query, label, threshold, classicTe
|
|
|
1006
1083
|
}
|
|
1007
1084
|
/** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
|
|
1008
1085
|
async function runSearchWithCountPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
1009
|
-
const filter = buildMongoFilter(query.filter);
|
|
1010
1086
|
const controls = query.controls || {};
|
|
1011
1087
|
const preStages = [];
|
|
1012
1088
|
if (threshold !== void 0) {
|
|
@@ -1025,7 +1101,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold,
|
|
|
1025
1101
|
const pipeline = [
|
|
1026
1102
|
stage,
|
|
1027
1103
|
...preStages,
|
|
1028
|
-
|
|
1104
|
+
...mongoFilterStages(query.filter, filterOptionsOf(host)),
|
|
1029
1105
|
{ $facet: {
|
|
1030
1106
|
data: dataStages,
|
|
1031
1107
|
meta: [{ $count: "count" }]
|
|
@@ -1088,11 +1164,28 @@ function buildViewPipeline(view) {
|
|
|
1088
1164
|
/** A column's source field as an aggregation operand. */
|
|
1089
1165
|
const colSourceField = (col) => `$${prefixes.get(col.sourceTable) ?? ""}${col.sourceColumn}`;
|
|
1090
1166
|
const colValue = (col) => col.nullable ? orNull(colSourceField(col)) : colSourceField(col);
|
|
1167
|
+
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1168
|
+
const rendered = /* @__PURE__ */ new Map();
|
|
1169
|
+
const computedExpr = (col, leaf) => {
|
|
1170
|
+
let out = rendered.get(col.viewPath);
|
|
1171
|
+
if (out === void 0) {
|
|
1172
|
+
out = exprToMongo(col.expr, (path) => {
|
|
1173
|
+
const c = byPath.get(path);
|
|
1174
|
+
return c.expr === void 0 ? { $toDouble: leaf(c) } : computedExpr(c, leaf);
|
|
1175
|
+
});
|
|
1176
|
+
rendered.set(col.viewPath, out);
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
};
|
|
1091
1180
|
if (columns.some((c) => c.aggFn)) {
|
|
1092
1181
|
const group = { _id: {} };
|
|
1093
1182
|
const project = { _id: 0 };
|
|
1094
1183
|
const distinctSizes = {};
|
|
1095
|
-
|
|
1184
|
+
const computed = {};
|
|
1185
|
+
for (const col of columns) if (col.expr !== void 0) {
|
|
1186
|
+
computed[col.viewColumn] = computedExpr(col, (c) => c.aggFn ? `$${c.viewColumn}` : `$_id.${c.viewColumn}`);
|
|
1187
|
+
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1188
|
+
} else if (col.aggFn) {
|
|
1096
1189
|
const predicate = col.aggFilter ? queryNodeToExpr(col.aggFilter, (ref) => `$${pathOf(ref)}`) : void 0;
|
|
1097
1190
|
group[col.viewColumn] = buildAccumulator(col.aggFn, col.aggField === "*" ? "*" : colSourceField(col), predicate, col.viewColumn);
|
|
1098
1191
|
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
@@ -1103,11 +1196,12 @@ function buildViewPipeline(view) {
|
|
|
1103
1196
|
}
|
|
1104
1197
|
pipeline.push({ $group: group });
|
|
1105
1198
|
if (Object.keys(distinctSizes).length > 0) pipeline.push({ $addFields: distinctSizes });
|
|
1106
|
-
if (
|
|
1199
|
+
if (Object.keys(computed).length > 0) pipeline.push({ $addFields: computed });
|
|
1200
|
+
if (plan.having) pipeline.push({ $match: havingMatch(plan.having, byPath) });
|
|
1107
1201
|
pipeline.push({ $project: project });
|
|
1108
1202
|
} else {
|
|
1109
1203
|
const project = { _id: 0 };
|
|
1110
|
-
for (const col of columns) project[col.viewColumn] = colValue(col);
|
|
1204
|
+
for (const col of columns) project[col.viewColumn] = col.expr === void 0 ? colValue(col) : computedExpr(col, colValue);
|
|
1111
1205
|
pipeline.push({ $project: project });
|
|
1112
1206
|
}
|
|
1113
1207
|
return pipeline;
|
|
@@ -1128,7 +1222,7 @@ function buildViewPipeline(view) {
|
|
|
1128
1222
|
function buildLookup(view, join, outer) {
|
|
1129
1223
|
const scope = join.scope;
|
|
1130
1224
|
const as = joinedAs(scope);
|
|
1131
|
-
const simple = simpleJoinFields(view, join, outer);
|
|
1225
|
+
const simple = join.first ? void 0 : simpleJoinFields(view, join, outer);
|
|
1132
1226
|
if (simple) return { $lookup: {
|
|
1133
1227
|
from: join.targetTable,
|
|
1134
1228
|
...simple,
|
|
@@ -1150,11 +1244,16 @@ function buildLookup(view, join, outer) {
|
|
|
1150
1244
|
}
|
|
1151
1245
|
return `$$${varName}`;
|
|
1152
1246
|
};
|
|
1153
|
-
const
|
|
1247
|
+
const pipeline = [{ $match: { $expr: queryNodeToExpr(join.condition, pathOf) } }];
|
|
1248
|
+
if (join.first) {
|
|
1249
|
+
const sort = {};
|
|
1250
|
+
for (const { ref, desc } of join.first.order) sort[view.resolveRefSource(ref).source.column] = desc ? -1 : 1;
|
|
1251
|
+
pipeline.push({ $sort: sort }, { $limit: 1 });
|
|
1252
|
+
}
|
|
1154
1253
|
return { $lookup: {
|
|
1155
1254
|
from: join.targetTable,
|
|
1156
1255
|
...varByPath.size > 0 ? { let: letVars } : {},
|
|
1157
|
-
pipeline
|
|
1256
|
+
pipeline,
|
|
1158
1257
|
as
|
|
1159
1258
|
} };
|
|
1160
1259
|
}
|
|
@@ -1204,12 +1303,11 @@ function viewMatch(node, pathOf) {
|
|
|
1204
1303
|
* `@db.view.having` as a `$match` after `$group`: refs name view fields —
|
|
1205
1304
|
* aggregates are top-level, dimensions under `_id`.
|
|
1206
1305
|
*/
|
|
1207
|
-
function havingMatch(node,
|
|
1208
|
-
const colMap = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1306
|
+
function havingMatch(node, colMap) {
|
|
1209
1307
|
return viewMatch(node, (ref) => {
|
|
1210
1308
|
const col = ref.type ? void 0 : colMap.get(ref.field);
|
|
1211
1309
|
if (!col) return ref.field;
|
|
1212
|
-
return col.aggFn ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1310
|
+
return col.aggFn || col.expr !== void 0 ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1213
1311
|
});
|
|
1214
1312
|
}
|
|
1215
1313
|
//#endregion
|
|
@@ -1699,6 +1797,16 @@ function objectIdFromStorage(value) {
|
|
|
1699
1797
|
if (Array.isArray(value)) return value.map(objectIdFromStorage);
|
|
1700
1798
|
return value;
|
|
1701
1799
|
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Documents per `_id $in` write when a mutation filter holds relational
|
|
1802
|
+
* predicates (see {@link MongoAdapter.supportsRelationFilters}).
|
|
1803
|
+
*/
|
|
1804
|
+
const REL_WRITE_BATCH = 1e3;
|
|
1805
|
+
/** Whether a bulk-write error carries a write-concern failure besides (or instead of) write errors. */
|
|
1806
|
+
function hasWriteConcernError(error) {
|
|
1807
|
+
const e = error;
|
|
1808
|
+
return Boolean(e.writeConcernError || e.result?.getWriteConcernError?.() || e.result?.writeConcernErrors?.length);
|
|
1809
|
+
}
|
|
1702
1810
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
1703
1811
|
db;
|
|
1704
1812
|
client;
|
|
@@ -1742,6 +1850,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1742
1850
|
return this.client ?? this.db;
|
|
1743
1851
|
}
|
|
1744
1852
|
/**
|
|
1853
|
+
* Same store = same client (or database handle) AND the same database: a
|
|
1854
|
+
* `$lookup` only reads collections of the pipeline's own database, and one
|
|
1855
|
+
* client may serve several (since 0.1.147).
|
|
1856
|
+
*/
|
|
1857
|
+
sharesStoreWith(other) {
|
|
1858
|
+
return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1745
1861
|
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
1746
1862
|
* Shared across all adapter instances for the same client so topology is probed once.
|
|
1747
1863
|
*/
|
|
@@ -1796,21 +1912,35 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1796
1912
|
if (!this._collection) this._collection = this.db.collection(this.resolveTableName(false));
|
|
1797
1913
|
return this._collection;
|
|
1798
1914
|
}
|
|
1799
|
-
aggregatePipeline(pipeline) {
|
|
1800
|
-
return this.collection.aggregate(pipeline,
|
|
1915
|
+
aggregatePipeline(pipeline, options) {
|
|
1916
|
+
return this.collection.aggregate(pipeline, {
|
|
1917
|
+
...options,
|
|
1918
|
+
...this._getSessionOpts()
|
|
1919
|
+
});
|
|
1801
1920
|
}
|
|
1802
1921
|
async aggregate(query) {
|
|
1803
|
-
const { buildAggregatePipeline, buildCountPipeline } = await import("./agg.mjs");
|
|
1922
|
+
const { aggregateOptions, buildAggregatePipeline, buildCountPipeline, buildMatchedProbe, emptyGroupRow, emptyGroupStages } = await import("./agg.mjs");
|
|
1923
|
+
const emptyGroup = async (forCount) => {
|
|
1924
|
+
const row = emptyGroupRow(query);
|
|
1925
|
+
if (!row) return void 0;
|
|
1926
|
+
const probe = buildMatchedProbe(query, searchStage, this._predicateFilterOpts);
|
|
1927
|
+
if ((await wrapInvalidQuery(() => this.aggregatePipeline(probe).toArray())).length > 0) return;
|
|
1928
|
+
const stages = emptyGroupStages(query, row, forCount);
|
|
1929
|
+
if (!stages) return row;
|
|
1930
|
+
return (await wrapInvalidQuery(() => this.aggregatePipeline(stages).toArray()))[0];
|
|
1931
|
+
};
|
|
1804
1932
|
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1805
1933
|
if (query.controls?.$count) {
|
|
1806
|
-
const pipeline = buildCountPipeline(query, searchStage);
|
|
1934
|
+
const pipeline = buildCountPipeline(query, searchStage, this._predicateFilterOpts);
|
|
1807
1935
|
this._log("aggregate (count)", pipeline);
|
|
1808
|
-
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1809
|
-
return result.length > 0 ? result : [{ count: 0 }];
|
|
1936
|
+
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
1937
|
+
return result.length > 0 ? result : [{ count: await emptyGroup(true) ? 1 : 0 }];
|
|
1810
1938
|
}
|
|
1811
|
-
const pipeline = buildAggregatePipeline(query, searchStage);
|
|
1939
|
+
const pipeline = buildAggregatePipeline(query, searchStage, this._predicateFilterOpts);
|
|
1812
1940
|
this._log("aggregate", pipeline);
|
|
1813
|
-
|
|
1941
|
+
const rows = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
1942
|
+
const empty = rows.length === 0 ? await emptyGroup(false) : void 0;
|
|
1943
|
+
return empty ? [empty] : rows;
|
|
1814
1944
|
}
|
|
1815
1945
|
get idType() {
|
|
1816
1946
|
const idProp = this._table.type.type.props.get("_id");
|
|
@@ -1846,14 +1976,22 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1846
1976
|
supportsNativePatch() {
|
|
1847
1977
|
return true;
|
|
1848
1978
|
}
|
|
1849
|
-
/**
|
|
1979
|
+
/** Every unit, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
|
|
1850
1980
|
calendarBucketUnits() {
|
|
1851
1981
|
return ALL_BUCKET_UNITS;
|
|
1852
1982
|
}
|
|
1853
|
-
/** Every aggregate function
|
|
1983
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
1854
1984
|
aggregateFns() {
|
|
1855
1985
|
return ALL_AGGREGATE_FNS;
|
|
1856
1986
|
}
|
|
1987
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
1988
|
+
supportsAggregateExpressions() {
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
/** Computed view columns and first-row joins. */
|
|
1992
|
+
viewCapabilities() {
|
|
1993
|
+
return ALL_VIEW_CAPABILITIES;
|
|
1994
|
+
}
|
|
1857
1995
|
/**
|
|
1858
1996
|
* See BaseDbAdapter.viewRenderRevision. 2 = 0.1.137 (null-guarded field filters, nested document paths).
|
|
1859
1997
|
* BUMP whenever `buildViewPipeline` output changes for an unchanged view definition.
|
|
@@ -1911,6 +2049,58 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1911
2049
|
supportsNativeRelations() {
|
|
1912
2050
|
return true;
|
|
1913
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Relational predicates (`{ nav: { $some | $none: … } }`) render as
|
|
2054
|
+
* correlated `$lookup` stages, so a read whose filter holds one runs as an
|
|
2055
|
+
* aggregation pipeline (find / count / findManyWithCount / grouped
|
|
2056
|
+
* aggregate / search / geo); predicate-free reads keep their plain
|
|
2057
|
+
* `find` / `countDocuments` path.
|
|
2058
|
+
*
|
|
2059
|
+
* Writes (`updateMany` / `replaceMany` / `deleteMany` and the single-row
|
|
2060
|
+
* variants, whose scope may carry a predicate) first resolve the matching
|
|
2061
|
+
* `_id`s through that pipeline, then write by `_id` in batches of
|
|
2062
|
+
* {@link REL_WRITE_BATCH} — re-checking the predicate-free part of the
|
|
2063
|
+
* filter. Inside an active transaction both steps share its session and
|
|
2064
|
+
* are atomic; without one there is a window between resolving and writing
|
|
2065
|
+
* in which a related document can change, so a written document may no
|
|
2066
|
+
* longer satisfy the predicate (or a newly matching one is missed).
|
|
2067
|
+
*
|
|
2068
|
+
* Collation: such a pipeline runs without an operation-wide `collation`
|
|
2069
|
+
* (it would govern the join keys and every related field too); each
|
|
2070
|
+
* table's `'nocase'` fields are compared case-insensitively explicitly
|
|
2071
|
+
* ({@link TMongoFilterOptions.collation}) — on reads and on these writes.
|
|
2072
|
+
*
|
|
2073
|
+
* @since 0.1.147
|
|
2074
|
+
*/
|
|
2075
|
+
supportsRelationFilters(_mode) {
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* The `@db.column.collate` of a field (physical or logical path), or
|
|
2080
|
+
* `undefined` for a byte-wise one — how predicate pipelines render this
|
|
2081
|
+
* table's `'nocase'` comparisons ({@link TMongoFilterOptions.collation}).
|
|
2082
|
+
*
|
|
2083
|
+
* @since 0.1.147
|
|
2084
|
+
*/
|
|
2085
|
+
fieldCollation(field) {
|
|
2086
|
+
this._table.flatMap;
|
|
2087
|
+
return this._collateFields?.get(field);
|
|
2088
|
+
}
|
|
2089
|
+
/** Filter-rendering options of a pipeline with relational predicates on this table. */
|
|
2090
|
+
get _predicateFilterOpts() {
|
|
2091
|
+
return { collation: collationOfAdapter(this) };
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Operation options of a read: the request collation for a predicate-free
|
|
2095
|
+
* filter ({@link _getCollationOpts}); none with relational predicates —
|
|
2096
|
+
* those pipelines render collation per field ({@link _predicateFilterOpts}).
|
|
2097
|
+
*/
|
|
2098
|
+
_readOpts(query) {
|
|
2099
|
+
return containsRelationFilter(query.filter) ? this._getSessionOpts() : {
|
|
2100
|
+
...this._getCollationOpts(query),
|
|
2101
|
+
...this._getSessionOpts()
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
1914
2104
|
async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
1915
2105
|
return loadRelationsImpl(this, rows, withRelations, relations, foreignKeys, tableResolver);
|
|
1916
2106
|
}
|
|
@@ -1923,7 +2113,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1923
2113
|
};
|
|
1924
2114
|
}
|
|
1925
2115
|
async nativePatch(filter, patch, ops, expectedVersion) {
|
|
1926
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "nativePatch");
|
|
2116
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "nativePatch");
|
|
2117
|
+
if (!mongoFilter) return {
|
|
2118
|
+
matchedCount: 0,
|
|
2119
|
+
modifiedCount: 0
|
|
2120
|
+
};
|
|
1927
2121
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1928
2122
|
const effectiveOps = versionColumn !== void 0 ? {
|
|
1929
2123
|
...ops,
|
|
@@ -1961,10 +2155,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1961
2155
|
}
|
|
1962
2156
|
onFieldScanned(field, _type, metadata) {
|
|
1963
2157
|
if (field === "_id") this._hasExplicitId = true;
|
|
1964
|
-
if (field !== "_id" && metadata.has("meta.id"))
|
|
1965
|
-
this._addMongoIndexField("unique", "__pk", field);
|
|
1966
|
-
this._pendingUniqueFields.push(field);
|
|
1967
|
-
}
|
|
2158
|
+
if (field !== "_id" && metadata.has("meta.id")) this._pendingUniqueFields.push(field);
|
|
1968
2159
|
for (const index of metadata.get("db.mongo.search.text") || []) this._pendingSearchFields.push({
|
|
1969
2160
|
indexName: index.indexName,
|
|
1970
2161
|
field,
|
|
@@ -2031,6 +2222,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2031
2222
|
};
|
|
2032
2223
|
}
|
|
2033
2224
|
onAfterFlatten() {
|
|
2225
|
+
const meta = this._table.getMetadata();
|
|
2226
|
+
for (const field of this._pendingUniqueFields) this._addMongoIndexField("unique", "__pk", meta.documentPath(field));
|
|
2034
2227
|
for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
|
|
2035
2228
|
this._pendingSearchFields = [];
|
|
2036
2229
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
@@ -2120,25 +2313,12 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2120
2313
|
return geoSearchWithCountImpl(this, point, query, indexName);
|
|
2121
2314
|
}
|
|
2122
2315
|
async findManyWithCount(query) {
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
const dataStages = [];
|
|
2126
|
-
if (controls.$sort) dataStages.push({ $sort: controls.$sort });
|
|
2127
|
-
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
2128
|
-
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
2129
|
-
if (controls.$select) {
|
|
2130
|
-
const projection = controls.$select.asProjection;
|
|
2131
|
-
if (projection) dataStages.push({ $project: dedupeProjection(projection) });
|
|
2132
|
-
}
|
|
2133
|
-
const pipeline = [{ $match: filter }, { $facet: {
|
|
2134
|
-
data: dataStages,
|
|
2316
|
+
const pipeline = [...mongoFilterStages(query.filter, this._predicateFilterOpts), { $facet: {
|
|
2317
|
+
data: pageStages(query.controls),
|
|
2135
2318
|
meta: [{ $count: "count" }]
|
|
2136
2319
|
} }];
|
|
2137
2320
|
this._log("aggregate (findManyWithCount)", pipeline);
|
|
2138
|
-
const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline,
|
|
2139
|
-
...this._getCollationOpts(query),
|
|
2140
|
-
...this._getSessionOpts()
|
|
2141
|
-
}).toArray());
|
|
2321
|
+
const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray());
|
|
2142
2322
|
return {
|
|
2143
2323
|
data: result[0]?.data || [],
|
|
2144
2324
|
count: result[0]?.meta[0]?.count || 0
|
|
@@ -2168,14 +2348,94 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2168
2348
|
* is supplied for a non-versioned table — fail loud at the adapter boundary
|
|
2169
2349
|
* rather than silently dropping the CAS predicate.
|
|
2170
2350
|
*/
|
|
2171
|
-
_buildCasFilter(filter, expectedVersion, op) {
|
|
2351
|
+
async _buildCasFilter(filter, expectedVersion, op) {
|
|
2172
2352
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2173
2353
|
if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error(`${op}: expectedVersion requires versionColumn`);
|
|
2174
|
-
const mongoFilter =
|
|
2354
|
+
const mongoFilter = await this._writeFilterOne(filter);
|
|
2355
|
+
if (!mongoFilter) return;
|
|
2175
2356
|
if (expectedVersion !== void 0 && versionColumn !== void 0) mongoFilter[versionColumn] = expectedVersion;
|
|
2176
2357
|
return mongoFilter;
|
|
2177
2358
|
}
|
|
2178
2359
|
/**
|
|
2360
|
+
* The id-resolving pipeline of a write filter holding relational
|
|
2361
|
+
* predicates: the matching `_id`s (in the active transaction's session,
|
|
2362
|
+
* if any), each table's `'nocase'` fields compared like on reads
|
|
2363
|
+
* ({@link _predicateFilterOpts}). `pre` is the predicate-free part, which
|
|
2364
|
+
* every `_id`-based write re-checks — so a document that stopped matching
|
|
2365
|
+
* it in between is not written.
|
|
2366
|
+
*/
|
|
2367
|
+
_predicateWritePlan(filter) {
|
|
2368
|
+
const plan = buildMongoQuery(filter, this._predicateFilterOpts);
|
|
2369
|
+
return {
|
|
2370
|
+
pipeline: planStages(plan),
|
|
2371
|
+
pre: plan.pre
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
/** A write filter's `_id` filter (re-checking `pre`). */
|
|
2375
|
+
static _byIds(ids, pre) {
|
|
2376
|
+
const byId = ids.length === 1 ? { _id: ids[0] } : { _id: { $in: ids } };
|
|
2377
|
+
return pre ? { $and: [byId, pre] } : byId;
|
|
2378
|
+
}
|
|
2379
|
+
/** Single-document write over a predicate filter: the first match's `_id` filter (`undefined`: none). */
|
|
2380
|
+
async _predicateWriteFilterOne(filter) {
|
|
2381
|
+
const { pipeline, pre } = this._predicateWritePlan(filter);
|
|
2382
|
+
pipeline.push({ $limit: 1 }, { $project: { _id: 1 } });
|
|
2383
|
+
this._log("aggregate (write id)", pipeline);
|
|
2384
|
+
const [doc] = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
2385
|
+
return doc ? MongoAdapter._byIds([doc._id], pre) : void 0;
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Multi-document write over `filter`: a predicate-free filter is written
|
|
2389
|
+
* as is; one with predicates calls `write` with one `_id`-based filter per
|
|
2390
|
+
* batch of {@link REL_WRITE_BATCH} matching ids, streamed from the cursor
|
|
2391
|
+
* (never all ids in memory at once).
|
|
2392
|
+
*
|
|
2393
|
+
* The ids come sorted by `_id`, so writes made while the cursor is open
|
|
2394
|
+
* can never feed back into it (otherwise a document an update moves within
|
|
2395
|
+
* the index the cursor scans could be returned — and written — twice):
|
|
2396
|
+
* after the `$lookup`s that `$sort` consumes every match before the first
|
|
2397
|
+
* batch returns, and an `_id` order read from the `_id` index is stable
|
|
2398
|
+
* (`_id` never changes).
|
|
2399
|
+
*/
|
|
2400
|
+
async _forEachMatching(filter, write) {
|
|
2401
|
+
if (!containsRelationFilter(filter)) return write(buildMongoFilter(filter));
|
|
2402
|
+
const { pipeline, pre } = this._predicateWritePlan(filter);
|
|
2403
|
+
pipeline.push({ $project: { _id: 1 } }, { $sort: { _id: 1 } });
|
|
2404
|
+
this._log("aggregate (write ids)", pipeline);
|
|
2405
|
+
const cursor = this.aggregatePipeline(pipeline, { allowDiskUse: true }).batchSize(REL_WRITE_BATCH);
|
|
2406
|
+
try {
|
|
2407
|
+
const next = () => wrapInvalidQuery(() => cursor.next());
|
|
2408
|
+
let ids = [];
|
|
2409
|
+
for (let doc = await next(); doc; doc = await next()) {
|
|
2410
|
+
ids.push(doc._id);
|
|
2411
|
+
if (ids.length === REL_WRITE_BATCH) {
|
|
2412
|
+
await write(MongoAdapter._byIds(ids, pre));
|
|
2413
|
+
ids = [];
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
if (ids.length > 0) await write(MongoAdapter._byIds(ids, pre));
|
|
2417
|
+
} finally {
|
|
2418
|
+
await cursor.close();
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
/** Runs an update-shaped write over `filter` ({@link _forEachMatching}), counts summed. */
|
|
2422
|
+
async _updateMatching(filter, write) {
|
|
2423
|
+
const total = {
|
|
2424
|
+
matchedCount: 0,
|
|
2425
|
+
modifiedCount: 0
|
|
2426
|
+
};
|
|
2427
|
+
await this._forEachMatching(filter, async (mongoFilter) => {
|
|
2428
|
+
const result = await write(mongoFilter);
|
|
2429
|
+
total.matchedCount += result.matchedCount;
|
|
2430
|
+
total.modifiedCount += result.modifiedCount;
|
|
2431
|
+
});
|
|
2432
|
+
return total;
|
|
2433
|
+
}
|
|
2434
|
+
/** The filter of a single-document write — its predicates resolved to one `_id`. */
|
|
2435
|
+
_writeFilterOne(filter) {
|
|
2436
|
+
return containsRelationFilter(filter) ? this._predicateWriteFilterOne(filter) : buildMongoFilter(filter);
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2179
2439
|
* Wraps an async operation to catch MongoDB duplicate key errors
|
|
2180
2440
|
* (code 11000) and rethrow as structured `DbError` — every write path
|
|
2181
2441
|
* (insert, replace, update, patch, their `*Many` variants) goes through it,
|
|
@@ -2185,13 +2445,17 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2185
2445
|
try {
|
|
2186
2446
|
return await fn();
|
|
2187
2447
|
} catch (error) {
|
|
2188
|
-
|
|
2189
|
-
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
2190
|
-
message: error.message
|
|
2191
|
-
}]);
|
|
2192
|
-
throw error;
|
|
2448
|
+
return this._mapConstraintError(error);
|
|
2193
2449
|
}
|
|
2194
2450
|
}
|
|
2451
|
+
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
2452
|
+
_mapConstraintError(error) {
|
|
2453
|
+
if (error instanceof MongoServerError && error.code === 11e3) throw new DbError("CONFLICT", [{
|
|
2454
|
+
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
2455
|
+
message: error.message
|
|
2456
|
+
}]);
|
|
2457
|
+
throw error;
|
|
2458
|
+
}
|
|
2195
2459
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
2196
2460
|
async _wrapUpdate(fn) {
|
|
2197
2461
|
const { matchedCount, modifiedCount } = await this._wrapDuplicateKeyError(fn);
|
|
@@ -2215,6 +2479,98 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2215
2479
|
return { insertedId: objectIdFromStorage(this._resolveInsertedId(data, result.insertedId)) };
|
|
2216
2480
|
}
|
|
2217
2481
|
async insertMany(data) {
|
|
2482
|
+
await this._prepareInsertBatch(data);
|
|
2483
|
+
this._log("insertMany", `${data.length} docs`);
|
|
2484
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
2485
|
+
return {
|
|
2486
|
+
insertedCount: result.insertedCount,
|
|
2487
|
+
insertedIds: data.map((item, i) => objectIdFromStorage(this._resolveInsertedId(item, result.insertedIds[i])))
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
supportsInsertIgnore() {
|
|
2491
|
+
return true;
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Conflict-ignoring batch insert (the core already removed in-batch duplicates).
|
|
2495
|
+
*
|
|
2496
|
+
* Outside a transaction: `insertMany(…, { ordered: false })`; duplicate-key
|
|
2497
|
+
* write errors (11000) mark the skipped rows, every other write error
|
|
2498
|
+
* rethrows after the batch (rows already written stay — as for a plain
|
|
2499
|
+
* non-transactional `insertMany`).
|
|
2500
|
+
*
|
|
2501
|
+
* Inside a transaction a duplicate key would abort the transaction even with
|
|
2502
|
+
* `ordered: false`, so the stored keys are looked up first (inside the
|
|
2503
|
+
* session), the matches are skipped and the rest is inserted ordered. A
|
|
2504
|
+
* residual 11000 (e.g. a collation-equal value) throws `CONFLICT`.
|
|
2505
|
+
*/
|
|
2506
|
+
async insertManyIgnore(data) {
|
|
2507
|
+
if (data.length === 0) return [];
|
|
2508
|
+
await this._prepareInsertBatch(data);
|
|
2509
|
+
this._log("insertManyIgnore", `${data.length} docs`);
|
|
2510
|
+
const idOf = (item) => objectIdFromStorage(this._resolveInsertedId(item, item._id));
|
|
2511
|
+
if (this._getTransactionState()) {
|
|
2512
|
+
const skipped = await this._findStoredKeyConflicts(data);
|
|
2513
|
+
const survivors = data.filter((_, i) => !skipped.has(i));
|
|
2514
|
+
if (survivors.length > 0) await this._wrapDuplicateKeyError(() => this.collection.insertMany(survivors, {
|
|
2515
|
+
ordered: true,
|
|
2516
|
+
...this._getSessionOpts()
|
|
2517
|
+
}));
|
|
2518
|
+
return data.map((item, i) => skipped.has(i) ? null : { insertedId: idOf(item) });
|
|
2519
|
+
}
|
|
2520
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
2521
|
+
try {
|
|
2522
|
+
await this.collection.insertMany(data, {
|
|
2523
|
+
ordered: false,
|
|
2524
|
+
...this._getSessionOpts()
|
|
2525
|
+
});
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
if (!(error instanceof MongoBulkWriteError)) this._mapConstraintError(error);
|
|
2528
|
+
const writeErrors = [error.writeErrors].flat().filter(Boolean);
|
|
2529
|
+
if (writeErrors.length === 0 || hasWriteConcernError(error)) throw error;
|
|
2530
|
+
for (const writeError of writeErrors) {
|
|
2531
|
+
if (writeError.code !== 11e3) throw error;
|
|
2532
|
+
skipped.add(writeError.index);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return data.map((item, i) => skipped.has(i) ? null : { insertedId: idOf(item) });
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Indices of `data` rows whose primary (`_id`) or unique-index key tuple is
|
|
2539
|
+
* already stored (rows with a null / missing key component never collide).
|
|
2540
|
+
* One `$or` query per chunk covers every key set.
|
|
2541
|
+
*/
|
|
2542
|
+
async _findStoredKeyConflicts(data) {
|
|
2543
|
+
const keySets = [["_id"], ...this._table.uniqueKeySets.filter((f) => !(f.length === 1 && f[0] === "_id"))];
|
|
2544
|
+
const rowTuples = data.map((row) => keySets.map((fields) => uniqueKeyTuple(row, fields)));
|
|
2545
|
+
const projection = Object.fromEntries(keySets.flat().map((f) => [f, 1]));
|
|
2546
|
+
const stored = keySets.map(() => /* @__PURE__ */ new Set());
|
|
2547
|
+
const CHUNK = 1e3;
|
|
2548
|
+
for (let offset = 0; offset < data.length; offset += CHUNK) {
|
|
2549
|
+
const clauses = [];
|
|
2550
|
+
keySets.forEach((fields, k) => {
|
|
2551
|
+
const rows = data.slice(offset, offset + CHUNK).filter((_, i) => rowTuples[offset + i][k]);
|
|
2552
|
+
if (rows.length === 0) return;
|
|
2553
|
+
if (fields.length === 1) clauses.push({ [fields[0]]: { $in: rows.map((row) => getPath(row, fields[0])) } });
|
|
2554
|
+
else for (const row of rows) clauses.push(Object.fromEntries(fields.map((f) => [f, getPath(row, f)])));
|
|
2555
|
+
});
|
|
2556
|
+
if (clauses.length === 0) continue;
|
|
2557
|
+
const found = await this.collection.find({ $or: clauses }, {
|
|
2558
|
+
projection,
|
|
2559
|
+
...this._getSessionOpts()
|
|
2560
|
+
}).toArray();
|
|
2561
|
+
for (const doc of found) keySets.forEach((fields, k) => {
|
|
2562
|
+
const tuple = uniqueKeyTuple(doc, fields);
|
|
2563
|
+
if (tuple !== void 0) stored[k].add(tuple);
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
2567
|
+
rowTuples.forEach((tuples, i) => {
|
|
2568
|
+
if (tuples.some((t, k) => t !== void 0 && stored[k].has(t))) skipped.add(i);
|
|
2569
|
+
});
|
|
2570
|
+
return skipped;
|
|
2571
|
+
}
|
|
2572
|
+
/** Version default + `@db.default.increment` allocation shared by every batch insert. */
|
|
2573
|
+
async _prepareInsertBatch(data) {
|
|
2218
2574
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2219
2575
|
if (versionColumn !== void 0) {
|
|
2220
2576
|
for (const item of data) if (!(versionColumn in item)) item[versionColumn] = 0;
|
|
@@ -2224,14 +2580,12 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2224
2580
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
2225
2581
|
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
2226
2582
|
}
|
|
2227
|
-
this._log("insertMany", `${data.length} docs`);
|
|
2228
|
-
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
2229
|
-
return {
|
|
2230
|
-
insertedCount: result.insertedCount,
|
|
2231
|
-
insertedIds: data.map((item, i) => objectIdFromStorage(this._resolveInsertedId(item, result.insertedIds[i])))
|
|
2232
|
-
};
|
|
2233
2583
|
}
|
|
2234
2584
|
async findOne(query) {
|
|
2585
|
+
if (containsRelationFilter(query.filter)) {
|
|
2586
|
+
const [row] = await this._aggregateFind(query, "findOne", 1);
|
|
2587
|
+
return row ?? null;
|
|
2588
|
+
}
|
|
2235
2589
|
const filter = buildMongoFilter(query.filter);
|
|
2236
2590
|
const opts = this._buildFindOptions(query.controls);
|
|
2237
2591
|
this._log("findOne", filter, opts);
|
|
@@ -2242,6 +2596,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2242
2596
|
}));
|
|
2243
2597
|
}
|
|
2244
2598
|
async findMany(query) {
|
|
2599
|
+
if (containsRelationFilter(query.filter)) return this._aggregateFind(query, "findMany");
|
|
2245
2600
|
const filter = buildMongoFilter(query.filter);
|
|
2246
2601
|
const opts = this._buildFindOptions(query.controls);
|
|
2247
2602
|
this._log("findMany", filter, opts);
|
|
@@ -2252,6 +2607,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2252
2607
|
}).toArray());
|
|
2253
2608
|
}
|
|
2254
2609
|
async count(query) {
|
|
2610
|
+
if (containsRelationFilter(query.filter)) {
|
|
2611
|
+
const pipeline = [...mongoFilterStages(query.filter, this._predicateFilterOpts), { $count: "count" }];
|
|
2612
|
+
this._log("aggregate (count)", pipeline);
|
|
2613
|
+
return (await wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray()))[0]?.count ?? 0;
|
|
2614
|
+
}
|
|
2255
2615
|
const filter = buildMongoFilter(query.filter);
|
|
2256
2616
|
this._log("countDocuments", filter);
|
|
2257
2617
|
return this.collection.countDocuments(filter, {
|
|
@@ -2260,13 +2620,21 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2260
2620
|
});
|
|
2261
2621
|
}
|
|
2262
2622
|
async updateOne(filter, data, ops, expectedVersion) {
|
|
2263
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "updateOne");
|
|
2623
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "updateOne");
|
|
2624
|
+
if (!mongoFilter) return {
|
|
2625
|
+
matchedCount: 0,
|
|
2626
|
+
modifiedCount: 0
|
|
2627
|
+
};
|
|
2264
2628
|
const updateDoc = buildMongoUpdateDoc(data, ops, this._table.versionColumnPhysical);
|
|
2265
2629
|
this._log("updateOne", mongoFilter, updateDoc);
|
|
2266
2630
|
return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2267
2631
|
}
|
|
2268
2632
|
async replaceOne(filter, data, expectedVersion) {
|
|
2269
|
-
const mongoFilter = this._buildCasFilter(filter, expectedVersion, "replaceOne");
|
|
2633
|
+
const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "replaceOne");
|
|
2634
|
+
if (!mongoFilter) return {
|
|
2635
|
+
matchedCount: 0,
|
|
2636
|
+
modifiedCount: 0
|
|
2637
|
+
};
|
|
2270
2638
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2271
2639
|
this._log("replaceOne", mongoFilter, data);
|
|
2272
2640
|
if (versionColumn !== void 0) {
|
|
@@ -2279,27 +2647,34 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2279
2647
|
return this._wrapUpdate(() => this.collection.replaceOne(mongoFilter, data, this._getSessionOpts()));
|
|
2280
2648
|
}
|
|
2281
2649
|
async deleteOne(filter) {
|
|
2282
|
-
const mongoFilter =
|
|
2650
|
+
const mongoFilter = await this._writeFilterOne(filter);
|
|
2651
|
+
if (!mongoFilter) return { deletedCount: 0 };
|
|
2283
2652
|
this._log("deleteOne", mongoFilter);
|
|
2284
2653
|
return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
2285
2654
|
}
|
|
2286
2655
|
async updateMany(filter, data, ops) {
|
|
2287
2656
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2288
|
-
const mongoFilter = buildMongoFilter(filter);
|
|
2289
2657
|
const updateDoc = buildMongoUpdateDoc(data, ops, versionColumn);
|
|
2290
|
-
this.
|
|
2291
|
-
|
|
2658
|
+
return this._updateMatching(filter, (mongoFilter) => {
|
|
2659
|
+
this._log("updateMany", mongoFilter, updateDoc);
|
|
2660
|
+
return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2661
|
+
});
|
|
2292
2662
|
}
|
|
2293
2663
|
async replaceMany(filter, data) {
|
|
2294
|
-
const mongoFilter = buildMongoFilter(filter);
|
|
2295
2664
|
const updateDoc = buildMongoUpdateDoc(data, void 0, this._table.versionColumnPhysical);
|
|
2296
|
-
this.
|
|
2297
|
-
|
|
2665
|
+
return this._updateMatching(filter, (mongoFilter) => {
|
|
2666
|
+
this._log("replaceMany", mongoFilter, updateDoc);
|
|
2667
|
+
return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
|
|
2668
|
+
});
|
|
2298
2669
|
}
|
|
2299
2670
|
async deleteMany(filter) {
|
|
2300
|
-
|
|
2301
|
-
this.
|
|
2302
|
-
|
|
2671
|
+
let deletedCount = 0;
|
|
2672
|
+
await this._forEachMatching(filter, async (mongoFilter) => {
|
|
2673
|
+
this._log("deleteMany", mongoFilter);
|
|
2674
|
+
const result = await this.collection.deleteMany(mongoFilter, this._getSessionOpts());
|
|
2675
|
+
deletedCount += result.deletedCount;
|
|
2676
|
+
});
|
|
2677
|
+
return { deletedCount };
|
|
2303
2678
|
}
|
|
2304
2679
|
clearCollectionCache() {
|
|
2305
2680
|
this._collection = void 0;
|
|
@@ -2426,6 +2801,15 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2426
2801
|
fieldCounters.set(physical, next + 1);
|
|
2427
2802
|
}
|
|
2428
2803
|
}
|
|
2804
|
+
/** A find whose filter holds relational predicates, as an aggregation pipeline. */
|
|
2805
|
+
async _aggregateFind(query, label, limit) {
|
|
2806
|
+
const pipeline = [...mongoFilterStages(query.filter, this._predicateFilterOpts), ...pageStages(limit ? {
|
|
2807
|
+
...query.controls,
|
|
2808
|
+
$limit: limit
|
|
2809
|
+
} : query.controls)];
|
|
2810
|
+
this._log(`aggregate (${label})`, pipeline);
|
|
2811
|
+
return wrapInvalidQuery(() => this.collection.aggregate(pipeline, this._readOpts(query)).toArray());
|
|
2812
|
+
}
|
|
2429
2813
|
_buildFindOptions(controls) {
|
|
2430
2814
|
const opts = {};
|
|
2431
2815
|
if (!controls) return opts;
|
|
@@ -2442,6 +2826,9 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2442
2826
|
* Returns MongoDB collation options if any filter field has a non-binary collation.
|
|
2443
2827
|
* Uses pre-computed insights when available, falls back to computing them on demand.
|
|
2444
2828
|
* Maps: nocase → strength 2 (case-insensitive), unicode → strength 1 (case+accent-insensitive).
|
|
2829
|
+
* Predicate-free filters only — a filter with relational predicates never gets an
|
|
2830
|
+
* operation-wide collation ({@link _readOpts}), so insights are never computed over
|
|
2831
|
+
* resolved predicate operands.
|
|
2445
2832
|
*/
|
|
2446
2833
|
_getCollationOpts(query) {
|
|
2447
2834
|
if (!this._collateFields) return;
|
|
@@ -2544,6 +2931,19 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2544
2931
|
return node.fields;
|
|
2545
2932
|
}
|
|
2546
2933
|
};
|
|
2934
|
+
/** `$sort` → `$skip` → `$limit` → `$project` stages of a read's controls. */
|
|
2935
|
+
function pageStages(controls) {
|
|
2936
|
+
const stages = [];
|
|
2937
|
+
if (!controls) return stages;
|
|
2938
|
+
if (controls.$sort) stages.push({ $sort: controls.$sort });
|
|
2939
|
+
if (controls.$skip) stages.push({ $skip: controls.$skip });
|
|
2940
|
+
if (controls.$limit) stages.push({ $limit: controls.$limit });
|
|
2941
|
+
if (controls.$select) {
|
|
2942
|
+
const projection = controls.$select.asProjection;
|
|
2943
|
+
if (projection) stages.push({ $project: dedupeProjection(projection) });
|
|
2944
|
+
}
|
|
2945
|
+
return stages;
|
|
2946
|
+
}
|
|
2547
2947
|
/**
|
|
2548
2948
|
* Normalizes a declared `fuzzy` arg (`0-2`) into the query-time metadata Atlas
|
|
2549
2949
|
* accepts. Atlas only honors an edit distance of `1` or `2`; `0`/undefined means
|
|
@@ -2603,7 +3003,7 @@ function buildMongoUpdateDoc(data, ops, versionColumn) {
|
|
|
2603
3003
|
function createAdapter(connection, _options) {
|
|
2604
3004
|
const client = new MongoClient(connection);
|
|
2605
3005
|
const db = client.db();
|
|
2606
|
-
return new DbSpace(() => new MongoAdapter(db, client));
|
|
3006
|
+
return new DbSpace(() => new MongoAdapter(db, client), { onClose: () => client.close() });
|
|
2607
3007
|
}
|
|
2608
3008
|
//#endregion
|
|
2609
|
-
export { CollectionPatcher, INDEX_PREFIX, MongoAdapter, buildMongoFilter, createAdapter, mongoIndexKey, validateMongoIdPlugin };
|
|
3009
|
+
export { CollectionPatcher, INDEX_PREFIX, MongoAdapter, REL_FILTER_TEMP_PREFIX, buildMongoFilter, buildMongoQuery, collationOfAdapter, createAdapter, mongoFilterStages, mongoIndexKey, planStages, validateMongoIdPlugin };
|