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