@routier/core 0.6.0 → 0.7.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 (52) hide show
  1. package/dist/assertions/index.cjs +19 -8
  2. package/dist/assertions/index.cjs.map +1 -1
  3. package/dist/assertions/index.d.ts +5 -1
  4. package/dist/assertions/index.js +21 -9
  5. package/dist/assertions/index.js.map +1 -1
  6. package/dist/collections/MemoryDataCollection.d.ts +10 -0
  7. package/dist/collections/index.cjs +29 -4
  8. package/dist/collections/index.cjs.map +1 -1
  9. package/dist/collections/index.js +29 -4
  10. package/dist/collections/index.js.map +1 -1
  11. package/dist/expressions/callSource.d.ts +41 -0
  12. package/dist/expressions/evaluate.d.ts +3 -0
  13. package/dist/expressions/fold.d.ts +7 -0
  14. package/dist/expressions/index.cjs +1754 -233
  15. package/dist/expressions/index.cjs.map +1 -1
  16. package/dist/expressions/index.d.ts +2 -0
  17. package/dist/expressions/index.js +1765 -234
  18. package/dist/expressions/index.js.map +1 -1
  19. package/dist/expressions/types.d.ts +45 -26
  20. package/dist/expressions/utils.d.ts +19 -1
  21. package/dist/index.cjs +2415 -364
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +2755 -684
  24. package/dist/index.js.map +1 -1
  25. package/dist/performance/index.cjs +6 -4
  26. package/dist/performance/index.cjs.map +1 -1
  27. package/dist/performance/index.js +6 -4
  28. package/dist/performance/index.js.map +1 -1
  29. package/dist/pipeline/index.cjs +6 -4
  30. package/dist/pipeline/index.cjs.map +1 -1
  31. package/dist/pipeline/index.js +6 -4
  32. package/dist/pipeline/index.js.map +1 -1
  33. package/dist/plugins/index.cjs +2309 -316
  34. package/dist/plugins/index.cjs.map +1 -1
  35. package/dist/plugins/index.js +2313 -311
  36. package/dist/plugins/index.js.map +1 -1
  37. package/dist/plugins/query/QueryOptionsCollection.d.ts +38 -10
  38. package/dist/plugins/query/describeFilter.d.ts +83 -0
  39. package/dist/plugins/query/explain.d.ts +71 -9
  40. package/dist/plugins/query/index.d.ts +1 -0
  41. package/dist/plugins/query/join.d.ts +4 -1
  42. package/dist/plugins/query/types.d.ts +36 -4
  43. package/dist/schema/PropertyInfo.d.ts +0 -1
  44. package/dist/schema/index.cjs +7 -14
  45. package/dist/schema/index.cjs.map +1 -1
  46. package/dist/schema/index.js +7 -14
  47. package/dist/schema/index.js.map +1 -1
  48. package/dist/utilities/index.cjs +242 -49
  49. package/dist/utilities/index.cjs.map +1 -1
  50. package/dist/utilities/index.js +242 -49
  51. package/dist/utilities/index.js.map +1 -1
  52. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  var __webpack_modules__ = ({
2
2
  126(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3
3
  __webpack_require__.d(__webpack_exports__, {
4
- e3: () => (isPropertyExpression)
4
+ S6: () => (isValueExpression),
5
+ e3: () => (isPropertyExpression),
6
+ xH: () => (isComparatorExpression)
5
7
  });
6
8
 
7
9
 
@@ -73,6 +75,11 @@ function isObjectWithType(value) {
73
75
  */ function isValueExpression(value) {
74
76
  return isObjectWithType(value) && value.type === "value";
75
77
  }
78
+ /**
79
+ * Type guard: narrows `value` to `CallExpression` when it is an object with `type === "call"`.
80
+ */ function isCallExpression(value) {
81
+ return isObjectWithType(value) && value.type === "call";
82
+ }
76
83
  /**
77
84
  * Type guard: narrows `value` to `EmptyExpression` when it is an object with `type === "empty"`.
78
85
  */ function isEmptyExpression(value) {
@@ -88,8 +95,42 @@ function isObjectWithType(value) {
88
95
  },
89
96
  63(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
90
97
  __webpack_require__.d(__webpack_exports__, {
91
- j: () => (forEach)
98
+ jJ: () => (forEach)
92
99
  });
100
+ /**
101
+ * Separates an operand from the calls applied to it.
102
+ *
103
+ * `null` when there is no operand beneath the calls. Every consumer needs this to decide whether a
104
+ * comparator side is a property or a value, so it lives here rather than in each translator.
105
+ */ function peelCalls(expression) {
106
+ const calls = [];
107
+ let current = expression;
108
+ while(current != null && current.type === "call"){
109
+ calls.unshift(current);
110
+ current = current.expression;
111
+ }
112
+ return current == null ? null : {
113
+ operand: current,
114
+ calls
115
+ };
116
+ }
117
+ function childrenOf(expression) {
118
+ if (expression.type === "call") {
119
+ const call = expression;
120
+ return [
121
+ call.expression,
122
+ ...call.arguments ?? []
123
+ ].filter((child)=>child != null);
124
+ }
125
+ const children = [];
126
+ if (expression.left != null) {
127
+ children.push(expression.left);
128
+ }
129
+ if (expression.right != null) {
130
+ children.push(expression.right);
131
+ }
132
+ return children;
133
+ }
93
134
  /**
94
135
  * Extracts all properties referenced in an expression
95
136
  * @param expression The expression to analyze
@@ -101,12 +142,8 @@ __webpack_require__.d(__webpack_exports__, {
101
142
  if (expr.type === "property") {
102
143
  properties.push(expr.property);
103
144
  }
104
- // Traverse left and right expressions if they exist
105
- if (expr.left) {
106
- traverse(expr.left);
107
- }
108
- if (expr.right) {
109
- traverse(expr.right);
145
+ for (const child of childrenOf(expr)){
146
+ traverse(child);
110
147
  }
111
148
  }
112
149
  traverse(expression);
@@ -119,14 +156,8 @@ function forEach(expression, callback) {
119
156
  if (!callback(expr)) {
120
157
  return false;
121
158
  }
122
- // Traverse left and right expressions if they exist
123
- if (expr.left) {
124
- if (!traverse(expr.left)) {
125
- return false;
126
- }
127
- }
128
- if (expr.right) {
129
- if (!traverse(expr.right)) {
159
+ for (const child of childrenOf(expr)){
160
+ if (!traverse(child)) {
130
161
  return false;
131
162
  }
132
163
  }
@@ -142,32 +173,62 @@ __webpack_require__.d(__webpack_exports__, {
142
173
  H: () => (QueryOptionsCollection)
143
174
  });
144
175
  /* import */ var _assertions__rspack_import_1 = __webpack_require__(126);
145
- /* import */ var _expressions_utils__rspack_import_0 = __webpack_require__(63);
176
+ /* import */ var _expressions_utils__rspack_import_2 = __webpack_require__(63);
177
+ /* import */ var _schema_types__rspack_import_0 = __webpack_require__(537);
178
+ /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
146
179
 
147
180
 
181
+
182
+
183
+ /** What a schema type is called in JavaScript, where one exists. A value of any other type cannot equal it. */ const JAVASCRIPT_TYPE_OF = {
184
+ [_schema_types__rspack_import_0/* .SchemaTypes.Number */.L.Number]: "number",
185
+ [_schema_types__rspack_import_0/* .SchemaTypes.String */.L.String]: "string",
186
+ [_schema_types__rspack_import_0/* .SchemaTypes.Boolean */.L.Boolean]: "boolean",
187
+ [_schema_types__rspack_import_0/* .SchemaTypes.Date */.L.Date]: "object"
188
+ };
189
+ const mismatchedSide = (property, value)=>{
190
+ if (property == null || value == null || !(0,_assertions__rspack_import_1/* .isPropertyExpression */.e3)(property) || !(0,_assertions__rspack_import_1/* .isValueExpression */.S6)(value)) {
191
+ return null;
192
+ }
193
+ const expected = JAVASCRIPT_TYPE_OF[property.property.type];
194
+ if (expected == null || value.value == null || typeof value.value === expected) {
195
+ return null;
196
+ }
197
+ return {
198
+ property,
199
+ value,
200
+ expected
201
+ };
202
+ };
203
+ /** A strict comparison whose answer is the same for every row, because the types cannot be equal. */ const comparesTypesThatCannotMatch = (expression)=>{
204
+ if (!(0,_assertions__rspack_import_1/* .isComparatorExpression */.xH)(expression) || expression.strict !== true) {
205
+ return false;
206
+ }
207
+ if (expression.comparator !== "equals") {
208
+ return false;
209
+ }
210
+ return mismatchedSide(expression.left, expression.right) != null || mismatchedSide(expression.right, expression.left) != null;
211
+ };
212
+ /** `JSON.stringify` throws on a BigInt, and this runs inside the guard that exists to catch one. */ const describeLiteral = (value)=>typeof value === "string" ? `"${value}"` : String(value);
213
+ const mismatchWarning = (expression)=>{
214
+ const side = mismatchedSide(expression.left, expression.right) ?? mismatchedSide(expression.right, expression.left);
215
+ const outcome = expression.negated ? "every row matches" : "no row matches";
216
+ return `Routier: '${side.property.property.getAssignmentPath()}' is a ${side.expected}, and this filter ` + `compares it against ${describeLiteral(side.value.value)}, which is a ${typeof side.value.value}. ` + `A strict comparison between them is the same answer for every row, so ${outcome} and the filter ` + `runs in memory. https://routier.dev/guides/strict-comparison-types`;
217
+ };
148
218
  class QueryOptionsCollection {
149
219
  options = new Map();
150
220
  nextExecutionTarget = "database";
151
221
  nextExecutionReason = null;
152
222
  nextIndex = 0;
153
223
  enumeratedItems = [];
224
+ dirty = true;
225
+ /** The collection a `splitAt`/`split` half came from. A capability report belongs to it. */ origin = null;
154
226
  /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */ cutOverToMemory(reason) {
155
227
  this.nextExecutionTarget = "memory";
156
228
  if (this.nextExecutionReason == null) {
157
229
  this.nextExecutionReason = reason;
158
230
  }
159
231
  }
160
- /**
161
- * True when `split()` or `splitAt()` produced this collection.
162
- *
163
- * Those rebuild each half by re-adding its options, which re-derives execution targets
164
- * without the options that caused them — a post-join filter alone in the memory half
165
- * derives back to `"database"`. Anything reading `target` as a report of where work runs
166
- * has to reject a derived collection; see `explainQuery`.
167
- */ derived = false;
168
- get isDerived() {
169
- return this.derived;
170
- }
171
232
  get items() {
172
233
  return this.options;
173
234
  }
@@ -202,7 +263,7 @@ class QueryOptionsCollection {
202
263
  if (filterValue.expression.type === "not-parsable") {
203
264
  this.cutOverToMemory("not-parsable");
204
265
  } else {
205
- (0,_expressions_utils__rspack_import_0/* .forEach */.j)(filterValue.expression, (expression)=>{
266
+ (0,_expressions_utils__rspack_import_2/* .forEach */.jJ)(filterValue.expression, (expression)=>{
206
267
  if ((0,_assertions__rspack_import_1/* .isPropertyExpression */.e3)(expression) && expression.property.isUnmapped) {
207
268
  // Cut over to memory execution, unmapped properties are not in the database and
208
269
  // cannot be queried
@@ -217,6 +278,11 @@ class QueryOptionsCollection {
217
278
  this.cutOverToMemory("renamed-property");
218
279
  return false;
219
280
  }
281
+ if (comparesTypesThatCannotMatch(expression)) {
282
+ _utilities__rspack_import_3/* .logger.warn */.vF.warn(mismatchWarning(expression));
283
+ this.cutOverToMemory("predicate-error");
284
+ return false;
285
+ }
220
286
  return true;
221
287
  });
222
288
  }
@@ -245,6 +311,11 @@ class QueryOptionsCollection {
245
311
  this.cutOverToMemory("renamed-property");
246
312
  }
247
313
  }
314
+ if ((name === "filter" || name === "sort") && (this.options.has("skip") || this.options.has("take"))) {
315
+ // SQL emits WHERE before LIMIT and Mongo's find() filters before skipping, so an option
316
+ // written after a window can only see the windowed rows if it runs after it.
317
+ this.cutOverToMemory("after-window");
318
+ }
248
319
  if (name === "join") {
249
320
  const joinValue = value;
250
321
  // A join whose two sides live on different plugins cannot be sent to EITHER of
@@ -257,18 +328,24 @@ class QueryOptionsCollection {
257
328
  this.cutOverToMemory("cross-plugin-join");
258
329
  }
259
330
  }
331
+ // `executed` is the plan, not a record: nothing has run when an option is added. Every
332
+ // consumer reads it after the plugin returned, so the optimistic window is never observed.
260
333
  const item = {
261
334
  index: this.nextIndex,
262
- option: {
335
+ option: this.nextExecutionTarget === "database" ? {
336
+ name,
337
+ value,
338
+ target: "database",
339
+ reason: "executed"
340
+ } : {
263
341
  name,
264
- target: this.nextExecutionTarget,
265
342
  value,
266
- ...this.nextExecutionReason == null ? {} : {
267
- reason: this.nextExecutionReason
268
- }
343
+ target: "memory",
344
+ reason: this.nextExecutionReason ?? "not-parsable"
269
345
  }
270
346
  };
271
347
  this.nextIndex++;
348
+ this.dirty = true;
272
349
  const found = this.options.get(name);
273
350
  this.options.set(name, [
274
351
  ...found ?? [],
@@ -313,8 +390,6 @@ class QueryOptionsCollection {
313
390
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
314
391
  const before = new QueryOptionsCollection();
315
392
  const after = new QueryOptionsCollection();
316
- before.derived = true;
317
- after.derived = true;
318
393
  let at = null;
319
394
  for(let i = 0, length = sortedItems.length; i < length; i++){
320
395
  const { option } = sortedItems[i];
@@ -323,8 +398,10 @@ class QueryOptionsCollection {
323
398
  continue;
324
399
  }
325
400
  const destination = at == null ? before : after;
326
- destination.add(option.name, option.value);
401
+ destination.adopt(sortedItems[i]);
327
402
  }
403
+ before.origin = this.origin ?? this;
404
+ after.origin = this.origin ?? this;
328
405
  return {
329
406
  before,
330
407
  at,
@@ -356,23 +433,99 @@ class QueryOptionsCollection {
356
433
  this.nextExecutionReason = nextExecutionReason;
357
434
  this.nextIndex = nextIndex;
358
435
  this.enumeratedItems = [];
436
+ // Clearing the list is not enough now that staleness is a flag rather than a count:
437
+ // without this, `resolveEnumeration` believes the empty list is current and every read
438
+ // of the collection sees no options at all.
439
+ this.dirty = true;
359
440
  };
360
441
  }
442
+ /** Takes an item as it stands — same object, same index, same target and reason. */ adopt(item) {
443
+ const found = this.options.get(item.option.name);
444
+ this.options.set(item.option.name, [
445
+ ...found ?? [],
446
+ item
447
+ ]);
448
+ this.nextIndex = Math.max(this.nextIndex, item.index + 1);
449
+ this.dirty = true;
450
+ }
451
+ /**
452
+ * A plugin reporting that its engine cannot express one option.
453
+ *
454
+ * Core marks the rest of the database phase `not-reached`, because the database has to stop
455
+ * there — a window applied in front of a filter that was not applied returns the wrong rows.
456
+ * Passing the cascade through core is what makes it impossible for a plugin to mark a
457
+ * non-contiguous cut.
458
+ *
459
+ * A report names a culprit and never un-names one, so reports commute.
460
+ *
461
+ * The option is not moved to the memory arm. It stays where it was planned, which is what keeps
462
+ * a redirect distinguishable from something core sent to memory in the first place.
463
+ */ reportMissingCapability(item) {
464
+ this.report(item, "missing-capability");
465
+ }
466
+ /**
467
+ * A plugin reporting that its engine would answer one option differently from JavaScript.
468
+ *
469
+ * Same cascade as `reportMissingCapability`, and a separate reason because the caller can act on
470
+ * one and not the other. See `DatabaseExecutionReason`.
471
+ */ reportEngineDivergence(item) {
472
+ this.report(item, "engine-divergence");
473
+ }
474
+ report(item, reason) {
475
+ // A half can only see its own slice, and the database has to stop for the whole dispatch.
476
+ if (this.origin != null) {
477
+ this.origin.report(item, reason);
478
+ return;
479
+ }
480
+ this.resolveEnumeration();
481
+ for (const candidate of this.enumeratedItems){
482
+ if (candidate.option.target !== "database" || candidate.index < item.index) {
483
+ continue;
484
+ }
485
+ if (candidate.index === item.index) {
486
+ candidate.option.reason = reason;
487
+ continue;
488
+ }
489
+ if (candidate.option.reason === "executed") {
490
+ candidate.option.reason = "not-reached";
491
+ }
492
+ }
493
+ }
494
+ /**
495
+ * Forgets what any previous dispatch reported.
496
+ *
497
+ * Capability is answered per dispatch, so a report is only an answer for the execution that
498
+ * produced it. The items are shared with any snapshot, so a report mutated in place otherwise
499
+ * survives a restore and a second terminal on the same queryable replays options the plugin
500
+ * did run — a `skip` applied twice, over rows already windowed.
501
+ */ forgetReports() {
502
+ this.resolveEnumeration();
503
+ for (const item of this.enumeratedItems){
504
+ if (item.option.target === "database") {
505
+ item.option.reason = "executed";
506
+ }
507
+ }
508
+ }
509
+ /** The options the database did not run, in the order they were written. */ notExecuted() {
510
+ this.resolveEnumeration();
511
+ return this.enumeratedItems.filter((item)=>item.option.target === "database" && item.option.reason !== "executed").toSorted((a, b)=>a.index - b.index);
512
+ }
361
513
  split() {
362
514
  this.resolveEnumeration();
363
515
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
364
516
  const memoryQueryOptionsCollection = new QueryOptionsCollection();
365
517
  const databaseQueryOptionsCollection = new QueryOptionsCollection();
366
- memoryQueryOptionsCollection.derived = true;
367
- databaseQueryOptionsCollection.derived = true;
368
518
  for(let i = 0, length = sortedItems.length; i < length; i++){
369
519
  const sortedItem = sortedItems[i];
370
- if (sortedItem.option.target === "database") {
371
- databaseQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
372
- continue;
373
- }
374
- memoryQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
520
+ const half = sortedItem.option.target === "database" ? databaseQueryOptionsCollection : memoryQueryOptionsCollection;
521
+ // The ITEM, not its name and value. Re-adding would re-derive target and reason from a
522
+ // fresh cascade, and a memory option re-added alone comes back out as `database` with no
523
+ // reason at all. Sharing it also means a plugin's report on the database half is the
524
+ // same object the explanation reads.
525
+ half.adopt(sortedItem);
375
526
  }
527
+ memoryQueryOptionsCollection.origin = this.origin ?? this;
528
+ databaseQueryOptionsCollection.origin = this.origin ?? this;
376
529
  return {
377
530
  memory: memoryQueryOptionsCollection,
378
531
  database: databaseQueryOptionsCollection
@@ -418,8 +571,11 @@ class QueryOptionsCollection {
418
571
  ].flat().toSorted((a, b)=>a.index - b.index);
419
572
  }
420
573
  resolveEnumeration() {
421
- if (this.enumeratedItems.length != this.nextIndex) {
574
+ // A flag, not a count: adopting leaves gaps in the indexes, so `length !== nextIndex` is
575
+ // true forever on a half and the enumeration rebuilds on every read.
576
+ if (this.dirty === true) {
422
577
  this.enumeratedItems = this.getEnumeration();
578
+ this.dirty = false;
423
579
  }
424
580
  }
425
581
  forEach(iterator) {
@@ -431,6 +587,41 @@ class QueryOptionsCollection {
431
587
  }
432
588
 
433
589
 
590
+ },
591
+ 537(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
592
+ __webpack_require__.d(__webpack_exports__, {
593
+ L: () => (SchemaTypes)
594
+ });
595
+ var SchemaTypes = /*#__PURE__*/ function(SchemaTypes) {
596
+ SchemaTypes["Array"] = "Array";
597
+ SchemaTypes["Boolean"] = "Boolean";
598
+ SchemaTypes["Date"] = "Date";
599
+ SchemaTypes["Number"] = "Number";
600
+ SchemaTypes["Object"] = "Object";
601
+ SchemaTypes["String"] = "String";
602
+ SchemaTypes["Definition"] = "Definition";
603
+ SchemaTypes["Function"] = "Function";
604
+ SchemaTypes["Computed"] = "Computed";
605
+ /**
606
+ * Content in, reference out. The only type whose write shape differs from its stored
607
+ * shape, and a leaf on purpose — see `SchemaFile`.
608
+ */ SchemaTypes["File"] = "File";
609
+ /**
610
+ * A fixed-length list of numbers, carrying its dimension count — see `SchemaVector`.
611
+ *
612
+ * Value-shaped exactly like `s.array(s.number())`, which is why every array codegen
613
+ * handler accepts it. It is a distinct type only so a backend can recognise it and store
614
+ * it natively; nothing else needs to tell the two apart.
615
+ */ SchemaTypes["Vector"] = "Vector";
616
+ return SchemaTypes;
617
+ }({});
618
+ var HashType = /*#__PURE__*/ (/* unused pure expression or super */ null && (function(HashType) {
619
+ HashType["Ids"] = "Ids";
620
+ HashType["Object"] = "Object";
621
+ return HashType;
622
+ }({})));
623
+
624
+
434
625
  },
435
626
  76(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
436
627
  __webpack_require__.d(__webpack_exports__, {
@@ -527,12 +718,14 @@ const isLogLevel = (value)=>typeof value === 'string' && LOG_LEVELS.includes(val
527
718
  const debug = process.env.DEBUG;
528
719
  if (debug === 'routier' || debug === '*') return 'debug';
529
720
  const env = "production"?.toLowerCase();
530
- // `test` is deliberately absent. It used to be here, which meant no test suite anywhere
531
- // could run Routier quietly. Opt in with DEBUG=routier or ROUTIER_LOG_LEVEL when a test
532
- // needs the output.
533
721
  if (env === 'dev' || env === 'development') return 'debug';
534
722
  }
535
- return 'silent';
723
+ // Warnings are on unless something turns them off.
724
+ //
725
+ // Routier warns when a query returns correct rows a slower way than it could, or when a filter
726
+ // compares types that can never match. Both are the caller's to act on, and a default of
727
+ // `silent` meant the only people who ever saw them were the ones who already knew to look.
728
+ return 'warn';
536
729
  };
537
730
  let level = resolveLevel();
538
731
  let rank = RANK[level];