@atscript/db-mongo 0.1.147 → 0.1.149
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 +107 -2
- package/dist/agg.d.cts +33 -1
- package/dist/agg.d.mts +33 -1
- package/dist/agg.mjs +104 -3
- package/dist/index.cjs +128 -18
- package/dist/index.d.cts +29 -2
- package/dist/index.d.mts +29 -2
- package/dist/index.mjs +130 -20
- package/dist/{mongo-accumulator-BJcQs2OV.cjs → mongo-accumulator-BKMdRf3s.cjs} +6 -0
- package/dist/{mongo-accumulator-Y_xVYo3C.mjs → mongo-accumulator-BvJNCVoc.mjs} +1 -1
- package/package.json +9 -9
package/dist/agg.cjs
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_accumulator = require("./mongo-accumulator-
|
|
2
|
+
const require_mongo_accumulator = require("./mongo-accumulator-BKMdRf3s.cjs");
|
|
3
|
+
let _atscript_db = require("@atscript/db");
|
|
3
4
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
4
5
|
let _uniqu_core = require("@uniqu/core");
|
|
5
6
|
//#region src/agg.ts
|
|
7
|
+
/**
|
|
8
|
+
* MongoDB aggregation pipeline builder.
|
|
9
|
+
* Dynamically imported by MongoAdapter.aggregate() on first call.
|
|
10
|
+
*
|
|
11
|
+
* Constructs MongoDB aggregation pipelines from translated DbQuery objects
|
|
12
|
+
* containing $groupBy, $select (with AggregateExpr), $having, $sort, etc.
|
|
13
|
+
*/
|
|
6
14
|
/** Maps an AggregateExpr to its MongoDB `$group` accumulator (see `buildAccumulator`). */
|
|
7
15
|
function toAccumulator(expr) {
|
|
8
16
|
return require_mongo_accumulator.buildAccumulator(expr.$fn, expr.$field === "*" ? "*" : `$${expr.$field}`);
|
|
9
17
|
}
|
|
18
|
+
/** An operand of query-time arithmetic: the field as a double (IEEE, like the SQL adapters). */
|
|
19
|
+
const asDouble = (path) => ({ $toDouble: `$${path}` });
|
|
20
|
+
/** Prefix of the hidden `$group` field counting a sum's non-null values. */
|
|
21
|
+
const NON_NULL_PREFIX = "__as_n_";
|
|
10
22
|
const DAY_MS = 864e5;
|
|
11
23
|
const ISO_DATE = "%Y-%m-%d";
|
|
12
24
|
/** Units `$dateToString` labels straight from the instant in the zone (literal `01` for day / month). */
|
|
@@ -142,15 +154,41 @@ function buildGroupedStages(query, { accumulators, searchStage, filterOptions })
|
|
|
142
154
|
controls
|
|
143
155
|
};
|
|
144
156
|
}
|
|
157
|
+
const firstLast = controls.$select?.firstLast ?? [];
|
|
158
|
+
const rowOrder = controls.$select?.rowOrder ?? [];
|
|
159
|
+
if (firstLast.length > 0 && rowOrder.length > 0) pipeline.push({ $sort: Object.fromEntries(rowOrder.map((k) => [k.column, k.desc ? -1 : 1])) });
|
|
145
160
|
const project = { _id: 0 };
|
|
146
161
|
for (const [field, idKey] of groupKeys) project[field] = `$_id.${idKey}`;
|
|
162
|
+
const nullWhenEmpty = (alias, nonNull) => {
|
|
163
|
+
const count = `${NON_NULL_PREFIX}${alias}`;
|
|
164
|
+
groupStage[count] = { $sum: { $cond: [
|
|
165
|
+
nonNull,
|
|
166
|
+
1,
|
|
167
|
+
0
|
|
168
|
+
] } };
|
|
169
|
+
return { $cond: [
|
|
170
|
+
{ $eq: [`$${count}`, 0] },
|
|
171
|
+
null,
|
|
172
|
+
`$${alias}`
|
|
173
|
+
] };
|
|
174
|
+
};
|
|
147
175
|
for (const expr of controls.$select?.aggregates ?? []) {
|
|
148
176
|
const alias = (0, _atscript_db_agg.resolveAlias)(expr);
|
|
149
177
|
groupStage[alias] = toAccumulator(expr);
|
|
150
|
-
project[alias] = expr.$fn === "countDistinct" ? require_mongo_accumulator.distinctCountExpr(`$${alias}`) : 1;
|
|
178
|
+
project[alias] = expr.$fn === "countDistinct" ? require_mongo_accumulator.distinctCountExpr(`$${alias}`) : expr.$fn === "sum" ? nullWhenEmpty(alias, require_mongo_accumulator.notNullExpr(`$${expr.$field}`)) : 1;
|
|
179
|
+
}
|
|
180
|
+
for (const e of controls.$select?.exprAggregates ?? []) {
|
|
181
|
+
const value = require_mongo_accumulator.exprToMongo(e.expr, asDouble);
|
|
182
|
+
groupStage[e.alias] = { [`$${e.fn}`]: value };
|
|
183
|
+
project[e.alias] = e.fn === "sum" ? nullWhenEmpty(e.alias, require_mongo_accumulator.notNullExpr(value)) : 1;
|
|
184
|
+
}
|
|
185
|
+
for (const fl of firstLast) {
|
|
186
|
+
groupStage[fl.alias] = { [`$${fl.fn}`]: require_mongo_accumulator.orNull(`$${fl.column}`) };
|
|
187
|
+
project[fl.alias] = 1;
|
|
151
188
|
}
|
|
152
189
|
pipeline.push({ $group: groupStage });
|
|
153
190
|
pipeline.push({ $project: project });
|
|
191
|
+
for (const e of controls.$select?.exprs ?? []) pipeline.push({ $addFields: { [e.alias]: require_mongo_accumulator.exprToMongo(e.expr, asDouble) } });
|
|
154
192
|
if (controls.$having) pipeline.push({ $match: require_mongo_accumulator.buildMongoFilter(controls.$having) });
|
|
155
193
|
return {
|
|
156
194
|
pipeline,
|
|
@@ -180,6 +218,69 @@ function buildAggregatePipeline(query, searchStage, filterOptions) {
|
|
|
180
218
|
return pipeline;
|
|
181
219
|
}
|
|
182
220
|
/**
|
|
221
|
+
* The row an UNGROUPED aggregate yields over no input rows: a pipeline's
|
|
222
|
+
* `$group` emits nothing there, while SQL (and the memory adapter) always
|
|
223
|
+
* yield the one group — counts 0, every other aggregate, `first` / `last` and
|
|
224
|
+
* the expressions over them `null`. `undefined` when the query is grouped or
|
|
225
|
+
* computes nothing. The row is BEFORE `$having` / `$skip` — see
|
|
226
|
+
* {@link emptyGroupStages}.
|
|
227
|
+
*/
|
|
228
|
+
function emptyGroupRow(query) {
|
|
229
|
+
const controls = query.controls;
|
|
230
|
+
const select = controls?.$select;
|
|
231
|
+
if (!controls || controls.$groupBy?.length) return void 0;
|
|
232
|
+
if (!select?.computedAliases.length) return void 0;
|
|
233
|
+
const row = {};
|
|
234
|
+
for (const expr of select.aggregates ?? []) row[(0, _atscript_db_agg.resolveAlias)(expr)] = expr.$fn === "count" || expr.$fn === "countDistinct" ? 0 : null;
|
|
235
|
+
for (const e of select.exprAggregates ?? []) row[e.alias] = null;
|
|
236
|
+
for (const fl of select.firstLast ?? []) row[fl.alias] = null;
|
|
237
|
+
for (const e of select.exprs ?? []) row[e.alias] = (0, _atscript_db.evaluateExpr)(e.expr, (name) => row[name]);
|
|
238
|
+
return row;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* A pipeline yielding at most one document iff ANY input row matches the
|
|
242
|
+
* query's filter (and search) — the probe that tells "no rows matched" (the
|
|
243
|
+
* one empty group exists) from "the real group was removed by `$having`".
|
|
244
|
+
*/
|
|
245
|
+
function buildMatchedProbe(query, searchStage, filterOptions) {
|
|
246
|
+
const { pipeline } = buildPrefix(query, searchStage, filterOptions);
|
|
247
|
+
pipeline.push({ $limit: 1 }, { $project: { _id: 1 } });
|
|
248
|
+
return pipeline;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The pipeline that applies the query's `$having` (and, for the row query, `$skip`
|
|
252
|
+
* and `$limit`) to the {@link emptyGroupRow} — over a single synthetic document
|
|
253
|
+
* (a `$facet` emits exactly one document, even from an empty input), so the
|
|
254
|
+
* server evaluates `$having` exactly as it does for a real group. `undefined`
|
|
255
|
+
* when there is nothing to apply (the row stands). Its result is empty when the
|
|
256
|
+
* row is filtered out.
|
|
257
|
+
*/
|
|
258
|
+
function emptyGroupStages(query, row, forCount) {
|
|
259
|
+
const controls = query.controls;
|
|
260
|
+
const having = controls?.$having;
|
|
261
|
+
const skip = forCount ? 0 : controls?.$skip ?? 0;
|
|
262
|
+
const limit = forCount ? 0 : controls?.$limit ?? 0;
|
|
263
|
+
if (!having && !skip && !limit) return void 0;
|
|
264
|
+
const stages = [
|
|
265
|
+
{ $match: { _id: { $in: [] } } },
|
|
266
|
+
{ $facet: { one: [] } },
|
|
267
|
+
{ $replaceRoot: { newRoot: { $literal: row } } }
|
|
268
|
+
];
|
|
269
|
+
if (having) stages.push({ $match: require_mongo_accumulator.buildMongoFilter(having) });
|
|
270
|
+
if (skip) stages.push({ $skip: skip });
|
|
271
|
+
if (limit) stages.push({ $limit: limit });
|
|
272
|
+
return stages;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `allowDiskUse` for a pipeline that sorts before it groups (`first` / `last`
|
|
276
|
+
* order every matching row by `$rowOrder` first, which may exceed the
|
|
277
|
+
* in-memory sort limit); `undefined` otherwise.
|
|
278
|
+
*/
|
|
279
|
+
function aggregateOptions(pipeline) {
|
|
280
|
+
const group = pipeline.findIndex((stage) => "$group" in stage);
|
|
281
|
+
return pipeline.slice(0, group < 0 ? 0 : group).some((stage) => "$sort" in stage) ? { allowDiskUse: true } : void 0;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
183
284
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
184
285
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
185
286
|
* stages as the row pipeline — the accumulators must run so an alias
|
|
@@ -201,6 +302,10 @@ function buildCountPipeline(query, searchStage, filterOptions) {
|
|
|
201
302
|
return pipeline;
|
|
202
303
|
}
|
|
203
304
|
//#endregion
|
|
305
|
+
exports.aggregateOptions = aggregateOptions;
|
|
204
306
|
exports.bucketExpression = bucketExpression;
|
|
205
307
|
exports.buildAggregatePipeline = buildAggregatePipeline;
|
|
206
308
|
exports.buildCountPipeline = buildCountPipeline;
|
|
309
|
+
exports.buildMatchedProbe = buildMatchedProbe;
|
|
310
|
+
exports.emptyGroupRow = emptyGroupRow;
|
|
311
|
+
exports.emptyGroupStages = emptyGroupStages;
|
package/dist/agg.d.cts
CHANGED
|
@@ -33,6 +33,38 @@ declare function bucketExpression(b: TResolvedBucket): Document;
|
|
|
33
33
|
* filter with relational predicates (`mongoFilterStages`).
|
|
34
34
|
*/
|
|
35
35
|
declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
36
|
+
/**
|
|
37
|
+
* The row an UNGROUPED aggregate yields over no input rows: a pipeline's
|
|
38
|
+
* `$group` emits nothing there, while SQL (and the memory adapter) always
|
|
39
|
+
* yield the one group — counts 0, every other aggregate, `first` / `last` and
|
|
40
|
+
* the expressions over them `null`. `undefined` when the query is grouped or
|
|
41
|
+
* computes nothing. The row is BEFORE `$having` / `$skip` — see
|
|
42
|
+
* {@link emptyGroupStages}.
|
|
43
|
+
*/
|
|
44
|
+
declare function emptyGroupRow(query: DbQuery): Document | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* A pipeline yielding at most one document iff ANY input row matches the
|
|
47
|
+
* query's filter (and search) — the probe that tells "no rows matched" (the
|
|
48
|
+
* one empty group exists) from "the real group was removed by `$having`".
|
|
49
|
+
*/
|
|
50
|
+
declare function buildMatchedProbe(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
51
|
+
/**
|
|
52
|
+
* The pipeline that applies the query's `$having` (and, for the row query, `$skip`
|
|
53
|
+
* and `$limit`) to the {@link emptyGroupRow} — over a single synthetic document
|
|
54
|
+
* (a `$facet` emits exactly one document, even from an empty input), so the
|
|
55
|
+
* server evaluates `$having` exactly as it does for a real group. `undefined`
|
|
56
|
+
* when there is nothing to apply (the row stands). Its result is empty when the
|
|
57
|
+
* row is filtered out.
|
|
58
|
+
*/
|
|
59
|
+
declare function emptyGroupStages(query: DbQuery, row: Document, forCount: boolean): Document[] | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* `allowDiskUse` for a pipeline that sorts before it groups (`first` / `last`
|
|
62
|
+
* order every matching row by `$rowOrder` first, which may exceed the
|
|
63
|
+
* in-memory sort limit); `undefined` otherwise.
|
|
64
|
+
*/
|
|
65
|
+
declare function aggregateOptions(pipeline: Document[]): {
|
|
66
|
+
allowDiskUse: true;
|
|
67
|
+
} | undefined;
|
|
36
68
|
/**
|
|
37
69
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
38
70
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
@@ -47,4 +79,4 @@ declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document,
|
|
|
47
79
|
*/
|
|
48
80
|
declare function buildCountPipeline(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
49
81
|
//#endregion
|
|
50
|
-
export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
|
|
82
|
+
export { aggregateOptions, bucketExpression, buildAggregatePipeline, buildCountPipeline, buildMatchedProbe, emptyGroupRow, emptyGroupStages };
|
package/dist/agg.d.mts
CHANGED
|
@@ -33,6 +33,38 @@ declare function bucketExpression(b: TResolvedBucket): Document;
|
|
|
33
33
|
* filter with relational predicates (`mongoFilterStages`).
|
|
34
34
|
*/
|
|
35
35
|
declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
36
|
+
/**
|
|
37
|
+
* The row an UNGROUPED aggregate yields over no input rows: a pipeline's
|
|
38
|
+
* `$group` emits nothing there, while SQL (and the memory adapter) always
|
|
39
|
+
* yield the one group — counts 0, every other aggregate, `first` / `last` and
|
|
40
|
+
* the expressions over them `null`. `undefined` when the query is grouped or
|
|
41
|
+
* computes nothing. The row is BEFORE `$having` / `$skip` — see
|
|
42
|
+
* {@link emptyGroupStages}.
|
|
43
|
+
*/
|
|
44
|
+
declare function emptyGroupRow(query: DbQuery): Document | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* A pipeline yielding at most one document iff ANY input row matches the
|
|
47
|
+
* query's filter (and search) — the probe that tells "no rows matched" (the
|
|
48
|
+
* one empty group exists) from "the real group was removed by `$having`".
|
|
49
|
+
*/
|
|
50
|
+
declare function buildMatchedProbe(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
51
|
+
/**
|
|
52
|
+
* The pipeline that applies the query's `$having` (and, for the row query, `$skip`
|
|
53
|
+
* and `$limit`) to the {@link emptyGroupRow} — over a single synthetic document
|
|
54
|
+
* (a `$facet` emits exactly one document, even from an empty input), so the
|
|
55
|
+
* server evaluates `$having` exactly as it does for a real group. `undefined`
|
|
56
|
+
* when there is nothing to apply (the row stands). Its result is empty when the
|
|
57
|
+
* row is filtered out.
|
|
58
|
+
*/
|
|
59
|
+
declare function emptyGroupStages(query: DbQuery, row: Document, forCount: boolean): Document[] | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* `allowDiskUse` for a pipeline that sorts before it groups (`first` / `last`
|
|
62
|
+
* order every matching row by `$rowOrder` first, which may exceed the
|
|
63
|
+
* in-memory sort limit); `undefined` otherwise.
|
|
64
|
+
*/
|
|
65
|
+
declare function aggregateOptions(pipeline: Document[]): {
|
|
66
|
+
allowDiskUse: true;
|
|
67
|
+
} | undefined;
|
|
36
68
|
/**
|
|
37
69
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
38
70
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
@@ -47,4 +79,4 @@ declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document,
|
|
|
47
79
|
*/
|
|
48
80
|
declare function buildCountPipeline(query: DbQuery, searchStage?: Document, filterOptions?: TMongoFilterOptions): Document[];
|
|
49
81
|
//#endregion
|
|
50
|
-
export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
|
|
82
|
+
export { aggregateOptions, bucketExpression, buildAggregatePipeline, buildCountPipeline, buildMatchedProbe, emptyGroupRow, emptyGroupStages };
|
package/dist/agg.mjs
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { i as buildMongoFilter, n as distinctCountExpr, s as mongoFilterStages, t as buildAccumulator, u as
|
|
1
|
+
import { d as orNull, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, s as mongoFilterStages, t as buildAccumulator, u as notNullExpr } from "./mongo-accumulator-BvJNCVoc.mjs";
|
|
2
|
+
import { evaluateExpr } from "@atscript/db";
|
|
2
3
|
import { resolveAlias } from "@atscript/db/agg";
|
|
3
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
4
5
|
//#region src/agg.ts
|
|
6
|
+
/**
|
|
7
|
+
* MongoDB aggregation pipeline builder.
|
|
8
|
+
* Dynamically imported by MongoAdapter.aggregate() on first call.
|
|
9
|
+
*
|
|
10
|
+
* Constructs MongoDB aggregation pipelines from translated DbQuery objects
|
|
11
|
+
* containing $groupBy, $select (with AggregateExpr), $having, $sort, etc.
|
|
12
|
+
*/
|
|
5
13
|
/** Maps an AggregateExpr to its MongoDB `$group` accumulator (see `buildAccumulator`). */
|
|
6
14
|
function toAccumulator(expr) {
|
|
7
15
|
return buildAccumulator(expr.$fn, expr.$field === "*" ? "*" : `$${expr.$field}`);
|
|
8
16
|
}
|
|
17
|
+
/** An operand of query-time arithmetic: the field as a double (IEEE, like the SQL adapters). */
|
|
18
|
+
const asDouble = (path) => ({ $toDouble: `$${path}` });
|
|
19
|
+
/** Prefix of the hidden `$group` field counting a sum's non-null values. */
|
|
20
|
+
const NON_NULL_PREFIX = "__as_n_";
|
|
9
21
|
const DAY_MS = 864e5;
|
|
10
22
|
const ISO_DATE = "%Y-%m-%d";
|
|
11
23
|
/** Units `$dateToString` labels straight from the instant in the zone (literal `01` for day / month). */
|
|
@@ -141,15 +153,41 @@ function buildGroupedStages(query, { accumulators, searchStage, filterOptions })
|
|
|
141
153
|
controls
|
|
142
154
|
};
|
|
143
155
|
}
|
|
156
|
+
const firstLast = controls.$select?.firstLast ?? [];
|
|
157
|
+
const rowOrder = controls.$select?.rowOrder ?? [];
|
|
158
|
+
if (firstLast.length > 0 && rowOrder.length > 0) pipeline.push({ $sort: Object.fromEntries(rowOrder.map((k) => [k.column, k.desc ? -1 : 1])) });
|
|
144
159
|
const project = { _id: 0 };
|
|
145
160
|
for (const [field, idKey] of groupKeys) project[field] = `$_id.${idKey}`;
|
|
161
|
+
const nullWhenEmpty = (alias, nonNull) => {
|
|
162
|
+
const count = `${NON_NULL_PREFIX}${alias}`;
|
|
163
|
+
groupStage[count] = { $sum: { $cond: [
|
|
164
|
+
nonNull,
|
|
165
|
+
1,
|
|
166
|
+
0
|
|
167
|
+
] } };
|
|
168
|
+
return { $cond: [
|
|
169
|
+
{ $eq: [`$${count}`, 0] },
|
|
170
|
+
null,
|
|
171
|
+
`$${alias}`
|
|
172
|
+
] };
|
|
173
|
+
};
|
|
146
174
|
for (const expr of controls.$select?.aggregates ?? []) {
|
|
147
175
|
const alias = resolveAlias(expr);
|
|
148
176
|
groupStage[alias] = toAccumulator(expr);
|
|
149
|
-
project[alias] = expr.$fn === "countDistinct" ? distinctCountExpr(`$${alias}`) : 1;
|
|
177
|
+
project[alias] = expr.$fn === "countDistinct" ? distinctCountExpr(`$${alias}`) : expr.$fn === "sum" ? nullWhenEmpty(alias, notNullExpr(`$${expr.$field}`)) : 1;
|
|
178
|
+
}
|
|
179
|
+
for (const e of controls.$select?.exprAggregates ?? []) {
|
|
180
|
+
const value = exprToMongo(e.expr, asDouble);
|
|
181
|
+
groupStage[e.alias] = { [`$${e.fn}`]: value };
|
|
182
|
+
project[e.alias] = e.fn === "sum" ? nullWhenEmpty(e.alias, notNullExpr(value)) : 1;
|
|
183
|
+
}
|
|
184
|
+
for (const fl of firstLast) {
|
|
185
|
+
groupStage[fl.alias] = { [`$${fl.fn}`]: orNull(`$${fl.column}`) };
|
|
186
|
+
project[fl.alias] = 1;
|
|
150
187
|
}
|
|
151
188
|
pipeline.push({ $group: groupStage });
|
|
152
189
|
pipeline.push({ $project: project });
|
|
190
|
+
for (const e of controls.$select?.exprs ?? []) pipeline.push({ $addFields: { [e.alias]: exprToMongo(e.expr, asDouble) } });
|
|
153
191
|
if (controls.$having) pipeline.push({ $match: buildMongoFilter(controls.$having) });
|
|
154
192
|
return {
|
|
155
193
|
pipeline,
|
|
@@ -179,6 +217,69 @@ function buildAggregatePipeline(query, searchStage, filterOptions) {
|
|
|
179
217
|
return pipeline;
|
|
180
218
|
}
|
|
181
219
|
/**
|
|
220
|
+
* The row an UNGROUPED aggregate yields over no input rows: a pipeline's
|
|
221
|
+
* `$group` emits nothing there, while SQL (and the memory adapter) always
|
|
222
|
+
* yield the one group — counts 0, every other aggregate, `first` / `last` and
|
|
223
|
+
* the expressions over them `null`. `undefined` when the query is grouped or
|
|
224
|
+
* computes nothing. The row is BEFORE `$having` / `$skip` — see
|
|
225
|
+
* {@link emptyGroupStages}.
|
|
226
|
+
*/
|
|
227
|
+
function emptyGroupRow(query) {
|
|
228
|
+
const controls = query.controls;
|
|
229
|
+
const select = controls?.$select;
|
|
230
|
+
if (!controls || controls.$groupBy?.length) return void 0;
|
|
231
|
+
if (!select?.computedAliases.length) return void 0;
|
|
232
|
+
const row = {};
|
|
233
|
+
for (const expr of select.aggregates ?? []) row[resolveAlias(expr)] = expr.$fn === "count" || expr.$fn === "countDistinct" ? 0 : null;
|
|
234
|
+
for (const e of select.exprAggregates ?? []) row[e.alias] = null;
|
|
235
|
+
for (const fl of select.firstLast ?? []) row[fl.alias] = null;
|
|
236
|
+
for (const e of select.exprs ?? []) row[e.alias] = evaluateExpr(e.expr, (name) => row[name]);
|
|
237
|
+
return row;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* A pipeline yielding at most one document iff ANY input row matches the
|
|
241
|
+
* query's filter (and search) — the probe that tells "no rows matched" (the
|
|
242
|
+
* one empty group exists) from "the real group was removed by `$having`".
|
|
243
|
+
*/
|
|
244
|
+
function buildMatchedProbe(query, searchStage, filterOptions) {
|
|
245
|
+
const { pipeline } = buildPrefix(query, searchStage, filterOptions);
|
|
246
|
+
pipeline.push({ $limit: 1 }, { $project: { _id: 1 } });
|
|
247
|
+
return pipeline;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The pipeline that applies the query's `$having` (and, for the row query, `$skip`
|
|
251
|
+
* and `$limit`) to the {@link emptyGroupRow} — over a single synthetic document
|
|
252
|
+
* (a `$facet` emits exactly one document, even from an empty input), so the
|
|
253
|
+
* server evaluates `$having` exactly as it does for a real group. `undefined`
|
|
254
|
+
* when there is nothing to apply (the row stands). Its result is empty when the
|
|
255
|
+
* row is filtered out.
|
|
256
|
+
*/
|
|
257
|
+
function emptyGroupStages(query, row, forCount) {
|
|
258
|
+
const controls = query.controls;
|
|
259
|
+
const having = controls?.$having;
|
|
260
|
+
const skip = forCount ? 0 : controls?.$skip ?? 0;
|
|
261
|
+
const limit = forCount ? 0 : controls?.$limit ?? 0;
|
|
262
|
+
if (!having && !skip && !limit) return void 0;
|
|
263
|
+
const stages = [
|
|
264
|
+
{ $match: { _id: { $in: [] } } },
|
|
265
|
+
{ $facet: { one: [] } },
|
|
266
|
+
{ $replaceRoot: { newRoot: { $literal: row } } }
|
|
267
|
+
];
|
|
268
|
+
if (having) stages.push({ $match: buildMongoFilter(having) });
|
|
269
|
+
if (skip) stages.push({ $skip: skip });
|
|
270
|
+
if (limit) stages.push({ $limit: limit });
|
|
271
|
+
return stages;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* `allowDiskUse` for a pipeline that sorts before it groups (`first` / `last`
|
|
275
|
+
* order every matching row by `$rowOrder` first, which may exceed the
|
|
276
|
+
* in-memory sort limit); `undefined` otherwise.
|
|
277
|
+
*/
|
|
278
|
+
function aggregateOptions(pipeline) {
|
|
279
|
+
const group = pipeline.findIndex((stage) => "$group" in stage);
|
|
280
|
+
return pipeline.slice(0, group < 0 ? 0 : group).some((stage) => "$sort" in stage) ? { allowDiskUse: true } : void 0;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
182
283
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
183
284
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
184
285
|
* stages as the row pipeline — the accumulators must run so an alias
|
|
@@ -200,4 +301,4 @@ function buildCountPipeline(query, searchStage, filterOptions) {
|
|
|
200
301
|
return pipeline;
|
|
201
302
|
}
|
|
202
303
|
//#endregion
|
|
203
|
-
export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
|
|
304
|
+
export { aggregateOptions, bucketExpression, buildAggregatePipeline, buildCountPipeline, buildMatchedProbe, emptyGroupRow, emptyGroupStages };
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_accumulator = require("./mongo-accumulator-
|
|
2
|
+
const require_mongo_accumulator = require("./mongo-accumulator-BKMdRf3s.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");
|
|
@@ -1803,6 +1803,11 @@ function objectIdFromStorage(value) {
|
|
|
1803
1803
|
* predicates (see {@link MongoAdapter.supportsRelationFilters}).
|
|
1804
1804
|
*/
|
|
1805
1805
|
const REL_WRITE_BATCH = 1e3;
|
|
1806
|
+
/** Whether a bulk-write error carries a write-concern failure besides (or instead of) write errors. */
|
|
1807
|
+
function hasWriteConcernError(error) {
|
|
1808
|
+
const e = error;
|
|
1809
|
+
return Boolean(e.writeConcernError || e.result?.getWriteConcernError?.() || e.result?.writeConcernErrors?.length);
|
|
1810
|
+
}
|
|
1806
1811
|
var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
1807
1812
|
db;
|
|
1808
1813
|
client;
|
|
@@ -1915,17 +1920,28 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1915
1920
|
});
|
|
1916
1921
|
}
|
|
1917
1922
|
async aggregate(query) {
|
|
1918
|
-
const { buildAggregatePipeline, buildCountPipeline } = await Promise.resolve().then(() => require("./agg.cjs"));
|
|
1923
|
+
const { aggregateOptions, buildAggregatePipeline, buildCountPipeline, buildMatchedProbe, emptyGroupRow, emptyGroupStages } = await Promise.resolve().then(() => require("./agg.cjs"));
|
|
1924
|
+
const emptyGroup = async (forCount) => {
|
|
1925
|
+
const row = emptyGroupRow(query);
|
|
1926
|
+
if (!row) return void 0;
|
|
1927
|
+
const probe = buildMatchedProbe(query, searchStage, this._predicateFilterOpts);
|
|
1928
|
+
if ((await wrapInvalidQuery(() => this.aggregatePipeline(probe).toArray())).length > 0) return;
|
|
1929
|
+
const stages = emptyGroupStages(query, row, forCount);
|
|
1930
|
+
if (!stages) return row;
|
|
1931
|
+
return (await wrapInvalidQuery(() => this.aggregatePipeline(stages).toArray()))[0];
|
|
1932
|
+
};
|
|
1919
1933
|
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1920
1934
|
if (query.controls?.$count) {
|
|
1921
1935
|
const pipeline = buildCountPipeline(query, searchStage, this._predicateFilterOpts);
|
|
1922
1936
|
this._log("aggregate (count)", pipeline);
|
|
1923
|
-
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1924
|
-
return result.length > 0 ? result : [{ count: 0 }];
|
|
1937
|
+
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
1938
|
+
return result.length > 0 ? result : [{ count: await emptyGroup(true) ? 1 : 0 }];
|
|
1925
1939
|
}
|
|
1926
1940
|
const pipeline = buildAggregatePipeline(query, searchStage, this._predicateFilterOpts);
|
|
1927
1941
|
this._log("aggregate", pipeline);
|
|
1928
|
-
|
|
1942
|
+
const rows = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline, aggregateOptions(pipeline)).toArray());
|
|
1943
|
+
const empty = rows.length === 0 ? await emptyGroup(false) : void 0;
|
|
1944
|
+
return empty ? [empty] : rows;
|
|
1929
1945
|
}
|
|
1930
1946
|
get idType() {
|
|
1931
1947
|
const idProp = this._table.type.type.props.get("_id");
|
|
@@ -1965,10 +1981,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1965
1981
|
calendarBucketUnits() {
|
|
1966
1982
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
1967
1983
|
}
|
|
1968
|
-
/** Every aggregate function
|
|
1984
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
1969
1985
|
aggregateFns() {
|
|
1970
1986
|
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
1971
1987
|
}
|
|
1988
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
1989
|
+
supportsAggregateExpressions() {
|
|
1990
|
+
return true;
|
|
1991
|
+
}
|
|
1972
1992
|
/** Computed view columns and first-row joins. */
|
|
1973
1993
|
viewCapabilities() {
|
|
1974
1994
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -2426,13 +2446,17 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2426
2446
|
try {
|
|
2427
2447
|
return await fn();
|
|
2428
2448
|
} catch (error) {
|
|
2429
|
-
|
|
2430
|
-
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
2431
|
-
message: error.message
|
|
2432
|
-
}]);
|
|
2433
|
-
throw error;
|
|
2449
|
+
return this._mapConstraintError(error);
|
|
2434
2450
|
}
|
|
2435
2451
|
}
|
|
2452
|
+
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
2453
|
+
_mapConstraintError(error) {
|
|
2454
|
+
if (error instanceof mongodb.MongoServerError && error.code === 11e3) throw new _atscript_db.DbError("CONFLICT", [{
|
|
2455
|
+
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
2456
|
+
message: error.message
|
|
2457
|
+
}]);
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2436
2460
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
2437
2461
|
async _wrapUpdate(fn) {
|
|
2438
2462
|
const { matchedCount, modifiedCount } = await this._wrapDuplicateKeyError(fn);
|
|
@@ -2456,6 +2480,98 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2456
2480
|
return { insertedId: objectIdFromStorage(this._resolveInsertedId(data, result.insertedId)) };
|
|
2457
2481
|
}
|
|
2458
2482
|
async insertMany(data) {
|
|
2483
|
+
await this._prepareInsertBatch(data);
|
|
2484
|
+
this._log("insertMany", `${data.length} docs`);
|
|
2485
|
+
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
2486
|
+
return {
|
|
2487
|
+
insertedCount: result.insertedCount,
|
|
2488
|
+
insertedIds: data.map((item, i) => objectIdFromStorage(this._resolveInsertedId(item, result.insertedIds[i])))
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
supportsInsertIgnore() {
|
|
2492
|
+
return true;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Conflict-ignoring batch insert (the core already removed in-batch duplicates).
|
|
2496
|
+
*
|
|
2497
|
+
* Outside a transaction: `insertMany(…, { ordered: false })`; duplicate-key
|
|
2498
|
+
* write errors (11000) mark the skipped rows, every other write error
|
|
2499
|
+
* rethrows after the batch (rows already written stay — as for a plain
|
|
2500
|
+
* non-transactional `insertMany`).
|
|
2501
|
+
*
|
|
2502
|
+
* Inside a transaction a duplicate key would abort the transaction even with
|
|
2503
|
+
* `ordered: false`, so the stored keys are looked up first (inside the
|
|
2504
|
+
* session), the matches are skipped and the rest is inserted ordered. A
|
|
2505
|
+
* residual 11000 (e.g. a collation-equal value) throws `CONFLICT`.
|
|
2506
|
+
*/
|
|
2507
|
+
async insertManyIgnore(data) {
|
|
2508
|
+
if (data.length === 0) return [];
|
|
2509
|
+
await this._prepareInsertBatch(data);
|
|
2510
|
+
this._log("insertManyIgnore", `${data.length} docs`);
|
|
2511
|
+
const idOf = (item) => objectIdFromStorage(this._resolveInsertedId(item, item._id));
|
|
2512
|
+
if (this._getTransactionState()) {
|
|
2513
|
+
const skipped = await this._findStoredKeyConflicts(data);
|
|
2514
|
+
const survivors = data.filter((_, i) => !skipped.has(i));
|
|
2515
|
+
if (survivors.length > 0) await this._wrapDuplicateKeyError(() => this.collection.insertMany(survivors, {
|
|
2516
|
+
ordered: true,
|
|
2517
|
+
...this._getSessionOpts()
|
|
2518
|
+
}));
|
|
2519
|
+
return data.map((item, i) => skipped.has(i) ? null : { insertedId: idOf(item) });
|
|
2520
|
+
}
|
|
2521
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
2522
|
+
try {
|
|
2523
|
+
await this.collection.insertMany(data, {
|
|
2524
|
+
ordered: false,
|
|
2525
|
+
...this._getSessionOpts()
|
|
2526
|
+
});
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
if (!(error instanceof mongodb.MongoBulkWriteError)) this._mapConstraintError(error);
|
|
2529
|
+
const writeErrors = [error.writeErrors].flat().filter(Boolean);
|
|
2530
|
+
if (writeErrors.length === 0 || hasWriteConcernError(error)) throw error;
|
|
2531
|
+
for (const writeError of writeErrors) {
|
|
2532
|
+
if (writeError.code !== 11e3) throw error;
|
|
2533
|
+
skipped.add(writeError.index);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return data.map((item, i) => skipped.has(i) ? null : { insertedId: idOf(item) });
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Indices of `data` rows whose primary (`_id`) or unique-index key tuple is
|
|
2540
|
+
* already stored (rows with a null / missing key component never collide).
|
|
2541
|
+
* One `$or` query per chunk covers every key set.
|
|
2542
|
+
*/
|
|
2543
|
+
async _findStoredKeyConflicts(data) {
|
|
2544
|
+
const keySets = [["_id"], ...this._table.uniqueKeySets.filter((f) => !(f.length === 1 && f[0] === "_id"))];
|
|
2545
|
+
const rowTuples = data.map((row) => keySets.map((fields) => (0, _atscript_db.uniqueKeyTuple)(row, fields)));
|
|
2546
|
+
const projection = Object.fromEntries(keySets.flat().map((f) => [f, 1]));
|
|
2547
|
+
const stored = keySets.map(() => /* @__PURE__ */ new Set());
|
|
2548
|
+
const CHUNK = 1e3;
|
|
2549
|
+
for (let offset = 0; offset < data.length; offset += CHUNK) {
|
|
2550
|
+
const clauses = [];
|
|
2551
|
+
keySets.forEach((fields, k) => {
|
|
2552
|
+
const rows = data.slice(offset, offset + CHUNK).filter((_, i) => rowTuples[offset + i][k]);
|
|
2553
|
+
if (rows.length === 0) return;
|
|
2554
|
+
if (fields.length === 1) clauses.push({ [fields[0]]: { $in: rows.map((row) => (0, _atscript_db.getPath)(row, fields[0])) } });
|
|
2555
|
+
else for (const row of rows) clauses.push(Object.fromEntries(fields.map((f) => [f, (0, _atscript_db.getPath)(row, f)])));
|
|
2556
|
+
});
|
|
2557
|
+
if (clauses.length === 0) continue;
|
|
2558
|
+
const found = await this.collection.find({ $or: clauses }, {
|
|
2559
|
+
projection,
|
|
2560
|
+
...this._getSessionOpts()
|
|
2561
|
+
}).toArray();
|
|
2562
|
+
for (const doc of found) keySets.forEach((fields, k) => {
|
|
2563
|
+
const tuple = (0, _atscript_db.uniqueKeyTuple)(doc, fields);
|
|
2564
|
+
if (tuple !== void 0) stored[k].add(tuple);
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
2568
|
+
rowTuples.forEach((tuples, i) => {
|
|
2569
|
+
if (tuples.some((t, k) => t !== void 0 && stored[k].has(t))) skipped.add(i);
|
|
2570
|
+
});
|
|
2571
|
+
return skipped;
|
|
2572
|
+
}
|
|
2573
|
+
/** Version default + `@db.default.increment` allocation shared by every batch insert. */
|
|
2574
|
+
async _prepareInsertBatch(data) {
|
|
2459
2575
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2460
2576
|
if (versionColumn !== void 0) {
|
|
2461
2577
|
for (const item of data) if (!(versionColumn in item)) item[versionColumn] = 0;
|
|
@@ -2465,12 +2581,6 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2465
2581
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
2466
2582
|
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
2467
2583
|
}
|
|
2468
|
-
this._log("insertMany", `${data.length} docs`);
|
|
2469
|
-
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
2470
|
-
return {
|
|
2471
|
-
insertedCount: result.insertedCount,
|
|
2472
|
-
insertedIds: data.map((item, i) => objectIdFromStorage(this._resolveInsertedId(item, result.insertedIds[i])))
|
|
2473
|
-
};
|
|
2474
2584
|
}
|
|
2475
2585
|
async findOne(query) {
|
|
2476
2586
|
if ((0, _atscript_db.containsRelationFilter)(query.filter)) {
|
|
@@ -2894,7 +3004,7 @@ function buildMongoUpdateDoc(data, ops, versionColumn) {
|
|
|
2894
3004
|
function createAdapter(connection, _options) {
|
|
2895
3005
|
const client = new mongodb.MongoClient(connection);
|
|
2896
3006
|
const db = client.db();
|
|
2897
|
-
return new _atscript_db.DbSpace(() => new MongoAdapter(db, client));
|
|
3007
|
+
return new _atscript_db.DbSpace(() => new MongoAdapter(db, client), { onClose: () => client.close() });
|
|
2898
3008
|
}
|
|
2899
3009
|
//#endregion
|
|
2900
3010
|
exports.CollectionPatcher = CollectionPatcher;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as buildMongoFilter, c as mongoFilterStages, i as TMongoFilterPlan, l as planStages, n as TMongoFieldCollation, o as buildMongoQuery, r as TMongoFilterOptions, s as collationOfAdapter, t as REL_FILTER_TEMP_PREFIX } from "./mongo-filter-CGd9ryOD.cjs";
|
|
2
|
-
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
2
|
+
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
3
3
|
import { AggregateOptions, AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
|
|
4
4
|
import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
|
|
5
5
|
import { AggregateFn, BucketUnit } from "@uniqu/core";
|
|
@@ -316,8 +316,10 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
316
316
|
supportsNativePatch(): boolean;
|
|
317
317
|
/** Every unit, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
|
|
318
318
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
319
|
-
/** Every aggregate function
|
|
319
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
320
320
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
321
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
322
|
+
supportsAggregateExpressions(): boolean;
|
|
321
323
|
/** Computed view columns and first-row joins. */
|
|
322
324
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
323
325
|
/**
|
|
@@ -481,10 +483,35 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
481
483
|
* so a unique-index violation is a `CONFLICT` whichever statement hit it.
|
|
482
484
|
*/
|
|
483
485
|
private _wrapDuplicateKeyError;
|
|
486
|
+
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
487
|
+
private _mapConstraintError;
|
|
484
488
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
485
489
|
private _wrapUpdate;
|
|
486
490
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
487
491
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
492
|
+
supportsInsertIgnore(): boolean;
|
|
493
|
+
/**
|
|
494
|
+
* Conflict-ignoring batch insert (the core already removed in-batch duplicates).
|
|
495
|
+
*
|
|
496
|
+
* Outside a transaction: `insertMany(…, { ordered: false })`; duplicate-key
|
|
497
|
+
* write errors (11000) mark the skipped rows, every other write error
|
|
498
|
+
* rethrows after the batch (rows already written stay — as for a plain
|
|
499
|
+
* non-transactional `insertMany`).
|
|
500
|
+
*
|
|
501
|
+
* Inside a transaction a duplicate key would abort the transaction even with
|
|
502
|
+
* `ordered: false`, so the stored keys are looked up first (inside the
|
|
503
|
+
* session), the matches are skipped and the rest is inserted ordered. A
|
|
504
|
+
* residual 11000 (e.g. a collation-equal value) throws `CONFLICT`.
|
|
505
|
+
*/
|
|
506
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
507
|
+
/**
|
|
508
|
+
* Indices of `data` rows whose primary (`_id`) or unique-index key tuple is
|
|
509
|
+
* already stored (rows with a null / missing key component never collide).
|
|
510
|
+
* One `$or` query per chunk covers every key set.
|
|
511
|
+
*/
|
|
512
|
+
private _findStoredKeyConflicts;
|
|
513
|
+
/** Version default + `@db.default.increment` allocation shared by every batch insert. */
|
|
514
|
+
private _prepareInsertBatch;
|
|
488
515
|
findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
|
|
489
516
|
findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
490
517
|
count(query: DbQuery): Promise<number>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as buildMongoFilter, c as mongoFilterStages, i as TMongoFilterPlan, l as planStages, n as TMongoFieldCollation, o as buildMongoQuery, r as TMongoFilterOptions, s as collationOfAdapter, t as REL_FILTER_TEMP_PREFIX } from "./mongo-filter-CGd9ryOD.mjs";
|
|
2
|
-
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
2
|
+
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
3
3
|
import { AggregateOptions, AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
|
|
4
4
|
import { AggregateFn, BucketUnit } from "@uniqu/core";
|
|
5
5
|
import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
|
|
@@ -316,8 +316,10 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
316
316
|
supportsNativePatch(): boolean;
|
|
317
317
|
/** Every unit, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
|
|
318
318
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
319
|
-
/** Every aggregate function
|
|
319
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
320
320
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
321
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
322
|
+
supportsAggregateExpressions(): boolean;
|
|
321
323
|
/** Computed view columns and first-row joins. */
|
|
322
324
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
323
325
|
/**
|
|
@@ -481,10 +483,35 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
481
483
|
* so a unique-index violation is a `CONFLICT` whichever statement hit it.
|
|
482
484
|
*/
|
|
483
485
|
private _wrapDuplicateKeyError;
|
|
486
|
+
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
487
|
+
private _mapConstraintError;
|
|
484
488
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
485
489
|
private _wrapUpdate;
|
|
486
490
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
487
491
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
492
|
+
supportsInsertIgnore(): boolean;
|
|
493
|
+
/**
|
|
494
|
+
* Conflict-ignoring batch insert (the core already removed in-batch duplicates).
|
|
495
|
+
*
|
|
496
|
+
* Outside a transaction: `insertMany(…, { ordered: false })`; duplicate-key
|
|
497
|
+
* write errors (11000) mark the skipped rows, every other write error
|
|
498
|
+
* rethrows after the batch (rows already written stay — as for a plain
|
|
499
|
+
* non-transactional `insertMany`).
|
|
500
|
+
*
|
|
501
|
+
* Inside a transaction a duplicate key would abort the transaction even with
|
|
502
|
+
* `ordered: false`, so the stored keys are looked up first (inside the
|
|
503
|
+
* session), the matches are skipped and the rest is inserted ordered. A
|
|
504
|
+
* residual 11000 (e.g. a collation-equal value) throws `CONFLICT`.
|
|
505
|
+
*/
|
|
506
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
507
|
+
/**
|
|
508
|
+
* Indices of `data` rows whose primary (`_id`) or unique-index key tuple is
|
|
509
|
+
* already stored (rows with a null / missing key component never collide).
|
|
510
|
+
* One `$or` query per chunk covers every key set.
|
|
511
|
+
*/
|
|
512
|
+
private _findStoredKeyConflicts;
|
|
513
|
+
/** Version default + `@db.default.increment` allocation shared by every batch insert. */
|
|
514
|
+
private _prepareInsertBatch;
|
|
488
515
|
findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
|
|
489
516
|
findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
|
|
490
517
|
count(query: DbQuery): Promise<number>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as buildMongoQuery, c as planStages, d as
|
|
2
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, containsRelationFilter, createFailureCollector, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, isAtscriptDbView, isFieldRef, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, tableNameOf, translateQueryTree, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
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) {
|
|
@@ -1802,6 +1802,11 @@ function objectIdFromStorage(value) {
|
|
|
1802
1802
|
* predicates (see {@link MongoAdapter.supportsRelationFilters}).
|
|
1803
1803
|
*/
|
|
1804
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
|
+
}
|
|
1805
1810
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
1806
1811
|
db;
|
|
1807
1812
|
client;
|
|
@@ -1914,17 +1919,28 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1914
1919
|
});
|
|
1915
1920
|
}
|
|
1916
1921
|
async aggregate(query) {
|
|
1917
|
-
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
|
+
};
|
|
1918
1932
|
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1919
1933
|
if (query.controls?.$count) {
|
|
1920
1934
|
const pipeline = buildCountPipeline(query, searchStage, this._predicateFilterOpts);
|
|
1921
1935
|
this._log("aggregate (count)", pipeline);
|
|
1922
|
-
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1923
|
-
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 }];
|
|
1924
1938
|
}
|
|
1925
1939
|
const pipeline = buildAggregatePipeline(query, searchStage, this._predicateFilterOpts);
|
|
1926
1940
|
this._log("aggregate", pipeline);
|
|
1927
|
-
|
|
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;
|
|
1928
1944
|
}
|
|
1929
1945
|
get idType() {
|
|
1930
1946
|
const idProp = this._table.type.type.props.get("_id");
|
|
@@ -1964,10 +1980,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1964
1980
|
calendarBucketUnits() {
|
|
1965
1981
|
return ALL_BUCKET_UNITS;
|
|
1966
1982
|
}
|
|
1967
|
-
/** Every aggregate function
|
|
1983
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
1968
1984
|
aggregateFns() {
|
|
1969
1985
|
return ALL_AGGREGATE_FNS;
|
|
1970
1986
|
}
|
|
1987
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
1988
|
+
supportsAggregateExpressions() {
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1971
1991
|
/** Computed view columns and first-row joins. */
|
|
1972
1992
|
viewCapabilities() {
|
|
1973
1993
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -2425,13 +2445,17 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2425
2445
|
try {
|
|
2426
2446
|
return await fn();
|
|
2427
2447
|
} catch (error) {
|
|
2428
|
-
|
|
2429
|
-
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
2430
|
-
message: error.message
|
|
2431
|
-
}]);
|
|
2432
|
-
throw error;
|
|
2448
|
+
return this._mapConstraintError(error);
|
|
2433
2449
|
}
|
|
2434
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
|
+
}
|
|
2435
2459
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
2436
2460
|
async _wrapUpdate(fn) {
|
|
2437
2461
|
const { matchedCount, modifiedCount } = await this._wrapDuplicateKeyError(fn);
|
|
@@ -2455,6 +2479,98 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2455
2479
|
return { insertedId: objectIdFromStorage(this._resolveInsertedId(data, result.insertedId)) };
|
|
2456
2480
|
}
|
|
2457
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) {
|
|
2458
2574
|
const versionColumn = this._table.versionColumnPhysical;
|
|
2459
2575
|
if (versionColumn !== void 0) {
|
|
2460
2576
|
for (const item of data) if (!(versionColumn in item)) item[versionColumn] = 0;
|
|
@@ -2464,12 +2580,6 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2464
2580
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
2465
2581
|
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
2466
2582
|
}
|
|
2467
|
-
this._log("insertMany", `${data.length} docs`);
|
|
2468
|
-
const result = await this._wrapDuplicateKeyError(() => this.collection.insertMany(data, this._getSessionOpts()));
|
|
2469
|
-
return {
|
|
2470
|
-
insertedCount: result.insertedCount,
|
|
2471
|
-
insertedIds: data.map((item, i) => objectIdFromStorage(this._resolveInsertedId(item, result.insertedIds[i])))
|
|
2472
|
-
};
|
|
2473
2583
|
}
|
|
2474
2584
|
async findOne(query) {
|
|
2475
2585
|
if (containsRelationFilter(query.filter)) {
|
|
@@ -2893,7 +3003,7 @@ function buildMongoUpdateDoc(data, ops, versionColumn) {
|
|
|
2893
3003
|
function createAdapter(connection, _options) {
|
|
2894
3004
|
const client = new MongoClient(connection);
|
|
2895
3005
|
const db = client.db();
|
|
2896
|
-
return new DbSpace(() => new MongoAdapter(db, client));
|
|
3006
|
+
return new DbSpace(() => new MongoAdapter(db, client), { onClose: () => client.close() });
|
|
2897
3007
|
}
|
|
2898
3008
|
//#endregion
|
|
2899
3009
|
export { CollectionPatcher, INDEX_PREFIX, MongoAdapter, REL_FILTER_TEMP_PREFIX, buildMongoFilter, buildMongoQuery, collationOfAdapter, createAdapter, mongoFilterStages, mongoIndexKey, planStages, validateMongoIdPlugin };
|
|
@@ -585,6 +585,12 @@ Object.defineProperty(exports, "mongoFilterStages", {
|
|
|
585
585
|
return mongoFilterStages;
|
|
586
586
|
}
|
|
587
587
|
});
|
|
588
|
+
Object.defineProperty(exports, "notNullExpr", {
|
|
589
|
+
enumerable: true,
|
|
590
|
+
get: function() {
|
|
591
|
+
return notNullExpr;
|
|
592
|
+
}
|
|
593
|
+
});
|
|
588
594
|
Object.defineProperty(exports, "orNull", {
|
|
589
595
|
enumerable: true,
|
|
590
596
|
get: function() {
|
|
@@ -531,4 +531,4 @@ function distinctCountExpr(set) {
|
|
|
531
531
|
return { $size: set };
|
|
532
532
|
}
|
|
533
533
|
//#endregion
|
|
534
|
-
export { buildMongoQuery as a, planStages as c,
|
|
534
|
+
export { buildMongoQuery as a, planStages as c, orNull as d, queryNodeToExpr as f, buildMongoFilter as i, exprToMongo as l, distinctCountExpr as n, collationOfAdapter as o, correlate as p, REL_FILTER_TEMP_PREFIX as r, mongoFilterStages as s, buildAccumulator as t, notNullExpr as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.149",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -46,19 +46,19 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@atscript/core": "^0.1.
|
|
50
|
-
"@atscript/typescript": "^0.1.
|
|
51
|
-
"@uniqu/core": "^0.1.
|
|
49
|
+
"@atscript/core": "^0.1.100",
|
|
50
|
+
"@atscript/typescript": "^0.1.100",
|
|
51
|
+
"@uniqu/core": "^0.1.13",
|
|
52
52
|
"mongodb": "^6.17.0",
|
|
53
53
|
"mongodb-memory-server-core": "^10.0.0",
|
|
54
|
-
"unplugin-atscript": "^0.1.
|
|
54
|
+
"unplugin-atscript": "^0.1.100"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@atscript/core": "^0.1.
|
|
58
|
-
"@atscript/typescript": "^0.1.
|
|
59
|
-
"@uniqu/core": "^0.1.
|
|
57
|
+
"@atscript/core": "^0.1.100",
|
|
58
|
+
"@atscript/typescript": "^0.1.100",
|
|
59
|
+
"@uniqu/core": "^0.1.13",
|
|
60
60
|
"mongodb": "^6.17.0",
|
|
61
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.149"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"postinstall": "asc -f dts",
|