@routier/core 0.7.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 (47) hide show
  1. package/dist/codegen/blocks.d.ts +17 -1
  2. package/dist/codegen/handlers/types.d.ts +13 -5
  3. package/dist/codegen/index.cjs +28 -0
  4. package/dist/codegen/index.cjs.map +1 -1
  5. package/dist/codegen/index.js +28 -0
  6. package/dist/codegen/index.js.map +1 -1
  7. package/dist/collections/MemoryDataCollection.d.ts +2 -4
  8. package/dist/collections/index.cjs +127 -15
  9. package/dist/collections/index.cjs.map +1 -1
  10. package/dist/collections/index.js +127 -15
  11. package/dist/collections/index.js.map +1 -1
  12. package/dist/expressions/index.cjs +226 -4
  13. package/dist/expressions/index.cjs.map +1 -1
  14. package/dist/expressions/index.js +228 -5
  15. package/dist/expressions/index.js.map +1 -1
  16. package/dist/expressions/parser.d.ts +42 -1
  17. package/dist/index.cjs +753 -345
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.js +756 -345
  20. package/dist/index.js.map +1 -1
  21. package/dist/plugins/EphemeralDataPlugin.d.ts +8 -0
  22. package/dist/plugins/index.cjs +566 -49
  23. package/dist/plugins/index.cjs.map +1 -1
  24. package/dist/plugins/index.js +566 -48
  25. package/dist/plugins/index.js.map +1 -1
  26. package/dist/plugins/query/QueryOptionsCollection.d.ts +15 -5
  27. package/dist/plugins/query/index.d.ts +1 -0
  28. package/dist/plugins/query/renames.d.ts +27 -0
  29. package/dist/plugins/query/types.d.ts +15 -1
  30. package/dist/plugins/translators/SqlTranslator.d.ts +15 -0
  31. package/dist/schema/SchemaDefinition.d.ts +8 -0
  32. package/dist/schema/changeTracker.d.ts +10 -0
  33. package/dist/schema/index.cjs +291 -274
  34. package/dist/schema/index.cjs.map +1 -1
  35. package/dist/schema/index.d.ts +1 -0
  36. package/dist/schema/index.js +294 -276
  37. package/dist/schema/index.js.map +1 -1
  38. package/dist/schema/types.d.ts +8 -7
  39. package/dist/schema/utils/storageDates.d.ts +25 -0
  40. package/dist/transfer/index.cjs.map +1 -1
  41. package/dist/transfer/index.js.map +1 -1
  42. package/dist/utilities/index.cjs +74 -29
  43. package/dist/utilities/index.cjs.map +1 -1
  44. package/dist/utilities/index.js +74 -29
  45. package/dist/utilities/index.js.map +1 -1
  46. package/package.json +2 -2
  47. package/dist/codegen/utils.d.ts +0 -22
@@ -1021,6 +1021,7 @@ __webpack_require__.d(__webpack_exports__, {
1021
1021
 
1022
1022
 
1023
1023
 
1024
+
1024
1025
  // Error message constants
1025
1026
  const ERROR_MESSAGES = {
1026
1027
  PROPERTY_NOT_FOUND: (path)=>`Error parsing query, could not find PropertyInfo for path: ${path}`,
@@ -1628,6 +1629,9 @@ const COALESCE_OPERATORS = sourceKeyed({
1628
1629
  // A comparison always names a schema property, so the condition alone settles it
1629
1630
  return true;
1630
1631
  }
1632
+ if (operand.kind === "opaque") {
1633
+ return operand.reads.some(containsProperty);
1634
+ }
1631
1635
  return operand.kind === "arithmetic" && (containsProperty(operand.left) || containsProperty(operand.right) || operand.extra != null && containsProperty(operand.extra));
1632
1636
  };
1633
1637
  const DECLARATION_KEYWORDS = new Set([
@@ -1805,13 +1809,18 @@ const resolveParamPath = (paramsName, path, data)=>{
1805
1809
  scope;
1806
1810
  paramsName;
1807
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;
1808
1816
  /** Set when a param value shaped the tree itself (e.g. x[p.name]) — such templates cannot be cached. */ structurallyDependsOnParams = false;
1809
- constructor(schema, stream, scope, paramsName, params){
1817
+ constructor(schema, stream, scope, paramsName, params, readsValues = false){
1810
1818
  this.schema = schema;
1811
1819
  this.stream = stream;
1812
1820
  this.scope = scope;
1813
1821
  this.paramsName = paramsName;
1814
1822
  this.params = params;
1823
+ this.readsValues = readsValues;
1815
1824
  }
1816
1825
  parse() {
1817
1826
  const expression = this.parseOr();
@@ -1833,6 +1842,71 @@ const resolveParamPath = (paramsName, path, data)=>{
1833
1842
  }
1834
1843
  return answer;
1835
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
+ }
1836
1910
  /** The expression a `{ … }` block answers with. */ parseBlock() {
1837
1911
  this.stream.expectPunctuation("{");
1838
1912
  const answer = this.parseStatements();
@@ -2096,7 +2170,7 @@ const resolveParamPath = (paramsName, path, data)=>{
2096
2170
  * A structural dependence found inside propagates outward: the template it belongs to cannot be
2097
2171
  * cached either.
2098
2172
  */ parseNested(source) {
2099
- const nested = new ExpressionParser(this.schema, new TokenStream(tokenize(source)), this.scope, this.paramsName, this.params);
2173
+ const nested = new ExpressionParser(this.schema, new TokenStream(tokenize(source)), this.scope, this.paramsName, this.params, this.readsValues);
2100
2174
  const operand = nested.parseInterpolation();
2101
2175
  // Leftover tokens mean the interpolation held something this reads only part of. Silently
2102
2176
  // keeping the part it understood is the worst outcome available: `${x.age > 5 ? "a" : "b"}`
@@ -2388,7 +2462,7 @@ const resolveParamPath = (paramsName, path, data)=>{
2388
2462
  if (argument.kind === "method-call") {
2389
2463
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("nested method call inside .includes()"));
2390
2464
  }
2391
- if (argument.kind === "arithmetic" || argument.kind === "conditional") {
2465
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2392
2466
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("arithmetic inside .includes()"));
2393
2467
  }
2394
2468
  return {
@@ -2482,7 +2556,7 @@ const resolveParamPath = (paramsName, path, data)=>{
2482
2556
  if (argument.kind === "method-call") {
2483
2557
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`nested method call inside .${method}()`));
2484
2558
  }
2485
- if (argument.kind === "arithmetic" || argument.kind === "conditional") {
2559
+ if (argument.kind === "arithmetic" || argument.kind === "conditional" || argument.kind === "opaque") {
2486
2560
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`arithmetic inside .${method}()`));
2487
2561
  }
2488
2562
  return {
@@ -2492,9 +2566,20 @@ const resolveParamPath = (paramsName, path, data)=>{
2492
2566
  argument
2493
2567
  };
2494
2568
  }
2569
+ if (this.readsValues) {
2570
+ return this.withGroupCall(this.opaqueCall(this.resolveChain(options.kind, path, transformer, locale)));
2571
+ }
2495
2572
  throw new Error(ERROR_MESSAGES.UNSUPPORTED(`method '.${method}()'`));
2496
2573
  }
2497
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
+ }
2498
2583
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("property access after a transform method"));
2499
2584
  }
2500
2585
  path.push(segment.value);
@@ -2639,10 +2724,39 @@ const resolveParamPath = (paramsName, path, data)=>{
2639
2724
  argument
2640
2725
  };
2641
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
+ }
2642
2739
  break;
2643
2740
  }
2644
2741
  return receiver;
2645
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
+ }
2646
2760
  withValueTransformer(operand) {
2647
2761
  if (this.stream.isPunctuation(".")) {
2648
2762
  const method = this.stream.peek(1);
@@ -2676,6 +2790,10 @@ const resolveParamPath = (paramsName, path, data)=>{
2676
2790
  if (right.kind === "method-call") {
2677
2791
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("method call on the right side of a comparison"));
2678
2792
  }
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
+ }
2679
2797
  if (needsBrackets(left) || needsBrackets(right)) {
2680
2798
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("a bitwise or nullish operator compared without brackets, which JavaScript reads the other way round"));
2681
2799
  }
@@ -2889,6 +3007,9 @@ const resolveParamPath = (paramsName, path, data)=>{
2889
3007
  if (operand.kind === "method-call") {
2890
3008
  throw new Error(ERROR_MESSAGES.UNSUPPORTED("a method call inside arithmetic"));
2891
3009
  }
3010
+ if (operand.kind === "opaque") {
3011
+ throw new Error(ERROR_MESSAGES.UNSUPPORTED("a call with no expression form inside arithmetic"));
3012
+ }
2892
3013
  return this.createValueExpression(operand, null, /* applyConverter */ false);
2893
3014
  }
2894
3015
  createPropertyExpression(operand) {
@@ -3224,6 +3345,104 @@ const toExpression = (schema, fn, params)=>{
3224
3345
  return _types__rspack_import_1/* .Expression.notParsable */.r4.notParsable(refusalOf(error));
3225
3346
  }
3226
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;
3376
+ }
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
3227
3446
 
3228
3447
 
3229
3448
  },
@@ -3931,6 +4150,30 @@ const mismatchWarning = (expression)=>{
3931
4150
  const outcome = expression.negated ? "every row matches" : "no row matches";
3932
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`;
3933
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
+ };
3934
4177
  class QueryOptionsCollection {
3935
4178
  options = new Map();
3936
4179
  nextExecutionTarget = "database";
@@ -3969,7 +4212,7 @@ class QueryOptionsCollection {
3969
4212
  }
3970
4213
  }
3971
4214
  if (name === "filter") {
3972
- // Need to check for unmapped and renamed properties
4215
+ // Need to check for unmapped properties
3973
4216
  const filterValue = value;
3974
4217
  // A tautology (`x => true`) filters nothing — skip it entirely so
3975
4218
  // plugins never see it
@@ -3986,14 +4229,10 @@ class QueryOptionsCollection {
3986
4229
  this.cutOverToMemory("unmapped-property");
3987
4230
  return false;
3988
4231
  }
3989
- if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.hasRenamedSegments) {
3990
- // Cut over to memory execution: the plugin stores data under the
3991
- // `from` (storage) names, but filter selectors reference the
3992
- // in-memory names. Memory execution runs after deserialization,
3993
- // where the in-memory names exist
3994
- this.cutOverToMemory("renamed-property");
3995
- return false;
3996
- }
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`
3997
4236
  if (comparesTypesThatCannotMatch(expression)) {
3998
4237
  _utilities__rspack_import_3/* .logger.warn */.vF.warn(mismatchWarning(expression));
3999
4238
  this.cutOverToMemory("predicate-error");
@@ -4005,26 +4244,19 @@ class QueryOptionsCollection {
4005
4244
  }
4006
4245
  if (name === "sort") {
4007
4246
  const sortValue = value;
4008
- // Same rule as filters: sort selectors reference in-memory names, which
4009
- // 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
4010
4249
  if (sortValue.property != null && sortValue.property.isUnmapped) {
4011
4250
  this.cutOverToMemory("unmapped-property");
4012
- } else if (sortValue.property != null && sortValue.property.hasRenamedSegments) {
4013
- this.cutOverToMemory("renamed-property");
4014
4251
  }
4015
4252
  }
4016
4253
  if (name === "nearest") {
4017
4254
  const nearestValue = value;
4018
- // Same rule as sort, and for the same reason: the plugin stores the vector under
4019
- // the `from` name, and an unmapped property is not stored at all. Both are only
4020
- // readable after deserialization, which is where memory execution runs.
4021
- //
4022
- // This is also what lets every translator's in-memory fallback read the column by
4023
- // 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.
4024
4258
  if (nearestValue.property != null && nearestValue.property.isUnmapped) {
4025
4259
  this.cutOverToMemory("unmapped-property");
4026
- } else if (nearestValue.property != null && nearestValue.property.hasRenamedSegments) {
4027
- this.cutOverToMemory("renamed-property");
4028
4260
  }
4029
4261
  }
4030
4262
  if ((name === "filter" || name === "sort") && (this.options.has("skip") || this.options.has("take"))) {
@@ -4131,6 +4363,9 @@ class QueryOptionsCollection {
4131
4363
  * the shared collection before executing. Without restoring, a re-executed terminal —
4132
4364
  * the whole point of a subscribed queryable — stacks its option a second time and
4133
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.
4134
4369
  */ snapshot() {
4135
4370
  const options = new Map([
4136
4371
  ...this.options.entries()
@@ -4208,19 +4443,48 @@ class QueryOptionsCollection {
4208
4443
  }
4209
4444
  }
4210
4445
  /**
4211
- * Forgets what any previous dispatch reported.
4446
+ * A copy of the collection for one dispatch to a plugin, with nothing reported on it.
4212
4447
  *
4213
4448
  * Capability is answered per dispatch, so a report is only an answer for the execution that
4214
- * produced it. The items are shared with any snapshot, so a report mutated in place otherwise
4215
- * survives a restore and a second terminal on the same queryable replays options the plugin
4216
- * did run a `skip` applied twice, over rows already windowed.
4217
- */ forgetReports() {
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();
4218
4464
  this.resolveEnumeration();
4219
4465
  for (const item of this.enumeratedItems){
4220
- if (item.option.target === "database") {
4221
- item.option.reason = "executed";
4222
- }
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));
4223
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
4487
+ };
4224
4488
  }
4225
4489
  /** The options the database did not run, in the order they were written. */ notExecuted() {
4226
4490
  this.resolveEnumeration();
@@ -4420,6 +4684,128 @@ var HashType = /*#__PURE__*/ (/* unused pure expression or super */ null && (fun
4420
4684
  }({})));
4421
4685
 
4422
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
+ };
4807
+
4808
+
4423
4809
  },
4424
4810
  76(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4425
4811
  __webpack_require__.d(__webpack_exports__, {
@@ -4689,6 +5075,7 @@ __webpack_require__.d(__webpack_exports__, {
4689
5075
  CacheDbPlugin: () => (/* reexport */ CacheDbPlugin),
4690
5076
  Query: () => (/* reexport */ Query),
4691
5077
  splitSendableOptions: () => (/* reexport */ splitSendableOptions),
5078
+ withInnerSide: () => (/* reexport */ withInnerSide),
4692
5079
  executeJoin: () => (/* reexport */ executeJoin),
4693
5080
  formatExplanation: () => (/* reexport */ formatExplanation),
4694
5081
  parameteriseDocument: () => (/* reexport */ parameteriseDocument),
@@ -4719,13 +5106,13 @@ __webpack_require__.d(__webpack_exports__, {
4719
5106
  readJoinKey: () => (/* reexport */ readJoinKey),
4720
5107
  loadJoinInnerSide: () => (/* reexport */ loadJoinInnerSide),
4721
5108
  DataTranslator: () => (/* reexport */ DataTranslator),
5109
+ reportRenamedProperties: () => (/* reexport */ reportRenamedProperties),
4722
5110
  serializeBulkPersist: () => (/* reexport */ serializeBulkPersist),
4723
- toEntityShape: () => (/* reexport */ toEntityShape),
4724
5111
  TelemetryDbPlugin: () => (/* reexport */ TelemetryDbPlugin),
4725
- withExecutedQueries: () => (/* reexport */ withExecutedQueries),
5112
+ toEntityShape: () => (/* reexport */ toEntityShape),
4726
5113
  createRequestHandler: () => (/* reexport */ createRequestHandler),
4727
5114
  SqlTranslator: () => (/* reexport */ SqlTranslator),
4728
- withInnerSide: () => (/* reexport */ withInnerSide),
5115
+ withExecutedQueries: () => (/* reexport */ withExecutedQueries),
4729
5116
  QueryOrdering: () => (/* reexport */ types_QueryOrdering),
4730
5117
  describeFilterAsJs: () => (/* reexport */ describeFilterAsJs),
4731
5118
  serializeQueryOptions: () => (/* reexport */ serializeQueryOptions),
@@ -5699,9 +6086,12 @@ class JsonTranslator extends DataTranslator {
5699
6086
  }
5700
6087
  }
5701
6088
 
6089
+ // EXTERNAL MODULE: ./src/schema/utils/storageDates.ts
6090
+ var storageDates = __webpack_require__(894);
5702
6091
  ;// CONCATENATED MODULE: ./src/plugins/translators/SqlTranslator.ts
5703
6092
 
5704
6093
 
6094
+
5705
6095
  /**
5706
6096
  * A stored vector as a list of numbers, whatever the driver handed back.
5707
6097
  *
@@ -5730,6 +6120,31 @@ class SqlTranslator extends DataTranslator {
5730
6120
  super(query);
5731
6121
  this.pushedDown = pushedDown;
5732
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
+ }
5733
6148
  count(data, _) {
5734
6149
  if (Array.isArray(data) && data.length > 0) {
5735
6150
  // Count is returned as the property alias on the query.
@@ -6181,7 +6596,6 @@ const isParameter = (value)=>typeof value === "object" && value !== null && PARA
6181
6596
  */ const MEMORY_EXECUTION_EXPLANATIONS = {
6182
6597
  "not-parsable": "A filter could not be parsed into an expression tree, so it and every option after it run in memory.",
6183
6598
  "unmapped-property": "The property is not stored in the database, so it can only be read after deserialization.",
6184
- "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.",
6185
6599
  "map-rename": "A map renames or drops properties, so every option after it refers to names the database does not have.",
6186
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.",
6187
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.",
@@ -6683,6 +7097,80 @@ const formatStep = (step, lines)=>{
6683
7097
  return lines.join("\n");
6684
7098
  };
6685
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
+
6686
7174
  ;// CONCATENATED MODULE: ./src/plugins/query/types.ts
6687
7175
  var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
6688
7176
  QueryOrdering["Descending"] = "desc";
@@ -6700,6 +7188,7 @@ var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
6700
7188
 
6701
7189
 
6702
7190
 
7191
+
6703
7192
  // EXTERNAL MODULE: ./src/expressions/evaluate.ts
6704
7193
  var evaluate = __webpack_require__(379);
6705
7194
  // EXTERNAL MODULE: ./src/expressions/fold.ts
@@ -6713,6 +7202,9 @@ var fold = __webpack_require__(43);
6713
7202
  * Everything except `map` and `group`. Those two are defined BY a closure — the projection is the
6714
7203
  * option — and no data form of them exists to send. Nothing else needs its closure: a sort is a
6715
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`.
6716
7208
  */ const SENDABLE = new Set([
6717
7209
  "skip",
6718
7210
  "take",
@@ -6726,6 +7218,7 @@ var fold = __webpack_require__(43);
6726
7218
  "sum",
6727
7219
  "distinct"
6728
7220
  ]);
7221
+ const isSendable = (name, value)=>SENDABLE.has(name) && (name !== "sort" && name !== "nearest" || value.isDirectProperty !== false);
6729
7222
  /**
6730
7223
  * Splits options into the PREFIX that can be sent and the remainder that cannot.
6731
7224
  *
@@ -6741,7 +7234,12 @@ var fold = __webpack_require__(43);
6741
7234
  const local = new QueryOptionsCollection/* .QueryOptionsCollection */.H();
6742
7235
  let stopped = false;
6743
7236
  options.forEach((option)=>{
6744
- 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) {
6745
7243
  stopped = true;
6746
7244
  }
6747
7245
  (stopped ? local : sendable).add(option.name, option.value);
@@ -7608,6 +8106,15 @@ class EphemeralDataPlugin {
7608
8106
  */ get databaseName() {
7609
8107
  return this._databaseName;
7610
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
+ }
7611
8118
  /**
7612
8119
  * All-or-nothing across every collection in the save.
7613
8120
  *
@@ -7815,7 +8322,9 @@ class EphemeralDataPlugin {
7815
8322
  * join discards the surplus. Same pairs either way.
7816
8323
  */ resolveJoinInnerSide(event, outerKeys, done) {
7817
8324
  const joinOption = event.operation.options.getLast("join");
7818
- 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") {
7819
8328
  done({
7820
8329
  ok: "success"
7821
8330
  });
@@ -7842,7 +8351,7 @@ class EphemeralDataPlugin {
7842
8351
  const innerRows = [];
7843
8352
  // Records are held in STORAGE shape, so the key is read by its resolved column name.
7844
8353
  const innerKey = joinOption.value.innerKey;
7845
- const keyColumn = innerKey.property?.getResolvedName() ?? innerKey.propertyName;
8354
+ const keyColumn = this.holdsStorageShape ? innerKey.property?.getResolvedName() ?? innerKey.propertyName : innerKey.propertyName;
7846
8355
  for (const record of innerCollection.values()){
7847
8356
  if (outerKeys != null && outerKeys.has(record[keyColumn]) === false) {
7848
8357
  continue;
@@ -7871,7 +8380,7 @@ class EphemeralDataPlugin {
7871
8380
  * to fall back to `structuredClone`, which is roughly an order of magnitude slower and was paid
7872
8381
  * on EVERY read of EVERY schema that renames a property.
7873
8382
  */ recordCloner(schema) {
7874
- const hasRenamedProperties = schema.properties.some((property)=>property.from != null);
8383
+ const hasRenamedProperties = this.holdsStorageShape && schema.properties.some((property)=>property.from != null);
7875
8384
  return hasRenamedProperties ? schema.cloneStorage : schema.clone;
7876
8385
  }
7877
8386
  query(event, done) {
@@ -7883,6 +8392,12 @@ class EphemeralDataPlugin {
7883
8392
  const schema = operation.schema;
7884
8393
  const collection = this.resolveCollection(schema);
7885
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
+ }
7886
8401
  collection.load((r)=>{
7887
8402
  if (r.ok === Result/* .Result.ERROR */.Q.ERROR) {
7888
8403
  done(Result/* .PluginEventResult.error */.D.error(event.id, r.error));
@@ -7891,7 +8406,9 @@ class EphemeralDataPlugin {
7891
8406
  const orderedOptions = [];
7892
8407
  operation.options.forEach((o)=>orderedOptions.push(o));
7893
8408
  let leadingFilterCount = 0;
7894
- 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"){
7895
8412
  leadingFilterCount++;
7896
8413
  }
7897
8414
  // Key-equality fast path: when a leading filter's parsed expression pins
@@ -7969,14 +8486,14 @@ class EphemeralDataPlugin {
7969
8486
  * that was never applied.
7970
8487
  *
7971
8488
  * Before the inner side, to match execution order.
7972
- */ const described = describeFilters(operation.options.get("filter").map((entry)=>entry.option.value));
8489
+ */ const described = describeFilters(operation.options.get("filter").filter((entry)=>entry.option.reason === "executed").map((entry)=>entry.option.value));
7973
8490
  event.executedQueries.push({
7974
8491
  text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ` + `${cloned.length === 1 ? "record" : "records"}, filter ${described.text}`,
7975
8492
  parameters: described.parameters.length > 0 ? described.parameters : undefined
7976
8493
  });
7977
8494
  const joinOption = operation.options.getLast("join");
7978
- const outerKeys = joinOption == null ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
7979
- storageShape: true
8495
+ const outerKeys = joinOption == null || joinOption.reason !== "executed" ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
8496
+ storageShape: this.holdsStorageShape
7980
8497
  });
7981
8498
  this.resolveJoinInnerSide(event, outerKeys, (joinResult)=>{
7982
8499
  if (joinResult.ok === "error") {
@@ -8152,7 +8669,7 @@ class TelemetryDbPlugin {
8152
8669
  *
8153
8670
  * The clone is a real date and fails `instanceof Date`, which is what a caller checks. Mutated in
8154
8671
  * place because the clone is already private to this call.
8155
- */ const reviveDates = (value)=>{
8672
+ */ const CacheDbPlugin_reviveDates = (value)=>{
8156
8673
  if (value == null || typeof value !== "object") {
8157
8674
  return value;
8158
8675
  }
@@ -8161,12 +8678,12 @@ class TelemetryDbPlugin {
8161
8678
  }
8162
8679
  if (Array.isArray(value)) {
8163
8680
  for(let i = 0, length = value.length; i < length; i++){
8164
- value[i] = reviveDates(value[i]);
8681
+ value[i] = CacheDbPlugin_reviveDates(value[i]);
8165
8682
  }
8166
8683
  return value;
8167
8684
  }
8168
8685
  for (const key of Object.keys(value)){
8169
- value[key] = reviveDates(value[key]);
8686
+ value[key] = CacheDbPlugin_reviveDates(value[key]);
8170
8687
  }
8171
8688
  return value;
8172
8689
  };
@@ -8208,7 +8725,7 @@ class CacheDbPlugin {
8208
8725
  * the next update would be written UNCHECKED with no error anywhere.
8209
8726
  * Pinned by `datastore/src/collections/wrapperStacking.test.ts`.
8210
8727
  */ rebuild(entry) {
8211
- return new entry.construct(reviveDates(structuredClone(entry.value)), entry.isTransformed);
8728
+ return new entry.construct(CacheDbPlugin_reviveDates(structuredClone(entry.value)), entry.isTransformed);
8212
8729
  }
8213
8730
  query(event, done) {
8214
8731
  const key = this.keyFor(event);