@warlock.js/cascade 5.12.0 → 5.13.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/cjs/index.cjs +685 -128
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/database-driver.contract.d.mts +44 -7
  5. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  6. package/esm/contracts/index.d.mts +2 -2
  7. package/esm/contracts/query-builder.contract.d.mts +61 -1
  8. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  9. package/esm/drivers/mongodb/mongodb-driver.d.mts +8 -4
  10. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-driver.mjs +16 -6
  12. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +32 -7
  14. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-query-builder.mjs +56 -8
  16. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +39 -12
  18. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  19. package/esm/drivers/mongodb/mongodb-query-parser.mjs +143 -55
  20. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  21. package/esm/drivers/mongodb/mongodb-update-translator.mjs +25 -0
  22. package/esm/drivers/mongodb/mongodb-update-translator.mjs.map +1 -0
  23. package/esm/drivers/mongodb/pipeline-stage-object.mjs +24 -0
  24. package/esm/drivers/mongodb/pipeline-stage-object.mjs.map +1 -0
  25. package/esm/drivers/mongodb/types.d.mts +7 -1
  26. package/esm/drivers/mongodb/types.d.mts.map +1 -1
  27. package/esm/drivers/postgres/postgres-driver.d.mts +52 -9
  28. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  29. package/esm/drivers/postgres/postgres-driver.mjs +170 -38
  30. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  31. package/esm/drivers/postgres/postgres-query-builder.d.mts +17 -1
  32. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  33. package/esm/drivers/postgres/postgres-query-builder.mjs +31 -0
  34. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  35. package/esm/drivers/postgres/postgres-update-validator.mjs +36 -0
  36. package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -0
  37. package/esm/errors/unsupported-lean-operation.error.d.mts +25 -0
  38. package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -0
  39. package/esm/errors/unsupported-lean-operation.error.mjs +31 -0
  40. package/esm/errors/unsupported-lean-operation.error.mjs.map +1 -0
  41. package/esm/errors/unsupported-query-operation.error.d.mts +30 -0
  42. package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -0
  43. package/esm/errors/unsupported-query-operation.error.mjs +37 -0
  44. package/esm/errors/unsupported-query-operation.error.mjs.map +1 -0
  45. package/esm/errors/unsupported-update-operation.error.d.mts +30 -0
  46. package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -0
  47. package/esm/errors/unsupported-update-operation.error.mjs +37 -0
  48. package/esm/errors/unsupported-update-operation.error.mjs.map +1 -0
  49. package/esm/index.d.mts +6 -3
  50. package/esm/index.mjs +4 -1
  51. package/esm/model/methods/query-methods.mjs +22 -6
  52. package/esm/model/methods/query-methods.mjs.map +1 -1
  53. package/esm/model/model.d.mts +28 -11
  54. package/esm/model/model.d.mts.map +1 -1
  55. package/esm/model/model.mjs +30 -13
  56. package/esm/model/model.mjs.map +1 -1
  57. package/esm/query-builder/lean-records.mjs +34 -0
  58. package/esm/query-builder/lean-records.mjs.map +1 -0
  59. package/esm/query-builder/query-builder.d.mts +13 -1
  60. package/esm/query-builder/query-builder.d.mts.map +1 -1
  61. package/esm/query-builder/query-builder.mjs +16 -0
  62. package/esm/query-builder/query-builder.mjs.map +1 -1
  63. package/llms-full.txt +72 -5
  64. package/llms.txt +3 -3
  65. package/package.json +4 -4
  66. package/skills/aggregate-data/SKILL.md +24 -1
  67. package/skills/perform-atomic-ops/SKILL.md +38 -3
  68. package/skills/query-data/SKILL.md +10 -1
@@ -71,6 +71,31 @@ declare class MongoQueryParser {
71
71
  * ```
72
72
  */
73
73
  parse(): any[];
74
+ /**
75
+ * Apply a raw escape-hatch operation to the pipeline built so far.
76
+ *
77
+ * - `joinRaw`: appends the caller-built stages verbatim.
78
+ * - `raw`: calls the callback with the pipeline array. A returned array
79
+ * replaces the pipeline; `undefined` keeps the (possibly mutated) array.
80
+ *
81
+ * @throws UnsupportedQueryOperationError when a `raw()` callback leaves
82
+ * something other than an array of stage objects
83
+ */
84
+ private applyRawOperation;
85
+ /**
86
+ * Order a `$sort` against the select `$project` it follows.
87
+ *
88
+ * A `$sort` after a `$project` cannot see fields the projection dropped, so
89
+ * `select(["title"]).orderBy("likes")` would not sort. The sort moves before
90
+ * the projection, unless it sorts by a field the projection computes
91
+ * (`selectRaw({ score: ... }).orderBy("score")`), which only exists after it:
92
+ *
93
+ * - no computed sort key: `$sort`, `$project`
94
+ * - only computed or selected sort keys: `$project`, `$sort` (unchanged)
95
+ * - computed AND unselected sort keys: `$addFields` (the computed keys),
96
+ * `$sort`, `$project` (those keys passed through)
97
+ */
98
+ private placeSortAroundProjection;
74
99
  /**
75
100
  * Reorder operations so filters run before projections, mirroring SQL
76
101
  * semantics: in `select(...).where(...)`, the WHERE always applies to the
@@ -78,11 +103,13 @@ declare class MongoQueryParser {
78
103
  * strips the filter column would run before the `$match` and silently drop
79
104
  * every document (`select(["a"]).where("b", x)` → `[]`).
80
105
  *
81
- * Only *mergeable* `$match` operations are hoisted, and only within a
82
- * segment of neighboring mergeable `$match` / `$project` / `$sort`
83
- * operations. Any other operation — `$group`, `$lookup`, `$limit`, `$skip`,
84
- * `$setWindowFields`, or a non-mergeable op (raw escapes, having-style
85
- * post-group matches, `$sample`) is a barrier: nothing moves across it.
106
+ * Only *mergeable* operations move, and only within a segment of neighboring
107
+ * mergeable `$match` / `$project` / `$sort` operations. Inside a segment the
108
+ * order is `$match`, then `$project`, then `$sort` (each keeping call order),
109
+ * and {@link placeSortAroundProjection} then decides whether the sort runs
110
+ * before the projection. Any other operation `$group`, `$lookup`,
111
+ * `$limit`, `$skip`, `$setWindowFields`, raw escapes, having-style
112
+ * post-group matches, `$sample` — is a barrier: nothing moves across it.
86
113
  * So `groupBy(...).where(...)` still filters AFTER the group, and
87
114
  * `limit(...)` / `random()` keep their call-order meaning.
88
115
  */
@@ -98,15 +125,15 @@ declare class MongoQueryParser {
98
125
  */
99
126
  private trackCountDistinctAliases;
100
127
  /**
101
- * Post-process pipeline to rename _id fields after $group stages.
102
- *
103
- * This automatically renames MongoDB's `_id` field to the actual field name(s)
104
- * used for grouping, making the results more intuitive.
128
+ * Build the `$project` that renames `_id` back to the grouped field name(s)
129
+ * right after a `$group` stage with tracked field names, so results carry
130
+ * the grouping columns instead of MongoDB's `_id`.
105
131
  *
106
- * @param pipeline - The aggregation pipeline
107
- * @returns The processed pipeline
132
+ * @param stage - The stage just emitted
133
+ * @param stageIndex - Its index in the pipeline
134
+ * @returns The renaming `$project` stage, or null when none is needed
108
135
  */
109
- private postProcessGroupStages;
136
+ private buildGroupRenameStage;
110
137
  /**
111
138
  * Convert the parsed pipeline to a pretty-printed string for debugging.
112
139
  *
@@ -1 +1 @@
1
- {"version":3,"file":"mongodb-query-parser.d.mts","names":[],"sources":["../../../../../../../../cascade/src/drivers/mongodb/mongodb-query-parser.ts"],"mappings":";;;;;;;AAgBA;KAAY,uBAAA;6CAEV,UAAA,EAAY,UAAA,EAEA;EAAZ,UAAA,EAAY,SAAA,IAE6B;EAAzC,gBAAA,QAAwB,iBAAA;AAAA;;;;;;;AAAiB;AAW3C;cAAa,gBAAA;;;;mBAIM,UAAA;EAUA;;;EAAA,iBALA,UAAA;EA0BW;;;EAAA,iBArBX,gBAAA;EAsJT;;;;EAAA,iBAhJS,eAAA;EA8WT;;;;;;EAAA,iBAtWS,oBAAA;EA66BT;;;;;cAt6BW,OAAA,EAAS,uBAAuB;EAmgC3C;;;;;;;;;;;;;;;;;;;;EAz+BD,KAAA;EAutCC;;;;;;;;;;;;;;;EAAA,QAjpCA,WAAA;EAqkDA;;;EAAA,QApiDA,oBAAA;EA+sDgB;AAAA;;;;EAAA,QA7qDhB,yBAAA;;;;;;;;;;UAyBA,sBAAA;;;;;;;;;;;;;;;;;;;;;;;;EA4ED,cAAA;;;;;;;;UAmCC,eAAA;;;;;;;;UAoDA,UAAA;;;;;;;;;;;UAiDA,eAAA;EAAA,QA8KA,iBAAA;;;;;;;;;UAkBA,sBAAA;;;;;;;UA8EA,mBAAA;;;;;;;;;;;;;UA+NA,oBAAA;;;;;;;;;UAgBA,sBAAA;;;;;;;UAyBA,gBAAA;EAAA,QAYA,oBAAA;;;;;;;;UA2BA,4BAAA;EAAA,QAsBA,aAAA;EAAA,QAYA,qBAAA;EAAA,QASA,4BAAA;EAAA,QAWA,UAAA;EAAA,QAIA,0BAAA;EAAA,QAOA,wBAAA;EAAA,QAKA,uBAAA;EAAA,QAKA,kBAAA;EAAA,QAgBA,sBAAA;EAAA,QAiBA,0BAAA;EAAA,QAQA,+BAAA;EAAA,QAMA,6BAAA;EAAA,QAMA,wBAAA;EAAA,QASA,sBAAA;EAAA,QAQA,yBAAA;EAAA,QASA,kBAAA;EAAA,QAWA,UAAA;EAAA,QAMA,QAAA;EAAA,QAMA,aAAA;EAAA,QAIA,qBAAA;;;;;;UAeA,qBAAA;EAAA,QAmCA,kBAAA;EAAA,QAeA,2BAAA;EAAA,QA0BA,uBAAA;EAAA,QAoBA,wBAAA;EAAA,QAwBA,qBAAA;EAAA,QAMA,wBAAA;EAAA,QAMA,mBAAA;EAAA,QAeA,mBAAA;;;;;UAkBA,0BAAA;EAAA,QAaA,cAAA;EAAA,QAYA,qBAAA;EAAA,QAMA,uBAAA;;;;;;;UAsBA,iBAAA;;;;;;;UAqHA,cAAA;;;;;;;UA2BA,eAAA;EAAA,QAgEA,iBAAA;;;;;;;;UAgBA,+BAAA;;;;;;;;;;UAoCA,qBAAA;;;;;;;UA0BA,sBAAA;;;;;;;UA4BA,4BAAA;;;;;;;;;;;;;UAuFA,uBAAA;EAAA,QAoCA,YAAA;;;;;;;UAgDA,gBAAA;AAAA"}
1
+ {"version":3,"file":"mongodb-query-parser.d.mts","names":[],"sources":["../../../../../../../../cascade/src/drivers/mongodb/mongodb-query-parser.ts"],"mappings":";;;;;;;AAkBA;KAAY,uBAAA;6CAEV,UAAA,EAAY,UAAA,EAEA;EAAZ,UAAA,EAAY,SAAA,IAE6B;EAAzC,gBAAA,QAAwB,iBAAA;AAAA;;;;;;;AAAiB;AAW3C;cAAa,gBAAA;;;;mBAIM,UAAA;EAUA;;;EAAA,iBALA,UAAA;EA0BW;;;EAAA,iBArBX,gBAAA;EA4KT;;;;EAAA,iBAtKS,eAAA;EAsXV;;;;;;EAAA,iBA9WU,oBAAA;EAg/BT;;;;;cAz+BW,OAAA,EAAS,uBAAuB;EA2lC3C;;;;;;;;;;;;;;;;;;;;EAjkCD,KAAA;EAqxCC;;;;;;;;;;EAAA,QA3rCA,iBAAA;EAm2CA;;;;;;;;;;;;AA6dgB;EA7dhB,QAh0CA,yBAAA;;;;;;;;;;;;;;;;;;UAuDA,WAAA;;;;UAkCA,oBAAA;;;;;;UAkCA,yBAAA;;;;;;;;;;UAyBA,qBAAA;;;;;;;;;;;;;;;;;;;;;;;;EA4DD,cAAA;;;;;;;;UAmCC,eAAA;;;;;;;;UAoDA,UAAA;;;;;;;;;;;UAyEA,eAAA;EAAA,QA8KA,iBAAA;;;;;;;;;UAkBA,sBAAA;;;;;;;UA8EA,mBAAA;;;;;;;;;;;;;UAoNA,oBAAA;;;;;;;;;UAgBA,sBAAA;;;;;;;UAyBA,gBAAA;EAAA,QAYA,oBAAA;;;;;;;;UA2BA,4BAAA;EAAA,QAsBA,aAAA;EAAA,QAYA,qBAAA;EAAA,QASA,4BAAA;EAAA,QAWA,UAAA;EAAA,QAIA,0BAAA;EAAA,QAOA,wBAAA;EAAA,QAKA,uBAAA;EAAA,QAKA,kBAAA;EAAA,QAgBA,sBAAA;EAAA,QAiBA,0BAAA;EAAA,QAQA,+BAAA;EAAA,QAMA,6BAAA;EAAA,QAMA,wBAAA;EAAA,QASA,sBAAA;EAAA,QAQA,yBAAA;EAAA,QASA,kBAAA;EAAA,QAWA,UAAA;EAAA,QAMA,QAAA;EAAA,QAMA,aAAA;EAAA,QAIA,qBAAA;;;;;;UAeA,qBAAA;EAAA,QAmCA,kBAAA;EAAA,QAeA,2BAAA;EAAA,QA0BA,uBAAA;EAAA,QAoBA,wBAAA;EAAA,QAwBA,qBAAA;EAAA,QAMA,wBAAA;EAAA,QAMA,mBAAA;EAAA,QAeA,mBAAA;;;;;UAkBA,0BAAA;EAAA,QAaA,cAAA;EAAA,QAYA,qBAAA;EAAA,QAMA,uBAAA;;;;;;;UAsBA,iBAAA;;;;;;;UAgHA,cAAA;;;;;;;UA2BA,eAAA;EAAA,QA6DA,iBAAA;;;;;;;;UAgBA,+BAAA;;;;;;;;;;UAoCA,qBAAA;;;;;;;UA0BA,sBAAA;;;;;;;UA4BA,4BAAA;;;;;;;;;;;;;UAuFA,uBAAA;EAAA,QAoCA,YAAA;;;;;;;UAgDA,gBAAA;AAAA"}
@@ -1,6 +1,8 @@
1
1
  import { UnsafeRawExpressionError } from "../../errors/unsafe-raw-expression.error.mjs";
2
+ import { UnsupportedQueryOperationError } from "../../errors/unsupported-query-operation.error.mjs";
2
3
  import { isAggregateExpression } from "../../expressions/aggregate-expressions.mjs";
3
4
  import { escapeRegex, resolveLikePattern } from "../../utils/escape-regex.mjs";
5
+ import { isPipelineStageObject } from "./pipeline-stage-object.mjs";
4
6
  import { colors } from "@mongez/copper";
5
7
 
6
8
  //#region ../cascade/src/drivers/mongodb/mongodb-query-parser.ts
@@ -68,42 +70,110 @@ var MongoQueryParser = class {
68
70
  * ```
69
71
  */
70
72
  parse() {
73
+ this.groupFieldNames.clear();
74
+ this.countDistinctAliases.clear();
71
75
  const pipeline = [];
72
76
  let currentStage = null;
73
77
  let currentBuffer = [];
74
- for (const op of this.orderStages(this.operations)) if (op.mergeable && op.stage === currentStage) currentBuffer.push(op);
75
- else {
78
+ let trailingProjectIndex = -1;
79
+ const emit = (stage, operations) => {
80
+ if (stage === "$raw") {
81
+ for (const op of operations) this.applyRawOperation(pipeline, op);
82
+ trailingProjectIndex = -1;
83
+ return;
84
+ }
85
+ const builtStage = this.buildStage(stage, operations);
86
+ if (!builtStage) return;
87
+ if (builtStage.$sort && trailingProjectIndex >= 0 && trailingProjectIndex === pipeline.length - 1) {
88
+ const projectStage = pipeline.pop();
89
+ pipeline.push(...this.placeSortAroundProjection(projectStage, builtStage));
90
+ trailingProjectIndex = -1;
91
+ return;
92
+ }
93
+ const stageIndex = pipeline.length;
94
+ pipeline.push(builtStage);
95
+ trailingProjectIndex = stage === "$project" ? stageIndex : -1;
96
+ this.trackGroupFieldNames(stage, operations, stageIndex);
97
+ const renameStage = this.buildGroupRenameStage(builtStage, stageIndex);
98
+ if (renameStage) pipeline.push(renameStage);
99
+ };
100
+ for (const op of this.orderStages(this.operations)) {
101
+ if (op.mergeable && op.stage === currentStage) {
102
+ currentBuffer.push(op);
103
+ continue;
104
+ }
76
105
  if (currentBuffer.length > 0) {
77
- const builtStage = this.buildStage(currentStage, currentBuffer);
78
- if (builtStage) {
79
- const stageIndex = pipeline.length;
80
- pipeline.push(builtStage);
81
- this.trackGroupFieldNames(currentStage, currentBuffer, stageIndex);
82
- }
106
+ emit(currentStage, currentBuffer);
83
107
  currentBuffer = [];
84
108
  }
85
109
  if (op.mergeable) {
86
110
  currentStage = op.stage;
87
111
  currentBuffer.push(op);
88
112
  } else {
89
- const builtStage = this.buildStage(op.stage, [op]);
90
- if (builtStage) {
91
- const stageIndex = pipeline.length;
92
- pipeline.push(builtStage);
93
- this.trackGroupFieldNames(op.stage, [op], stageIndex);
94
- }
113
+ emit(op.stage, [op]);
95
114
  currentStage = null;
96
115
  }
97
116
  }
98
- if (currentBuffer.length > 0) {
99
- const builtStage = this.buildStage(currentStage, currentBuffer);
100
- if (builtStage) {
101
- const stageIndex = pipeline.length;
102
- pipeline.push(builtStage);
103
- this.trackGroupFieldNames(currentStage, currentBuffer, stageIndex);
104
- }
117
+ if (currentBuffer.length > 0) emit(currentStage, currentBuffer);
118
+ return pipeline;
119
+ }
120
+ /**
121
+ * Apply a raw escape-hatch operation to the pipeline built so far.
122
+ *
123
+ * - `joinRaw`: appends the caller-built stages verbatim.
124
+ * - `raw`: calls the callback with the pipeline array. A returned array
125
+ * replaces the pipeline; `undefined` keeps the (possibly mutated) array.
126
+ *
127
+ * @throws UnsupportedQueryOperationError when a `raw()` callback leaves
128
+ * something other than an array of stage objects
129
+ */
130
+ applyRawOperation(pipeline, op) {
131
+ if (op.type === "joinRaw") {
132
+ pipeline.push(...op.data.stages);
133
+ return;
105
134
  }
106
- return this.postProcessGroupStages(pipeline);
135
+ const result = op.data.builder(pipeline);
136
+ const next = result === void 0 ? pipeline : result;
137
+ if (!Array.isArray(next) || !next.every(isPipelineStageObject)) throw new UnsupportedQueryOperationError("raw", "mongodb", "The raw() callback receives the aggregation pipeline array and must return an array of stage objects, or mutate that array and return nothing.");
138
+ if (next !== pipeline) pipeline.splice(0, pipeline.length, ...next);
139
+ }
140
+ /**
141
+ * Order a `$sort` against the select `$project` it follows.
142
+ *
143
+ * A `$sort` after a `$project` cannot see fields the projection dropped, so
144
+ * `select(["title"]).orderBy("likes")` would not sort. The sort moves before
145
+ * the projection, unless it sorts by a field the projection computes
146
+ * (`selectRaw({ score: ... }).orderBy("score")`), which only exists after it:
147
+ *
148
+ * - no computed sort key: `$sort`, `$project`
149
+ * - only computed or selected sort keys: `$project`, `$sort` (unchanged)
150
+ * - computed AND unselected sort keys: `$addFields` (the computed keys),
151
+ * `$sort`, `$project` (those keys passed through)
152
+ */
153
+ placeSortAroundProjection(projectStage, sortStage) {
154
+ const projection = projectStage.$project;
155
+ const sortKeys = Object.keys(sortStage.$sort);
156
+ const isInclusionFlag = (value) => value === 1 || value === true || value === 0 || value === false;
157
+ const computedKeys = sortKeys.filter((key) => {
158
+ const value = projection[key];
159
+ return value !== void 0 && !isInclusionFlag(value) && value !== `$${key}`;
160
+ });
161
+ if (computedKeys.length === 0) return [sortStage, projectStage];
162
+ if (!sortKeys.some((key) => {
163
+ const value = projection[key];
164
+ return value === void 0 || value === 0 || value === false;
165
+ })) return [projectStage, sortStage];
166
+ const addFields = {};
167
+ const passThrough = { ...projection };
168
+ for (const key of computedKeys) {
169
+ addFields[key] = projection[key];
170
+ passThrough[key] = 1;
171
+ }
172
+ return [
173
+ { $addFields: addFields },
174
+ sortStage,
175
+ { $project: passThrough }
176
+ ];
107
177
  }
108
178
  /**
109
179
  * Reorder operations so filters run before projections, mirroring SQL
@@ -112,11 +182,13 @@ var MongoQueryParser = class {
112
182
  * strips the filter column would run before the `$match` and silently drop
113
183
  * every document (`select(["a"]).where("b", x)` → `[]`).
114
184
  *
115
- * Only *mergeable* `$match` operations are hoisted, and only within a
116
- * segment of neighboring mergeable `$match` / `$project` / `$sort`
117
- * operations. Any other operation — `$group`, `$lookup`, `$limit`, `$skip`,
118
- * `$setWindowFields`, or a non-mergeable op (raw escapes, having-style
119
- * post-group matches, `$sample`) is a barrier: nothing moves across it.
185
+ * Only *mergeable* operations move, and only within a segment of neighboring
186
+ * mergeable `$match` / `$project` / `$sort` operations. Inside a segment the
187
+ * order is `$match`, then `$project`, then `$sort` (each keeping call order),
188
+ * and {@link placeSortAroundProjection} then decides whether the sort runs
189
+ * before the projection. Any other operation `$group`, `$lookup`,
190
+ * `$limit`, `$skip`, `$setWindowFields`, raw escapes, having-style
191
+ * post-group matches, `$sample` — is a barrier: nothing moves across it.
120
192
  * So `groupBy(...).where(...)` still filters AFTER the group, and
121
193
  * `limit(...)` / `random()` keep their call-order meaning.
122
194
  */
@@ -126,7 +198,8 @@ var MongoQueryParser = class {
126
198
  const flushSegment = () => {
127
199
  if (segment.length === 0) return;
128
200
  reordered.push(...segment.filter((op) => op.stage === "$match"));
129
- reordered.push(...segment.filter((op) => op.stage !== "$match"));
201
+ reordered.push(...segment.filter((op) => op.stage === "$project"));
202
+ reordered.push(...segment.filter((op) => op.stage === "$sort"));
130
203
  segment = [];
131
204
  };
132
205
  for (const op of operations) if (op.mergeable && (op.stage === "$match" || op.stage === "$project" || op.stage === "$sort")) segment.push(op);
@@ -165,35 +238,26 @@ var MongoQueryParser = class {
165
238
  if (distinctAliases.size > 0) this.countDistinctAliases.set(stageIndex, distinctAliases);
166
239
  }
167
240
  /**
168
- * Post-process pipeline to rename _id fields after $group stages.
169
- *
170
- * This automatically renames MongoDB's `_id` field to the actual field name(s)
171
- * used for grouping, making the results more intuitive.
241
+ * Build the `$project` that renames `_id` back to the grouped field name(s)
242
+ * right after a `$group` stage with tracked field names, so results carry
243
+ * the grouping columns instead of MongoDB's `_id`.
172
244
  *
173
- * @param pipeline - The aggregation pipeline
174
- * @returns The processed pipeline
245
+ * @param stage - The stage just emitted
246
+ * @param stageIndex - Its index in the pipeline
247
+ * @returns The renaming `$project` stage, or null when none is needed
175
248
  */
176
- postProcessGroupStages(pipeline) {
177
- const processed = [];
178
- for (let i = 0; i < pipeline.length; i++) {
179
- const stage = pipeline[i];
180
- if (stage.$group && this.groupFieldNames.has(i)) {
181
- const fieldNames = this.groupFieldNames.get(i);
182
- processed.push(stage);
183
- const projection = {};
184
- if (typeof fieldNames === "string") projection[fieldNames] = "$_id";
185
- else if (Array.isArray(fieldNames) && fieldNames.length > 0) for (const fieldName of fieldNames) projection[fieldName] = `$_id.${fieldName}`;
186
- const distinctAliases = this.countDistinctAliases.get(i);
187
- const aggregateFields = Object.keys(stage.$group).filter((key) => key !== "_id");
188
- for (const field of aggregateFields) if (distinctAliases?.has(field)) projection[field] = { $size: `$${field}` };
189
- else projection[field] = 1;
190
- if (Object.keys(projection).length > 0) {
191
- projection._id = 0;
192
- processed.push({ $project: projection });
193
- }
194
- } else processed.push(stage);
195
- }
196
- return processed;
249
+ buildGroupRenameStage(stage, stageIndex) {
250
+ if (!stage.$group || !this.groupFieldNames.has(stageIndex)) return null;
251
+ const fieldNames = this.groupFieldNames.get(stageIndex);
252
+ const projection = {};
253
+ if (typeof fieldNames === "string") projection[fieldNames] = "$_id";
254
+ else if (Array.isArray(fieldNames) && fieldNames.length > 0) for (const fieldName of fieldNames) projection[fieldName] = `$_id.${fieldName}`;
255
+ const distinctAliases = this.countDistinctAliases.get(stageIndex);
256
+ const aggregateFields = Object.keys(stage.$group).filter((key) => key !== "_id");
257
+ for (const field of aggregateFields) projection[field] = distinctAliases?.has(field) ? { $size: `$${field}` } : 1;
258
+ if (Object.keys(projection).length === 0) return null;
259
+ projection._id = 0;
260
+ return { $project: projection };
197
261
  }
198
262
  /**
199
263
  * Convert the parsed pipeline to a pretty-printed string for debugging.
@@ -293,6 +357,23 @@ var MongoQueryParser = class {
293
357
  const [first] = operations;
294
358
  return first === void 0 ? null : { $setWindowFields: first.data.spec };
295
359
  }
360
+ case "$unwind": {
361
+ const [first] = operations;
362
+ if (first === void 0) return null;
363
+ const { path, ...options } = first.data;
364
+ return Object.keys(options).length === 0 ? { $unwind: path } : { $unwind: {
365
+ path,
366
+ ...options
367
+ } };
368
+ }
369
+ case "$addFields": {
370
+ const [first] = operations;
371
+ return first === void 0 ? null : { $addFields: first.data };
372
+ }
373
+ case "$vectorSearch": {
374
+ const [first] = operations;
375
+ return first === void 0 ? null : { $vectorSearch: first.data };
376
+ }
296
377
  default: return null;
297
378
  }
298
379
  }
@@ -1109,6 +1190,13 @@ var MongoQueryParser = class {
1109
1190
  const op = operations[0];
1110
1191
  if (op === void 0) return null;
1111
1192
  const options = op.data;
1193
+ if (Array.isArray(options.pipeline)) return { $lookup: {
1194
+ from: options.table,
1195
+ ...options.localField !== void 0 ? { localField: options.localField } : {},
1196
+ ...options.foreignField !== void 0 ? { foreignField: options.foreignField } : {},
1197
+ as: options.alias || options.table,
1198
+ pipeline: options.pipeline
1199
+ } };
1112
1200
  return { $lookup: {
1113
1201
  from: options.table,
1114
1202
  localField: options.localField,