@routier/core 0.6.0 → 0.8.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 (70) 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/codegen/blocks.d.ts +17 -1
  7. package/dist/codegen/handlers/types.d.ts +13 -5
  8. package/dist/codegen/index.cjs +28 -0
  9. package/dist/codegen/index.cjs.map +1 -1
  10. package/dist/codegen/index.js +28 -0
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/collections/MemoryDataCollection.d.ts +8 -0
  13. package/dist/collections/index.cjs +141 -4
  14. package/dist/collections/index.cjs.map +1 -1
  15. package/dist/collections/index.js +141 -4
  16. package/dist/collections/index.js.map +1 -1
  17. package/dist/expressions/callSource.d.ts +41 -0
  18. package/dist/expressions/evaluate.d.ts +3 -0
  19. package/dist/expressions/fold.d.ts +7 -0
  20. package/dist/expressions/index.cjs +1985 -242
  21. package/dist/expressions/index.cjs.map +1 -1
  22. package/dist/expressions/index.d.ts +2 -0
  23. package/dist/expressions/index.js +1997 -243
  24. package/dist/expressions/index.js.map +1 -1
  25. package/dist/expressions/parser.d.ts +42 -1
  26. package/dist/expressions/types.d.ts +45 -26
  27. package/dist/expressions/utils.d.ts +19 -1
  28. package/dist/index.cjs +3139 -680
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +3479 -997
  31. package/dist/index.js.map +1 -1
  32. package/dist/performance/index.cjs +6 -4
  33. package/dist/performance/index.cjs.map +1 -1
  34. package/dist/performance/index.js +6 -4
  35. package/dist/performance/index.js.map +1 -1
  36. package/dist/pipeline/index.cjs +6 -4
  37. package/dist/pipeline/index.cjs.map +1 -1
  38. package/dist/pipeline/index.js +6 -4
  39. package/dist/pipeline/index.js.map +1 -1
  40. package/dist/plugins/EphemeralDataPlugin.d.ts +8 -0
  41. package/dist/plugins/index.cjs +2921 -411
  42. package/dist/plugins/index.cjs.map +1 -1
  43. package/dist/plugins/index.js +2863 -343
  44. package/dist/plugins/index.js.map +1 -1
  45. package/dist/plugins/query/QueryOptionsCollection.d.ts +48 -10
  46. package/dist/plugins/query/describeFilter.d.ts +83 -0
  47. package/dist/plugins/query/explain.d.ts +71 -9
  48. package/dist/plugins/query/index.d.ts +2 -0
  49. package/dist/plugins/query/join.d.ts +4 -1
  50. package/dist/plugins/query/renames.d.ts +27 -0
  51. package/dist/plugins/query/types.d.ts +50 -4
  52. package/dist/plugins/translators/SqlTranslator.d.ts +15 -0
  53. package/dist/schema/PropertyInfo.d.ts +0 -1
  54. package/dist/schema/SchemaDefinition.d.ts +8 -0
  55. package/dist/schema/changeTracker.d.ts +10 -0
  56. package/dist/schema/index.cjs +298 -288
  57. package/dist/schema/index.cjs.map +1 -1
  58. package/dist/schema/index.d.ts +1 -0
  59. package/dist/schema/index.js +301 -290
  60. package/dist/schema/index.js.map +1 -1
  61. package/dist/schema/types.d.ts +8 -7
  62. package/dist/schema/utils/storageDates.d.ts +25 -0
  63. package/dist/transfer/index.cjs.map +1 -1
  64. package/dist/transfer/index.js.map +1 -1
  65. package/dist/utilities/index.cjs +306 -68
  66. package/dist/utilities/index.cjs.map +1 -1
  67. package/dist/utilities/index.js +306 -68
  68. package/dist/utilities/index.js.map +1 -1
  69. package/package.json +2 -2
  70. package/dist/codegen/utils.d.ts +0 -22
@@ -4,6 +4,7 @@ __webpack_require__.d(__webpack_exports__, {
4
4
  Cv: () => (assertString),
5
5
  S6: () => (isValueExpression),
6
6
  e3: () => (isPropertyExpression),
7
+ fm: () => (isCallExpression),
7
8
  jf: () => (assertIsNotNull),
8
9
  vg: () => (isOperatorExpression),
9
10
  xH: () => (isComparatorExpression),
@@ -79,6 +80,11 @@ function isObjectWithType(value) {
79
80
  */ function isValueExpression(value) {
80
81
  return isObjectWithType(value) && value.type === "value";
81
82
  }
83
+ /**
84
+ * Type guard: narrows `value` to `CallExpression` when it is an object with `type === "call"`.
85
+ */ function isCallExpression(value) {
86
+ return isObjectWithType(value) && value.type === "call";
87
+ }
82
88
  /**
83
89
  * Type guard: narrows `value` to `EmptyExpression` when it is an object with `type === "empty"`.
84
90
  */ function isEmptyExpression(value) {
@@ -399,45 +405,374 @@ __webpack_require__.d(__webpack_exports__, {
399
405
  }
400
406
 
401
407
 
408
+ },
409
+ 429(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
410
+ __webpack_require__.d(__webpack_exports__, {
411
+ a: () => (renderCallAsJs)
412
+ });
413
+ const CALL_SOURCE = {
414
+ "to-lower-case": {
415
+ form: "method",
416
+ name: "toLowerCase"
417
+ },
418
+ "to-upper-case": {
419
+ form: "method",
420
+ name: "toUpperCase"
421
+ },
422
+ "length": {
423
+ form: "property",
424
+ name: "length"
425
+ },
426
+ "trim": {
427
+ form: "method",
428
+ name: "trim"
429
+ },
430
+ "trim-start": {
431
+ form: "method",
432
+ name: "trimStart"
433
+ },
434
+ "trim-end": {
435
+ form: "method",
436
+ name: "trimEnd"
437
+ },
438
+ "index-of": {
439
+ form: "method",
440
+ name: "indexOf"
441
+ },
442
+ "substring": {
443
+ form: "method",
444
+ name: "substring"
445
+ },
446
+ "concat": {
447
+ form: "method",
448
+ name: "concat"
449
+ },
450
+ "replace": {
451
+ form: "method",
452
+ name: "replace"
453
+ },
454
+ "replace-all": {
455
+ form: "method",
456
+ name: "replaceAll"
457
+ },
458
+ "absolute": {
459
+ form: "function",
460
+ name: "Math.abs"
461
+ },
462
+ "floor": {
463
+ form: "function",
464
+ name: "Math.floor"
465
+ },
466
+ "ceiling": {
467
+ form: "function",
468
+ name: "Math.ceil"
469
+ },
470
+ "round": {
471
+ form: "function",
472
+ name: "Math.round"
473
+ },
474
+ "sign": {
475
+ form: "function",
476
+ name: "Math.sign"
477
+ },
478
+ "square-root": {
479
+ form: "function",
480
+ name: "Math.sqrt"
481
+ },
482
+ "add": {
483
+ form: "operator",
484
+ symbol: "+"
485
+ },
486
+ "subtract": {
487
+ form: "operator",
488
+ symbol: "-"
489
+ },
490
+ "multiply": {
491
+ form: "operator",
492
+ symbol: "*"
493
+ },
494
+ "divide": {
495
+ form: "operator",
496
+ symbol: "/"
497
+ },
498
+ "modulo": {
499
+ form: "operator",
500
+ symbol: "%"
501
+ },
502
+ "utc-year": {
503
+ form: "method",
504
+ name: "getUTCFullYear"
505
+ },
506
+ "utc-month": {
507
+ form: "method",
508
+ name: "getUTCMonth"
509
+ },
510
+ "utc-day-of-month": {
511
+ form: "method",
512
+ name: "getUTCDate"
513
+ },
514
+ "utc-day-of-week": {
515
+ form: "method",
516
+ name: "getUTCDay"
517
+ },
518
+ "utc-hour": {
519
+ form: "method",
520
+ name: "getUTCHours"
521
+ },
522
+ "utc-minute": {
523
+ form: "method",
524
+ name: "getUTCMinutes"
525
+ },
526
+ "utc-second": {
527
+ form: "method",
528
+ name: "getUTCSeconds"
529
+ },
530
+ "utc-millisecond": {
531
+ form: "method",
532
+ name: "getUTCMilliseconds"
533
+ },
534
+ "epoch-ms": {
535
+ form: "method",
536
+ name: "getTime"
537
+ },
538
+ "to-string": {
539
+ form: "function",
540
+ name: "String"
541
+ },
542
+ "to-number": {
543
+ form: "function",
544
+ name: "Number"
545
+ },
546
+ "to-boolean": {
547
+ form: "function",
548
+ name: "Boolean"
549
+ },
550
+ "type-of": {
551
+ form: "prefix",
552
+ keyword: "typeof"
553
+ },
554
+ "some": {
555
+ form: "method",
556
+ name: "some"
557
+ },
558
+ "every": {
559
+ form: "method",
560
+ name: "every"
561
+ },
562
+ // `Math.pow(a, b)` parses to the same call; `**` is the shorter of the two spellings
563
+ "power": {
564
+ form: "operator",
565
+ symbol: "**"
566
+ },
567
+ "bit-and": {
568
+ form: "operator",
569
+ symbol: "&"
570
+ },
571
+ "bit-or": {
572
+ form: "operator",
573
+ symbol: "|"
574
+ },
575
+ "bit-xor": {
576
+ form: "operator",
577
+ symbol: "^"
578
+ },
579
+ "shift-left": {
580
+ form: "operator",
581
+ symbol: "<<"
582
+ },
583
+ "shift-right": {
584
+ form: "operator",
585
+ symbol: ">>"
586
+ },
587
+ "shift-right-unsigned": {
588
+ form: "operator",
589
+ symbol: ">>>"
590
+ },
591
+ "bit-not": {
592
+ form: "prefix",
593
+ keyword: "~"
594
+ },
595
+ "coalesce": {
596
+ form: "operator",
597
+ symbol: "??"
598
+ },
599
+ "conditional": {
600
+ form: "conditional"
601
+ },
602
+ "matches": {
603
+ form: "regex-test"
604
+ }
605
+ };
606
+ /**
607
+ * A call rendered as the JavaScript that produced it, from operand and argument text already
608
+ * rendered by the caller.
609
+ *
610
+ * Takes strings so one implementation serves a live tree and a serialized one.
611
+ */ /**
612
+ * Thunked because rendering a side can record a parameter, and `regex-test` emits its argument
613
+ * before its operand — so the two orders have to agree.
614
+ */ const renderCallAsJs = (call, renderOperand, renderArgs)=>{
615
+ const source = CALL_SOURCE[call];
616
+ if (source == null) {
617
+ const operand = renderOperand();
618
+ return `${operand}.${call}(${renderArgs().join(", ")})`;
619
+ }
620
+ if (source.form === "property") {
621
+ return `${renderOperand()}.${source.name}`;
622
+ }
623
+ if (source.form === "regex-test") {
624
+ const pattern = renderArgs()[0] ?? "?";
625
+ return `${pattern}.test(${renderOperand()})`;
626
+ }
627
+ if (source.form === "method") {
628
+ const operand = renderOperand();
629
+ return `${operand}.${source.name}(${renderArgs().join(", ")})`;
630
+ }
631
+ if (source.form === "function") {
632
+ const operand = renderOperand();
633
+ return `${source.name}(${[
634
+ operand,
635
+ ...renderArgs()
636
+ ].join(", ")})`;
637
+ }
638
+ if (source.form === "prefix") {
639
+ // `~x`, not `~ x` — a bitwise complement is written tight, unlike `typeof`
640
+ const operand = renderOperand();
641
+ return source.keyword === "~" ? `${source.keyword}${operand}` : `${source.keyword} ${operand}`;
642
+ }
643
+ if (source.form === "conditional") {
644
+ const operand = renderOperand();
645
+ const args = renderArgs();
646
+ return `${operand} ? ${args[0] ?? "?"} : ${args[1] ?? "?"}`;
647
+ }
648
+ const operand = renderOperand();
649
+ return `${[
650
+ operand,
651
+ ...renderArgs()
652
+ ].join(` ${source.symbol} `)}`;
653
+ };
654
+
655
+
402
656
  },
403
657
  379(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
404
658
  __webpack_require__.d(__webpack_exports__, {
659
+ Vv: () => (operandValue),
660
+ _3: () => (evaluate),
661
+ gm: () => (UNRESOLVED),
405
662
  wS: () => (toStrictPredicate)
406
663
  });
407
664
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
408
665
 
409
666
  /** Reads a property or literal operand, or `UNRESOLVED` when the node is not one. */ const UNRESOLVED = Symbol("unresolved");
410
- const applyTransformer = (value, transformer)=>{
411
- if (transformer == null) {
412
- return value;
667
+ const ARITHMETIC = {
668
+ "add": (left, right)=>left + right,
669
+ "subtract": (left, right)=>left - right,
670
+ "multiply": (left, right)=>left * right,
671
+ "divide": (left, right)=>left / right,
672
+ "modulo": (left, right)=>left % right,
673
+ "power": (left, right)=>left ** right,
674
+ "bit-and": (left, right)=>left & right,
675
+ "bit-or": (left, right)=>left | right,
676
+ "bit-xor": (left, right)=>left ^ right,
677
+ "shift-left": (left, right)=>left << right,
678
+ "shift-right": (left, right)=>left >> right,
679
+ "shift-right-unsigned": (left, right)=>left >>> right
680
+ };
681
+ const applyCall = (call, value, args)=>{
682
+ // Above the guard: a template renders null as "null" in JavaScript, so these two are total.
683
+ if (call === "to-string") {
684
+ return String(value);
413
685
  }
414
- // A transformer applied to an absent value has no answer, and inventing one ("" for a missing
415
- // string) is how a filter starts matching rows it should not.
686
+ if (call === "concat") {
687
+ return [
688
+ value,
689
+ ...args
690
+ ].map(String).join("");
691
+ }
692
+ // A call applied to an absent value has no answer, and inventing one ("" for a missing string)
693
+ // is how a filter starts matching rows it should not.
416
694
  if (value == null) {
417
695
  return UNRESOLVED;
418
696
  }
419
- if (transformer === "to-lower-case") {
420
- return typeof value === "string" ? value.toLowerCase() : UNRESOLVED;
421
- }
422
- if (transformer === "to-upper-case") {
423
- return typeof value === "string" ? value.toUpperCase() : UNRESOLVED;
697
+ if (call === "to-lower-case" || call === "to-upper-case") {
698
+ if (typeof value !== "string") {
699
+ return UNRESOLVED;
700
+ }
701
+ const lower = call === "to-lower-case";
702
+ if (args.length === 0 || args[0] == null) {
703
+ return lower ? value.toLowerCase() : value.toUpperCase();
704
+ }
705
+ if (typeof args[0] !== "string") {
706
+ return UNRESOLVED;
707
+ }
708
+ try {
709
+ // An explicit locale is deterministic; dropping it answers a different question in Turkish.
710
+ return lower ? value.toLocaleLowerCase(args[0]) : value.toLocaleUpperCase(args[0]);
711
+ } catch {
712
+ // An invalid language tag throws RangeError; no answer beats the host's default.
713
+ return UNRESOLVED;
714
+ }
424
715
  }
425
- if (transformer === "length") {
716
+ if (call === "length") {
426
717
  return typeof value === "string" || Array.isArray(value) ? value.length : UNRESOLVED;
427
718
  }
719
+ if (call === "bit-not") {
720
+ return typeof value === "number" ? ~value : UNRESOLVED;
721
+ }
722
+ if (call === "matches") {
723
+ if (typeof value !== "string" || !(args[0] instanceof RegExp)) {
724
+ return UNRESOLVED;
725
+ }
726
+ // `test` advances `lastIndex` on a global or sticky pattern, and the pattern is shared with
727
+ // the cached template, where a source evaluates fresh in JavaScript.
728
+ return args[0].global || args[0].sticky ? new RegExp(args[0].source, args[0].flags).test(value) : args[0].test(value);
729
+ }
730
+ const arithmetic = ARITHMETIC[call];
731
+ if (arithmetic != null) {
732
+ return typeof value === "number" && typeof args[0] === "number" ? arithmetic(value, args[0]) : UNRESOLVED;
733
+ }
428
734
  return UNRESOLVED;
429
735
  };
430
- const operand = (expression, row)=>{
736
+ const operandValue = (expression, row)=>{
431
737
  if (expression == null) {
432
738
  return UNRESOLVED;
433
739
  }
434
740
  if ((0,_assertions__rspack_import_0/* .isValueExpression */.S6)(expression)) {
435
- return applyTransformer(expression.value, expression.transformer);
741
+ return expression.value;
436
742
  }
437
743
  if ((0,_assertions__rspack_import_0/* .isPropertyExpression */.e3)(expression)) {
438
744
  // Through the PropertyInfo, so a nested path and a `from`-renamed segment resolve the same
439
745
  // way every other consumer of the tree resolves them.
440
- return applyTransformer(expression.property.getValue(row), expression.transformer);
746
+ return expression.property.getValue(row);
747
+ }
748
+ if ((0,_assertions__rspack_import_0/* .isCallExpression */.fm)(expression)) {
749
+ /**
750
+ * `??` and `? :` are the two calls whose whole job is to answer when something is absent, so
751
+ * they run before the guard that refuses an absent operand.
752
+ */ if (expression.call === "coalesce") {
753
+ const left = operandValue(expression.expression, row);
754
+ return left === UNRESOLVED || left == null ? operandValue(expression.arguments[0], row) : left;
755
+ }
756
+ if (expression.call === "conditional") {
757
+ const condition = evaluate(expression.expression, row);
758
+ if (condition === undefined) {
759
+ return UNRESOLVED;
760
+ }
761
+ return operandValue(expression.arguments[condition === true ? 0 : 1], row);
762
+ }
763
+ const inner = operandValue(expression.expression, row);
764
+ if (inner === UNRESOLVED) {
765
+ return UNRESOLVED;
766
+ }
767
+ const args = [];
768
+ for (const argument of expression.arguments){
769
+ const resolved = operandValue(argument, row);
770
+ if (resolved === UNRESOLVED) {
771
+ return UNRESOLVED;
772
+ }
773
+ args.push(resolved);
774
+ }
775
+ return applyCall(expression.call, inner, args);
441
776
  }
442
777
  return UNRESOLVED;
443
778
  };
@@ -522,8 +857,8 @@ const evaluateComparator = (comparator, left, right, strict)=>{
522
857
  return left === false && right === false ? false : undefined;
523
858
  }
524
859
  if ((0,_assertions__rspack_import_0/* .isComparatorExpression */.xH)(expression)) {
525
- const left = operand(expression.left, row);
526
- const right = operand(expression.right, row);
860
+ const left = operandValue(expression.left, row);
861
+ const right = operandValue(expression.right, row);
527
862
  if (left === UNRESOLVED || right === UNRESOLVED) {
528
863
  return undefined;
529
864
  }
@@ -561,19 +896,130 @@ const evaluateComparator = (comparator, left, right, strict)=>{
561
896
  };
562
897
 
563
898
 
899
+ },
900
+ 43(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
901
+ __webpack_require__.d(__webpack_exports__, {
902
+ F5: () => (foldConstantCalls)
903
+ });
904
+ /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
905
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
906
+ /* import */ var _types__rspack_import_2 = __webpack_require__(27);
907
+ /* import */ var _utils__rspack_import_1 = __webpack_require__(63);
908
+
909
+
910
+
911
+
912
+ /** Calls fold may compute. Absent means a plugin declines it, so a new call is opt-in. */ const FOLDABLE = new Set([
913
+ "to-lower-case",
914
+ "to-upper-case",
915
+ "length",
916
+ "bit-not",
917
+ "matches",
918
+ "to-string",
919
+ "concat",
920
+ "add",
921
+ "subtract",
922
+ "multiply",
923
+ "divide",
924
+ "modulo",
925
+ "power",
926
+ "bit-and",
927
+ "bit-or",
928
+ "bit-xor",
929
+ "shift-left",
930
+ "shift-right",
931
+ "shift-right-unsigned",
932
+ "coalesce",
933
+ "conditional"
934
+ ]);
935
+ /** `String(value)` on an object is the host's rendering — a Date carries its timezone. */ const COERCES_TO_TEXT = new Set([
936
+ "to-string",
937
+ "concat"
938
+ ]);
939
+ const isFrozenPrimitive = (value)=>value == null || typeof value !== "object";
940
+ const readsAProperty = (expression)=>{
941
+ if ((0,_assertions__rspack_import_0/* .isPropertyExpression */.e3)(expression)) {
942
+ return true;
943
+ }
944
+ return (0,_utils__rspack_import_1/* .childrenOf */.LU)(expression).some(readsAProperty);
945
+ };
946
+ /** A `conditional` holds a condition where every other call holds a value. */ const isConstant = (call)=>{
947
+ if (!FOLDABLE.has(call.call) || !call.arguments.every(_assertions__rspack_import_0/* .isValueExpression */.S6)) {
948
+ return false;
949
+ }
950
+ if (COERCES_TO_TEXT.has(call.call) && [
951
+ call.expression,
952
+ ...call.arguments
953
+ ].some((operand)=>(0,_assertions__rspack_import_0/* .isValueExpression */.S6)(operand) && !isFrozenPrimitive(operand.value))) {
954
+ return false;
955
+ }
956
+ return call.call === "conditional" ? !readsAProperty(call.expression) : (0,_assertions__rspack_import_0/* .isValueExpression */.S6)(call.expression);
957
+ };
958
+ /** Computes every call whose operand and arguments are all literals. Runs after `bindExpression`. */ const foldConstantCalls = (expression)=>{
959
+ if ((0,_assertions__rspack_import_0/* .isCallExpression */.fm)(expression)) {
960
+ const folded = new _types__rspack_import_2/* .CallExpression */.DG({
961
+ call: expression.call,
962
+ expression: foldConstantCalls(expression.expression),
963
+ arguments: expression.arguments.map(foldConstantCalls)
964
+ });
965
+ if (!isConstant(folded)) {
966
+ return folded;
967
+ }
968
+ const value = (0,_evaluate__rspack_import_3/* .operandValue */.Vv)(folded, {});
969
+ return value === _evaluate__rspack_import_3/* .UNRESOLVED */.gm ? folded : new _types__rspack_import_2/* .ValueExpression */.Ko({
970
+ value
971
+ });
972
+ }
973
+ if ((0,_assertions__rspack_import_0/* .isComparatorExpression */.xH)(expression)) {
974
+ return new _types__rspack_import_2/* .ComparatorExpression */.bQ({
975
+ comparator: expression.comparator,
976
+ negated: expression.negated,
977
+ strict: expression.strict,
978
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
979
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
980
+ });
981
+ }
982
+ if ((0,_assertions__rspack_import_0/* .isOperatorExpression */.vg)(expression)) {
983
+ return new _types__rspack_import_2/* .OperatorExpression */.fw({
984
+ operator: expression.operator,
985
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
986
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
987
+ });
988
+ }
989
+ return expression;
990
+ };
991
+ /** The value a literal operand binds as once the calls on it are computed. Throws if it cannot. */ const foldedOperandValue = (operand, calls)=>{
992
+ if (calls.length === 0) {
993
+ return operand.value;
994
+ }
995
+ // `peelCalls` returns calls innermost first, so the last one evaluates the whole chain.
996
+ const outermost = calls[calls.length - 1];
997
+ const value = readsAProperty(outermost) ? UNRESOLVED : operandValue(outermost, {});
998
+ if (value === UNRESOLVED) {
999
+ throw new Error(`'${calls.map((call)=>call.call).join("', '")}' cannot be computed on the literal ` + `'${String(operand.value)}'.`);
1000
+ }
1001
+ return value;
1002
+ };
1003
+
1004
+
564
1005
  },
565
1006
  91(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
566
1007
  __webpack_require__.d(__webpack_exports__, {
567
1008
  MY: () => (toExpression)
568
1009
  });
569
- /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
1010
+ /* import */ var _utilities__rspack_import_4 = __webpack_require__(581);
570
1011
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
571
1012
  /* import */ var _schema__rspack_import_2 = __webpack_require__(537);
1013
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
1014
+ /* import */ var _fold__rspack_import_5 = __webpack_require__(43);
572
1015
  /* import */ var _types__rspack_import_1 = __webpack_require__(27);
573
1016
 
574
1017
 
575
1018
 
576
1019
 
1020
+
1021
+
1022
+
577
1023
  // Error message constants
578
1024
  const ERROR_MESSAGES = {
579
1025
  PROPERTY_NOT_FOUND: (path)=>`Error parsing query, could not find PropertyInfo for path: ${path}`,
@@ -619,8 +1065,13 @@ const converters = {
619
1065
  };
620
1066
  // Longest first so multi-character punctuation wins over its prefixes
621
1067
  const MULTI_CHARACTER_PUNCTUATION = [
1068
+ ">>>",
622
1069
  "===",
623
1070
  "!==",
1071
+ "**",
1072
+ "<<",
1073
+ ">>",
1074
+ "??",
624
1075
  "?.",
625
1076
  "&&",
626
1077
  "||",
@@ -657,9 +1108,17 @@ const SINGLE_CHARACTER_PUNCTUATION = new Set([
657
1108
  "?",
658
1109
  ":",
659
1110
  "&",
660
- "|"
1111
+ "|",
1112
+ "^",
1113
+ "~"
661
1114
  ]);
662
- const STRING_ESCAPES = {
1115
+ /**
1116
+ * A lookup table keyed by source text.
1117
+ *
1118
+ * Null-prototype: `TRANSFORM_METHODS["toString"]` otherwise returns `Object.prototype.toString`,
1119
+ * which is truthy, and the parser reads a method it does not support as one it does.
1120
+ */ const sourceKeyed = (entries)=>Object.assign(Object.create(null), entries);
1121
+ const STRING_ESCAPES = sourceKeyed({
663
1122
  "n": "\n",
664
1123
  "r": "\r",
665
1124
  "t": "\t",
@@ -667,6 +1126,24 @@ const STRING_ESCAPES = {
667
1126
  "f": "\f",
668
1127
  "v": "\v",
669
1128
  "0": "\0"
1129
+ });
1130
+ /**
1131
+ * Whether a `/` here opens a regex rather than dividing.
1132
+ *
1133
+ * A regex cannot follow a value. Everything else — the start of the source, an operator, an opening
1134
+ * bracket, a comma — is a position where only a regex makes sense.
1135
+ */ const regexCanStartHere = (tokens)=>{
1136
+ const previous = tokens[tokens.length - 1];
1137
+ if (previous == null) {
1138
+ return true;
1139
+ }
1140
+ if (previous.kind === "number" || previous.kind === "string" || previous.kind === "bigint" || previous.kind === "regex") {
1141
+ return false;
1142
+ }
1143
+ if (previous.kind === "identifier") {
1144
+ return false;
1145
+ }
1146
+ return previous.value !== ")" && previous.value !== "]";
670
1147
  };
671
1148
  const isIdentifierStart = (char)=>/[a-zA-Z_$]/.test(char);
672
1149
  const isIdentifierPart = (char)=>/[a-zA-Z0-9_$]/.test(char);
@@ -716,6 +1193,51 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
716
1193
  i++;
717
1194
  continue;
718
1195
  }
1196
+ /**
1197
+ * A regex literal, told from division by what came before it.
1198
+ *
1199
+ * `/` after a value — a number, string, identifier, `)` or `]` — is division. Anywhere else
1200
+ * it opens a regex. That is the same rule a JavaScript lexer uses, and it is why `x.a / 2`
1201
+ * and `/^a/.test(x.a)` can share a character.
1202
+ */ if (char === "/" && source[i + 1] !== "/" && source[i + 1] !== "*" && regexCanStartHere(tokens)) {
1203
+ let value = "";
1204
+ let inClass = false;
1205
+ let j = i + 1;
1206
+ while(j < source.length){
1207
+ const current = source[j];
1208
+ if (current === "\\") {
1209
+ value += current + (source[j + 1] ?? "");
1210
+ j += 2;
1211
+ continue;
1212
+ }
1213
+ if (current === "[") {
1214
+ inClass = true;
1215
+ } else if (current === "]") {
1216
+ inClass = false;
1217
+ } else if (current === "/" && inClass === false) {
1218
+ break;
1219
+ } else if (current === "\n") {
1220
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
1221
+ }
1222
+ value += current;
1223
+ j++;
1224
+ }
1225
+ if (j >= source.length) {
1226
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
1227
+ }
1228
+ j++;
1229
+ let flags = "";
1230
+ while(j < source.length && isIdentifierPart(source[j])){
1231
+ flags += source[j];
1232
+ j++;
1233
+ }
1234
+ i = j;
1235
+ tokens.push({
1236
+ kind: "regex",
1237
+ value: `${value}\u0000${flags}`
1238
+ });
1239
+ continue;
1240
+ }
719
1241
  // Comments
720
1242
  if (char === "/" && source[i + 1] === "/") {
721
1243
  while(i < source.length && source[i] !== "\n"){
@@ -735,6 +1257,8 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
735
1257
  if (char === "'" || char === "\"" || char === "`") {
736
1258
  const quote = char;
737
1259
  let value = "";
1260
+ const chunks = [];
1261
+ const expressions = [];
738
1262
  i++;
739
1263
  while(i < source.length && source[i] !== quote){
740
1264
  if (source[i] === "\\") {
@@ -749,8 +1273,43 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
749
1273
  i += 2;
750
1274
  continue;
751
1275
  }
752
- if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
753
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("template literal interpolation"));
1276
+ /**
1277
+ * An interpolation. The literal so far becomes a chunk and the expression source is
1278
+ * kept whole, to be parsed by its own stream — nesting means the inner source can
1279
+ * hold anything, including another template.
1280
+ */ if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
1281
+ let depth = 1;
1282
+ let expression = "";
1283
+ let at = i + 2;
1284
+ while(at < source.length && depth > 0){
1285
+ const current = source[at];
1286
+ if (current === "{") {
1287
+ depth++;
1288
+ } else if (current === "}") {
1289
+ depth--;
1290
+ if (depth === 0) {
1291
+ break;
1292
+ }
1293
+ } else if (current === "'" || current === '"' || current === "`") {
1294
+ const closing = current;
1295
+ expression += current;
1296
+ at++;
1297
+ while(at < source.length && source[at] !== closing){
1298
+ expression += source[at] === "\\" ? source[at] + (source[at + 1] ?? "") : source[at];
1299
+ at += source[at] === "\\" ? 2 : 1;
1300
+ }
1301
+ }
1302
+ expression += source[at];
1303
+ at++;
1304
+ }
1305
+ if (depth > 0) {
1306
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated template interpolation"));
1307
+ }
1308
+ chunks.push(value);
1309
+ expressions.push(expression);
1310
+ value = "";
1311
+ i = at + 1;
1312
+ continue;
754
1313
  }
755
1314
  value += source[i];
756
1315
  i++;
@@ -759,6 +1318,17 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
759
1318
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated string literal"));
760
1319
  }
761
1320
  i++; // consume closing quote
1321
+ if (expressions.length > 0) {
1322
+ chunks.push(value);
1323
+ tokens.push({
1324
+ kind: "template",
1325
+ value: JSON.stringify({
1326
+ chunks,
1327
+ expressions
1328
+ })
1329
+ });
1330
+ continue;
1331
+ }
762
1332
  tokens.push({
763
1333
  kind: "string",
764
1334
  value
@@ -802,6 +1372,14 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
802
1372
  }
803
1373
  }
804
1374
  }
1375
+ if (source[i] === "n") {
1376
+ i++;
1377
+ tokens.push({
1378
+ kind: "bigint",
1379
+ value: value.replace(/_/g, "")
1380
+ });
1381
+ continue;
1382
+ }
805
1383
  tokens.push({
806
1384
  kind: "number",
807
1385
  value: value.replace(/_/g, "")
@@ -852,6 +1430,24 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
852
1430
  constructor(tokens){
853
1431
  this.tokens = tokens;
854
1432
  }
1433
+ /** Inserts tokens at the cursor, bracketed so they keep their own precedence. */ splice(tokens) {
1434
+ const bracketed = [
1435
+ {
1436
+ kind: "punctuation",
1437
+ value: "("
1438
+ },
1439
+ ...tokens,
1440
+ {
1441
+ kind: "punctuation",
1442
+ value: ")"
1443
+ }
1444
+ ];
1445
+ this.tokens = [
1446
+ ...this.tokens.slice(0, this.index),
1447
+ ...bracketed,
1448
+ ...this.tokens.slice(this.index)
1449
+ ];
1450
+ }
855
1451
  get isAtEnd() {
856
1452
  return this.index >= this.tokens.length;
857
1453
  }
@@ -866,10 +1462,108 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
866
1462
  this.index++;
867
1463
  return token;
868
1464
  }
1465
+ /** The tokens of one statement's value, through the `;` or block end that closes it. */ takeStatementTokens() {
1466
+ const tokens = [];
1467
+ let depth = 0;
1468
+ while(!this.isAtEnd){
1469
+ const token = this.peek();
1470
+ if (token.kind === "punctuation") {
1471
+ if (token.value === "(" || token.value === "[" || token.value === "{") {
1472
+ depth++;
1473
+ } else if (token.value === ")" || token.value === "]" || token.value === "}") {
1474
+ if (depth === 0) {
1475
+ break;
1476
+ }
1477
+ depth--;
1478
+ } else if (token.value === ";" && depth === 0) {
1479
+ this.next();
1480
+ break;
1481
+ }
1482
+ }
1483
+ tokens.push(this.next());
1484
+ }
1485
+ if (tokens.length === 0) {
1486
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a declaration with no value"));
1487
+ }
1488
+ return tokens;
1489
+ }
869
1490
  isPunctuation(value, offset = 0) {
870
1491
  const token = this.peek(offset);
871
1492
  return token != null && token.kind === "punctuation" && token.value === value;
872
1493
  }
1494
+ /**
1495
+ * Whether the group starting here holds a value rather than a condition.
1496
+ *
1497
+ * `(a && b)` is a boolean sub-expression; `(x.name ?? '') === 'ada'` and `(x.name).length` are
1498
+ * values. Only the token after the matching bracket tells them apart, so the decision is made by
1499
+ * looking ahead rather than by parsing one way and catching the failure — a rewind on exception
1500
+ * would swallow a genuine syntax error inside the group and report it as something else.
1501
+ */ groupIsValue() {
1502
+ let depth = 0;
1503
+ let at = this.index;
1504
+ for(; at < this.tokens.length; at++){
1505
+ const token = this.tokens[at];
1506
+ if (token.kind !== "punctuation") {
1507
+ continue;
1508
+ }
1509
+ if (token.value === "(") {
1510
+ depth++;
1511
+ continue;
1512
+ }
1513
+ if (token.value === ")") {
1514
+ depth--;
1515
+ if (depth === 0) {
1516
+ break;
1517
+ }
1518
+ }
1519
+ }
1520
+ const after = this.tokens[at + 1];
1521
+ if (after == null || after.kind !== "punctuation") {
1522
+ return false;
1523
+ }
1524
+ return COMPARISON_OPERATORS[after.value] != null || after.value === "." || after.value === "?.";
1525
+ }
1526
+ /** Whether a `?` sits at the top level of what is left, so this is a conditional. */ holdsConditional() {
1527
+ let depth = 0;
1528
+ for(let at = this.index; at < this.tokens.length; at++){
1529
+ const token = this.tokens[at];
1530
+ if (token.kind !== "punctuation") {
1531
+ continue;
1532
+ }
1533
+ if (token.value === "(" || token.value === "[") {
1534
+ depth++;
1535
+ } else if (token.value === ")" || token.value === "]") {
1536
+ depth--;
1537
+ } else if (token.value === "?" && depth === 0) {
1538
+ return true;
1539
+ }
1540
+ }
1541
+ return false;
1542
+ }
1543
+ /** Whether the group starting here is `( … ? … : … )` rather than a plain value. */ groupHoldsConditional() {
1544
+ let depth = 0;
1545
+ for(let at = this.index; at < this.tokens.length; at++){
1546
+ const token = this.tokens[at];
1547
+ if (token.kind !== "punctuation") {
1548
+ continue;
1549
+ }
1550
+ if (token.value === "(") {
1551
+ depth++;
1552
+ continue;
1553
+ }
1554
+ if (token.value === ")") {
1555
+ depth--;
1556
+ if (depth === 0) {
1557
+ return false;
1558
+ }
1559
+ continue;
1560
+ }
1561
+ if (token.value === "?" && depth === 1) {
1562
+ return true;
1563
+ }
1564
+ }
1565
+ return false;
1566
+ }
873
1567
  matchPunctuation(value) {
874
1568
  if (this.isPunctuation(value)) {
875
1569
  this.index++;
@@ -883,12 +1577,104 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
883
1577
  }
884
1578
  }
885
1579
  }
886
- const COMPARATOR_METHODS = {
1580
+ /**
1581
+ * Calls JavaScript binds LOOSER than a comparison.
1582
+ *
1583
+ * This grammar reads a comparison's operands as values, which puts these tighter than they belong:
1584
+ * `x.flags & 6 === 2` is `x.flags & (6 === 2)` in JavaScript and would be read here as
1585
+ * `(x.flags & 6) === 2`. The two answer differently, so an ungrouped one is refused rather than
1586
+ * reinterpreted — the filter then runs in memory against the caller's own function, which is right by
1587
+ * construction. Brackets say which was meant, and JavaScript itself makes an unbracketed `??` mix a
1588
+ * syntax error for the same reason.
1589
+ */ const LOOSER_THAN_COMPARISON = [
1590
+ "bit-and",
1591
+ "bit-or",
1592
+ "bit-xor",
1593
+ "coalesce"
1594
+ ];
1595
+ const needsBrackets = (operand)=>operand.kind === "arithmetic" && operand.grouped !== true && LOOSER_THAN_COMPARISON.includes(operand.call);
1596
+ /** JavaScript precedence: `*`, `/`, `%` bind tighter than `+` and `-`. */ const MULTIPLICATIVE_OPERATORS = sourceKeyed({
1597
+ "*": "multiply",
1598
+ "/": "divide",
1599
+ "%": "modulo"
1600
+ });
1601
+ const ADDITIVE_OPERATORS = sourceKeyed({
1602
+ "+": "add",
1603
+ "-": "subtract"
1604
+ });
1605
+ const SHIFT_OPERATORS = sourceKeyed({
1606
+ "<<": "shift-left",
1607
+ ">>": "shift-right",
1608
+ ">>>": "shift-right-unsigned"
1609
+ });
1610
+ const BITWISE_AND_OPERATORS = sourceKeyed({
1611
+ "&": "bit-and"
1612
+ });
1613
+ const BITWISE_XOR_OPERATORS = sourceKeyed({
1614
+ "^": "bit-xor"
1615
+ });
1616
+ const BITWISE_OR_OPERATORS = sourceKeyed({
1617
+ "|": "bit-or"
1618
+ });
1619
+ const COALESCE_OPERATORS = sourceKeyed({
1620
+ "??": "coalesce"
1621
+ });
1622
+ /** Whether a schema property is reachable in here, which decides which side of a comparison it is. */ const containsProperty = (operand)=>{
1623
+ if (operand.kind === "property") {
1624
+ return true;
1625
+ }
1626
+ if (operand.kind === "conditional") {
1627
+ // A comparison always names a schema property, so the condition alone settles it
1628
+ return true;
1629
+ }
1630
+ if (operand.kind === "opaque") {
1631
+ return operand.reads.some(containsProperty);
1632
+ }
1633
+ return operand.kind === "arithmetic" && (containsProperty(operand.left) || containsProperty(operand.right) || operand.extra != null && containsProperty(operand.extra));
1634
+ };
1635
+ const DECLARATION_KEYWORDS = new Set([
1636
+ "const",
1637
+ "let",
1638
+ "var"
1639
+ ]);
1640
+ /** An operand whose value only a row can supply. */ const UNKNOWN_UNTIL_ROW = Symbol("unknown until row");
1641
+ /** The empty argument slot of a unary call. Compared by identity, so a real `undefined` still counts. */ const NO_ARGUMENT = Object.freeze({
1642
+ kind: "value",
1643
+ value: undefined,
1644
+ transformer: null,
1645
+ locale: null
1646
+ });
1647
+ const noArgument = ()=>NO_ARGUMENT;
1648
+ /** A predicate no row satisfies. Never reaches a tree: no expression node means "match nothing". */ const NEVER = "never";
1649
+ const and = (left, right)=>{
1650
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left)) {
1651
+ return right;
1652
+ }
1653
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1654
+ return left;
1655
+ }
1656
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1657
+ operator: "&&",
1658
+ left,
1659
+ right
1660
+ });
1661
+ };
1662
+ const or = (left, right)=>{
1663
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left) || _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1664
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
1665
+ }
1666
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1667
+ operator: "||",
1668
+ left,
1669
+ right
1670
+ });
1671
+ };
1672
+ const COMPARATOR_METHODS = sourceKeyed({
887
1673
  startsWith: "starts-with",
888
1674
  endsWith: "ends-with",
889
1675
  includes: "includes"
890
- };
891
- const TRANSFORM_METHODS = {
1676
+ });
1677
+ const TRANSFORM_METHODS = sourceKeyed({
892
1678
  toLowerCase: {
893
1679
  transformer: "to-lower-case",
894
1680
  locale: null
@@ -905,8 +1691,8 @@ const TRANSFORM_METHODS = {
905
1691
  transformer: "to-upper-case",
906
1692
  locale: "en-US"
907
1693
  }
908
- };
909
- const COMPARISON_OPERATORS = {
1694
+ });
1695
+ const COMPARISON_OPERATORS = sourceKeyed({
910
1696
  "==": {
911
1697
  comparator: "equals",
912
1698
  negated: false,
@@ -947,7 +1733,7 @@ const COMPARISON_OPERATORS = {
947
1733
  negated: false,
948
1734
  strict: false
949
1735
  }
950
- };
1736
+ });
951
1737
  const SWAPPED_COMPARATORS = {
952
1738
  "equals": "equals",
953
1739
  "greater-than": "less-than",
@@ -1018,16 +1804,21 @@ const resolveParamPath = (paramsName, path, data)=>{
1018
1804
  */ class ExpressionParser {
1019
1805
  schema;
1020
1806
  stream;
1021
- entityName;
1807
+ scope;
1022
1808
  paramsName;
1023
1809
  params;
1810
+ /**
1811
+ * Whether this parses a value selector rather than a filter, and so reads a call it has no node for
1812
+ * as an `OpaqueOperand` instead of refusing it.
1813
+ */ readsValues;
1024
1814
  /** Set when a param value shaped the tree itself (e.g. x[p.name]) — such templates cannot be cached. */ structurallyDependsOnParams = false;
1025
- constructor(schema, stream, entityName, paramsName, params){
1815
+ constructor(schema, stream, scope, paramsName, params, readsValues = false){
1026
1816
  this.schema = schema;
1027
1817
  this.stream = stream;
1028
- this.entityName = entityName;
1818
+ this.scope = scope;
1029
1819
  this.paramsName = paramsName;
1030
1820
  this.params = params;
1821
+ this.readsValues = readsValues;
1031
1822
  }
1032
1823
  parse() {
1033
1824
  const expression = this.parseOr();
@@ -1036,6 +1827,245 @@ const resolveParamPath = (paramsName, path, data)=>{
1036
1827
  }
1037
1828
  return expression;
1038
1829
  }
1830
+ parseBody() {
1831
+ if (!this.stream.isPunctuation("{")) {
1832
+ return this.parse();
1833
+ }
1834
+ const answer = this.parseBlock();
1835
+ if (!this.stream.isAtEnd) {
1836
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`unexpected token '${this.stream.peek()?.value}'`));
1837
+ }
1838
+ if (answer === NEVER) {
1839
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a predicate no row can satisfy"));
1840
+ }
1841
+ return answer;
1842
+ }
1843
+ /**
1844
+ * What a value selector returns: one value, or the fields of an object literal.
1845
+ *
1846
+ * A block body is read only when it does nothing but return, which is what a transpiler makes of an
1847
+ * arrow function. Anything more is refused, and the caller falls back to running the function.
1848
+ */ parseSelector() {
1849
+ const block = this.stream.matchPunctuation("{");
1850
+ if (block) {
1851
+ const keyword = this.stream.next();
1852
+ if (keyword.kind !== "identifier" || keyword.value !== "return") {
1853
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a selector block that does more than return"));
1854
+ }
1855
+ }
1856
+ const selected = this.stream.isPunctuation("{") ? this.parseObjectLiteral() : this.stream.isPunctuation("(") && this.stream.isPunctuation("{", 1) ? this.parseBracketedObjectLiteral() : this.parseInterpolation();
1857
+ if (block) {
1858
+ this.stream.matchPunctuation(";");
1859
+ this.stream.expectPunctuation("}");
1860
+ }
1861
+ if (!this.stream.isAtEnd) {
1862
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`unexpected token '${this.stream.peek()?.value}'`));
1863
+ }
1864
+ return selected;
1865
+ }
1866
+ /** `({ … })`, the arrow body that returns an object. */ parseBracketedObjectLiteral() {
1867
+ this.stream.expectPunctuation("(");
1868
+ const fields = this.parseObjectLiteral();
1869
+ this.stream.expectPunctuation(")");
1870
+ return fields;
1871
+ }
1872
+ /**
1873
+ * The fields of an object literal, each one value.
1874
+ *
1875
+ * A field's value is read without a top-level conditional, which needs brackets here: the look-ahead
1876
+ * for a `?` does not stop at the comma that ends the field.
1877
+ */ parseObjectLiteral() {
1878
+ this.stream.expectPunctuation("{");
1879
+ const fields = [];
1880
+ while(!this.stream.matchPunctuation("}")){
1881
+ const key = this.stream.next();
1882
+ if (key.kind !== "identifier" && key.kind !== "string") {
1883
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the object key '${key.value}'`));
1884
+ }
1885
+ fields.push({
1886
+ name: key.value,
1887
+ operand: this.stream.matchPunctuation(":") ? this.parseValue() : this.parseShorthand(key)
1888
+ });
1889
+ if (!this.stream.matchPunctuation(",")) {
1890
+ this.stream.expectPunctuation("}");
1891
+ break;
1892
+ }
1893
+ }
1894
+ return fields;
1895
+ }
1896
+ /** `{ name }`, which reads what the parameter list bound `name` to. */ parseShorthand(key) {
1897
+ const binding = key.kind === "identifier" ? this.scope.get(key.value) : undefined;
1898
+ if (binding == null || binding.kind === "inlined") {
1899
+ throw new Error(ERROR_MESSAGES.VARIABLE_VALUE(key.value));
1900
+ }
1901
+ return this.parseChain({
1902
+ kind: binding.kind,
1903
+ path: [
1904
+ ...binding.path
1905
+ ]
1906
+ });
1907
+ }
1908
+ /** The expression a `{ … }` block answers with. */ parseBlock() {
1909
+ this.stream.expectPunctuation("{");
1910
+ const answer = this.parseStatements();
1911
+ this.stream.expectPunctuation("}");
1912
+ return answer;
1913
+ }
1914
+ /** Statements up to the one that returns. What follows a `return` is never read, as in JavaScript. */ parseStatements() {
1915
+ if (this.stream.isPunctuation("}") || this.stream.isAtEnd) {
1916
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a block body that returns nothing"));
1917
+ }
1918
+ const keyword = this.stream.peek();
1919
+ if (keyword == null || keyword.kind !== "identifier") {
1920
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`a statement starting '${keyword?.value}'`));
1921
+ }
1922
+ if (DECLARATION_KEYWORDS.has(keyword.value)) {
1923
+ this.declare();
1924
+ return this.parseStatements();
1925
+ }
1926
+ if (keyword.value === "return") {
1927
+ this.stream.next();
1928
+ const answer = this.parseReturnedCondition();
1929
+ this.stream.matchPunctuation(";");
1930
+ return answer;
1931
+ }
1932
+ if (keyword.value === "if") {
1933
+ return this.parseIfStatement();
1934
+ }
1935
+ if (keyword.value === "switch") {
1936
+ return this.parseSwitchStatement();
1937
+ }
1938
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the statement '${keyword.value}'`));
1939
+ }
1940
+ /**
1941
+ * Binds a `const`/`let`/`var` name to the tokens of its initializer — tokens rather than a parsed
1942
+ * expression, so the name works as an operand, an argument, or a call receiver alike.
1943
+ */ declare() {
1944
+ this.stream.next();
1945
+ const name = this.stream.next();
1946
+ if (name.kind !== "identifier") {
1947
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the declaration '${name.value}'`));
1948
+ }
1949
+ this.stream.expectPunctuation("=");
1950
+ this.scope.set(name.value, {
1951
+ kind: "inlined",
1952
+ tokens: this.stream.takeStatementTokens()
1953
+ });
1954
+ }
1955
+ /** `return false` on its own, which no row satisfies, and every other returned condition. */ parseReturnedCondition() {
1956
+ const next = this.stream.peek();
1957
+ const after = this.stream.peek(1);
1958
+ const endsHere = after == null || after.kind === "punctuation" && (after.value === ";" || after.value === "}");
1959
+ if (next != null && next.kind === "identifier" && next.value === "false" && endsHere) {
1960
+ this.stream.next();
1961
+ return NEVER;
1962
+ }
1963
+ return this.parseOr();
1964
+ }
1965
+ parseIfStatement() {
1966
+ this.stream.next();
1967
+ this.stream.expectPunctuation("(");
1968
+ const condition = this.parseOr();
1969
+ this.stream.expectPunctuation(")");
1970
+ const whenTrue = this.parseBranch();
1971
+ if (this.stream.peek()?.value === "else") {
1972
+ this.stream.next();
1973
+ return this.either(condition, whenTrue, this.parseBranch());
1974
+ }
1975
+ // Without an `else`, the statements after the `if` are the other branch
1976
+ return this.either(condition, whenTrue, this.parseStatements());
1977
+ }
1978
+ /** One arm of an `if`: a block, or a single statement. */ parseBranch() {
1979
+ return this.stream.isPunctuation("{") ? this.parseBlock() : this.parseStatements();
1980
+ }
1981
+ /** A `switch` over one subject, as the disjunction of its cases. */ parseSwitchStatement() {
1982
+ this.stream.next();
1983
+ this.stream.expectPunctuation("(");
1984
+ const subject = this.parseValue();
1985
+ this.stream.expectPunctuation(")");
1986
+ this.stream.expectPunctuation("{");
1987
+ let matching = null;
1988
+ let pending = [];
1989
+ let everyLabel = [];
1990
+ let byDefault = null;
1991
+ let anyCaseBroke = false;
1992
+ while(!this.stream.matchPunctuation("}")){
1993
+ const label = this.stream.next();
1994
+ if (label.kind !== "identifier" || label.value !== "case" && label.value !== "default") {
1995
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'${label.value}' inside a switch`));
1996
+ }
1997
+ if (label.value === "case") {
1998
+ const test = this.buildComparison(subject, COMPARISON_OPERATORS["==="], this.parseValue());
1999
+ pending.push(test);
2000
+ everyLabel.push(test);
2001
+ }
2002
+ this.stream.expectPunctuation(":");
2003
+ // `case 'a':` with no body of its own runs the next case's body
2004
+ if (this.stream.peek()?.value === "case" || this.stream.peek()?.value === "default") {
2005
+ continue;
2006
+ }
2007
+ if (this.stream.peek()?.value === "break") {
2008
+ this.stream.next();
2009
+ this.stream.matchPunctuation(";");
2010
+ anyCaseBroke = true;
2011
+ pending = [];
2012
+ continue;
2013
+ }
2014
+ const body = this.parseCaseBody();
2015
+ if (label.value === "default") {
2016
+ byDefault = body === NEVER ? null : body;
2017
+ continue;
2018
+ }
2019
+ if (body !== NEVER && pending.length > 0) {
2020
+ const reached = pending.reduce((left, right)=>or(left, right));
2021
+ const term = _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(body) ? reached : and(reached, body);
2022
+ matching = matching == null ? term : or(matching, term);
2023
+ }
2024
+ pending = [];
2025
+ }
2026
+ // Falling out of the switch continues after it, so the statements there are the default too
2027
+ const afterSwitch = byDefault == null && !this.stream.isPunctuation("}") && !this.stream.isAtEnd ? this.parseStatements() : NEVER;
2028
+ if (afterSwitch !== NEVER) {
2029
+ // A `break` also continues after the switch, so its case would take that answer rather
2030
+ // than none — a distinction this rewrite cannot carry
2031
+ if (anyCaseBroke) {
2032
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a switch that breaks and then falls into more statements"));
2033
+ }
2034
+ byDefault = afterSwitch;
2035
+ }
2036
+ // A `default` runs only when every case failed, wherever it was written
2037
+ if (byDefault != null) {
2038
+ const noCaseMatched = everyLabel.length === 0 ? byDefault : and(this.negateExpression(everyLabel.reduce((left, right)=>or(left, right))), byDefault);
2039
+ matching = matching == null ? noCaseMatched : or(matching, noCaseMatched);
2040
+ }
2041
+ return matching ?? NEVER;
2042
+ }
2043
+ /** One case body, and the `break` that may follow its `return`. */ parseCaseBody() {
2044
+ const answer = this.parseStatements();
2045
+ if (this.stream.peek()?.value === "break") {
2046
+ this.stream.next();
2047
+ this.stream.matchPunctuation(";");
2048
+ }
2049
+ return answer;
2050
+ }
2051
+ /**
2052
+ * The predicate an `if`/`else` answers: `(condition && whenTrue) || (!condition && whenFalse)`,
2053
+ * with each case below that form after a constant branch cancels out.
2054
+ */ either(condition, whenTrue, whenFalse) {
2055
+ if (whenTrue === NEVER) {
2056
+ return whenFalse === NEVER ? NEVER : and(this.negateExpression(condition), whenFalse);
2057
+ }
2058
+ if (whenFalse === NEVER) {
2059
+ return and(condition, whenTrue);
2060
+ }
2061
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenTrue)) {
2062
+ return or(condition, whenFalse);
2063
+ }
2064
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenFalse)) {
2065
+ return or(this.negateExpression(condition), whenTrue);
2066
+ }
2067
+ return or(and(condition, whenTrue), and(this.negateExpression(condition), whenFalse));
2068
+ }
1039
2069
  // || binds loosest, so it sits at the root of the parse
1040
2070
  parseOr() {
1041
2071
  let left = this.parseAnd();
@@ -1083,10 +2113,18 @@ const resolveParamPath = (paramsName, path, data)=>{
1083
2113
  /**
1084
2114
  * Applies `!` to an already-parsed expression: comparators flip their
1085
2115
  * negated flag, compound expressions distribute via De Morgan's laws.
2116
+ *
2117
+ * Builds a new tree rather than flipping the flag in place, because an `if` uses its condition
2118
+ * twice — once negated — and a shared node would carry the flip into both branches.
1086
2119
  */ negateExpression(expression) {
1087
2120
  if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
1088
- expression.negated = !expression.negated;
1089
- return expression;
2121
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2122
+ comparator: expression.comparator,
2123
+ negated: !expression.negated,
2124
+ strict: expression.strict,
2125
+ left: expression.left,
2126
+ right: expression.right
2127
+ });
1090
2128
  }
1091
2129
  if (expression instanceof _types__rspack_import_1/* .OperatorExpression */.fw && expression.left != null && expression.right != null) {
1092
2130
  return new _types__rspack_import_1/* .OperatorExpression */.fw({
@@ -1098,25 +2136,125 @@ const resolveParamPath = (paramsName, path, data)=>{
1098
2136
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("'!' on this expression"));
1099
2137
  }
1100
2138
  parseComparison() {
1101
- // Parenthesized group
1102
- if (this.stream.matchPunctuation("(")) {
2139
+ /**
2140
+ * A parenthesised group is either a boolean sub-expression or a VALUE — `(a && b)` against
2141
+ * `(x.name ?? '') === 'ada'` — and which one it is is only known at the closing bracket, by
2142
+ * what follows. So the boolean reading is tried first and rewound if a comparator turns up.
2143
+ */ if (this.stream.isPunctuation("(") && this.stream.groupIsValue() === false) {
2144
+ this.stream.next();
1103
2145
  const expression = this.parseOr();
1104
2146
  this.stream.expectPunctuation(")");
1105
- const trailing = this.stream.peek();
1106
- if (trailing != null && trailing.kind === "punctuation" && COMPARISON_OPERATORS[trailing.value] != null) {
1107
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison against a parenthesized expression"));
1108
- }
1109
2147
  return expression;
1110
2148
  }
1111
- const left = this.parseOperand();
2149
+ const left = this.parseValue();
1112
2150
  const operatorToken = this.stream.peek();
1113
2151
  if (operatorToken != null && operatorToken.kind === "punctuation" && COMPARISON_OPERATORS[operatorToken.value] != null) {
1114
2152
  this.stream.next();
1115
- const right = this.parseOperand();
2153
+ const right = this.parseValue();
1116
2154
  return this.buildComparison(left, COMPARISON_OPERATORS[operatorToken.value], right);
1117
2155
  }
1118
2156
  return this.buildStandalone(left);
1119
2157
  }
2158
+ /**
2159
+ * A value, at JavaScript's precedence.
2160
+ *
2161
+ * Lowest first: the conditional operator, then nullish coalescing, then the bitwise levels, then
2162
+ * the shifts, then the arithmetic. Comparison sits between the shifts and the bitwise levels in
2163
+ * JavaScript, but a comparison is a boolean and is handled by `parseComparison` above, so this
2164
+ * chain skips it — a bitwise operand here is always a value.
2165
+ */ /**
2166
+ * An operand from its own source, sharing this parser's schema and parameter names.
2167
+ *
2168
+ * A structural dependence found inside propagates outward: the template it belongs to cannot be
2169
+ * cached either.
2170
+ */ parseNested(source) {
2171
+ const nested = new ExpressionParser(this.schema, new TokenStream(tokenize(source)), this.scope, this.paramsName, this.params, this.readsValues);
2172
+ const operand = nested.parseInterpolation();
2173
+ // Leftover tokens mean the interpolation held something this reads only part of. Silently
2174
+ // keeping the part it understood is the worst outcome available: `${x.age > 5 ? "a" : "b"}`
2175
+ // would become `x.age`, and the filter would answer a question nobody asked.
2176
+ if (nested.stream.isAtEnd === false) {
2177
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("an interpolation this parser reads only part of"));
2178
+ }
2179
+ if (nested.structurallyDependsOnParams === true) {
2180
+ this.structurallyDependsOnParams = true;
2181
+ }
2182
+ return operand;
2183
+ }
2184
+ /**
2185
+ * The whole of one `${…}`.
2186
+ *
2187
+ * A conditional is read here rather than in `parseValue`, because an interpolation is the one
2188
+ * place a conditional appears without brackets around it.
2189
+ */ parseInterpolation() {
2190
+ if (this.stream.holdsConditional()) {
2191
+ const condition = this.parseOr();
2192
+ this.stream.expectPunctuation("?");
2193
+ const whenTrue = this.parseValue();
2194
+ this.stream.expectPunctuation(":");
2195
+ const whenFalse = this.parseValue();
2196
+ return {
2197
+ kind: "conditional",
2198
+ condition,
2199
+ whenTrue,
2200
+ whenFalse
2201
+ };
2202
+ }
2203
+ return this.parseValue();
2204
+ }
2205
+ parseValue() {
2206
+ return this.parseCoalesce();
2207
+ }
2208
+ parseCoalesce() {
2209
+ return this.parseBinary(COALESCE_OPERATORS, ()=>this.parseBitwiseOr());
2210
+ }
2211
+ parseBitwiseOr() {
2212
+ return this.parseBinary(BITWISE_OR_OPERATORS, ()=>this.parseBitwiseXor());
2213
+ }
2214
+ parseBitwiseXor() {
2215
+ return this.parseBinary(BITWISE_XOR_OPERATORS, ()=>this.parseBitwiseAnd());
2216
+ }
2217
+ parseBitwiseAnd() {
2218
+ return this.parseBinary(BITWISE_AND_OPERATORS, ()=>this.parseShift());
2219
+ }
2220
+ parseShift() {
2221
+ return this.parseBinary(SHIFT_OPERATORS, ()=>this.parseAdditive());
2222
+ }
2223
+ parseAdditive() {
2224
+ return this.parseBinary(ADDITIVE_OPERATORS, ()=>this.parseMultiplicative());
2225
+ }
2226
+ parseMultiplicative() {
2227
+ return this.parseBinary(MULTIPLICATIVE_OPERATORS, ()=>this.parseExponent());
2228
+ }
2229
+ /** `**` is RIGHT-associative: `2 ** 3 ** 2` is 2 ** 9, not 8 ** 2. */ parseExponent() {
2230
+ const left = this.parseOperand();
2231
+ if (this.stream.isPunctuation("**") === false) {
2232
+ return left;
2233
+ }
2234
+ this.stream.next();
2235
+ return {
2236
+ kind: "arithmetic",
2237
+ call: "power",
2238
+ left,
2239
+ right: this.parseExponent()
2240
+ };
2241
+ }
2242
+ /** Left-associative, so `a - b - c` is `(a - b) - c` rather than `a - (b - c)`. */ parseBinary(operators, next) {
2243
+ let left = next();
2244
+ for(;;){
2245
+ const token = this.stream.peek();
2246
+ if (token == null || token.kind !== "punctuation" || operators[token.value] == null) {
2247
+ return left;
2248
+ }
2249
+ this.stream.next();
2250
+ left = {
2251
+ kind: "arithmetic",
2252
+ call: operators[token.value],
2253
+ left,
2254
+ right: next()
2255
+ };
2256
+ }
2257
+ }
1120
2258
  parseOperand() {
1121
2259
  const token = this.stream.peek();
1122
2260
  if (token == null) {
@@ -1140,6 +2278,131 @@ const resolveParamPath = (paramsName, path, data)=>{
1140
2278
  locale: null
1141
2279
  };
1142
2280
  }
2281
+ // A parenthesised VALUE — `(x.price & 1)`, `(x.name ?? '')`. The boolean reading of a group
2282
+ // is handled in parseComparison; by the time an operand sees one it is arithmetic.
2283
+ if (token.kind === "punctuation" && token.value === "(") {
2284
+ const conditional = this.stream.groupHoldsConditional();
2285
+ this.stream.next();
2286
+ if (conditional === true) {
2287
+ const condition = this.parseOr();
2288
+ this.stream.expectPunctuation("?");
2289
+ const whenTrue = this.parseValue();
2290
+ this.stream.expectPunctuation(":");
2291
+ const whenFalse = this.parseValue();
2292
+ this.stream.expectPunctuation(")");
2293
+ return {
2294
+ kind: "conditional",
2295
+ condition,
2296
+ whenTrue,
2297
+ whenFalse
2298
+ };
2299
+ }
2300
+ const inner = this.parseValue();
2301
+ this.stream.expectPunctuation(")");
2302
+ const grouped = inner.kind === "arithmetic" ? {
2303
+ ...inner,
2304
+ grouped: true
2305
+ } : inner;
2306
+ return this.withGroupCall(grouped);
2307
+ }
2308
+ /**
2309
+ * A template with interpolation, folded into `concat`.
2310
+ *
2311
+ * Each `${…}` was kept as source by the tokenizer and is parsed by its own stream, so it can
2312
+ * hold anything an operand can — a property, a param, arithmetic, another template. Empty
2313
+ * chunks are dropped: `${a}${b}` is two operands, not two operands and three empty strings.
2314
+ */ if (token.kind === "template") {
2315
+ this.stream.next();
2316
+ const { chunks, expressions } = JSON.parse(token.value);
2317
+ const pieces = [];
2318
+ for(let at = 0; at < chunks.length; at++){
2319
+ if (chunks[at].length > 0) {
2320
+ pieces.push({
2321
+ kind: "value",
2322
+ value: chunks[at],
2323
+ transformer: null,
2324
+ locale: null
2325
+ });
2326
+ }
2327
+ if (at < expressions.length) {
2328
+ pieces.push(this.parseNested(expressions[at]));
2329
+ }
2330
+ }
2331
+ if (pieces.length === 0) {
2332
+ return {
2333
+ kind: "value",
2334
+ value: "",
2335
+ transformer: null,
2336
+ locale: null
2337
+ };
2338
+ }
2339
+ // One piece and no chunk means no concat to do the coercion, so the conversion has to be
2340
+ // explicit: `` `${x.age}` `` is the STRING "9", not the number 9.
2341
+ if (pieces.length === 1) {
2342
+ const only = pieces[0];
2343
+ const alreadyText = only.kind === "value" && typeof only.value === "string";
2344
+ return alreadyText ? only : {
2345
+ kind: "arithmetic",
2346
+ call: "to-string",
2347
+ left: only,
2348
+ right: noArgument()
2349
+ };
2350
+ }
2351
+ return pieces.reduce((left, right)=>({
2352
+ kind: "arithmetic",
2353
+ call: "concat",
2354
+ left,
2355
+ right
2356
+ }));
2357
+ }
2358
+ if (token.kind === "bigint") {
2359
+ this.stream.next();
2360
+ return {
2361
+ kind: "value",
2362
+ value: BigInt(token.value),
2363
+ transformer: null,
2364
+ locale: null
2365
+ };
2366
+ }
2367
+ if (token.kind === "regex") {
2368
+ this.stream.next();
2369
+ const [source, flags] = token.value.split("\u0000");
2370
+ const pattern = {
2371
+ kind: "value",
2372
+ value: new RegExp(source, flags),
2373
+ transformer: null,
2374
+ locale: null
2375
+ };
2376
+ // `/^a/.test(x.name)` — the pattern is the literal, the subject is the argument, and the
2377
+ // tree puts them the other way round: the property is what the call applies to.
2378
+ if (this.stream.isPunctuation(".")) {
2379
+ const method = this.stream.peek(1);
2380
+ if (method != null && method.kind === "identifier" && method.value === "test") {
2381
+ this.stream.next();
2382
+ this.stream.next();
2383
+ this.stream.expectPunctuation("(");
2384
+ const subject = this.parseValue();
2385
+ this.stream.expectPunctuation(")");
2386
+ return {
2387
+ kind: "arithmetic",
2388
+ call: "matches",
2389
+ left: subject,
2390
+ right: pattern
2391
+ };
2392
+ }
2393
+ }
2394
+ return pattern;
2395
+ }
2396
+ if (token.kind === "punctuation" && token.value === "~") {
2397
+ this.stream.next();
2398
+ // Unary, so the tree carries the operand and no argument
2399
+ return {
2400
+ kind: "arithmetic",
2401
+ call: "bit-not",
2402
+ left: this.parseOperand(),
2403
+ right: noArgument()
2404
+ };
2405
+ }
1143
2406
  if (token.kind === "punctuation" && token.value === "-") {
1144
2407
  this.stream.next();
1145
2408
  const numberToken = this.stream.next();
@@ -1197,6 +2460,9 @@ const resolveParamPath = (paramsName, path, data)=>{
1197
2460
  if (argument.kind === "method-call") {
1198
2461
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("nested method call inside .includes()"));
1199
2462
  }
2463
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2464
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic inside .includes()"));
2465
+ }
1200
2466
  return {
1201
2467
  kind: "method-call",
1202
2468
  target: array,
@@ -1242,16 +2508,17 @@ const resolveParamPath = (paramsName, path, data)=>{
1242
2508
  locale: null
1243
2509
  };
1244
2510
  }
1245
- if (root === this.entityName) {
1246
- return this.parseChain({
1247
- kind: "property",
1248
- root
1249
- });
1250
- }
1251
- if (this.paramsName != null && root === this.paramsName) {
2511
+ const binding = this.scope.get(root);
2512
+ if (binding != null) {
2513
+ if (binding.kind === "inlined") {
2514
+ this.stream.splice(binding.tokens);
2515
+ return this.parseOperand();
2516
+ }
1252
2517
  return this.parseChain({
1253
- kind: "param",
1254
- root
2518
+ kind: binding.kind,
2519
+ path: [
2520
+ ...binding.path
2521
+ ]
1255
2522
  });
1256
2523
  }
1257
2524
  // A bare variable from the outer scope — its value cannot be derived from source text
@@ -1261,7 +2528,7 @@ const resolveParamPath = (paramsName, path, data)=>{
1261
2528
  * Parses the segments after an entity/params root: dot access, bracket
1262
2529
  * access, transform methods and comparator methods.
1263
2530
  */ parseChain(options) {
1264
- const path = [];
2531
+ const path = options.path;
1265
2532
  let transformer = null;
1266
2533
  let locale = null;
1267
2534
  while(true){
@@ -1287,6 +2554,9 @@ const resolveParamPath = (paramsName, path, data)=>{
1287
2554
  if (argument.kind === "method-call") {
1288
2555
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`nested method call inside .${method}()`));
1289
2556
  }
2557
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2558
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`arithmetic inside .${method}()`));
2559
+ }
1290
2560
  return {
1291
2561
  kind: "method-call",
1292
2562
  target: this.resolveChain(options.kind, path, transformer, locale),
@@ -1294,9 +2564,20 @@ const resolveParamPath = (paramsName, path, data)=>{
1294
2564
  argument
1295
2565
  };
1296
2566
  }
2567
+ if (this.readsValues) {
2568
+ return this.withGroupCall(this.opaqueCall(this.resolveChain(options.kind, path, transformer, locale)));
2569
+ }
1297
2570
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`method '.${method}()'`));
1298
2571
  }
1299
2572
  if (transformer != null) {
2573
+ if (this.readsValues) {
2574
+ return this.withGroupCall({
2575
+ kind: "opaque",
2576
+ reads: [
2577
+ this.resolveChain(options.kind, path, transformer, locale)
2578
+ ]
2579
+ });
2580
+ }
1300
2581
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("property access after a transform method"));
1301
2582
  }
1302
2583
  path.push(segment.value);
@@ -1323,12 +2604,15 @@ const resolveParamPath = (paramsName, path, data)=>{
1323
2604
  // docs/mutation-backlog.md) — every mutation of this four-conjunct guard reroutes
1324
2605
  // bracket access between two paths that both collapse to NOT_PARSABLE; the
1325
2606
  // experiment recorded there aimed 30 tests at this line and killed none.
1326
- if (kind === "property" && token.kind === "identifier" && this.paramsName != null && token.value === this.paramsName) {
1327
- const paramPath = [];
2607
+ const binding = token.kind === "identifier" ? this.scope.get(token.value) : undefined;
2608
+ if (kind === "property" && binding != null && binding.kind === "param") {
2609
+ const paramPath = [
2610
+ ...binding.path
2611
+ ];
1328
2612
  while(this.stream.matchPunctuation(".") || this.stream.matchPunctuation("?.")){
1329
2613
  paramPath.push(this.stream.next().value);
1330
2614
  }
1331
- const resolved = resolveParamPath(this.paramsName, paramPath, this.params);
2615
+ const resolved = resolveParamPath(this.paramsName ?? token.value, paramPath, this.params);
1332
2616
  if (typeof resolved !== "string") {
1333
2617
  throw new ParamDependentParseError(ERROR_MESSAGES.PROPERTY_NOT_FOUND(paramPath.join(".")));
1334
2618
  }
@@ -1372,13 +2656,103 @@ const resolveParamPath = (paramsName, path, data)=>{
1372
2656
  };
1373
2657
  }
1374
2658
  }
1375
- throw new Error(ERROR_MESSAGES.PROPERTY_NOT_FOUND(pathString));
2659
+ throw new Error(ERROR_MESSAGES.PROPERTY_NOT_FOUND(pathString));
2660
+ }
2661
+ return {
2662
+ kind: "property",
2663
+ property,
2664
+ transformer,
2665
+ locale
2666
+ };
2667
+ }
2668
+ /**
2669
+ * A call on a parenthesised value: `(x.name).toLowerCase()`, `(x.age + 1).length`. Any operand can
2670
+ * receive one here, unlike a property chain, which carries at most one transform.
2671
+ */ withGroupCall(operand) {
2672
+ let receiver = operand;
2673
+ while(this.stream.isPunctuation(".") || this.stream.isPunctuation("?.")){
2674
+ const segment = this.stream.peek(1);
2675
+ if (segment == null || segment.kind !== "identifier") {
2676
+ break;
2677
+ }
2678
+ if (segment.value === "length" && !this.stream.isPunctuation("(", 2)) {
2679
+ this.stream.next();
2680
+ this.stream.next();
2681
+ receiver = {
2682
+ kind: "arithmetic",
2683
+ call: "length",
2684
+ left: receiver,
2685
+ right: noArgument()
2686
+ };
2687
+ continue;
2688
+ }
2689
+ const transform = TRANSFORM_METHODS[segment.value];
2690
+ if (transform != null) {
2691
+ this.stream.next();
2692
+ this.stream.next();
2693
+ this.stream.expectPunctuation("(");
2694
+ this.stream.expectPunctuation(")");
2695
+ receiver = {
2696
+ kind: "arithmetic",
2697
+ call: transform.transformer,
2698
+ left: receiver,
2699
+ right: transform.locale == null ? noArgument() : {
2700
+ kind: "value",
2701
+ value: transform.locale,
2702
+ transformer: null,
2703
+ locale: null
2704
+ }
2705
+ };
2706
+ continue;
2707
+ }
2708
+ // A comparator method needs a property target, which only an ungrouped chain produces
2709
+ if (COMPARATOR_METHODS[segment.value] != null && receiver.kind === "property") {
2710
+ this.stream.next();
2711
+ this.stream.next();
2712
+ this.stream.expectPunctuation("(");
2713
+ const argument = this.parseOperand();
2714
+ this.stream.expectPunctuation(")");
2715
+ if (argument.kind !== "property" && argument.kind !== "value" && argument.kind !== "param") {
2716
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'.${segment.value}()' on that argument`));
2717
+ }
2718
+ return {
2719
+ kind: "method-call",
2720
+ target: receiver,
2721
+ method: segment.value,
2722
+ argument
2723
+ };
2724
+ }
2725
+ // Any other member or call of a value, which a selector reads through
2726
+ if (this.readsValues) {
2727
+ this.stream.next();
2728
+ this.stream.next();
2729
+ receiver = this.stream.isPunctuation("(") ? this.opaqueCall(receiver) : {
2730
+ kind: "opaque",
2731
+ reads: [
2732
+ receiver
2733
+ ]
2734
+ };
2735
+ continue;
2736
+ }
2737
+ break;
2738
+ }
2739
+ return receiver;
2740
+ }
2741
+ /** A call with no node of its own, from its `(`, kept for what its receiver and arguments read. */ opaqueCall(receiver) {
2742
+ const reads = [
2743
+ receiver
2744
+ ];
2745
+ this.stream.expectPunctuation("(");
2746
+ while(!this.stream.matchPunctuation(")")){
2747
+ reads.push(this.parseValue());
2748
+ if (!this.stream.matchPunctuation(",")) {
2749
+ this.stream.expectPunctuation(")");
2750
+ break;
2751
+ }
1376
2752
  }
1377
2753
  return {
1378
- kind: "property",
1379
- property,
1380
- transformer,
1381
- locale
2754
+ kind: "opaque",
2755
+ reads
1382
2756
  };
1383
2757
  }
1384
2758
  withValueTransformer(operand) {
@@ -1414,12 +2788,26 @@ const resolveParamPath = (paramsName, path, data)=>{
1414
2788
  if (right.kind === "method-call") {
1415
2789
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("method call on the right side of a comparison"));
1416
2790
  }
1417
- if (left.kind === "property" && right.kind === "property") {
1418
- // Casing transformers are only valid with string-matching comparators,
1419
- // which cannot produce a property-to-property comparison
1420
- if (left.transformer === "to-lower-case" || left.transformer === "to-upper-case" || right.transformer === "to-lower-case" || right.transformer === "to-upper-case") {
1421
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
2791
+ // A comparison is a tree a backend renders, and this operand has no node in one
2792
+ if (left.kind === "opaque" || right.kind === "opaque") {
2793
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a call with no expression form inside a comparison"));
2794
+ }
2795
+ if (needsBrackets(left) || needsBrackets(right)) {
2796
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a bitwise or nullish operator compared without brackets, which JavaScript reads the other way round"));
2797
+ }
2798
+ if (left.kind === "arithmetic" || right.kind === "arithmetic" || left.kind === "conditional" || right.kind === "conditional") {
2799
+ if (containsProperty(left) === false && containsProperty(right) === false) {
2800
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic that references no schema property"));
1422
2801
  }
2802
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2803
+ comparator: operator.comparator,
2804
+ negated: operator.negated,
2805
+ strict: operator.strict,
2806
+ left: this.createOperandExpression(left),
2807
+ right: this.createOperandExpression(right)
2808
+ });
2809
+ }
2810
+ if (left.kind === "property" && right.kind === "property") {
1423
2811
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1424
2812
  comparator: operator.comparator,
1425
2813
  negated: operator.negated,
@@ -1428,18 +2816,63 @@ const resolveParamPath = (paramsName, path, data)=>{
1428
2816
  right: this.createPropertyExpression(right)
1429
2817
  });
1430
2818
  }
2819
+ // Only a loose comparison coerces. `===` records `strict` and honouring it is the point.
1431
2820
  if (left.kind === "property" && right.kind !== "property") {
1432
- return this.buildPropertyComparator(left, operator, right, /* applyConverter */ true);
2821
+ return this.buildPropertyComparator(left, operator, right, /* applyConverter */ !operator.strict);
1433
2822
  }
1434
2823
  if (right.kind === "property" && left.kind !== "property") {
1435
2824
  const swapped = {
1436
2825
  ...operator,
1437
2826
  comparator: SWAPPED_COMPARATORS[operator.comparator]
1438
2827
  };
1439
- return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ true);
2828
+ return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ !operator.strict);
2829
+ }
2830
+ const settled = this.settleConstantComparison(left, operator, right);
2831
+ if (settled != null) {
2832
+ return settled;
1440
2833
  }
1441
2834
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison requires a schema property on at least one side"));
1442
2835
  }
2836
+ /**
2837
+ * The answer a comparison of two constants gives, when that answer is `true`. The other answer
2838
+ * excludes every row, which has no expression node.
2839
+ */ settleConstantComparison(left, operator, right) {
2840
+ const leftValue = this.constantOf(left);
2841
+ const rightValue = this.constantOf(right);
2842
+ if (leftValue === UNKNOWN_UNTIL_ROW || rightValue === UNKNOWN_UNTIL_ROW) {
2843
+ return null;
2844
+ }
2845
+ const answer = (0,_evaluate__rspack_import_3/* .evaluate */._3)(new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2846
+ comparator: operator.comparator,
2847
+ negated: operator.negated,
2848
+ strict: operator.strict,
2849
+ left: new _types__rspack_import_1/* .ValueExpression */.Ko({
2850
+ value: leftValue
2851
+ }),
2852
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2853
+ value: rightValue
2854
+ })
2855
+ }), {});
2856
+ if (answer === true) {
2857
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
2858
+ }
2859
+ // Params decided this, so the refusal must not be cached against the source: the same filter
2860
+ // with other params can be a tautology.
2861
+ if (left.kind === "param" || right.kind === "param") {
2862
+ throw new ParamDependentParseError(ERROR_MESSAGES.UNSUPPORTED("a params comparison no row satisfies"));
2863
+ }
2864
+ return null;
2865
+ }
2866
+ /** The value an operand holds already, for the operands that do not depend on a row. */ constantOf(operand) {
2867
+ if (operand.kind === "value" && operand.transformer == null) {
2868
+ return operand.value;
2869
+ }
2870
+ if (operand.kind === "param" && operand.transformer == null) {
2871
+ this.structurallyDependsOnParams = true;
2872
+ return resolveParamPath(this.paramsName ?? "params", operand.path, this.params);
2873
+ }
2874
+ return UNKNOWN_UNTIL_ROW;
2875
+ }
1443
2876
  buildStandalone(operand) {
1444
2877
  if (operand.kind === "method-call") {
1445
2878
  return this.buildMethodComparator(operand);
@@ -1462,6 +2895,21 @@ const resolveParamPath = (paramsName, path, data)=>{
1462
2895
  locale: null
1463
2896
  }, /* applyConverter */ true);
1464
2897
  }
2898
+ // A boolean-valued call standing alone IS the predicate
2899
+ if (operand.kind === "arithmetic" && operand.call === "matches") {
2900
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2901
+ comparator: "equals",
2902
+ negated: false,
2903
+ strict: false,
2904
+ left: this.createOperandExpression(operand),
2905
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2906
+ value: true
2907
+ })
2908
+ });
2909
+ }
2910
+ if (operand.kind === "arithmetic" || operand.kind === "conditional") {
2911
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic used as a condition rather than compared"));
2912
+ }
1465
2913
  // Constant `true` — a tautology, which parseAnd/parseOr simplify away
1466
2914
  if (operand.kind === "value" && operand.value === true && operand.transformer == null) {
1467
2915
  return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
@@ -1514,12 +2962,6 @@ const resolveParamPath = (paramsName, path, data)=>{
1514
2962
  right: this.createValueExpression(value, null, /* applyConverter */ false)
1515
2963
  });
1516
2964
  }
1517
- // Casing transformers on a property are only meaningful with string-matching
1518
- // comparators; on relational comparators the plugins would silently
1519
- // ignore them and return wrong data
1520
- if (property.transformer != null && !isStringMatch) {
1521
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
1522
- }
1523
2965
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1524
2966
  comparator: operator.comparator,
1525
2967
  negated: operator.negated,
@@ -1528,31 +2970,63 @@ const resolveParamPath = (paramsName, path, data)=>{
1528
2970
  right: this.createValueExpression(value, property.property, applyConverter)
1529
2971
  });
1530
2972
  }
2973
+ /**
2974
+ * Any operand as an expression.
2975
+ *
2976
+ * Values inside arithmetic take no paired property: the result is a computed number, so the
2977
+ * property's serializer and type converter do not describe it — the same reason `.length` skips
2978
+ * them.
2979
+ */ createOperandExpression(operand) {
2980
+ if (operand.kind === "conditional") {
2981
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2982
+ call: "conditional",
2983
+ expression: operand.condition,
2984
+ arguments: [
2985
+ this.createOperandExpression(operand.whenTrue),
2986
+ this.createOperandExpression(operand.whenFalse)
2987
+ ]
2988
+ });
2989
+ }
2990
+ if (operand.kind === "arithmetic") {
2991
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2992
+ call: operand.call,
2993
+ expression: this.createOperandExpression(operand.left),
2994
+ arguments: operand.right === NO_ARGUMENT ? [] : operand.extra == null ? [
2995
+ this.createOperandExpression(operand.right)
2996
+ ] : [
2997
+ this.createOperandExpression(operand.right),
2998
+ this.createOperandExpression(operand.extra)
2999
+ ]
3000
+ });
3001
+ }
3002
+ if (operand.kind === "property") {
3003
+ return this.createPropertyExpression(operand);
3004
+ }
3005
+ if (operand.kind === "method-call") {
3006
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a method call inside arithmetic"));
3007
+ }
3008
+ if (operand.kind === "opaque") {
3009
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a call with no expression form inside arithmetic"));
3010
+ }
3011
+ return this.createValueExpression(operand, null, /* applyConverter */ false);
3012
+ }
1531
3013
  createPropertyExpression(operand) {
1532
- const expression = new _types__rspack_import_1/* .PropertyExpression */.ep({
3014
+ return asCall(new _types__rspack_import_1/* .PropertyExpression */.ep({
1533
3015
  property: operand.property
1534
- });
1535
- expression.transformer = operand.transformer;
1536
- expression.locale = operand.locale;
1537
- return expression;
3016
+ }), operand.transformer, operand.locale);
1538
3017
  }
1539
3018
  createValueExpression(operand, pairedProperty, applyConverter) {
1540
3019
  if (operand.kind === "param") {
1541
- const expression = new ParamReferenceExpression({
3020
+ return asCall(new ParamReferenceExpression({
1542
3021
  paramPath: operand.path,
1543
3022
  pairedProperty,
1544
3023
  applyConverter
1545
- });
1546
- expression.transformer = operand.transformer;
1547
- expression.locale = operand.locale;
1548
- return expression;
3024
+ }), operand.transformer, operand.locale);
1549
3025
  }
1550
3026
  const expression = new _types__rspack_import_1/* .ValueExpression */.Ko({
1551
3027
  value: resolvePairedValue(operand.value, pairedProperty, applyConverter)
1552
3028
  });
1553
- expression.transformer = operand.transformer;
1554
- expression.locale = operand.locale;
1555
- return expression;
3029
+ return asCall(expression, operand.transformer, operand.locale);
1556
3030
  }
1557
3031
  }
1558
3032
  // #endregion
@@ -1564,28 +3038,19 @@ const resolveParamPath = (paramsName, path, data)=>{
1564
3038
  */ const bindExpression = (expression, paramsName, params)=>{
1565
3039
  if (expression instanceof ParamReferenceExpression) {
1566
3040
  const raw = resolveParamPath(paramsName ?? "params", expression.paramPath, params);
1567
- const bound = new _types__rspack_import_1/* .ValueExpression */.Ko({
3041
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1568
3042
  value: resolvePairedValue(raw, expression.pairedProperty, expression.applyConverter)
1569
3043
  });
1570
- bound.transformer = expression.transformer;
1571
- bound.locale = expression.locale;
1572
- return bound;
1573
3044
  }
1574
3045
  if (expression instanceof _types__rspack_import_1/* .ValueExpression */.Ko) {
1575
- const clone = new _types__rspack_import_1/* .ValueExpression */.Ko({
3046
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1576
3047
  value: expression.value
1577
3048
  });
1578
- clone.transformer = expression.transformer;
1579
- clone.locale = expression.locale;
1580
- return clone;
1581
3049
  }
1582
3050
  if (expression instanceof _types__rspack_import_1/* .PropertyExpression */.ep) {
1583
- const clone = new _types__rspack_import_1/* .PropertyExpression */.ep({
3051
+ return new _types__rspack_import_1/* .PropertyExpression */.ep({
1584
3052
  property: expression.property
1585
3053
  });
1586
- clone.transformer = expression.transformer;
1587
- clone.locale = expression.locale;
1588
- return clone;
1589
3054
  }
1590
3055
  if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
1591
3056
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
@@ -1603,8 +3068,99 @@ const resolveParamPath = (paramsName, path, data)=>{
1603
3068
  right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
1604
3069
  });
1605
3070
  }
3071
+ if (expression instanceof _types__rspack_import_1/* .CallExpression */.DG) {
3072
+ return new _types__rspack_import_1/* .CallExpression */.DG({
3073
+ call: expression.call,
3074
+ expression: bindExpression(expression.expression, paramsName, params),
3075
+ arguments: expression.arguments.map((argument)=>bindExpression(argument, paramsName, params))
3076
+ });
3077
+ }
1606
3078
  return expression;
1607
3079
  };
3080
+ /**
3081
+ * Wraps an operand in the call a transform method named, if there was one.
3082
+ *
3083
+ * `Transformer` and `Call` share these three names, so the transform IS the call name. A locale
3084
+ * becomes the call's first argument, which is where it belongs — it qualifies the casing, not the
3085
+ * property.
3086
+ */ const asCall = (inner, transformer, locale)=>{
3087
+ if (transformer == null) {
3088
+ return inner;
3089
+ }
3090
+ return new _types__rspack_import_1/* .CallExpression */.DG({
3091
+ call: transformer,
3092
+ expression: inner,
3093
+ arguments: locale == null ? [] : [
3094
+ new _types__rspack_import_1/* .ValueExpression */.Ko({
3095
+ value: locale
3096
+ })
3097
+ ]
3098
+ });
3099
+ };
3100
+ /** Binds every name a destructuring pattern introduces to the path it reads. */ const bindPattern = (stream, kind, path, scope)=>{
3101
+ if (!stream.matchPunctuation("{")) {
3102
+ const name = stream.next();
3103
+ if (name.kind !== "identifier") {
3104
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`parameter '${name.value}'`));
3105
+ }
3106
+ scope.set(name.value, {
3107
+ kind,
3108
+ path
3109
+ });
3110
+ return;
3111
+ }
3112
+ while(!stream.matchPunctuation("}")){
3113
+ const key = stream.next();
3114
+ if (key.kind !== "identifier") {
3115
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`destructured key '${key.value}'`));
3116
+ }
3117
+ if (stream.matchPunctuation(":")) {
3118
+ bindPattern(stream, kind, [
3119
+ ...path,
3120
+ key.value
3121
+ ], scope);
3122
+ } else {
3123
+ scope.set(key.value, {
3124
+ kind,
3125
+ path: [
3126
+ ...path,
3127
+ key.value
3128
+ ]
3129
+ });
3130
+ }
3131
+ if (!stream.matchPunctuation(",")) {
3132
+ stream.expectPunctuation("}");
3133
+ return;
3134
+ }
3135
+ }
3136
+ };
3137
+ /** Reads a filter's parameter list — the entity alone, or the `[entity, params]` pair — into a scope. */ const buildScope = (parameterNames, hasParams)=>{
3138
+ const stream = new TokenStream(tokenize(parameterNames));
3139
+ const scope = new Map();
3140
+ if (!stream.matchPunctuation("[")) {
3141
+ bindPattern(stream, "property", [], scope);
3142
+ return {
3143
+ scope,
3144
+ paramsName: null
3145
+ };
3146
+ }
3147
+ bindPattern(stream, "property", [], scope);
3148
+ if (hasParams && stream.matchPunctuation(",") && !stream.isPunctuation("]")) {
3149
+ bindPattern(stream, "param", [], scope);
3150
+ }
3151
+ return {
3152
+ scope,
3153
+ paramsName: wholeParamsName(scope)
3154
+ };
3155
+ };
3156
+ /** The name the whole params object was given, when it was not destructured. Error messages only. */ const wholeParamsName = (scope)=>{
3157
+ for (const [name, binding] of scope){
3158
+ if (binding.kind === "param" && binding.path.length === 0) {
3159
+ return name;
3160
+ }
3161
+ }
3162
+ return null;
3163
+ };
1608
3164
  /**
1609
3165
  * Splits stringified filter source into parameter names and the expression
1610
3166
  * body, unwrapping single-return block bodies.
@@ -1634,33 +3190,12 @@ const resolveParamPath = (paramsName, path, data)=>{
1634
3190
  parameterNames = parameterNames.slice(1, -1).trim();
1635
3191
  }
1636
3192
  }
1637
- let entityName;
1638
- let paramsName = null;
1639
- if (parameterNames.startsWith("[") && parameterNames.endsWith("]")) {
1640
- const destructured = parameterNames.slice(1, -1).split(",").map((w)=>w.trim());
1641
- entityName = destructured[0];
1642
- if (hasParams) {
1643
- paramsName = destructured[1] ?? null;
1644
- }
1645
- } else {
1646
- entityName = parameterNames;
1647
- }
1648
- if (entityName == null || entityName.length === 0) {
3193
+ if (parameterNames.length === 0) {
1649
3194
  throw new Error("Invalid Function");
1650
3195
  }
1651
- // Unwrap a single-return block body: { return <expression>; }
1652
- if (body.startsWith("{")) {
1653
- const inner = body.slice(1, body.lastIndexOf("}")).trim();
1654
- if (!inner.startsWith("return")) {
1655
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("block body without a single return statement"));
1656
- }
1657
- body = inner.slice("return".length).trim();
1658
- if (body.endsWith(";")) {
1659
- body = body.slice(0, -1).trim();
1660
- }
1661
- }
3196
+ const { scope, paramsName } = buildScope(parameterNames, hasParams);
1662
3197
  return {
1663
- entityName,
3198
+ scope,
1664
3199
  paramsName,
1665
3200
  body
1666
3201
  };
@@ -1728,17 +3263,27 @@ const combineExpressions = (...expressions)=>{
1728
3263
  */ const parseFragment = (schema, body, rootName)=>{
1729
3264
  try {
1730
3265
  const stream = new TokenStream(tokenize(body));
1731
- const parser = new ExpressionParser(schema, stream, rootName, null, undefined);
1732
- return parser.parse();
3266
+ const scope = new Map([
3267
+ [
3268
+ rootName,
3269
+ {
3270
+ kind: "property",
3271
+ path: []
3272
+ }
3273
+ ]
3274
+ ]);
3275
+ const parser = new ExpressionParser(schema, stream, scope, null, undefined);
3276
+ return foldConstantCalls(parser.parse());
1733
3277
  } catch {
1734
3278
  // The failure is expected and informative — see above — so it is not logged. A caller that
1735
3279
  // parses one conjunct against two schemas would otherwise warn on every successful split.
1736
3280
  return Expression.NOT_PARSABLE;
1737
3281
  }
1738
3282
  };
3283
+ /** What the parser refused, from a throw that may not be an `Error`. */ const refusalOf = (error)=>error instanceof Error ? error.message : String(error);
1739
3284
  const toExpression = (schema, fn, params)=>{
1740
3285
  const stringifiedFunction = fn.toString();
1741
- const warn = (error)=>_utilities__rspack_import_3/* .logger.warn */.vF.warn("Error parsing expression", {
3286
+ const warn = (error)=>_utilities__rspack_import_4/* .logger.warn */.vF.warn("Error parsing expression", {
1742
3287
  error,
1743
3288
  collectionName: schema.collectionName,
1744
3289
  params,
@@ -1746,16 +3291,17 @@ const toExpression = (schema, fn, params)=>{
1746
3291
  });
1747
3292
  const cached = getCachedTemplate(schema, stringifiedFunction);
1748
3293
  if (cached != null) {
1749
- // A cached failure — the warning was already logged when it was discovered
3294
+ // A cached failure — the warning was already logged when it was discovered. The template
3295
+ // carries what was refused, and `.explain()` is usually called once the cache is warm.
1750
3296
  if (_types__rspack_import_1/* .Expression.isNotParsable */.r4.isNotParsable(cached.template)) {
1751
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3297
+ return cached.template;
1752
3298
  }
1753
3299
  try {
1754
- return bindExpression(cached.template, cached.paramsName, params);
3300
+ return (0,_fold__rspack_import_5/* .foldConstantCalls */.F5)(bindExpression(cached.template, cached.paramsName, params));
1755
3301
  } catch (error) {
1756
3302
  // Binding failures are param-dependent by nature — never cached
1757
3303
  warn(error);
1758
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3304
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1759
3305
  }
1760
3306
  }
1761
3307
  let paramsName = null;
@@ -1764,22 +3310,23 @@ const toExpression = (schema, fn, params)=>{
1764
3310
  try {
1765
3311
  const shape = resolveFunctionShape(stringifiedFunction, params != null);
1766
3312
  const stream = new TokenStream(tokenize(shape.body));
1767
- const parser = new ExpressionParser(schema, stream, shape.entityName, shape.paramsName, params);
3313
+ const parser = new ExpressionParser(schema, stream, shape.scope, shape.paramsName, params);
1768
3314
  paramsName = shape.paramsName;
1769
- template = parser.parse();
3315
+ template = parser.parseBody();
1770
3316
  structurallyDependsOnParams = parser.structurallyDependsOnParams;
1771
3317
  } catch (error) {
1772
3318
  // Cache the failure so a hot query on an unsupported filter doesn't
1773
3319
  // re-parse and re-warn on every execution. Param-dependent failures are
1774
3320
  // exempt: the same source can succeed with different params.
3321
+ const refused = _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1775
3322
  if (!(error instanceof ParamDependentParseError)) {
1776
3323
  setCachedTemplate(schema, stringifiedFunction, {
1777
- template: _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE,
3324
+ template: refused,
1778
3325
  paramsName: null
1779
3326
  });
1780
3327
  }
1781
3328
  warn(error);
1782
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3329
+ return refused;
1783
3330
  }
1784
3331
  // Templates whose structure was resolved from param values are only
1785
3332
  // valid for this exact params object — parse those fresh every time
@@ -1790,17 +3337,116 @@ const toExpression = (schema, fn, params)=>{
1790
3337
  });
1791
3338
  }
1792
3339
  try {
1793
- return bindExpression(template, paramsName, params);
3340
+ return (0,_fold__rspack_import_5/* .foldConstantCalls */.F5)(bindExpression(template, paramsName, params));
1794
3341
  } catch (error) {
1795
3342
  warn(error);
1796
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3343
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1797
3344
  }
1798
3345
  };
3346
+ const collectReads = (operand, into)=>{
3347
+ switch(operand.kind){
3348
+ case "property":
3349
+ into.add(operand.property);
3350
+ return;
3351
+ case "method-call":
3352
+ collectReads(operand.target, into);
3353
+ collectReads(operand.argument, into);
3354
+ return;
3355
+ case "arithmetic":
3356
+ collectReads(operand.left, into);
3357
+ collectReads(operand.right, into);
3358
+ if (operand.extra != null) {
3359
+ collectReads(operand.extra, into);
3360
+ }
3361
+ return;
3362
+ case "conditional":
3363
+ for (const property of getProperties(operand.condition)){
3364
+ into.add(property);
3365
+ }
3366
+ collectReads(operand.whenTrue, into);
3367
+ collectReads(operand.whenFalse, into);
3368
+ return;
3369
+ case "opaque":
3370
+ for (const read of operand.reads){
3371
+ collectReads(read, into);
3372
+ }
3373
+ return;
3374
+ }
3375
+ };
3376
+ const selectedValue = (operand)=>{
3377
+ const found = new Set();
3378
+ collectReads(operand, found);
3379
+ const reads = [
3380
+ ...found
3381
+ ];
3382
+ return {
3383
+ property: reads.length === 1 ? reads[0] : null,
3384
+ reads,
3385
+ isDirectProperty: operand.kind === "property" && operand.transformer == null
3386
+ };
3387
+ };
3388
+ // Keyed like the template cache. A selector takes no params, so every result is cacheable
3389
+ const selectorCache = new WeakMap();
3390
+ /**
3391
+ * Reads a sort, map, group or `nearest` selector with the grammar filters use, for what its value is
3392
+ * read from.
3393
+ *
3394
+ * Not for evaluating it: the caller's function stays the value, and nothing here renders one. A plugin
3395
+ * decides from the result whether it can run the option. One that orders or projects by column cannot
3396
+ * run a value that is not the property itself, and one that runs the function over stored rows cannot
3397
+ * run it over a renamed property.
3398
+ *
3399
+ * So the grammar is wider here than for a filter. A call it has no node for, such as `getFullYear()`,
3400
+ * is kept for the operands it reads rather than refused, since the function it came from still runs.
3401
+ *
3402
+ * `not-parsable` is not logged. The option runs as it did before the selector was parsed.
3403
+ *
3404
+ * Cached by function source per schema, like `toExpression`. The result is shared, so it is read-only.
3405
+ */ const parseSelector = (schema, selector)=>{
3406
+ const source = selector.toString();
3407
+ let bySource = selectorCache.get(schema);
3408
+ const cached = bySource?.get(source);
3409
+ if (cached != null) {
3410
+ return cached;
3411
+ }
3412
+ let parsed;
3413
+ try {
3414
+ const shape = resolveFunctionShape(source, false);
3415
+ const parser = new ExpressionParser(schema, new TokenStream(tokenize(shape.body)), shape.scope, null, undefined, true);
3416
+ const selected = parser.parseSelector();
3417
+ parsed = Array.isArray(selected) ? {
3418
+ kind: "object",
3419
+ fields: selected.map((field)=>({
3420
+ name: field.name,
3421
+ ...selectedValue(field.operand)
3422
+ }))
3423
+ } : {
3424
+ kind: "value",
3425
+ value: selectedValue(selected)
3426
+ };
3427
+ } catch (error) {
3428
+ parsed = {
3429
+ kind: "not-parsable",
3430
+ reason: refusalOf(error)
3431
+ };
3432
+ }
3433
+ if (bySource == null) {
3434
+ bySource = new Map();
3435
+ selectorCache.set(schema, bySource);
3436
+ }
3437
+ // Stryker disable next-line all: the same resource bound as the template cache's
3438
+ if (bySource.size >= MAX_CACHED_TEMPLATES_PER_SCHEMA) {
3439
+ bySource.clear();
3440
+ }
3441
+ bySource.set(source, parsed);
3442
+ return parsed;
3443
+ }; // #endregion
1799
3444
 
1800
3445
 
1801
3446
  },
1802
3447
  27(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1803
3448
  __webpack_require__.d(__webpack_exports__, {
3449
+ DG: () => (CallExpression),
1804
3450
  Ko: () => (ValueExpression),
1805
3451
  bQ: () => (ComparatorExpression),
1806
3452
  ep: () => (PropertyExpression),
@@ -1810,59 +3456,69 @@ __webpack_require__.d(__webpack_exports__, {
1810
3456
  const valueToJson = (value)=>{
1811
3457
  if (value === undefined) {
1812
3458
  return {
1813
- k: "undefined"
3459
+ undefined: true
1814
3460
  };
1815
3461
  }
1816
3462
  if (value === null) {
1817
- return {
1818
- k: "raw",
1819
- v: null
1820
- };
3463
+ return null;
1821
3464
  }
1822
3465
  if (value instanceof Date) {
1823
3466
  // ISO rather than epoch millis: it survives a human reading the payload, and an invalid
1824
3467
  // Date has no ISO form — so it is caught here rather than becoming a silent `null`.
1825
3468
  return {
1826
- k: "date",
1827
- v: value.toISOString()
3469
+ date: value.toISOString()
1828
3470
  };
1829
3471
  }
1830
3472
  if (Array.isArray(value)) {
1831
- return {
1832
- k: "array",
1833
- v: value.map(valueToJson)
1834
- };
3473
+ return value.map(valueToJson);
1835
3474
  }
1836
3475
  if (typeof value === "number" && Number.isFinite(value) === false) {
1837
3476
  // `JSON.stringify` turns all three of these into `null`, which would compare as a different
1838
3477
  // value entirely rather than failing.
1839
3478
  return {
1840
- k: "number",
1841
- v: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
3479
+ number: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
1842
3480
  };
1843
3481
  }
1844
3482
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3483
+ return value;
3484
+ }
3485
+ if (value instanceof RegExp) {
3486
+ return {
3487
+ regex: {
3488
+ source: value.source,
3489
+ flags: value.flags
3490
+ }
3491
+ };
3492
+ }
3493
+ // `JSON.stringify` throws outright on a bigint rather than losing it quietly, so this is the one
3494
+ // tag that turns a crash into a value
3495
+ if (typeof value === "bigint") {
1845
3496
  return {
1846
- k: "raw",
1847
- v: value
3497
+ bigint: value.toString()
1848
3498
  };
1849
3499
  }
1850
3500
  throw new Error(`Cannot serialize this filter value: only strings, numbers, booleans, null, undefined, Dates and arrays of those can cross a wire. ` + `Received: ${Object.prototype.toString.call(value)}`);
1851
3501
  };
1852
3502
  const valueFromJson = (value)=>{
1853
- if (value.k === "undefined") {
1854
- return undefined;
3503
+ if (value === null || typeof value !== "object") {
3504
+ return value;
1855
3505
  }
1856
- if (value.k === "date") {
1857
- return new Date(value.v);
3506
+ if (Array.isArray(value)) {
3507
+ return value.map(valueFromJson);
3508
+ }
3509
+ if ("date" in value) {
3510
+ return new Date(value.date);
3511
+ }
3512
+ if ("undefined" in value) {
3513
+ return undefined;
1858
3514
  }
1859
- if (value.k === "array") {
1860
- return value.v.map(valueFromJson);
3515
+ if ("regex" in value) {
3516
+ return new RegExp(value.regex.source, value.regex.flags);
1861
3517
  }
1862
- if (value.k === "number") {
1863
- return value.v === "NaN" ? Number.NaN : value.v === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
3518
+ if ("bigint" in value) {
3519
+ return BigInt(value.bigint);
1864
3520
  }
1865
- return value.v;
3521
+ return value.number === "NaN" ? Number.NaN : value.number === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1866
3522
  };
1867
3523
  /**
1868
3524
  * The base class for all expression types.
@@ -1879,6 +3535,9 @@ const valueFromJson = (value)=>{
1879
3535
  static get NOT_PARSABLE() {
1880
3536
  return new NotParsableExpression();
1881
3537
  }
3538
+ /** `NOT_PARSABLE`, carrying what the parser refused. */ static notParsable(reason) {
3539
+ return new NotParsableExpression(reason);
3540
+ }
1882
3541
  static isEmpty(expression) {
1883
3542
  return expression.type === "empty" || expression instanceof EmptyExpression;
1884
3543
  }
@@ -1895,7 +3554,7 @@ const valueFromJson = (value)=>{
1895
3554
  *
1896
3555
  * ## Why it is this small
1897
3556
  *
1898
- * Of the six node types a bound tree can contain, exactly one holds anything JSON cannot carry:
3557
+ * Of the seven node types a bound tree can contain, exactly one holds anything JSON cannot carry:
1899
3558
  * `PropertyExpression`, whose live `PropertyInfo` has functions, a parent chain and caches. It
1900
3559
  * reduces to a property PATH — `PropertyInfo.id` IS the dotted path, and `getProperty` is keyed by
1901
3560
  * exactly that — so rebinding is one lookup.
@@ -1910,7 +3569,7 @@ const valueFromJson = (value)=>{
1910
3569
  if (expression.type === "operator") {
1911
3570
  const operator = expression;
1912
3571
  return {
1913
- t: "operator",
3572
+ type: "operator",
1914
3573
  operator: operator.operator,
1915
3574
  ...operator.left != null && {
1916
3575
  left: Expression.toJson(operator.left)
@@ -1923,7 +3582,7 @@ const valueFromJson = (value)=>{
1923
3582
  if (expression.type === "comparator") {
1924
3583
  const comparator = expression;
1925
3584
  return {
1926
- t: "comparator",
3585
+ type: "comparator",
1927
3586
  comparator: comparator.comparator,
1928
3587
  negated: comparator.negated,
1929
3588
  strict: comparator.strict,
@@ -1935,29 +3594,41 @@ const valueFromJson = (value)=>{
1935
3594
  }
1936
3595
  };
1937
3596
  }
3597
+ if (expression.type === "call") {
3598
+ const call = expression;
3599
+ return {
3600
+ type: "call",
3601
+ call: call.call,
3602
+ expression: Expression.toJson(call.expression),
3603
+ arguments: call.arguments.map(Expression.toJson)
3604
+ };
3605
+ }
1938
3606
  if (expression.type === "property") {
1939
3607
  const property = expression;
1940
3608
  return {
1941
- t: "property",
3609
+ type: "property",
1942
3610
  // The dotted path, which is exactly the key `getProperty` is looking up
1943
- path: property.property.id,
1944
- transformer: property.transformer,
1945
- locale: property.locale
3611
+ path: property.property.id
1946
3612
  };
1947
3613
  }
1948
3614
  if (expression.type === "value") {
1949
3615
  const value = expression;
1950
3616
  return {
1951
- t: "value",
1952
- value: valueToJson(value.value),
1953
- transformer: value.transformer,
1954
- locale: value.locale
3617
+ type: "value",
3618
+ value: valueToJson(value.value)
1955
3619
  };
1956
3620
  }
1957
- return expression.type === "empty" ? {
1958
- t: "empty"
3621
+ if (expression.type === "empty") {
3622
+ return {
3623
+ type: "empty"
3624
+ };
3625
+ }
3626
+ const reason = expression.reason;
3627
+ return reason == null ? {
3628
+ type: "not-parsable"
1959
3629
  } : {
1960
- t: "not-parsable"
3630
+ type: "not-parsable",
3631
+ reason
1961
3632
  };
1962
3633
  }
1963
3634
  /**
@@ -1973,14 +3644,14 @@ const valueFromJson = (value)=>{
1973
3644
  * failure here worse than an error.
1974
3645
  */ static fromJson(json, schema) {
1975
3646
  const child = (node)=>node == null ? undefined : Expression.fromJson(node, schema);
1976
- if (json.t === "operator") {
3647
+ if (json.type === "operator") {
1977
3648
  return new OperatorExpression({
1978
3649
  operator: json.operator,
1979
3650
  left: child(json.left),
1980
3651
  right: child(json.right)
1981
3652
  });
1982
3653
  }
1983
- if (json.t === "comparator") {
3654
+ if (json.type === "comparator") {
1984
3655
  return new ComparatorExpression({
1985
3656
  comparator: json.comparator,
1986
3657
  negated: json.negated,
@@ -1989,27 +3660,34 @@ const valueFromJson = (value)=>{
1989
3660
  right: child(json.right)
1990
3661
  });
1991
3662
  }
1992
- if (json.t === "property") {
3663
+ if (json.type === "call") {
3664
+ if (json.expression == null) {
3665
+ throw new Error(`Cannot deserialize a filter: a '${json.call}' call carries no operand. ` + `Collection: ${schema.collectionName}.`);
3666
+ }
3667
+ return new CallExpression({
3668
+ call: json.call,
3669
+ expression: Expression.fromJson(json.expression, schema),
3670
+ arguments: (json.arguments ?? []).map((argument)=>Expression.fromJson(argument, schema))
3671
+ });
3672
+ }
3673
+ if (json.type === "property") {
1993
3674
  const property = schema.getProperty(json.path);
1994
3675
  if (property == null) {
1995
3676
  throw new Error(`Cannot deserialize a filter: this schema does not declare the property it names. ` + `Property: ${json.path}, Collection: ${schema.collectionName}. ` + `The two sides disagree about the shape of the data, so the filter cannot be applied.`);
1996
3677
  }
1997
- const rebuilt = new PropertyExpression({
3678
+ return new PropertyExpression({
1998
3679
  property
1999
3680
  });
2000
- rebuilt.transformer = json.transformer;
2001
- rebuilt.locale = json.locale;
2002
- return rebuilt;
2003
3681
  }
2004
- if (json.t === "value") {
2005
- const rebuilt = new ValueExpression({
3682
+ if (json.type === "value") {
3683
+ return new ValueExpression({
2006
3684
  value: valueFromJson(json.value)
2007
3685
  });
2008
- rebuilt.transformer = json.transformer;
2009
- rebuilt.locale = json.locale;
2010
- return rebuilt;
2011
3686
  }
2012
- return json.t === "empty" ? Expression.EMPTY : Expression.NOT_PARSABLE;
3687
+ if (json.type === "empty") {
3688
+ return Expression.EMPTY;
3689
+ }
3690
+ return json.reason == null ? Expression.NOT_PARSABLE : Expression.notParsable(json.reason);
2013
3691
  }
2014
3692
  }
2015
3693
  class EmptyExpression extends Expression {
@@ -2017,6 +3695,11 @@ class EmptyExpression extends Expression {
2017
3695
  }
2018
3696
  class NotParsableExpression extends Expression {
2019
3697
  type = "not-parsable";
3698
+ /** What the parser refused, when it knows. `.explain()` prints it beside the source. */ reason;
3699
+ constructor(reason){
3700
+ super();
3701
+ this.reason = reason;
3702
+ }
2020
3703
  }
2021
3704
  /**
2022
3705
  * A class representing a comparison operation (e.g., equals, greater-than).
@@ -2047,20 +3730,28 @@ class NotParsableExpression extends Expression {
2047
3730
  */ class PropertyExpression extends Expression {
2048
3731
  /** The type of the expression (always 'property'). */ type = "property";
2049
3732
  /** The property info for the path. */ property;
2050
- transformer = null;
2051
- locale = null;
2052
3733
  constructor(options){
2053
3734
  super();
2054
3735
  this.property = options.property;
2055
3736
  }
2056
3737
  }
3738
+ class CallExpression extends Expression {
3739
+ type = "call";
3740
+ call;
3741
+ expression;
3742
+ /** Empty for a unary call. */ arguments;
3743
+ constructor(options){
3744
+ super();
3745
+ this.call = options.call;
3746
+ this.expression = options.expression;
3747
+ this.arguments = options.arguments ?? [];
3748
+ }
3749
+ }
2057
3750
  /**
2058
3751
  * A class representing a literal value.
2059
3752
  */ class ValueExpression extends Expression {
2060
3753
  /** The type of the expression (always 'value'). */ type = "value";
2061
3754
  /** The literal value. */ value;
2062
- transformer = null;
2063
- locale = null;
2064
3755
  constructor(options){
2065
3756
  super();
2066
3757
  this.value = options.value;
@@ -2071,8 +3762,43 @@ class NotParsableExpression extends Expression {
2071
3762
  },
2072
3763
  63(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2073
3764
  __webpack_require__.d(__webpack_exports__, {
2074
- j: () => (forEach)
3765
+ LU: () => (childrenOf),
3766
+ jJ: () => (forEach)
2075
3767
  });
3768
+ /**
3769
+ * Separates an operand from the calls applied to it.
3770
+ *
3771
+ * `null` when there is no operand beneath the calls. Every consumer needs this to decide whether a
3772
+ * comparator side is a property or a value, so it lives here rather than in each translator.
3773
+ */ function peelCalls(expression) {
3774
+ const calls = [];
3775
+ let current = expression;
3776
+ while(current != null && current.type === "call"){
3777
+ calls.unshift(current);
3778
+ current = current.expression;
3779
+ }
3780
+ return current == null ? null : {
3781
+ operand: current,
3782
+ calls
3783
+ };
3784
+ }
3785
+ function childrenOf(expression) {
3786
+ if (expression.type === "call") {
3787
+ const call = expression;
3788
+ return [
3789
+ call.expression,
3790
+ ...call.arguments ?? []
3791
+ ].filter((child)=>child != null);
3792
+ }
3793
+ const children = [];
3794
+ if (expression.left != null) {
3795
+ children.push(expression.left);
3796
+ }
3797
+ if (expression.right != null) {
3798
+ children.push(expression.right);
3799
+ }
3800
+ return children;
3801
+ }
2076
3802
  /**
2077
3803
  * Extracts all properties referenced in an expression
2078
3804
  * @param expression The expression to analyze
@@ -2084,12 +3810,8 @@ __webpack_require__.d(__webpack_exports__, {
2084
3810
  if (expr.type === "property") {
2085
3811
  properties.push(expr.property);
2086
3812
  }
2087
- // Traverse left and right expressions if they exist
2088
- if (expr.left) {
2089
- traverse(expr.left);
2090
- }
2091
- if (expr.right) {
2092
- traverse(expr.right);
3813
+ for (const child of childrenOf(expr)){
3814
+ traverse(child);
2093
3815
  }
2094
3816
  }
2095
3817
  traverse(expression);
@@ -2102,14 +3824,8 @@ function forEach(expression, callback) {
2102
3824
  if (!callback(expr)) {
2103
3825
  return false;
2104
3826
  }
2105
- // Traverse left and right expressions if they exist
2106
- if (expr.left) {
2107
- if (!traverse(expr.left)) {
2108
- return false;
2109
- }
2110
- }
2111
- if (expr.right) {
2112
- if (!traverse(expr.right)) {
3827
+ for (const child of childrenOf(expr)){
3828
+ if (!traverse(child)) {
2113
3829
  return false;
2114
3830
  }
2115
3831
  }
@@ -2390,32 +4106,86 @@ __webpack_require__.d(__webpack_exports__, {
2390
4106
  H: () => (QueryOptionsCollection)
2391
4107
  });
2392
4108
  /* import */ var _assertions__rspack_import_1 = __webpack_require__(126);
2393
- /* import */ var _expressions_utils__rspack_import_0 = __webpack_require__(63);
4109
+ /* import */ var _expressions_utils__rspack_import_2 = __webpack_require__(63);
4110
+ /* import */ var _schema_types__rspack_import_0 = __webpack_require__(537);
4111
+ /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
2394
4112
 
2395
4113
 
4114
+
4115
+
4116
+ /** What a schema type is called in JavaScript, where one exists. A value of any other type cannot equal it. */ const JAVASCRIPT_TYPE_OF = {
4117
+ [_schema_types__rspack_import_0/* .SchemaTypes.Number */.L.Number]: "number",
4118
+ [_schema_types__rspack_import_0/* .SchemaTypes.String */.L.String]: "string",
4119
+ [_schema_types__rspack_import_0/* .SchemaTypes.Boolean */.L.Boolean]: "boolean",
4120
+ [_schema_types__rspack_import_0/* .SchemaTypes.Date */.L.Date]: "object"
4121
+ };
4122
+ const mismatchedSide = (property, value)=>{
4123
+ if (property == null || value == null || !(0,_assertions__rspack_import_1/* .isPropertyExpression */.e3)(property) || !(0,_assertions__rspack_import_1/* .isValueExpression */.S6)(value)) {
4124
+ return null;
4125
+ }
4126
+ const expected = JAVASCRIPT_TYPE_OF[property.property.type];
4127
+ if (expected == null || value.value == null || typeof value.value === expected) {
4128
+ return null;
4129
+ }
4130
+ return {
4131
+ property,
4132
+ value,
4133
+ expected
4134
+ };
4135
+ };
4136
+ /** A strict comparison whose answer is the same for every row, because the types cannot be equal. */ const comparesTypesThatCannotMatch = (expression)=>{
4137
+ if (!(0,_assertions__rspack_import_1/* .isComparatorExpression */.xH)(expression) || expression.strict !== true) {
4138
+ return false;
4139
+ }
4140
+ if (expression.comparator !== "equals") {
4141
+ return false;
4142
+ }
4143
+ return mismatchedSide(expression.left, expression.right) != null || mismatchedSide(expression.right, expression.left) != null;
4144
+ };
4145
+ /** `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);
4146
+ const mismatchWarning = (expression)=>{
4147
+ const side = mismatchedSide(expression.left, expression.right) ?? mismatchedSide(expression.right, expression.left);
4148
+ const outcome = expression.negated ? "every row matches" : "no row matches";
4149
+ 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`;
4150
+ };
4151
+ /**
4152
+ * An item as one dispatch receives it: a new object, so a report written on it stays with that dispatch.
4153
+ *
4154
+ * A database option starts `executed` again, because a report is only an answer from the plugin that
4155
+ * made it. A memory option keeps the reason core planned it with. A join's inner options are copied the
4156
+ * same way, since a plugin can report on them too.
4157
+ */ const toDispatchItem = (item)=>{
4158
+ const option = item.option;
4159
+ const value = option.name === "join" ? {
4160
+ ...option.value,
4161
+ innerOptions: option.value.innerOptions.forDispatch()
4162
+ } : option.value;
4163
+ return {
4164
+ index: item.index,
4165
+ option: option.target === "database" ? {
4166
+ ...option,
4167
+ value,
4168
+ reason: "executed"
4169
+ } : {
4170
+ ...option,
4171
+ value
4172
+ }
4173
+ };
4174
+ };
2396
4175
  class QueryOptionsCollection {
2397
4176
  options = new Map();
2398
4177
  nextExecutionTarget = "database";
2399
4178
  nextExecutionReason = null;
2400
4179
  nextIndex = 0;
2401
4180
  enumeratedItems = [];
4181
+ dirty = true;
4182
+ /** The collection a `splitAt`/`split` half came from. A capability report belongs to it. */ origin = null;
2402
4183
  /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */ cutOverToMemory(reason) {
2403
4184
  this.nextExecutionTarget = "memory";
2404
4185
  if (this.nextExecutionReason == null) {
2405
4186
  this.nextExecutionReason = reason;
2406
4187
  }
2407
4188
  }
2408
- /**
2409
- * True when `split()` or `splitAt()` produced this collection.
2410
- *
2411
- * Those rebuild each half by re-adding its options, which re-derives execution targets
2412
- * without the options that caused them — a post-join filter alone in the memory half
2413
- * derives back to `"database"`. Anything reading `target` as a report of where work runs
2414
- * has to reject a derived collection; see `explainQuery`.
2415
- */ derived = false;
2416
- get isDerived() {
2417
- return this.derived;
2418
- }
2419
4189
  get items() {
2420
4190
  return this.options;
2421
4191
  }
@@ -2440,7 +4210,7 @@ class QueryOptionsCollection {
2440
4210
  }
2441
4211
  }
2442
4212
  if (name === "filter") {
2443
- // Need to check for unmapped and renamed properties
4213
+ // Need to check for unmapped properties
2444
4214
  const filterValue = value;
2445
4215
  // A tautology (`x => true`) filters nothing — skip it entirely so
2446
4216
  // plugins never see it
@@ -2450,19 +4220,20 @@ class QueryOptionsCollection {
2450
4220
  if (filterValue.expression.type === "not-parsable") {
2451
4221
  this.cutOverToMemory("not-parsable");
2452
4222
  } else {
2453
- (0,_expressions_utils__rspack_import_0/* .forEach */.j)(filterValue.expression, (expression)=>{
4223
+ (0,_expressions_utils__rspack_import_2/* .forEach */.jJ)(filterValue.expression, (expression)=>{
2454
4224
  if ((0,_assertions__rspack_import_1/* .isPropertyExpression */.e3)(expression) && expression.property.isUnmapped) {
2455
4225
  // Cut over to memory execution, unmapped properties are not in the database and
2456
4226
  // cannot be queried
2457
4227
  this.cutOverToMemory("unmapped-property");
2458
4228
  return false;
2459
4229
  }
2460
- if ((0,_assertions__rspack_import_1/* .isPropertyExpression */.e3)(expression) && expression.property.hasRenamedSegments) {
2461
- // Cut over to memory execution: the plugin stores data under the
2462
- // `from` (storage) names, but filter selectors reference the
2463
- // in-memory names. Memory execution runs after deserialization,
2464
- // where the in-memory names exist
2465
- this.cutOverToMemory("renamed-property");
4230
+ // A renamed property stays with the database. Whether the backend can read a
4231
+ // `from` name is the plugin's to know, not this collection's: the property
4232
+ // travels with the option, and a plugin that cannot resolve it reports it
4233
+ // back see `reportRenamedProperties`
4234
+ if (comparesTypesThatCannotMatch(expression)) {
4235
+ _utilities__rspack_import_3/* .logger.warn */.vF.warn(mismatchWarning(expression));
4236
+ this.cutOverToMemory("predicate-error");
2466
4237
  return false;
2467
4238
  }
2468
4239
  return true;
@@ -2471,28 +4242,26 @@ class QueryOptionsCollection {
2471
4242
  }
2472
4243
  if (name === "sort") {
2473
4244
  const sortValue = value;
2474
- // Same rule as filters: sort selectors reference in-memory names, which
2475
- // only exist after deserialization when the property is renamed or unmapped
4245
+ // Same rule as filters: an unmapped property only exists after deserialization. A
4246
+ // renamed one stays with the database, for the plugin to resolve or report
2476
4247
  if (sortValue.property != null && sortValue.property.isUnmapped) {
2477
4248
  this.cutOverToMemory("unmapped-property");
2478
- } else if (sortValue.property != null && sortValue.property.hasRenamedSegments) {
2479
- this.cutOverToMemory("renamed-property");
2480
4249
  }
2481
4250
  }
2482
4251
  if (name === "nearest") {
2483
4252
  const nearestValue = value;
2484
- // Same rule as sort, and for the same reason: the plugin stores the vector under
2485
- // the `from` name, and an unmapped property is not stored at all. Both are only
2486
- // readable after deserialization, which is where memory execution runs.
2487
- //
2488
- // This is also what lets every translator's in-memory fallback read the column by
2489
- // its resolved name — anything whose storage name differs never reaches them.
4253
+ // Same rule as sort, and for the same reason: an unmapped property is not stored at
4254
+ // all, so it is only readable after deserialization, which is where memory execution
4255
+ // runs. A vector stored under a `from` name is the plugin's to resolve or report.
2490
4256
  if (nearestValue.property != null && nearestValue.property.isUnmapped) {
2491
4257
  this.cutOverToMemory("unmapped-property");
2492
- } else if (nearestValue.property != null && nearestValue.property.hasRenamedSegments) {
2493
- this.cutOverToMemory("renamed-property");
2494
4258
  }
2495
4259
  }
4260
+ if ((name === "filter" || name === "sort") && (this.options.has("skip") || this.options.has("take"))) {
4261
+ // SQL emits WHERE before LIMIT and Mongo's find() filters before skipping, so an option
4262
+ // written after a window can only see the windowed rows if it runs after it.
4263
+ this.cutOverToMemory("after-window");
4264
+ }
2496
4265
  if (name === "join") {
2497
4266
  const joinValue = value;
2498
4267
  // A join whose two sides live on different plugins cannot be sent to EITHER of
@@ -2505,18 +4274,24 @@ class QueryOptionsCollection {
2505
4274
  this.cutOverToMemory("cross-plugin-join");
2506
4275
  }
2507
4276
  }
4277
+ // `executed` is the plan, not a record: nothing has run when an option is added. Every
4278
+ // consumer reads it after the plugin returned, so the optimistic window is never observed.
2508
4279
  const item = {
2509
4280
  index: this.nextIndex,
2510
- option: {
4281
+ option: this.nextExecutionTarget === "database" ? {
2511
4282
  name,
2512
- target: this.nextExecutionTarget,
2513
4283
  value,
2514
- ...this.nextExecutionReason == null ? {} : {
2515
- reason: this.nextExecutionReason
2516
- }
4284
+ target: "database",
4285
+ reason: "executed"
4286
+ } : {
4287
+ name,
4288
+ value,
4289
+ target: "memory",
4290
+ reason: this.nextExecutionReason ?? "not-parsable"
2517
4291
  }
2518
4292
  };
2519
4293
  this.nextIndex++;
4294
+ this.dirty = true;
2520
4295
  const found = this.options.get(name);
2521
4296
  this.options.set(name, [
2522
4297
  ...found ?? [],
@@ -2561,8 +4336,6 @@ class QueryOptionsCollection {
2561
4336
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
2562
4337
  const before = new QueryOptionsCollection();
2563
4338
  const after = new QueryOptionsCollection();
2564
- before.derived = true;
2565
- after.derived = true;
2566
4339
  let at = null;
2567
4340
  for(let i = 0, length = sortedItems.length; i < length; i++){
2568
4341
  const { option } = sortedItems[i];
@@ -2571,8 +4344,10 @@ class QueryOptionsCollection {
2571
4344
  continue;
2572
4345
  }
2573
4346
  const destination = at == null ? before : after;
2574
- destination.add(option.name, option.value);
4347
+ destination.adopt(sortedItems[i]);
2575
4348
  }
4349
+ before.origin = this.origin ?? this;
4350
+ after.origin = this.origin ?? this;
2576
4351
  return {
2577
4352
  before,
2578
4353
  at,
@@ -2586,6 +4361,9 @@ class QueryOptionsCollection {
2586
4361
  * the shared collection before executing. Without restoring, a re-executed terminal —
2587
4362
  * the whole point of a subscribed queryable — stacks its option a second time and
2588
4363
  * runs it over the first execution's scalar result.
4364
+ *
4365
+ * The item objects are shared with the snapshot. Nothing reports on them, because every
4366
+ * dispatch sends a `forDispatch` copy, so a restore brings back no reports.
2589
4367
  */ snapshot() {
2590
4368
  const options = new Map([
2591
4369
  ...this.options.entries()
@@ -2604,23 +4382,128 @@ class QueryOptionsCollection {
2604
4382
  this.nextExecutionReason = nextExecutionReason;
2605
4383
  this.nextIndex = nextIndex;
2606
4384
  this.enumeratedItems = [];
4385
+ // Clearing the list is not enough now that staleness is a flag rather than a count:
4386
+ // without this, `resolveEnumeration` believes the empty list is current and every read
4387
+ // of the collection sees no options at all.
4388
+ this.dirty = true;
4389
+ };
4390
+ }
4391
+ /** Takes an item as it stands — same object, same index, same target and reason. */ adopt(item) {
4392
+ const found = this.options.get(item.option.name);
4393
+ this.options.set(item.option.name, [
4394
+ ...found ?? [],
4395
+ item
4396
+ ]);
4397
+ this.nextIndex = Math.max(this.nextIndex, item.index + 1);
4398
+ this.dirty = true;
4399
+ }
4400
+ /**
4401
+ * A plugin reporting that its engine cannot express one option.
4402
+ *
4403
+ * Core marks the rest of the database phase `not-reached`, because the database has to stop
4404
+ * there — a window applied in front of a filter that was not applied returns the wrong rows.
4405
+ * Passing the cascade through core is what makes it impossible for a plugin to mark a
4406
+ * non-contiguous cut.
4407
+ *
4408
+ * A report names a culprit and never un-names one, so reports commute.
4409
+ *
4410
+ * The option is not moved to the memory arm. It stays where it was planned, which is what keeps
4411
+ * a redirect distinguishable from something core sent to memory in the first place.
4412
+ */ reportMissingCapability(item) {
4413
+ this.report(item, "missing-capability");
4414
+ }
4415
+ /**
4416
+ * A plugin reporting that its engine would answer one option differently from JavaScript.
4417
+ *
4418
+ * Same cascade as `reportMissingCapability`, and a separate reason because the caller can act on
4419
+ * one and not the other. See `DatabaseExecutionReason`.
4420
+ */ reportEngineDivergence(item) {
4421
+ this.report(item, "engine-divergence");
4422
+ }
4423
+ report(item, reason) {
4424
+ // A half can only see its own slice, and the database has to stop for the whole dispatch.
4425
+ if (this.origin != null) {
4426
+ this.origin.report(item, reason);
4427
+ return;
4428
+ }
4429
+ this.resolveEnumeration();
4430
+ for (const candidate of this.enumeratedItems){
4431
+ if (candidate.option.target !== "database" || candidate.index < item.index) {
4432
+ continue;
4433
+ }
4434
+ if (candidate.index === item.index) {
4435
+ candidate.option.reason = reason;
4436
+ continue;
4437
+ }
4438
+ if (candidate.option.reason === "executed") {
4439
+ candidate.option.reason = "not-reached";
4440
+ }
4441
+ }
4442
+ }
4443
+ /**
4444
+ * A copy of the collection for one dispatch to a plugin, with nothing reported on it.
4445
+ *
4446
+ * Capability is answered per dispatch, so a report is only an answer for the execution that
4447
+ * produced it. Reports are written onto items, and the items of a queryable's collection
4448
+ * outlive any one execution: a snapshot shares them, and a subscription dispatches the same
4449
+ * query on every change. A report left on them replays options the plugin did run on the
4450
+ * next execution, such as a `skip` applied twice over rows already windowed, or hands a
4451
+ * renamed filter to memory that the engine could have run.
4452
+ *
4453
+ * Each item keeps its index, name, value and target. A half from `split`/`splitAt` is copied
4454
+ * with a copy of its origin, and its items are that copy's items, so a report on the half still
4455
+ * cascades over the whole dispatch without reaching the collection it was copied from.
4456
+ */ forDispatch() {
4457
+ if (this.origin == null) {
4458
+ return this.copyForDispatch().copy;
4459
+ }
4460
+ const { copy: root, copies } = this.origin.copyForDispatch();
4461
+ const half = new QueryOptionsCollection();
4462
+ this.resolveEnumeration();
4463
+ for (const item of this.enumeratedItems){
4464
+ // An item added to the half after it was split has no counterpart in the origin
4465
+ half.adopt(copies.get(item) ?? toDispatchItem(item));
4466
+ }
4467
+ half.origin = root;
4468
+ return half;
4469
+ }
4470
+ copyForDispatch() {
4471
+ const copy = new QueryOptionsCollection();
4472
+ const copies = new Map();
4473
+ this.resolveEnumeration();
4474
+ for (const item of this.enumeratedItems){
4475
+ const copied = toDispatchItem(item);
4476
+ copies.set(item, copied);
4477
+ copy.adopt(copied);
4478
+ }
4479
+ copy.nextExecutionTarget = this.nextExecutionTarget;
4480
+ copy.nextExecutionReason = this.nextExecutionReason;
4481
+ copy.nextIndex = this.nextIndex;
4482
+ return {
4483
+ copy,
4484
+ copies
2607
4485
  };
2608
4486
  }
4487
+ /** The options the database did not run, in the order they were written. */ notExecuted() {
4488
+ this.resolveEnumeration();
4489
+ return this.enumeratedItems.filter((item)=>item.option.target === "database" && item.option.reason !== "executed").toSorted((a, b)=>a.index - b.index);
4490
+ }
2609
4491
  split() {
2610
4492
  this.resolveEnumeration();
2611
4493
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
2612
4494
  const memoryQueryOptionsCollection = new QueryOptionsCollection();
2613
4495
  const databaseQueryOptionsCollection = new QueryOptionsCollection();
2614
- memoryQueryOptionsCollection.derived = true;
2615
- databaseQueryOptionsCollection.derived = true;
2616
4496
  for(let i = 0, length = sortedItems.length; i < length; i++){
2617
4497
  const sortedItem = sortedItems[i];
2618
- if (sortedItem.option.target === "database") {
2619
- databaseQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
2620
- continue;
2621
- }
2622
- memoryQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
2623
- }
4498
+ const half = sortedItem.option.target === "database" ? databaseQueryOptionsCollection : memoryQueryOptionsCollection;
4499
+ // The ITEM, not its name and value. Re-adding would re-derive target and reason from a
4500
+ // fresh cascade, and a memory option re-added alone comes back out as `database` with no
4501
+ // reason at all. Sharing it also means a plugin's report on the database half is the
4502
+ // same object the explanation reads.
4503
+ half.adopt(sortedItem);
4504
+ }
4505
+ memoryQueryOptionsCollection.origin = this.origin ?? this;
4506
+ databaseQueryOptionsCollection.origin = this.origin ?? this;
2624
4507
  return {
2625
4508
  memory: memoryQueryOptionsCollection,
2626
4509
  database: databaseQueryOptionsCollection
@@ -2666,8 +4549,11 @@ class QueryOptionsCollection {
2666
4549
  ].flat().toSorted((a, b)=>a.index - b.index);
2667
4550
  }
2668
4551
  resolveEnumeration() {
2669
- if (this.enumeratedItems.length != this.nextIndex) {
4552
+ // A flag, not a count: adopting leaves gaps in the indexes, so `length !== nextIndex` is
4553
+ // true forever on a half and the enumeration rebuilds on every read.
4554
+ if (this.dirty === true) {
2670
4555
  this.enumeratedItems = this.getEnumeration();
4556
+ this.dirty = false;
2671
4557
  }
2672
4558
  }
2673
4559
  forEach(iterator) {
@@ -2796,6 +4682,128 @@ var HashType = /*#__PURE__*/ (/* unused pure expression or super */ null && (fun
2796
4682
  }({})));
2797
4683
 
2798
4684
 
4685
+ },
4686
+ 575(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4687
+ __webpack_require__.d(__webpack_exports__, {
4688
+ l: () => (isArrayValued)
4689
+ });
4690
+ /* import */ var _types__rspack_import_0 = __webpack_require__(537);
4691
+
4692
+ /**
4693
+ * Types whose runtime value is a JS array.
4694
+ *
4695
+ * `Array` and `Vector` are the same thing to every layer that copies, compares, hashes or
4696
+ * freezes a value — a vector is a list of numbers and nothing more. They differ only where a
4697
+ * backend chooses storage, which is the one place that should ask for `SchemaTypes.Vector` by
4698
+ * name.
4699
+ *
4700
+ * This exists so adding a third array-shaped type is one edit rather than a hunt through
4701
+ * twelve handlers. Missing one of those is silent in the worst way: a vector that clones by
4702
+ * reference is shared with the change tracker's copy, so overwriting an embedding produces no
4703
+ * diff and the save reports nothing to do.
4704
+ */ const ARRAY_VALUED_TYPES = new Set([
4705
+ _types__rspack_import_0/* .SchemaTypes.Array */.L.Array,
4706
+ _types__rspack_import_0/* .SchemaTypes.Vector */.L.Vector
4707
+ ]);
4708
+ /** True when the property's value is a JS array and needs value rather than reference semantics. */ const isArrayValued = (type)=>ARRAY_VALUED_TYPES.has(type);
4709
+ /**
4710
+ * True when the property's elements are primitives, so a spread is a sufficient copy.
4711
+ *
4712
+ * A vector is always numbers, so it never needs the per-element deep copy an array of objects
4713
+ * or dates does.
4714
+ */ const PRIMITIVE_ELEMENT_TYPES = new Set([
4715
+ _types__rspack_import_0/* .SchemaTypes.String */.L.String,
4716
+ _types__rspack_import_0/* .SchemaTypes.Number */.L.Number,
4717
+ _types__rspack_import_0/* .SchemaTypes.Boolean */.L.Boolean
4718
+ ]);
4719
+ const hasPrimitiveElements = (type, elementType)=>type === SchemaTypes.Vector || PRIMITIVE_ELEMENT_TYPES.has(elementType);
4720
+
4721
+
4722
+ },
4723
+ 894(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4724
+ __webpack_require__.d(__webpack_exports__, {
4725
+ T: () => (getStorageDateReviver)
4726
+ });
4727
+ /* import */ var _types__rspack_import_0 = __webpack_require__(537);
4728
+ /* import */ var _propertyKind__rspack_import_1 = __webpack_require__(575);
4729
+
4730
+
4731
+ const collectDatePaths = (properties, paths)=>{
4732
+ for (const property of properties){
4733
+ // The stored value belongs to whoever wrote it: a custom serializer, deserializer or
4734
+ // transform reads it back, and would be handed a Date it did not expect. Unmapped
4735
+ // properties are never stored.
4736
+ if (property.valueSerializer != null || property.valueDeserializer != null || property.transform != null || property.isUnmapped) {
4737
+ continue;
4738
+ }
4739
+ if (property.type === _types__rspack_import_0/* .SchemaTypes.Object */.L.Object) {
4740
+ collectDatePaths(property.children, paths);
4741
+ continue;
4742
+ }
4743
+ const isArray = (0,_propertyKind__rspack_import_1/* .isArrayValued */.l)(property.type) && property.innerSchema?.type === _types__rspack_import_0/* .SchemaTypes.Date */.L.Date;
4744
+ if (property.type !== _types__rspack_import_0/* .SchemaTypes.Date */.L.Date && isArray === false) {
4745
+ continue;
4746
+ }
4747
+ paths.push({
4748
+ segments: [
4749
+ ...property.getParentPathArray({
4750
+ useFromPropertyName: true
4751
+ }),
4752
+ property.getResolvedName()
4753
+ ],
4754
+ isArray
4755
+ });
4756
+ }
4757
+ };
4758
+ const reviveAt = (record, path)=>{
4759
+ const { segments } = path;
4760
+ let parent = record;
4761
+ for(let i = 0, length = segments.length - 1; i < length; i++){
4762
+ parent = parent[segments[i]];
4763
+ // An absent or null parent holds no date
4764
+ if (parent == null || typeof parent !== "object") {
4765
+ return;
4766
+ }
4767
+ }
4768
+ const key = segments[segments.length - 1];
4769
+ const value = parent[key];
4770
+ if (path.isArray === false) {
4771
+ if (typeof value === "string") {
4772
+ parent[key] = new Date(value);
4773
+ }
4774
+ return;
4775
+ }
4776
+ if (Array.isArray(value)) {
4777
+ for(let i = 0, length = value.length; i < length; i++){
4778
+ if (typeof value[i] === "string") {
4779
+ value[i] = new Date(value[i]);
4780
+ }
4781
+ }
4782
+ }
4783
+ };
4784
+ /** Per schema: `null` for a schema with no dates, so a caller can skip the pass. */ const revivers = new WeakMap();
4785
+ /**
4786
+ * The reviver for `schema`'s records, or `null` when it declares no dates.
4787
+ *
4788
+ * Built once per compiled schema. A read revives every row it returns, so the paths are resolved
4789
+ * here rather than per row.
4790
+ */ const getStorageDateReviver = (schema)=>{
4791
+ const cached = revivers.get(schema);
4792
+ if (cached !== undefined) {
4793
+ return cached;
4794
+ }
4795
+ const paths = [];
4796
+ collectDatePaths(schema.properties, paths);
4797
+ const reviver = paths.length === 0 ? null : (record)=>{
4798
+ for(let i = 0, length = paths.length; i < length; i++){
4799
+ reviveAt(record, paths[i]);
4800
+ }
4801
+ };
4802
+ revivers.set(schema, reviver);
4803
+ return reviver;
4804
+ };
4805
+
4806
+
2799
4807
  },
2800
4808
  76(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2801
4809
  __webpack_require__.d(__webpack_exports__, {
@@ -2887,12 +4895,14 @@ const isLogLevel = (value)=>typeof value === 'string' && LOG_LEVELS.includes(val
2887
4895
  const debug = process.env.DEBUG;
2888
4896
  if (debug === 'routier' || debug === '*') return 'debug';
2889
4897
  const env = "production"?.toLowerCase();
2890
- // `test` is deliberately absent. It used to be here, which meant no test suite anywhere
2891
- // could run Routier quietly. Opt in with DEBUG=routier or ROUTIER_LOG_LEVEL when a test
2892
- // needs the output.
2893
4898
  if (env === 'dev' || env === 'development') return 'debug';
2894
4899
  }
2895
- return 'silent';
4900
+ // Warnings are on unless something turns them off.
4901
+ //
4902
+ // Routier warns when a query returns correct rows a slower way than it could, or when a filter
4903
+ // compares types that can never match. Both are the caller's to act on, and a default of
4904
+ // `silent` meant the only people who ever saw them were the ones who already knew to look.
4905
+ return 'warn';
2896
4906
  };
2897
4907
  let level = resolveLevel();
2898
4908
  let rank = RANK[level];
@@ -3050,6 +5060,7 @@ __webpack_require__.d(__webpack_exports__, {
3050
5060
  _b: () => (/* reexport */ DEFAULT_SEMI_JOIN_KEY_THRESHOLD),
3051
5061
  pt: () => (/* reexport */ types_QueryOrdering),
3052
5062
  Ib: () => (/* reexport */ TranslatedSingleValue),
5063
+ fp: () => (/* reexport */ describeFilterAsJs),
3053
5064
  RK: () => (/* reexport */ distinctJoinKeys),
3054
5065
  Bg: () => (/* reexport */ hashJoin),
3055
5066
  _1: () => (/* reexport */ mappedResultColumns),
@@ -3058,27 +5069,36 @@ __webpack_require__.d(__webpack_exports__, {
3058
5069
  yR: () => (/* reexport */ serializePersistResult),
3059
5070
  y4: () => (/* reexport */ CacheDbPlugin),
3060
5071
  lO: () => (/* reexport */ cosineDistance),
5072
+ QC: () => (/* reexport */ DATABASE_EXECUTION_EXPLANATIONS),
3061
5073
  d0: () => (/* reexport */ JsonTranslator),
5074
+ Wi: () => (/* reexport */ parameter),
3062
5075
  PP: () => (/* reexport */ splitSendableOptions),
3063
5076
  f2: () => (/* reexport */ TranslatedGroupValue),
3064
5077
  wN: () => (/* reexport */ collectingSink),
3065
5078
  QB: () => (/* reexport */ RetryDbPlugin),
3066
5079
  m6: () => (/* reexport */ executeJoin),
3067
- __: () => (/* reexport */ toEntityShape),
5080
+ i1: () => (/* reexport */ parameteriseDocument),
5081
+ wk: () => (/* reexport */ reportRenamedProperties),
3068
5082
  VW: () => (/* reexport */ applyInnerOptions),
5083
+ B2: () => (/* reexport */ describeUnparsableFilter),
5084
+ __: () => (/* reexport */ toEntityShape),
3069
5085
  Jd: () => (/* reexport */ EphemeralDataPlugin),
3070
5086
  Pl: () => (/* reexport */ deserializePersistResult),
3071
5087
  JF: () => (/* reexport */ DataTranslator),
3072
5088
  HM: () => (/* reexport */ QueryOptionsCollection/* .QueryOptionsCollection */.H),
3073
5089
  KB: () => (/* reexport */ createRequestHandler),
3074
- vZ: () => (/* reexport */ formatExplanation),
5090
+ fN: () => (/* reexport */ executedQueriesOf),
3075
5091
  II: () => (/* reexport */ deserializeQueryOptions),
3076
- n: () => (/* reexport */ serializeBulkPersist),
5092
+ vZ: () => (/* reexport */ formatExplanation),
5093
+ yX: () => (/* reexport */ isDatabaseStep),
3077
5094
  as: () => (/* reexport */ loadJoinInnerSide),
5095
+ n: () => (/* reexport */ serializeBulkPersist),
3078
5096
  lA: () => (/* reexport */ semiJoinFilter),
5097
+ oJ: () => (/* reexport */ withInnerSide),
3079
5098
  kX: () => (/* reexport */ BatchingDbPlugin),
3080
5099
  DF: () => (/* reexport */ SqlTranslator),
3081
5100
  qj: () => (/* reexport */ loggerSink),
5101
+ To: () => (/* reexport */ describeFilters),
3082
5102
  gH: () => (/* reexport */ MEMORY_EXECUTION_EXPLANATIONS),
3083
5103
  BL: () => (/* reexport */ serializeQueryOptions),
3084
5104
  Kg: () => (/* reexport */ withExecutedQueries),
@@ -3195,6 +5215,10 @@ class DataTranslator {
3195
5215
  translate(data) {
3196
5216
  const isTransformed = this.query.options.hasTransformations();
3197
5217
  this.query.options.forEach((item)=>{
5218
+ // The plugin reported it could not run this one, so the memory pass owns it now.
5219
+ if (item.target === "database" && item.reason !== "executed") {
5220
+ return;
5221
+ }
3198
5222
  data = this.functionMap[item.name](data, item);
3199
5223
  });
3200
5224
  if (Array.isArray(data)) {
@@ -3607,7 +5631,7 @@ class Query {
3607
5631
  * Only a plugin that runs its outer query FIRST can supply these, and most run this loader
3608
5632
  * before anything else — so it is optional, and its absence costs a wider inner read rather
3609
5633
  * than a wrong one.
3610
- */ outerKeys)=>{
5634
+ */ outerKeys, /** Where the inner read reports what it executed. Defaults to the outer read's own list. */ innerExecutedQueries)=>{
3611
5635
  const joinOption = event.operation.options.getLast("join");
3612
5636
  if (joinOption == null) {
3613
5637
  done({
@@ -3635,9 +5659,10 @@ class Query {
3635
5659
  action: "query",
3636
5660
  reason: "join inner side",
3637
5661
  explain: event.explain,
3638
- // The same array the outer read pushes into, so a join reports BOTH reads in execution
3639
- // order. Built fresh rather than spread, so this has to be carried explicitly.
3640
- executedQueries: event.executedQueries
5662
+ // The caller decides where the inner read reports, because only it knows whether the inner
5663
+ // side is the SAME plugin where both reads belong in one explanation — or a different one,
5664
+ // where a PouchDB scan filed under SqliteDbPlugin is a lie.
5665
+ executedQueries: innerExecutedQueries ?? event.executedQueries
3641
5666
  };
3642
5667
  query(innerEvent, (result)=>{
3643
5668
  if (result.ok === Result/* .PluginEventResult.ERROR */.D.ERROR) {
@@ -3714,6 +5739,12 @@ class Query {
3714
5739
  return;
3715
5740
  }
3716
5741
  const outerRows = outerResult.data.value ?? [];
5742
+ if (at.reason !== "executed") {
5743
+ // The outer read reported something, so the database phase stopped before the join.
5744
+ // The datastore's own join branch pairs these rows.
5745
+ done(Result/* .PluginEventResult.success */.D.success(event.id, new TranslatedArrayValue(outerRows, false)));
5746
+ return;
5747
+ }
3717
5748
  // Storage shape: the plugin returns rows as it holds them, and deserialization is what
3718
5749
  // `executeJoin` does per side below.
3719
5750
  const outerKeys = distinctJoinKeys(outerRows, at.value.outerKey, at.value.semiJoinKeyThreshold, {
@@ -3829,9 +5860,7 @@ class JsonTranslator extends DataTranslator {
3829
5860
  if (field.property != null) {
3830
5861
  const value = field.property.getValue(data[i]);
3831
5862
  if (value != null) {
3832
- // Some types do not support deserialization (Array, Function, Computed, etc), just directly set the incoming value
3833
- const resolvedValue = field.property.supportsDeserialization ? field.property.deserialize(value) : value;
3834
- field.property.setValue(data[i], resolvedValue);
5863
+ field.property.setValue(data[i], field.property.deserialize(value));
3835
5864
  }
3836
5865
  }
3837
5866
  }
@@ -3855,9 +5884,7 @@ class JsonTranslator extends DataTranslator {
3855
5884
  if (field.property != null) {
3856
5885
  const value = field.property.getValue(data[i]);
3857
5886
  if (value != null) {
3858
- // Some types do not support deserialization (Array, Function, Computed, etc), just directly set the incoming value
3859
- const resolvedValue = field.property.supportsDeserialization ? field.property.deserialize(value) : value;
3860
- field.property.setValue(item, resolvedValue);
5887
+ field.property.setValue(item, field.property.deserialize(value));
3861
5888
  continue;
3862
5889
  }
3863
5890
  // The property exists, lets set it to the value (null/undefined)
@@ -4045,9 +6072,12 @@ class JsonTranslator extends DataTranslator {
4045
6072
  }
4046
6073
  }
4047
6074
 
6075
+ // EXTERNAL MODULE: ./src/schema/utils/storageDates.ts
6076
+ var storageDates = __webpack_require__(894);
4048
6077
  ;// CONCATENATED MODULE: ./src/plugins/translators/SqlTranslator.ts
4049
6078
 
4050
6079
 
6080
+
4051
6081
  /**
4052
6082
  * A stored vector as a list of numbers, whatever the driver handed back.
4053
6083
  *
@@ -4076,6 +6106,31 @@ class SqlTranslator extends DataTranslator {
4076
6106
  super(query);
4077
6107
  this.pushedDown = pushedDown;
4078
6108
  }
6109
+ /**
6110
+ * Dates back as Dates, before the caller's selectors run over the rows.
6111
+ *
6112
+ * A `group` key and a `map` are the caller's lambdas, run here over rows as the engine returned them.
6113
+ * SQLite, D1 and libSQL hand a date back as the TEXT it was stored as, which has no `getFullYear()`
6114
+ * and groups apart from the Date the entity holds. Revived at storage paths and in place, which the
6115
+ * datastore's deserialization still reads, and after `decodeJsonColumns`, so a date inside a JSON
6116
+ * column is revived too. Only a string is converted, so an engine that returns a Date (PostgreSQL,
6117
+ * PGlite, MySQL) is left alone, and so is a row already revived.
6118
+ *
6119
+ * Not a joined statement's rows, which are tuples, each half already deserialized. Nor rows whose
6120
+ * `group` or `map` was handed back, which the datastore runs after deserializing them.
6121
+ */ translate(data) {
6122
+ const runsHere = (name)=>this.query.options.get(name).some((item)=>item.option.target === "database" && item.option.reason === "executed");
6123
+ if (this.pushedDown.join !== true && this.query.schema != null && Array.isArray(data) && (runsHere("group") || runsHere("map"))) {
6124
+ const reviveDates = (0,storageDates/* .getStorageDateReviver */.T)(this.query.schema);
6125
+ for(let i = 0, length = reviveDates == null ? 0 : data.length; i < length; i++){
6126
+ const row = data[i];
6127
+ if (row != null && typeof row === "object") {
6128
+ reviveDates(row);
6129
+ }
6130
+ }
6131
+ }
6132
+ return super.translate(data);
6133
+ }
4079
6134
  count(data, _) {
4080
6135
  if (Array.isArray(data) && data.length > 0) {
4081
6136
  // Count is returned as the property alias on the query.
@@ -4198,9 +6253,12 @@ class SqlTranslator extends DataTranslator {
4198
6253
  for(let j = 0, l = option.value.fields.length; j < l; j++){
4199
6254
  const field = option.value.fields[j];
4200
6255
  if (field.property != null) {
4201
- const value = field.property.getValue(data[i]);
6256
+ const row = data[i];
6257
+ // A nested field arrives FLAT, under the alias the statement emitted, because
6258
+ // the value was read out of a JSON column. `setValue` puts it back on its path.
6259
+ const value = Object.prototype.hasOwnProperty.call(row, field.sourceName) ? row[field.sourceName] : field.property.getValue(row);
4202
6260
  if (value != null) {
4203
- field.property.setValue(data[i], field.property.deserialize(value));
6261
+ field.property.setValue(row, field.property.deserialize(value));
4204
6262
  }
4205
6263
  }
4206
6264
  }
@@ -4337,6 +6395,183 @@ class SqlTranslator extends DataTranslator {
4337
6395
 
4338
6396
 
4339
6397
 
6398
+ // EXTERNAL MODULE: ./src/expressions/callSource.ts
6399
+ var callSource = __webpack_require__(429);
6400
+ ;// CONCATENATED MODULE: ./src/plugins/query/describeFilter.ts
6401
+
6402
+
6403
+ const COMPARATOR_OPERATORS = {
6404
+ "equals": "===",
6405
+ "greater-than": ">",
6406
+ "greater-than-equals": ">=",
6407
+ "less-than": "<",
6408
+ "less-than-equals": "<="
6409
+ };
6410
+ /** The three comparators that read as a method call rather than an operator. */ const COMPARATOR_METHODS = {
6411
+ "starts-with": "startsWith",
6412
+ "includes": "includes",
6413
+ "ends-with": "endsWith"
6414
+ };
6415
+ const renderProperty = (property)=>property.property.getPathArray().join(".");
6416
+ /**
6417
+ * The predicate as JavaScript, with every value replaced by `?`.
6418
+ *
6419
+ * Rendered from the parsed tree rather than from the function's source. The tree is what the
6420
+ * backend was actually given, so this cannot drift from what ran; and a value reaching the tree
6421
+ * as a literal is indistinguishable from one arriving through a params object, which is what
6422
+ * makes both come out as `?` the way SQL treats them.
6423
+ */ const describeFilterAsJs = (expression)=>{
6424
+ const parameters = [];
6425
+ const hold = (value)=>{
6426
+ parameters.push(value);
6427
+ return "?";
6428
+ };
6429
+ const side = (part)=>{
6430
+ if (part == null) {
6431
+ return "?";
6432
+ }
6433
+ if ((0,assertions/* .isPropertyExpression */.e3)(part)) {
6434
+ return renderProperty(part);
6435
+ }
6436
+ if ((0,assertions/* .isValueExpression */.S6)(part)) {
6437
+ return hold(part.value);
6438
+ }
6439
+ if ((0,assertions/* .isCallExpression */.fm)(part)) {
6440
+ return (0,callSource/* .renderCallAsJs */.a)(part.call, ()=>side(part.expression), ()=>part.arguments.map(side));
6441
+ }
6442
+ return walk(part);
6443
+ };
6444
+ const walk = (current)=>{
6445
+ if ((0,assertions/* .isOperatorExpression */.vg)(current)) {
6446
+ const operator = current.operator === "&&" ? "&&" : "||";
6447
+ return `(${side(current.left)} ${operator} ${side(current.right)})`;
6448
+ }
6449
+ if ((0,assertions/* .isComparatorExpression */.xH)(current)) {
6450
+ const method = COMPARATOR_METHODS[current.comparator];
6451
+ // Evaluated LEFT then RIGHT, always: the parameter order has to match the reading
6452
+ // order of the text, or the values line up against the wrong placeholders.
6453
+ const left = side(current.left);
6454
+ const right = side(current.right);
6455
+ if (method != null) {
6456
+ const call = `${left}.${method}(${right})`;
6457
+ return current.negated ? `${call} === false` : call;
6458
+ }
6459
+ const symbol = COMPARATOR_OPERATORS[current.comparator];
6460
+ if (symbol == null) {
6461
+ return `${left} ${current.comparator} ${right}`;
6462
+ }
6463
+ return `${left} ${current.negated ? negate(symbol) : symbol} ${right}`;
6464
+ }
6465
+ if ((0,assertions/* .isCallExpression */.fm)(current)) {
6466
+ return (0,callSource/* .renderCallAsJs */.a)(current.call, ()=>side(current.expression), ()=>current.arguments.map(side));
6467
+ }
6468
+ if (current.type === "empty") {
6469
+ return "(no filter)";
6470
+ }
6471
+ return current.type === "not-parsable" ? "(not parsable)" : `(unsupported: ${current.type})`;
6472
+ };
6473
+ return {
6474
+ text: walk(expression),
6475
+ parameters
6476
+ };
6477
+ };
6478
+ const negate = (symbol)=>{
6479
+ switch(symbol){
6480
+ case "===":
6481
+ return "!==";
6482
+ case ">":
6483
+ return "<=";
6484
+ case ">=":
6485
+ return "<";
6486
+ case "<":
6487
+ return ">=";
6488
+ case "<=":
6489
+ return ">";
6490
+ default:
6491
+ return `!${symbol}`;
6492
+ }
6493
+ };
6494
+ /**
6495
+ * Marks a value inside a query document so it is replaced by `?` rather than printed.
6496
+ *
6497
+ * A document language carries its values inline, so there is nothing in the shape itself to say
6498
+ * which parts are operators and which are data. A dialect wraps the data as it builds the
6499
+ * document, and `parameteriseDocument` reads the wrapper.
6500
+ */ const PARAMETER = Symbol("routier.parameter");
6501
+ const parameter = (value)=>({
6502
+ [PARAMETER]: value
6503
+ });
6504
+ const isParameter = (value)=>typeof value === "object" && value !== null && PARAMETER in value;
6505
+ /**
6506
+ * Renders a query DOCUMENT with its values replaced by `?`.
6507
+ *
6508
+ * Language-agnostic on purpose: an MQL filter and a Mango selector are both plain objects, and so
6509
+ * is whatever a future document store wants reported. The dialect decides the shape; this only
6510
+ * decides how it is written down.
6511
+ *
6512
+ * A value not wrapped by `parameter` is structural — an operator name, a field path, a nesting
6513
+ * level — and is printed as it is. That is the whole distinction, and it has to be made where the
6514
+ * document is built, because by the time it is an object the two are the same kind of thing.
6515
+ */ const parameteriseDocument = (document)=>{
6516
+ const parameters = [];
6517
+ const render = (value)=>{
6518
+ if (isParameter(value)) {
6519
+ parameters.push(value[PARAMETER]);
6520
+ return "?";
6521
+ }
6522
+ if (Array.isArray(value)) {
6523
+ return `[${value.map(render).join(", ")}]`;
6524
+ }
6525
+ if (typeof value === "object" && value !== null) {
6526
+ const entries = Object.entries(value).map(([key, nested])=>`${JSON.stringify(key)}: ${render(nested)}`);
6527
+ return `{ ${entries.join(", ")} }`;
6528
+ }
6529
+ return JSON.stringify(value) ?? String(value);
6530
+ };
6531
+ return {
6532
+ text: render(document),
6533
+ parameters
6534
+ };
6535
+ };
6536
+ /**
6537
+ * Every filter on a query, as one description.
6538
+ *
6539
+ * Filters accumulate — `.where(a).where(b)` is `a && b` — so they are reported as one predicate
6540
+ * rather than several, which is how the caller thinks of them and how a SQL plugin renders them
6541
+ * into one `WHERE`. Parameters run left to right across the whole thing, matching the text.
6542
+ *
6543
+ * A filter that could not be parsed falls back to its source. Mixing the two is deliberate: one
6544
+ * unparsable filter does not make the others unreadable, and seeing which one it was is the
6545
+ * point.
6546
+ */ const describeFilters = (filters)=>{
6547
+ const parameters = [];
6548
+ const parts = filters.map((entry)=>{
6549
+ const described = entry.expression?.type === "not-parsable" ? describeUnparsableFilter(entry.filter, entry.expression.reason) : describeFilterAsJs(entry.expression);
6550
+ parameters.push(...described.parameters);
6551
+ return described.text;
6552
+ });
6553
+ if (parts.length === 0) {
6554
+ return {
6555
+ text: "(no filter)",
6556
+ parameters: []
6557
+ };
6558
+ }
6559
+ return {
6560
+ text: parts.length === 1 ? parts[0] : parts.join(" && "),
6561
+ parameters
6562
+ };
6563
+ };
6564
+ /**
6565
+ * A predicate core could not parse, shown as the caller wrote it.
6566
+ *
6567
+ * This is the case where the source matters most: an unparsable filter is why the query did not
6568
+ * push down, and the reason codes say that it happened without showing what it was. There are no
6569
+ * parameters — nothing was extracted, because nothing was understood.
6570
+ */ const describeUnparsableFilter = (filter, reason)=>({
6571
+ text: typeof filter === "function" ? `${String(filter)} — ${reason ?? "could not be parsed"}, evaluated in memory` : "(not parsable)",
6572
+ parameters: []
6573
+ });
6574
+
4340
6575
  ;// CONCATENATED MODULE: ./src/plugins/query/explain.ts
4341
6576
 
4342
6577
  /**
@@ -4347,16 +6582,26 @@ class SqlTranslator extends DataTranslator {
4347
6582
  */ const MEMORY_EXECUTION_EXPLANATIONS = {
4348
6583
  "not-parsable": "A filter could not be parsed into an expression tree, so it and every option after it run in memory.",
4349
6584
  "unmapped-property": "The property is not stored in the database, so it can only be read after deserialization.",
4350
- "renamed-property": "The property is stored under a different name, and selectors use the in-memory name, so it can only be read after deserialization.",
4351
6585
  "map-rename": "A map renames or drops properties, so every option after it refers to names the database does not have.",
4352
6586
  "after-nearest": "A similarity search orders and limits rows, and the plugin cannot report whether it performed the search, so every option after it runs in memory.",
4353
6587
  "after-join": "A join produces [outer, inner] tuples rather than entities, and the plugin cannot report how it joined, so every option after it runs in memory.",
4354
- "cross-plugin-join": "The two sides of this join live on different plugins, so neither can read the other's rows and the join runs in the datastore."
6588
+ "cross-plugin-join": "The two sides of this join live on different plugins, so neither can read the other's rows and the join runs in the datastore.",
6589
+ "after-window": "A skip or take runs before this option, and SQL applies WHERE before LIMIT, so pushing it down would window rows this option had not seen yet. It runs in memory over the windowed rows instead.",
6590
+ "predicate-error": "A strict comparison compares a column against a value of a type it can never equal, so the answer is the same for every row and the filter runs in memory. Check the types in the filter."
4355
6591
  };
4356
6592
  const EXECUTED_QUERIES_UNSUPPORTED = "This plugin did not report what it executed. It may not support explain.";
4357
- const DATABASE_STEP_DESCRIPTION = "These options are sent to the plugin.";
4358
- const MEMORY_STEP_DESCRIPTION = "Routier runs these over the rows the database returned, after deserializing them.";
4359
- const UNNARROWED_READ_DESCRIPTION = "No option could be pushed down, so the plugin reads the whole collection.";
6593
+ /**
6594
+ * Why an option planned for the database did not run there. `executed` has no sentence: it needs no
6595
+ * explaining, and a step made of executed options is a database step like any other.
6596
+ */ const DATABASE_EXECUTION_EXPLANATIONS = {
6597
+ "missing-capability": "The plugin's engine cannot express this option, so it runs in memory over the rows the plugin did return. Only the plugin can know this — an engine's capabilities are not visible from here.",
6598
+ "engine-divergence": "The plugin's engine would answer this option differently from JavaScript, so it runs in memory instead and the rows match what the predicate means. Nothing in the query needs changing.",
6599
+ "not-reached": "The database stopped at an option it could not express, so this one runs in memory too. Carrying on would apply it to rows the earlier option had not filtered."
6600
+ };
6601
+ /**
6602
+ * TypeScript does not narrow a union from a discriminant nested inside a property, so the two kinds
6603
+ * of step need a guard rather than an inline check.
6604
+ */ const isDatabaseStep = (step)=>step.executedIn.kind === "database";
4360
6605
  /**
4361
6606
  * The reportable shape of one option's value.
4362
6607
  *
@@ -4437,12 +6682,16 @@ const explainedOptionsOf = (options)=>{
4437
6682
  options.forEach((option)=>explained.push(explainedOptionOf(option, index++)));
4438
6683
  return explained;
4439
6684
  };
6685
+ /** Every sentence, whoever decided — the summary reads the same either way. */ const EXPLANATIONS = {
6686
+ ...MEMORY_EXECUTION_EXPLANATIONS,
6687
+ ...DATABASE_EXECUTION_EXPLANATIONS
6688
+ };
4440
6689
  const summarize = (steps)=>{
4441
6690
  const reasons = [];
4442
6691
  let database = 0;
4443
6692
  let memory = 0;
4444
6693
  for (const step of steps){
4445
- if (step.executedIn === "database") {
6694
+ if (isDatabaseStep(step)) {
4446
6695
  database += step.options.length;
4447
6696
  continue;
4448
6697
  }
@@ -4452,7 +6701,7 @@ const summarize = (steps)=>{
4452
6701
  }
4453
6702
  }
4454
6703
  const counts = `${database} ${database === 1 ? "option ran" : "options ran"} in the database, ${memory} ran in memory.`;
4455
- const causes = reasons.map((reason)=>MEMORY_EXECUTION_EXPLANATIONS[reason]).join(" ");
6704
+ const causes = reasons.map((reason)=>EXPLANATIONS[reason]).join(" ");
4456
6705
  return {
4457
6706
  database,
4458
6707
  memory,
@@ -4460,50 +6709,87 @@ const summarize = (steps)=>{
4460
6709
  explanation: causes.length === 0 ? counts : `${counts} ${causes}`
4461
6710
  };
4462
6711
  };
4463
- /**
4464
- * Groups options into consecutive runs that execute in the same place.
4465
- *
4466
- * A step boundary is where execution moves, and a reader has to see the statement as step 1 OF
4467
- * 2 to understand it is not the whole query. Cutting over to memory is a ratchet, so the
4468
- * database options are always a prefix and there are at most two steps.
4469
- *
4470
- * A database step is emitted even when NO option pushed down, because the plugin is dispatched
4471
- * either way — `createQueryPayload` always builds a database event. Without it, the worst case
4472
- * the feature exists to expose reports "0 in the database" while the backend reads the whole
4473
- * table, which is the opposite of the truth.
4474
- */ const toExecutionSteps = (options)=>{
6712
+ const outcomeOf = (option)=>{
6713
+ if (option.target === "memory") {
6714
+ return {
6715
+ executedIn: "memory",
6716
+ reason: option.reason,
6717
+ explanation: MEMORY_EXECUTION_EXPLANATIONS[option.reason]
6718
+ };
6719
+ }
6720
+ if (option.reason === "executed") {
6721
+ return {
6722
+ executedIn: "database",
6723
+ reason: null,
6724
+ explanation: null
6725
+ };
6726
+ }
6727
+ return {
6728
+ executedIn: "memory",
6729
+ reason: option.reason,
6730
+ explanation: DATABASE_EXECUTION_EXPLANATIONS[option.reason]
6731
+ };
6732
+ };
6733
+ /** Whether an option belongs to the step already open, or starts a new one. */ const continuesStep = (current, outcome)=>{
6734
+ if (current == null) {
6735
+ return false;
6736
+ }
6737
+ if (current.executedIn.kind === "database") {
6738
+ return outcome.reason == null;
6739
+ }
6740
+ // `?? null` because a step with no reason omits the key, and `undefined === null` is false —
6741
+ // without it every option started a step of its own
6742
+ return outcome.reason != null && (current.reason ?? null) === outcome.reason;
6743
+ };
6744
+ const toExecutionSteps = (options, ranIn)=>{
4475
6745
  const steps = [];
4476
6746
  let index = 0;
4477
6747
  options.forEach((option)=>{
4478
6748
  const explained = explainedOptionOf(option, index++);
4479
6749
  const current = steps[steps.length - 1];
4480
- if (current != null && current.executedIn === option.target) {
6750
+ const outcome = outcomeOf(option);
6751
+ // Grouped by outcome, not by target: an option the database could not express and one core
6752
+ // sent to memory both run in memory, for different reasons a reader needs told apart.
6753
+ if (continuesStep(current, outcome) === true) {
4481
6754
  current.options.push(explained);
4482
6755
  return;
4483
6756
  }
4484
- steps.push({
4485
- step: steps.length + 1,
6757
+ steps.push(outcome.reason == null ? {
6758
+ step: 0,
4486
6759
  of: 0,
4487
- executedIn: option.target,
4488
- description: option.target === "database" ? DATABASE_STEP_DESCRIPTION : MEMORY_STEP_DESCRIPTION,
6760
+ executedIn: ranIn,
4489
6761
  options: [
4490
6762
  explained
4491
6763
  ],
4492
- ...option.reason == null ? {} : {
4493
- reason: option.reason,
4494
- explanation: MEMORY_EXECUTION_EXPLANATIONS[option.reason]
4495
- }
6764
+ executedQueries: []
6765
+ } : {
6766
+ step: 0,
6767
+ of: 0,
6768
+ executedIn: {
6769
+ kind: "memory"
6770
+ },
6771
+ options: [
6772
+ explained
6773
+ ],
6774
+ reason: outcome.reason,
6775
+ explanation: outcome.explanation ?? undefined
4496
6776
  });
4497
6777
  });
4498
- if (steps[0]?.executedIn !== "database") {
6778
+ // A database step even when nothing pushed down: the plugin is dispatched either way, so
6779
+ // reporting "0 in the database" while the backend reads the whole table is the opposite of
6780
+ // the truth.
6781
+ if (steps[0]?.executedIn.kind !== "database") {
4499
6782
  steps.unshift({
4500
6783
  step: 0,
4501
6784
  of: 0,
4502
- executedIn: "database",
4503
- description: UNNARROWED_READ_DESCRIPTION,
4504
- options: []
6785
+ executedIn: ranIn,
6786
+ options: [],
6787
+ executedQueries: []
4505
6788
  });
4506
6789
  }
6790
+ return steps;
6791
+ };
6792
+ /** Numbers a finished list, so `step 1 of 3` reads as the shape of the whole query. */ const numbered = (steps)=>{
4507
6793
  for(let i = 0; i < steps.length; i++){
4508
6794
  steps[i].step = i + 1;
4509
6795
  steps[i].of = steps.length;
@@ -4518,10 +6804,11 @@ const summarize = (steps)=>{
4518
6804
  * post-join filter alone in the memory half derives back to `"database"`, and the document
4519
6805
  * would report memory work as having run in the database.
4520
6806
  */ const explainQuery = (options, context)=>{
4521
- if (options.isDerived === true) {
4522
- throw new Error("explainQuery was given a collection produced by split() or splitAt(). Those re-derive " + "execution targets without the options that caused them, so the explanation would report " + "memory work as having run in the database. Pass the collection as it was resolved, " + "before splitting.");
4523
- }
4524
- const executionSteps = toExecutionSteps(options);
6807
+ const executionSteps = numbered(toExecutionSteps(options, {
6808
+ kind: "database",
6809
+ database: context.database,
6810
+ plugin: context.pluginKind
6811
+ }));
4525
6812
  return {
4526
6813
  collection: context.collection,
4527
6814
  database: context.database,
@@ -4548,7 +6835,7 @@ const summarize = (steps)=>{
4548
6835
  // Only the first database step: a plugin reports what IT ran, and everything it ran
4549
6836
  // was sent as one dispatch. Stamping the same statements onto a second database step
4550
6837
  // would claim they ran twice.
4551
- if (step.executedIn !== "database" || attached === true) {
6838
+ if (isDatabaseStep(step) === false || attached === true) {
4552
6839
  return {
4553
6840
  ...step,
4554
6841
  options: [
@@ -4581,8 +6868,47 @@ const summarize = (steps)=>{
4581
6868
  executionSteps
4582
6869
  };
4583
6870
  };
6871
+ /**
6872
+ * Adds the step for a cross-plugin join's inner side.
6873
+ *
6874
+ * Appended by the executor rather than derived from the options, because the inner side's options
6875
+ * live on the join, in its own collection, and were never part of this query's chain. It goes before
6876
+ * the memory steps that consume it — the join cannot run until both sides are read.
6877
+ */ /**
6878
+ * Every statement the query ran, across every database it touched, in execution order.
6879
+ *
6880
+ * A step is a place, so the statements live on the steps — this is for a caller that wants them all
6881
+ * without caring which plugin ran which.
6882
+ */ const executedQueriesOf = (explanation)=>explanation.executionSteps.flatMap((step)=>isDatabaseStep(step) ? step.executedQueries : []);
6883
+ const withInnerSide = (explanation, innerSide)=>{
6884
+ const step = {
6885
+ step: 0,
6886
+ of: 0,
6887
+ executedIn: {
6888
+ kind: "database",
6889
+ database: innerSide.database,
6890
+ plugin: innerSide.plugin
6891
+ },
6892
+ options: [],
6893
+ executedQueries: innerSide.executedQueries
6894
+ };
6895
+ const firstMemory = explanation.executionSteps.findIndex((current)=>isDatabaseStep(current) === false);
6896
+ const at = firstMemory === -1 ? explanation.executionSteps.length : firstMemory;
6897
+ const executionSteps = numbered([
6898
+ ...explanation.executionSteps.slice(0, at),
6899
+ step,
6900
+ ...explanation.executionSteps.slice(at)
6901
+ ]);
6902
+ return {
6903
+ ...explanation,
6904
+ executionSteps,
6905
+ summary: summarize(executionSteps)
6906
+ };
6907
+ };
4584
6908
 
4585
6909
  ;// CONCATENATED MODULE: ./src/plugins/query/formatExplanation.ts
6910
+
6911
+
4586
6912
  const OPTION_LABEL_WIDTH = 8;
4587
6913
  const WRAP_WIDTH = 68;
4588
6914
  /** Wraps `text` to `WRAP_WIDTH`, prefixing every line with `indent`. */ const wrap = (text, indent)=>{
@@ -4608,29 +6934,41 @@ const COMPARATOR_SYMBOLS = {
4608
6934
  "less-than": "<",
4609
6935
  "less-than-equals": "<="
4610
6936
  };
4611
- const describeValue = (value)=>{
4612
- if (value == null) {
6937
+ /** Typed against the union so a new OBJECT tag is a compile error here, not an "undefined" in output. */ const describeValue = (value)=>{
6938
+ if (value === null) {
6939
+ return "null";
6940
+ }
6941
+ if (value === undefined) {
4613
6942
  return "?";
4614
6943
  }
4615
- if (value.k === "raw") {
4616
- return typeof value.v === "string" ? `"${value.v}"` : String(value.v);
6944
+ if (Array.isArray(value)) {
6945
+ return `[${value.map(describeValue).join(", ")}]`;
6946
+ }
6947
+ if (typeof value !== "object") {
6948
+ return typeof value === "string" ? `"${value}"` : String(value);
6949
+ }
6950
+ if ("date" in value) {
6951
+ return value.date;
4617
6952
  }
4618
- if (value.k === "date") {
4619
- return value.v;
6953
+ if ("undefined" in value) {
6954
+ return "undefined";
4620
6955
  }
4621
- if (value.k === "array") {
4622
- return `[${value.v.map(describeValue).join(", ")}]`;
6956
+ if ("regex" in value) {
6957
+ return `/${value.regex.source}/${value.regex.flags}`;
4623
6958
  }
4624
- return value.k === "undefined" ? "undefined" : String(value.v);
6959
+ if ("bigint" in value) {
6960
+ return `${value.bigint}n`;
6961
+ }
6962
+ return value.number;
4625
6963
  };
4626
6964
  /** Renders a serialized expression back to something close to the source predicate. */ const describeExpression = (expression)=>{
4627
6965
  if (expression == null) {
4628
6966
  return "?";
4629
6967
  }
4630
- if (expression.t === "operator") {
6968
+ if (expression.type === "operator") {
4631
6969
  return `${describeExpression(expression.left)} ${expression.operator} ${describeExpression(expression.right)}`;
4632
6970
  }
4633
- if (expression.t === "comparator") {
6971
+ if (expression.type === "comparator") {
4634
6972
  const left = describeExpression(expression.left);
4635
6973
  const right = describeExpression(expression.right);
4636
6974
  const symbol = COMPARATOR_SYMBOLS[expression.comparator];
@@ -4639,13 +6977,24 @@ const describeValue = (value)=>{
4639
6977
  }
4640
6978
  return `${left} ${expression.negated === true ? "!==" : symbol} ${right}`;
4641
6979
  }
4642
- if (expression.t === "property") {
6980
+ if (expression.type === "property") {
4643
6981
  return expression.path;
4644
6982
  }
4645
- if (expression.t === "value") {
6983
+ if (expression.type === "value") {
4646
6984
  return describeValue(expression.value);
4647
6985
  }
4648
- return expression.t === "empty" ? "(no filter)" : "(not parsable)";
6986
+ if (expression.type === "call") {
6987
+ return (0,callSource/* .renderCallAsJs */.a)(expression.call, ()=>describeExpression(expression.expression), ()=>(expression.arguments ?? []).map(describeExpression));
6988
+ }
6989
+ if (expression.type === "empty") {
6990
+ return "(no filter)";
6991
+ }
6992
+ // Distinguishable from "(not parsable)", which means the parser gave up and this runs in memory
6993
+ if (expression.type === "not-parsable") {
6994
+ return expression.reason == null ? "(not parsable)" : `(not parsable: ${expression.reason})`;
6995
+ }
6996
+ // Unreachable while the union is exhausted above; a payload from a newer sender is not.
6997
+ return `(unsupported: ${expression.type})`;
4649
6998
  };
4650
6999
  const describeOption = (option)=>{
4651
7000
  const detail = option.detail;
@@ -4673,18 +7022,35 @@ const describeOption = (option)=>{
4673
7022
  }
4674
7023
  return "";
4675
7024
  };
7025
+ /**
7026
+ * The sentence for a kind of step.
7027
+ *
7028
+ * Here rather than on the step: it is one of two constants keyed off `executedIn`, so carrying it in
7029
+ * the payload put prose beside the field it was derived from.
7030
+ */ const DATABASE_STEP_DESCRIPTION = "These options are sent to the plugin.";
7031
+ const MEMORY_STEP_DESCRIPTION = "Routier runs these over the rows the database returned, after deserializing them.";
7032
+ const UNNARROWED_READ_DESCRIPTION = "No option could be pushed down, so the plugin reads the whole collection.";
7033
+ /** `database · orders.db · SqliteDbPlugin`, so a cross-plugin join says who ran what. */ const whereItRan = (step)=>isDatabaseStep(step) ? `database · ${step.executedIn.database} · ${step.executedIn.plugin}` : "memory";
4676
7034
  const formatStep = (step, lines)=>{
4677
- const reason = step.reason == null ? "" : ` [${step.reason}]`;
4678
- lines.push(` STEP ${step.step} of ${step.of} — ${step.executedIn}${reason}`);
4679
- lines.push(...wrap(step.description, " "));
4680
- if (step.explanation != null) {
4681
- lines.push(...wrap(step.explanation, " "));
7035
+ const reason = isDatabaseStep(step) || step.reason == null ? "" : ` [${step.reason}]`;
7036
+ lines.push(` STEP ${step.step} of ${step.of} — ${whereItRan(step)}${reason}`);
7037
+ if (isDatabaseStep(step)) {
7038
+ lines.push(...wrap(step.options.length === 0 ? UNNARROWED_READ_DESCRIPTION : DATABASE_STEP_DESCRIPTION, " "));
7039
+ } else {
7040
+ lines.push(...wrap(MEMORY_STEP_DESCRIPTION, " "));
7041
+ if (step.explanation != null) {
7042
+ lines.push(...wrap(step.explanation, " "));
7043
+ }
4682
7044
  }
4683
7045
  lines.push("");
4684
7046
  for (const option of step.options){
4685
7047
  lines.push(` ${option.name.padEnd(OPTION_LABEL_WIDTH)} ${describeOption(option)}`.trimEnd());
4686
7048
  }
4687
- for (const executed of step.executedQueries ?? []){
7049
+ if (isDatabaseStep(step) === false) {
7050
+ lines.push("");
7051
+ return;
7052
+ }
7053
+ for (const executed of step.executedQueries){
4688
7054
  lines.push("");
4689
7055
  lines.push(...executed.text.split("\n").map((line)=>` ${line}`));
4690
7056
  if (executed.parameters != null && executed.parameters.length > 0) {
@@ -4717,6 +7083,80 @@ const formatStep = (step, lines)=>{
4717
7083
  return lines.join("\n");
4718
7084
  };
4719
7085
 
7086
+ // EXTERNAL MODULE: ./src/expressions/utils.ts
7087
+ var utils = __webpack_require__(63);
7088
+ ;// CONCATENATED MODULE: ./src/plugins/query/renames.ts
7089
+
7090
+
7091
+ const PROPERTY_READING_OPTIONS = [
7092
+ "filter",
7093
+ "sort",
7094
+ "nearest",
7095
+ "map",
7096
+ "group"
7097
+ ];
7098
+ const namesRenamedProperty = (expression)=>{
7099
+ let found = false;
7100
+ if (expression == null) {
7101
+ return found;
7102
+ }
7103
+ (0,utils/* .forEach */.jJ)(expression, (node)=>{
7104
+ if ((0,assertions/* .isPropertyExpression */.e3)(node) && node.property.hasRenamedSegments) {
7105
+ found = true;
7106
+ return false;
7107
+ }
7108
+ return true;
7109
+ });
7110
+ return found;
7111
+ };
7112
+ const isRenamed = (property)=>property != null && property.hasRenamedSegments;
7113
+ /**
7114
+ * Whether a selector's value is read from a renamed property, whether it is that property or computed
7115
+ * from it: `r => r.dueDate.getTime()` reads `dueDate` all the same. Every property it reads when the
7116
+ * selector was parsed, and otherwise the property recorded for it.
7117
+ */ const readsRenamedValue = (value)=>value != null && (value.reads != null ? value.reads.some(isRenamed) : isRenamed(value.property));
7118
+ const readsRenamedField = (fields)=>fields != null && fields.some(readsRenamedValue);
7119
+ const readsRenamedProperty = (name, value)=>{
7120
+ switch(name){
7121
+ case "filter":
7122
+ return namesRenamedProperty(value.expression);
7123
+ case "map":
7124
+ // A projection reads each field it selects
7125
+ return readsRenamedField(value.fields);
7126
+ case "group":
7127
+ // A group reads its key, then copies every field of the row into its members: every schema
7128
+ // property, or what a `map` before it selected
7129
+ return readsRenamedValue(value.key) || readsRenamedField(value.fields);
7130
+ default:
7131
+ return readsRenamedValue(value);
7132
+ }
7133
+ };
7134
+ /**
7135
+ * Hands back every option over a property stored under a `.from()` name, for the datastore to run
7136
+ * in memory.
7137
+ *
7138
+ * Core keeps such an option with the database, because only the plugin knows whether its backend
7139
+ * reads storage names. One that translates the option — SQL renders the column from
7140
+ * `getResolvedName()` — needs nothing from here. One that runs the caller's lambda over rows as it
7141
+ * stores them reads a key the row does not have, and answers wrongly without an error: that plugin
7142
+ * calls this before it reads anything, and the datastore finishes the query after deserialization,
7143
+ * where the in-memory names exist.
7144
+ *
7145
+ * Reported as `missing-capability`: the backend cannot express the option as written, and like
7146
+ * every capability, that is only knowable by the plugin.
7147
+ *
7148
+ * @param names Which options to check, for a plugin that resolves some of them itself — Mongo renders
7149
+ * filters and sorts through the stored path, and runs `nearest`, `map` and `group` in JavaScript.
7150
+ */ const reportRenamedProperties = (options, names = PROPERTY_READING_OPTIONS)=>{
7151
+ for (const name of names){
7152
+ for (const item of options.get(name)){
7153
+ if (readsRenamedProperty(name, item.option.value)) {
7154
+ options.reportMissingCapability(item);
7155
+ }
7156
+ }
7157
+ }
7158
+ };
7159
+
4720
7160
  ;// CONCATENATED MODULE: ./src/plugins/query/types.ts
4721
7161
  var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4722
7162
  QueryOrdering["Descending"] = "desc";
@@ -4733,8 +7173,12 @@ var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4733
7173
 
4734
7174
 
4735
7175
 
7176
+
7177
+
4736
7178
  // EXTERNAL MODULE: ./src/expressions/evaluate.ts
4737
7179
  var evaluate = __webpack_require__(379);
7180
+ // EXTERNAL MODULE: ./src/expressions/fold.ts
7181
+ var fold = __webpack_require__(43);
4738
7182
  ;// CONCATENATED MODULE: ./src/plugins/wire/query.ts
4739
7183
 
4740
7184
 
@@ -4744,6 +7188,9 @@ var evaluate = __webpack_require__(379);
4744
7188
  * Everything except `map` and `group`. Those two are defined BY a closure — the projection is the
4745
7189
  * option — and no data form of them exists to send. Nothing else needs its closure: a sort is a
4746
7190
  * property and a direction, a filter is an expression tree, `nearest` is a vector and a count.
7191
+ *
7192
+ * Except a sort or `nearest` whose selector computes its value, such as `x => x.name.length`. It is sent
7193
+ * as the property it reads, and the receiver would order by that instead. See `isSendable`.
4747
7194
  */ const SENDABLE = new Set([
4748
7195
  "skip",
4749
7196
  "take",
@@ -4757,6 +7204,7 @@ var evaluate = __webpack_require__(379);
4757
7204
  "sum",
4758
7205
  "distinct"
4759
7206
  ]);
7207
+ const isSendable = (name, value)=>SENDABLE.has(name) && (name !== "sort" && name !== "nearest" || value.isDirectProperty !== false);
4760
7208
  /**
4761
7209
  * Splits options into the PREFIX that can be sent and the remainder that cannot.
4762
7210
  *
@@ -4772,7 +7220,12 @@ var evaluate = __webpack_require__(379);
4772
7220
  const local = new QueryOptionsCollection/* .QueryOptionsCollection */.H();
4773
7221
  let stopped = false;
4774
7222
  options.forEach((option)=>{
4775
- if (stopped === false && SENDABLE.has(option.name) === false) {
7223
+ // Reported by the plugin, so it belongs to the datastore, and so does everything after it —
7224
+ // a report cascades to the end of the database phase, which keeps what is left a prefix
7225
+ if (option.target === "database" && option.reason !== "executed") {
7226
+ return;
7227
+ }
7228
+ if (stopped === false && isSendable(option.name, option.value) === false) {
4776
7229
  stopped = true;
4777
7230
  }
4778
7231
  (stopped ? local : sendable).add(option.name, option.value);
@@ -4935,7 +7388,7 @@ const serializeQueryOptions = (options)=>{
4935
7388
  }
4936
7389
  case "filter":
4937
7390
  {
4938
- const expression = types/* .Expression.fromJson */.r4.fromJson(option.value.expression, schema);
7391
+ const expression = (0,fold/* .foldConstantCalls */.F5)(types/* .Expression.fromJson */.r4.fromJson(option.value.expression, schema));
4939
7392
  options.add("filter", {
4940
7393
  filter: (0,evaluate/* .toStrictPredicate */.wS)(expression),
4941
7394
  expression,
@@ -5619,7 +8072,8 @@ var TrampolinePipeline = __webpack_require__(416);
5619
8072
  if (!(0,assertions/* .isPropertyExpression */.e3)(left) || !(0,assertions/* .isValueExpression */.S6)(right)) {
5620
8073
  return null;
5621
8074
  }
5622
- if (left.property.isKey !== true || left.transformer != null || right.value == null) {
8075
+ // A called property is a CallExpression, so it fails the isPropertyExpression check above
8076
+ if (left.property.isKey !== true || right.value == null) {
5623
8077
  return null;
5624
8078
  }
5625
8079
  return {
@@ -5638,6 +8092,15 @@ class EphemeralDataPlugin {
5638
8092
  */ get databaseName() {
5639
8093
  return this._databaseName;
5640
8094
  }
8095
+ /**
8096
+ * Whether the records this plugin holds are in storage shape, keyed by `from` names.
8097
+ *
8098
+ * True for every store of what the datastore serialized, which is why a renamed property is
8099
+ * reported and records are cloned and keyed by their storage names. The datastore's change probe
8100
+ * holds rows the broadcast has already deserialized, so it reads them by in-memory names instead.
8101
+ */ get holdsStorageShape() {
8102
+ return true;
8103
+ }
5641
8104
  /**
5642
8105
  * All-or-nothing across every collection in the save.
5643
8106
  *
@@ -5845,7 +8308,9 @@ class EphemeralDataPlugin {
5845
8308
  * join discards the surplus. Same pairs either way.
5846
8309
  */ resolveJoinInnerSide(event, outerKeys, done) {
5847
8310
  const joinOption = event.operation.options.getLast("join");
5848
- if (joinOption == null) {
8311
+ // Not reached when an option before it was reported: the datastore's own join branch pairs
8312
+ // the rows this read returns.
8313
+ if (joinOption == null || joinOption.reason !== "executed") {
5849
8314
  done({
5850
8315
  ok: "success"
5851
8316
  });
@@ -5872,7 +8337,7 @@ class EphemeralDataPlugin {
5872
8337
  const innerRows = [];
5873
8338
  // Records are held in STORAGE shape, so the key is read by its resolved column name.
5874
8339
  const innerKey = joinOption.value.innerKey;
5875
- const keyColumn = innerKey.property?.getResolvedName() ?? innerKey.propertyName;
8340
+ const keyColumn = this.holdsStorageShape ? innerKey.property?.getResolvedName() ?? innerKey.propertyName : innerKey.propertyName;
5876
8341
  for (const record of innerCollection.values()){
5877
8342
  if (outerKeys != null && outerKeys.has(record[keyColumn]) === false) {
5878
8343
  continue;
@@ -5901,7 +8366,7 @@ class EphemeralDataPlugin {
5901
8366
  * to fall back to `structuredClone`, which is roughly an order of magnitude slower and was paid
5902
8367
  * on EVERY read of EVERY schema that renames a property.
5903
8368
  */ recordCloner(schema) {
5904
- const hasRenamedProperties = schema.properties.some((property)=>property.from != null);
8369
+ const hasRenamedProperties = this.holdsStorageShape && schema.properties.some((property)=>property.from != null);
5905
8370
  return hasRenamedProperties ? schema.cloneStorage : schema.clone;
5906
8371
  }
5907
8372
  query(event, done) {
@@ -5913,6 +8378,12 @@ class EphemeralDataPlugin {
5913
8378
  const schema = operation.schema;
5914
8379
  const collection = this.resolveCollection(schema);
5915
8380
  const cloneRecord = this.recordCloner(schema);
8381
+ // Records are held in storage shape and every option below runs the caller's lambda
8382
+ // over them, so a `from` property is read by a name the record does not have. Handed
8383
+ // back, and the datastore runs it after deserialization.
8384
+ if (this.holdsStorageShape) {
8385
+ reportRenamedProperties(operation.options);
8386
+ }
5916
8387
  collection.load((r)=>{
5917
8388
  if (r.ok === Result/* .Result.ERROR */.Q.ERROR) {
5918
8389
  done(Result/* .PluginEventResult.error */.D.error(event.id, r.error));
@@ -5921,7 +8392,9 @@ class EphemeralDataPlugin {
5921
8392
  const orderedOptions = [];
5922
8393
  operation.options.forEach((o)=>orderedOptions.push(o));
5923
8394
  let leadingFilterCount = 0;
5924
- while(leadingFilterCount < orderedOptions.length && orderedOptions[leadingFilterCount].name === "filter"){
8395
+ // Stops at a reported filter too: the database phase ends there, and the datastore
8396
+ // runs it and everything after it.
8397
+ while(leadingFilterCount < orderedOptions.length && orderedOptions[leadingFilterCount].name === "filter" && orderedOptions[leadingFilterCount].reason === "executed"){
5925
8398
  leadingFilterCount++;
5926
8399
  }
5927
8400
  // Key-equality fast path: when a leading filter's parsed expression pins
@@ -5992,15 +8465,21 @@ class EphemeralDataPlugin {
5992
8465
  * collection to pair it with three rows.
5993
8466
  *
5994
8467
  * `cloned` is in storage shape, so the keys are read by resolved column name.
5995
- */ // No statement to quote — an ephemeral store walks its own records. Said
5996
- // plainly so `.explain()` does not leave a reader wondering whether the
5997
- // plugin simply failed to report. Before the inner side, to match execution order.
8468
+ */ /**
8469
+ * No statement to quote an ephemeral store walks its own records — so the scan
8470
+ * is said plainly, and the PREDICATE is reported as JavaScript beside it. A count
8471
+ * alone leaves a reader unable to tell a filter that matched nothing from one
8472
+ * that was never applied.
8473
+ *
8474
+ * Before the inner side, to match execution order.
8475
+ */ const described = describeFilters(operation.options.get("filter").filter((entry)=>entry.option.reason === "executed").map((entry)=>entry.option.value));
5998
8476
  event.executedQueries.push({
5999
- text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ${cloned.length === 1 ? "record" : "records"}`
8477
+ text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ` + `${cloned.length === 1 ? "record" : "records"}, filter ${described.text}`,
8478
+ parameters: described.parameters.length > 0 ? described.parameters : undefined
6000
8479
  });
6001
8480
  const joinOption = operation.options.getLast("join");
6002
- const outerKeys = joinOption == null ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
6003
- storageShape: true
8481
+ const outerKeys = joinOption == null || joinOption.reason !== "executed" ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
8482
+ storageShape: this.holdsStorageShape
6004
8483
  });
6005
8484
  this.resolveJoinInnerSide(event, outerKeys, (joinResult)=>{
6006
8485
  if (joinResult.ok === "error") {
@@ -6171,6 +8650,29 @@ class TelemetryDbPlugin {
6171
8650
  return JSON.stringify(option.value ?? null);
6172
8651
  }
6173
8652
  };
8653
+ /**
8654
+ * Restores a `Date` that `structuredClone` produced outside this realm.
8655
+ *
8656
+ * The clone is a real date and fails `instanceof Date`, which is what a caller checks. Mutated in
8657
+ * place because the clone is already private to this call.
8658
+ */ const CacheDbPlugin_reviveDates = (value)=>{
8659
+ if (value == null || typeof value !== "object") {
8660
+ return value;
8661
+ }
8662
+ if (Object.prototype.toString.call(value) === "[object Date]") {
8663
+ return value instanceof Date ? value : new Date(value);
8664
+ }
8665
+ if (Array.isArray(value)) {
8666
+ for(let i = 0, length = value.length; i < length; i++){
8667
+ value[i] = CacheDbPlugin_reviveDates(value[i]);
8668
+ }
8669
+ return value;
8670
+ }
8671
+ for (const key of Object.keys(value)){
8672
+ value[key] = CacheDbPlugin_reviveDates(value[key]);
8673
+ }
8674
+ return value;
8675
+ };
6174
8676
  class CacheDbPlugin {
6175
8677
  plugin;
6176
8678
  max;
@@ -6209,7 +8711,7 @@ class CacheDbPlugin {
6209
8711
  * the next update would be written UNCHECKED with no error anywhere.
6210
8712
  * Pinned by `datastore/src/collections/wrapperStacking.test.ts`.
6211
8713
  */ rebuild(entry) {
6212
- return new entry.construct(structuredClone(entry.value), entry.isTransformed);
8714
+ return new entry.construct(CacheDbPlugin_reviveDates(structuredClone(entry.value)), entry.isTransformed);
6213
8715
  }
6214
8716
  query(event, done) {
6215
8717
  const key = this.keyFor(event);
@@ -6232,6 +8734,14 @@ class CacheDbPlugin {
6232
8734
  done(result);
6233
8735
  return;
6234
8736
  }
8737
+ // A partial answer must never be cached. When the plugin reports an option it cannot
8738
+ // express, these rows are what came back BEFORE the datastore finished the query — and a
8739
+ // later hit skips the plugin entirely, so nothing would report and the rows would be
8740
+ // returned as if they were the whole answer. Unfiltered, silently.
8741
+ if (event.operation.options.notExecuted().length > 0) {
8742
+ done(Result/* .PluginEventResult.success */.D.success(event.id, result.data));
8743
+ return;
8744
+ }
6235
8745
  this.store(key, result.data);
6236
8746
  // The caller gets a rebuilt value too, not the one just stored, so that mutating
6237
8747
  // the result of a MISS cannot corrupt what the next hit returns.
@@ -6615,6 +9125,7 @@ const DEFAULT_MAX_BATCH_SIZE = 100;
6615
9125
  var __webpack_exports__BatchingDbPlugin = __webpack_exports__.kX;
6616
9126
  var __webpack_exports__CacheDbPlugin = __webpack_exports__.y4;
6617
9127
  var __webpack_exports__ConcurrencyDbPlugin = __webpack_exports__.bX;
9128
+ var __webpack_exports__DATABASE_EXECUTION_EXPLANATIONS = __webpack_exports__.QC;
6618
9129
  var __webpack_exports__DEFAULT_SEMI_JOIN_KEY_THRESHOLD = __webpack_exports__._b;
6619
9130
  var __webpack_exports__DataTranslator = __webpack_exports__.JF;
6620
9131
  var __webpack_exports__EXECUTED_QUERIES_UNSUPPORTED = __webpack_exports__.jE;
@@ -6635,20 +9146,28 @@ var __webpack_exports__applyInnerOptions = __webpack_exports__.VW;
6635
9146
  var __webpack_exports__collectingSink = __webpack_exports__.wN;
6636
9147
  var __webpack_exports__cosineDistance = __webpack_exports__.lO;
6637
9148
  var __webpack_exports__createRequestHandler = __webpack_exports__.KB;
9149
+ var __webpack_exports__describeFilterAsJs = __webpack_exports__.fp;
9150
+ var __webpack_exports__describeFilters = __webpack_exports__.To;
9151
+ var __webpack_exports__describeUnparsableFilter = __webpack_exports__.B2;
6638
9152
  var __webpack_exports__deserializeBulkPersist = __webpack_exports__.Mr;
6639
9153
  var __webpack_exports__deserializePersistResult = __webpack_exports__.Pl;
6640
9154
  var __webpack_exports__deserializeQueryOptions = __webpack_exports__.II;
6641
9155
  var __webpack_exports__distinctJoinKeys = __webpack_exports__.RK;
6642
9156
  var __webpack_exports__executeJoin = __webpack_exports__.m6;
9157
+ var __webpack_exports__executedQueriesOf = __webpack_exports__.fN;
6643
9158
  var __webpack_exports__explainQuery = __webpack_exports__.ae;
6644
9159
  var __webpack_exports__formatExplanation = __webpack_exports__.vZ;
6645
9160
  var __webpack_exports__hashJoin = __webpack_exports__.Bg;
9161
+ var __webpack_exports__isDatabaseStep = __webpack_exports__.yX;
6646
9162
  var __webpack_exports__joinInPlugin = __webpack_exports__.zH;
6647
9163
  var __webpack_exports__loadJoinInnerSide = __webpack_exports__.as;
6648
9164
  var __webpack_exports__loggerSink = __webpack_exports__.qj;
6649
9165
  var __webpack_exports__mappedResultColumns = __webpack_exports__._1;
6650
9166
  var __webpack_exports__nearestBy = __webpack_exports__.iG;
9167
+ var __webpack_exports__parameter = __webpack_exports__.Wi;
9168
+ var __webpack_exports__parameteriseDocument = __webpack_exports__.i1;
6651
9169
  var __webpack_exports__readJoinKey = __webpack_exports__.qy;
9170
+ var __webpack_exports__reportRenamedProperties = __webpack_exports__.wk;
6652
9171
  var __webpack_exports__semiJoinFilter = __webpack_exports__.lA;
6653
9172
  var __webpack_exports__serializeBulkPersist = __webpack_exports__.n;
6654
9173
  var __webpack_exports__serializePersistResult = __webpack_exports__.yR;
@@ -6656,6 +9175,7 @@ var __webpack_exports__serializeQueryOptions = __webpack_exports__.BL;
6656
9175
  var __webpack_exports__splitSendableOptions = __webpack_exports__.PP;
6657
9176
  var __webpack_exports__toEntityShape = __webpack_exports__.__;
6658
9177
  var __webpack_exports__withExecutedQueries = __webpack_exports__.Kg;
6659
- export { __webpack_exports__BatchingDbPlugin as BatchingDbPlugin, __webpack_exports__CacheDbPlugin as CacheDbPlugin, __webpack_exports__ConcurrencyDbPlugin as ConcurrencyDbPlugin, __webpack_exports__DEFAULT_SEMI_JOIN_KEY_THRESHOLD as DEFAULT_SEMI_JOIN_KEY_THRESHOLD, __webpack_exports__DataTranslator as DataTranslator, __webpack_exports__EXECUTED_QUERIES_UNSUPPORTED as EXECUTED_QUERIES_UNSUPPORTED, __webpack_exports__EphemeralDataPlugin as EphemeralDataPlugin, __webpack_exports__JsonTranslator as JsonTranslator, __webpack_exports__MEMORY_EXECUTION_EXPLANATIONS as MEMORY_EXECUTION_EXPLANATIONS, __webpack_exports__Query as Query, __webpack_exports__QueryOptionsCollection as QueryOptionsCollection, __webpack_exports__QueryOrdering as QueryOrdering, __webpack_exports__RetryDbPlugin as RetryDbPlugin, __webpack_exports__SqlTranslator as SqlTranslator, __webpack_exports__TelemetryDbPlugin as TelemetryDbPlugin, __webpack_exports__TranslatedArrayValue as TranslatedArrayValue, __webpack_exports__TranslatedGroupValue as TranslatedGroupValue, __webpack_exports__TranslatedSingleValue as TranslatedSingleValue, __webpack_exports__TupleTranslator as TupleTranslator, __webpack_exports__applyInnerOptions as applyInnerOptions, __webpack_exports__collectingSink as collectingSink, __webpack_exports__cosineDistance as cosineDistance, __webpack_exports__createRequestHandler as createRequestHandler, __webpack_exports__deserializeBulkPersist as deserializeBulkPersist, __webpack_exports__deserializePersistResult as deserializePersistResult, __webpack_exports__deserializeQueryOptions as deserializeQueryOptions, __webpack_exports__distinctJoinKeys as distinctJoinKeys, __webpack_exports__executeJoin as executeJoin, __webpack_exports__explainQuery as explainQuery, __webpack_exports__formatExplanation as formatExplanation, __webpack_exports__hashJoin as hashJoin, __webpack_exports__joinInPlugin as joinInPlugin, __webpack_exports__loadJoinInnerSide as loadJoinInnerSide, __webpack_exports__loggerSink as loggerSink, __webpack_exports__mappedResultColumns as mappedResultColumns, __webpack_exports__nearestBy as nearestBy, __webpack_exports__readJoinKey as readJoinKey, __webpack_exports__semiJoinFilter as semiJoinFilter, __webpack_exports__serializeBulkPersist as serializeBulkPersist, __webpack_exports__serializePersistResult as serializePersistResult, __webpack_exports__serializeQueryOptions as serializeQueryOptions, __webpack_exports__splitSendableOptions as splitSendableOptions, __webpack_exports__toEntityShape as toEntityShape, __webpack_exports__withExecutedQueries as withExecutedQueries };
9178
+ var __webpack_exports__withInnerSide = __webpack_exports__.oJ;
9179
+ export { __webpack_exports__BatchingDbPlugin as BatchingDbPlugin, __webpack_exports__CacheDbPlugin as CacheDbPlugin, __webpack_exports__ConcurrencyDbPlugin as ConcurrencyDbPlugin, __webpack_exports__DATABASE_EXECUTION_EXPLANATIONS as DATABASE_EXECUTION_EXPLANATIONS, __webpack_exports__DEFAULT_SEMI_JOIN_KEY_THRESHOLD as DEFAULT_SEMI_JOIN_KEY_THRESHOLD, __webpack_exports__DataTranslator as DataTranslator, __webpack_exports__EXECUTED_QUERIES_UNSUPPORTED as EXECUTED_QUERIES_UNSUPPORTED, __webpack_exports__EphemeralDataPlugin as EphemeralDataPlugin, __webpack_exports__JsonTranslator as JsonTranslator, __webpack_exports__MEMORY_EXECUTION_EXPLANATIONS as MEMORY_EXECUTION_EXPLANATIONS, __webpack_exports__Query as Query, __webpack_exports__QueryOptionsCollection as QueryOptionsCollection, __webpack_exports__QueryOrdering as QueryOrdering, __webpack_exports__RetryDbPlugin as RetryDbPlugin, __webpack_exports__SqlTranslator as SqlTranslator, __webpack_exports__TelemetryDbPlugin as TelemetryDbPlugin, __webpack_exports__TranslatedArrayValue as TranslatedArrayValue, __webpack_exports__TranslatedGroupValue as TranslatedGroupValue, __webpack_exports__TranslatedSingleValue as TranslatedSingleValue, __webpack_exports__TupleTranslator as TupleTranslator, __webpack_exports__applyInnerOptions as applyInnerOptions, __webpack_exports__collectingSink as collectingSink, __webpack_exports__cosineDistance as cosineDistance, __webpack_exports__createRequestHandler as createRequestHandler, __webpack_exports__describeFilterAsJs as describeFilterAsJs, __webpack_exports__describeFilters as describeFilters, __webpack_exports__describeUnparsableFilter as describeUnparsableFilter, __webpack_exports__deserializeBulkPersist as deserializeBulkPersist, __webpack_exports__deserializePersistResult as deserializePersistResult, __webpack_exports__deserializeQueryOptions as deserializeQueryOptions, __webpack_exports__distinctJoinKeys as distinctJoinKeys, __webpack_exports__executeJoin as executeJoin, __webpack_exports__executedQueriesOf as executedQueriesOf, __webpack_exports__explainQuery as explainQuery, __webpack_exports__formatExplanation as formatExplanation, __webpack_exports__hashJoin as hashJoin, __webpack_exports__isDatabaseStep as isDatabaseStep, __webpack_exports__joinInPlugin as joinInPlugin, __webpack_exports__loadJoinInnerSide as loadJoinInnerSide, __webpack_exports__loggerSink as loggerSink, __webpack_exports__mappedResultColumns as mappedResultColumns, __webpack_exports__nearestBy as nearestBy, __webpack_exports__parameter as parameter, __webpack_exports__parameteriseDocument as parameteriseDocument, __webpack_exports__readJoinKey as readJoinKey, __webpack_exports__reportRenamedProperties as reportRenamedProperties, __webpack_exports__semiJoinFilter as semiJoinFilter, __webpack_exports__serializeBulkPersist as serializeBulkPersist, __webpack_exports__serializePersistResult as serializePersistResult, __webpack_exports__serializeQueryOptions as serializeQueryOptions, __webpack_exports__splitSendableOptions as splitSendableOptions, __webpack_exports__toEntityShape as toEntityShape, __webpack_exports__withExecutedQueries as withExecutedQueries, __webpack_exports__withInnerSide as withInnerSide };
6660
9180
 
6661
9181
  //# sourceMappingURL=index.js.map