@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
@@ -6,6 +6,7 @@ __webpack_require__.d(__webpack_exports__, {
6
6
  assertIsArray: () => (assertIsArray),
7
7
  assertIsNotNull: () => (assertIsNotNull),
8
8
  assertString: () => (assertString),
9
+ isCallExpression: () => (isCallExpression),
9
10
  isComparatorExpression: () => (isComparatorExpression),
10
11
  isOperatorExpression: () => (isOperatorExpression),
11
12
  isPropertyExpression: () => (isPropertyExpression),
@@ -81,6 +82,11 @@ function isObjectWithType(value) {
81
82
  */ function isValueExpression(value) {
82
83
  return isObjectWithType(value) && value.type === "value";
83
84
  }
85
+ /**
86
+ * Type guard: narrows `value` to `CallExpression` when it is an object with `type === "call"`.
87
+ */ function isCallExpression(value) {
88
+ return isObjectWithType(value) && value.type === "call";
89
+ }
84
90
  /**
85
91
  * Type guard: narrows `value` to `EmptyExpression` when it is an object with `type === "empty"`.
86
92
  */ function isEmptyExpression(value) {
@@ -401,45 +407,374 @@ __webpack_require__.d(__webpack_exports__, {
401
407
  }
402
408
 
403
409
 
410
+ },
411
+ 429(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
412
+ __webpack_require__.d(__webpack_exports__, {
413
+ a: () => (renderCallAsJs)
414
+ });
415
+ const CALL_SOURCE = {
416
+ "to-lower-case": {
417
+ form: "method",
418
+ name: "toLowerCase"
419
+ },
420
+ "to-upper-case": {
421
+ form: "method",
422
+ name: "toUpperCase"
423
+ },
424
+ "length": {
425
+ form: "property",
426
+ name: "length"
427
+ },
428
+ "trim": {
429
+ form: "method",
430
+ name: "trim"
431
+ },
432
+ "trim-start": {
433
+ form: "method",
434
+ name: "trimStart"
435
+ },
436
+ "trim-end": {
437
+ form: "method",
438
+ name: "trimEnd"
439
+ },
440
+ "index-of": {
441
+ form: "method",
442
+ name: "indexOf"
443
+ },
444
+ "substring": {
445
+ form: "method",
446
+ name: "substring"
447
+ },
448
+ "concat": {
449
+ form: "method",
450
+ name: "concat"
451
+ },
452
+ "replace": {
453
+ form: "method",
454
+ name: "replace"
455
+ },
456
+ "replace-all": {
457
+ form: "method",
458
+ name: "replaceAll"
459
+ },
460
+ "absolute": {
461
+ form: "function",
462
+ name: "Math.abs"
463
+ },
464
+ "floor": {
465
+ form: "function",
466
+ name: "Math.floor"
467
+ },
468
+ "ceiling": {
469
+ form: "function",
470
+ name: "Math.ceil"
471
+ },
472
+ "round": {
473
+ form: "function",
474
+ name: "Math.round"
475
+ },
476
+ "sign": {
477
+ form: "function",
478
+ name: "Math.sign"
479
+ },
480
+ "square-root": {
481
+ form: "function",
482
+ name: "Math.sqrt"
483
+ },
484
+ "add": {
485
+ form: "operator",
486
+ symbol: "+"
487
+ },
488
+ "subtract": {
489
+ form: "operator",
490
+ symbol: "-"
491
+ },
492
+ "multiply": {
493
+ form: "operator",
494
+ symbol: "*"
495
+ },
496
+ "divide": {
497
+ form: "operator",
498
+ symbol: "/"
499
+ },
500
+ "modulo": {
501
+ form: "operator",
502
+ symbol: "%"
503
+ },
504
+ "utc-year": {
505
+ form: "method",
506
+ name: "getUTCFullYear"
507
+ },
508
+ "utc-month": {
509
+ form: "method",
510
+ name: "getUTCMonth"
511
+ },
512
+ "utc-day-of-month": {
513
+ form: "method",
514
+ name: "getUTCDate"
515
+ },
516
+ "utc-day-of-week": {
517
+ form: "method",
518
+ name: "getUTCDay"
519
+ },
520
+ "utc-hour": {
521
+ form: "method",
522
+ name: "getUTCHours"
523
+ },
524
+ "utc-minute": {
525
+ form: "method",
526
+ name: "getUTCMinutes"
527
+ },
528
+ "utc-second": {
529
+ form: "method",
530
+ name: "getUTCSeconds"
531
+ },
532
+ "utc-millisecond": {
533
+ form: "method",
534
+ name: "getUTCMilliseconds"
535
+ },
536
+ "epoch-ms": {
537
+ form: "method",
538
+ name: "getTime"
539
+ },
540
+ "to-string": {
541
+ form: "function",
542
+ name: "String"
543
+ },
544
+ "to-number": {
545
+ form: "function",
546
+ name: "Number"
547
+ },
548
+ "to-boolean": {
549
+ form: "function",
550
+ name: "Boolean"
551
+ },
552
+ "type-of": {
553
+ form: "prefix",
554
+ keyword: "typeof"
555
+ },
556
+ "some": {
557
+ form: "method",
558
+ name: "some"
559
+ },
560
+ "every": {
561
+ form: "method",
562
+ name: "every"
563
+ },
564
+ // `Math.pow(a, b)` parses to the same call; `**` is the shorter of the two spellings
565
+ "power": {
566
+ form: "operator",
567
+ symbol: "**"
568
+ },
569
+ "bit-and": {
570
+ form: "operator",
571
+ symbol: "&"
572
+ },
573
+ "bit-or": {
574
+ form: "operator",
575
+ symbol: "|"
576
+ },
577
+ "bit-xor": {
578
+ form: "operator",
579
+ symbol: "^"
580
+ },
581
+ "shift-left": {
582
+ form: "operator",
583
+ symbol: "<<"
584
+ },
585
+ "shift-right": {
586
+ form: "operator",
587
+ symbol: ">>"
588
+ },
589
+ "shift-right-unsigned": {
590
+ form: "operator",
591
+ symbol: ">>>"
592
+ },
593
+ "bit-not": {
594
+ form: "prefix",
595
+ keyword: "~"
596
+ },
597
+ "coalesce": {
598
+ form: "operator",
599
+ symbol: "??"
600
+ },
601
+ "conditional": {
602
+ form: "conditional"
603
+ },
604
+ "matches": {
605
+ form: "regex-test"
606
+ }
607
+ };
608
+ /**
609
+ * A call rendered as the JavaScript that produced it, from operand and argument text already
610
+ * rendered by the caller.
611
+ *
612
+ * Takes strings so one implementation serves a live tree and a serialized one.
613
+ */ /**
614
+ * Thunked because rendering a side can record a parameter, and `regex-test` emits its argument
615
+ * before its operand — so the two orders have to agree.
616
+ */ const renderCallAsJs = (call, renderOperand, renderArgs)=>{
617
+ const source = CALL_SOURCE[call];
618
+ if (source == null) {
619
+ const operand = renderOperand();
620
+ return `${operand}.${call}(${renderArgs().join(", ")})`;
621
+ }
622
+ if (source.form === "property") {
623
+ return `${renderOperand()}.${source.name}`;
624
+ }
625
+ if (source.form === "regex-test") {
626
+ const pattern = renderArgs()[0] ?? "?";
627
+ return `${pattern}.test(${renderOperand()})`;
628
+ }
629
+ if (source.form === "method") {
630
+ const operand = renderOperand();
631
+ return `${operand}.${source.name}(${renderArgs().join(", ")})`;
632
+ }
633
+ if (source.form === "function") {
634
+ const operand = renderOperand();
635
+ return `${source.name}(${[
636
+ operand,
637
+ ...renderArgs()
638
+ ].join(", ")})`;
639
+ }
640
+ if (source.form === "prefix") {
641
+ // `~x`, not `~ x` — a bitwise complement is written tight, unlike `typeof`
642
+ const operand = renderOperand();
643
+ return source.keyword === "~" ? `${source.keyword}${operand}` : `${source.keyword} ${operand}`;
644
+ }
645
+ if (source.form === "conditional") {
646
+ const operand = renderOperand();
647
+ const args = renderArgs();
648
+ return `${operand} ? ${args[0] ?? "?"} : ${args[1] ?? "?"}`;
649
+ }
650
+ const operand = renderOperand();
651
+ return `${[
652
+ operand,
653
+ ...renderArgs()
654
+ ].join(` ${source.symbol} `)}`;
655
+ };
656
+
657
+
404
658
  },
405
659
  379(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
406
660
  __webpack_require__.d(__webpack_exports__, {
661
+ Vv: () => (operandValue),
662
+ _3: () => (evaluate),
663
+ gm: () => (UNRESOLVED),
407
664
  wS: () => (toStrictPredicate)
408
665
  });
409
666
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
410
667
 
411
668
  /** Reads a property or literal operand, or `UNRESOLVED` when the node is not one. */ const UNRESOLVED = Symbol("unresolved");
412
- const applyTransformer = (value, transformer)=>{
413
- if (transformer == null) {
414
- return value;
669
+ const ARITHMETIC = {
670
+ "add": (left, right)=>left + right,
671
+ "subtract": (left, right)=>left - right,
672
+ "multiply": (left, right)=>left * right,
673
+ "divide": (left, right)=>left / right,
674
+ "modulo": (left, right)=>left % right,
675
+ "power": (left, right)=>left ** right,
676
+ "bit-and": (left, right)=>left & right,
677
+ "bit-or": (left, right)=>left | right,
678
+ "bit-xor": (left, right)=>left ^ right,
679
+ "shift-left": (left, right)=>left << right,
680
+ "shift-right": (left, right)=>left >> right,
681
+ "shift-right-unsigned": (left, right)=>left >>> right
682
+ };
683
+ const applyCall = (call, value, args)=>{
684
+ // Above the guard: a template renders null as "null" in JavaScript, so these two are total.
685
+ if (call === "to-string") {
686
+ return String(value);
687
+ }
688
+ if (call === "concat") {
689
+ return [
690
+ value,
691
+ ...args
692
+ ].map(String).join("");
415
693
  }
416
- // A transformer applied to an absent value has no answer, and inventing one ("" for a missing
417
- // string) is how a filter starts matching rows it should not.
694
+ // A call applied to an absent value has no answer, and inventing one ("" for a missing string)
695
+ // is how a filter starts matching rows it should not.
418
696
  if (value == null) {
419
697
  return UNRESOLVED;
420
698
  }
421
- if (transformer === "to-lower-case") {
422
- return typeof value === "string" ? value.toLowerCase() : UNRESOLVED;
423
- }
424
- if (transformer === "to-upper-case") {
425
- return typeof value === "string" ? value.toUpperCase() : UNRESOLVED;
699
+ if (call === "to-lower-case" || call === "to-upper-case") {
700
+ if (typeof value !== "string") {
701
+ return UNRESOLVED;
702
+ }
703
+ const lower = call === "to-lower-case";
704
+ if (args.length === 0 || args[0] == null) {
705
+ return lower ? value.toLowerCase() : value.toUpperCase();
706
+ }
707
+ if (typeof args[0] !== "string") {
708
+ return UNRESOLVED;
709
+ }
710
+ try {
711
+ // An explicit locale is deterministic; dropping it answers a different question in Turkish.
712
+ return lower ? value.toLocaleLowerCase(args[0]) : value.toLocaleUpperCase(args[0]);
713
+ } catch {
714
+ // An invalid language tag throws RangeError; no answer beats the host's default.
715
+ return UNRESOLVED;
716
+ }
426
717
  }
427
- if (transformer === "length") {
718
+ if (call === "length") {
428
719
  return typeof value === "string" || Array.isArray(value) ? value.length : UNRESOLVED;
429
720
  }
721
+ if (call === "bit-not") {
722
+ return typeof value === "number" ? ~value : UNRESOLVED;
723
+ }
724
+ if (call === "matches") {
725
+ if (typeof value !== "string" || !(args[0] instanceof RegExp)) {
726
+ return UNRESOLVED;
727
+ }
728
+ // `test` advances `lastIndex` on a global or sticky pattern, and the pattern is shared with
729
+ // the cached template, where a source evaluates fresh in JavaScript.
730
+ return args[0].global || args[0].sticky ? new RegExp(args[0].source, args[0].flags).test(value) : args[0].test(value);
731
+ }
732
+ const arithmetic = ARITHMETIC[call];
733
+ if (arithmetic != null) {
734
+ return typeof value === "number" && typeof args[0] === "number" ? arithmetic(value, args[0]) : UNRESOLVED;
735
+ }
430
736
  return UNRESOLVED;
431
737
  };
432
- const operand = (expression, row)=>{
738
+ const operandValue = (expression, row)=>{
433
739
  if (expression == null) {
434
740
  return UNRESOLVED;
435
741
  }
436
742
  if ((0,_assertions__rspack_import_0.isValueExpression)(expression)) {
437
- return applyTransformer(expression.value, expression.transformer);
743
+ return expression.value;
438
744
  }
439
745
  if ((0,_assertions__rspack_import_0.isPropertyExpression)(expression)) {
440
746
  // Through the PropertyInfo, so a nested path and a `from`-renamed segment resolve the same
441
747
  // way every other consumer of the tree resolves them.
442
- return applyTransformer(expression.property.getValue(row), expression.transformer);
748
+ return expression.property.getValue(row);
749
+ }
750
+ if ((0,_assertions__rspack_import_0.isCallExpression)(expression)) {
751
+ /**
752
+ * `??` and `? :` are the two calls whose whole job is to answer when something is absent, so
753
+ * they run before the guard that refuses an absent operand.
754
+ */ if (expression.call === "coalesce") {
755
+ const left = operandValue(expression.expression, row);
756
+ return left === UNRESOLVED || left == null ? operandValue(expression.arguments[0], row) : left;
757
+ }
758
+ if (expression.call === "conditional") {
759
+ const condition = evaluate(expression.expression, row);
760
+ if (condition === undefined) {
761
+ return UNRESOLVED;
762
+ }
763
+ return operandValue(expression.arguments[condition === true ? 0 : 1], row);
764
+ }
765
+ const inner = operandValue(expression.expression, row);
766
+ if (inner === UNRESOLVED) {
767
+ return UNRESOLVED;
768
+ }
769
+ const args = [];
770
+ for (const argument of expression.arguments){
771
+ const resolved = operandValue(argument, row);
772
+ if (resolved === UNRESOLVED) {
773
+ return UNRESOLVED;
774
+ }
775
+ args.push(resolved);
776
+ }
777
+ return applyCall(expression.call, inner, args);
443
778
  }
444
779
  return UNRESOLVED;
445
780
  };
@@ -524,8 +859,8 @@ const evaluateComparator = (comparator, left, right, strict)=>{
524
859
  return left === false && right === false ? false : undefined;
525
860
  }
526
861
  if ((0,_assertions__rspack_import_0.isComparatorExpression)(expression)) {
527
- const left = operand(expression.left, row);
528
- const right = operand(expression.right, row);
862
+ const left = operandValue(expression.left, row);
863
+ const right = operandValue(expression.right, row);
529
864
  if (left === UNRESOLVED || right === UNRESOLVED) {
530
865
  return undefined;
531
866
  }
@@ -563,19 +898,130 @@ const evaluateComparator = (comparator, left, right, strict)=>{
563
898
  };
564
899
 
565
900
 
901
+ },
902
+ 43(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
903
+ __webpack_require__.d(__webpack_exports__, {
904
+ F5: () => (foldConstantCalls)
905
+ });
906
+ /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
907
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
908
+ /* import */ var _types__rspack_import_2 = __webpack_require__(27);
909
+ /* import */ var _utils__rspack_import_1 = __webpack_require__(63);
910
+
911
+
912
+
913
+
914
+ /** Calls fold may compute. Absent means a plugin declines it, so a new call is opt-in. */ const FOLDABLE = new Set([
915
+ "to-lower-case",
916
+ "to-upper-case",
917
+ "length",
918
+ "bit-not",
919
+ "matches",
920
+ "to-string",
921
+ "concat",
922
+ "add",
923
+ "subtract",
924
+ "multiply",
925
+ "divide",
926
+ "modulo",
927
+ "power",
928
+ "bit-and",
929
+ "bit-or",
930
+ "bit-xor",
931
+ "shift-left",
932
+ "shift-right",
933
+ "shift-right-unsigned",
934
+ "coalesce",
935
+ "conditional"
936
+ ]);
937
+ /** `String(value)` on an object is the host's rendering — a Date carries its timezone. */ const COERCES_TO_TEXT = new Set([
938
+ "to-string",
939
+ "concat"
940
+ ]);
941
+ const isFrozenPrimitive = (value)=>value == null || typeof value !== "object";
942
+ const readsAProperty = (expression)=>{
943
+ if ((0,_assertions__rspack_import_0.isPropertyExpression)(expression)) {
944
+ return true;
945
+ }
946
+ return (0,_utils__rspack_import_1/* .childrenOf */.LU)(expression).some(readsAProperty);
947
+ };
948
+ /** A `conditional` holds a condition where every other call holds a value. */ const isConstant = (call)=>{
949
+ if (!FOLDABLE.has(call.call) || !call.arguments.every(_assertions__rspack_import_0.isValueExpression)) {
950
+ return false;
951
+ }
952
+ if (COERCES_TO_TEXT.has(call.call) && [
953
+ call.expression,
954
+ ...call.arguments
955
+ ].some((operand)=>(0,_assertions__rspack_import_0.isValueExpression)(operand) && !isFrozenPrimitive(operand.value))) {
956
+ return false;
957
+ }
958
+ return call.call === "conditional" ? !readsAProperty(call.expression) : (0,_assertions__rspack_import_0.isValueExpression)(call.expression);
959
+ };
960
+ /** Computes every call whose operand and arguments are all literals. Runs after `bindExpression`. */ const foldConstantCalls = (expression)=>{
961
+ if ((0,_assertions__rspack_import_0.isCallExpression)(expression)) {
962
+ const folded = new _types__rspack_import_2/* .CallExpression */.DG({
963
+ call: expression.call,
964
+ expression: foldConstantCalls(expression.expression),
965
+ arguments: expression.arguments.map(foldConstantCalls)
966
+ });
967
+ if (!isConstant(folded)) {
968
+ return folded;
969
+ }
970
+ const value = (0,_evaluate__rspack_import_3/* .operandValue */.Vv)(folded, {});
971
+ return value === _evaluate__rspack_import_3/* .UNRESOLVED */.gm ? folded : new _types__rspack_import_2/* .ValueExpression */.Ko({
972
+ value
973
+ });
974
+ }
975
+ if ((0,_assertions__rspack_import_0.isComparatorExpression)(expression)) {
976
+ return new _types__rspack_import_2/* .ComparatorExpression */.bQ({
977
+ comparator: expression.comparator,
978
+ negated: expression.negated,
979
+ strict: expression.strict,
980
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
981
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
982
+ });
983
+ }
984
+ if ((0,_assertions__rspack_import_0.isOperatorExpression)(expression)) {
985
+ return new _types__rspack_import_2/* .OperatorExpression */.fw({
986
+ operator: expression.operator,
987
+ left: expression.left == null ? undefined : foldConstantCalls(expression.left),
988
+ right: expression.right == null ? undefined : foldConstantCalls(expression.right)
989
+ });
990
+ }
991
+ return expression;
992
+ };
993
+ /** The value a literal operand binds as once the calls on it are computed. Throws if it cannot. */ const foldedOperandValue = (operand, calls)=>{
994
+ if (calls.length === 0) {
995
+ return operand.value;
996
+ }
997
+ // `peelCalls` returns calls innermost first, so the last one evaluates the whole chain.
998
+ const outermost = calls[calls.length - 1];
999
+ const value = readsAProperty(outermost) ? UNRESOLVED : operandValue(outermost, {});
1000
+ if (value === UNRESOLVED) {
1001
+ throw new Error(`'${calls.map((call)=>call.call).join("', '")}' cannot be computed on the literal ` + `'${String(operand.value)}'.`);
1002
+ }
1003
+ return value;
1004
+ };
1005
+
1006
+
566
1007
  },
567
1008
  91(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
568
1009
  __webpack_require__.d(__webpack_exports__, {
569
1010
  MY: () => (toExpression)
570
1011
  });
571
- /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
1012
+ /* import */ var _utilities__rspack_import_4 = __webpack_require__(581);
572
1013
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
573
1014
  /* import */ var _schema__rspack_import_2 = __webpack_require__(537);
1015
+ /* import */ var _evaluate__rspack_import_3 = __webpack_require__(379);
1016
+ /* import */ var _fold__rspack_import_5 = __webpack_require__(43);
574
1017
  /* import */ var _types__rspack_import_1 = __webpack_require__(27);
575
1018
 
576
1019
 
577
1020
 
578
1021
 
1022
+
1023
+
1024
+
579
1025
  // Error message constants
580
1026
  const ERROR_MESSAGES = {
581
1027
  PROPERTY_NOT_FOUND: (path)=>`Error parsing query, could not find PropertyInfo for path: ${path}`,
@@ -621,8 +1067,13 @@ const converters = {
621
1067
  };
622
1068
  // Longest first so multi-character punctuation wins over its prefixes
623
1069
  const MULTI_CHARACTER_PUNCTUATION = [
1070
+ ">>>",
624
1071
  "===",
625
1072
  "!==",
1073
+ "**",
1074
+ "<<",
1075
+ ">>",
1076
+ "??",
626
1077
  "?.",
627
1078
  "&&",
628
1079
  "||",
@@ -659,9 +1110,17 @@ const SINGLE_CHARACTER_PUNCTUATION = new Set([
659
1110
  "?",
660
1111
  ":",
661
1112
  "&",
662
- "|"
1113
+ "|",
1114
+ "^",
1115
+ "~"
663
1116
  ]);
664
- const STRING_ESCAPES = {
1117
+ /**
1118
+ * A lookup table keyed by source text.
1119
+ *
1120
+ * Null-prototype: `TRANSFORM_METHODS["toString"]` otherwise returns `Object.prototype.toString`,
1121
+ * which is truthy, and the parser reads a method it does not support as one it does.
1122
+ */ const sourceKeyed = (entries)=>Object.assign(Object.create(null), entries);
1123
+ const STRING_ESCAPES = sourceKeyed({
665
1124
  "n": "\n",
666
1125
  "r": "\r",
667
1126
  "t": "\t",
@@ -669,6 +1128,24 @@ const STRING_ESCAPES = {
669
1128
  "f": "\f",
670
1129
  "v": "\v",
671
1130
  "0": "\0"
1131
+ });
1132
+ /**
1133
+ * Whether a `/` here opens a regex rather than dividing.
1134
+ *
1135
+ * A regex cannot follow a value. Everything else — the start of the source, an operator, an opening
1136
+ * bracket, a comma — is a position where only a regex makes sense.
1137
+ */ const regexCanStartHere = (tokens)=>{
1138
+ const previous = tokens[tokens.length - 1];
1139
+ if (previous == null) {
1140
+ return true;
1141
+ }
1142
+ if (previous.kind === "number" || previous.kind === "string" || previous.kind === "bigint" || previous.kind === "regex") {
1143
+ return false;
1144
+ }
1145
+ if (previous.kind === "identifier") {
1146
+ return false;
1147
+ }
1148
+ return previous.value !== ")" && previous.value !== "]";
672
1149
  };
673
1150
  const isIdentifierStart = (char)=>/[a-zA-Z_$]/.test(char);
674
1151
  const isIdentifierPart = (char)=>/[a-zA-Z0-9_$]/.test(char);
@@ -718,6 +1195,51 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
718
1195
  i++;
719
1196
  continue;
720
1197
  }
1198
+ /**
1199
+ * A regex literal, told from division by what came before it.
1200
+ *
1201
+ * `/` after a value — a number, string, identifier, `)` or `]` — is division. Anywhere else
1202
+ * it opens a regex. That is the same rule a JavaScript lexer uses, and it is why `x.a / 2`
1203
+ * and `/^a/.test(x.a)` can share a character.
1204
+ */ if (char === "/" && source[i + 1] !== "/" && source[i + 1] !== "*" && regexCanStartHere(tokens)) {
1205
+ let value = "";
1206
+ let inClass = false;
1207
+ let j = i + 1;
1208
+ while(j < source.length){
1209
+ const current = source[j];
1210
+ if (current === "\\") {
1211
+ value += current + (source[j + 1] ?? "");
1212
+ j += 2;
1213
+ continue;
1214
+ }
1215
+ if (current === "[") {
1216
+ inClass = true;
1217
+ } else if (current === "]") {
1218
+ inClass = false;
1219
+ } else if (current === "/" && inClass === false) {
1220
+ break;
1221
+ } else if (current === "\n") {
1222
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
1223
+ }
1224
+ value += current;
1225
+ j++;
1226
+ }
1227
+ if (j >= source.length) {
1228
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated regular expression"));
1229
+ }
1230
+ j++;
1231
+ let flags = "";
1232
+ while(j < source.length && isIdentifierPart(source[j])){
1233
+ flags += source[j];
1234
+ j++;
1235
+ }
1236
+ i = j;
1237
+ tokens.push({
1238
+ kind: "regex",
1239
+ value: `${value}\u0000${flags}`
1240
+ });
1241
+ continue;
1242
+ }
721
1243
  // Comments
722
1244
  if (char === "/" && source[i + 1] === "/") {
723
1245
  while(i < source.length && source[i] !== "\n"){
@@ -737,6 +1259,8 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
737
1259
  if (char === "'" || char === "\"" || char === "`") {
738
1260
  const quote = char;
739
1261
  let value = "";
1262
+ const chunks = [];
1263
+ const expressions = [];
740
1264
  i++;
741
1265
  while(i < source.length && source[i] !== quote){
742
1266
  if (source[i] === "\\") {
@@ -751,8 +1275,43 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
751
1275
  i += 2;
752
1276
  continue;
753
1277
  }
754
- if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
755
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("template literal interpolation"));
1278
+ /**
1279
+ * An interpolation. The literal so far becomes a chunk and the expression source is
1280
+ * kept whole, to be parsed by its own stream — nesting means the inner source can
1281
+ * hold anything, including another template.
1282
+ */ if (quote === "`" && source[i] === "$" && source[i + 1] === "{") {
1283
+ let depth = 1;
1284
+ let expression = "";
1285
+ let at = i + 2;
1286
+ while(at < source.length && depth > 0){
1287
+ const current = source[at];
1288
+ if (current === "{") {
1289
+ depth++;
1290
+ } else if (current === "}") {
1291
+ depth--;
1292
+ if (depth === 0) {
1293
+ break;
1294
+ }
1295
+ } else if (current === "'" || current === '"' || current === "`") {
1296
+ const closing = current;
1297
+ expression += current;
1298
+ at++;
1299
+ while(at < source.length && source[at] !== closing){
1300
+ expression += source[at] === "\\" ? source[at] + (source[at + 1] ?? "") : source[at];
1301
+ at += source[at] === "\\" ? 2 : 1;
1302
+ }
1303
+ }
1304
+ expression += source[at];
1305
+ at++;
1306
+ }
1307
+ if (depth > 0) {
1308
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated template interpolation"));
1309
+ }
1310
+ chunks.push(value);
1311
+ expressions.push(expression);
1312
+ value = "";
1313
+ i = at + 1;
1314
+ continue;
756
1315
  }
757
1316
  value += source[i];
758
1317
  i++;
@@ -761,6 +1320,17 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
761
1320
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("unterminated string literal"));
762
1321
  }
763
1322
  i++; // consume closing quote
1323
+ if (expressions.length > 0) {
1324
+ chunks.push(value);
1325
+ tokens.push({
1326
+ kind: "template",
1327
+ value: JSON.stringify({
1328
+ chunks,
1329
+ expressions
1330
+ })
1331
+ });
1332
+ continue;
1333
+ }
764
1334
  tokens.push({
765
1335
  kind: "string",
766
1336
  value
@@ -804,6 +1374,14 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
804
1374
  }
805
1375
  }
806
1376
  }
1377
+ if (source[i] === "n") {
1378
+ i++;
1379
+ tokens.push({
1380
+ kind: "bigint",
1381
+ value: value.replace(/_/g, "")
1382
+ });
1383
+ continue;
1384
+ }
807
1385
  tokens.push({
808
1386
  kind: "number",
809
1387
  value: value.replace(/_/g, "")
@@ -854,6 +1432,24 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
854
1432
  constructor(tokens){
855
1433
  this.tokens = tokens;
856
1434
  }
1435
+ /** Inserts tokens at the cursor, bracketed so they keep their own precedence. */ splice(tokens) {
1436
+ const bracketed = [
1437
+ {
1438
+ kind: "punctuation",
1439
+ value: "("
1440
+ },
1441
+ ...tokens,
1442
+ {
1443
+ kind: "punctuation",
1444
+ value: ")"
1445
+ }
1446
+ ];
1447
+ this.tokens = [
1448
+ ...this.tokens.slice(0, this.index),
1449
+ ...bracketed,
1450
+ ...this.tokens.slice(this.index)
1451
+ ];
1452
+ }
857
1453
  get isAtEnd() {
858
1454
  return this.index >= this.tokens.length;
859
1455
  }
@@ -868,10 +1464,108 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
868
1464
  this.index++;
869
1465
  return token;
870
1466
  }
1467
+ /** The tokens of one statement's value, through the `;` or block end that closes it. */ takeStatementTokens() {
1468
+ const tokens = [];
1469
+ let depth = 0;
1470
+ while(!this.isAtEnd){
1471
+ const token = this.peek();
1472
+ if (token.kind === "punctuation") {
1473
+ if (token.value === "(" || token.value === "[" || token.value === "{") {
1474
+ depth++;
1475
+ } else if (token.value === ")" || token.value === "]" || token.value === "}") {
1476
+ if (depth === 0) {
1477
+ break;
1478
+ }
1479
+ depth--;
1480
+ } else if (token.value === ";" && depth === 0) {
1481
+ this.next();
1482
+ break;
1483
+ }
1484
+ }
1485
+ tokens.push(this.next());
1486
+ }
1487
+ if (tokens.length === 0) {
1488
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a declaration with no value"));
1489
+ }
1490
+ return tokens;
1491
+ }
871
1492
  isPunctuation(value, offset = 0) {
872
1493
  const token = this.peek(offset);
873
1494
  return token != null && token.kind === "punctuation" && token.value === value;
874
1495
  }
1496
+ /**
1497
+ * Whether the group starting here holds a value rather than a condition.
1498
+ *
1499
+ * `(a && b)` is a boolean sub-expression; `(x.name ?? '') === 'ada'` and `(x.name).length` are
1500
+ * values. Only the token after the matching bracket tells them apart, so the decision is made by
1501
+ * looking ahead rather than by parsing one way and catching the failure — a rewind on exception
1502
+ * would swallow a genuine syntax error inside the group and report it as something else.
1503
+ */ groupIsValue() {
1504
+ let depth = 0;
1505
+ let at = this.index;
1506
+ for(; at < this.tokens.length; at++){
1507
+ const token = this.tokens[at];
1508
+ if (token.kind !== "punctuation") {
1509
+ continue;
1510
+ }
1511
+ if (token.value === "(") {
1512
+ depth++;
1513
+ continue;
1514
+ }
1515
+ if (token.value === ")") {
1516
+ depth--;
1517
+ if (depth === 0) {
1518
+ break;
1519
+ }
1520
+ }
1521
+ }
1522
+ const after = this.tokens[at + 1];
1523
+ if (after == null || after.kind !== "punctuation") {
1524
+ return false;
1525
+ }
1526
+ return COMPARISON_OPERATORS[after.value] != null || after.value === "." || after.value === "?.";
1527
+ }
1528
+ /** Whether a `?` sits at the top level of what is left, so this is a conditional. */ holdsConditional() {
1529
+ let depth = 0;
1530
+ for(let at = this.index; at < this.tokens.length; at++){
1531
+ const token = this.tokens[at];
1532
+ if (token.kind !== "punctuation") {
1533
+ continue;
1534
+ }
1535
+ if (token.value === "(" || token.value === "[") {
1536
+ depth++;
1537
+ } else if (token.value === ")" || token.value === "]") {
1538
+ depth--;
1539
+ } else if (token.value === "?" && depth === 0) {
1540
+ return true;
1541
+ }
1542
+ }
1543
+ return false;
1544
+ }
1545
+ /** Whether the group starting here is `( … ? … : … )` rather than a plain value. */ groupHoldsConditional() {
1546
+ let depth = 0;
1547
+ for(let at = this.index; at < this.tokens.length; at++){
1548
+ const token = this.tokens[at];
1549
+ if (token.kind !== "punctuation") {
1550
+ continue;
1551
+ }
1552
+ if (token.value === "(") {
1553
+ depth++;
1554
+ continue;
1555
+ }
1556
+ if (token.value === ")") {
1557
+ depth--;
1558
+ if (depth === 0) {
1559
+ return false;
1560
+ }
1561
+ continue;
1562
+ }
1563
+ if (token.value === "?" && depth === 1) {
1564
+ return true;
1565
+ }
1566
+ }
1567
+ return false;
1568
+ }
875
1569
  matchPunctuation(value) {
876
1570
  if (this.isPunctuation(value)) {
877
1571
  this.index++;
@@ -885,12 +1579,104 @@ const isHexDigit = (char)=>isDigit(char) || char >= "a" && char <= "f" || char >
885
1579
  }
886
1580
  }
887
1581
  }
888
- const COMPARATOR_METHODS = {
1582
+ /**
1583
+ * Calls JavaScript binds LOOSER than a comparison.
1584
+ *
1585
+ * This grammar reads a comparison's operands as values, which puts these tighter than they belong:
1586
+ * `x.flags & 6 === 2` is `x.flags & (6 === 2)` in JavaScript and would be read here as
1587
+ * `(x.flags & 6) === 2`. The two answer differently, so an ungrouped one is refused rather than
1588
+ * reinterpreted — the filter then runs in memory against the caller's own function, which is right by
1589
+ * construction. Brackets say which was meant, and JavaScript itself makes an unbracketed `??` mix a
1590
+ * syntax error for the same reason.
1591
+ */ const LOOSER_THAN_COMPARISON = [
1592
+ "bit-and",
1593
+ "bit-or",
1594
+ "bit-xor",
1595
+ "coalesce"
1596
+ ];
1597
+ const needsBrackets = (operand)=>operand.kind === "arithmetic" && operand.grouped !== true && LOOSER_THAN_COMPARISON.includes(operand.call);
1598
+ /** JavaScript precedence: `*`, `/`, `%` bind tighter than `+` and `-`. */ const MULTIPLICATIVE_OPERATORS = sourceKeyed({
1599
+ "*": "multiply",
1600
+ "/": "divide",
1601
+ "%": "modulo"
1602
+ });
1603
+ const ADDITIVE_OPERATORS = sourceKeyed({
1604
+ "+": "add",
1605
+ "-": "subtract"
1606
+ });
1607
+ const SHIFT_OPERATORS = sourceKeyed({
1608
+ "<<": "shift-left",
1609
+ ">>": "shift-right",
1610
+ ">>>": "shift-right-unsigned"
1611
+ });
1612
+ const BITWISE_AND_OPERATORS = sourceKeyed({
1613
+ "&": "bit-and"
1614
+ });
1615
+ const BITWISE_XOR_OPERATORS = sourceKeyed({
1616
+ "^": "bit-xor"
1617
+ });
1618
+ const BITWISE_OR_OPERATORS = sourceKeyed({
1619
+ "|": "bit-or"
1620
+ });
1621
+ const COALESCE_OPERATORS = sourceKeyed({
1622
+ "??": "coalesce"
1623
+ });
1624
+ /** Whether a schema property is reachable in here, which decides which side of a comparison it is. */ const containsProperty = (operand)=>{
1625
+ if (operand.kind === "property") {
1626
+ return true;
1627
+ }
1628
+ if (operand.kind === "conditional") {
1629
+ // A comparison always names a schema property, so the condition alone settles it
1630
+ return true;
1631
+ }
1632
+ if (operand.kind === "opaque") {
1633
+ return operand.reads.some(containsProperty);
1634
+ }
1635
+ return operand.kind === "arithmetic" && (containsProperty(operand.left) || containsProperty(operand.right) || operand.extra != null && containsProperty(operand.extra));
1636
+ };
1637
+ const DECLARATION_KEYWORDS = new Set([
1638
+ "const",
1639
+ "let",
1640
+ "var"
1641
+ ]);
1642
+ /** An operand whose value only a row can supply. */ const UNKNOWN_UNTIL_ROW = Symbol("unknown until row");
1643
+ /** The empty argument slot of a unary call. Compared by identity, so a real `undefined` still counts. */ const NO_ARGUMENT = Object.freeze({
1644
+ kind: "value",
1645
+ value: undefined,
1646
+ transformer: null,
1647
+ locale: null
1648
+ });
1649
+ const noArgument = ()=>NO_ARGUMENT;
1650
+ /** A predicate no row satisfies. Never reaches a tree: no expression node means "match nothing". */ const NEVER = "never";
1651
+ const and = (left, right)=>{
1652
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left)) {
1653
+ return right;
1654
+ }
1655
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1656
+ return left;
1657
+ }
1658
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1659
+ operator: "&&",
1660
+ left,
1661
+ right
1662
+ });
1663
+ };
1664
+ const or = (left, right)=>{
1665
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left) || _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1666
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
1667
+ }
1668
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
1669
+ operator: "||",
1670
+ left,
1671
+ right
1672
+ });
1673
+ };
1674
+ const COMPARATOR_METHODS = sourceKeyed({
889
1675
  startsWith: "starts-with",
890
1676
  endsWith: "ends-with",
891
1677
  includes: "includes"
892
- };
893
- const TRANSFORM_METHODS = {
1678
+ });
1679
+ const TRANSFORM_METHODS = sourceKeyed({
894
1680
  toLowerCase: {
895
1681
  transformer: "to-lower-case",
896
1682
  locale: null
@@ -907,8 +1693,8 @@ const TRANSFORM_METHODS = {
907
1693
  transformer: "to-upper-case",
908
1694
  locale: "en-US"
909
1695
  }
910
- };
911
- const COMPARISON_OPERATORS = {
1696
+ });
1697
+ const COMPARISON_OPERATORS = sourceKeyed({
912
1698
  "==": {
913
1699
  comparator: "equals",
914
1700
  negated: false,
@@ -949,7 +1735,7 @@ const COMPARISON_OPERATORS = {
949
1735
  negated: false,
950
1736
  strict: false
951
1737
  }
952
- };
1738
+ });
953
1739
  const SWAPPED_COMPARATORS = {
954
1740
  "equals": "equals",
955
1741
  "greater-than": "less-than",
@@ -1020,16 +1806,21 @@ const resolveParamPath = (paramsName, path, data)=>{
1020
1806
  */ class ExpressionParser {
1021
1807
  schema;
1022
1808
  stream;
1023
- entityName;
1809
+ scope;
1024
1810
  paramsName;
1025
1811
  params;
1812
+ /**
1813
+ * Whether this parses a value selector rather than a filter, and so reads a call it has no node for
1814
+ * as an `OpaqueOperand` instead of refusing it.
1815
+ */ readsValues;
1026
1816
  /** Set when a param value shaped the tree itself (e.g. x[p.name]) — such templates cannot be cached. */ structurallyDependsOnParams = false;
1027
- constructor(schema, stream, entityName, paramsName, params){
1817
+ constructor(schema, stream, scope, paramsName, params, readsValues = false){
1028
1818
  this.schema = schema;
1029
1819
  this.stream = stream;
1030
- this.entityName = entityName;
1820
+ this.scope = scope;
1031
1821
  this.paramsName = paramsName;
1032
1822
  this.params = params;
1823
+ this.readsValues = readsValues;
1033
1824
  }
1034
1825
  parse() {
1035
1826
  const expression = this.parseOr();
@@ -1038,24 +1829,263 @@ const resolveParamPath = (paramsName, path, data)=>{
1038
1829
  }
1039
1830
  return expression;
1040
1831
  }
1041
- // || binds loosest, so it sits at the root of the parse
1042
- parseOr() {
1043
- let left = this.parseAnd();
1044
- while(this.stream.matchPunctuation("||")){
1045
- const right = this.parseAnd();
1046
- // A tautology (`true`) absorbs the whole disjunction
1047
- if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left) || _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
1048
- left = _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
1049
- continue;
1050
- }
1051
- left = new _types__rspack_import_1/* .OperatorExpression */.fw({
1052
- operator: "||",
1053
- left,
1054
- right
1055
- });
1832
+ parseBody() {
1833
+ if (!this.stream.isPunctuation("{")) {
1834
+ return this.parse();
1056
1835
  }
1057
- return left;
1058
- }
1836
+ const answer = this.parseBlock();
1837
+ if (!this.stream.isAtEnd) {
1838
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`unexpected token '${this.stream.peek()?.value}'`));
1839
+ }
1840
+ if (answer === NEVER) {
1841
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a predicate no row can satisfy"));
1842
+ }
1843
+ return answer;
1844
+ }
1845
+ /**
1846
+ * What a value selector returns: one value, or the fields of an object literal.
1847
+ *
1848
+ * A block body is read only when it does nothing but return, which is what a transpiler makes of an
1849
+ * arrow function. Anything more is refused, and the caller falls back to running the function.
1850
+ */ parseSelector() {
1851
+ const block = this.stream.matchPunctuation("{");
1852
+ if (block) {
1853
+ const keyword = this.stream.next();
1854
+ if (keyword.kind !== "identifier" || keyword.value !== "return") {
1855
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a selector block that does more than return"));
1856
+ }
1857
+ }
1858
+ const selected = this.stream.isPunctuation("{") ? this.parseObjectLiteral() : this.stream.isPunctuation("(") && this.stream.isPunctuation("{", 1) ? this.parseBracketedObjectLiteral() : this.parseInterpolation();
1859
+ if (block) {
1860
+ this.stream.matchPunctuation(";");
1861
+ this.stream.expectPunctuation("}");
1862
+ }
1863
+ if (!this.stream.isAtEnd) {
1864
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`unexpected token '${this.stream.peek()?.value}'`));
1865
+ }
1866
+ return selected;
1867
+ }
1868
+ /** `({ … })`, the arrow body that returns an object. */ parseBracketedObjectLiteral() {
1869
+ this.stream.expectPunctuation("(");
1870
+ const fields = this.parseObjectLiteral();
1871
+ this.stream.expectPunctuation(")");
1872
+ return fields;
1873
+ }
1874
+ /**
1875
+ * The fields of an object literal, each one value.
1876
+ *
1877
+ * A field's value is read without a top-level conditional, which needs brackets here: the look-ahead
1878
+ * for a `?` does not stop at the comma that ends the field.
1879
+ */ parseObjectLiteral() {
1880
+ this.stream.expectPunctuation("{");
1881
+ const fields = [];
1882
+ while(!this.stream.matchPunctuation("}")){
1883
+ const key = this.stream.next();
1884
+ if (key.kind !== "identifier" && key.kind !== "string") {
1885
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the object key '${key.value}'`));
1886
+ }
1887
+ fields.push({
1888
+ name: key.value,
1889
+ operand: this.stream.matchPunctuation(":") ? this.parseValue() : this.parseShorthand(key)
1890
+ });
1891
+ if (!this.stream.matchPunctuation(",")) {
1892
+ this.stream.expectPunctuation("}");
1893
+ break;
1894
+ }
1895
+ }
1896
+ return fields;
1897
+ }
1898
+ /** `{ name }`, which reads what the parameter list bound `name` to. */ parseShorthand(key) {
1899
+ const binding = key.kind === "identifier" ? this.scope.get(key.value) : undefined;
1900
+ if (binding == null || binding.kind === "inlined") {
1901
+ throw new Error(ERROR_MESSAGES.VARIABLE_VALUE(key.value));
1902
+ }
1903
+ return this.parseChain({
1904
+ kind: binding.kind,
1905
+ path: [
1906
+ ...binding.path
1907
+ ]
1908
+ });
1909
+ }
1910
+ /** The expression a `{ … }` block answers with. */ parseBlock() {
1911
+ this.stream.expectPunctuation("{");
1912
+ const answer = this.parseStatements();
1913
+ this.stream.expectPunctuation("}");
1914
+ return answer;
1915
+ }
1916
+ /** Statements up to the one that returns. What follows a `return` is never read, as in JavaScript. */ parseStatements() {
1917
+ if (this.stream.isPunctuation("}") || this.stream.isAtEnd) {
1918
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a block body that returns nothing"));
1919
+ }
1920
+ const keyword = this.stream.peek();
1921
+ if (keyword == null || keyword.kind !== "identifier") {
1922
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`a statement starting '${keyword?.value}'`));
1923
+ }
1924
+ if (DECLARATION_KEYWORDS.has(keyword.value)) {
1925
+ this.declare();
1926
+ return this.parseStatements();
1927
+ }
1928
+ if (keyword.value === "return") {
1929
+ this.stream.next();
1930
+ const answer = this.parseReturnedCondition();
1931
+ this.stream.matchPunctuation(";");
1932
+ return answer;
1933
+ }
1934
+ if (keyword.value === "if") {
1935
+ return this.parseIfStatement();
1936
+ }
1937
+ if (keyword.value === "switch") {
1938
+ return this.parseSwitchStatement();
1939
+ }
1940
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the statement '${keyword.value}'`));
1941
+ }
1942
+ /**
1943
+ * Binds a `const`/`let`/`var` name to the tokens of its initializer — tokens rather than a parsed
1944
+ * expression, so the name works as an operand, an argument, or a call receiver alike.
1945
+ */ declare() {
1946
+ this.stream.next();
1947
+ const name = this.stream.next();
1948
+ if (name.kind !== "identifier") {
1949
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`the declaration '${name.value}'`));
1950
+ }
1951
+ this.stream.expectPunctuation("=");
1952
+ this.scope.set(name.value, {
1953
+ kind: "inlined",
1954
+ tokens: this.stream.takeStatementTokens()
1955
+ });
1956
+ }
1957
+ /** `return false` on its own, which no row satisfies, and every other returned condition. */ parseReturnedCondition() {
1958
+ const next = this.stream.peek();
1959
+ const after = this.stream.peek(1);
1960
+ const endsHere = after == null || after.kind === "punctuation" && (after.value === ";" || after.value === "}");
1961
+ if (next != null && next.kind === "identifier" && next.value === "false" && endsHere) {
1962
+ this.stream.next();
1963
+ return NEVER;
1964
+ }
1965
+ return this.parseOr();
1966
+ }
1967
+ parseIfStatement() {
1968
+ this.stream.next();
1969
+ this.stream.expectPunctuation("(");
1970
+ const condition = this.parseOr();
1971
+ this.stream.expectPunctuation(")");
1972
+ const whenTrue = this.parseBranch();
1973
+ if (this.stream.peek()?.value === "else") {
1974
+ this.stream.next();
1975
+ return this.either(condition, whenTrue, this.parseBranch());
1976
+ }
1977
+ // Without an `else`, the statements after the `if` are the other branch
1978
+ return this.either(condition, whenTrue, this.parseStatements());
1979
+ }
1980
+ /** One arm of an `if`: a block, or a single statement. */ parseBranch() {
1981
+ return this.stream.isPunctuation("{") ? this.parseBlock() : this.parseStatements();
1982
+ }
1983
+ /** A `switch` over one subject, as the disjunction of its cases. */ parseSwitchStatement() {
1984
+ this.stream.next();
1985
+ this.stream.expectPunctuation("(");
1986
+ const subject = this.parseValue();
1987
+ this.stream.expectPunctuation(")");
1988
+ this.stream.expectPunctuation("{");
1989
+ let matching = null;
1990
+ let pending = [];
1991
+ let everyLabel = [];
1992
+ let byDefault = null;
1993
+ let anyCaseBroke = false;
1994
+ while(!this.stream.matchPunctuation("}")){
1995
+ const label = this.stream.next();
1996
+ if (label.kind !== "identifier" || label.value !== "case" && label.value !== "default") {
1997
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'${label.value}' inside a switch`));
1998
+ }
1999
+ if (label.value === "case") {
2000
+ const test = this.buildComparison(subject, COMPARISON_OPERATORS["==="], this.parseValue());
2001
+ pending.push(test);
2002
+ everyLabel.push(test);
2003
+ }
2004
+ this.stream.expectPunctuation(":");
2005
+ // `case 'a':` with no body of its own runs the next case's body
2006
+ if (this.stream.peek()?.value === "case" || this.stream.peek()?.value === "default") {
2007
+ continue;
2008
+ }
2009
+ if (this.stream.peek()?.value === "break") {
2010
+ this.stream.next();
2011
+ this.stream.matchPunctuation(";");
2012
+ anyCaseBroke = true;
2013
+ pending = [];
2014
+ continue;
2015
+ }
2016
+ const body = this.parseCaseBody();
2017
+ if (label.value === "default") {
2018
+ byDefault = body === NEVER ? null : body;
2019
+ continue;
2020
+ }
2021
+ if (body !== NEVER && pending.length > 0) {
2022
+ const reached = pending.reduce((left, right)=>or(left, right));
2023
+ const term = _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(body) ? reached : and(reached, body);
2024
+ matching = matching == null ? term : or(matching, term);
2025
+ }
2026
+ pending = [];
2027
+ }
2028
+ // Falling out of the switch continues after it, so the statements there are the default too
2029
+ const afterSwitch = byDefault == null && !this.stream.isPunctuation("}") && !this.stream.isAtEnd ? this.parseStatements() : NEVER;
2030
+ if (afterSwitch !== NEVER) {
2031
+ // A `break` also continues after the switch, so its case would take that answer rather
2032
+ // than none — a distinction this rewrite cannot carry
2033
+ if (anyCaseBroke) {
2034
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a switch that breaks and then falls into more statements"));
2035
+ }
2036
+ byDefault = afterSwitch;
2037
+ }
2038
+ // A `default` runs only when every case failed, wherever it was written
2039
+ if (byDefault != null) {
2040
+ const noCaseMatched = everyLabel.length === 0 ? byDefault : and(this.negateExpression(everyLabel.reduce((left, right)=>or(left, right))), byDefault);
2041
+ matching = matching == null ? noCaseMatched : or(matching, noCaseMatched);
2042
+ }
2043
+ return matching ?? NEVER;
2044
+ }
2045
+ /** One case body, and the `break` that may follow its `return`. */ parseCaseBody() {
2046
+ const answer = this.parseStatements();
2047
+ if (this.stream.peek()?.value === "break") {
2048
+ this.stream.next();
2049
+ this.stream.matchPunctuation(";");
2050
+ }
2051
+ return answer;
2052
+ }
2053
+ /**
2054
+ * The predicate an `if`/`else` answers: `(condition && whenTrue) || (!condition && whenFalse)`,
2055
+ * with each case below that form after a constant branch cancels out.
2056
+ */ either(condition, whenTrue, whenFalse) {
2057
+ if (whenTrue === NEVER) {
2058
+ return whenFalse === NEVER ? NEVER : and(this.negateExpression(condition), whenFalse);
2059
+ }
2060
+ if (whenFalse === NEVER) {
2061
+ return and(condition, whenTrue);
2062
+ }
2063
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenTrue)) {
2064
+ return or(condition, whenFalse);
2065
+ }
2066
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(whenFalse)) {
2067
+ return or(this.negateExpression(condition), whenTrue);
2068
+ }
2069
+ return or(and(condition, whenTrue), and(this.negateExpression(condition), whenFalse));
2070
+ }
2071
+ // || binds loosest, so it sits at the root of the parse
2072
+ parseOr() {
2073
+ let left = this.parseAnd();
2074
+ while(this.stream.matchPunctuation("||")){
2075
+ const right = this.parseAnd();
2076
+ // A tautology (`true`) absorbs the whole disjunction
2077
+ if (_types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(left) || _types__rspack_import_1/* .Expression.isEmpty */.r4.isEmpty(right)) {
2078
+ left = _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
2079
+ continue;
2080
+ }
2081
+ left = new _types__rspack_import_1/* .OperatorExpression */.fw({
2082
+ operator: "||",
2083
+ left,
2084
+ right
2085
+ });
2086
+ }
2087
+ return left;
2088
+ }
1059
2089
  parseAnd() {
1060
2090
  let left = this.parseUnary();
1061
2091
  while(this.stream.matchPunctuation("&&")){
@@ -1085,10 +2115,18 @@ const resolveParamPath = (paramsName, path, data)=>{
1085
2115
  /**
1086
2116
  * Applies `!` to an already-parsed expression: comparators flip their
1087
2117
  * negated flag, compound expressions distribute via De Morgan's laws.
2118
+ *
2119
+ * Builds a new tree rather than flipping the flag in place, because an `if` uses its condition
2120
+ * twice — once negated — and a shared node would carry the flip into both branches.
1088
2121
  */ negateExpression(expression) {
1089
2122
  if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
1090
- expression.negated = !expression.negated;
1091
- return expression;
2123
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2124
+ comparator: expression.comparator,
2125
+ negated: !expression.negated,
2126
+ strict: expression.strict,
2127
+ left: expression.left,
2128
+ right: expression.right
2129
+ });
1092
2130
  }
1093
2131
  if (expression instanceof _types__rspack_import_1/* .OperatorExpression */.fw && expression.left != null && expression.right != null) {
1094
2132
  return new _types__rspack_import_1/* .OperatorExpression */.fw({
@@ -1100,25 +2138,125 @@ const resolveParamPath = (paramsName, path, data)=>{
1100
2138
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("'!' on this expression"));
1101
2139
  }
1102
2140
  parseComparison() {
1103
- // Parenthesized group
1104
- if (this.stream.matchPunctuation("(")) {
2141
+ /**
2142
+ * A parenthesised group is either a boolean sub-expression or a VALUE — `(a && b)` against
2143
+ * `(x.name ?? '') === 'ada'` — and which one it is is only known at the closing bracket, by
2144
+ * what follows. So the boolean reading is tried first and rewound if a comparator turns up.
2145
+ */ if (this.stream.isPunctuation("(") && this.stream.groupIsValue() === false) {
2146
+ this.stream.next();
1105
2147
  const expression = this.parseOr();
1106
2148
  this.stream.expectPunctuation(")");
1107
- const trailing = this.stream.peek();
1108
- if (trailing != null && trailing.kind === "punctuation" && COMPARISON_OPERATORS[trailing.value] != null) {
1109
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison against a parenthesized expression"));
1110
- }
1111
2149
  return expression;
1112
2150
  }
1113
- const left = this.parseOperand();
2151
+ const left = this.parseValue();
1114
2152
  const operatorToken = this.stream.peek();
1115
2153
  if (operatorToken != null && operatorToken.kind === "punctuation" && COMPARISON_OPERATORS[operatorToken.value] != null) {
1116
2154
  this.stream.next();
1117
- const right = this.parseOperand();
2155
+ const right = this.parseValue();
1118
2156
  return this.buildComparison(left, COMPARISON_OPERATORS[operatorToken.value], right);
1119
2157
  }
1120
2158
  return this.buildStandalone(left);
1121
2159
  }
2160
+ /**
2161
+ * A value, at JavaScript's precedence.
2162
+ *
2163
+ * Lowest first: the conditional operator, then nullish coalescing, then the bitwise levels, then
2164
+ * the shifts, then the arithmetic. Comparison sits between the shifts and the bitwise levels in
2165
+ * JavaScript, but a comparison is a boolean and is handled by `parseComparison` above, so this
2166
+ * chain skips it — a bitwise operand here is always a value.
2167
+ */ /**
2168
+ * An operand from its own source, sharing this parser's schema and parameter names.
2169
+ *
2170
+ * A structural dependence found inside propagates outward: the template it belongs to cannot be
2171
+ * cached either.
2172
+ */ parseNested(source) {
2173
+ const nested = new ExpressionParser(this.schema, new TokenStream(tokenize(source)), this.scope, this.paramsName, this.params, this.readsValues);
2174
+ const operand = nested.parseInterpolation();
2175
+ // Leftover tokens mean the interpolation held something this reads only part of. Silently
2176
+ // keeping the part it understood is the worst outcome available: `${x.age > 5 ? "a" : "b"}`
2177
+ // would become `x.age`, and the filter would answer a question nobody asked.
2178
+ if (nested.stream.isAtEnd === false) {
2179
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("an interpolation this parser reads only part of"));
2180
+ }
2181
+ if (nested.structurallyDependsOnParams === true) {
2182
+ this.structurallyDependsOnParams = true;
2183
+ }
2184
+ return operand;
2185
+ }
2186
+ /**
2187
+ * The whole of one `${…}`.
2188
+ *
2189
+ * A conditional is read here rather than in `parseValue`, because an interpolation is the one
2190
+ * place a conditional appears without brackets around it.
2191
+ */ parseInterpolation() {
2192
+ if (this.stream.holdsConditional()) {
2193
+ const condition = this.parseOr();
2194
+ this.stream.expectPunctuation("?");
2195
+ const whenTrue = this.parseValue();
2196
+ this.stream.expectPunctuation(":");
2197
+ const whenFalse = this.parseValue();
2198
+ return {
2199
+ kind: "conditional",
2200
+ condition,
2201
+ whenTrue,
2202
+ whenFalse
2203
+ };
2204
+ }
2205
+ return this.parseValue();
2206
+ }
2207
+ parseValue() {
2208
+ return this.parseCoalesce();
2209
+ }
2210
+ parseCoalesce() {
2211
+ return this.parseBinary(COALESCE_OPERATORS, ()=>this.parseBitwiseOr());
2212
+ }
2213
+ parseBitwiseOr() {
2214
+ return this.parseBinary(BITWISE_OR_OPERATORS, ()=>this.parseBitwiseXor());
2215
+ }
2216
+ parseBitwiseXor() {
2217
+ return this.parseBinary(BITWISE_XOR_OPERATORS, ()=>this.parseBitwiseAnd());
2218
+ }
2219
+ parseBitwiseAnd() {
2220
+ return this.parseBinary(BITWISE_AND_OPERATORS, ()=>this.parseShift());
2221
+ }
2222
+ parseShift() {
2223
+ return this.parseBinary(SHIFT_OPERATORS, ()=>this.parseAdditive());
2224
+ }
2225
+ parseAdditive() {
2226
+ return this.parseBinary(ADDITIVE_OPERATORS, ()=>this.parseMultiplicative());
2227
+ }
2228
+ parseMultiplicative() {
2229
+ return this.parseBinary(MULTIPLICATIVE_OPERATORS, ()=>this.parseExponent());
2230
+ }
2231
+ /** `**` is RIGHT-associative: `2 ** 3 ** 2` is 2 ** 9, not 8 ** 2. */ parseExponent() {
2232
+ const left = this.parseOperand();
2233
+ if (this.stream.isPunctuation("**") === false) {
2234
+ return left;
2235
+ }
2236
+ this.stream.next();
2237
+ return {
2238
+ kind: "arithmetic",
2239
+ call: "power",
2240
+ left,
2241
+ right: this.parseExponent()
2242
+ };
2243
+ }
2244
+ /** Left-associative, so `a - b - c` is `(a - b) - c` rather than `a - (b - c)`. */ parseBinary(operators, next) {
2245
+ let left = next();
2246
+ for(;;){
2247
+ const token = this.stream.peek();
2248
+ if (token == null || token.kind !== "punctuation" || operators[token.value] == null) {
2249
+ return left;
2250
+ }
2251
+ this.stream.next();
2252
+ left = {
2253
+ kind: "arithmetic",
2254
+ call: operators[token.value],
2255
+ left,
2256
+ right: next()
2257
+ };
2258
+ }
2259
+ }
1122
2260
  parseOperand() {
1123
2261
  const token = this.stream.peek();
1124
2262
  if (token == null) {
@@ -1142,6 +2280,131 @@ const resolveParamPath = (paramsName, path, data)=>{
1142
2280
  locale: null
1143
2281
  };
1144
2282
  }
2283
+ // A parenthesised VALUE — `(x.price & 1)`, `(x.name ?? '')`. The boolean reading of a group
2284
+ // is handled in parseComparison; by the time an operand sees one it is arithmetic.
2285
+ if (token.kind === "punctuation" && token.value === "(") {
2286
+ const conditional = this.stream.groupHoldsConditional();
2287
+ this.stream.next();
2288
+ if (conditional === true) {
2289
+ const condition = this.parseOr();
2290
+ this.stream.expectPunctuation("?");
2291
+ const whenTrue = this.parseValue();
2292
+ this.stream.expectPunctuation(":");
2293
+ const whenFalse = this.parseValue();
2294
+ this.stream.expectPunctuation(")");
2295
+ return {
2296
+ kind: "conditional",
2297
+ condition,
2298
+ whenTrue,
2299
+ whenFalse
2300
+ };
2301
+ }
2302
+ const inner = this.parseValue();
2303
+ this.stream.expectPunctuation(")");
2304
+ const grouped = inner.kind === "arithmetic" ? {
2305
+ ...inner,
2306
+ grouped: true
2307
+ } : inner;
2308
+ return this.withGroupCall(grouped);
2309
+ }
2310
+ /**
2311
+ * A template with interpolation, folded into `concat`.
2312
+ *
2313
+ * Each `${…}` was kept as source by the tokenizer and is parsed by its own stream, so it can
2314
+ * hold anything an operand can — a property, a param, arithmetic, another template. Empty
2315
+ * chunks are dropped: `${a}${b}` is two operands, not two operands and three empty strings.
2316
+ */ if (token.kind === "template") {
2317
+ this.stream.next();
2318
+ const { chunks, expressions } = JSON.parse(token.value);
2319
+ const pieces = [];
2320
+ for(let at = 0; at < chunks.length; at++){
2321
+ if (chunks[at].length > 0) {
2322
+ pieces.push({
2323
+ kind: "value",
2324
+ value: chunks[at],
2325
+ transformer: null,
2326
+ locale: null
2327
+ });
2328
+ }
2329
+ if (at < expressions.length) {
2330
+ pieces.push(this.parseNested(expressions[at]));
2331
+ }
2332
+ }
2333
+ if (pieces.length === 0) {
2334
+ return {
2335
+ kind: "value",
2336
+ value: "",
2337
+ transformer: null,
2338
+ locale: null
2339
+ };
2340
+ }
2341
+ // One piece and no chunk means no concat to do the coercion, so the conversion has to be
2342
+ // explicit: `` `${x.age}` `` is the STRING "9", not the number 9.
2343
+ if (pieces.length === 1) {
2344
+ const only = pieces[0];
2345
+ const alreadyText = only.kind === "value" && typeof only.value === "string";
2346
+ return alreadyText ? only : {
2347
+ kind: "arithmetic",
2348
+ call: "to-string",
2349
+ left: only,
2350
+ right: noArgument()
2351
+ };
2352
+ }
2353
+ return pieces.reduce((left, right)=>({
2354
+ kind: "arithmetic",
2355
+ call: "concat",
2356
+ left,
2357
+ right
2358
+ }));
2359
+ }
2360
+ if (token.kind === "bigint") {
2361
+ this.stream.next();
2362
+ return {
2363
+ kind: "value",
2364
+ value: BigInt(token.value),
2365
+ transformer: null,
2366
+ locale: null
2367
+ };
2368
+ }
2369
+ if (token.kind === "regex") {
2370
+ this.stream.next();
2371
+ const [source, flags] = token.value.split("\u0000");
2372
+ const pattern = {
2373
+ kind: "value",
2374
+ value: new RegExp(source, flags),
2375
+ transformer: null,
2376
+ locale: null
2377
+ };
2378
+ // `/^a/.test(x.name)` — the pattern is the literal, the subject is the argument, and the
2379
+ // tree puts them the other way round: the property is what the call applies to.
2380
+ if (this.stream.isPunctuation(".")) {
2381
+ const method = this.stream.peek(1);
2382
+ if (method != null && method.kind === "identifier" && method.value === "test") {
2383
+ this.stream.next();
2384
+ this.stream.next();
2385
+ this.stream.expectPunctuation("(");
2386
+ const subject = this.parseValue();
2387
+ this.stream.expectPunctuation(")");
2388
+ return {
2389
+ kind: "arithmetic",
2390
+ call: "matches",
2391
+ left: subject,
2392
+ right: pattern
2393
+ };
2394
+ }
2395
+ }
2396
+ return pattern;
2397
+ }
2398
+ if (token.kind === "punctuation" && token.value === "~") {
2399
+ this.stream.next();
2400
+ // Unary, so the tree carries the operand and no argument
2401
+ return {
2402
+ kind: "arithmetic",
2403
+ call: "bit-not",
2404
+ left: this.parseOperand(),
2405
+ right: noArgument()
2406
+ };
2407
+ }
1145
2408
  if (token.kind === "punctuation" && token.value === "-") {
1146
2409
  this.stream.next();
1147
2410
  const numberToken = this.stream.next();
@@ -1199,6 +2462,9 @@ const resolveParamPath = (paramsName, path, data)=>{
1199
2462
  if (argument.kind === "method-call") {
1200
2463
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("nested method call inside .includes()"));
1201
2464
  }
2465
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2466
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic inside .includes()"));
2467
+ }
1202
2468
  return {
1203
2469
  kind: "method-call",
1204
2470
  target: array,
@@ -1244,16 +2510,17 @@ const resolveParamPath = (paramsName, path, data)=>{
1244
2510
  locale: null
1245
2511
  };
1246
2512
  }
1247
- if (root === this.entityName) {
1248
- return this.parseChain({
1249
- kind: "property",
1250
- root
1251
- });
1252
- }
1253
- if (this.paramsName != null && root === this.paramsName) {
2513
+ const binding = this.scope.get(root);
2514
+ if (binding != null) {
2515
+ if (binding.kind === "inlined") {
2516
+ this.stream.splice(binding.tokens);
2517
+ return this.parseOperand();
2518
+ }
1254
2519
  return this.parseChain({
1255
- kind: "param",
1256
- root
2520
+ kind: binding.kind,
2521
+ path: [
2522
+ ...binding.path
2523
+ ]
1257
2524
  });
1258
2525
  }
1259
2526
  // A bare variable from the outer scope — its value cannot be derived from source text
@@ -1263,7 +2530,7 @@ const resolveParamPath = (paramsName, path, data)=>{
1263
2530
  * Parses the segments after an entity/params root: dot access, bracket
1264
2531
  * access, transform methods and comparator methods.
1265
2532
  */ parseChain(options) {
1266
- const path = [];
2533
+ const path = options.path;
1267
2534
  let transformer = null;
1268
2535
  let locale = null;
1269
2536
  while(true){
@@ -1289,6 +2556,9 @@ const resolveParamPath = (paramsName, path, data)=>{
1289
2556
  if (argument.kind === "method-call") {
1290
2557
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`nested method call inside .${method}()`));
1291
2558
  }
2559
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2560
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`arithmetic inside .${method}()`));
2561
+ }
1292
2562
  return {
1293
2563
  kind: "method-call",
1294
2564
  target: this.resolveChain(options.kind, path, transformer, locale),
@@ -1296,9 +2566,20 @@ const resolveParamPath = (paramsName, path, data)=>{
1296
2566
  argument
1297
2567
  };
1298
2568
  }
2569
+ if (this.readsValues) {
2570
+ return this.withGroupCall(this.opaqueCall(this.resolveChain(options.kind, path, transformer, locale)));
2571
+ }
1299
2572
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`method '.${method}()'`));
1300
2573
  }
1301
2574
  if (transformer != null) {
2575
+ if (this.readsValues) {
2576
+ return this.withGroupCall({
2577
+ kind: "opaque",
2578
+ reads: [
2579
+ this.resolveChain(options.kind, path, transformer, locale)
2580
+ ]
2581
+ });
2582
+ }
1302
2583
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("property access after a transform method"));
1303
2584
  }
1304
2585
  path.push(segment.value);
@@ -1325,12 +2606,15 @@ const resolveParamPath = (paramsName, path, data)=>{
1325
2606
  // docs/mutation-backlog.md) — every mutation of this four-conjunct guard reroutes
1326
2607
  // bracket access between two paths that both collapse to NOT_PARSABLE; the
1327
2608
  // experiment recorded there aimed 30 tests at this line and killed none.
1328
- if (kind === "property" && token.kind === "identifier" && this.paramsName != null && token.value === this.paramsName) {
1329
- const paramPath = [];
2609
+ const binding = token.kind === "identifier" ? this.scope.get(token.value) : undefined;
2610
+ if (kind === "property" && binding != null && binding.kind === "param") {
2611
+ const paramPath = [
2612
+ ...binding.path
2613
+ ];
1330
2614
  while(this.stream.matchPunctuation(".") || this.stream.matchPunctuation("?.")){
1331
2615
  paramPath.push(this.stream.next().value);
1332
2616
  }
1333
- const resolved = resolveParamPath(this.paramsName, paramPath, this.params);
2617
+ const resolved = resolveParamPath(this.paramsName ?? token.value, paramPath, this.params);
1334
2618
  if (typeof resolved !== "string") {
1335
2619
  throw new ParamDependentParseError(ERROR_MESSAGES.PROPERTY_NOT_FOUND(paramPath.join(".")));
1336
2620
  }
@@ -1383,6 +2667,96 @@ const resolveParamPath = (paramsName, path, data)=>{
1383
2667
  locale
1384
2668
  };
1385
2669
  }
2670
+ /**
2671
+ * A call on a parenthesised value: `(x.name).toLowerCase()`, `(x.age + 1).length`. Any operand can
2672
+ * receive one here, unlike a property chain, which carries at most one transform.
2673
+ */ withGroupCall(operand) {
2674
+ let receiver = operand;
2675
+ while(this.stream.isPunctuation(".") || this.stream.isPunctuation("?.")){
2676
+ const segment = this.stream.peek(1);
2677
+ if (segment == null || segment.kind !== "identifier") {
2678
+ break;
2679
+ }
2680
+ if (segment.value === "length" && !this.stream.isPunctuation("(", 2)) {
2681
+ this.stream.next();
2682
+ this.stream.next();
2683
+ receiver = {
2684
+ kind: "arithmetic",
2685
+ call: "length",
2686
+ left: receiver,
2687
+ right: noArgument()
2688
+ };
2689
+ continue;
2690
+ }
2691
+ const transform = TRANSFORM_METHODS[segment.value];
2692
+ if (transform != null) {
2693
+ this.stream.next();
2694
+ this.stream.next();
2695
+ this.stream.expectPunctuation("(");
2696
+ this.stream.expectPunctuation(")");
2697
+ receiver = {
2698
+ kind: "arithmetic",
2699
+ call: transform.transformer,
2700
+ left: receiver,
2701
+ right: transform.locale == null ? noArgument() : {
2702
+ kind: "value",
2703
+ value: transform.locale,
2704
+ transformer: null,
2705
+ locale: null
2706
+ }
2707
+ };
2708
+ continue;
2709
+ }
2710
+ // A comparator method needs a property target, which only an ungrouped chain produces
2711
+ if (COMPARATOR_METHODS[segment.value] != null && receiver.kind === "property") {
2712
+ this.stream.next();
2713
+ this.stream.next();
2714
+ this.stream.expectPunctuation("(");
2715
+ const argument = this.parseOperand();
2716
+ this.stream.expectPunctuation(")");
2717
+ if (argument.kind !== "property" && argument.kind !== "value" && argument.kind !== "param") {
2718
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`'.${segment.value}()' on that argument`));
2719
+ }
2720
+ return {
2721
+ kind: "method-call",
2722
+ target: receiver,
2723
+ method: segment.value,
2724
+ argument
2725
+ };
2726
+ }
2727
+ // Any other member or call of a value, which a selector reads through
2728
+ if (this.readsValues) {
2729
+ this.stream.next();
2730
+ this.stream.next();
2731
+ receiver = this.stream.isPunctuation("(") ? this.opaqueCall(receiver) : {
2732
+ kind: "opaque",
2733
+ reads: [
2734
+ receiver
2735
+ ]
2736
+ };
2737
+ continue;
2738
+ }
2739
+ break;
2740
+ }
2741
+ return receiver;
2742
+ }
2743
+ /** A call with no node of its own, from its `(`, kept for what its receiver and arguments read. */ opaqueCall(receiver) {
2744
+ const reads = [
2745
+ receiver
2746
+ ];
2747
+ this.stream.expectPunctuation("(");
2748
+ while(!this.stream.matchPunctuation(")")){
2749
+ reads.push(this.parseValue());
2750
+ if (!this.stream.matchPunctuation(",")) {
2751
+ this.stream.expectPunctuation(")");
2752
+ break;
2753
+ }
2754
+ }
2755
+ return {
2756
+ kind: "opaque",
2757
+ reads
2758
+ };
2759
+ }
1386
2760
  withValueTransformer(operand) {
1387
2761
  if (this.stream.isPunctuation(".")) {
1388
2762
  const method = this.stream.peek(1);
@@ -1416,12 +2790,26 @@ const resolveParamPath = (paramsName, path, data)=>{
1416
2790
  if (right.kind === "method-call") {
1417
2791
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("method call on the right side of a comparison"));
1418
2792
  }
1419
- if (left.kind === "property" && right.kind === "property") {
1420
- // Casing transformers are only valid with string-matching comparators,
1421
- // which cannot produce a property-to-property comparison
1422
- if (left.transformer === "to-lower-case" || left.transformer === "to-upper-case" || right.transformer === "to-lower-case" || right.transformer === "to-upper-case") {
1423
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
2793
+ // A comparison is a tree a backend renders, and this operand has no node in one
2794
+ if (left.kind === "opaque" || right.kind === "opaque") {
2795
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a call with no expression form inside a comparison"));
2796
+ }
2797
+ if (needsBrackets(left) || needsBrackets(right)) {
2798
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a bitwise or nullish operator compared without brackets, which JavaScript reads the other way round"));
2799
+ }
2800
+ if (left.kind === "arithmetic" || right.kind === "arithmetic" || left.kind === "conditional" || right.kind === "conditional") {
2801
+ if (containsProperty(left) === false && containsProperty(right) === false) {
2802
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic that references no schema property"));
1424
2803
  }
2804
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2805
+ comparator: operator.comparator,
2806
+ negated: operator.negated,
2807
+ strict: operator.strict,
2808
+ left: this.createOperandExpression(left),
2809
+ right: this.createOperandExpression(right)
2810
+ });
2811
+ }
2812
+ if (left.kind === "property" && right.kind === "property") {
1425
2813
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1426
2814
  comparator: operator.comparator,
1427
2815
  negated: operator.negated,
@@ -1430,18 +2818,63 @@ const resolveParamPath = (paramsName, path, data)=>{
1430
2818
  right: this.createPropertyExpression(right)
1431
2819
  });
1432
2820
  }
2821
+ // Only a loose comparison coerces. `===` records `strict` and honouring it is the point.
1433
2822
  if (left.kind === "property" && right.kind !== "property") {
1434
- return this.buildPropertyComparator(left, operator, right, /* applyConverter */ true);
2823
+ return this.buildPropertyComparator(left, operator, right, /* applyConverter */ !operator.strict);
1435
2824
  }
1436
2825
  if (right.kind === "property" && left.kind !== "property") {
1437
2826
  const swapped = {
1438
2827
  ...operator,
1439
2828
  comparator: SWAPPED_COMPARATORS[operator.comparator]
1440
2829
  };
1441
- return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ true);
2830
+ return this.buildPropertyComparator(right, swapped, left, /* applyConverter */ !operator.strict);
2831
+ }
2832
+ const settled = this.settleConstantComparison(left, operator, right);
2833
+ if (settled != null) {
2834
+ return settled;
1442
2835
  }
1443
2836
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("comparison requires a schema property on at least one side"));
1444
2837
  }
2838
+ /**
2839
+ * The answer a comparison of two constants gives, when that answer is `true`. The other answer
2840
+ * excludes every row, which has no expression node.
2841
+ */ settleConstantComparison(left, operator, right) {
2842
+ const leftValue = this.constantOf(left);
2843
+ const rightValue = this.constantOf(right);
2844
+ if (leftValue === UNKNOWN_UNTIL_ROW || rightValue === UNKNOWN_UNTIL_ROW) {
2845
+ return null;
2846
+ }
2847
+ const answer = (0,_evaluate__rspack_import_3/* .evaluate */._3)(new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2848
+ comparator: operator.comparator,
2849
+ negated: operator.negated,
2850
+ strict: operator.strict,
2851
+ left: new _types__rspack_import_1/* .ValueExpression */.Ko({
2852
+ value: leftValue
2853
+ }),
2854
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2855
+ value: rightValue
2856
+ })
2857
+ }), {});
2858
+ if (answer === true) {
2859
+ return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
2860
+ }
2861
+ // Params decided this, so the refusal must not be cached against the source: the same filter
2862
+ // with other params can be a tautology.
2863
+ if (left.kind === "param" || right.kind === "param") {
2864
+ throw new ParamDependentParseError(ERROR_MESSAGES.UNSUPPORTED("a params comparison no row satisfies"));
2865
+ }
2866
+ return null;
2867
+ }
2868
+ /** The value an operand holds already, for the operands that do not depend on a row. */ constantOf(operand) {
2869
+ if (operand.kind === "value" && operand.transformer == null) {
2870
+ return operand.value;
2871
+ }
2872
+ if (operand.kind === "param" && operand.transformer == null) {
2873
+ this.structurallyDependsOnParams = true;
2874
+ return resolveParamPath(this.paramsName ?? "params", operand.path, this.params);
2875
+ }
2876
+ return UNKNOWN_UNTIL_ROW;
2877
+ }
1445
2878
  buildStandalone(operand) {
1446
2879
  if (operand.kind === "method-call") {
1447
2880
  return this.buildMethodComparator(operand);
@@ -1464,6 +2897,21 @@ const resolveParamPath = (paramsName, path, data)=>{
1464
2897
  locale: null
1465
2898
  }, /* applyConverter */ true);
1466
2899
  }
2900
+ // A boolean-valued call standing alone IS the predicate
2901
+ if (operand.kind === "arithmetic" && operand.call === "matches") {
2902
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
2903
+ comparator: "equals",
2904
+ negated: false,
2905
+ strict: false,
2906
+ left: this.createOperandExpression(operand),
2907
+ right: new _types__rspack_import_1/* .ValueExpression */.Ko({
2908
+ value: true
2909
+ })
2910
+ });
2911
+ }
2912
+ if (operand.kind === "arithmetic" || operand.kind === "conditional") {
2913
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic used as a condition rather than compared"));
2914
+ }
1467
2915
  // Constant `true` — a tautology, which parseAnd/parseOr simplify away
1468
2916
  if (operand.kind === "value" && operand.value === true && operand.transformer == null) {
1469
2917
  return _types__rspack_import_1/* .Expression.EMPTY */.r4.EMPTY;
@@ -1516,12 +2964,6 @@ const resolveParamPath = (paramsName, path, data)=>{
1516
2964
  right: this.createValueExpression(value, null, /* applyConverter */ false)
1517
2965
  });
1518
2966
  }
1519
- // Casing transformers on a property are only meaningful with string-matching
1520
- // comparators; on relational comparators the plugins would silently
1521
- // ignore them and return wrong data
1522
- if (property.transformer != null && !isStringMatch) {
1523
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("transform method outside of startsWith/endsWith/includes"));
1524
- }
1525
2967
  return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1526
2968
  comparator: operator.comparator,
1527
2969
  negated: operator.negated,
@@ -1530,31 +2972,63 @@ const resolveParamPath = (paramsName, path, data)=>{
1530
2972
  right: this.createValueExpression(value, property.property, applyConverter)
1531
2973
  });
1532
2974
  }
2975
+ /**
2976
+ * Any operand as an expression.
2977
+ *
2978
+ * Values inside arithmetic take no paired property: the result is a computed number, so the
2979
+ * property's serializer and type converter do not describe it — the same reason `.length` skips
2980
+ * them.
2981
+ */ createOperandExpression(operand) {
2982
+ if (operand.kind === "conditional") {
2983
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2984
+ call: "conditional",
2985
+ expression: operand.condition,
2986
+ arguments: [
2987
+ this.createOperandExpression(operand.whenTrue),
2988
+ this.createOperandExpression(operand.whenFalse)
2989
+ ]
2990
+ });
2991
+ }
2992
+ if (operand.kind === "arithmetic") {
2993
+ return new _types__rspack_import_1/* .CallExpression */.DG({
2994
+ call: operand.call,
2995
+ expression: this.createOperandExpression(operand.left),
2996
+ arguments: operand.right === NO_ARGUMENT ? [] : operand.extra == null ? [
2997
+ this.createOperandExpression(operand.right)
2998
+ ] : [
2999
+ this.createOperandExpression(operand.right),
3000
+ this.createOperandExpression(operand.extra)
3001
+ ]
3002
+ });
3003
+ }
3004
+ if (operand.kind === "property") {
3005
+ return this.createPropertyExpression(operand);
3006
+ }
3007
+ if (operand.kind === "method-call") {
3008
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a method call inside arithmetic"));
3009
+ }
3010
+ if (operand.kind === "opaque") {
3011
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a call with no expression form inside arithmetic"));
3012
+ }
3013
+ return this.createValueExpression(operand, null, /* applyConverter */ false);
3014
+ }
1533
3015
  createPropertyExpression(operand) {
1534
- const expression = new _types__rspack_import_1/* .PropertyExpression */.ep({
3016
+ return asCall(new _types__rspack_import_1/* .PropertyExpression */.ep({
1535
3017
  property: operand.property
1536
- });
1537
- expression.transformer = operand.transformer;
1538
- expression.locale = operand.locale;
1539
- return expression;
3018
+ }), operand.transformer, operand.locale);
1540
3019
  }
1541
3020
  createValueExpression(operand, pairedProperty, applyConverter) {
1542
3021
  if (operand.kind === "param") {
1543
- const expression = new ParamReferenceExpression({
3022
+ return asCall(new ParamReferenceExpression({
1544
3023
  paramPath: operand.path,
1545
3024
  pairedProperty,
1546
3025
  applyConverter
1547
- });
1548
- expression.transformer = operand.transformer;
1549
- expression.locale = operand.locale;
1550
- return expression;
3026
+ }), operand.transformer, operand.locale);
1551
3027
  }
1552
3028
  const expression = new _types__rspack_import_1/* .ValueExpression */.Ko({
1553
3029
  value: resolvePairedValue(operand.value, pairedProperty, applyConverter)
1554
3030
  });
1555
- expression.transformer = operand.transformer;
1556
- expression.locale = operand.locale;
1557
- return expression;
3031
+ return asCall(expression, operand.transformer, operand.locale);
1558
3032
  }
1559
3033
  }
1560
3034
  // #endregion
@@ -1566,46 +3040,128 @@ const resolveParamPath = (paramsName, path, data)=>{
1566
3040
  */ const bindExpression = (expression, paramsName, params)=>{
1567
3041
  if (expression instanceof ParamReferenceExpression) {
1568
3042
  const raw = resolveParamPath(paramsName ?? "params", expression.paramPath, params);
1569
- const bound = new _types__rspack_import_1/* .ValueExpression */.Ko({
3043
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1570
3044
  value: resolvePairedValue(raw, expression.pairedProperty, expression.applyConverter)
1571
3045
  });
1572
- bound.transformer = expression.transformer;
1573
- bound.locale = expression.locale;
1574
- return bound;
1575
3046
  }
1576
3047
  if (expression instanceof _types__rspack_import_1/* .ValueExpression */.Ko) {
1577
- const clone = new _types__rspack_import_1/* .ValueExpression */.Ko({
3048
+ return new _types__rspack_import_1/* .ValueExpression */.Ko({
1578
3049
  value: expression.value
1579
3050
  });
1580
- clone.transformer = expression.transformer;
1581
- clone.locale = expression.locale;
1582
- return clone;
1583
3051
  }
1584
3052
  if (expression instanceof _types__rspack_import_1/* .PropertyExpression */.ep) {
1585
- const clone = new _types__rspack_import_1/* .PropertyExpression */.ep({
3053
+ return new _types__rspack_import_1/* .PropertyExpression */.ep({
1586
3054
  property: expression.property
1587
3055
  });
1588
- clone.transformer = expression.transformer;
1589
- clone.locale = expression.locale;
1590
- return clone;
1591
3056
  }
1592
- if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
1593
- return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
1594
- comparator: expression.comparator,
1595
- negated: expression.negated,
1596
- strict: expression.strict,
1597
- left: expression.left ? bindExpression(expression.left, paramsName, params) : undefined,
1598
- right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
1599
- });
3057
+ if (expression instanceof _types__rspack_import_1/* .ComparatorExpression */.bQ) {
3058
+ return new _types__rspack_import_1/* .ComparatorExpression */.bQ({
3059
+ comparator: expression.comparator,
3060
+ negated: expression.negated,
3061
+ strict: expression.strict,
3062
+ left: expression.left ? bindExpression(expression.left, paramsName, params) : undefined,
3063
+ right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
3064
+ });
3065
+ }
3066
+ if (expression instanceof _types__rspack_import_1/* .OperatorExpression */.fw) {
3067
+ return new _types__rspack_import_1/* .OperatorExpression */.fw({
3068
+ operator: expression.operator,
3069
+ left: expression.left ? bindExpression(expression.left, paramsName, params) : undefined,
3070
+ right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
3071
+ });
3072
+ }
3073
+ if (expression instanceof _types__rspack_import_1/* .CallExpression */.DG) {
3074
+ return new _types__rspack_import_1/* .CallExpression */.DG({
3075
+ call: expression.call,
3076
+ expression: bindExpression(expression.expression, paramsName, params),
3077
+ arguments: expression.arguments.map((argument)=>bindExpression(argument, paramsName, params))
3078
+ });
3079
+ }
3080
+ return expression;
3081
+ };
3082
+ /**
3083
+ * Wraps an operand in the call a transform method named, if there was one.
3084
+ *
3085
+ * `Transformer` and `Call` share these three names, so the transform IS the call name. A locale
3086
+ * becomes the call's first argument, which is where it belongs — it qualifies the casing, not the
3087
+ * property.
3088
+ */ const asCall = (inner, transformer, locale)=>{
3089
+ if (transformer == null) {
3090
+ return inner;
3091
+ }
3092
+ return new _types__rspack_import_1/* .CallExpression */.DG({
3093
+ call: transformer,
3094
+ expression: inner,
3095
+ arguments: locale == null ? [] : [
3096
+ new _types__rspack_import_1/* .ValueExpression */.Ko({
3097
+ value: locale
3098
+ })
3099
+ ]
3100
+ });
3101
+ };
3102
+ /** Binds every name a destructuring pattern introduces to the path it reads. */ const bindPattern = (stream, kind, path, scope)=>{
3103
+ if (!stream.matchPunctuation("{")) {
3104
+ const name = stream.next();
3105
+ if (name.kind !== "identifier") {
3106
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`parameter '${name.value}'`));
3107
+ }
3108
+ scope.set(name.value, {
3109
+ kind,
3110
+ path
3111
+ });
3112
+ return;
3113
+ }
3114
+ while(!stream.matchPunctuation("}")){
3115
+ const key = stream.next();
3116
+ if (key.kind !== "identifier") {
3117
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED(`destructured key '${key.value}'`));
3118
+ }
3119
+ if (stream.matchPunctuation(":")) {
3120
+ bindPattern(stream, kind, [
3121
+ ...path,
3122
+ key.value
3123
+ ], scope);
3124
+ } else {
3125
+ scope.set(key.value, {
3126
+ kind,
3127
+ path: [
3128
+ ...path,
3129
+ key.value
3130
+ ]
3131
+ });
3132
+ }
3133
+ if (!stream.matchPunctuation(",")) {
3134
+ stream.expectPunctuation("}");
3135
+ return;
3136
+ }
3137
+ }
3138
+ };
3139
+ /** Reads a filter's parameter list — the entity alone, or the `[entity, params]` pair — into a scope. */ const buildScope = (parameterNames, hasParams)=>{
3140
+ const stream = new TokenStream(tokenize(parameterNames));
3141
+ const scope = new Map();
3142
+ if (!stream.matchPunctuation("[")) {
3143
+ bindPattern(stream, "property", [], scope);
3144
+ return {
3145
+ scope,
3146
+ paramsName: null
3147
+ };
3148
+ }
3149
+ bindPattern(stream, "property", [], scope);
3150
+ if (hasParams && stream.matchPunctuation(",") && !stream.isPunctuation("]")) {
3151
+ bindPattern(stream, "param", [], scope);
1600
3152
  }
1601
- if (expression instanceof _types__rspack_import_1/* .OperatorExpression */.fw) {
1602
- return new _types__rspack_import_1/* .OperatorExpression */.fw({
1603
- operator: expression.operator,
1604
- left: expression.left ? bindExpression(expression.left, paramsName, params) : undefined,
1605
- right: expression.right ? bindExpression(expression.right, paramsName, params) : undefined
1606
- });
3153
+ return {
3154
+ scope,
3155
+ paramsName: wholeParamsName(scope)
3156
+ };
3157
+ };
3158
+ /** The name the whole params object was given, when it was not destructured. Error messages only. */ const wholeParamsName = (scope)=>{
3159
+ for (const [name, binding] of scope){
3160
+ if (binding.kind === "param" && binding.path.length === 0) {
3161
+ return name;
3162
+ }
1607
3163
  }
1608
- return expression;
3164
+ return null;
1609
3165
  };
1610
3166
  /**
1611
3167
  * Splits stringified filter source into parameter names and the expression
@@ -1636,33 +3192,12 @@ const resolveParamPath = (paramsName, path, data)=>{
1636
3192
  parameterNames = parameterNames.slice(1, -1).trim();
1637
3193
  }
1638
3194
  }
1639
- let entityName;
1640
- let paramsName = null;
1641
- if (parameterNames.startsWith("[") && parameterNames.endsWith("]")) {
1642
- const destructured = parameterNames.slice(1, -1).split(",").map((w)=>w.trim());
1643
- entityName = destructured[0];
1644
- if (hasParams) {
1645
- paramsName = destructured[1] ?? null;
1646
- }
1647
- } else {
1648
- entityName = parameterNames;
1649
- }
1650
- if (entityName == null || entityName.length === 0) {
3195
+ if (parameterNames.length === 0) {
1651
3196
  throw new Error("Invalid Function");
1652
3197
  }
1653
- // Unwrap a single-return block body: { return <expression>; }
1654
- if (body.startsWith("{")) {
1655
- const inner = body.slice(1, body.lastIndexOf("}")).trim();
1656
- if (!inner.startsWith("return")) {
1657
- throw new Error(ERROR_MESSAGES.UNSUPPORTED("block body without a single return statement"));
1658
- }
1659
- body = inner.slice("return".length).trim();
1660
- if (body.endsWith(";")) {
1661
- body = body.slice(0, -1).trim();
1662
- }
1663
- }
3198
+ const { scope, paramsName } = buildScope(parameterNames, hasParams);
1664
3199
  return {
1665
- entityName,
3200
+ scope,
1666
3201
  paramsName,
1667
3202
  body
1668
3203
  };
@@ -1730,17 +3265,27 @@ const combineExpressions = (...expressions)=>{
1730
3265
  */ const parseFragment = (schema, body, rootName)=>{
1731
3266
  try {
1732
3267
  const stream = new TokenStream(tokenize(body));
1733
- const parser = new ExpressionParser(schema, stream, rootName, null, undefined);
1734
- return parser.parse();
3268
+ const scope = new Map([
3269
+ [
3270
+ rootName,
3271
+ {
3272
+ kind: "property",
3273
+ path: []
3274
+ }
3275
+ ]
3276
+ ]);
3277
+ const parser = new ExpressionParser(schema, stream, scope, null, undefined);
3278
+ return foldConstantCalls(parser.parse());
1735
3279
  } catch {
1736
3280
  // The failure is expected and informative — see above — so it is not logged. A caller that
1737
3281
  // parses one conjunct against two schemas would otherwise warn on every successful split.
1738
3282
  return Expression.NOT_PARSABLE;
1739
3283
  }
1740
3284
  };
3285
+ /** What the parser refused, from a throw that may not be an `Error`. */ const refusalOf = (error)=>error instanceof Error ? error.message : String(error);
1741
3286
  const toExpression = (schema, fn, params)=>{
1742
3287
  const stringifiedFunction = fn.toString();
1743
- const warn = (error)=>_utilities__rspack_import_3/* .logger.warn */.vF.warn("Error parsing expression", {
3288
+ const warn = (error)=>_utilities__rspack_import_4/* .logger.warn */.vF.warn("Error parsing expression", {
1744
3289
  error,
1745
3290
  collectionName: schema.collectionName,
1746
3291
  params,
@@ -1748,16 +3293,17 @@ const toExpression = (schema, fn, params)=>{
1748
3293
  });
1749
3294
  const cached = getCachedTemplate(schema, stringifiedFunction);
1750
3295
  if (cached != null) {
1751
- // A cached failure — the warning was already logged when it was discovered
3296
+ // A cached failure — the warning was already logged when it was discovered. The template
3297
+ // carries what was refused, and `.explain()` is usually called once the cache is warm.
1752
3298
  if (_types__rspack_import_1/* .Expression.isNotParsable */.r4.isNotParsable(cached.template)) {
1753
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3299
+ return cached.template;
1754
3300
  }
1755
3301
  try {
1756
- return bindExpression(cached.template, cached.paramsName, params);
3302
+ return (0,_fold__rspack_import_5/* .foldConstantCalls */.F5)(bindExpression(cached.template, cached.paramsName, params));
1757
3303
  } catch (error) {
1758
3304
  // Binding failures are param-dependent by nature — never cached
1759
3305
  warn(error);
1760
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3306
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1761
3307
  }
1762
3308
  }
1763
3309
  let paramsName = null;
@@ -1766,22 +3312,23 @@ const toExpression = (schema, fn, params)=>{
1766
3312
  try {
1767
3313
  const shape = resolveFunctionShape(stringifiedFunction, params != null);
1768
3314
  const stream = new TokenStream(tokenize(shape.body));
1769
- const parser = new ExpressionParser(schema, stream, shape.entityName, shape.paramsName, params);
3315
+ const parser = new ExpressionParser(schema, stream, shape.scope, shape.paramsName, params);
1770
3316
  paramsName = shape.paramsName;
1771
- template = parser.parse();
3317
+ template = parser.parseBody();
1772
3318
  structurallyDependsOnParams = parser.structurallyDependsOnParams;
1773
3319
  } catch (error) {
1774
3320
  // Cache the failure so a hot query on an unsupported filter doesn't
1775
3321
  // re-parse and re-warn on every execution. Param-dependent failures are
1776
3322
  // exempt: the same source can succeed with different params.
3323
+ const refused = _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
1777
3324
  if (!(error instanceof ParamDependentParseError)) {
1778
3325
  setCachedTemplate(schema, stringifiedFunction, {
1779
- template: _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE,
3326
+ template: refused,
1780
3327
  paramsName: null
1781
3328
  });
1782
3329
  }
1783
3330
  warn(error);
1784
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3331
+ return refused;
1785
3332
  }
1786
3333
  // Templates whose structure was resolved from param values are only
1787
3334
  // valid for this exact params object — parse those fresh every time
@@ -1792,17 +3339,116 @@ const toExpression = (schema, fn, params)=>{
1792
3339
  });
1793
3340
  }
1794
3341
  try {
1795
- return bindExpression(template, paramsName, params);
3342
+ return (0,_fold__rspack_import_5/* .foldConstantCalls */.F5)(bindExpression(template, paramsName, params));
1796
3343
  } catch (error) {
1797
3344
  warn(error);
1798
- return _types__rspack_import_1/* .Expression.NOT_PARSABLE */.r4.NOT_PARSABLE;
3345
+ return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
3346
+ }
3347
+ };
3348
+ const collectReads = (operand, into)=>{
3349
+ switch(operand.kind){
3350
+ case "property":
3351
+ into.add(operand.property);
3352
+ return;
3353
+ case "method-call":
3354
+ collectReads(operand.target, into);
3355
+ collectReads(operand.argument, into);
3356
+ return;
3357
+ case "arithmetic":
3358
+ collectReads(operand.left, into);
3359
+ collectReads(operand.right, into);
3360
+ if (operand.extra != null) {
3361
+ collectReads(operand.extra, into);
3362
+ }
3363
+ return;
3364
+ case "conditional":
3365
+ for (const property of getProperties(operand.condition)){
3366
+ into.add(property);
3367
+ }
3368
+ collectReads(operand.whenTrue, into);
3369
+ collectReads(operand.whenFalse, into);
3370
+ return;
3371
+ case "opaque":
3372
+ for (const read of operand.reads){
3373
+ collectReads(read, into);
3374
+ }
3375
+ return;
1799
3376
  }
1800
3377
  };
3378
+ const selectedValue = (operand)=>{
3379
+ const found = new Set();
3380
+ collectReads(operand, found);
3381
+ const reads = [
3382
+ ...found
3383
+ ];
3384
+ return {
3385
+ property: reads.length === 1 ? reads[0] : null,
3386
+ reads,
3387
+ isDirectProperty: operand.kind === "property" && operand.transformer == null
3388
+ };
3389
+ };
3390
+ // Keyed like the template cache. A selector takes no params, so every result is cacheable
3391
+ const selectorCache = new WeakMap();
3392
+ /**
3393
+ * Reads a sort, map, group or `nearest` selector with the grammar filters use, for what its value is
3394
+ * read from.
3395
+ *
3396
+ * Not for evaluating it: the caller's function stays the value, and nothing here renders one. A plugin
3397
+ * decides from the result whether it can run the option. One that orders or projects by column cannot
3398
+ * run a value that is not the property itself, and one that runs the function over stored rows cannot
3399
+ * run it over a renamed property.
3400
+ *
3401
+ * So the grammar is wider here than for a filter. A call it has no node for, such as `getFullYear()`,
3402
+ * is kept for the operands it reads rather than refused, since the function it came from still runs.
3403
+ *
3404
+ * `not-parsable` is not logged. The option runs as it did before the selector was parsed.
3405
+ *
3406
+ * Cached by function source per schema, like `toExpression`. The result is shared, so it is read-only.
3407
+ */ const parseSelector = (schema, selector)=>{
3408
+ const source = selector.toString();
3409
+ let bySource = selectorCache.get(schema);
3410
+ const cached = bySource?.get(source);
3411
+ if (cached != null) {
3412
+ return cached;
3413
+ }
3414
+ let parsed;
3415
+ try {
3416
+ const shape = resolveFunctionShape(source, false);
3417
+ const parser = new ExpressionParser(schema, new TokenStream(tokenize(shape.body)), shape.scope, null, undefined, true);
3418
+ const selected = parser.parseSelector();
3419
+ parsed = Array.isArray(selected) ? {
3420
+ kind: "object",
3421
+ fields: selected.map((field)=>({
3422
+ name: field.name,
3423
+ ...selectedValue(field.operand)
3424
+ }))
3425
+ } : {
3426
+ kind: "value",
3427
+ value: selectedValue(selected)
3428
+ };
3429
+ } catch (error) {
3430
+ parsed = {
3431
+ kind: "not-parsable",
3432
+ reason: refusalOf(error)
3433
+ };
3434
+ }
3435
+ if (bySource == null) {
3436
+ bySource = new Map();
3437
+ selectorCache.set(schema, bySource);
3438
+ }
3439
+ // Stryker disable next-line all: the same resource bound as the template cache's
3440
+ if (bySource.size >= MAX_CACHED_TEMPLATES_PER_SCHEMA) {
3441
+ bySource.clear();
3442
+ }
3443
+ bySource.set(source, parsed);
3444
+ return parsed;
3445
+ }; // #endregion
1801
3446
 
1802
3447
 
1803
3448
  },
1804
3449
  27(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1805
3450
  __webpack_require__.d(__webpack_exports__, {
3451
+ DG: () => (CallExpression),
1806
3452
  Ko: () => (ValueExpression),
1807
3453
  bQ: () => (ComparatorExpression),
1808
3454
  ep: () => (PropertyExpression),
@@ -1812,59 +3458,69 @@ __webpack_require__.d(__webpack_exports__, {
1812
3458
  const valueToJson = (value)=>{
1813
3459
  if (value === undefined) {
1814
3460
  return {
1815
- k: "undefined"
3461
+ undefined: true
1816
3462
  };
1817
3463
  }
1818
3464
  if (value === null) {
1819
- return {
1820
- k: "raw",
1821
- v: null
1822
- };
3465
+ return null;
1823
3466
  }
1824
3467
  if (value instanceof Date) {
1825
3468
  // ISO rather than epoch millis: it survives a human reading the payload, and an invalid
1826
3469
  // Date has no ISO form — so it is caught here rather than becoming a silent `null`.
1827
3470
  return {
1828
- k: "date",
1829
- v: value.toISOString()
3471
+ date: value.toISOString()
1830
3472
  };
1831
3473
  }
1832
3474
  if (Array.isArray(value)) {
1833
- return {
1834
- k: "array",
1835
- v: value.map(valueToJson)
1836
- };
3475
+ return value.map(valueToJson);
1837
3476
  }
1838
3477
  if (typeof value === "number" && Number.isFinite(value) === false) {
1839
3478
  // `JSON.stringify` turns all three of these into `null`, which would compare as a different
1840
3479
  // value entirely rather than failing.
1841
3480
  return {
1842
- k: "number",
1843
- v: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
3481
+ number: Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity"
1844
3482
  };
1845
3483
  }
1846
3484
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3485
+ return value;
3486
+ }
3487
+ if (value instanceof RegExp) {
1847
3488
  return {
1848
- k: "raw",
1849
- v: value
3489
+ regex: {
3490
+ source: value.source,
3491
+ flags: value.flags
3492
+ }
3493
+ };
3494
+ }
3495
+ // `JSON.stringify` throws outright on a bigint rather than losing it quietly, so this is the one
3496
+ // tag that turns a crash into a value
3497
+ if (typeof value === "bigint") {
3498
+ return {
3499
+ bigint: value.toString()
1850
3500
  };
1851
3501
  }
1852
3502
  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)}`);
1853
3503
  };
1854
3504
  const valueFromJson = (value)=>{
1855
- if (value.k === "undefined") {
1856
- return undefined;
3505
+ if (value === null || typeof value !== "object") {
3506
+ return value;
3507
+ }
3508
+ if (Array.isArray(value)) {
3509
+ return value.map(valueFromJson);
1857
3510
  }
1858
- if (value.k === "date") {
1859
- return new Date(value.v);
3511
+ if ("date" in value) {
3512
+ return new Date(value.date);
1860
3513
  }
1861
- if (value.k === "array") {
1862
- return value.v.map(valueFromJson);
3514
+ if ("undefined" in value) {
3515
+ return undefined;
3516
+ }
3517
+ if ("regex" in value) {
3518
+ return new RegExp(value.regex.source, value.regex.flags);
1863
3519
  }
1864
- if (value.k === "number") {
1865
- return value.v === "NaN" ? Number.NaN : value.v === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
3520
+ if ("bigint" in value) {
3521
+ return BigInt(value.bigint);
1866
3522
  }
1867
- return value.v;
3523
+ return value.number === "NaN" ? Number.NaN : value.number === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1868
3524
  };
1869
3525
  /**
1870
3526
  * The base class for all expression types.
@@ -1881,6 +3537,9 @@ const valueFromJson = (value)=>{
1881
3537
  static get NOT_PARSABLE() {
1882
3538
  return new NotParsableExpression();
1883
3539
  }
3540
+ /** `NOT_PARSABLE`, carrying what the parser refused. */ static notParsable(reason) {
3541
+ return new NotParsableExpression(reason);
3542
+ }
1884
3543
  static isEmpty(expression) {
1885
3544
  return expression.type === "empty" || expression instanceof EmptyExpression;
1886
3545
  }
@@ -1897,7 +3556,7 @@ const valueFromJson = (value)=>{
1897
3556
  *
1898
3557
  * ## Why it is this small
1899
3558
  *
1900
- * Of the six node types a bound tree can contain, exactly one holds anything JSON cannot carry:
3559
+ * Of the seven node types a bound tree can contain, exactly one holds anything JSON cannot carry:
1901
3560
  * `PropertyExpression`, whose live `PropertyInfo` has functions, a parent chain and caches. It
1902
3561
  * reduces to a property PATH — `PropertyInfo.id` IS the dotted path, and `getProperty` is keyed by
1903
3562
  * exactly that — so rebinding is one lookup.
@@ -1912,7 +3571,7 @@ const valueFromJson = (value)=>{
1912
3571
  if (expression.type === "operator") {
1913
3572
  const operator = expression;
1914
3573
  return {
1915
- t: "operator",
3574
+ type: "operator",
1916
3575
  operator: operator.operator,
1917
3576
  ...operator.left != null && {
1918
3577
  left: Expression.toJson(operator.left)
@@ -1925,7 +3584,7 @@ const valueFromJson = (value)=>{
1925
3584
  if (expression.type === "comparator") {
1926
3585
  const comparator = expression;
1927
3586
  return {
1928
- t: "comparator",
3587
+ type: "comparator",
1929
3588
  comparator: comparator.comparator,
1930
3589
  negated: comparator.negated,
1931
3590
  strict: comparator.strict,
@@ -1937,29 +3596,41 @@ const valueFromJson = (value)=>{
1937
3596
  }
1938
3597
  };
1939
3598
  }
3599
+ if (expression.type === "call") {
3600
+ const call = expression;
3601
+ return {
3602
+ type: "call",
3603
+ call: call.call,
3604
+ expression: Expression.toJson(call.expression),
3605
+ arguments: call.arguments.map(Expression.toJson)
3606
+ };
3607
+ }
1940
3608
  if (expression.type === "property") {
1941
3609
  const property = expression;
1942
3610
  return {
1943
- t: "property",
3611
+ type: "property",
1944
3612
  // The dotted path, which is exactly the key `getProperty` is looking up
1945
- path: property.property.id,
1946
- transformer: property.transformer,
1947
- locale: property.locale
3613
+ path: property.property.id
1948
3614
  };
1949
3615
  }
1950
3616
  if (expression.type === "value") {
1951
3617
  const value = expression;
1952
3618
  return {
1953
- t: "value",
1954
- value: valueToJson(value.value),
1955
- transformer: value.transformer,
1956
- locale: value.locale
3619
+ type: "value",
3620
+ value: valueToJson(value.value)
3621
+ };
3622
+ }
3623
+ if (expression.type === "empty") {
3624
+ return {
3625
+ type: "empty"
1957
3626
  };
1958
3627
  }
1959
- return expression.type === "empty" ? {
1960
- t: "empty"
3628
+ const reason = expression.reason;
3629
+ return reason == null ? {
3630
+ type: "not-parsable"
1961
3631
  } : {
1962
- t: "not-parsable"
3632
+ type: "not-parsable",
3633
+ reason
1963
3634
  };
1964
3635
  }
1965
3636
  /**
@@ -1975,14 +3646,14 @@ const valueFromJson = (value)=>{
1975
3646
  * failure here worse than an error.
1976
3647
  */ static fromJson(json, schema) {
1977
3648
  const child = (node)=>node == null ? undefined : Expression.fromJson(node, schema);
1978
- if (json.t === "operator") {
3649
+ if (json.type === "operator") {
1979
3650
  return new OperatorExpression({
1980
3651
  operator: json.operator,
1981
3652
  left: child(json.left),
1982
3653
  right: child(json.right)
1983
3654
  });
1984
3655
  }
1985
- if (json.t === "comparator") {
3656
+ if (json.type === "comparator") {
1986
3657
  return new ComparatorExpression({
1987
3658
  comparator: json.comparator,
1988
3659
  negated: json.negated,
@@ -1991,27 +3662,34 @@ const valueFromJson = (value)=>{
1991
3662
  right: child(json.right)
1992
3663
  });
1993
3664
  }
1994
- if (json.t === "property") {
3665
+ if (json.type === "call") {
3666
+ if (json.expression == null) {
3667
+ throw new Error(`Cannot deserialize a filter: a '${json.call}' call carries no operand. ` + `Collection: ${schema.collectionName}.`);
3668
+ }
3669
+ return new CallExpression({
3670
+ call: json.call,
3671
+ expression: Expression.fromJson(json.expression, schema),
3672
+ arguments: (json.arguments ?? []).map((argument)=>Expression.fromJson(argument, schema))
3673
+ });
3674
+ }
3675
+ if (json.type === "property") {
1995
3676
  const property = schema.getProperty(json.path);
1996
3677
  if (property == null) {
1997
3678
  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.`);
1998
3679
  }
1999
- const rebuilt = new PropertyExpression({
3680
+ return new PropertyExpression({
2000
3681
  property
2001
3682
  });
2002
- rebuilt.transformer = json.transformer;
2003
- rebuilt.locale = json.locale;
2004
- return rebuilt;
2005
3683
  }
2006
- if (json.t === "value") {
2007
- const rebuilt = new ValueExpression({
3684
+ if (json.type === "value") {
3685
+ return new ValueExpression({
2008
3686
  value: valueFromJson(json.value)
2009
3687
  });
2010
- rebuilt.transformer = json.transformer;
2011
- rebuilt.locale = json.locale;
2012
- return rebuilt;
2013
3688
  }
2014
- return json.t === "empty" ? Expression.EMPTY : Expression.NOT_PARSABLE;
3689
+ if (json.type === "empty") {
3690
+ return Expression.EMPTY;
3691
+ }
3692
+ return json.reason == null ? Expression.NOT_PARSABLE : Expression.notParsable(json.reason);
2015
3693
  }
2016
3694
  }
2017
3695
  class EmptyExpression extends Expression {
@@ -2019,6 +3697,11 @@ class EmptyExpression extends Expression {
2019
3697
  }
2020
3698
  class NotParsableExpression extends Expression {
2021
3699
  type = "not-parsable";
3700
+ /** What the parser refused, when it knows. `.explain()` prints it beside the source. */ reason;
3701
+ constructor(reason){
3702
+ super();
3703
+ this.reason = reason;
3704
+ }
2022
3705
  }
2023
3706
  /**
2024
3707
  * A class representing a comparison operation (e.g., equals, greater-than).
@@ -2049,20 +3732,28 @@ class NotParsableExpression extends Expression {
2049
3732
  */ class PropertyExpression extends Expression {
2050
3733
  /** The type of the expression (always 'property'). */ type = "property";
2051
3734
  /** The property info for the path. */ property;
2052
- transformer = null;
2053
- locale = null;
2054
3735
  constructor(options){
2055
3736
  super();
2056
3737
  this.property = options.property;
2057
3738
  }
2058
3739
  }
3740
+ class CallExpression extends Expression {
3741
+ type = "call";
3742
+ call;
3743
+ expression;
3744
+ /** Empty for a unary call. */ arguments;
3745
+ constructor(options){
3746
+ super();
3747
+ this.call = options.call;
3748
+ this.expression = options.expression;
3749
+ this.arguments = options.arguments ?? [];
3750
+ }
3751
+ }
2059
3752
  /**
2060
3753
  * A class representing a literal value.
2061
3754
  */ class ValueExpression extends Expression {
2062
3755
  /** The type of the expression (always 'value'). */ type = "value";
2063
3756
  /** The literal value. */ value;
2064
- transformer = null;
2065
- locale = null;
2066
3757
  constructor(options){
2067
3758
  super();
2068
3759
  this.value = options.value;
@@ -2073,8 +3764,43 @@ class NotParsableExpression extends Expression {
2073
3764
  },
2074
3765
  63(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2075
3766
  __webpack_require__.d(__webpack_exports__, {
2076
- j: () => (forEach)
3767
+ LU: () => (childrenOf),
3768
+ jJ: () => (forEach)
2077
3769
  });
3770
+ /**
3771
+ * Separates an operand from the calls applied to it.
3772
+ *
3773
+ * `null` when there is no operand beneath the calls. Every consumer needs this to decide whether a
3774
+ * comparator side is a property or a value, so it lives here rather than in each translator.
3775
+ */ function peelCalls(expression) {
3776
+ const calls = [];
3777
+ let current = expression;
3778
+ while(current != null && current.type === "call"){
3779
+ calls.unshift(current);
3780
+ current = current.expression;
3781
+ }
3782
+ return current == null ? null : {
3783
+ operand: current,
3784
+ calls
3785
+ };
3786
+ }
3787
+ function childrenOf(expression) {
3788
+ if (expression.type === "call") {
3789
+ const call = expression;
3790
+ return [
3791
+ call.expression,
3792
+ ...call.arguments ?? []
3793
+ ].filter((child)=>child != null);
3794
+ }
3795
+ const children = [];
3796
+ if (expression.left != null) {
3797
+ children.push(expression.left);
3798
+ }
3799
+ if (expression.right != null) {
3800
+ children.push(expression.right);
3801
+ }
3802
+ return children;
3803
+ }
2078
3804
  /**
2079
3805
  * Extracts all properties referenced in an expression
2080
3806
  * @param expression The expression to analyze
@@ -2086,12 +3812,8 @@ __webpack_require__.d(__webpack_exports__, {
2086
3812
  if (expr.type === "property") {
2087
3813
  properties.push(expr.property);
2088
3814
  }
2089
- // Traverse left and right expressions if they exist
2090
- if (expr.left) {
2091
- traverse(expr.left);
2092
- }
2093
- if (expr.right) {
2094
- traverse(expr.right);
3815
+ for (const child of childrenOf(expr)){
3816
+ traverse(child);
2095
3817
  }
2096
3818
  }
2097
3819
  traverse(expression);
@@ -2104,14 +3826,8 @@ function forEach(expression, callback) {
2104
3826
  if (!callback(expr)) {
2105
3827
  return false;
2106
3828
  }
2107
- // Traverse left and right expressions if they exist
2108
- if (expr.left) {
2109
- if (!traverse(expr.left)) {
2110
- return false;
2111
- }
2112
- }
2113
- if (expr.right) {
2114
- if (!traverse(expr.right)) {
3829
+ for (const child of childrenOf(expr)){
3830
+ if (!traverse(child)) {
2115
3831
  return false;
2116
3832
  }
2117
3833
  }
@@ -2392,32 +4108,86 @@ __webpack_require__.d(__webpack_exports__, {
2392
4108
  H: () => (QueryOptionsCollection)
2393
4109
  });
2394
4110
  /* import */ var _assertions__rspack_import_1 = __webpack_require__(126);
2395
- /* import */ var _expressions_utils__rspack_import_0 = __webpack_require__(63);
4111
+ /* import */ var _expressions_utils__rspack_import_2 = __webpack_require__(63);
4112
+ /* import */ var _schema_types__rspack_import_0 = __webpack_require__(537);
4113
+ /* import */ var _utilities__rspack_import_3 = __webpack_require__(581);
4114
+
4115
+
2396
4116
 
2397
4117
 
4118
+ /** What a schema type is called in JavaScript, where one exists. A value of any other type cannot equal it. */ const JAVASCRIPT_TYPE_OF = {
4119
+ [_schema_types__rspack_import_0/* .SchemaTypes.Number */.L.Number]: "number",
4120
+ [_schema_types__rspack_import_0/* .SchemaTypes.String */.L.String]: "string",
4121
+ [_schema_types__rspack_import_0/* .SchemaTypes.Boolean */.L.Boolean]: "boolean",
4122
+ [_schema_types__rspack_import_0/* .SchemaTypes.Date */.L.Date]: "object"
4123
+ };
4124
+ const mismatchedSide = (property, value)=>{
4125
+ if (property == null || value == null || !(0,_assertions__rspack_import_1.isPropertyExpression)(property) || !(0,_assertions__rspack_import_1.isValueExpression)(value)) {
4126
+ return null;
4127
+ }
4128
+ const expected = JAVASCRIPT_TYPE_OF[property.property.type];
4129
+ if (expected == null || value.value == null || typeof value.value === expected) {
4130
+ return null;
4131
+ }
4132
+ return {
4133
+ property,
4134
+ value,
4135
+ expected
4136
+ };
4137
+ };
4138
+ /** A strict comparison whose answer is the same for every row, because the types cannot be equal. */ const comparesTypesThatCannotMatch = (expression)=>{
4139
+ if (!(0,_assertions__rspack_import_1.isComparatorExpression)(expression) || expression.strict !== true) {
4140
+ return false;
4141
+ }
4142
+ if (expression.comparator !== "equals") {
4143
+ return false;
4144
+ }
4145
+ return mismatchedSide(expression.left, expression.right) != null || mismatchedSide(expression.right, expression.left) != null;
4146
+ };
4147
+ /** `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);
4148
+ const mismatchWarning = (expression)=>{
4149
+ const side = mismatchedSide(expression.left, expression.right) ?? mismatchedSide(expression.right, expression.left);
4150
+ const outcome = expression.negated ? "every row matches" : "no row matches";
4151
+ 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`;
4152
+ };
4153
+ /**
4154
+ * An item as one dispatch receives it: a new object, so a report written on it stays with that dispatch.
4155
+ *
4156
+ * A database option starts `executed` again, because a report is only an answer from the plugin that
4157
+ * made it. A memory option keeps the reason core planned it with. A join's inner options are copied the
4158
+ * same way, since a plugin can report on them too.
4159
+ */ const toDispatchItem = (item)=>{
4160
+ const option = item.option;
4161
+ const value = option.name === "join" ? {
4162
+ ...option.value,
4163
+ innerOptions: option.value.innerOptions.forDispatch()
4164
+ } : option.value;
4165
+ return {
4166
+ index: item.index,
4167
+ option: option.target === "database" ? {
4168
+ ...option,
4169
+ value,
4170
+ reason: "executed"
4171
+ } : {
4172
+ ...option,
4173
+ value
4174
+ }
4175
+ };
4176
+ };
2398
4177
  class QueryOptionsCollection {
2399
4178
  options = new Map();
2400
4179
  nextExecutionTarget = "database";
2401
4180
  nextExecutionReason = null;
2402
4181
  nextIndex = 0;
2403
4182
  enumeratedItems = [];
4183
+ dirty = true;
4184
+ /** The collection a `splitAt`/`split` half came from. A capability report belongs to it. */ origin = null;
2404
4185
  /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */ cutOverToMemory(reason) {
2405
4186
  this.nextExecutionTarget = "memory";
2406
4187
  if (this.nextExecutionReason == null) {
2407
4188
  this.nextExecutionReason = reason;
2408
4189
  }
2409
4190
  }
2410
- /**
2411
- * True when `split()` or `splitAt()` produced this collection.
2412
- *
2413
- * Those rebuild each half by re-adding its options, which re-derives execution targets
2414
- * without the options that caused them — a post-join filter alone in the memory half
2415
- * derives back to `"database"`. Anything reading `target` as a report of where work runs
2416
- * has to reject a derived collection; see `explainQuery`.
2417
- */ derived = false;
2418
- get isDerived() {
2419
- return this.derived;
2420
- }
2421
4191
  get items() {
2422
4192
  return this.options;
2423
4193
  }
@@ -2442,7 +4212,7 @@ class QueryOptionsCollection {
2442
4212
  }
2443
4213
  }
2444
4214
  if (name === "filter") {
2445
- // Need to check for unmapped and renamed properties
4215
+ // Need to check for unmapped properties
2446
4216
  const filterValue = value;
2447
4217
  // A tautology (`x => true`) filters nothing — skip it entirely so
2448
4218
  // plugins never see it
@@ -2452,19 +4222,20 @@ class QueryOptionsCollection {
2452
4222
  if (filterValue.expression.type === "not-parsable") {
2453
4223
  this.cutOverToMemory("not-parsable");
2454
4224
  } else {
2455
- (0,_expressions_utils__rspack_import_0/* .forEach */.j)(filterValue.expression, (expression)=>{
4225
+ (0,_expressions_utils__rspack_import_2/* .forEach */.jJ)(filterValue.expression, (expression)=>{
2456
4226
  if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.isUnmapped) {
2457
4227
  // Cut over to memory execution, unmapped properties are not in the database and
2458
4228
  // cannot be queried
2459
4229
  this.cutOverToMemory("unmapped-property");
2460
4230
  return false;
2461
4231
  }
2462
- if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.hasRenamedSegments) {
2463
- // Cut over to memory execution: the plugin stores data under the
2464
- // `from` (storage) names, but filter selectors reference the
2465
- // in-memory names. Memory execution runs after deserialization,
2466
- // where the in-memory names exist
2467
- this.cutOverToMemory("renamed-property");
4232
+ // A renamed property stays with the database. Whether the backend can read a
4233
+ // `from` name is the plugin's to know, not this collection's: the property
4234
+ // travels with the option, and a plugin that cannot resolve it reports it
4235
+ // back see `reportRenamedProperties`
4236
+ if (comparesTypesThatCannotMatch(expression)) {
4237
+ _utilities__rspack_import_3/* .logger.warn */.vF.warn(mismatchWarning(expression));
4238
+ this.cutOverToMemory("predicate-error");
2468
4239
  return false;
2469
4240
  }
2470
4241
  return true;
@@ -2473,28 +4244,26 @@ class QueryOptionsCollection {
2473
4244
  }
2474
4245
  if (name === "sort") {
2475
4246
  const sortValue = value;
2476
- // Same rule as filters: sort selectors reference in-memory names, which
2477
- // only exist after deserialization when the property is renamed or unmapped
4247
+ // Same rule as filters: an unmapped property only exists after deserialization. A
4248
+ // renamed one stays with the database, for the plugin to resolve or report
2478
4249
  if (sortValue.property != null && sortValue.property.isUnmapped) {
2479
4250
  this.cutOverToMemory("unmapped-property");
2480
- } else if (sortValue.property != null && sortValue.property.hasRenamedSegments) {
2481
- this.cutOverToMemory("renamed-property");
2482
4251
  }
2483
4252
  }
2484
4253
  if (name === "nearest") {
2485
4254
  const nearestValue = value;
2486
- // Same rule as sort, and for the same reason: the plugin stores the vector under
2487
- // the `from` name, and an unmapped property is not stored at all. Both are only
2488
- // readable after deserialization, which is where memory execution runs.
2489
- //
2490
- // This is also what lets every translator's in-memory fallback read the column by
2491
- // its resolved name — anything whose storage name differs never reaches them.
4255
+ // Same rule as sort, and for the same reason: an unmapped property is not stored at
4256
+ // all, so it is only readable after deserialization, which is where memory execution
4257
+ // runs. A vector stored under a `from` name is the plugin's to resolve or report.
2492
4258
  if (nearestValue.property != null && nearestValue.property.isUnmapped) {
2493
4259
  this.cutOverToMemory("unmapped-property");
2494
- } else if (nearestValue.property != null && nearestValue.property.hasRenamedSegments) {
2495
- this.cutOverToMemory("renamed-property");
2496
4260
  }
2497
4261
  }
4262
+ if ((name === "filter" || name === "sort") && (this.options.has("skip") || this.options.has("take"))) {
4263
+ // SQL emits WHERE before LIMIT and Mongo's find() filters before skipping, so an option
4264
+ // written after a window can only see the windowed rows if it runs after it.
4265
+ this.cutOverToMemory("after-window");
4266
+ }
2498
4267
  if (name === "join") {
2499
4268
  const joinValue = value;
2500
4269
  // A join whose two sides live on different plugins cannot be sent to EITHER of
@@ -2507,18 +4276,24 @@ class QueryOptionsCollection {
2507
4276
  this.cutOverToMemory("cross-plugin-join");
2508
4277
  }
2509
4278
  }
4279
+ // `executed` is the plan, not a record: nothing has run when an option is added. Every
4280
+ // consumer reads it after the plugin returned, so the optimistic window is never observed.
2510
4281
  const item = {
2511
4282
  index: this.nextIndex,
2512
- option: {
4283
+ option: this.nextExecutionTarget === "database" ? {
2513
4284
  name,
2514
- target: this.nextExecutionTarget,
2515
4285
  value,
2516
- ...this.nextExecutionReason == null ? {} : {
2517
- reason: this.nextExecutionReason
2518
- }
4286
+ target: "database",
4287
+ reason: "executed"
4288
+ } : {
4289
+ name,
4290
+ value,
4291
+ target: "memory",
4292
+ reason: this.nextExecutionReason ?? "not-parsable"
2519
4293
  }
2520
4294
  };
2521
4295
  this.nextIndex++;
4296
+ this.dirty = true;
2522
4297
  const found = this.options.get(name);
2523
4298
  this.options.set(name, [
2524
4299
  ...found ?? [],
@@ -2563,8 +4338,6 @@ class QueryOptionsCollection {
2563
4338
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
2564
4339
  const before = new QueryOptionsCollection();
2565
4340
  const after = new QueryOptionsCollection();
2566
- before.derived = true;
2567
- after.derived = true;
2568
4341
  let at = null;
2569
4342
  for(let i = 0, length = sortedItems.length; i < length; i++){
2570
4343
  const { option } = sortedItems[i];
@@ -2573,8 +4346,10 @@ class QueryOptionsCollection {
2573
4346
  continue;
2574
4347
  }
2575
4348
  const destination = at == null ? before : after;
2576
- destination.add(option.name, option.value);
4349
+ destination.adopt(sortedItems[i]);
2577
4350
  }
4351
+ before.origin = this.origin ?? this;
4352
+ after.origin = this.origin ?? this;
2578
4353
  return {
2579
4354
  before,
2580
4355
  at,
@@ -2588,6 +4363,9 @@ class QueryOptionsCollection {
2588
4363
  * the shared collection before executing. Without restoring, a re-executed terminal —
2589
4364
  * the whole point of a subscribed queryable — stacks its option a second time and
2590
4365
  * runs it over the first execution's scalar result.
4366
+ *
4367
+ * The item objects are shared with the snapshot. Nothing reports on them, because every
4368
+ * dispatch sends a `forDispatch` copy, so a restore brings back no reports.
2591
4369
  */ snapshot() {
2592
4370
  const options = new Map([
2593
4371
  ...this.options.entries()
@@ -2606,23 +4384,128 @@ class QueryOptionsCollection {
2606
4384
  this.nextExecutionReason = nextExecutionReason;
2607
4385
  this.nextIndex = nextIndex;
2608
4386
  this.enumeratedItems = [];
4387
+ // Clearing the list is not enough now that staleness is a flag rather than a count:
4388
+ // without this, `resolveEnumeration` believes the empty list is current and every read
4389
+ // of the collection sees no options at all.
4390
+ this.dirty = true;
4391
+ };
4392
+ }
4393
+ /** Takes an item as it stands — same object, same index, same target and reason. */ adopt(item) {
4394
+ const found = this.options.get(item.option.name);
4395
+ this.options.set(item.option.name, [
4396
+ ...found ?? [],
4397
+ item
4398
+ ]);
4399
+ this.nextIndex = Math.max(this.nextIndex, item.index + 1);
4400
+ this.dirty = true;
4401
+ }
4402
+ /**
4403
+ * A plugin reporting that its engine cannot express one option.
4404
+ *
4405
+ * Core marks the rest of the database phase `not-reached`, because the database has to stop
4406
+ * there — a window applied in front of a filter that was not applied returns the wrong rows.
4407
+ * Passing the cascade through core is what makes it impossible for a plugin to mark a
4408
+ * non-contiguous cut.
4409
+ *
4410
+ * A report names a culprit and never un-names one, so reports commute.
4411
+ *
4412
+ * The option is not moved to the memory arm. It stays where it was planned, which is what keeps
4413
+ * a redirect distinguishable from something core sent to memory in the first place.
4414
+ */ reportMissingCapability(item) {
4415
+ this.report(item, "missing-capability");
4416
+ }
4417
+ /**
4418
+ * A plugin reporting that its engine would answer one option differently from JavaScript.
4419
+ *
4420
+ * Same cascade as `reportMissingCapability`, and a separate reason because the caller can act on
4421
+ * one and not the other. See `DatabaseExecutionReason`.
4422
+ */ reportEngineDivergence(item) {
4423
+ this.report(item, "engine-divergence");
4424
+ }
4425
+ report(item, reason) {
4426
+ // A half can only see its own slice, and the database has to stop for the whole dispatch.
4427
+ if (this.origin != null) {
4428
+ this.origin.report(item, reason);
4429
+ return;
4430
+ }
4431
+ this.resolveEnumeration();
4432
+ for (const candidate of this.enumeratedItems){
4433
+ if (candidate.option.target !== "database" || candidate.index < item.index) {
4434
+ continue;
4435
+ }
4436
+ if (candidate.index === item.index) {
4437
+ candidate.option.reason = reason;
4438
+ continue;
4439
+ }
4440
+ if (candidate.option.reason === "executed") {
4441
+ candidate.option.reason = "not-reached";
4442
+ }
4443
+ }
4444
+ }
4445
+ /**
4446
+ * A copy of the collection for one dispatch to a plugin, with nothing reported on it.
4447
+ *
4448
+ * Capability is answered per dispatch, so a report is only an answer for the execution that
4449
+ * produced it. Reports are written onto items, and the items of a queryable's collection
4450
+ * outlive any one execution: a snapshot shares them, and a subscription dispatches the same
4451
+ * query on every change. A report left on them replays options the plugin did run on the
4452
+ * next execution, such as a `skip` applied twice over rows already windowed, or hands a
4453
+ * renamed filter to memory that the engine could have run.
4454
+ *
4455
+ * Each item keeps its index, name, value and target. A half from `split`/`splitAt` is copied
4456
+ * with a copy of its origin, and its items are that copy's items, so a report on the half still
4457
+ * cascades over the whole dispatch without reaching the collection it was copied from.
4458
+ */ forDispatch() {
4459
+ if (this.origin == null) {
4460
+ return this.copyForDispatch().copy;
4461
+ }
4462
+ const { copy: root, copies } = this.origin.copyForDispatch();
4463
+ const half = new QueryOptionsCollection();
4464
+ this.resolveEnumeration();
4465
+ for (const item of this.enumeratedItems){
4466
+ // An item added to the half after it was split has no counterpart in the origin
4467
+ half.adopt(copies.get(item) ?? toDispatchItem(item));
4468
+ }
4469
+ half.origin = root;
4470
+ return half;
4471
+ }
4472
+ copyForDispatch() {
4473
+ const copy = new QueryOptionsCollection();
4474
+ const copies = new Map();
4475
+ this.resolveEnumeration();
4476
+ for (const item of this.enumeratedItems){
4477
+ const copied = toDispatchItem(item);
4478
+ copies.set(item, copied);
4479
+ copy.adopt(copied);
4480
+ }
4481
+ copy.nextExecutionTarget = this.nextExecutionTarget;
4482
+ copy.nextExecutionReason = this.nextExecutionReason;
4483
+ copy.nextIndex = this.nextIndex;
4484
+ return {
4485
+ copy,
4486
+ copies
2609
4487
  };
2610
4488
  }
4489
+ /** The options the database did not run, in the order they were written. */ notExecuted() {
4490
+ this.resolveEnumeration();
4491
+ return this.enumeratedItems.filter((item)=>item.option.target === "database" && item.option.reason !== "executed").toSorted((a, b)=>a.index - b.index);
4492
+ }
2611
4493
  split() {
2612
4494
  this.resolveEnumeration();
2613
4495
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
2614
4496
  const memoryQueryOptionsCollection = new QueryOptionsCollection();
2615
4497
  const databaseQueryOptionsCollection = new QueryOptionsCollection();
2616
- memoryQueryOptionsCollection.derived = true;
2617
- databaseQueryOptionsCollection.derived = true;
2618
4498
  for(let i = 0, length = sortedItems.length; i < length; i++){
2619
4499
  const sortedItem = sortedItems[i];
2620
- if (sortedItem.option.target === "database") {
2621
- databaseQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
2622
- continue;
2623
- }
2624
- memoryQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
2625
- }
4500
+ const half = sortedItem.option.target === "database" ? databaseQueryOptionsCollection : memoryQueryOptionsCollection;
4501
+ // The ITEM, not its name and value. Re-adding would re-derive target and reason from a
4502
+ // fresh cascade, and a memory option re-added alone comes back out as `database` with no
4503
+ // reason at all. Sharing it also means a plugin's report on the database half is the
4504
+ // same object the explanation reads.
4505
+ half.adopt(sortedItem);
4506
+ }
4507
+ memoryQueryOptionsCollection.origin = this.origin ?? this;
4508
+ databaseQueryOptionsCollection.origin = this.origin ?? this;
2626
4509
  return {
2627
4510
  memory: memoryQueryOptionsCollection,
2628
4511
  database: databaseQueryOptionsCollection
@@ -2668,8 +4551,11 @@ class QueryOptionsCollection {
2668
4551
  ].flat().toSorted((a, b)=>a.index - b.index);
2669
4552
  }
2670
4553
  resolveEnumeration() {
2671
- if (this.enumeratedItems.length != this.nextIndex) {
4554
+ // A flag, not a count: adopting leaves gaps in the indexes, so `length !== nextIndex` is
4555
+ // true forever on a half and the enumeration rebuilds on every read.
4556
+ if (this.dirty === true) {
2672
4557
  this.enumeratedItems = this.getEnumeration();
4558
+ this.dirty = false;
2673
4559
  }
2674
4560
  }
2675
4561
  forEach(iterator) {
@@ -2760,42 +4646,164 @@ class PluginEventResult extends BaseResult {
2760
4646
  throw new Error(`Expected success result, but got ${result.ok}: ${result.error}`);
2761
4647
  }
2762
4648
  }
2763
- }
2764
-
2765
-
2766
- },
2767
- 537(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2768
- __webpack_require__.d(__webpack_exports__, {
2769
- L: () => (SchemaTypes)
2770
- });
2771
- var SchemaTypes = /*#__PURE__*/ function(SchemaTypes) {
2772
- SchemaTypes["Array"] = "Array";
2773
- SchemaTypes["Boolean"] = "Boolean";
2774
- SchemaTypes["Date"] = "Date";
2775
- SchemaTypes["Number"] = "Number";
2776
- SchemaTypes["Object"] = "Object";
2777
- SchemaTypes["String"] = "String";
2778
- SchemaTypes["Definition"] = "Definition";
2779
- SchemaTypes["Function"] = "Function";
2780
- SchemaTypes["Computed"] = "Computed";
2781
- /**
2782
- * Content in, reference out. The only type whose write shape differs from its stored
2783
- * shape, and a leaf on purpose — see `SchemaFile`.
2784
- */ SchemaTypes["File"] = "File";
2785
- /**
2786
- * A fixed-length list of numbers, carrying its dimension count — see `SchemaVector`.
2787
- *
2788
- * Value-shaped exactly like `s.array(s.number())`, which is why every array codegen
2789
- * handler accepts it. It is a distinct type only so a backend can recognise it and store
2790
- * it natively; nothing else needs to tell the two apart.
2791
- */ SchemaTypes["Vector"] = "Vector";
2792
- return SchemaTypes;
2793
- }({});
2794
- var HashType = /*#__PURE__*/ (/* unused pure expression or super */ null && (function(HashType) {
2795
- HashType["Ids"] = "Ids";
2796
- HashType["Object"] = "Object";
2797
- return HashType;
2798
- }({})));
4649
+ }
4650
+
4651
+
4652
+ },
4653
+ 537(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4654
+ __webpack_require__.d(__webpack_exports__, {
4655
+ L: () => (SchemaTypes)
4656
+ });
4657
+ var SchemaTypes = /*#__PURE__*/ function(SchemaTypes) {
4658
+ SchemaTypes["Array"] = "Array";
4659
+ SchemaTypes["Boolean"] = "Boolean";
4660
+ SchemaTypes["Date"] = "Date";
4661
+ SchemaTypes["Number"] = "Number";
4662
+ SchemaTypes["Object"] = "Object";
4663
+ SchemaTypes["String"] = "String";
4664
+ SchemaTypes["Definition"] = "Definition";
4665
+ SchemaTypes["Function"] = "Function";
4666
+ SchemaTypes["Computed"] = "Computed";
4667
+ /**
4668
+ * Content in, reference out. The only type whose write shape differs from its stored
4669
+ * shape, and a leaf on purpose — see `SchemaFile`.
4670
+ */ SchemaTypes["File"] = "File";
4671
+ /**
4672
+ * A fixed-length list of numbers, carrying its dimension count — see `SchemaVector`.
4673
+ *
4674
+ * Value-shaped exactly like `s.array(s.number())`, which is why every array codegen
4675
+ * handler accepts it. It is a distinct type only so a backend can recognise it and store
4676
+ * it natively; nothing else needs to tell the two apart.
4677
+ */ SchemaTypes["Vector"] = "Vector";
4678
+ return SchemaTypes;
4679
+ }({});
4680
+ var HashType = /*#__PURE__*/ (/* unused pure expression or super */ null && (function(HashType) {
4681
+ HashType["Ids"] = "Ids";
4682
+ HashType["Object"] = "Object";
4683
+ return HashType;
4684
+ }({})));
4685
+
4686
+
4687
+ },
4688
+ 575(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4689
+ __webpack_require__.d(__webpack_exports__, {
4690
+ l: () => (isArrayValued)
4691
+ });
4692
+ /* import */ var _types__rspack_import_0 = __webpack_require__(537);
4693
+
4694
+ /**
4695
+ * Types whose runtime value is a JS array.
4696
+ *
4697
+ * `Array` and `Vector` are the same thing to every layer that copies, compares, hashes or
4698
+ * freezes a value — a vector is a list of numbers and nothing more. They differ only where a
4699
+ * backend chooses storage, which is the one place that should ask for `SchemaTypes.Vector` by
4700
+ * name.
4701
+ *
4702
+ * This exists so adding a third array-shaped type is one edit rather than a hunt through
4703
+ * twelve handlers. Missing one of those is silent in the worst way: a vector that clones by
4704
+ * reference is shared with the change tracker's copy, so overwriting an embedding produces no
4705
+ * diff and the save reports nothing to do.
4706
+ */ const ARRAY_VALUED_TYPES = new Set([
4707
+ _types__rspack_import_0/* .SchemaTypes.Array */.L.Array,
4708
+ _types__rspack_import_0/* .SchemaTypes.Vector */.L.Vector
4709
+ ]);
4710
+ /** 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);
4711
+ /**
4712
+ * True when the property's elements are primitives, so a spread is a sufficient copy.
4713
+ *
4714
+ * A vector is always numbers, so it never needs the per-element deep copy an array of objects
4715
+ * or dates does.
4716
+ */ const PRIMITIVE_ELEMENT_TYPES = new Set([
4717
+ _types__rspack_import_0/* .SchemaTypes.String */.L.String,
4718
+ _types__rspack_import_0/* .SchemaTypes.Number */.L.Number,
4719
+ _types__rspack_import_0/* .SchemaTypes.Boolean */.L.Boolean
4720
+ ]);
4721
+ const hasPrimitiveElements = (type, elementType)=>type === SchemaTypes.Vector || PRIMITIVE_ELEMENT_TYPES.has(elementType);
4722
+
4723
+
4724
+ },
4725
+ 894(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4726
+ __webpack_require__.d(__webpack_exports__, {
4727
+ T: () => (getStorageDateReviver)
4728
+ });
4729
+ /* import */ var _types__rspack_import_0 = __webpack_require__(537);
4730
+ /* import */ var _propertyKind__rspack_import_1 = __webpack_require__(575);
4731
+
4732
+
4733
+ const collectDatePaths = (properties, paths)=>{
4734
+ for (const property of properties){
4735
+ // The stored value belongs to whoever wrote it: a custom serializer, deserializer or
4736
+ // transform reads it back, and would be handed a Date it did not expect. Unmapped
4737
+ // properties are never stored.
4738
+ if (property.valueSerializer != null || property.valueDeserializer != null || property.transform != null || property.isUnmapped) {
4739
+ continue;
4740
+ }
4741
+ if (property.type === _types__rspack_import_0/* .SchemaTypes.Object */.L.Object) {
4742
+ collectDatePaths(property.children, paths);
4743
+ continue;
4744
+ }
4745
+ const isArray = (0,_propertyKind__rspack_import_1/* .isArrayValued */.l)(property.type) && property.innerSchema?.type === _types__rspack_import_0/* .SchemaTypes.Date */.L.Date;
4746
+ if (property.type !== _types__rspack_import_0/* .SchemaTypes.Date */.L.Date && isArray === false) {
4747
+ continue;
4748
+ }
4749
+ paths.push({
4750
+ segments: [
4751
+ ...property.getParentPathArray({
4752
+ useFromPropertyName: true
4753
+ }),
4754
+ property.getResolvedName()
4755
+ ],
4756
+ isArray
4757
+ });
4758
+ }
4759
+ };
4760
+ const reviveAt = (record, path)=>{
4761
+ const { segments } = path;
4762
+ let parent = record;
4763
+ for(let i = 0, length = segments.length - 1; i < length; i++){
4764
+ parent = parent[segments[i]];
4765
+ // An absent or null parent holds no date
4766
+ if (parent == null || typeof parent !== "object") {
4767
+ return;
4768
+ }
4769
+ }
4770
+ const key = segments[segments.length - 1];
4771
+ const value = parent[key];
4772
+ if (path.isArray === false) {
4773
+ if (typeof value === "string") {
4774
+ parent[key] = new Date(value);
4775
+ }
4776
+ return;
4777
+ }
4778
+ if (Array.isArray(value)) {
4779
+ for(let i = 0, length = value.length; i < length; i++){
4780
+ if (typeof value[i] === "string") {
4781
+ value[i] = new Date(value[i]);
4782
+ }
4783
+ }
4784
+ }
4785
+ };
4786
+ /** Per schema: `null` for a schema with no dates, so a caller can skip the pass. */ const revivers = new WeakMap();
4787
+ /**
4788
+ * The reviver for `schema`'s records, or `null` when it declares no dates.
4789
+ *
4790
+ * Built once per compiled schema. A read revives every row it returns, so the paths are resolved
4791
+ * here rather than per row.
4792
+ */ const getStorageDateReviver = (schema)=>{
4793
+ const cached = revivers.get(schema);
4794
+ if (cached !== undefined) {
4795
+ return cached;
4796
+ }
4797
+ const paths = [];
4798
+ collectDatePaths(schema.properties, paths);
4799
+ const reviver = paths.length === 0 ? null : (record)=>{
4800
+ for(let i = 0, length = paths.length; i < length; i++){
4801
+ reviveAt(record, paths[i]);
4802
+ }
4803
+ };
4804
+ revivers.set(schema, reviver);
4805
+ return reviver;
4806
+ };
2799
4807
 
2800
4808
 
2801
4809
  },
@@ -2889,12 +4897,14 @@ const isLogLevel = (value)=>typeof value === 'string' && LOG_LEVELS.includes(val
2889
4897
  const debug = process.env.DEBUG;
2890
4898
  if (debug === 'routier' || debug === '*') return 'debug';
2891
4899
  const env = "production"?.toLowerCase();
2892
- // `test` is deliberately absent. It used to be here, which meant no test suite anywhere
2893
- // could run Routier quietly. Opt in with DEBUG=routier or ROUTIER_LOG_LEVEL when a test
2894
- // needs the output.
2895
4900
  if (env === 'dev' || env === 'development') return 'debug';
2896
4901
  }
2897
- return 'silent';
4902
+ // Warnings are on unless something turns them off.
4903
+ //
4904
+ // Routier warns when a query returns correct rows a slower way than it could, or when a filter
4905
+ // compares types that can never match. Both are the caller's to act on, and a default of
4906
+ // `silent` meant the only people who ever saw them were the ones who already knew to look.
4907
+ return 'warn';
2898
4908
  };
2899
4909
  let level = resolveLevel();
2900
4910
  let rank = RANK[level];
@@ -3057,6 +5067,7 @@ __webpack_require__.r(__webpack_exports__);
3057
5067
 
3058
5068
  // EXPORTS
3059
5069
  __webpack_require__.d(__webpack_exports__, {
5070
+ describeFilters: () => (/* reexport */ describeFilters),
3060
5071
  DEFAULT_SEMI_JOIN_KEY_THRESHOLD: () => (/* reexport */ DEFAULT_SEMI_JOIN_KEY_THRESHOLD),
3061
5072
  TranslatedArrayValue: () => (/* reexport */ TranslatedArrayValue),
3062
5073
  collectingSink: () => (/* reexport */ collectingSink),
@@ -3064,15 +5075,18 @@ __webpack_require__.d(__webpack_exports__, {
3064
5075
  CacheDbPlugin: () => (/* reexport */ CacheDbPlugin),
3065
5076
  Query: () => (/* reexport */ Query),
3066
5077
  splitSendableOptions: () => (/* reexport */ splitSendableOptions),
5078
+ withInnerSide: () => (/* reexport */ withInnerSide),
3067
5079
  executeJoin: () => (/* reexport */ executeJoin),
3068
5080
  formatExplanation: () => (/* reexport */ formatExplanation),
3069
- serializePersistResult: () => (/* reexport */ serializePersistResult),
5081
+ parameteriseDocument: () => (/* reexport */ parameteriseDocument),
3070
5082
  mappedResultColumns: () => (/* reexport */ mappedResultColumns),
3071
5083
  explainQuery: () => (/* reexport */ explainQuery),
5084
+ parameter: () => (/* reexport */ parameter),
5085
+ serializePersistResult: () => (/* reexport */ serializePersistResult),
5086
+ applyInnerOptions: () => (/* reexport */ applyInnerOptions),
3072
5087
  cosineDistance: () => (/* reexport */ cosineDistance),
3073
5088
  RetryDbPlugin: () => (/* reexport */ RetryDbPlugin),
3074
5089
  BatchingDbPlugin: () => (/* reexport */ BatchingDbPlugin),
3075
- applyInnerOptions: () => (/* reexport */ applyInnerOptions),
3076
5090
  MEMORY_EXECUTION_EXPLANATIONS: () => (/* reexport */ MEMORY_EXECUTION_EXPLANATIONS),
3077
5091
  TupleTranslator: () => (/* reexport */ TupleTranslator),
3078
5092
  TranslatedSingleValue: () => (/* reexport */ TranslatedSingleValue),
@@ -3080,27 +5094,33 @@ __webpack_require__.d(__webpack_exports__, {
3080
5094
  TranslatedGroupValue: () => (/* reexport */ TranslatedGroupValue),
3081
5095
  deserializeBulkPersist: () => (/* reexport */ deserializeBulkPersist),
3082
5096
  EXECUTED_QUERIES_UNSUPPORTED: () => (/* reexport */ EXECUTED_QUERIES_UNSUPPORTED),
3083
- joinInPlugin: () => (/* reexport */ joinInPlugin),
5097
+ executedQueriesOf: () => (/* reexport */ executedQueriesOf),
3084
5098
  deserializePersistResult: () => (/* reexport */ deserializePersistResult),
5099
+ joinInPlugin: () => (/* reexport */ joinInPlugin),
3085
5100
  loggerSink: () => (/* reexport */ loggerSink),
3086
5101
  nearestBy: () => (/* reexport */ nearestBy),
3087
- readJoinKey: () => (/* reexport */ readJoinKey),
3088
5102
  hashJoin: () => (/* reexport */ hashJoin),
3089
5103
  EphemeralDataPlugin: () => (/* reexport */ EphemeralDataPlugin),
3090
5104
  QueryOptionsCollection: () => (/* reexport */ QueryOptionsCollection/* .QueryOptionsCollection */.H),
3091
- serializeBulkPersist: () => (/* reexport */ serializeBulkPersist),
3092
- toEntityShape: () => (/* reexport */ toEntityShape),
5105
+ describeUnparsableFilter: () => (/* reexport */ describeUnparsableFilter),
5106
+ readJoinKey: () => (/* reexport */ readJoinKey),
3093
5107
  loadJoinInnerSide: () => (/* reexport */ loadJoinInnerSide),
3094
5108
  DataTranslator: () => (/* reexport */ DataTranslator),
3095
- withExecutedQueries: () => (/* reexport */ withExecutedQueries),
5109
+ reportRenamedProperties: () => (/* reexport */ reportRenamedProperties),
5110
+ serializeBulkPersist: () => (/* reexport */ serializeBulkPersist),
3096
5111
  TelemetryDbPlugin: () => (/* reexport */ TelemetryDbPlugin),
5112
+ toEntityShape: () => (/* reexport */ toEntityShape),
3097
5113
  createRequestHandler: () => (/* reexport */ createRequestHandler),
3098
5114
  SqlTranslator: () => (/* reexport */ SqlTranslator),
5115
+ withExecutedQueries: () => (/* reexport */ withExecutedQueries),
3099
5116
  QueryOrdering: () => (/* reexport */ types_QueryOrdering),
5117
+ describeFilterAsJs: () => (/* reexport */ describeFilterAsJs),
3100
5118
  serializeQueryOptions: () => (/* reexport */ serializeQueryOptions),
3101
- distinctJoinKeys: () => (/* reexport */ distinctJoinKeys),
3102
5119
  JsonTranslator: () => (/* reexport */ JsonTranslator),
3103
- deserializeQueryOptions: () => (/* reexport */ deserializeQueryOptions)
5120
+ DATABASE_EXECUTION_EXPLANATIONS: () => (/* reexport */ DATABASE_EXECUTION_EXPLANATIONS),
5121
+ distinctJoinKeys: () => (/* reexport */ distinctJoinKeys),
5122
+ deserializeQueryOptions: () => (/* reexport */ deserializeQueryOptions),
5123
+ isDatabaseStep: () => (/* reexport */ isDatabaseStep)
3104
5124
  });
3105
5125
 
3106
5126
  ;// CONCATENATED MODULE: ./src/plugins/resultShape.ts
@@ -3209,6 +5229,10 @@ class DataTranslator {
3209
5229
  translate(data) {
3210
5230
  const isTransformed = this.query.options.hasTransformations();
3211
5231
  this.query.options.forEach((item)=>{
5232
+ // The plugin reported it could not run this one, so the memory pass owns it now.
5233
+ if (item.target === "database" && item.reason !== "executed") {
5234
+ return;
5235
+ }
3212
5236
  data = this.functionMap[item.name](data, item);
3213
5237
  });
3214
5238
  if (Array.isArray(data)) {
@@ -3621,7 +5645,7 @@ class Query {
3621
5645
  * Only a plugin that runs its outer query FIRST can supply these, and most run this loader
3622
5646
  * before anything else — so it is optional, and its absence costs a wider inner read rather
3623
5647
  * than a wrong one.
3624
- */ outerKeys)=>{
5648
+ */ outerKeys, /** Where the inner read reports what it executed. Defaults to the outer read's own list. */ innerExecutedQueries)=>{
3625
5649
  const joinOption = event.operation.options.getLast("join");
3626
5650
  if (joinOption == null) {
3627
5651
  done({
@@ -3649,9 +5673,10 @@ class Query {
3649
5673
  action: "query",
3650
5674
  reason: "join inner side",
3651
5675
  explain: event.explain,
3652
- // The same array the outer read pushes into, so a join reports BOTH reads in execution
3653
- // order. Built fresh rather than spread, so this has to be carried explicitly.
3654
- executedQueries: event.executedQueries
5676
+ // The caller decides where the inner read reports, because only it knows whether the inner
5677
+ // side is the SAME plugin where both reads belong in one explanation — or a different one,
5678
+ // where a PouchDB scan filed under SqliteDbPlugin is a lie.
5679
+ executedQueries: innerExecutedQueries ?? event.executedQueries
3655
5680
  };
3656
5681
  query(innerEvent, (result)=>{
3657
5682
  if (result.ok === Result/* .PluginEventResult.ERROR */.D.ERROR) {
@@ -3728,6 +5753,12 @@ class Query {
3728
5753
  return;
3729
5754
  }
3730
5755
  const outerRows = outerResult.data.value ?? [];
5756
+ if (at.reason !== "executed") {
5757
+ // The outer read reported something, so the database phase stopped before the join.
5758
+ // The datastore's own join branch pairs these rows.
5759
+ done(Result/* .PluginEventResult.success */.D.success(event.id, new TranslatedArrayValue(outerRows, false)));
5760
+ return;
5761
+ }
3731
5762
  // Storage shape: the plugin returns rows as it holds them, and deserialization is what
3732
5763
  // `executeJoin` does per side below.
3733
5764
  const outerKeys = distinctJoinKeys(outerRows, at.value.outerKey, at.value.semiJoinKeyThreshold, {
@@ -3843,9 +5874,7 @@ class JsonTranslator extends DataTranslator {
3843
5874
  if (field.property != null) {
3844
5875
  const value = field.property.getValue(data[i]);
3845
5876
  if (value != null) {
3846
- // Some types do not support deserialization (Array, Function, Computed, etc), just directly set the incoming value
3847
- const resolvedValue = field.property.supportsDeserialization ? field.property.deserialize(value) : value;
3848
- field.property.setValue(data[i], resolvedValue);
5877
+ field.property.setValue(data[i], field.property.deserialize(value));
3849
5878
  }
3850
5879
  }
3851
5880
  }
@@ -3869,9 +5898,7 @@ class JsonTranslator extends DataTranslator {
3869
5898
  if (field.property != null) {
3870
5899
  const value = field.property.getValue(data[i]);
3871
5900
  if (value != null) {
3872
- // Some types do not support deserialization (Array, Function, Computed, etc), just directly set the incoming value
3873
- const resolvedValue = field.property.supportsDeserialization ? field.property.deserialize(value) : value;
3874
- field.property.setValue(item, resolvedValue);
5901
+ field.property.setValue(item, field.property.deserialize(value));
3875
5902
  continue;
3876
5903
  }
3877
5904
  // The property exists, lets set it to the value (null/undefined)
@@ -4059,9 +6086,12 @@ class JsonTranslator extends DataTranslator {
4059
6086
  }
4060
6087
  }
4061
6088
 
6089
+ // EXTERNAL MODULE: ./src/schema/utils/storageDates.ts
6090
+ var storageDates = __webpack_require__(894);
4062
6091
  ;// CONCATENATED MODULE: ./src/plugins/translators/SqlTranslator.ts
4063
6092
 
4064
6093
 
6094
+
4065
6095
  /**
4066
6096
  * A stored vector as a list of numbers, whatever the driver handed back.
4067
6097
  *
@@ -4090,6 +6120,31 @@ class SqlTranslator extends DataTranslator {
4090
6120
  super(query);
4091
6121
  this.pushedDown = pushedDown;
4092
6122
  }
6123
+ /**
6124
+ * Dates back as Dates, before the caller's selectors run over the rows.
6125
+ *
6126
+ * A `group` key and a `map` are the caller's lambdas, run here over rows as the engine returned them.
6127
+ * SQLite, D1 and libSQL hand a date back as the TEXT it was stored as, which has no `getFullYear()`
6128
+ * and groups apart from the Date the entity holds. Revived at storage paths and in place, which the
6129
+ * datastore's deserialization still reads, and after `decodeJsonColumns`, so a date inside a JSON
6130
+ * column is revived too. Only a string is converted, so an engine that returns a Date (PostgreSQL,
6131
+ * PGlite, MySQL) is left alone, and so is a row already revived.
6132
+ *
6133
+ * Not a joined statement's rows, which are tuples, each half already deserialized. Nor rows whose
6134
+ * `group` or `map` was handed back, which the datastore runs after deserializing them.
6135
+ */ translate(data) {
6136
+ const runsHere = (name)=>this.query.options.get(name).some((item)=>item.option.target === "database" && item.option.reason === "executed");
6137
+ if (this.pushedDown.join !== true && this.query.schema != null && Array.isArray(data) && (runsHere("group") || runsHere("map"))) {
6138
+ const reviveDates = (0,storageDates/* .getStorageDateReviver */.T)(this.query.schema);
6139
+ for(let i = 0, length = reviveDates == null ? 0 : data.length; i < length; i++){
6140
+ const row = data[i];
6141
+ if (row != null && typeof row === "object") {
6142
+ reviveDates(row);
6143
+ }
6144
+ }
6145
+ }
6146
+ return super.translate(data);
6147
+ }
4093
6148
  count(data, _) {
4094
6149
  if (Array.isArray(data) && data.length > 0) {
4095
6150
  // Count is returned as the property alias on the query.
@@ -4212,9 +6267,12 @@ class SqlTranslator extends DataTranslator {
4212
6267
  for(let j = 0, l = option.value.fields.length; j < l; j++){
4213
6268
  const field = option.value.fields[j];
4214
6269
  if (field.property != null) {
4215
- const value = field.property.getValue(data[i]);
6270
+ const row = data[i];
6271
+ // A nested field arrives FLAT, under the alias the statement emitted, because
6272
+ // the value was read out of a JSON column. `setValue` puts it back on its path.
6273
+ const value = Object.prototype.hasOwnProperty.call(row, field.sourceName) ? row[field.sourceName] : field.property.getValue(row);
4216
6274
  if (value != null) {
4217
- field.property.setValue(data[i], field.property.deserialize(value));
6275
+ field.property.setValue(row, field.property.deserialize(value));
4218
6276
  }
4219
6277
  }
4220
6278
  }
@@ -4351,6 +6409,183 @@ class SqlTranslator extends DataTranslator {
4351
6409
 
4352
6410
 
4353
6411
 
6412
+ // EXTERNAL MODULE: ./src/expressions/callSource.ts
6413
+ var callSource = __webpack_require__(429);
6414
+ ;// CONCATENATED MODULE: ./src/plugins/query/describeFilter.ts
6415
+
6416
+
6417
+ const COMPARATOR_OPERATORS = {
6418
+ "equals": "===",
6419
+ "greater-than": ">",
6420
+ "greater-than-equals": ">=",
6421
+ "less-than": "<",
6422
+ "less-than-equals": "<="
6423
+ };
6424
+ /** The three comparators that read as a method call rather than an operator. */ const COMPARATOR_METHODS = {
6425
+ "starts-with": "startsWith",
6426
+ "includes": "includes",
6427
+ "ends-with": "endsWith"
6428
+ };
6429
+ const renderProperty = (property)=>property.property.getPathArray().join(".");
6430
+ /**
6431
+ * The predicate as JavaScript, with every value replaced by `?`.
6432
+ *
6433
+ * Rendered from the parsed tree rather than from the function's source. The tree is what the
6434
+ * backend was actually given, so this cannot drift from what ran; and a value reaching the tree
6435
+ * as a literal is indistinguishable from one arriving through a params object, which is what
6436
+ * makes both come out as `?` the way SQL treats them.
6437
+ */ const describeFilterAsJs = (expression)=>{
6438
+ const parameters = [];
6439
+ const hold = (value)=>{
6440
+ parameters.push(value);
6441
+ return "?";
6442
+ };
6443
+ const side = (part)=>{
6444
+ if (part == null) {
6445
+ return "?";
6446
+ }
6447
+ if ((0,assertions.isPropertyExpression)(part)) {
6448
+ return renderProperty(part);
6449
+ }
6450
+ if ((0,assertions.isValueExpression)(part)) {
6451
+ return hold(part.value);
6452
+ }
6453
+ if ((0,assertions.isCallExpression)(part)) {
6454
+ return (0,callSource/* .renderCallAsJs */.a)(part.call, ()=>side(part.expression), ()=>part.arguments.map(side));
6455
+ }
6456
+ return walk(part);
6457
+ };
6458
+ const walk = (current)=>{
6459
+ if ((0,assertions.isOperatorExpression)(current)) {
6460
+ const operator = current.operator === "&&" ? "&&" : "||";
6461
+ return `(${side(current.left)} ${operator} ${side(current.right)})`;
6462
+ }
6463
+ if ((0,assertions.isComparatorExpression)(current)) {
6464
+ const method = COMPARATOR_METHODS[current.comparator];
6465
+ // Evaluated LEFT then RIGHT, always: the parameter order has to match the reading
6466
+ // order of the text, or the values line up against the wrong placeholders.
6467
+ const left = side(current.left);
6468
+ const right = side(current.right);
6469
+ if (method != null) {
6470
+ const call = `${left}.${method}(${right})`;
6471
+ return current.negated ? `${call} === false` : call;
6472
+ }
6473
+ const symbol = COMPARATOR_OPERATORS[current.comparator];
6474
+ if (symbol == null) {
6475
+ return `${left} ${current.comparator} ${right}`;
6476
+ }
6477
+ return `${left} ${current.negated ? negate(symbol) : symbol} ${right}`;
6478
+ }
6479
+ if ((0,assertions.isCallExpression)(current)) {
6480
+ return (0,callSource/* .renderCallAsJs */.a)(current.call, ()=>side(current.expression), ()=>current.arguments.map(side));
6481
+ }
6482
+ if (current.type === "empty") {
6483
+ return "(no filter)";
6484
+ }
6485
+ return current.type === "not-parsable" ? "(not parsable)" : `(unsupported: ${current.type})`;
6486
+ };
6487
+ return {
6488
+ text: walk(expression),
6489
+ parameters
6490
+ };
6491
+ };
6492
+ const negate = (symbol)=>{
6493
+ switch(symbol){
6494
+ case "===":
6495
+ return "!==";
6496
+ case ">":
6497
+ return "<=";
6498
+ case ">=":
6499
+ return "<";
6500
+ case "<":
6501
+ return ">=";
6502
+ case "<=":
6503
+ return ">";
6504
+ default:
6505
+ return `!${symbol}`;
6506
+ }
6507
+ };
6508
+ /**
6509
+ * Marks a value inside a query document so it is replaced by `?` rather than printed.
6510
+ *
6511
+ * A document language carries its values inline, so there is nothing in the shape itself to say
6512
+ * which parts are operators and which are data. A dialect wraps the data as it builds the
6513
+ * document, and `parameteriseDocument` reads the wrapper.
6514
+ */ const PARAMETER = Symbol("routier.parameter");
6515
+ const parameter = (value)=>({
6516
+ [PARAMETER]: value
6517
+ });
6518
+ const isParameter = (value)=>typeof value === "object" && value !== null && PARAMETER in value;
6519
+ /**
6520
+ * Renders a query DOCUMENT with its values replaced by `?`.
6521
+ *
6522
+ * Language-agnostic on purpose: an MQL filter and a Mango selector are both plain objects, and so
6523
+ * is whatever a future document store wants reported. The dialect decides the shape; this only
6524
+ * decides how it is written down.
6525
+ *
6526
+ * A value not wrapped by `parameter` is structural — an operator name, a field path, a nesting
6527
+ * level — and is printed as it is. That is the whole distinction, and it has to be made where the
6528
+ * document is built, because by the time it is an object the two are the same kind of thing.
6529
+ */ const parameteriseDocument = (document)=>{
6530
+ const parameters = [];
6531
+ const render = (value)=>{
6532
+ if (isParameter(value)) {
6533
+ parameters.push(value[PARAMETER]);
6534
+ return "?";
6535
+ }
6536
+ if (Array.isArray(value)) {
6537
+ return `[${value.map(render).join(", ")}]`;
6538
+ }
6539
+ if (typeof value === "object" && value !== null) {
6540
+ const entries = Object.entries(value).map(([key, nested])=>`${JSON.stringify(key)}: ${render(nested)}`);
6541
+ return `{ ${entries.join(", ")} }`;
6542
+ }
6543
+ return JSON.stringify(value) ?? String(value);
6544
+ };
6545
+ return {
6546
+ text: render(document),
6547
+ parameters
6548
+ };
6549
+ };
6550
+ /**
6551
+ * Every filter on a query, as one description.
6552
+ *
6553
+ * Filters accumulate — `.where(a).where(b)` is `a && b` — so they are reported as one predicate
6554
+ * rather than several, which is how the caller thinks of them and how a SQL plugin renders them
6555
+ * into one `WHERE`. Parameters run left to right across the whole thing, matching the text.
6556
+ *
6557
+ * A filter that could not be parsed falls back to its source. Mixing the two is deliberate: one
6558
+ * unparsable filter does not make the others unreadable, and seeing which one it was is the
6559
+ * point.
6560
+ */ const describeFilters = (filters)=>{
6561
+ const parameters = [];
6562
+ const parts = filters.map((entry)=>{
6563
+ const described = entry.expression?.type === "not-parsable" ? describeUnparsableFilter(entry.filter, entry.expression.reason) : describeFilterAsJs(entry.expression);
6564
+ parameters.push(...described.parameters);
6565
+ return described.text;
6566
+ });
6567
+ if (parts.length === 0) {
6568
+ return {
6569
+ text: "(no filter)",
6570
+ parameters: []
6571
+ };
6572
+ }
6573
+ return {
6574
+ text: parts.length === 1 ? parts[0] : parts.join(" && "),
6575
+ parameters
6576
+ };
6577
+ };
6578
+ /**
6579
+ * A predicate core could not parse, shown as the caller wrote it.
6580
+ *
6581
+ * This is the case where the source matters most: an unparsable filter is why the query did not
6582
+ * push down, and the reason codes say that it happened without showing what it was. There are no
6583
+ * parameters — nothing was extracted, because nothing was understood.
6584
+ */ const describeUnparsableFilter = (filter, reason)=>({
6585
+ text: typeof filter === "function" ? `${String(filter)} — ${reason ?? "could not be parsed"}, evaluated in memory` : "(not parsable)",
6586
+ parameters: []
6587
+ });
6588
+
4354
6589
  ;// CONCATENATED MODULE: ./src/plugins/query/explain.ts
4355
6590
 
4356
6591
  /**
@@ -4361,16 +6596,26 @@ class SqlTranslator extends DataTranslator {
4361
6596
  */ const MEMORY_EXECUTION_EXPLANATIONS = {
4362
6597
  "not-parsable": "A filter could not be parsed into an expression tree, so it and every option after it run in memory.",
4363
6598
  "unmapped-property": "The property is not stored in the database, so it can only be read after deserialization.",
4364
- "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.",
4365
6599
  "map-rename": "A map renames or drops properties, so every option after it refers to names the database does not have.",
4366
6600
  "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.",
4367
6601
  "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.",
4368
- "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."
6602
+ "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.",
6603
+ "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.",
6604
+ "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."
4369
6605
  };
4370
6606
  const EXECUTED_QUERIES_UNSUPPORTED = "This plugin did not report what it executed. It may not support explain.";
4371
- const DATABASE_STEP_DESCRIPTION = "These options are sent to the plugin.";
4372
- const MEMORY_STEP_DESCRIPTION = "Routier runs these over the rows the database returned, after deserializing them.";
4373
- const UNNARROWED_READ_DESCRIPTION = "No option could be pushed down, so the plugin reads the whole collection.";
6607
+ /**
6608
+ * Why an option planned for the database did not run there. `executed` has no sentence: it needs no
6609
+ * explaining, and a step made of executed options is a database step like any other.
6610
+ */ const DATABASE_EXECUTION_EXPLANATIONS = {
6611
+ "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.",
6612
+ "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.",
6613
+ "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."
6614
+ };
6615
+ /**
6616
+ * TypeScript does not narrow a union from a discriminant nested inside a property, so the two kinds
6617
+ * of step need a guard rather than an inline check.
6618
+ */ const isDatabaseStep = (step)=>step.executedIn.kind === "database";
4374
6619
  /**
4375
6620
  * The reportable shape of one option's value.
4376
6621
  *
@@ -4451,12 +6696,16 @@ const explainedOptionsOf = (options)=>{
4451
6696
  options.forEach((option)=>explained.push(explainedOptionOf(option, index++)));
4452
6697
  return explained;
4453
6698
  };
6699
+ /** Every sentence, whoever decided — the summary reads the same either way. */ const EXPLANATIONS = {
6700
+ ...MEMORY_EXECUTION_EXPLANATIONS,
6701
+ ...DATABASE_EXECUTION_EXPLANATIONS
6702
+ };
4454
6703
  const summarize = (steps)=>{
4455
6704
  const reasons = [];
4456
6705
  let database = 0;
4457
6706
  let memory = 0;
4458
6707
  for (const step of steps){
4459
- if (step.executedIn === "database") {
6708
+ if (isDatabaseStep(step)) {
4460
6709
  database += step.options.length;
4461
6710
  continue;
4462
6711
  }
@@ -4466,7 +6715,7 @@ const summarize = (steps)=>{
4466
6715
  }
4467
6716
  }
4468
6717
  const counts = `${database} ${database === 1 ? "option ran" : "options ran"} in the database, ${memory} ran in memory.`;
4469
- const causes = reasons.map((reason)=>MEMORY_EXECUTION_EXPLANATIONS[reason]).join(" ");
6718
+ const causes = reasons.map((reason)=>EXPLANATIONS[reason]).join(" ");
4470
6719
  return {
4471
6720
  database,
4472
6721
  memory,
@@ -4474,50 +6723,87 @@ const summarize = (steps)=>{
4474
6723
  explanation: causes.length === 0 ? counts : `${counts} ${causes}`
4475
6724
  };
4476
6725
  };
4477
- /**
4478
- * Groups options into consecutive runs that execute in the same place.
4479
- *
4480
- * A step boundary is where execution moves, and a reader has to see the statement as step 1 OF
4481
- * 2 to understand it is not the whole query. Cutting over to memory is a ratchet, so the
4482
- * database options are always a prefix and there are at most two steps.
4483
- *
4484
- * A database step is emitted even when NO option pushed down, because the plugin is dispatched
4485
- * either way — `createQueryPayload` always builds a database event. Without it, the worst case
4486
- * the feature exists to expose reports "0 in the database" while the backend reads the whole
4487
- * table, which is the opposite of the truth.
4488
- */ const toExecutionSteps = (options)=>{
6726
+ const outcomeOf = (option)=>{
6727
+ if (option.target === "memory") {
6728
+ return {
6729
+ executedIn: "memory",
6730
+ reason: option.reason,
6731
+ explanation: MEMORY_EXECUTION_EXPLANATIONS[option.reason]
6732
+ };
6733
+ }
6734
+ if (option.reason === "executed") {
6735
+ return {
6736
+ executedIn: "database",
6737
+ reason: null,
6738
+ explanation: null
6739
+ };
6740
+ }
6741
+ return {
6742
+ executedIn: "memory",
6743
+ reason: option.reason,
6744
+ explanation: DATABASE_EXECUTION_EXPLANATIONS[option.reason]
6745
+ };
6746
+ };
6747
+ /** Whether an option belongs to the step already open, or starts a new one. */ const continuesStep = (current, outcome)=>{
6748
+ if (current == null) {
6749
+ return false;
6750
+ }
6751
+ if (current.executedIn.kind === "database") {
6752
+ return outcome.reason == null;
6753
+ }
6754
+ // `?? null` because a step with no reason omits the key, and `undefined === null` is false —
6755
+ // without it every option started a step of its own
6756
+ return outcome.reason != null && (current.reason ?? null) === outcome.reason;
6757
+ };
6758
+ const toExecutionSteps = (options, ranIn)=>{
4489
6759
  const steps = [];
4490
6760
  let index = 0;
4491
6761
  options.forEach((option)=>{
4492
6762
  const explained = explainedOptionOf(option, index++);
4493
6763
  const current = steps[steps.length - 1];
4494
- if (current != null && current.executedIn === option.target) {
6764
+ const outcome = outcomeOf(option);
6765
+ // Grouped by outcome, not by target: an option the database could not express and one core
6766
+ // sent to memory both run in memory, for different reasons a reader needs told apart.
6767
+ if (continuesStep(current, outcome) === true) {
4495
6768
  current.options.push(explained);
4496
6769
  return;
4497
6770
  }
4498
- steps.push({
4499
- step: steps.length + 1,
6771
+ steps.push(outcome.reason == null ? {
6772
+ step: 0,
4500
6773
  of: 0,
4501
- executedIn: option.target,
4502
- description: option.target === "database" ? DATABASE_STEP_DESCRIPTION : MEMORY_STEP_DESCRIPTION,
6774
+ executedIn: ranIn,
4503
6775
  options: [
4504
6776
  explained
4505
6777
  ],
4506
- ...option.reason == null ? {} : {
4507
- reason: option.reason,
4508
- explanation: MEMORY_EXECUTION_EXPLANATIONS[option.reason]
4509
- }
6778
+ executedQueries: []
6779
+ } : {
6780
+ step: 0,
6781
+ of: 0,
6782
+ executedIn: {
6783
+ kind: "memory"
6784
+ },
6785
+ options: [
6786
+ explained
6787
+ ],
6788
+ reason: outcome.reason,
6789
+ explanation: outcome.explanation ?? undefined
4510
6790
  });
4511
6791
  });
4512
- if (steps[0]?.executedIn !== "database") {
6792
+ // A database step even when nothing pushed down: the plugin is dispatched either way, so
6793
+ // reporting "0 in the database" while the backend reads the whole table is the opposite of
6794
+ // the truth.
6795
+ if (steps[0]?.executedIn.kind !== "database") {
4513
6796
  steps.unshift({
4514
6797
  step: 0,
4515
6798
  of: 0,
4516
- executedIn: "database",
4517
- description: UNNARROWED_READ_DESCRIPTION,
4518
- options: []
6799
+ executedIn: ranIn,
6800
+ options: [],
6801
+ executedQueries: []
4519
6802
  });
4520
6803
  }
6804
+ return steps;
6805
+ };
6806
+ /** Numbers a finished list, so `step 1 of 3` reads as the shape of the whole query. */ const numbered = (steps)=>{
4521
6807
  for(let i = 0; i < steps.length; i++){
4522
6808
  steps[i].step = i + 1;
4523
6809
  steps[i].of = steps.length;
@@ -4532,10 +6818,11 @@ const summarize = (steps)=>{
4532
6818
  * post-join filter alone in the memory half derives back to `"database"`, and the document
4533
6819
  * would report memory work as having run in the database.
4534
6820
  */ const explainQuery = (options, context)=>{
4535
- if (options.isDerived === true) {
4536
- 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.");
4537
- }
4538
- const executionSteps = toExecutionSteps(options);
6821
+ const executionSteps = numbered(toExecutionSteps(options, {
6822
+ kind: "database",
6823
+ database: context.database,
6824
+ plugin: context.pluginKind
6825
+ }));
4539
6826
  return {
4540
6827
  collection: context.collection,
4541
6828
  database: context.database,
@@ -4562,7 +6849,7 @@ const summarize = (steps)=>{
4562
6849
  // Only the first database step: a plugin reports what IT ran, and everything it ran
4563
6850
  // was sent as one dispatch. Stamping the same statements onto a second database step
4564
6851
  // would claim they ran twice.
4565
- if (step.executedIn !== "database" || attached === true) {
6852
+ if (isDatabaseStep(step) === false || attached === true) {
4566
6853
  return {
4567
6854
  ...step,
4568
6855
  options: [
@@ -4595,8 +6882,47 @@ const summarize = (steps)=>{
4595
6882
  executionSteps
4596
6883
  };
4597
6884
  };
6885
+ /**
6886
+ * Adds the step for a cross-plugin join's inner side.
6887
+ *
6888
+ * Appended by the executor rather than derived from the options, because the inner side's options
6889
+ * live on the join, in its own collection, and were never part of this query's chain. It goes before
6890
+ * the memory steps that consume it — the join cannot run until both sides are read.
6891
+ */ /**
6892
+ * Every statement the query ran, across every database it touched, in execution order.
6893
+ *
6894
+ * A step is a place, so the statements live on the steps — this is for a caller that wants them all
6895
+ * without caring which plugin ran which.
6896
+ */ const executedQueriesOf = (explanation)=>explanation.executionSteps.flatMap((step)=>isDatabaseStep(step) ? step.executedQueries : []);
6897
+ const withInnerSide = (explanation, innerSide)=>{
6898
+ const step = {
6899
+ step: 0,
6900
+ of: 0,
6901
+ executedIn: {
6902
+ kind: "database",
6903
+ database: innerSide.database,
6904
+ plugin: innerSide.plugin
6905
+ },
6906
+ options: [],
6907
+ executedQueries: innerSide.executedQueries
6908
+ };
6909
+ const firstMemory = explanation.executionSteps.findIndex((current)=>isDatabaseStep(current) === false);
6910
+ const at = firstMemory === -1 ? explanation.executionSteps.length : firstMemory;
6911
+ const executionSteps = numbered([
6912
+ ...explanation.executionSteps.slice(0, at),
6913
+ step,
6914
+ ...explanation.executionSteps.slice(at)
6915
+ ]);
6916
+ return {
6917
+ ...explanation,
6918
+ executionSteps,
6919
+ summary: summarize(executionSteps)
6920
+ };
6921
+ };
4598
6922
 
4599
6923
  ;// CONCATENATED MODULE: ./src/plugins/query/formatExplanation.ts
6924
+
6925
+
4600
6926
  const OPTION_LABEL_WIDTH = 8;
4601
6927
  const WRAP_WIDTH = 68;
4602
6928
  /** Wraps `text` to `WRAP_WIDTH`, prefixing every line with `indent`. */ const wrap = (text, indent)=>{
@@ -4622,29 +6948,41 @@ const COMPARATOR_SYMBOLS = {
4622
6948
  "less-than": "<",
4623
6949
  "less-than-equals": "<="
4624
6950
  };
4625
- const describeValue = (value)=>{
4626
- if (value == null) {
6951
+ /** Typed against the union so a new OBJECT tag is a compile error here, not an "undefined" in output. */ const describeValue = (value)=>{
6952
+ if (value === null) {
6953
+ return "null";
6954
+ }
6955
+ if (value === undefined) {
4627
6956
  return "?";
4628
6957
  }
4629
- if (value.k === "raw") {
4630
- return typeof value.v === "string" ? `"${value.v}"` : String(value.v);
6958
+ if (Array.isArray(value)) {
6959
+ return `[${value.map(describeValue).join(", ")}]`;
6960
+ }
6961
+ if (typeof value !== "object") {
6962
+ return typeof value === "string" ? `"${value}"` : String(value);
6963
+ }
6964
+ if ("date" in value) {
6965
+ return value.date;
4631
6966
  }
4632
- if (value.k === "date") {
4633
- return value.v;
6967
+ if ("undefined" in value) {
6968
+ return "undefined";
4634
6969
  }
4635
- if (value.k === "array") {
4636
- return `[${value.v.map(describeValue).join(", ")}]`;
6970
+ if ("regex" in value) {
6971
+ return `/${value.regex.source}/${value.regex.flags}`;
4637
6972
  }
4638
- return value.k === "undefined" ? "undefined" : String(value.v);
6973
+ if ("bigint" in value) {
6974
+ return `${value.bigint}n`;
6975
+ }
6976
+ return value.number;
4639
6977
  };
4640
6978
  /** Renders a serialized expression back to something close to the source predicate. */ const describeExpression = (expression)=>{
4641
6979
  if (expression == null) {
4642
6980
  return "?";
4643
6981
  }
4644
- if (expression.t === "operator") {
6982
+ if (expression.type === "operator") {
4645
6983
  return `${describeExpression(expression.left)} ${expression.operator} ${describeExpression(expression.right)}`;
4646
6984
  }
4647
- if (expression.t === "comparator") {
6985
+ if (expression.type === "comparator") {
4648
6986
  const left = describeExpression(expression.left);
4649
6987
  const right = describeExpression(expression.right);
4650
6988
  const symbol = COMPARATOR_SYMBOLS[expression.comparator];
@@ -4653,13 +6991,24 @@ const describeValue = (value)=>{
4653
6991
  }
4654
6992
  return `${left} ${expression.negated === true ? "!==" : symbol} ${right}`;
4655
6993
  }
4656
- if (expression.t === "property") {
6994
+ if (expression.type === "property") {
4657
6995
  return expression.path;
4658
6996
  }
4659
- if (expression.t === "value") {
6997
+ if (expression.type === "value") {
4660
6998
  return describeValue(expression.value);
4661
6999
  }
4662
- return expression.t === "empty" ? "(no filter)" : "(not parsable)";
7000
+ if (expression.type === "call") {
7001
+ return (0,callSource/* .renderCallAsJs */.a)(expression.call, ()=>describeExpression(expression.expression), ()=>(expression.arguments ?? []).map(describeExpression));
7002
+ }
7003
+ if (expression.type === "empty") {
7004
+ return "(no filter)";
7005
+ }
7006
+ // Distinguishable from "(not parsable)", which means the parser gave up and this runs in memory
7007
+ if (expression.type === "not-parsable") {
7008
+ return expression.reason == null ? "(not parsable)" : `(not parsable: ${expression.reason})`;
7009
+ }
7010
+ // Unreachable while the union is exhausted above; a payload from a newer sender is not.
7011
+ return `(unsupported: ${expression.type})`;
4663
7012
  };
4664
7013
  const describeOption = (option)=>{
4665
7014
  const detail = option.detail;
@@ -4687,18 +7036,35 @@ const describeOption = (option)=>{
4687
7036
  }
4688
7037
  return "";
4689
7038
  };
7039
+ /**
7040
+ * The sentence for a kind of step.
7041
+ *
7042
+ * Here rather than on the step: it is one of two constants keyed off `executedIn`, so carrying it in
7043
+ * the payload put prose beside the field it was derived from.
7044
+ */ const DATABASE_STEP_DESCRIPTION = "These options are sent to the plugin.";
7045
+ const MEMORY_STEP_DESCRIPTION = "Routier runs these over the rows the database returned, after deserializing them.";
7046
+ const UNNARROWED_READ_DESCRIPTION = "No option could be pushed down, so the plugin reads the whole collection.";
7047
+ /** `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";
4690
7048
  const formatStep = (step, lines)=>{
4691
- const reason = step.reason == null ? "" : ` [${step.reason}]`;
4692
- lines.push(` STEP ${step.step} of ${step.of} — ${step.executedIn}${reason}`);
4693
- lines.push(...wrap(step.description, " "));
4694
- if (step.explanation != null) {
4695
- lines.push(...wrap(step.explanation, " "));
7049
+ const reason = isDatabaseStep(step) || step.reason == null ? "" : ` [${step.reason}]`;
7050
+ lines.push(` STEP ${step.step} of ${step.of} — ${whereItRan(step)}${reason}`);
7051
+ if (isDatabaseStep(step)) {
7052
+ lines.push(...wrap(step.options.length === 0 ? UNNARROWED_READ_DESCRIPTION : DATABASE_STEP_DESCRIPTION, " "));
7053
+ } else {
7054
+ lines.push(...wrap(MEMORY_STEP_DESCRIPTION, " "));
7055
+ if (step.explanation != null) {
7056
+ lines.push(...wrap(step.explanation, " "));
7057
+ }
4696
7058
  }
4697
7059
  lines.push("");
4698
7060
  for (const option of step.options){
4699
7061
  lines.push(` ${option.name.padEnd(OPTION_LABEL_WIDTH)} ${describeOption(option)}`.trimEnd());
4700
7062
  }
4701
- for (const executed of step.executedQueries ?? []){
7063
+ if (isDatabaseStep(step) === false) {
7064
+ lines.push("");
7065
+ return;
7066
+ }
7067
+ for (const executed of step.executedQueries){
4702
7068
  lines.push("");
4703
7069
  lines.push(...executed.text.split("\n").map((line)=>` ${line}`));
4704
7070
  if (executed.parameters != null && executed.parameters.length > 0) {
@@ -4731,6 +7097,80 @@ const formatStep = (step, lines)=>{
4731
7097
  return lines.join("\n");
4732
7098
  };
4733
7099
 
7100
+ // EXTERNAL MODULE: ./src/expressions/utils.ts
7101
+ var utils = __webpack_require__(63);
7102
+ ;// CONCATENATED MODULE: ./src/plugins/query/renames.ts
7103
+
7104
+
7105
+ const PROPERTY_READING_OPTIONS = [
7106
+ "filter",
7107
+ "sort",
7108
+ "nearest",
7109
+ "map",
7110
+ "group"
7111
+ ];
7112
+ const namesRenamedProperty = (expression)=>{
7113
+ let found = false;
7114
+ if (expression == null) {
7115
+ return found;
7116
+ }
7117
+ (0,utils/* .forEach */.jJ)(expression, (node)=>{
7118
+ if ((0,assertions.isPropertyExpression)(node) && node.property.hasRenamedSegments) {
7119
+ found = true;
7120
+ return false;
7121
+ }
7122
+ return true;
7123
+ });
7124
+ return found;
7125
+ };
7126
+ const isRenamed = (property)=>property != null && property.hasRenamedSegments;
7127
+ /**
7128
+ * Whether a selector's value is read from a renamed property, whether it is that property or computed
7129
+ * from it: `r => r.dueDate.getTime()` reads `dueDate` all the same. Every property it reads when the
7130
+ * selector was parsed, and otherwise the property recorded for it.
7131
+ */ const readsRenamedValue = (value)=>value != null && (value.reads != null ? value.reads.some(isRenamed) : isRenamed(value.property));
7132
+ const readsRenamedField = (fields)=>fields != null && fields.some(readsRenamedValue);
7133
+ const readsRenamedProperty = (name, value)=>{
7134
+ switch(name){
7135
+ case "filter":
7136
+ return namesRenamedProperty(value.expression);
7137
+ case "map":
7138
+ // A projection reads each field it selects
7139
+ return readsRenamedField(value.fields);
7140
+ case "group":
7141
+ // A group reads its key, then copies every field of the row into its members: every schema
7142
+ // property, or what a `map` before it selected
7143
+ return readsRenamedValue(value.key) || readsRenamedField(value.fields);
7144
+ default:
7145
+ return readsRenamedValue(value);
7146
+ }
7147
+ };
7148
+ /**
7149
+ * Hands back every option over a property stored under a `.from()` name, for the datastore to run
7150
+ * in memory.
7151
+ *
7152
+ * Core keeps such an option with the database, because only the plugin knows whether its backend
7153
+ * reads storage names. One that translates the option — SQL renders the column from
7154
+ * `getResolvedName()` — needs nothing from here. One that runs the caller's lambda over rows as it
7155
+ * stores them reads a key the row does not have, and answers wrongly without an error: that plugin
7156
+ * calls this before it reads anything, and the datastore finishes the query after deserialization,
7157
+ * where the in-memory names exist.
7158
+ *
7159
+ * Reported as `missing-capability`: the backend cannot express the option as written, and like
7160
+ * every capability, that is only knowable by the plugin.
7161
+ *
7162
+ * @param names Which options to check, for a plugin that resolves some of them itself — Mongo renders
7163
+ * filters and sorts through the stored path, and runs `nearest`, `map` and `group` in JavaScript.
7164
+ */ const reportRenamedProperties = (options, names = PROPERTY_READING_OPTIONS)=>{
7165
+ for (const name of names){
7166
+ for (const item of options.get(name)){
7167
+ if (readsRenamedProperty(name, item.option.value)) {
7168
+ options.reportMissingCapability(item);
7169
+ }
7170
+ }
7171
+ }
7172
+ };
7173
+
4734
7174
  ;// CONCATENATED MODULE: ./src/plugins/query/types.ts
4735
7175
  var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4736
7176
  QueryOrdering["Descending"] = "desc";
@@ -4747,8 +7187,12 @@ var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4747
7187
 
4748
7188
 
4749
7189
 
7190
+
7191
+
4750
7192
  // EXTERNAL MODULE: ./src/expressions/evaluate.ts
4751
7193
  var evaluate = __webpack_require__(379);
7194
+ // EXTERNAL MODULE: ./src/expressions/fold.ts
7195
+ var fold = __webpack_require__(43);
4752
7196
  ;// CONCATENATED MODULE: ./src/plugins/wire/query.ts
4753
7197
 
4754
7198
 
@@ -4758,6 +7202,9 @@ var evaluate = __webpack_require__(379);
4758
7202
  * Everything except `map` and `group`. Those two are defined BY a closure — the projection is the
4759
7203
  * option — and no data form of them exists to send. Nothing else needs its closure: a sort is a
4760
7204
  * property and a direction, a filter is an expression tree, `nearest` is a vector and a count.
7205
+ *
7206
+ * Except a sort or `nearest` whose selector computes its value, such as `x => x.name.length`. It is sent
7207
+ * as the property it reads, and the receiver would order by that instead. See `isSendable`.
4761
7208
  */ const SENDABLE = new Set([
4762
7209
  "skip",
4763
7210
  "take",
@@ -4771,6 +7218,7 @@ var evaluate = __webpack_require__(379);
4771
7218
  "sum",
4772
7219
  "distinct"
4773
7220
  ]);
7221
+ const isSendable = (name, value)=>SENDABLE.has(name) && (name !== "sort" && name !== "nearest" || value.isDirectProperty !== false);
4774
7222
  /**
4775
7223
  * Splits options into the PREFIX that can be sent and the remainder that cannot.
4776
7224
  *
@@ -4786,7 +7234,12 @@ var evaluate = __webpack_require__(379);
4786
7234
  const local = new QueryOptionsCollection/* .QueryOptionsCollection */.H();
4787
7235
  let stopped = false;
4788
7236
  options.forEach((option)=>{
4789
- if (stopped === false && SENDABLE.has(option.name) === false) {
7237
+ // Reported by the plugin, so it belongs to the datastore, and so does everything after it —
7238
+ // a report cascades to the end of the database phase, which keeps what is left a prefix
7239
+ if (option.target === "database" && option.reason !== "executed") {
7240
+ return;
7241
+ }
7242
+ if (stopped === false && isSendable(option.name, option.value) === false) {
4790
7243
  stopped = true;
4791
7244
  }
4792
7245
  (stopped ? local : sendable).add(option.name, option.value);
@@ -4949,7 +7402,7 @@ const serializeQueryOptions = (options)=>{
4949
7402
  }
4950
7403
  case "filter":
4951
7404
  {
4952
- const expression = types/* .Expression.fromJson */.r4.fromJson(option.value.expression, schema);
7405
+ const expression = (0,fold/* .foldConstantCalls */.F5)(types/* .Expression.fromJson */.r4.fromJson(option.value.expression, schema));
4953
7406
  options.add("filter", {
4954
7407
  filter: (0,evaluate/* .toStrictPredicate */.wS)(expression),
4955
7408
  expression,
@@ -5633,7 +8086,8 @@ var TrampolinePipeline = __webpack_require__(416);
5633
8086
  if (!(0,assertions.isPropertyExpression)(left) || !(0,assertions.isValueExpression)(right)) {
5634
8087
  return null;
5635
8088
  }
5636
- if (left.property.isKey !== true || left.transformer != null || right.value == null) {
8089
+ // A called property is a CallExpression, so it fails the isPropertyExpression check above
8090
+ if (left.property.isKey !== true || right.value == null) {
5637
8091
  return null;
5638
8092
  }
5639
8093
  return {
@@ -5652,6 +8106,15 @@ class EphemeralDataPlugin {
5652
8106
  */ get databaseName() {
5653
8107
  return this._databaseName;
5654
8108
  }
8109
+ /**
8110
+ * Whether the records this plugin holds are in storage shape, keyed by `from` names.
8111
+ *
8112
+ * True for every store of what the datastore serialized, which is why a renamed property is
8113
+ * reported and records are cloned and keyed by their storage names. The datastore's change probe
8114
+ * holds rows the broadcast has already deserialized, so it reads them by in-memory names instead.
8115
+ */ get holdsStorageShape() {
8116
+ return true;
8117
+ }
5655
8118
  /**
5656
8119
  * All-or-nothing across every collection in the save.
5657
8120
  *
@@ -5859,7 +8322,9 @@ class EphemeralDataPlugin {
5859
8322
  * join discards the surplus. Same pairs either way.
5860
8323
  */ resolveJoinInnerSide(event, outerKeys, done) {
5861
8324
  const joinOption = event.operation.options.getLast("join");
5862
- if (joinOption == null) {
8325
+ // Not reached when an option before it was reported: the datastore's own join branch pairs
8326
+ // the rows this read returns.
8327
+ if (joinOption == null || joinOption.reason !== "executed") {
5863
8328
  done({
5864
8329
  ok: "success"
5865
8330
  });
@@ -5886,7 +8351,7 @@ class EphemeralDataPlugin {
5886
8351
  const innerRows = [];
5887
8352
  // Records are held in STORAGE shape, so the key is read by its resolved column name.
5888
8353
  const innerKey = joinOption.value.innerKey;
5889
- const keyColumn = innerKey.property?.getResolvedName() ?? innerKey.propertyName;
8354
+ const keyColumn = this.holdsStorageShape ? innerKey.property?.getResolvedName() ?? innerKey.propertyName : innerKey.propertyName;
5890
8355
  for (const record of innerCollection.values()){
5891
8356
  if (outerKeys != null && outerKeys.has(record[keyColumn]) === false) {
5892
8357
  continue;
@@ -5915,7 +8380,7 @@ class EphemeralDataPlugin {
5915
8380
  * to fall back to `structuredClone`, which is roughly an order of magnitude slower and was paid
5916
8381
  * on EVERY read of EVERY schema that renames a property.
5917
8382
  */ recordCloner(schema) {
5918
- const hasRenamedProperties = schema.properties.some((property)=>property.from != null);
8383
+ const hasRenamedProperties = this.holdsStorageShape && schema.properties.some((property)=>property.from != null);
5919
8384
  return hasRenamedProperties ? schema.cloneStorage : schema.clone;
5920
8385
  }
5921
8386
  query(event, done) {
@@ -5927,6 +8392,12 @@ class EphemeralDataPlugin {
5927
8392
  const schema = operation.schema;
5928
8393
  const collection = this.resolveCollection(schema);
5929
8394
  const cloneRecord = this.recordCloner(schema);
8395
+ // Records are held in storage shape and every option below runs the caller's lambda
8396
+ // over them, so a `from` property is read by a name the record does not have. Handed
8397
+ // back, and the datastore runs it after deserialization.
8398
+ if (this.holdsStorageShape) {
8399
+ reportRenamedProperties(operation.options);
8400
+ }
5930
8401
  collection.load((r)=>{
5931
8402
  if (r.ok === Result/* .Result.ERROR */.Q.ERROR) {
5932
8403
  done(Result/* .PluginEventResult.error */.D.error(event.id, r.error));
@@ -5935,7 +8406,9 @@ class EphemeralDataPlugin {
5935
8406
  const orderedOptions = [];
5936
8407
  operation.options.forEach((o)=>orderedOptions.push(o));
5937
8408
  let leadingFilterCount = 0;
5938
- while(leadingFilterCount < orderedOptions.length && orderedOptions[leadingFilterCount].name === "filter"){
8409
+ // Stops at a reported filter too: the database phase ends there, and the datastore
8410
+ // runs it and everything after it.
8411
+ while(leadingFilterCount < orderedOptions.length && orderedOptions[leadingFilterCount].name === "filter" && orderedOptions[leadingFilterCount].reason === "executed"){
5939
8412
  leadingFilterCount++;
5940
8413
  }
5941
8414
  // Key-equality fast path: when a leading filter's parsed expression pins
@@ -6006,15 +8479,21 @@ class EphemeralDataPlugin {
6006
8479
  * collection to pair it with three rows.
6007
8480
  *
6008
8481
  * `cloned` is in storage shape, so the keys are read by resolved column name.
6009
- */ // No statement to quote — an ephemeral store walks its own records. Said
6010
- // plainly so `.explain()` does not leave a reader wondering whether the
6011
- // plugin simply failed to report. Before the inner side, to match execution order.
8482
+ */ /**
8483
+ * No statement to quote an ephemeral store walks its own records — so the scan
8484
+ * is said plainly, and the PREDICATE is reported as JavaScript beside it. A count
8485
+ * alone leaves a reader unable to tell a filter that matched nothing from one
8486
+ * that was never applied.
8487
+ *
8488
+ * Before the inner side, to match execution order.
8489
+ */ const described = describeFilters(operation.options.get("filter").filter((entry)=>entry.option.reason === "executed").map((entry)=>entry.option.value));
6012
8490
  event.executedQueries.push({
6013
- text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ${cloned.length === 1 ? "record" : "records"}`
8491
+ text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ` + `${cloned.length === 1 ? "record" : "records"}, filter ${described.text}`,
8492
+ parameters: described.parameters.length > 0 ? described.parameters : undefined
6014
8493
  });
6015
8494
  const joinOption = operation.options.getLast("join");
6016
- const outerKeys = joinOption == null ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
6017
- storageShape: true
8495
+ const outerKeys = joinOption == null || joinOption.reason !== "executed" ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
8496
+ storageShape: this.holdsStorageShape
6018
8497
  });
6019
8498
  this.resolveJoinInnerSide(event, outerKeys, (joinResult)=>{
6020
8499
  if (joinResult.ok === "error") {
@@ -6185,6 +8664,29 @@ class TelemetryDbPlugin {
6185
8664
  return JSON.stringify(option.value ?? null);
6186
8665
  }
6187
8666
  };
8667
+ /**
8668
+ * Restores a `Date` that `structuredClone` produced outside this realm.
8669
+ *
8670
+ * The clone is a real date and fails `instanceof Date`, which is what a caller checks. Mutated in
8671
+ * place because the clone is already private to this call.
8672
+ */ const CacheDbPlugin_reviveDates = (value)=>{
8673
+ if (value == null || typeof value !== "object") {
8674
+ return value;
8675
+ }
8676
+ if (Object.prototype.toString.call(value) === "[object Date]") {
8677
+ return value instanceof Date ? value : new Date(value);
8678
+ }
8679
+ if (Array.isArray(value)) {
8680
+ for(let i = 0, length = value.length; i < length; i++){
8681
+ value[i] = CacheDbPlugin_reviveDates(value[i]);
8682
+ }
8683
+ return value;
8684
+ }
8685
+ for (const key of Object.keys(value)){
8686
+ value[key] = CacheDbPlugin_reviveDates(value[key]);
8687
+ }
8688
+ return value;
8689
+ };
6188
8690
  class CacheDbPlugin {
6189
8691
  plugin;
6190
8692
  max;
@@ -6223,7 +8725,7 @@ class CacheDbPlugin {
6223
8725
  * the next update would be written UNCHECKED with no error anywhere.
6224
8726
  * Pinned by `datastore/src/collections/wrapperStacking.test.ts`.
6225
8727
  */ rebuild(entry) {
6226
- return new entry.construct(structuredClone(entry.value), entry.isTransformed);
8728
+ return new entry.construct(CacheDbPlugin_reviveDates(structuredClone(entry.value)), entry.isTransformed);
6227
8729
  }
6228
8730
  query(event, done) {
6229
8731
  const key = this.keyFor(event);
@@ -6246,6 +8748,14 @@ class CacheDbPlugin {
6246
8748
  done(result);
6247
8749
  return;
6248
8750
  }
8751
+ // A partial answer must never be cached. When the plugin reports an option it cannot
8752
+ // express, these rows are what came back BEFORE the datastore finished the query — and a
8753
+ // later hit skips the plugin entirely, so nothing would report and the rows would be
8754
+ // returned as if they were the whole answer. Unfiltered, silently.
8755
+ if (event.operation.options.notExecuted().length > 0) {
8756
+ done(Result/* .PluginEventResult.success */.D.success(event.id, result.data));
8757
+ return;
8758
+ }
6249
8759
  this.store(key, result.data);
6250
8760
  // The caller gets a rebuilt value too, not the one just stored, so that mutating
6251
8761
  // the result of a MISS cannot corrupt what the next hit returns.