@prisma-next/mongo-query-ast 0.0.1
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/LICENSE +201 -0
- package/README.md +22 -0
- package/package.json +45 -0
- package/src/aggregation-expressions.ts +482 -0
- package/src/ast-node.ts +7 -0
- package/src/commands.ts +148 -0
- package/src/exports/index.ts +97 -0
- package/src/filter-expressions.ts +210 -0
- package/src/query-plan.ts +11 -0
- package/src/raw-commands.ts +143 -0
- package/src/stages.ts +1024 -0
- package/src/visitors.ts +135 -0
package/src/stages.ts
ADDED
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import type { MongoAggAccumulator, MongoAggExpr } from './aggregation-expressions';
|
|
2
|
+
import { MongoAstNode } from './ast-node';
|
|
3
|
+
import type { MongoFilterExpr } from './filter-expressions';
|
|
4
|
+
import type {
|
|
5
|
+
MongoAggExprRewriter,
|
|
6
|
+
MongoStageRewriterContext,
|
|
7
|
+
MongoStageVisitor,
|
|
8
|
+
} from './visitors';
|
|
9
|
+
|
|
10
|
+
export type MongoGroupId = null | MongoAggExpr | Readonly<Record<string, MongoAggExpr>>;
|
|
11
|
+
export type MongoProjectionValue = 0 | 1 | MongoAggExpr;
|
|
12
|
+
|
|
13
|
+
// Structural guard: MongoAggExpr nodes always carry a `kind` string discriminant,
|
|
14
|
+
// while scalar projection values (0 | 1) are numbers. This convention holds for all
|
|
15
|
+
// current AST node types. If non-node objects with `kind` are introduced in the future,
|
|
16
|
+
// consider a shared branded isAggExprNode() guard.
|
|
17
|
+
function isAggExpr(value: MongoProjectionValue): value is MongoAggExpr {
|
|
18
|
+
return typeof value === 'object' && value !== null && 'kind' in value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Discriminate MongoAggExpr from Record<string, MongoAggExpr> via the accept() method
|
|
22
|
+
// that all AST nodes inherit from MongoAstNode. A plain record won't have accept(),
|
|
23
|
+
// so this is robust even if a compound _id contains a key named "kind".
|
|
24
|
+
function isAggExprNode(value: object): value is MongoAggExpr {
|
|
25
|
+
return 'accept' in value && typeof value.accept === 'function';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rewriteGroupId(groupId: MongoGroupId, rewriter: MongoAggExprRewriter): MongoGroupId {
|
|
29
|
+
if (groupId === null) return null;
|
|
30
|
+
if (isAggExprNode(groupId)) return groupId.rewrite(rewriter);
|
|
31
|
+
const result: Record<string, MongoAggExpr> = {};
|
|
32
|
+
for (const [key, val] of Object.entries(groupId)) {
|
|
33
|
+
result[key] = val.rewrite(rewriter);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rewriteExprRecord(
|
|
39
|
+
fields: Readonly<Record<string, MongoAggExpr>>,
|
|
40
|
+
rewriter: MongoAggExprRewriter,
|
|
41
|
+
): Record<string, MongoAggExpr> {
|
|
42
|
+
const result: Record<string, MongoAggExpr> = {};
|
|
43
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
44
|
+
result[key] = val.rewrite(rewriter);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rewriteAccumulatorRecord(
|
|
50
|
+
accumulators: Readonly<Record<string, MongoAggAccumulator>>,
|
|
51
|
+
rewriter: MongoAggExprRewriter,
|
|
52
|
+
): Record<string, MongoAggAccumulator> {
|
|
53
|
+
const result: Record<string, MongoAggAccumulator> = {};
|
|
54
|
+
for (const [key, acc] of Object.entries(accumulators)) {
|
|
55
|
+
result[key] = acc.rewrite(rewriter) as MongoAggAccumulator;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
abstract class MongoStageNode extends MongoAstNode {
|
|
61
|
+
abstract accept<R>(visitor: MongoStageVisitor<R>): R;
|
|
62
|
+
abstract rewrite(context: MongoStageRewriterContext): MongoPipelineStage;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class MongoMatchStage extends MongoStageNode {
|
|
66
|
+
readonly kind = 'match' as const;
|
|
67
|
+
readonly filter: MongoFilterExpr;
|
|
68
|
+
|
|
69
|
+
constructor(filter: MongoFilterExpr) {
|
|
70
|
+
super();
|
|
71
|
+
this.filter = filter;
|
|
72
|
+
this.freeze();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
76
|
+
return visitor.match(this);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
80
|
+
return new MongoMatchStage(this.filter.rewrite(context.filter ?? {}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class MongoProjectStage extends MongoStageNode {
|
|
85
|
+
readonly kind = 'project' as const;
|
|
86
|
+
readonly projection: Readonly<Record<string, MongoProjectionValue>>;
|
|
87
|
+
|
|
88
|
+
constructor(projection: Record<string, MongoProjectionValue>) {
|
|
89
|
+
super();
|
|
90
|
+
this.projection = Object.freeze({ ...projection });
|
|
91
|
+
this.freeze();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
95
|
+
return visitor.project(this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
99
|
+
const rewriter = context.aggExpr;
|
|
100
|
+
if (!rewriter) return this;
|
|
101
|
+
let hasExpr = false;
|
|
102
|
+
for (const val of Object.values(this.projection)) {
|
|
103
|
+
if (isAggExpr(val)) {
|
|
104
|
+
hasExpr = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!hasExpr) return this;
|
|
109
|
+
const newProjection: Record<string, MongoProjectionValue> = {};
|
|
110
|
+
for (const [key, val] of Object.entries(this.projection)) {
|
|
111
|
+
newProjection[key] = isAggExpr(val) ? val.rewrite(rewriter) : val;
|
|
112
|
+
}
|
|
113
|
+
return new MongoProjectStage(newProjection);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class MongoSortStage extends MongoStageNode {
|
|
118
|
+
readonly kind = 'sort' as const;
|
|
119
|
+
readonly sort: Readonly<Record<string, 1 | -1>>;
|
|
120
|
+
|
|
121
|
+
constructor(sort: Record<string, 1 | -1>) {
|
|
122
|
+
super();
|
|
123
|
+
this.sort = Object.freeze({ ...sort });
|
|
124
|
+
this.freeze();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
128
|
+
return visitor.sort(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class MongoLimitStage extends MongoStageNode {
|
|
137
|
+
readonly kind = 'limit' as const;
|
|
138
|
+
readonly limit: number;
|
|
139
|
+
|
|
140
|
+
constructor(limit: number) {
|
|
141
|
+
super();
|
|
142
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
143
|
+
throw new RangeError('limit must be a non-negative integer');
|
|
144
|
+
}
|
|
145
|
+
this.limit = limit;
|
|
146
|
+
this.freeze();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
150
|
+
return visitor.limit(this);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class MongoSkipStage extends MongoStageNode {
|
|
159
|
+
readonly kind = 'skip' as const;
|
|
160
|
+
readonly skip: number;
|
|
161
|
+
|
|
162
|
+
constructor(skip: number) {
|
|
163
|
+
super();
|
|
164
|
+
if (!Number.isInteger(skip) || skip < 0) {
|
|
165
|
+
throw new RangeError('skip must be a non-negative integer');
|
|
166
|
+
}
|
|
167
|
+
this.skip = skip;
|
|
168
|
+
this.freeze();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
172
|
+
return visitor.skip(this);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class MongoLookupStage extends MongoStageNode {
|
|
181
|
+
readonly kind = 'lookup' as const;
|
|
182
|
+
readonly from: string;
|
|
183
|
+
readonly localField: string | undefined;
|
|
184
|
+
readonly foreignField: string | undefined;
|
|
185
|
+
readonly as: string;
|
|
186
|
+
readonly pipeline: ReadonlyArray<MongoPipelineStage> | undefined;
|
|
187
|
+
readonly let_: Readonly<Record<string, MongoAggExpr>> | undefined;
|
|
188
|
+
|
|
189
|
+
constructor(options: {
|
|
190
|
+
from: string;
|
|
191
|
+
localField?: string;
|
|
192
|
+
foreignField?: string;
|
|
193
|
+
as: string;
|
|
194
|
+
pipeline?: ReadonlyArray<MongoPipelineStage>;
|
|
195
|
+
let_?: Record<string, MongoAggExpr>;
|
|
196
|
+
}) {
|
|
197
|
+
super();
|
|
198
|
+
const hasLocalField = options.localField !== undefined;
|
|
199
|
+
const hasForeignField = options.foreignField !== undefined;
|
|
200
|
+
const hasPipeline = !!options.pipeline;
|
|
201
|
+
if (hasLocalField !== hasForeignField) {
|
|
202
|
+
throw new Error('MongoLookupStage requires both localField and foreignField together');
|
|
203
|
+
}
|
|
204
|
+
if (!hasLocalField && !hasPipeline) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
'MongoLookupStage requires either equality fields (localField/foreignField) or a pipeline',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (options.let_ && !hasPipeline) {
|
|
210
|
+
throw new Error('MongoLookupStage let_ requires a pipeline');
|
|
211
|
+
}
|
|
212
|
+
this.from = options.from;
|
|
213
|
+
this.localField = options.localField;
|
|
214
|
+
this.foreignField = options.foreignField;
|
|
215
|
+
this.as = options.as;
|
|
216
|
+
this.pipeline = options.pipeline ? Object.freeze([...options.pipeline]) : undefined;
|
|
217
|
+
this.let_ = options.let_ ? Object.freeze({ ...options.let_ }) : undefined;
|
|
218
|
+
this.freeze();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
222
|
+
return visitor.lookup(this);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
226
|
+
if (!this.pipeline && !this.let_) return this;
|
|
227
|
+
const rewrittenLet =
|
|
228
|
+
this.let_ && context.aggExpr ? rewriteExprRecord(this.let_, context.aggExpr) : this.let_;
|
|
229
|
+
const options: {
|
|
230
|
+
from: string;
|
|
231
|
+
localField?: string;
|
|
232
|
+
foreignField?: string;
|
|
233
|
+
as: string;
|
|
234
|
+
pipeline?: ReadonlyArray<MongoPipelineStage>;
|
|
235
|
+
let_?: Record<string, MongoAggExpr>;
|
|
236
|
+
} = { from: this.from, as: this.as };
|
|
237
|
+
if (this.localField !== undefined) options.localField = this.localField;
|
|
238
|
+
if (this.foreignField !== undefined) options.foreignField = this.foreignField;
|
|
239
|
+
if (this.pipeline) options.pipeline = this.pipeline.map((stage) => stage.rewrite(context));
|
|
240
|
+
if (rewrittenLet) options.let_ = { ...rewrittenLet };
|
|
241
|
+
return new MongoLookupStage(options);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class MongoUnwindStage extends MongoStageNode {
|
|
246
|
+
readonly kind = 'unwind' as const;
|
|
247
|
+
readonly path: string;
|
|
248
|
+
readonly preserveNullAndEmptyArrays: boolean;
|
|
249
|
+
readonly includeArrayIndex: string | undefined;
|
|
250
|
+
|
|
251
|
+
constructor(path: string, preserveNullAndEmptyArrays: boolean, includeArrayIndex?: string) {
|
|
252
|
+
super();
|
|
253
|
+
this.path = path;
|
|
254
|
+
this.preserveNullAndEmptyArrays = preserveNullAndEmptyArrays;
|
|
255
|
+
this.includeArrayIndex = includeArrayIndex;
|
|
256
|
+
this.freeze();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
260
|
+
return visitor.unwind(this);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export class MongoGroupStage extends MongoStageNode {
|
|
269
|
+
readonly kind = 'group' as const;
|
|
270
|
+
readonly groupId: MongoGroupId;
|
|
271
|
+
readonly accumulators: Readonly<Record<string, MongoAggAccumulator>>;
|
|
272
|
+
|
|
273
|
+
constructor(groupId: MongoGroupId, accumulators: Record<string, MongoAggAccumulator>) {
|
|
274
|
+
super();
|
|
275
|
+
this.groupId = groupId;
|
|
276
|
+
this.accumulators = Object.freeze({ ...accumulators });
|
|
277
|
+
this.freeze();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
281
|
+
return visitor.group(this);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
285
|
+
const rewriter = context.aggExpr;
|
|
286
|
+
if (!rewriter) return this;
|
|
287
|
+
const newAccumulators: Record<string, MongoAggAccumulator> = {};
|
|
288
|
+
for (const [key, acc] of Object.entries(this.accumulators)) {
|
|
289
|
+
// MongoAggAccumulator.rewrite() returns MongoAggExpr (the base union). The cast is safe
|
|
290
|
+
// because the default rewriter rebuilds an accumulator from its rewritten arg. A custom
|
|
291
|
+
// accumulator() hook could technically return a non-accumulator — narrowing the return type
|
|
292
|
+
// on MongoAggAccumulator.rewrite() is tracked as a follow-up for the agg expression AST.
|
|
293
|
+
newAccumulators[key] = acc.rewrite(rewriter) as MongoAggAccumulator;
|
|
294
|
+
}
|
|
295
|
+
return new MongoGroupStage(rewriteGroupId(this.groupId, rewriter), newAccumulators);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export class MongoAddFieldsStage extends MongoStageNode {
|
|
300
|
+
readonly kind = 'addFields' as const;
|
|
301
|
+
readonly fields: Readonly<Record<string, MongoAggExpr>>;
|
|
302
|
+
|
|
303
|
+
constructor(fields: Record<string, MongoAggExpr>) {
|
|
304
|
+
super();
|
|
305
|
+
this.fields = Object.freeze({ ...fields });
|
|
306
|
+
this.freeze();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
310
|
+
return visitor.addFields(this);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
314
|
+
const rewriter = context.aggExpr;
|
|
315
|
+
if (!rewriter) return this;
|
|
316
|
+
return new MongoAddFieldsStage(rewriteExprRecord(this.fields, rewriter));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export class MongoReplaceRootStage extends MongoStageNode {
|
|
321
|
+
readonly kind = 'replaceRoot' as const;
|
|
322
|
+
readonly newRoot: MongoAggExpr;
|
|
323
|
+
|
|
324
|
+
constructor(newRoot: MongoAggExpr) {
|
|
325
|
+
super();
|
|
326
|
+
this.newRoot = newRoot;
|
|
327
|
+
this.freeze();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
331
|
+
return visitor.replaceRoot(this);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
335
|
+
const rewriter = context.aggExpr;
|
|
336
|
+
if (!rewriter) return this;
|
|
337
|
+
return new MongoReplaceRootStage(this.newRoot.rewrite(rewriter));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export class MongoCountStage extends MongoStageNode {
|
|
342
|
+
readonly kind = 'count' as const;
|
|
343
|
+
readonly field: string;
|
|
344
|
+
|
|
345
|
+
constructor(field: string) {
|
|
346
|
+
super();
|
|
347
|
+
this.field = field;
|
|
348
|
+
this.freeze();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
352
|
+
return visitor.count(this);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export class MongoSortByCountStage extends MongoStageNode {
|
|
361
|
+
readonly kind = 'sortByCount' as const;
|
|
362
|
+
readonly expr: MongoAggExpr;
|
|
363
|
+
|
|
364
|
+
constructor(expr: MongoAggExpr) {
|
|
365
|
+
super();
|
|
366
|
+
this.expr = expr;
|
|
367
|
+
this.freeze();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
371
|
+
return visitor.sortByCount(this);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
375
|
+
const rewriter = context.aggExpr;
|
|
376
|
+
if (!rewriter) return this;
|
|
377
|
+
return new MongoSortByCountStage(this.expr.rewrite(rewriter));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export class MongoSampleStage extends MongoStageNode {
|
|
382
|
+
readonly kind = 'sample' as const;
|
|
383
|
+
readonly size: number;
|
|
384
|
+
|
|
385
|
+
constructor(size: number) {
|
|
386
|
+
super();
|
|
387
|
+
if (!Number.isInteger(size) || size < 0) {
|
|
388
|
+
throw new RangeError('size must be a non-negative integer');
|
|
389
|
+
}
|
|
390
|
+
this.size = size;
|
|
391
|
+
this.freeze();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
395
|
+
return visitor.sample(this);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export class MongoRedactStage extends MongoStageNode {
|
|
404
|
+
readonly kind = 'redact' as const;
|
|
405
|
+
readonly expr: MongoAggExpr;
|
|
406
|
+
|
|
407
|
+
constructor(expr: MongoAggExpr) {
|
|
408
|
+
super();
|
|
409
|
+
this.expr = expr;
|
|
410
|
+
this.freeze();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
414
|
+
return visitor.redact(this);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
418
|
+
const rewriter = context.aggExpr;
|
|
419
|
+
if (!rewriter) return this;
|
|
420
|
+
return new MongoRedactStage(this.expr.rewrite(rewriter));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export class MongoOutStage extends MongoStageNode {
|
|
425
|
+
readonly kind = 'out' as const;
|
|
426
|
+
readonly collection: string;
|
|
427
|
+
readonly db: string | undefined;
|
|
428
|
+
|
|
429
|
+
constructor(collection: string, db?: string) {
|
|
430
|
+
super();
|
|
431
|
+
this.collection = collection;
|
|
432
|
+
this.db = db;
|
|
433
|
+
this.freeze();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
437
|
+
return visitor.out(this);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export class MongoUnionWithStage extends MongoStageNode {
|
|
446
|
+
readonly kind = 'unionWith' as const;
|
|
447
|
+
readonly collection: string;
|
|
448
|
+
readonly pipeline: ReadonlyArray<MongoPipelineStage> | undefined;
|
|
449
|
+
|
|
450
|
+
constructor(collection: string, pipeline?: ReadonlyArray<MongoPipelineStage>) {
|
|
451
|
+
super();
|
|
452
|
+
this.collection = collection;
|
|
453
|
+
this.pipeline = pipeline ? Object.freeze([...pipeline]) : undefined;
|
|
454
|
+
this.freeze();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
458
|
+
return visitor.unionWith(this);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
462
|
+
if (!this.pipeline) return this;
|
|
463
|
+
return new MongoUnionWithStage(
|
|
464
|
+
this.collection,
|
|
465
|
+
this.pipeline.map((stage) => stage.rewrite(context)),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export class MongoBucketStage extends MongoStageNode {
|
|
471
|
+
readonly kind = 'bucket' as const;
|
|
472
|
+
readonly groupBy: MongoAggExpr;
|
|
473
|
+
readonly boundaries: ReadonlyArray<unknown>;
|
|
474
|
+
readonly default_: unknown;
|
|
475
|
+
readonly output: Readonly<Record<string, MongoAggAccumulator>> | undefined;
|
|
476
|
+
|
|
477
|
+
constructor(options: {
|
|
478
|
+
groupBy: MongoAggExpr;
|
|
479
|
+
boundaries: ReadonlyArray<unknown>;
|
|
480
|
+
default_?: unknown;
|
|
481
|
+
output?: Record<string, MongoAggAccumulator>;
|
|
482
|
+
}) {
|
|
483
|
+
super();
|
|
484
|
+
if (options.boundaries.length < 2) {
|
|
485
|
+
throw new RangeError('boundaries must contain at least 2 values');
|
|
486
|
+
}
|
|
487
|
+
this.groupBy = options.groupBy;
|
|
488
|
+
this.boundaries = Object.freeze([...options.boundaries]);
|
|
489
|
+
this.default_ = options.default_;
|
|
490
|
+
this.output = options.output ? Object.freeze({ ...options.output }) : undefined;
|
|
491
|
+
this.freeze();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
495
|
+
return visitor.bucket(this);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
499
|
+
const rewriter = context.aggExpr;
|
|
500
|
+
if (!rewriter) return this;
|
|
501
|
+
const opts: {
|
|
502
|
+
groupBy: MongoAggExpr;
|
|
503
|
+
boundaries: ReadonlyArray<unknown>;
|
|
504
|
+
default_?: unknown;
|
|
505
|
+
output?: Record<string, MongoAggAccumulator>;
|
|
506
|
+
} = { groupBy: this.groupBy.rewrite(rewriter), boundaries: this.boundaries };
|
|
507
|
+
if (this.default_ !== undefined) opts.default_ = this.default_;
|
|
508
|
+
if (this.output) opts.output = rewriteAccumulatorRecord(this.output, rewriter);
|
|
509
|
+
return new MongoBucketStage(opts);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export class MongoBucketAutoStage extends MongoStageNode {
|
|
514
|
+
readonly kind = 'bucketAuto' as const;
|
|
515
|
+
readonly groupBy: MongoAggExpr;
|
|
516
|
+
readonly buckets: number;
|
|
517
|
+
readonly output: Readonly<Record<string, MongoAggAccumulator>> | undefined;
|
|
518
|
+
readonly granularity: string | undefined;
|
|
519
|
+
|
|
520
|
+
constructor(options: {
|
|
521
|
+
groupBy: MongoAggExpr;
|
|
522
|
+
buckets: number;
|
|
523
|
+
output?: Record<string, MongoAggAccumulator>;
|
|
524
|
+
granularity?: string;
|
|
525
|
+
}) {
|
|
526
|
+
super();
|
|
527
|
+
if (!Number.isInteger(options.buckets) || options.buckets < 1) {
|
|
528
|
+
throw new RangeError('buckets must be a positive integer');
|
|
529
|
+
}
|
|
530
|
+
this.groupBy = options.groupBy;
|
|
531
|
+
this.buckets = options.buckets;
|
|
532
|
+
this.output = options.output ? Object.freeze({ ...options.output }) : undefined;
|
|
533
|
+
this.granularity = options.granularity;
|
|
534
|
+
this.freeze();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
538
|
+
return visitor.bucketAuto(this);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
542
|
+
const rewriter = context.aggExpr;
|
|
543
|
+
if (!rewriter) return this;
|
|
544
|
+
const opts: {
|
|
545
|
+
groupBy: MongoAggExpr;
|
|
546
|
+
buckets: number;
|
|
547
|
+
output?: Record<string, MongoAggAccumulator>;
|
|
548
|
+
granularity?: string;
|
|
549
|
+
} = { groupBy: this.groupBy.rewrite(rewriter), buckets: this.buckets };
|
|
550
|
+
if (this.output) opts.output = rewriteAccumulatorRecord(this.output, rewriter);
|
|
551
|
+
if (this.granularity !== undefined) opts.granularity = this.granularity;
|
|
552
|
+
return new MongoBucketAutoStage(opts);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export class MongoGeoNearStage extends MongoStageNode {
|
|
557
|
+
readonly kind = 'geoNear' as const;
|
|
558
|
+
readonly near: unknown;
|
|
559
|
+
readonly distanceField: string;
|
|
560
|
+
readonly spherical: boolean | undefined;
|
|
561
|
+
readonly maxDistance: number | undefined;
|
|
562
|
+
readonly minDistance: number | undefined;
|
|
563
|
+
readonly query: MongoFilterExpr | undefined;
|
|
564
|
+
readonly key: string | undefined;
|
|
565
|
+
readonly distanceMultiplier: number | undefined;
|
|
566
|
+
readonly includeLocs: string | undefined;
|
|
567
|
+
|
|
568
|
+
constructor(options: {
|
|
569
|
+
near: unknown;
|
|
570
|
+
distanceField: string;
|
|
571
|
+
spherical?: boolean;
|
|
572
|
+
maxDistance?: number;
|
|
573
|
+
minDistance?: number;
|
|
574
|
+
query?: MongoFilterExpr;
|
|
575
|
+
key?: string;
|
|
576
|
+
distanceMultiplier?: number;
|
|
577
|
+
includeLocs?: string;
|
|
578
|
+
}) {
|
|
579
|
+
super();
|
|
580
|
+
this.near = options.near;
|
|
581
|
+
this.distanceField = options.distanceField;
|
|
582
|
+
this.spherical = options.spherical;
|
|
583
|
+
this.maxDistance = options.maxDistance;
|
|
584
|
+
this.minDistance = options.minDistance;
|
|
585
|
+
this.query = options.query;
|
|
586
|
+
this.key = options.key;
|
|
587
|
+
this.distanceMultiplier = options.distanceMultiplier;
|
|
588
|
+
this.includeLocs = options.includeLocs;
|
|
589
|
+
this.freeze();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
593
|
+
return visitor.geoNear(this);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
597
|
+
if (!this.query || !context.filter) return this;
|
|
598
|
+
const opts: {
|
|
599
|
+
near: unknown;
|
|
600
|
+
distanceField: string;
|
|
601
|
+
spherical?: boolean;
|
|
602
|
+
maxDistance?: number;
|
|
603
|
+
minDistance?: number;
|
|
604
|
+
query?: MongoFilterExpr;
|
|
605
|
+
key?: string;
|
|
606
|
+
distanceMultiplier?: number;
|
|
607
|
+
includeLocs?: string;
|
|
608
|
+
} = { near: this.near, distanceField: this.distanceField };
|
|
609
|
+
if (this.spherical !== undefined) opts.spherical = this.spherical;
|
|
610
|
+
if (this.maxDistance !== undefined) opts.maxDistance = this.maxDistance;
|
|
611
|
+
if (this.minDistance !== undefined) opts.minDistance = this.minDistance;
|
|
612
|
+
opts.query = this.query.rewrite(context.filter);
|
|
613
|
+
if (this.key !== undefined) opts.key = this.key;
|
|
614
|
+
if (this.distanceMultiplier !== undefined) opts.distanceMultiplier = this.distanceMultiplier;
|
|
615
|
+
if (this.includeLocs !== undefined) opts.includeLocs = this.includeLocs;
|
|
616
|
+
return new MongoGeoNearStage(opts);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export class MongoFacetStage extends MongoStageNode {
|
|
621
|
+
readonly kind = 'facet' as const;
|
|
622
|
+
readonly facets: Readonly<Record<string, ReadonlyArray<MongoPipelineStage>>>;
|
|
623
|
+
|
|
624
|
+
constructor(facets: Record<string, ReadonlyArray<MongoPipelineStage>>) {
|
|
625
|
+
super();
|
|
626
|
+
const frozen: Record<string, ReadonlyArray<MongoPipelineStage>> = {};
|
|
627
|
+
for (const [key, pipeline] of Object.entries(facets)) {
|
|
628
|
+
frozen[key] = Object.freeze([...pipeline]);
|
|
629
|
+
}
|
|
630
|
+
this.facets = Object.freeze(frozen);
|
|
631
|
+
this.freeze();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
635
|
+
return visitor.facet(this);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
639
|
+
const newFacets: Record<string, ReadonlyArray<MongoPipelineStage>> = {};
|
|
640
|
+
for (const [key, pipeline] of Object.entries(this.facets)) {
|
|
641
|
+
newFacets[key] = pipeline.map((stage) => stage.rewrite(context));
|
|
642
|
+
}
|
|
643
|
+
return new MongoFacetStage(newFacets);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export class MongoGraphLookupStage extends MongoStageNode {
|
|
648
|
+
readonly kind = 'graphLookup' as const;
|
|
649
|
+
readonly from: string;
|
|
650
|
+
readonly startWith: MongoAggExpr;
|
|
651
|
+
readonly connectFromField: string;
|
|
652
|
+
readonly connectToField: string;
|
|
653
|
+
readonly as: string;
|
|
654
|
+
readonly maxDepth: number | undefined;
|
|
655
|
+
readonly depthField: string | undefined;
|
|
656
|
+
readonly restrictSearchWithMatch: MongoFilterExpr | undefined;
|
|
657
|
+
|
|
658
|
+
constructor(options: {
|
|
659
|
+
from: string;
|
|
660
|
+
startWith: MongoAggExpr;
|
|
661
|
+
connectFromField: string;
|
|
662
|
+
connectToField: string;
|
|
663
|
+
as: string;
|
|
664
|
+
maxDepth?: number;
|
|
665
|
+
depthField?: string;
|
|
666
|
+
restrictSearchWithMatch?: MongoFilterExpr;
|
|
667
|
+
}) {
|
|
668
|
+
super();
|
|
669
|
+
this.from = options.from;
|
|
670
|
+
this.startWith = options.startWith;
|
|
671
|
+
this.connectFromField = options.connectFromField;
|
|
672
|
+
this.connectToField = options.connectToField;
|
|
673
|
+
this.as = options.as;
|
|
674
|
+
this.maxDepth = options.maxDepth;
|
|
675
|
+
this.depthField = options.depthField;
|
|
676
|
+
this.restrictSearchWithMatch = options.restrictSearchWithMatch;
|
|
677
|
+
this.freeze();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
681
|
+
return visitor.graphLookup(this);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
685
|
+
const rewrittenStartWith = context.aggExpr
|
|
686
|
+
? this.startWith.rewrite(context.aggExpr)
|
|
687
|
+
: this.startWith;
|
|
688
|
+
const rewrittenMatch =
|
|
689
|
+
this.restrictSearchWithMatch && context.filter
|
|
690
|
+
? this.restrictSearchWithMatch.rewrite(context.filter)
|
|
691
|
+
: this.restrictSearchWithMatch;
|
|
692
|
+
if (rewrittenStartWith === this.startWith && rewrittenMatch === this.restrictSearchWithMatch) {
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
const opts: {
|
|
696
|
+
from: string;
|
|
697
|
+
startWith: MongoAggExpr;
|
|
698
|
+
connectFromField: string;
|
|
699
|
+
connectToField: string;
|
|
700
|
+
as: string;
|
|
701
|
+
maxDepth?: number;
|
|
702
|
+
depthField?: string;
|
|
703
|
+
restrictSearchWithMatch?: MongoFilterExpr;
|
|
704
|
+
} = {
|
|
705
|
+
from: this.from,
|
|
706
|
+
startWith: rewrittenStartWith,
|
|
707
|
+
connectFromField: this.connectFromField,
|
|
708
|
+
connectToField: this.connectToField,
|
|
709
|
+
as: this.as,
|
|
710
|
+
};
|
|
711
|
+
if (this.maxDepth !== undefined) opts.maxDepth = this.maxDepth;
|
|
712
|
+
if (this.depthField !== undefined) opts.depthField = this.depthField;
|
|
713
|
+
if (rewrittenMatch) opts.restrictSearchWithMatch = rewrittenMatch;
|
|
714
|
+
return new MongoGraphLookupStage(opts);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export class MongoMergeStage extends MongoStageNode {
|
|
719
|
+
readonly kind = 'merge' as const;
|
|
720
|
+
readonly into: string | { readonly db: string; readonly coll: string };
|
|
721
|
+
readonly on: string | ReadonlyArray<string> | undefined;
|
|
722
|
+
readonly whenMatched: string | ReadonlyArray<MongoUpdatePipelineStage> | undefined;
|
|
723
|
+
readonly whenNotMatched: string | undefined;
|
|
724
|
+
|
|
725
|
+
constructor(options: {
|
|
726
|
+
into: string | { db: string; coll: string };
|
|
727
|
+
on?: string | ReadonlyArray<string>;
|
|
728
|
+
whenMatched?: string | ReadonlyArray<MongoUpdatePipelineStage>;
|
|
729
|
+
whenNotMatched?: string;
|
|
730
|
+
}) {
|
|
731
|
+
super();
|
|
732
|
+
this.into =
|
|
733
|
+
typeof options.into === 'string' ? options.into : Object.freeze({ ...options.into });
|
|
734
|
+
this.on =
|
|
735
|
+
options.on === undefined
|
|
736
|
+
? undefined
|
|
737
|
+
: typeof options.on === 'string'
|
|
738
|
+
? options.on
|
|
739
|
+
: Object.freeze([...options.on]);
|
|
740
|
+
this.whenMatched =
|
|
741
|
+
options.whenMatched === undefined
|
|
742
|
+
? undefined
|
|
743
|
+
: typeof options.whenMatched === 'string'
|
|
744
|
+
? options.whenMatched
|
|
745
|
+
: Object.freeze([...options.whenMatched]);
|
|
746
|
+
this.whenNotMatched = options.whenNotMatched;
|
|
747
|
+
this.freeze();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
751
|
+
return visitor.merge(this);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
755
|
+
if (!Array.isArray(this.whenMatched)) return this;
|
|
756
|
+
const opts: {
|
|
757
|
+
into: string | { db: string; coll: string };
|
|
758
|
+
on?: string | ReadonlyArray<string>;
|
|
759
|
+
whenMatched?: string | ReadonlyArray<MongoUpdatePipelineStage>;
|
|
760
|
+
whenNotMatched?: string;
|
|
761
|
+
} = { into: this.into };
|
|
762
|
+
if (this.on !== undefined) opts.on = this.on;
|
|
763
|
+
// rewrite() preserves the concrete stage type at runtime
|
|
764
|
+
opts.whenMatched = this.whenMatched.map(
|
|
765
|
+
(stage) => stage.rewrite(context) as MongoUpdatePipelineStage,
|
|
766
|
+
);
|
|
767
|
+
if (this.whenNotMatched !== undefined) opts.whenNotMatched = this.whenNotMatched;
|
|
768
|
+
return new MongoMergeStage(opts);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface MongoWindowField {
|
|
773
|
+
readonly operator: MongoAggExpr;
|
|
774
|
+
readonly window?: {
|
|
775
|
+
readonly documents?: readonly [number, number];
|
|
776
|
+
readonly range?: { readonly start: unknown; readonly end: unknown; readonly unit?: string };
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export class MongoSetWindowFieldsStage extends MongoStageNode {
|
|
781
|
+
readonly kind = 'setWindowFields' as const;
|
|
782
|
+
readonly partitionBy: MongoAggExpr | undefined;
|
|
783
|
+
readonly sortBy: Readonly<Record<string, 1 | -1>> | undefined;
|
|
784
|
+
readonly output: Readonly<Record<string, MongoWindowField>>;
|
|
785
|
+
|
|
786
|
+
constructor(options: {
|
|
787
|
+
partitionBy?: MongoAggExpr;
|
|
788
|
+
sortBy?: Record<string, 1 | -1>;
|
|
789
|
+
output: Record<string, MongoWindowField>;
|
|
790
|
+
}) {
|
|
791
|
+
super();
|
|
792
|
+
this.partitionBy = options.partitionBy;
|
|
793
|
+
this.sortBy = options.sortBy ? Object.freeze({ ...options.sortBy }) : undefined;
|
|
794
|
+
this.output = Object.freeze({ ...options.output });
|
|
795
|
+
this.freeze();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
799
|
+
return visitor.setWindowFields(this);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
803
|
+
const rewriter = context.aggExpr;
|
|
804
|
+
if (!rewriter) return this;
|
|
805
|
+
const newOutput: Record<string, MongoWindowField> = {};
|
|
806
|
+
for (const [key, wf] of Object.entries(this.output)) {
|
|
807
|
+
newOutput[key] = { ...wf, operator: wf.operator.rewrite(rewriter) };
|
|
808
|
+
}
|
|
809
|
+
const opts: {
|
|
810
|
+
partitionBy?: MongoAggExpr;
|
|
811
|
+
sortBy?: Record<string, 1 | -1>;
|
|
812
|
+
output: Record<string, MongoWindowField>;
|
|
813
|
+
} = { output: newOutput };
|
|
814
|
+
if (this.partitionBy) opts.partitionBy = this.partitionBy.rewrite(rewriter);
|
|
815
|
+
if (this.sortBy) opts.sortBy = { ...this.sortBy };
|
|
816
|
+
return new MongoSetWindowFieldsStage(opts);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export interface MongoDensifyRange {
|
|
821
|
+
readonly step: number;
|
|
822
|
+
readonly unit?: string;
|
|
823
|
+
readonly bounds: 'full' | 'partition' | readonly [unknown, unknown];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export class MongoDensifyStage extends MongoStageNode {
|
|
827
|
+
readonly kind = 'densify' as const;
|
|
828
|
+
readonly field: string;
|
|
829
|
+
readonly partitionByFields: ReadonlyArray<string> | undefined;
|
|
830
|
+
readonly range: MongoDensifyRange;
|
|
831
|
+
|
|
832
|
+
constructor(options: {
|
|
833
|
+
field: string;
|
|
834
|
+
partitionByFields?: ReadonlyArray<string>;
|
|
835
|
+
range: MongoDensifyRange;
|
|
836
|
+
}) {
|
|
837
|
+
super();
|
|
838
|
+
this.field = options.field;
|
|
839
|
+
this.partitionByFields = options.partitionByFields
|
|
840
|
+
? Object.freeze([...options.partitionByFields])
|
|
841
|
+
: undefined;
|
|
842
|
+
this.range = Object.freeze({ ...options.range });
|
|
843
|
+
this.freeze();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
847
|
+
return visitor.densify(this);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
851
|
+
return this;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export interface MongoFillOutput {
|
|
856
|
+
readonly method?: string;
|
|
857
|
+
readonly value?: MongoAggExpr;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export class MongoFillStage extends MongoStageNode {
|
|
861
|
+
readonly kind = 'fill' as const;
|
|
862
|
+
readonly partitionBy: MongoAggExpr | undefined;
|
|
863
|
+
readonly partitionByFields: ReadonlyArray<string> | undefined;
|
|
864
|
+
readonly sortBy: Readonly<Record<string, 1 | -1>> | undefined;
|
|
865
|
+
readonly output: Readonly<Record<string, MongoFillOutput>>;
|
|
866
|
+
|
|
867
|
+
constructor(options: {
|
|
868
|
+
partitionBy?: MongoAggExpr;
|
|
869
|
+
partitionByFields?: ReadonlyArray<string>;
|
|
870
|
+
sortBy?: Record<string, 1 | -1>;
|
|
871
|
+
output: Record<string, MongoFillOutput>;
|
|
872
|
+
}) {
|
|
873
|
+
super();
|
|
874
|
+
this.partitionBy = options.partitionBy;
|
|
875
|
+
this.partitionByFields = options.partitionByFields
|
|
876
|
+
? Object.freeze([...options.partitionByFields])
|
|
877
|
+
: undefined;
|
|
878
|
+
this.sortBy = options.sortBy ? Object.freeze({ ...options.sortBy }) : undefined;
|
|
879
|
+
this.output = Object.freeze({ ...options.output });
|
|
880
|
+
this.freeze();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
884
|
+
return visitor.fill(this);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
rewrite(context: MongoStageRewriterContext): MongoPipelineStage {
|
|
888
|
+
const rewriter = context.aggExpr;
|
|
889
|
+
if (!rewriter) return this;
|
|
890
|
+
const newOutput: Record<string, MongoFillOutput> = {};
|
|
891
|
+
for (const [key, fo] of Object.entries(this.output)) {
|
|
892
|
+
newOutput[key] = fo.value ? { ...fo, value: fo.value.rewrite(rewriter) } : fo;
|
|
893
|
+
}
|
|
894
|
+
const opts: {
|
|
895
|
+
partitionBy?: MongoAggExpr;
|
|
896
|
+
partitionByFields?: ReadonlyArray<string>;
|
|
897
|
+
sortBy?: Record<string, 1 | -1>;
|
|
898
|
+
output: Record<string, MongoFillOutput>;
|
|
899
|
+
} = { output: newOutput };
|
|
900
|
+
if (this.partitionBy) opts.partitionBy = this.partitionBy.rewrite(rewriter);
|
|
901
|
+
if (this.partitionByFields) opts.partitionByFields = [...this.partitionByFields];
|
|
902
|
+
if (this.sortBy) opts.sortBy = { ...this.sortBy };
|
|
903
|
+
return new MongoFillStage(opts);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export class MongoSearchStage extends MongoStageNode {
|
|
908
|
+
readonly kind = 'search' as const;
|
|
909
|
+
readonly index: string | undefined;
|
|
910
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
911
|
+
|
|
912
|
+
constructor(config: Record<string, unknown>, index?: string) {
|
|
913
|
+
super();
|
|
914
|
+
this.config = Object.freeze({ ...config });
|
|
915
|
+
this.index = index;
|
|
916
|
+
this.freeze();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
920
|
+
return visitor.search(this);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
924
|
+
return this;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export class MongoSearchMetaStage extends MongoStageNode {
|
|
929
|
+
readonly kind = 'searchMeta' as const;
|
|
930
|
+
readonly index: string | undefined;
|
|
931
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
932
|
+
|
|
933
|
+
constructor(config: Record<string, unknown>, index?: string) {
|
|
934
|
+
super();
|
|
935
|
+
this.config = Object.freeze({ ...config });
|
|
936
|
+
this.index = index;
|
|
937
|
+
this.freeze();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
941
|
+
return visitor.searchMeta(this);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
945
|
+
return this;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export class MongoVectorSearchStage extends MongoStageNode {
|
|
950
|
+
readonly kind = 'vectorSearch' as const;
|
|
951
|
+
readonly index: string;
|
|
952
|
+
readonly path: string;
|
|
953
|
+
readonly queryVector: ReadonlyArray<number>;
|
|
954
|
+
readonly numCandidates: number;
|
|
955
|
+
readonly limit: number;
|
|
956
|
+
readonly filter: Readonly<Record<string, unknown>> | undefined;
|
|
957
|
+
|
|
958
|
+
constructor(options: {
|
|
959
|
+
index: string;
|
|
960
|
+
path: string;
|
|
961
|
+
queryVector: ReadonlyArray<number>;
|
|
962
|
+
numCandidates: number;
|
|
963
|
+
limit: number;
|
|
964
|
+
filter?: Record<string, unknown>;
|
|
965
|
+
}) {
|
|
966
|
+
super();
|
|
967
|
+
if (!Number.isInteger(options.limit) || options.limit < 1) {
|
|
968
|
+
throw new RangeError('limit must be a positive integer');
|
|
969
|
+
}
|
|
970
|
+
if (!Number.isInteger(options.numCandidates) || options.numCandidates < options.limit) {
|
|
971
|
+
throw new RangeError('numCandidates must be an integer >= limit');
|
|
972
|
+
}
|
|
973
|
+
this.index = options.index;
|
|
974
|
+
this.path = options.path;
|
|
975
|
+
this.queryVector = Object.freeze([...options.queryVector]);
|
|
976
|
+
this.numCandidates = options.numCandidates;
|
|
977
|
+
this.limit = options.limit;
|
|
978
|
+
this.filter = options.filter ? Object.freeze({ ...options.filter }) : undefined;
|
|
979
|
+
this.freeze();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
accept<R>(visitor: MongoStageVisitor<R>): R {
|
|
983
|
+
return visitor.vectorSearch(this);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
rewrite(_context: MongoStageRewriterContext): MongoPipelineStage {
|
|
987
|
+
return this;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export type MongoUpdatePipelineStage =
|
|
992
|
+
| MongoAddFieldsStage
|
|
993
|
+
| MongoProjectStage
|
|
994
|
+
| MongoReplaceRootStage;
|
|
995
|
+
|
|
996
|
+
export type MongoPipelineStage =
|
|
997
|
+
| MongoMatchStage
|
|
998
|
+
| MongoProjectStage
|
|
999
|
+
| MongoSortStage
|
|
1000
|
+
| MongoLimitStage
|
|
1001
|
+
| MongoSkipStage
|
|
1002
|
+
| MongoLookupStage
|
|
1003
|
+
| MongoUnwindStage
|
|
1004
|
+
| MongoGroupStage
|
|
1005
|
+
| MongoAddFieldsStage
|
|
1006
|
+
| MongoReplaceRootStage
|
|
1007
|
+
| MongoCountStage
|
|
1008
|
+
| MongoSortByCountStage
|
|
1009
|
+
| MongoSampleStage
|
|
1010
|
+
| MongoRedactStage
|
|
1011
|
+
| MongoOutStage
|
|
1012
|
+
| MongoUnionWithStage
|
|
1013
|
+
| MongoBucketStage
|
|
1014
|
+
| MongoBucketAutoStage
|
|
1015
|
+
| MongoGeoNearStage
|
|
1016
|
+
| MongoFacetStage
|
|
1017
|
+
| MongoGraphLookupStage
|
|
1018
|
+
| MongoMergeStage
|
|
1019
|
+
| MongoSetWindowFieldsStage
|
|
1020
|
+
| MongoDensifyStage
|
|
1021
|
+
| MongoFillStage
|
|
1022
|
+
| MongoSearchStage
|
|
1023
|
+
| MongoSearchMetaStage
|
|
1024
|
+
| MongoVectorSearchStage;
|