@atscript/db-mongo 0.1.152 → 0.1.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,40 +1,7 @@
1
1
  let _atscript_db = require("@atscript/db");
2
2
  let mongodb = require("mongodb");
3
+ let _uniqu_core = require("@uniqu/core");
3
4
  let _atscript_db_agg = require("@atscript/db/agg");
4
- //#region src/lib/lookup-join.ts
5
- /**
6
- * The correlation of a pipeline `$lookup`: `let` binds each outer field to a
7
- * variable (`<prefix>k<i>`), and the first sub-pipeline stages match the
8
- * inner field against it with SQL `=` semantics — a NULL or missing key on
9
- * either side never relates (aggregation `$eq(null, null)` is true, and a
10
- * missing `let` field would equal a missing inner field):
11
- *
12
- * 1. `$match: { $expr: { $eq: [inner, $$var] } }` (an `$and` of `$eq`s for a
13
- * composite key) — kept bare so the lookup can use an index on the inner
14
- * field(s); anything else inside that `$expr` turns every lookup into a
15
- * collection scan;
16
- * 2. `$match: { <inner>: { $ne: null } }` per key part — the NULL guard, as a
17
- * query-level stage. A NULL / missing outer key (`$ifNull` folds "missing"
18
- * into `null`) only `$eq`s inner NULL / missing values, which this stage
19
- * drops.
20
- */
21
- function correlate(prefix, pairs) {
22
- if (pairs.length === 0) throw new Error("A $lookup correlation needs at least one key pair");
23
- const vars = {};
24
- const eqs = [];
25
- const guard = {};
26
- for (const [i, pair] of pairs.entries()) {
27
- const name = `${prefix}k${i}`;
28
- vars[name] = { $ifNull: [`$${pair.outer}`, null] };
29
- eqs.push({ $eq: [`$${pair.inner}`, `$$${name}`] });
30
- guard[pair.inner] = { $ne: null };
31
- }
32
- return {
33
- let: vars,
34
- stages: [{ $match: { $expr: eqs.length === 1 ? eqs[0] : { $and: eqs } } }, { $match: guard }]
35
- };
36
- }
37
- //#endregion
38
5
  //#region src/lib/mongo-view-expr.ts
39
6
  /**
40
7
  * `x IS NOT NULL` in SQL terms — true when `x` is neither null nor missing.
@@ -161,6 +128,123 @@ function exprToMongo(node, operand) {
161
128
  }
162
129
  }
163
130
  //#endregion
131
+ //#region src/lib/mongo-sort.ts
132
+ /**
133
+ * Prefix of the temporary fields a NULL-placement sort adds (one per placed
134
+ * key: `__atscript_nulls_0`, `__atscript_nulls_1`, …). Reserved — a stored
135
+ * field never carries it — and dropped before any row leaves the pipeline.
136
+ *
137
+ * @since 0.1.153
138
+ */
139
+ const NULLS_FLAG_PREFIX = "__atscript_nulls_";
140
+ /**
141
+ * Whether `controls` places NULL for at least one of its `$sort` keys
142
+ * differently from BSON order (null and missing smallest: `'first'`
143
+ * ascending and `'last'` descending need no flag — {@link placedKey}).
144
+ */
145
+ function hasNullsPlacement(controls) {
146
+ const { $sort: sort, $nulls: nulls } = controls ?? {};
147
+ if (!sort || !nulls) return false;
148
+ for (const [key, dir] of Object.entries(sort)) if (placedKey(nulls[key], dir)) return true;
149
+ return false;
150
+ }
151
+ /**
152
+ * The placement of one `$sort` key when it differs from BSON order, else
153
+ * `undefined` — a native placement keeps the plain, index-served sort.
154
+ */
155
+ function placedKey(placement, dir) {
156
+ if (!(0, _uniqu_core.isNullsPlacement)(placement)) return void 0;
157
+ return placement === "first" === (dir !== -1) ? void 0 : placement;
158
+ }
159
+ /**
160
+ * The stages ordering rows by `$sort` with the `$nulls` placement of its keys.
161
+ *
162
+ * Without a placement entry that differs from BSON order (null and missing
163
+ * are the smallest values) this is exactly `[{ $sort }]`. Otherwise each
164
+ * placed key `k` gets a flag — `true` when `k` is null or missing — added before the
165
+ * sort and ordered right before `k`: descending for `'first'`, ascending for
166
+ * `'last'`, whatever `k`'s own direction. `cleanup` drops the flags again;
167
+ * the caller emits it after `$skip` / `$limit` and before any projection.
168
+ *
169
+ * `find().sort()` cannot express this — a placed sort runs as a pipeline,
170
+ * and it is a blocking sort (no index serves the computed flag).
171
+ */
172
+ function sortStages(controls) {
173
+ const { $sort: sort, $nulls: nulls } = controls ?? {};
174
+ if (!sort) return { stages: [] };
175
+ if (!hasNullsPlacement(controls)) return { stages: [{ $sort: sort }] };
176
+ const flags = {};
177
+ const drop = {};
178
+ const ordered = {};
179
+ let n = 0;
180
+ for (const [key, dir] of Object.entries(sort)) {
181
+ const placement = placedKey(nulls[key], dir);
182
+ if (placement) {
183
+ const flag = `${NULLS_FLAG_PREFIX}${n++}`;
184
+ flags[flag] = isNullExpr(`$${key}`);
185
+ drop[flag] = 0;
186
+ ordered[flag] = placement === "first" ? -1 : 1;
187
+ }
188
+ ordered[key] = dir;
189
+ }
190
+ return {
191
+ stages: [{ $addFields: flags }, { $sort: ordered }],
192
+ cleanup: { $project: drop }
193
+ };
194
+ }
195
+ /**
196
+ * The `$sort` of a `first()` / `last()` row order (`TRowOrderKey[]`), with
197
+ * the flags its `nulls` entries need. Rows are grouped right after, so the
198
+ * flags never reach a row — `$last` reads the same forward order, so a
199
+ * placement holds for both ends.
200
+ */
201
+ function rowOrderStages(rowOrder) {
202
+ const sort = {};
203
+ const nulls = {};
204
+ for (const k of rowOrder) {
205
+ sort[k.column] = k.desc ? -1 : 1;
206
+ if (k.nulls) nulls[k.column] = k.nulls;
207
+ }
208
+ return sortStages({
209
+ $sort: sort,
210
+ $nulls: nulls
211
+ }).stages;
212
+ }
213
+ //#endregion
214
+ //#region src/lib/lookup-join.ts
215
+ /**
216
+ * The correlation of a pipeline `$lookup`: `let` binds each outer field to a
217
+ * variable (`<prefix>k<i>`), and the first sub-pipeline stages match the
218
+ * inner field against it with SQL `=` semantics — a NULL or missing key on
219
+ * either side never relates (aggregation `$eq(null, null)` is true, and a
220
+ * missing `let` field would equal a missing inner field):
221
+ *
222
+ * 1. `$match: { $expr: { $eq: [inner, $$var] } }` (an `$and` of `$eq`s for a
223
+ * composite key) — kept bare so the lookup can use an index on the inner
224
+ * field(s); anything else inside that `$expr` turns every lookup into a
225
+ * collection scan;
226
+ * 2. `$match: { <inner>: { $ne: null } }` per key part — the NULL guard, as a
227
+ * query-level stage. A NULL / missing outer key (`$ifNull` folds "missing"
228
+ * into `null`) only `$eq`s inner NULL / missing values, which this stage
229
+ * drops.
230
+ */
231
+ function correlate(prefix, pairs) {
232
+ if (pairs.length === 0) throw new Error("A $lookup correlation needs at least one key pair");
233
+ const vars = {};
234
+ const eqs = [];
235
+ const guard = {};
236
+ for (const [i, pair] of pairs.entries()) {
237
+ const name = `${prefix}k${i}`;
238
+ vars[name] = { $ifNull: [`$${pair.outer}`, null] };
239
+ eqs.push({ $eq: [`$${pair.inner}`, `$$${name}`] });
240
+ guard[pair.inner] = { $ne: null };
241
+ }
242
+ return {
243
+ let: vars,
244
+ stages: [{ $match: { $expr: eqs.length === 1 ? eqs[0] : { $and: eqs } } }, { $match: guard }]
245
+ };
246
+ }
247
+ //#endregion
164
248
  //#region src/lib/mongo-filter.ts
165
249
  const EMPTY = {};
166
250
  function parseRegexString(value) {
@@ -594,6 +678,12 @@ Object.defineProperty(exports, "exprToMongo", {
594
678
  return exprToMongo;
595
679
  }
596
680
  });
681
+ Object.defineProperty(exports, "hasNullsPlacement", {
682
+ enumerable: true,
683
+ get: function() {
684
+ return hasNullsPlacement;
685
+ }
686
+ });
597
687
  Object.defineProperty(exports, "mongoFilterStages", {
598
688
  enumerable: true,
599
689
  get: function() {
@@ -624,3 +714,15 @@ Object.defineProperty(exports, "queryNodeToExpr", {
624
714
  return queryNodeToExpr;
625
715
  }
626
716
  });
717
+ Object.defineProperty(exports, "rowOrderStages", {
718
+ enumerable: true,
719
+ get: function() {
720
+ return rowOrderStages;
721
+ }
722
+ });
723
+ Object.defineProperty(exports, "sortStages", {
724
+ enumerable: true,
725
+ get: function() {
726
+ return sortStages;
727
+ }
728
+ });
@@ -1,40 +1,7 @@
1
1
  import { DbError, INTEGER_REGEX_OP, andFilters, containsRelationFilter, isFieldRef, isResolvedRelationFilter, walkFilter } from "@atscript/db";
2
2
  import { BSONRegExp } from "mongodb";
3
+ import { isNullsPlacement } from "@uniqu/core";
3
4
  import { assertAggregateFn } from "@atscript/db/agg";
4
- //#region src/lib/lookup-join.ts
5
- /**
6
- * The correlation of a pipeline `$lookup`: `let` binds each outer field to a
7
- * variable (`<prefix>k<i>`), and the first sub-pipeline stages match the
8
- * inner field against it with SQL `=` semantics — a NULL or missing key on
9
- * either side never relates (aggregation `$eq(null, null)` is true, and a
10
- * missing `let` field would equal a missing inner field):
11
- *
12
- * 1. `$match: { $expr: { $eq: [inner, $$var] } }` (an `$and` of `$eq`s for a
13
- * composite key) — kept bare so the lookup can use an index on the inner
14
- * field(s); anything else inside that `$expr` turns every lookup into a
15
- * collection scan;
16
- * 2. `$match: { <inner>: { $ne: null } }` per key part — the NULL guard, as a
17
- * query-level stage. A NULL / missing outer key (`$ifNull` folds "missing"
18
- * into `null`) only `$eq`s inner NULL / missing values, which this stage
19
- * drops.
20
- */
21
- function correlate(prefix, pairs) {
22
- if (pairs.length === 0) throw new Error("A $lookup correlation needs at least one key pair");
23
- const vars = {};
24
- const eqs = [];
25
- const guard = {};
26
- for (const [i, pair] of pairs.entries()) {
27
- const name = `${prefix}k${i}`;
28
- vars[name] = { $ifNull: [`$${pair.outer}`, null] };
29
- eqs.push({ $eq: [`$${pair.inner}`, `$$${name}`] });
30
- guard[pair.inner] = { $ne: null };
31
- }
32
- return {
33
- let: vars,
34
- stages: [{ $match: { $expr: eqs.length === 1 ? eqs[0] : { $and: eqs } } }, { $match: guard }]
35
- };
36
- }
37
- //#endregion
38
5
  //#region src/lib/mongo-view-expr.ts
39
6
  /**
40
7
  * `x IS NOT NULL` in SQL terms — true when `x` is neither null nor missing.
@@ -161,6 +128,123 @@ function exprToMongo(node, operand) {
161
128
  }
162
129
  }
163
130
  //#endregion
131
+ //#region src/lib/mongo-sort.ts
132
+ /**
133
+ * Prefix of the temporary fields a NULL-placement sort adds (one per placed
134
+ * key: `__atscript_nulls_0`, `__atscript_nulls_1`, …). Reserved — a stored
135
+ * field never carries it — and dropped before any row leaves the pipeline.
136
+ *
137
+ * @since 0.1.153
138
+ */
139
+ const NULLS_FLAG_PREFIX = "__atscript_nulls_";
140
+ /**
141
+ * Whether `controls` places NULL for at least one of its `$sort` keys
142
+ * differently from BSON order (null and missing smallest: `'first'`
143
+ * ascending and `'last'` descending need no flag — {@link placedKey}).
144
+ */
145
+ function hasNullsPlacement(controls) {
146
+ const { $sort: sort, $nulls: nulls } = controls ?? {};
147
+ if (!sort || !nulls) return false;
148
+ for (const [key, dir] of Object.entries(sort)) if (placedKey(nulls[key], dir)) return true;
149
+ return false;
150
+ }
151
+ /**
152
+ * The placement of one `$sort` key when it differs from BSON order, else
153
+ * `undefined` — a native placement keeps the plain, index-served sort.
154
+ */
155
+ function placedKey(placement, dir) {
156
+ if (!isNullsPlacement(placement)) return void 0;
157
+ return placement === "first" === (dir !== -1) ? void 0 : placement;
158
+ }
159
+ /**
160
+ * The stages ordering rows by `$sort` with the `$nulls` placement of its keys.
161
+ *
162
+ * Without a placement entry that differs from BSON order (null and missing
163
+ * are the smallest values) this is exactly `[{ $sort }]`. Otherwise each
164
+ * placed key `k` gets a flag — `true` when `k` is null or missing — added before the
165
+ * sort and ordered right before `k`: descending for `'first'`, ascending for
166
+ * `'last'`, whatever `k`'s own direction. `cleanup` drops the flags again;
167
+ * the caller emits it after `$skip` / `$limit` and before any projection.
168
+ *
169
+ * `find().sort()` cannot express this — a placed sort runs as a pipeline,
170
+ * and it is a blocking sort (no index serves the computed flag).
171
+ */
172
+ function sortStages(controls) {
173
+ const { $sort: sort, $nulls: nulls } = controls ?? {};
174
+ if (!sort) return { stages: [] };
175
+ if (!hasNullsPlacement(controls)) return { stages: [{ $sort: sort }] };
176
+ const flags = {};
177
+ const drop = {};
178
+ const ordered = {};
179
+ let n = 0;
180
+ for (const [key, dir] of Object.entries(sort)) {
181
+ const placement = placedKey(nulls[key], dir);
182
+ if (placement) {
183
+ const flag = `${NULLS_FLAG_PREFIX}${n++}`;
184
+ flags[flag] = isNullExpr(`$${key}`);
185
+ drop[flag] = 0;
186
+ ordered[flag] = placement === "first" ? -1 : 1;
187
+ }
188
+ ordered[key] = dir;
189
+ }
190
+ return {
191
+ stages: [{ $addFields: flags }, { $sort: ordered }],
192
+ cleanup: { $project: drop }
193
+ };
194
+ }
195
+ /**
196
+ * The `$sort` of a `first()` / `last()` row order (`TRowOrderKey[]`), with
197
+ * the flags its `nulls` entries need. Rows are grouped right after, so the
198
+ * flags never reach a row — `$last` reads the same forward order, so a
199
+ * placement holds for both ends.
200
+ */
201
+ function rowOrderStages(rowOrder) {
202
+ const sort = {};
203
+ const nulls = {};
204
+ for (const k of rowOrder) {
205
+ sort[k.column] = k.desc ? -1 : 1;
206
+ if (k.nulls) nulls[k.column] = k.nulls;
207
+ }
208
+ return sortStages({
209
+ $sort: sort,
210
+ $nulls: nulls
211
+ }).stages;
212
+ }
213
+ //#endregion
214
+ //#region src/lib/lookup-join.ts
215
+ /**
216
+ * The correlation of a pipeline `$lookup`: `let` binds each outer field to a
217
+ * variable (`<prefix>k<i>`), and the first sub-pipeline stages match the
218
+ * inner field against it with SQL `=` semantics — a NULL or missing key on
219
+ * either side never relates (aggregation `$eq(null, null)` is true, and a
220
+ * missing `let` field would equal a missing inner field):
221
+ *
222
+ * 1. `$match: { $expr: { $eq: [inner, $$var] } }` (an `$and` of `$eq`s for a
223
+ * composite key) — kept bare so the lookup can use an index on the inner
224
+ * field(s); anything else inside that `$expr` turns every lookup into a
225
+ * collection scan;
226
+ * 2. `$match: { <inner>: { $ne: null } }` per key part — the NULL guard, as a
227
+ * query-level stage. A NULL / missing outer key (`$ifNull` folds "missing"
228
+ * into `null`) only `$eq`s inner NULL / missing values, which this stage
229
+ * drops.
230
+ */
231
+ function correlate(prefix, pairs) {
232
+ if (pairs.length === 0) throw new Error("A $lookup correlation needs at least one key pair");
233
+ const vars = {};
234
+ const eqs = [];
235
+ const guard = {};
236
+ for (const [i, pair] of pairs.entries()) {
237
+ const name = `${prefix}k${i}`;
238
+ vars[name] = { $ifNull: [`$${pair.outer}`, null] };
239
+ eqs.push({ $eq: [`$${pair.inner}`, `$$${name}`] });
240
+ guard[pair.inner] = { $ne: null };
241
+ }
242
+ return {
243
+ let: vars,
244
+ stages: [{ $match: { $expr: eqs.length === 1 ? eqs[0] : { $and: eqs } } }, { $match: guard }]
245
+ };
246
+ }
247
+ //#endregion
164
248
  //#region src/lib/mongo-filter.ts
165
249
  const EMPTY = {};
166
250
  function parseRegexString(value) {
@@ -546,4 +630,4 @@ function distinctCountExpr(set) {
546
630
  return { $size: set };
547
631
  }
548
632
  //#endregion
549
- 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 };
633
+ export { buildMongoQuery as a, planStages as c, rowOrderStages as d, sortStages as f, queryNodeToExpr as g, orNull as h, buildMongoFilter as i, correlate as l, notNullExpr as m, distinctCountExpr as n, collationOfAdapter as o, exprToMongo as p, REL_FILTER_TEMP_PREFIX as r, mongoFilterStages as s, buildAccumulator as t, hasNullsPlacement as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.152",
3
+ "version": "0.1.153",
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.102",
50
- "@atscript/typescript": "^0.1.102",
51
- "@uniqu/core": "^0.1.14",
49
+ "@atscript/core": "^0.1.103",
50
+ "@atscript/typescript": "^0.1.103",
51
+ "@uniqu/core": "^0.1.15",
52
52
  "mongodb": "^6.17.0",
53
53
  "mongodb-memory-server-core": "^10.0.0",
54
- "unplugin-atscript": "^0.1.102"
54
+ "unplugin-atscript": "^0.1.103"
55
55
  },
56
56
  "peerDependencies": {
57
- "@atscript/core": "^0.1.102",
58
- "@atscript/typescript": "^0.1.102",
59
- "@uniqu/core": "^0.1.14",
57
+ "@atscript/core": "^0.1.103",
58
+ "@atscript/typescript": "^0.1.103",
59
+ "@uniqu/core": "^0.1.15",
60
60
  "mongodb": "^6.17.0",
61
- "@atscript/db": "^0.1.152"
61
+ "@atscript/db": "^0.1.153"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",