@atscript/db-mongo 0.1.135 → 0.1.137
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 +7 -23
- package/dist/agg.mjs +6 -22
- package/dist/index.cjs +259 -191
- package/dist/index.d.cts +16 -2
- package/dist/index.d.mts +16 -2
- package/dist/index.mjs +246 -178
- package/dist/mongo-accumulator-BalE9LkU.cjs +265 -0
- package/dist/mongo-accumulator-Dc0w4t0l.mjs +236 -0
- package/package.json +2 -2
- package/dist/mongo-filter-Bf5rWV4P.cjs +0 -73
- package/dist/mongo-filter-CtyITmZM.mjs +0 -68
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_mongo_accumulator = require("./mongo-accumulator-BalE9LkU.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");
|
|
@@ -498,7 +498,7 @@ function buildFromLookup(host, withRel, relation, tableResolver) {
|
|
|
498
498
|
if (!targetMeta) return;
|
|
499
499
|
const remoteFK = findRemoteFK(targetMeta, host._table.tableName, relation.alias);
|
|
500
500
|
if (!remoteFK) return;
|
|
501
|
-
const targetTableName =
|
|
501
|
+
const targetTableName = (0, _atscript_db.tableNameOf)(relation.targetType());
|
|
502
502
|
const innerPipeline = buildLookupInnerPipeline(withRel, remoteFK.fields);
|
|
503
503
|
const { letVars, exprMatch } = buildLookupJoin(remoteFK.targetFields, remoteFK.fields, "pk_");
|
|
504
504
|
const stages = [{ $lookup: {
|
|
@@ -523,8 +523,8 @@ function buildViaLookup(host, withRel, relation, tableResolver) {
|
|
|
523
523
|
if (!junctionType) return;
|
|
524
524
|
const junctionMeta = tableResolver(junctionType);
|
|
525
525
|
if (!junctionMeta) return;
|
|
526
|
-
const junctionTableName =
|
|
527
|
-
const targetTableName =
|
|
526
|
+
const junctionTableName = (0, _atscript_db.tableNameOf)(junctionType);
|
|
527
|
+
const targetTableName = (0, _atscript_db.tableNameOf)(relation.targetType());
|
|
528
528
|
const fkToThis = findRemoteFK(junctionMeta, host._table.tableName);
|
|
529
529
|
if (!fkToThis) return;
|
|
530
530
|
const fkToTarget = findRemoteFK(junctionMeta, targetTableName);
|
|
@@ -565,7 +565,7 @@ function buildLookupInnerPipeline(withRel, requiredFields) {
|
|
|
565
565
|
const limit = nested.$limit ?? flatRel.$limit;
|
|
566
566
|
const skip = nested.$skip ?? flatRel.$skip;
|
|
567
567
|
const select = nested.$select || flatRel.$select;
|
|
568
|
-
if (filter && Object.keys(filter).length > 0) pipeline.push({ $match:
|
|
568
|
+
if (filter && Object.keys(filter).length > 0) pipeline.push({ $match: require_mongo_accumulator.buildMongoFilter(filter) });
|
|
569
569
|
if (sort) pipeline.push({ $sort: sort });
|
|
570
570
|
if (skip) pipeline.push({ $skip: skip });
|
|
571
571
|
if (limit !== null && limit !== void 0) pipeline.push({ $limit: limit });
|
|
@@ -620,7 +620,7 @@ function mergeRelationResults(rows, results, primaryKeys, relMeta) {
|
|
|
620
620
|
}
|
|
621
621
|
/** Finds FK entry for a TO relation from this table's foreignKeys map. */
|
|
622
622
|
function findFKForRelation(relation, foreignKeys) {
|
|
623
|
-
const targetTableName =
|
|
623
|
+
const targetTableName = (0, _atscript_db.tableNameOf)(relation.targetType());
|
|
624
624
|
for (const fk of foreignKeys.values()) if (relation.alias) {
|
|
625
625
|
if (fk.alias === relation.alias) return {
|
|
626
626
|
localFields: fk.fields,
|
|
@@ -640,11 +640,6 @@ function findRemoteFK(target, thisTableName, alias) {
|
|
|
640
640
|
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
641
641
|
}
|
|
642
642
|
}
|
|
643
|
-
/** Resolves the target table/collection name from a relation's target type. */
|
|
644
|
-
function resolveRelTargetTableName(relation) {
|
|
645
|
-
const targetType = relation.targetType();
|
|
646
|
-
return targetType?.metadata?.get("db.table") || targetType?.id || "";
|
|
647
|
-
}
|
|
648
643
|
//#endregion
|
|
649
644
|
//#region src/lib/mongo-search.ts
|
|
650
645
|
/** Returns available search indexes as generic metadata for UI. */
|
|
@@ -752,7 +747,7 @@ function buildGeoNearStage(host, point, query, indexName) {
|
|
|
752
747
|
distanceField: DISTANCE_FIELD,
|
|
753
748
|
spherical: true,
|
|
754
749
|
key: resolveGeoKeyPath(host, indexName),
|
|
755
|
-
query:
|
|
750
|
+
query: require_mongo_accumulator.buildMongoFilter(query.filter)
|
|
756
751
|
};
|
|
757
752
|
if (typeof controls.$maxDistance === "number") geoNear.maxDistance = controls.$maxDistance;
|
|
758
753
|
if (typeof controls.$minDistance === "number") geoNear.minDistance = controls.$minDistance;
|
|
@@ -965,7 +960,7 @@ function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
|
965
960
|
}
|
|
966
961
|
/** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
|
|
967
962
|
async function runSearchPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
968
|
-
const filter =
|
|
963
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
969
964
|
const controls = query.controls || {};
|
|
970
965
|
const pipeline = [stage];
|
|
971
966
|
if (threshold !== void 0) {
|
|
@@ -987,7 +982,7 @@ async function runSearchPipeline(host, stage, query, label, threshold, classicTe
|
|
|
987
982
|
}
|
|
988
983
|
/** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
|
|
989
984
|
async function runSearchWithCountPipeline(host, stage, query, label, threshold, classicText = false) {
|
|
990
|
-
const filter =
|
|
985
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
991
986
|
const controls = query.controls || {};
|
|
992
987
|
const preStages = [];
|
|
993
988
|
if (threshold !== void 0) {
|
|
@@ -1020,6 +1015,175 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold,
|
|
|
1020
1015
|
};
|
|
1021
1016
|
}
|
|
1022
1017
|
//#endregion
|
|
1018
|
+
//#region src/lib/mongo-view-pipeline.ts
|
|
1019
|
+
/** Where a joined table's document lands (`$lookup.as`, then unwound). */
|
|
1020
|
+
function joinedAs(table) {
|
|
1021
|
+
return `${JOINED_PREFIX}${table}`;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Builds the aggregation pipeline of a managed MongoDB view (created with
|
|
1025
|
+
* `viewOn: <entry collection>`):
|
|
1026
|
+
*
|
|
1027
|
+
* 1. per join, in declaration order: `$lookup` + `$unwind` (inner joins drop
|
|
1028
|
+
* unmatched documents, left joins keep them — `preserveNullAndEmptyArrays`);
|
|
1029
|
+
* 2. `@db.view.filter` → `$match`;
|
|
1030
|
+
* 3. aggregates → `$group` (+ `$addFields` turning `countDistinct` sets into
|
|
1031
|
+
* their sizes, + `@db.view.having` → `$match`) + `$project`, otherwise a
|
|
1032
|
+
* flat `$project`. A conditional aggregate's predicate reads the same
|
|
1033
|
+
* pipeline paths (SQL null semantics — `queryNodeToExpr`).
|
|
1034
|
+
*
|
|
1035
|
+
* All field paths are PHYSICAL document paths (`@db.column` renames), joined
|
|
1036
|
+
* tables live under `__joined_<table>`. A column whose source may be missing
|
|
1037
|
+
* (`nullable` mapping) reads as null; every other column is a plain path, so
|
|
1038
|
+
* a `$match` / `$sort` on the view can still push down to an index.
|
|
1039
|
+
* @since 0.1.136
|
|
1040
|
+
*/
|
|
1041
|
+
function buildViewPipeline(view) {
|
|
1042
|
+
const plan = view.viewPlan;
|
|
1043
|
+
const columns = view.getViewColumnMappings();
|
|
1044
|
+
const pipeline = [];
|
|
1045
|
+
const prefixes = new Map([[plan.entryTable, ""]]);
|
|
1046
|
+
const pathOf = (ref) => {
|
|
1047
|
+
const { table, source } = view.resolveRefSource(ref);
|
|
1048
|
+
const prefix = prefixes.get(table);
|
|
1049
|
+
if (prefix === void 0) throw new Error(`View "${view.tableName}": "${table}" is not joined before it is referenced`);
|
|
1050
|
+
return prefix + source.column;
|
|
1051
|
+
};
|
|
1052
|
+
for (const join of plan.joins) {
|
|
1053
|
+
pipeline.push(buildLookup(view, join, prefixes));
|
|
1054
|
+
pipeline.push({ $unwind: {
|
|
1055
|
+
path: `$${joinedAs(join.targetTable)}`,
|
|
1056
|
+
preserveNullAndEmptyArrays: join.kind === "left"
|
|
1057
|
+
} });
|
|
1058
|
+
prefixes.set(join.targetTable, `${joinedAs(join.targetTable)}.`);
|
|
1059
|
+
}
|
|
1060
|
+
if (plan.filter) pipeline.push({ $match: viewMatch(plan.filter, pathOf) });
|
|
1061
|
+
/** A column's source field as an aggregation operand. */
|
|
1062
|
+
const colSourceField = (col) => `$${prefixes.get(col.sourceTable) ?? ""}${col.sourceColumn}`;
|
|
1063
|
+
const colValue = (col) => col.nullable ? require_mongo_accumulator.orNull(colSourceField(col)) : colSourceField(col);
|
|
1064
|
+
if (columns.some((c) => c.aggFn)) {
|
|
1065
|
+
const group = { _id: {} };
|
|
1066
|
+
const project = { _id: 0 };
|
|
1067
|
+
const distinctSizes = {};
|
|
1068
|
+
for (const col of columns) if (col.aggFn) {
|
|
1069
|
+
const predicate = col.aggFilter ? require_mongo_accumulator.queryNodeToExpr(col.aggFilter, (ref) => `$${pathOf(ref)}`) : void 0;
|
|
1070
|
+
group[col.viewColumn] = require_mongo_accumulator.buildAccumulator(col.aggFn, col.aggField === "*" ? "*" : colSourceField(col), predicate, col.viewColumn);
|
|
1071
|
+
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1072
|
+
if (col.aggFn === "countDistinct") distinctSizes[col.viewColumn] = require_mongo_accumulator.distinctCountExpr(`$${col.viewColumn}`);
|
|
1073
|
+
} else {
|
|
1074
|
+
group._id[col.viewColumn] = colValue(col);
|
|
1075
|
+
project[col.viewColumn] = `$_id.${col.viewColumn}`;
|
|
1076
|
+
}
|
|
1077
|
+
pipeline.push({ $group: group });
|
|
1078
|
+
if (Object.keys(distinctSizes).length > 0) pipeline.push({ $addFields: distinctSizes });
|
|
1079
|
+
if (plan.having) pipeline.push({ $match: havingMatch(plan.having, columns) });
|
|
1080
|
+
pipeline.push({ $project: project });
|
|
1081
|
+
} else {
|
|
1082
|
+
const project = { _id: 0 };
|
|
1083
|
+
for (const col of columns) project[col.viewColumn] = colValue(col);
|
|
1084
|
+
pipeline.push({ $project: project });
|
|
1085
|
+
}
|
|
1086
|
+
return pipeline;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* One join's `$lookup`.
|
|
1090
|
+
*
|
|
1091
|
+
* The simple `localField` / `foreignField` form is used whenever it is exact:
|
|
1092
|
+
* a single `=` between a field of the joined table and a field of an outer
|
|
1093
|
+
* table (the entry or an earlier join), with the joined field REQUIRED — the
|
|
1094
|
+
* simple form matches a null / missing local value to null / missing foreign
|
|
1095
|
+
* values, which a required foreign field can never hold. It is kept (rather
|
|
1096
|
+
* than always using the pipeline form) because it can use an index on the
|
|
1097
|
+
* foreign field on every MongoDB version; the pipeline form's `$expr` match
|
|
1098
|
+
* can't before MongoDB 5.0. Everything else uses the pipeline form (`let` +
|
|
1099
|
+
* `$match: { $expr }`, SQL null semantics).
|
|
1100
|
+
*/
|
|
1101
|
+
function buildLookup(view, join, outer) {
|
|
1102
|
+
const as = joinedAs(join.targetTable);
|
|
1103
|
+
const simple = simpleJoinFields(view, join, outer);
|
|
1104
|
+
if (simple) return { $lookup: {
|
|
1105
|
+
from: join.targetTable,
|
|
1106
|
+
...simple,
|
|
1107
|
+
as
|
|
1108
|
+
} };
|
|
1109
|
+
const letVars = {};
|
|
1110
|
+
const varByPath = /* @__PURE__ */ new Map();
|
|
1111
|
+
const pathOf = (ref) => {
|
|
1112
|
+
const { table, source } = view.resolveRefSource(ref);
|
|
1113
|
+
if (table === join.targetTable) return `$${source.column}`;
|
|
1114
|
+
const prefix = outer.get(table);
|
|
1115
|
+
if (prefix === void 0) throw new Error(`View "${view.tableName}": the join on "${join.targetTable}" references "${table}", which is not joined before it`);
|
|
1116
|
+
const outerPath = prefix + source.column;
|
|
1117
|
+
let varName = varByPath.get(outerPath);
|
|
1118
|
+
if (!varName) {
|
|
1119
|
+
varName = `v${varByPath.size}`;
|
|
1120
|
+
varByPath.set(outerPath, varName);
|
|
1121
|
+
letVars[varName] = `$${outerPath}`;
|
|
1122
|
+
}
|
|
1123
|
+
return `$$${varName}`;
|
|
1124
|
+
};
|
|
1125
|
+
const expr = require_mongo_accumulator.queryNodeToExpr(join.condition, pathOf);
|
|
1126
|
+
return { $lookup: {
|
|
1127
|
+
from: join.targetTable,
|
|
1128
|
+
...varByPath.size > 0 ? { let: letVars } : {},
|
|
1129
|
+
pipeline: [{ $match: { $expr: expr } }],
|
|
1130
|
+
as
|
|
1131
|
+
} };
|
|
1132
|
+
}
|
|
1133
|
+
/** `{ localField, foreignField }` when the simple `$lookup` form is exact, else `undefined`. */
|
|
1134
|
+
function simpleJoinFields(view, join, outer) {
|
|
1135
|
+
let node = join.condition;
|
|
1136
|
+
if ("$and" in node) {
|
|
1137
|
+
const children = node.$and;
|
|
1138
|
+
if (children.length !== 1) return void 0;
|
|
1139
|
+
node = children[0];
|
|
1140
|
+
}
|
|
1141
|
+
if ("$or" in node || "$not" in node || "$and" in node) return void 0;
|
|
1142
|
+
const comp = node;
|
|
1143
|
+
if (comp.op !== "$eq" || !(0, _atscript_db.isFieldRef)(comp.right)) return void 0;
|
|
1144
|
+
const left = view.resolveRefSource(comp.left);
|
|
1145
|
+
const right = view.resolveRefSource(comp.right);
|
|
1146
|
+
let target;
|
|
1147
|
+
let local;
|
|
1148
|
+
if (left.table === join.targetTable && right.table !== join.targetTable) {
|
|
1149
|
+
target = left;
|
|
1150
|
+
local = right;
|
|
1151
|
+
} else if (right.table === join.targetTable && left.table !== join.targetTable) {
|
|
1152
|
+
target = right;
|
|
1153
|
+
local = left;
|
|
1154
|
+
} else return;
|
|
1155
|
+
const localPrefix = outer.get(local.table);
|
|
1156
|
+
if (localPrefix === void 0) return void 0;
|
|
1157
|
+
const foreign = target.source;
|
|
1158
|
+
if (!(!foreign.optional && foreign.designType !== "union" && foreign.designType !== "unknown")) return void 0;
|
|
1159
|
+
return {
|
|
1160
|
+
localField: localPrefix + local.source.column,
|
|
1161
|
+
foreignField: foreign.column
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* A view predicate (`@db.view.filter`, `@db.view.having`) as a `$match`
|
|
1166
|
+
* over pipeline paths: the shared query translation (`translateQueryTree` →
|
|
1167
|
+
* `buildMongoFilter`), so operators mean what they mean in queries (`exists`
|
|
1168
|
+
* = holds a value, `matches` takes `/re/flags`); a field-to-field comparison
|
|
1169
|
+
* becomes `$expr`.
|
|
1170
|
+
*/
|
|
1171
|
+
function viewMatch(node, pathOf) {
|
|
1172
|
+
return require_mongo_accumulator.buildMongoFilter((0, _atscript_db.translateQueryTree)(node, pathOf), { fieldOperands: true });
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* `@db.view.having` as a `$match` after `$group`: refs name view fields —
|
|
1176
|
+
* aggregates are top-level, dimensions under `_id`.
|
|
1177
|
+
*/
|
|
1178
|
+
function havingMatch(node, columns) {
|
|
1179
|
+
const colMap = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1180
|
+
return viewMatch(node, (ref) => {
|
|
1181
|
+
const col = ref.type ? void 0 : colMap.get(ref.field);
|
|
1182
|
+
if (!col) return ref.field;
|
|
1183
|
+
return col.aggFn ? col.viewColumn : `_id.${col.viewColumn}`;
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
//#endregion
|
|
1023
1187
|
//#region src/lib/mongo-schema-sync.ts
|
|
1024
1188
|
const DESTRUCTIVE_OPTION_KEYS = new Set([
|
|
1025
1189
|
"capped",
|
|
@@ -1080,134 +1244,17 @@ async function getObjectKindImpl(host, name) {
|
|
|
1080
1244
|
if (type === void 0) return;
|
|
1081
1245
|
return type === "view" ? "view" : "table";
|
|
1082
1246
|
}
|
|
1083
|
-
/** Creates a MongoDB view from the AtscriptDbView's view plan. */
|
|
1247
|
+
/** Creates a MongoDB view from the AtscriptDbView's view plan (pipeline: {@link buildViewPipeline}). */
|
|
1084
1248
|
async function ensureView(host, view) {
|
|
1085
1249
|
if (await host.collectionExists()) return;
|
|
1086
1250
|
const plan = view.viewPlan;
|
|
1087
|
-
const
|
|
1088
|
-
const pipeline = [];
|
|
1089
|
-
for (const join of plan.joins) {
|
|
1090
|
-
const { localField, foreignField } = resolveJoinFields(join.condition, plan.entryTable, join.targetTable);
|
|
1091
|
-
pipeline.push({ $lookup: {
|
|
1092
|
-
from: join.targetTable,
|
|
1093
|
-
localField,
|
|
1094
|
-
foreignField,
|
|
1095
|
-
as: `${JOINED_PREFIX}${join.targetTable}`
|
|
1096
|
-
} });
|
|
1097
|
-
pipeline.push({ $unwind: {
|
|
1098
|
-
path: `$__joined_${join.targetTable}`,
|
|
1099
|
-
preserveNullAndEmptyArrays: true
|
|
1100
|
-
} });
|
|
1101
|
-
}
|
|
1102
|
-
if (plan.filter) {
|
|
1103
|
-
const matchExpr = queryNodeToMatch(plan.filter, plan.entryTable);
|
|
1104
|
-
pipeline.push({ $match: matchExpr });
|
|
1105
|
-
}
|
|
1106
|
-
const hasAggregates = columns.some((c) => c.aggFn);
|
|
1107
|
-
/** Resolves a column to its MongoDB source field path. */
|
|
1108
|
-
const colSourceField = (col) => col.sourceTable === plan.entryTable ? `$${col.sourceColumn}` : `$${JOINED_PREFIX}${col.sourceTable}.${col.sourceColumn}`;
|
|
1109
|
-
if (hasAggregates) {
|
|
1110
|
-
const group = { _id: {} };
|
|
1111
|
-
const project = { _id: 0 };
|
|
1112
|
-
for (const col of columns) if (col.aggFn) {
|
|
1113
|
-
if (col.aggFn === "count" && col.aggField === "*") group[col.viewColumn] = { $sum: 1 };
|
|
1114
|
-
else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
1115
|
-
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
1116
|
-
} else {
|
|
1117
|
-
group._id[col.viewColumn] = colSourceField(col);
|
|
1118
|
-
project[col.viewColumn] = `$_id.${col.viewColumn}`;
|
|
1119
|
-
}
|
|
1120
|
-
pipeline.push({ $group: group });
|
|
1121
|
-
if (plan.having) {
|
|
1122
|
-
const havingMatch = queryNodeToHaving(plan.having, columns);
|
|
1123
|
-
pipeline.push({ $match: havingMatch });
|
|
1124
|
-
}
|
|
1125
|
-
pipeline.push({ $project: project });
|
|
1126
|
-
} else {
|
|
1127
|
-
const project = { _id: 0 };
|
|
1128
|
-
for (const col of columns) project[col.viewColumn] = colSourceField(col);
|
|
1129
|
-
pipeline.push({ $project: project });
|
|
1130
|
-
}
|
|
1251
|
+
const pipeline = buildViewPipeline(view);
|
|
1131
1252
|
host._log("createView", host._table.tableName, plan.entryTable, pipeline);
|
|
1132
1253
|
await host.db.createCollection(host._table.tableName, {
|
|
1133
1254
|
viewOn: plan.entryTable,
|
|
1134
1255
|
pipeline
|
|
1135
1256
|
});
|
|
1136
1257
|
}
|
|
1137
|
-
/** Extracts localField/foreignField from a join condition. */
|
|
1138
|
-
function resolveJoinFields(condition, entryTable, joinTable) {
|
|
1139
|
-
const c = "$and" in condition ? condition.$and[0] : condition;
|
|
1140
|
-
if ((c.left.type ? c.left.type()?.metadata?.get("db.table") || "" : entryTable) === joinTable) return {
|
|
1141
|
-
localField: c.right.field,
|
|
1142
|
-
foreignField: c.left.field
|
|
1143
|
-
};
|
|
1144
|
-
return {
|
|
1145
|
-
localField: c.left.field,
|
|
1146
|
-
foreignField: c.right.field
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
/** Translates an AtscriptQueryNode to a MongoDB $match expression. */
|
|
1150
|
-
function queryNodeToMatch(node, entryTable) {
|
|
1151
|
-
if ("$and" in node) {
|
|
1152
|
-
const items = node.$and;
|
|
1153
|
-
const result = [];
|
|
1154
|
-
for (const item of items) result.push(queryNodeToMatch(item, entryTable));
|
|
1155
|
-
return { $and: result };
|
|
1156
|
-
}
|
|
1157
|
-
if ("$or" in node) {
|
|
1158
|
-
const items = node.$or;
|
|
1159
|
-
const result = [];
|
|
1160
|
-
for (const item of items) result.push(queryNodeToMatch(item, entryTable));
|
|
1161
|
-
return { $or: result };
|
|
1162
|
-
}
|
|
1163
|
-
if ("$not" in node) return { $not: queryNodeToMatch(node.$not, entryTable) };
|
|
1164
|
-
const comp = node;
|
|
1165
|
-
const fieldPath = resolveViewFieldPath(comp.left, entryTable);
|
|
1166
|
-
if (comp.right && typeof comp.right === "object" && "field" in comp.right) {
|
|
1167
|
-
const rightPath = resolveViewFieldPath(comp.right, entryTable);
|
|
1168
|
-
return { $expr: { [comp.op]: [`$${fieldPath}`, `$${rightPath}`] } };
|
|
1169
|
-
}
|
|
1170
|
-
if (comp.op === "$eq") return { [fieldPath]: comp.right };
|
|
1171
|
-
if (comp.op === "$ne") return { [fieldPath]: { $ne: comp.right } };
|
|
1172
|
-
return { [fieldPath]: { [comp.op]: comp.right } };
|
|
1173
|
-
}
|
|
1174
|
-
/** Resolves a field ref to a MongoDB dot path for view pipeline expressions. */
|
|
1175
|
-
function resolveViewFieldPath(ref, entryTable) {
|
|
1176
|
-
if (!ref.type) return ref.field;
|
|
1177
|
-
const table = ref.type()?.metadata?.get("db.table") || "";
|
|
1178
|
-
if (table === entryTable) return ref.field;
|
|
1179
|
-
return `${JOINED_PREFIX}${table}.${ref.field}`;
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Translates an AtscriptQueryNode to a MongoDB $match for use after $group (HAVING).
|
|
1183
|
-
* After $group, aggregate fields are top-level and dimension fields are under _id.
|
|
1184
|
-
*/
|
|
1185
|
-
function queryNodeToHaving(node, columns) {
|
|
1186
|
-
const colMap = new Map(columns.map((c) => [c.viewColumn, c]));
|
|
1187
|
-
const resolveHavingField = (ref) => {
|
|
1188
|
-
if (!ref.type) {
|
|
1189
|
-
const col = colMap.get(ref.field);
|
|
1190
|
-
if (col?.aggFn) return ref.field;
|
|
1191
|
-
if (col) return `_id.${ref.field}`;
|
|
1192
|
-
}
|
|
1193
|
-
return ref.field;
|
|
1194
|
-
};
|
|
1195
|
-
return queryNodeToHavingInner(node, resolveHavingField);
|
|
1196
|
-
}
|
|
1197
|
-
function queryNodeToHavingInner(node, resolveField) {
|
|
1198
|
-
if ("$and" in node) return { $and: node.$and.map((n) => queryNodeToHavingInner(n, resolveField)) };
|
|
1199
|
-
if ("$or" in node) return { $or: node.$or.map((n) => queryNodeToHavingInner(n, resolveField)) };
|
|
1200
|
-
if ("$not" in node) return { $not: queryNodeToHavingInner(node.$not, resolveField) };
|
|
1201
|
-
const comp = node;
|
|
1202
|
-
const fieldPath = resolveField(comp.left);
|
|
1203
|
-
if (comp.right && typeof comp.right === "object" && "field" in comp.right) {
|
|
1204
|
-
const rightPath = resolveField(comp.right);
|
|
1205
|
-
return { $expr: { [comp.op]: [`$${fieldPath}`, `$${rightPath}`] } };
|
|
1206
|
-
}
|
|
1207
|
-
if (comp.op === "$eq") return { [fieldPath]: comp.right };
|
|
1208
|
-
if (comp.op === "$ne") return { [fieldPath]: { $ne: comp.right } };
|
|
1209
|
-
return { [fieldPath]: { [comp.op]: comp.right } };
|
|
1210
|
-
}
|
|
1211
1258
|
async function dropTableImpl(host) {
|
|
1212
1259
|
host._log("drop", host._table.tableName);
|
|
1213
1260
|
await host.collection.drop();
|
|
@@ -1250,65 +1297,75 @@ async function renameTableImpl(host, oldName) {
|
|
|
1250
1297
|
async function syncColumnsImpl(host, diff) {
|
|
1251
1298
|
const renamed = [];
|
|
1252
1299
|
const added = [];
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
host._log("syncColumns: skipping $rename for array-element field", r.oldName, "→", r.field.physicalName, "(Mongo $rename cannot traverse arrays)");
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
|
-
renameSpec[r.oldName] = r.field.physicalName;
|
|
1262
|
-
renamed.push(r.field.physicalName);
|
|
1263
|
-
}
|
|
1264
|
-
if (Object.keys(renameSpec).length > 0) update.$rename = renameSpec;
|
|
1265
|
-
}
|
|
1266
|
-
if (diff.added.length > 0) {
|
|
1267
|
-
const setSpec = {};
|
|
1268
|
-
for (const field of diff.added) {
|
|
1269
|
-
const defaultVal = resolveSyncDefault(field);
|
|
1270
|
-
if (defaultVal !== void 0) setSpec[arraySafePath(host, field.physicalName, field.path)] = defaultVal;
|
|
1271
|
-
added.push(field.physicalName);
|
|
1300
|
+
const renameSpec = {};
|
|
1301
|
+
for (const r of diff.renamed) {
|
|
1302
|
+
if (pathCrossesArray(host, r.field.path)) {
|
|
1303
|
+
host._log("syncColumns: skipping $rename for array-element field", r.oldName, "→", r.field.physicalName, "(Mongo $rename cannot traverse arrays)");
|
|
1304
|
+
continue;
|
|
1272
1305
|
}
|
|
1273
|
-
|
|
1306
|
+
renameSpec[r.oldName] = r.field.physicalName;
|
|
1307
|
+
renamed.push(r.field.physicalName);
|
|
1308
|
+
}
|
|
1309
|
+
if (renamed.length > 0) await host.collection.updateMany({}, { $rename: renameSpec }, host._getSessionOpts());
|
|
1310
|
+
for (const field of diff.added) {
|
|
1311
|
+
const value = resolveSyncDefault(field);
|
|
1312
|
+
if (value !== void 0) await defaultBackfill(host, field.path, value);
|
|
1313
|
+
added.push(field.physicalName);
|
|
1274
1314
|
}
|
|
1275
|
-
if (Object.keys(update).length > 0) await host.collection.updateMany({}, update, host._getSessionOpts());
|
|
1276
1315
|
return {
|
|
1277
1316
|
added,
|
|
1278
1317
|
renamed
|
|
1279
1318
|
};
|
|
1280
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Backfills an added field's `@db.default` literal where the field is
|
|
1322
|
+
* missing — a stored value is never overwritten. Optional fields without a
|
|
1323
|
+
* default get no backfill (absent = "optional" in Mongo). A path crossing an
|
|
1324
|
+
* array narrows the innermost `$[]` to an `arrayFilters` element missing the
|
|
1325
|
+
* sub-path; outer arrays keep `$[]`, so empty arrays are a no-op (a bare
|
|
1326
|
+
* dotted path into an empty array is rejected with code 28).
|
|
1327
|
+
*/
|
|
1328
|
+
async function defaultBackfill(host, logicalPath, value) {
|
|
1329
|
+
const segments = positionalSegments(host, logicalPath);
|
|
1330
|
+
const inner = segments.lastIndexOf("$[]");
|
|
1331
|
+
const opts = host._getSessionOpts();
|
|
1332
|
+
if (inner === -1) {
|
|
1333
|
+
const path = segments.join(".");
|
|
1334
|
+
await host.collection.updateMany({ [path]: { $exists: false } }, { $set: { [path]: value } }, opts);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
segments[inner] = "$[backfill]";
|
|
1338
|
+
const rest = segments.slice(inner + 1).join(".");
|
|
1339
|
+
await host.collection.updateMany({}, { $set: { [segments.join(".")]: value } }, {
|
|
1340
|
+
...opts,
|
|
1341
|
+
arrayFilters: [{ [`backfill.${rest}`]: { $exists: false } }]
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1281
1344
|
async function dropColumnsImpl(host, columns) {
|
|
1282
1345
|
if (columns.length === 0) return;
|
|
1283
1346
|
const dropped = new Set(columns);
|
|
1284
1347
|
const minimal = columns.filter((col) => !hasAncestorIn(col, dropped));
|
|
1285
1348
|
const unsetSpec = {};
|
|
1286
|
-
for (const col of minimal) unsetSpec[
|
|
1349
|
+
for (const col of minimal) unsetSpec[positionalSegments(host, col, col).join(".")] = "";
|
|
1287
1350
|
await host.collection.updateMany({}, { $unset: unsetSpec }, host._getSessionOpts());
|
|
1288
1351
|
}
|
|
1289
1352
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
* `logicalPath` drives the array-boundary walk (flatMap is keyed by logical
|
|
1295
|
-
* path); the leaf of `physicalPath` is preserved so any `@db.column` rename
|
|
1296
|
-
* on the leaf still applies.
|
|
1353
|
+
* The segments of a field's physical document path with `$[]` after every
|
|
1354
|
+
* segment whose logical prefix is an array in the flatMap — Mongo's
|
|
1355
|
+
* all-positional operator walks every element. `physicalPath` defaults to
|
|
1356
|
+
* the logical path's document path (same segment count).
|
|
1297
1357
|
*/
|
|
1298
|
-
function
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
const physicalSegments = physicalPath.split(".");
|
|
1302
|
-
const physicalLeaf = physicalSegments[physicalSegments.length - 1];
|
|
1358
|
+
function positionalSegments(host, logicalPath, physicalPath = host._table.getMetadata().documentPath(logicalPath)) {
|
|
1359
|
+
const logical = logicalPath.split(".");
|
|
1360
|
+
const physical = physicalPath.split(".");
|
|
1303
1361
|
const out = [];
|
|
1304
1362
|
let prefix = "";
|
|
1305
|
-
for (let i = 0; i <
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (!isLeaf && isArrayPath(host._table.flatMap, prefix)) out.push("$[]");
|
|
1363
|
+
for (let i = 0; i < logical.length; i++) {
|
|
1364
|
+
out.push(physical[i]);
|
|
1365
|
+
prefix = joinPath(prefix, logical[i]);
|
|
1366
|
+
if (i < logical.length - 1 && isArrayPath(host._table.flatMap, prefix)) out.push("$[]");
|
|
1310
1367
|
}
|
|
1311
|
-
return out
|
|
1368
|
+
return out;
|
|
1312
1369
|
}
|
|
1313
1370
|
/** Returns true if any non-leaf segment of the path is typed as an array. */
|
|
1314
1371
|
function pathCrossesArray(host, logicalPath) {
|
|
@@ -1767,6 +1824,17 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1767
1824
|
calendarBucketUnits() {
|
|
1768
1825
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
1769
1826
|
}
|
|
1827
|
+
/** Every aggregate function, `countDistinct` included. */
|
|
1828
|
+
aggregateFns() {
|
|
1829
|
+
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* See BaseDbAdapter.viewRenderRevision. 2 = 0.1.137 (null-guarded field filters, nested document paths).
|
|
1833
|
+
* BUMP whenever `buildViewPipeline` output changes for an unchanged view definition.
|
|
1834
|
+
*/
|
|
1835
|
+
viewRenderRevision() {
|
|
1836
|
+
return "2";
|
|
1837
|
+
}
|
|
1770
1838
|
getValidatorPlugins() {
|
|
1771
1839
|
return [validateMongoIdPlugin];
|
|
1772
1840
|
}
|
|
@@ -1875,11 +1943,6 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1875
1943
|
this._addMongoIndexField("unique", "__pk", field);
|
|
1876
1944
|
this._pendingUniqueFields.push(field);
|
|
1877
1945
|
}
|
|
1878
|
-
if (metadata.has("db.default.increment")) {
|
|
1879
|
-
const physicalName = metadata.get("db.column") ?? field;
|
|
1880
|
-
const startValue = metadata.get("db.default.increment");
|
|
1881
|
-
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1882
|
-
}
|
|
1883
1946
|
for (const index of metadata.get("db.mongo.search.text") || []) this._pendingSearchFields.push({
|
|
1884
1947
|
indexName: index.indexName,
|
|
1885
1948
|
field,
|
|
@@ -1955,9 +2018,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1955
2018
|
path: value
|
|
1956
2019
|
});
|
|
1957
2020
|
}
|
|
1958
|
-
for (const fd of this._table.fieldDescriptors)
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
2021
|
+
for (const fd of this._table.fieldDescriptors) {
|
|
2022
|
+
if (fd.collate && fd.collate !== "binary") {
|
|
2023
|
+
this._collateFields ??= /* @__PURE__ */ new Map();
|
|
2024
|
+
this._collateFields.set(fd.path, fd.collate);
|
|
2025
|
+
this._collateFields.set(fd.physicalName, fd.collate);
|
|
2026
|
+
}
|
|
2027
|
+
const def = fd.defaultValue;
|
|
2028
|
+
if (def?.kind === "fn" && def.fn === "increment") this._incrementFields.set(fd.physicalName, def.start);
|
|
1961
2029
|
}
|
|
1962
2030
|
}
|
|
1963
2031
|
/** Returns MongoDB-specific search index map (internal). */
|
|
@@ -2030,7 +2098,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2030
2098
|
return geoSearchWithCountImpl(this, point, query, indexName);
|
|
2031
2099
|
}
|
|
2032
2100
|
async findManyWithCount(query) {
|
|
2033
|
-
const filter =
|
|
2101
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2034
2102
|
const controls = query.controls || {};
|
|
2035
2103
|
const dataStages = [];
|
|
2036
2104
|
if (controls.$sort) dataStages.push({ $sort: controls.$sort });
|
|
@@ -2077,7 +2145,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2077
2145
|
_buildCasFilter(filter, expectedVersion, op) {
|
|
2078
2146
|
const versionColumn = this._table.versionColumn;
|
|
2079
2147
|
if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error(`${op}: expectedVersion requires versionColumn`);
|
|
2080
|
-
const mongoFilter =
|
|
2148
|
+
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2081
2149
|
if (expectedVersion !== void 0 && versionColumn !== void 0) mongoFilter[versionColumn] = expectedVersion;
|
|
2082
2150
|
return mongoFilter;
|
|
2083
2151
|
}
|
|
@@ -2128,7 +2196,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2128
2196
|
};
|
|
2129
2197
|
}
|
|
2130
2198
|
async findOne(query) {
|
|
2131
|
-
const filter =
|
|
2199
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2132
2200
|
const opts = this._buildFindOptions(query.controls);
|
|
2133
2201
|
this._log("findOne", filter, opts);
|
|
2134
2202
|
return wrapInvalidQuery(() => this.collection.findOne(filter, {
|
|
@@ -2138,7 +2206,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2138
2206
|
}));
|
|
2139
2207
|
}
|
|
2140
2208
|
async findMany(query) {
|
|
2141
|
-
const filter =
|
|
2209
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2142
2210
|
const opts = this._buildFindOptions(query.controls);
|
|
2143
2211
|
this._log("findMany", filter, opts);
|
|
2144
2212
|
return wrapInvalidQuery(() => this.collection.find(filter, {
|
|
@@ -2148,7 +2216,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2148
2216
|
}).toArray());
|
|
2149
2217
|
}
|
|
2150
2218
|
async count(query) {
|
|
2151
|
-
const filter =
|
|
2219
|
+
const filter = require_mongo_accumulator.buildMongoFilter(query.filter);
|
|
2152
2220
|
this._log("countDocuments", filter);
|
|
2153
2221
|
return this.collection.countDocuments(filter, {
|
|
2154
2222
|
...this._getCollationOpts(query),
|
|
@@ -2183,13 +2251,13 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2183
2251
|
};
|
|
2184
2252
|
}
|
|
2185
2253
|
async deleteOne(filter) {
|
|
2186
|
-
const mongoFilter =
|
|
2254
|
+
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2187
2255
|
this._log("deleteOne", mongoFilter);
|
|
2188
2256
|
return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
2189
2257
|
}
|
|
2190
2258
|
async updateMany(filter, data, ops) {
|
|
2191
2259
|
const versionColumn = this._table.versionColumn;
|
|
2192
|
-
const mongoFilter =
|
|
2260
|
+
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2193
2261
|
const updateDoc = buildMongoUpdateDoc(data, ops, versionColumn);
|
|
2194
2262
|
this._log("updateMany", mongoFilter, updateDoc);
|
|
2195
2263
|
const result = await this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts());
|
|
@@ -2199,7 +2267,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2199
2267
|
};
|
|
2200
2268
|
}
|
|
2201
2269
|
async replaceMany(filter, data) {
|
|
2202
|
-
const mongoFilter =
|
|
2270
|
+
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2203
2271
|
const updateDoc = buildMongoUpdateDoc(data, void 0, this._table.versionColumn);
|
|
2204
2272
|
this._log("replaceMany", mongoFilter, updateDoc);
|
|
2205
2273
|
const result = await this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts());
|
|
@@ -2209,7 +2277,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2209
2277
|
};
|
|
2210
2278
|
}
|
|
2211
2279
|
async deleteMany(filter) {
|
|
2212
|
-
const mongoFilter =
|
|
2280
|
+
const mongoFilter = require_mongo_accumulator.buildMongoFilter(filter);
|
|
2213
2281
|
this._log("deleteMany", mongoFilter);
|
|
2214
2282
|
return { deletedCount: (await this.collection.deleteMany(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
2215
2283
|
}
|
|
@@ -2519,7 +2587,7 @@ function createAdapter(connection, _options) {
|
|
|
2519
2587
|
exports.CollectionPatcher = CollectionPatcher;
|
|
2520
2588
|
exports.INDEX_PREFIX = INDEX_PREFIX;
|
|
2521
2589
|
exports.MongoAdapter = MongoAdapter;
|
|
2522
|
-
exports.buildMongoFilter =
|
|
2590
|
+
exports.buildMongoFilter = require_mongo_accumulator.buildMongoFilter;
|
|
2523
2591
|
exports.createAdapter = createAdapter;
|
|
2524
2592
|
exports.mongoIndexKey = mongoIndexKey;
|
|
2525
2593
|
exports.validateMongoIdPlugin = validateMongoIdPlugin;
|