@routier/core 0.3.0 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -3687,7 +3687,7 @@ var TrampolinePipeline = __webpack_require__(416);
3687
3687
 
3688
3688
 
3689
3689
  },
3690
- 771(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3690
+ 301(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3691
3691
 
3692
3692
  // EXPORTS
3693
3693
  __webpack_require__.d(__webpack_exports__, {
@@ -3698,27 +3698,32 @@ __webpack_require__.d(__webpack_exports__, {
3698
3698
  Query: () => (/* reexport */ Query),
3699
3699
  splitSendableOptions: () => (/* reexport */ splitSendableOptions),
3700
3700
  executeJoin: () => (/* reexport */ executeJoin),
3701
+ formatExplanation: () => (/* reexport */ formatExplanation),
3701
3702
  serializePersistResult: () => (/* reexport */ serializePersistResult),
3703
+ explainQuery: () => (/* reexport */ explainQuery),
3704
+ cosineDistance: () => (/* reexport */ cosineDistance),
3702
3705
  RetryDbPlugin: () => (/* reexport */ RetryDbPlugin),
3703
- nearestBy: () => (/* reexport */ nearestBy),
3704
3706
  BatchingDbPlugin: () => (/* reexport */ BatchingDbPlugin),
3705
3707
  applyInnerOptions: () => (/* reexport */ applyInnerOptions),
3706
- TranslatedSingleValue: () => (/* reexport */ TranslatedSingleValue),
3708
+ MEMORY_EXECUTION_EXPLANATIONS: () => (/* reexport */ MEMORY_EXECUTION_EXPLANATIONS),
3707
3709
  TupleTranslator: () => (/* reexport */ TupleTranslator),
3708
- cosineDistance: () => (/* reexport */ cosineDistance),
3710
+ TranslatedSingleValue: () => (/* reexport */ TranslatedSingleValue),
3709
3711
  ConcurrencyDbPlugin: () => (/* reexport */ ConcurrencyDbPlugin),
3710
3712
  TranslatedGroupValue: () => (/* reexport */ TranslatedGroupValue),
3711
3713
  deserializeBulkPersist: () => (/* reexport */ deserializeBulkPersist),
3714
+ EXECUTED_QUERIES_UNSUPPORTED: () => (/* reexport */ EXECUTED_QUERIES_UNSUPPORTED),
3712
3715
  joinInPlugin: () => (/* reexport */ joinInPlugin),
3713
- readJoinKey: () => (/* reexport */ readJoinKey),
3714
3716
  deserializePersistResult: () => (/* reexport */ deserializePersistResult),
3715
- toEntityShape: () => (/* reexport */ toEntityShape),
3717
+ nearestBy: () => (/* reexport */ nearestBy),
3718
+ readJoinKey: () => (/* reexport */ readJoinKey),
3716
3719
  serializeBulkPersist: () => (/* reexport */ serializeBulkPersist),
3717
3720
  hashJoin: () => (/* reexport */ hashJoin),
3718
3721
  EphemeralDataPlugin: () => (/* reexport */ EphemeralDataPlugin),
3719
3722
  QueryOptionsCollection: () => (/* reexport */ QueryOptionsCollection/* .QueryOptionsCollection */.H),
3723
+ toEntityShape: () => (/* reexport */ toEntityShape),
3720
3724
  loadJoinInnerSide: () => (/* reexport */ loadJoinInnerSide),
3721
3725
  DataTranslator: () => (/* reexport */ DataTranslator),
3726
+ withExecutedQueries: () => (/* reexport */ withExecutedQueries),
3722
3727
  createRequestHandler: () => (/* reexport */ createRequestHandler),
3723
3728
  SqlTranslator: () => (/* reexport */ SqlTranslator),
3724
3729
  QueryOrdering: () => (/* reexport */ types_QueryOrdering),
@@ -4260,7 +4265,11 @@ class Query {
4260
4265
  id: `${event.id}-inner`,
4261
4266
  source: event.source,
4262
4267
  action: "query",
4263
- reason: "join inner side"
4268
+ reason: "join inner side",
4269
+ explain: event.explain,
4270
+ // The same array the outer read pushes into, so a join reports BOTH reads in execution
4271
+ // order. Built fresh rather than spread, so this has to be carried explicitly.
4272
+ executedQueries: event.executedQueries
4264
4273
  };
4265
4274
  query(innerEvent, (result)=>{
4266
4275
  if (result.ok === Result/* .PluginEventResult.ERROR */.D.ERROR) {
@@ -4960,6 +4969,386 @@ class SqlTranslator extends DataTranslator {
4960
4969
 
4961
4970
 
4962
4971
 
4972
+ ;// CONCATENATED MODULE: ./src/plugins/query/explain.ts
4973
+
4974
+ /**
4975
+ * One sentence per reason code, written for someone meeting pushdown for the first time.
4976
+ *
4977
+ * Beside the codes rather than in the formatter, so console output, a failing test and the
4978
+ * docs all say the same thing.
4979
+ */ const MEMORY_EXECUTION_EXPLANATIONS = {
4980
+ "not-parsable": "A filter could not be parsed into an expression tree, so it and every option after it run in memory.",
4981
+ "unmapped-property": "The property is not stored in the database, so it can only be read after deserialization.",
4982
+ "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.",
4983
+ "map-rename": "A map renames or drops properties, so every option after it refers to names the database does not have.",
4984
+ "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.",
4985
+ "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.",
4986
+ "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."
4987
+ };
4988
+ const EXECUTED_QUERIES_UNSUPPORTED = "This plugin did not report what it executed. It may not support explain.";
4989
+ const DATABASE_STEP_DESCRIPTION = "These options are sent to the plugin.";
4990
+ const MEMORY_STEP_DESCRIPTION = "Routier runs these over the rows the database returned, after deserializing them.";
4991
+ const UNNARROWED_READ_DESCRIPTION = "No option could be pushed down, so the plugin reads the whole collection.";
4992
+ /**
4993
+ * The reportable shape of one option's value.
4994
+ *
4995
+ * Serializable facts only, never the live selector functions — an explanation is a document a
4996
+ * caller may log, diff in a test, or send to a server, and a closure survives none of that.
4997
+ */ const detailOf = (option)=>{
4998
+ if (option.name === "filter") {
4999
+ const value = option.value;
5000
+ if (value.expression == null) {
5001
+ return undefined;
5002
+ }
5003
+ try {
5004
+ return {
5005
+ expression: types/* .Expression.toJson */.r4.toJson(value.expression)
5006
+ };
5007
+ } catch {
5008
+ // `valueToJson` rejects a value no wire can carry. Reporting the rest of the
5009
+ // explanation beats taking the diagnostic down with the query it describes.
5010
+ return {
5011
+ expressionUnavailable: "This filter holds a value that cannot be serialized."
5012
+ };
5013
+ }
5014
+ }
5015
+ if (option.name === "sort") {
5016
+ const value = option.value;
5017
+ return {
5018
+ propertyName: value.propertyName,
5019
+ direction: value.direction
5020
+ };
5021
+ }
5022
+ if (option.name === "skip" || option.name === "take") {
5023
+ return {
5024
+ value: option.value
5025
+ };
5026
+ }
5027
+ if (option.name === "nearest") {
5028
+ const value = option.value;
5029
+ return {
5030
+ propertyName: value.propertyName,
5031
+ dimensions: value.vector.length,
5032
+ count: value.count
5033
+ };
5034
+ }
5035
+ if (option.name === "join") {
5036
+ const value = option.value;
5037
+ return {
5038
+ kind: value.kind,
5039
+ outerKey: value.outerKey.propertyName,
5040
+ innerKey: value.innerKey.propertyName,
5041
+ crossPlugin: value.crossPlugin,
5042
+ innerOptions: explainedOptionsOf(value.innerOptions)
5043
+ };
5044
+ }
5045
+ if (option.name === "map" || option.name === "group") {
5046
+ const value = option.value;
5047
+ return {
5048
+ fields: value.fields.map((x)=>({
5049
+ from: x.sourceName,
5050
+ to: x.destinationName
5051
+ }))
5052
+ };
5053
+ }
5054
+ return undefined;
5055
+ };
5056
+ const explainedOptionOf = (option, index)=>{
5057
+ const detail = detailOf(option);
5058
+ return {
5059
+ index,
5060
+ name: option.name,
5061
+ ...detail == null ? {} : {
5062
+ detail
5063
+ }
5064
+ };
5065
+ };
5066
+ const explainedOptionsOf = (options)=>{
5067
+ const explained = [];
5068
+ let index = 0;
5069
+ options.forEach((option)=>explained.push(explainedOptionOf(option, index++)));
5070
+ return explained;
5071
+ };
5072
+ const summarize = (steps)=>{
5073
+ const reasons = [];
5074
+ let database = 0;
5075
+ let memory = 0;
5076
+ for (const step of steps){
5077
+ if (step.executedIn === "database") {
5078
+ database += step.options.length;
5079
+ continue;
5080
+ }
5081
+ memory += step.options.length;
5082
+ if (step.reason != null && reasons.includes(step.reason) === false) {
5083
+ reasons.push(step.reason);
5084
+ }
5085
+ }
5086
+ const counts = `${database} ${database === 1 ? "option ran" : "options ran"} in the database, ${memory} ran in memory.`;
5087
+ const causes = reasons.map((reason)=>MEMORY_EXECUTION_EXPLANATIONS[reason]).join(" ");
5088
+ return {
5089
+ database,
5090
+ memory,
5091
+ reasons,
5092
+ explanation: causes.length === 0 ? counts : `${counts} ${causes}`
5093
+ };
5094
+ };
5095
+ /**
5096
+ * Groups options into consecutive runs that execute in the same place.
5097
+ *
5098
+ * A step boundary is where execution moves, and a reader has to see the statement as step 1 OF
5099
+ * 2 to understand it is not the whole query. Cutting over to memory is a ratchet, so the
5100
+ * database options are always a prefix and there are at most two steps.
5101
+ *
5102
+ * A database step is emitted even when NO option pushed down, because the plugin is dispatched
5103
+ * either way — `createQueryPayload` always builds a database event. Without it, the worst case
5104
+ * the feature exists to expose reports "0 in the database" while the backend reads the whole
5105
+ * table, which is the opposite of the truth.
5106
+ */ const toExecutionSteps = (options)=>{
5107
+ const steps = [];
5108
+ let index = 0;
5109
+ options.forEach((option)=>{
5110
+ const explained = explainedOptionOf(option, index++);
5111
+ const current = steps[steps.length - 1];
5112
+ if (current != null && current.executedIn === option.target) {
5113
+ current.options.push(explained);
5114
+ return;
5115
+ }
5116
+ steps.push({
5117
+ step: steps.length + 1,
5118
+ of: 0,
5119
+ executedIn: option.target,
5120
+ description: option.target === "database" ? DATABASE_STEP_DESCRIPTION : MEMORY_STEP_DESCRIPTION,
5121
+ options: [
5122
+ explained
5123
+ ],
5124
+ ...option.reason == null ? {} : {
5125
+ reason: option.reason,
5126
+ explanation: MEMORY_EXECUTION_EXPLANATIONS[option.reason]
5127
+ }
5128
+ });
5129
+ });
5130
+ if (steps[0]?.executedIn !== "database") {
5131
+ steps.unshift({
5132
+ step: 0,
5133
+ of: 0,
5134
+ executedIn: "database",
5135
+ description: UNNARROWED_READ_DESCRIPTION,
5136
+ options: []
5137
+ });
5138
+ }
5139
+ for(let i = 0; i < steps.length; i++){
5140
+ steps[i].step = i + 1;
5141
+ steps[i].of = steps.length;
5142
+ }
5143
+ return steps;
5144
+ };
5145
+ /**
5146
+ * Builds the explanation from the resolved options, with no plugin involvement.
5147
+ *
5148
+ * Takes the collection BEFORE `split()`, and throws otherwise. Splitting re-adds each half
5149
+ * into a fresh collection, which re-derives targets without the options that caused them — a
5150
+ * post-join filter alone in the memory half derives back to `"database"`, and the document
5151
+ * would report memory work as having run in the database.
5152
+ */ const explainQuery = (options, context)=>{
5153
+ if (options.isDerived === true) {
5154
+ 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.");
5155
+ }
5156
+ const executionSteps = toExecutionSteps(options);
5157
+ return {
5158
+ collection: context.collection,
5159
+ database: context.database,
5160
+ summary: summarize(executionSteps),
5161
+ executionSteps,
5162
+ plugin: {
5163
+ kind: context.pluginKind
5164
+ }
5165
+ };
5166
+ };
5167
+ /**
5168
+ * Attaches what the backend reported to the step that was sent to it.
5169
+ *
5170
+ * Reporting is optional for a plugin, so an empty report is not an error: the step is marked
5171
+ * `executedQueriesUnsupported` instead, and the rest of the explanation stands — the pushdown
5172
+ * analysis comes from the options and is correct with or without the plugin's statements.
5173
+ *
5174
+ * Copies the steps rather than writing into them, so the explanation a caller already holds
5175
+ * does not gain statements after the fact. Options and their details are shared with the
5176
+ * original — nothing mutates them, and copying deeper would only look safer than it is.
5177
+ */ const withExecutedQueries = (explanation, executedQueries)=>{
5178
+ let attached = false;
5179
+ const executionSteps = explanation.executionSteps.map((step)=>{
5180
+ // Only the first database step: a plugin reports what IT ran, and everything it ran
5181
+ // was sent as one dispatch. Stamping the same statements onto a second database step
5182
+ // would claim they ran twice.
5183
+ if (step.executedIn !== "database" || attached === true) {
5184
+ return {
5185
+ ...step,
5186
+ options: [
5187
+ ...step.options
5188
+ ]
5189
+ };
5190
+ }
5191
+ attached = true;
5192
+ if (executedQueries.length === 0) {
5193
+ return {
5194
+ ...step,
5195
+ options: [
5196
+ ...step.options
5197
+ ],
5198
+ executedQueriesUnsupported: EXECUTED_QUERIES_UNSUPPORTED
5199
+ };
5200
+ }
5201
+ return {
5202
+ ...step,
5203
+ options: [
5204
+ ...step.options
5205
+ ],
5206
+ executedQueries: [
5207
+ ...executedQueries
5208
+ ]
5209
+ };
5210
+ });
5211
+ return {
5212
+ ...explanation,
5213
+ executionSteps
5214
+ };
5215
+ };
5216
+
5217
+ ;// CONCATENATED MODULE: ./src/plugins/query/formatExplanation.ts
5218
+ const OPTION_LABEL_WIDTH = 8;
5219
+ const WRAP_WIDTH = 68;
5220
+ /** Wraps `text` to `WRAP_WIDTH`, prefixing every line with `indent`. */ const wrap = (text, indent)=>{
5221
+ const lines = [];
5222
+ let line = "";
5223
+ for (const word of text.split(" ")){
5224
+ if (line.length > 0 && line.length + word.length + 1 > WRAP_WIDTH) {
5225
+ lines.push(indent + line);
5226
+ line = word;
5227
+ continue;
5228
+ }
5229
+ line = line.length === 0 ? word : `${line} ${word}`;
5230
+ }
5231
+ if (line.length > 0) {
5232
+ lines.push(indent + line);
5233
+ }
5234
+ return lines;
5235
+ };
5236
+ const COMPARATOR_SYMBOLS = {
5237
+ "equals": "===",
5238
+ "greater-than": ">",
5239
+ "greater-than-equals": ">=",
5240
+ "less-than": "<",
5241
+ "less-than-equals": "<="
5242
+ };
5243
+ const describeValue = (value)=>{
5244
+ if (value == null) {
5245
+ return "?";
5246
+ }
5247
+ if (value.k === "raw") {
5248
+ return typeof value.v === "string" ? `"${value.v}"` : String(value.v);
5249
+ }
5250
+ if (value.k === "date") {
5251
+ return value.v;
5252
+ }
5253
+ if (value.k === "array") {
5254
+ return `[${value.v.map(describeValue).join(", ")}]`;
5255
+ }
5256
+ return value.k === "undefined" ? "undefined" : String(value.v);
5257
+ };
5258
+ /** Renders a serialized expression back to something close to the source predicate. */ const describeExpression = (expression)=>{
5259
+ if (expression == null) {
5260
+ return "?";
5261
+ }
5262
+ if (expression.t === "operator") {
5263
+ return `${describeExpression(expression.left)} ${expression.operator} ${describeExpression(expression.right)}`;
5264
+ }
5265
+ if (expression.t === "comparator") {
5266
+ const left = describeExpression(expression.left);
5267
+ const right = describeExpression(expression.right);
5268
+ const symbol = COMPARATOR_SYMBOLS[expression.comparator];
5269
+ if (symbol == null) {
5270
+ return `${left}.${expression.comparator}(${right})${expression.negated === true ? " === false" : ""}`;
5271
+ }
5272
+ return `${left} ${expression.negated === true ? "!==" : symbol} ${right}`;
5273
+ }
5274
+ if (expression.t === "property") {
5275
+ return expression.path;
5276
+ }
5277
+ if (expression.t === "value") {
5278
+ return describeValue(expression.value);
5279
+ }
5280
+ return expression.t === "empty" ? "(no filter)" : "(not parsable)";
5281
+ };
5282
+ const describeOption = (option)=>{
5283
+ const detail = option.detail;
5284
+ if (detail == null) {
5285
+ return "";
5286
+ }
5287
+ if (option.name === "filter") {
5288
+ return detail.expression == null ? String(detail.expressionUnavailable ?? "") : describeExpression(detail.expression);
5289
+ }
5290
+ if (option.name === "sort") {
5291
+ return `${detail.propertyName} ${detail.direction}`;
5292
+ }
5293
+ if (option.name === "skip" || option.name === "take") {
5294
+ return String(detail.value);
5295
+ }
5296
+ if (option.name === "join") {
5297
+ return `${detail.kind} → ${detail.outerKey} = ${detail.innerKey}`;
5298
+ }
5299
+ if (option.name === "nearest") {
5300
+ return `${detail.propertyName}, ${detail.count} nearest`;
5301
+ }
5302
+ if (option.name === "map" || option.name === "group") {
5303
+ const fields = detail.fields;
5304
+ return fields.map((x)=>x.from === x.to ? x.from : `${x.from} → ${x.to}`).join(", ");
5305
+ }
5306
+ return "";
5307
+ };
5308
+ const formatStep = (step, lines)=>{
5309
+ const reason = step.reason == null ? "" : ` [${step.reason}]`;
5310
+ lines.push(` STEP ${step.step} of ${step.of} — ${step.executedIn}${reason}`);
5311
+ lines.push(...wrap(step.description, " "));
5312
+ if (step.explanation != null) {
5313
+ lines.push(...wrap(step.explanation, " "));
5314
+ }
5315
+ lines.push("");
5316
+ for (const option of step.options){
5317
+ lines.push(` ${option.name.padEnd(OPTION_LABEL_WIDTH)} ${describeOption(option)}`.trimEnd());
5318
+ }
5319
+ for (const executed of step.executedQueries ?? []){
5320
+ lines.push("");
5321
+ lines.push(...executed.text.split("\n").map((line)=>` ${line}`));
5322
+ if (executed.parameters != null && executed.parameters.length > 0) {
5323
+ lines.push(` parameters: ${JSON.stringify(executed.parameters)}`);
5324
+ }
5325
+ }
5326
+ if (step.executedQueriesUnsupported != null) {
5327
+ lines.push("");
5328
+ lines.push(...wrap(step.executedQueriesUnsupported, " "));
5329
+ }
5330
+ lines.push("");
5331
+ };
5332
+ /**
5333
+ * Renders an explanation for a terminal.
5334
+ *
5335
+ * The STEP headers carry the whole lesson: a reader who has never heard of pushdown still sees
5336
+ * that the statement in step 1 is not the entire query. Nobody should have to notice a missing
5337
+ * ORDER BY to work that out.
5338
+ */ const formatExplanation = (explanation)=>{
5339
+ const { collection, database, summary, executionSteps } = explanation;
5340
+ const stepCount = `${executionSteps.length} ${executionSteps.length === 1 ? "step" : "steps"}`;
5341
+ const lines = [
5342
+ `${collection} · ${database} · ${stepCount}`,
5343
+ ""
5344
+ ];
5345
+ for (const step of executionSteps){
5346
+ formatStep(step, lines);
5347
+ }
5348
+ lines.push(...wrap(summary.explanation, " "));
5349
+ return lines.join("\n");
5350
+ };
5351
+
4963
5352
  ;// CONCATENATED MODULE: ./src/plugins/query/types.ts
4964
5353
  var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4965
5354
  QueryOrdering["Descending"] = "desc";
@@ -4974,6 +5363,8 @@ var types_QueryOrdering = /*#__PURE__*/ function(QueryOrdering) {
4974
5363
 
4975
5364
 
4976
5365
 
5366
+
5367
+
4977
5368
  // EXTERNAL MODULE: ./src/expressions/evaluate.ts
4978
5369
  var evaluate = __webpack_require__(379);
4979
5370
  ;// CONCATENATED MODULE: ./src/plugins/wire/query.ts
@@ -5445,6 +5836,7 @@ const createRequestHandler = (options)=>{
5445
5836
  const queryOptions = deserializeQueryOptions(request.options, schema, resolveSchema, // Applied to the outer collection AND to every collection a join reaches, so a
5446
5837
  // join cannot be used to read around a scope
5447
5838
  (target)=>scopeExpressionFor(target, context, "query"));
5839
+ const executedQueries = [];
5448
5840
  return await new Promise((resolve)=>{
5449
5841
  plugin.query({
5450
5842
  // `false`: nothing here attaches to a change tracker — the tracker lives on
@@ -5453,7 +5845,9 @@ const createRequestHandler = (options)=>{
5453
5845
  schemas: schemas,
5454
5846
  id: (0,uuid/* .uuid */.u)(8),
5455
5847
  source: "RequestHandler",
5456
- action: "query"
5848
+ action: "query",
5849
+ explain: request.explain,
5850
+ executedQueries
5457
5851
  }, (result)=>{
5458
5852
  if (result.ok === Result/* .PluginEventResult.ERROR */.D.ERROR) {
5459
5853
  resolve(failed(result.error));
@@ -5462,7 +5856,13 @@ const createRequestHandler = (options)=>{
5462
5856
  resolve({
5463
5857
  ok: true,
5464
5858
  kind: "query",
5465
- value: result.data.value
5859
+ value: result.data.value,
5860
+ // Only when asked, and only what the plugin reported. A plugin
5861
+ // that reported nothing sends nothing, and the caller marks the
5862
+ // remote step as not reported.
5863
+ ...request.explain === true && executedQueries.length > 0 ? {
5864
+ executedQueries
5865
+ } : {}
5466
5866
  });
5467
5867
  });
5468
5868
  });
@@ -6111,6 +6511,10 @@ class EphemeralDataPlugin {
6111
6511
  }
6112
6512
  innerRows.push(cloneRecord(record));
6113
6513
  }
6514
+ const narrowing = outerKeys == null ? "full scan" : `narrowed by ${outerKeys.size} outer ${outerKeys.size === 1 ? "key" : "keys"}`;
6515
+ event.executedQueries.push({
6516
+ text: `${innerSchema.collectionName}: scanned ${innerRows.length} in-memory ${innerRows.length === 1 ? "record" : "records"} for join inner side (${narrowing})`
6517
+ });
6114
6518
  done({
6115
6519
  ok: "success",
6116
6520
  innerSide: {
@@ -6220,7 +6624,13 @@ class EphemeralDataPlugin {
6220
6624
  * collection to pair it with three rows.
6221
6625
  *
6222
6626
  * `cloned` is in storage shape, so the keys are read by resolved column name.
6223
- */ const joinOption = operation.options.getLast("join");
6627
+ */ // No statement to quote — an ephemeral store walks its own records. Said
6628
+ // plainly so `.explain()` does not leave a reader wondering whether the
6629
+ // plugin simply failed to report. Before the inner side, to match execution order.
6630
+ event.executedQueries.push({
6631
+ text: `${operation.schema.collectionName}: scanned ${cloned.length} in-memory ${cloned.length === 1 ? "record" : "records"}`
6632
+ });
6633
+ const joinOption = operation.options.getLast("join");
6224
6634
  const outerKeys = joinOption == null ? null : distinctJoinKeys(cloned, joinOption.value.outerKey, joinOption.value.semiJoinKeyThreshold, {
6225
6635
  storageShape: true
6226
6636
  });
@@ -6313,7 +6723,7 @@ class RetryDbPlugin {
6313
6723
  * `PropertyInfo` objects carrying functions and would not survive `JSON.stringify`. A filter is
6314
6724
  * identified by its source text plus its params, which is what the expression is derived from
6315
6725
  * anyway — two queries with the same source and params are the same query.
6316
- */ const describeOption = (option)=>{
6726
+ */ const CacheDbPlugin_describeOption = (option)=>{
6317
6727
  const value = option.value;
6318
6728
  switch(option.name){
6319
6729
  case "filter":
@@ -6347,7 +6757,7 @@ class CacheDbPlugin {
6347
6757
  /** `schemaId` first, so invalidating a schema is a prefix match. */ keyFor(event) {
6348
6758
  const parts = [];
6349
6759
  event.operation.options.forEach((option)=>{
6350
- parts.push(`${option.name}:${describeOption(option)}`);
6760
+ parts.push(`${option.name}:${CacheDbPlugin_describeOption(option)}`);
6351
6761
  });
6352
6762
  return `${String(event.operation.schema.id)}\u0000${parts.join("\u0001")}`;
6353
6763
  }
@@ -6377,6 +6787,12 @@ class CacheDbPlugin {
6377
6787
  // Re-set to move it to the end of the insertion order: most recently used.
6378
6788
  this.entries.delete(key);
6379
6789
  this.entries.set(key, cached);
6790
+ // Said rather than left blank. A hit means no database was touched, which is a fact
6791
+ // worth reporting — and an empty report would otherwise read as a plugin that failed
6792
+ // to say what it ran.
6793
+ event.executedQueries.push({
6794
+ text: "cache hit — no query was executed"
6795
+ });
6380
6796
  done(Result/* .PluginEventResult.success */.D.success(event.id, this.rebuild(cached)));
6381
6797
  return;
6382
6798
  }
@@ -6774,8 +7190,26 @@ __webpack_require__.d(__webpack_exports__, {
6774
7190
  class QueryOptionsCollection {
6775
7191
  options = new Map();
6776
7192
  nextExecutionTarget = "database";
7193
+ nextExecutionReason = null;
6777
7194
  nextIndex = 0;
6778
7195
  enumeratedItems = [];
7196
+ /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */ cutOverToMemory(reason) {
7197
+ this.nextExecutionTarget = "memory";
7198
+ if (this.nextExecutionReason == null) {
7199
+ this.nextExecutionReason = reason;
7200
+ }
7201
+ }
7202
+ /**
7203
+ * True when `split()` or `splitAt()` produced this collection.
7204
+ *
7205
+ * Those rebuild each half by re-adding its options, which re-derives execution targets
7206
+ * without the options that caused them — a post-join filter alone in the memory half
7207
+ * derives back to `"database"`. Anything reading `target` as a report of where work runs
7208
+ * has to reject a derived collection; see `explainQuery`.
7209
+ */ derived = false;
7210
+ get isDerived() {
7211
+ return this.derived;
7212
+ }
6779
7213
  get items() {
6780
7214
  return this.options;
6781
7215
  }
@@ -6796,7 +7230,7 @@ class QueryOptionsCollection {
6796
7230
  if (mapValue.fields.some((x)=>x.isRename === true) || mapValue.fields.some((x)=>x.property?.isUnmapped === true)) {
6797
7231
  // Cut over to memory execution since we are renaming a property with .map
6798
7232
  // We do not want to figure out how the new name flows through the entire query
6799
- this.nextExecutionTarget = "memory";
7233
+ this.cutOverToMemory("map-rename");
6800
7234
  }
6801
7235
  }
6802
7236
  if (name === "filter") {
@@ -6808,13 +7242,13 @@ class QueryOptionsCollection {
6808
7242
  return;
6809
7243
  }
6810
7244
  if (filterValue.expression.type === "not-parsable") {
6811
- this.nextExecutionTarget = "memory";
7245
+ this.cutOverToMemory("not-parsable");
6812
7246
  } else {
6813
7247
  (0,_expressions_utils__rspack_import_0/* .forEach */.j)(filterValue.expression, (expression)=>{
6814
7248
  if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.isUnmapped) {
6815
7249
  // Cut over to memory execution, unmapped properties are not in the database and
6816
7250
  // cannot be queried
6817
- this.nextExecutionTarget = "memory";
7251
+ this.cutOverToMemory("unmapped-property");
6818
7252
  return false;
6819
7253
  }
6820
7254
  if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.hasRenamedSegments) {
@@ -6822,7 +7256,7 @@ class QueryOptionsCollection {
6822
7256
  // `from` (storage) names, but filter selectors reference the
6823
7257
  // in-memory names. Memory execution runs after deserialization,
6824
7258
  // where the in-memory names exist
6825
- this.nextExecutionTarget = "memory";
7259
+ this.cutOverToMemory("renamed-property");
6826
7260
  return false;
6827
7261
  }
6828
7262
  return true;
@@ -6833,8 +7267,10 @@ class QueryOptionsCollection {
6833
7267
  const sortValue = value;
6834
7268
  // Same rule as filters: sort selectors reference in-memory names, which
6835
7269
  // only exist after deserialization when the property is renamed or unmapped
6836
- if (sortValue.property != null && (sortValue.property.isUnmapped || sortValue.property.hasRenamedSegments)) {
6837
- this.nextExecutionTarget = "memory";
7270
+ if (sortValue.property != null && sortValue.property.isUnmapped) {
7271
+ this.cutOverToMemory("unmapped-property");
7272
+ } else if (sortValue.property != null && sortValue.property.hasRenamedSegments) {
7273
+ this.cutOverToMemory("renamed-property");
6838
7274
  }
6839
7275
  }
6840
7276
  if (name === "nearest") {
@@ -6845,8 +7281,10 @@ class QueryOptionsCollection {
6845
7281
  //
6846
7282
  // This is also what lets every translator's in-memory fallback read the column by
6847
7283
  // its resolved name — anything whose storage name differs never reaches them.
6848
- if (nearestValue.property != null && (nearestValue.property.isUnmapped || nearestValue.property.hasRenamedSegments)) {
6849
- this.nextExecutionTarget = "memory";
7284
+ if (nearestValue.property != null && nearestValue.property.isUnmapped) {
7285
+ this.cutOverToMemory("unmapped-property");
7286
+ } else if (nearestValue.property != null && nearestValue.property.hasRenamedSegments) {
7287
+ this.cutOverToMemory("renamed-property");
6850
7288
  }
6851
7289
  }
6852
7290
  if (name === "join") {
@@ -6858,7 +7296,7 @@ class QueryOptionsCollection {
6858
7296
  // Set BEFORE the item is created, unlike `nearest`'s ratchet below, because this
6859
7297
  // moves the join option itself rather than everything after it.
6860
7298
  if (joinValue.crossPlugin === true) {
6861
- this.nextExecutionTarget = "memory";
7299
+ this.cutOverToMemory("cross-plugin-join");
6862
7300
  }
6863
7301
  }
6864
7302
  const item = {
@@ -6866,7 +7304,10 @@ class QueryOptionsCollection {
6866
7304
  option: {
6867
7305
  name,
6868
7306
  target: this.nextExecutionTarget,
6869
- value
7307
+ value,
7308
+ ...this.nextExecutionReason == null ? {} : {
7309
+ reason: this.nextExecutionReason
7310
+ }
6870
7311
  }
6871
7312
  };
6872
7313
  this.nextIndex++;
@@ -6889,7 +7330,7 @@ class QueryOptionsCollection {
6889
7330
  //
6890
7331
  // A plugin that DID push the search down loses nothing but the chance to also
6891
7332
  // push down what follows it, which is a limit over ten rows.
6892
- this.nextExecutionTarget = "memory";
7333
+ this.cutOverToMemory("after-nearest");
6893
7334
  }
6894
7335
  if (name === "join") {
6895
7336
  // Everything AFTER a join runs in memory, for the same reason as `nearest`: this
@@ -6900,7 +7341,7 @@ class QueryOptionsCollection {
6900
7341
  // OUTER rows read, not the pairs produced — a plausible-looking result with the
6901
7342
  // wrong number of rows in it. Conjuncts that can safely run earlier are split off
6902
7343
  // by the query builder BEFORE dispatch, which is the only exception.
6903
- this.nextExecutionTarget = "memory";
7344
+ this.cutOverToMemory("after-join");
6904
7345
  }
6905
7346
  }
6906
7347
  /**
@@ -6914,6 +7355,8 @@ class QueryOptionsCollection {
6914
7355
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
6915
7356
  const before = new QueryOptionsCollection();
6916
7357
  const after = new QueryOptionsCollection();
7358
+ before.derived = true;
7359
+ after.derived = true;
6917
7360
  let at = null;
6918
7361
  for(let i = 0, length = sortedItems.length; i < length; i++){
6919
7362
  const { option } = sortedItems[i];
@@ -6947,10 +7390,12 @@ class QueryOptionsCollection {
6947
7390
  ]
6948
7391
  ]));
6949
7392
  const nextExecutionTarget = this.nextExecutionTarget;
7393
+ const nextExecutionReason = this.nextExecutionReason;
6950
7394
  const nextIndex = this.nextIndex;
6951
7395
  return ()=>{
6952
7396
  this.options = new Map(options);
6953
7397
  this.nextExecutionTarget = nextExecutionTarget;
7398
+ this.nextExecutionReason = nextExecutionReason;
6954
7399
  this.nextIndex = nextIndex;
6955
7400
  this.enumeratedItems = [];
6956
7401
  };
@@ -6960,6 +7405,8 @@ class QueryOptionsCollection {
6960
7405
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
6961
7406
  const memoryQueryOptionsCollection = new QueryOptionsCollection();
6962
7407
  const databaseQueryOptionsCollection = new QueryOptionsCollection();
7408
+ memoryQueryOptionsCollection.derived = true;
7409
+ databaseQueryOptionsCollection.derived = true;
6963
7410
  for(let i = 0, length = sortedItems.length; i < length; i++){
6964
7411
  const sortedItem = sortedItems[i];
6965
7412
  if (sortedItem.option.target === "database") {
@@ -13743,6 +14190,7 @@ __webpack_require__.d(__webpack_exports__, {
13743
14190
  ContainerBlock: () => (/* reexport safe */ _codegen__rspack_import_1.ContainerBlock),
13744
14191
  DEFAULT_SEMI_JOIN_KEY_THRESHOLD: () => (/* reexport safe */ _plugins__rspack_import_7.DEFAULT_SEMI_JOIN_KEY_THRESHOLD),
13745
14192
  DataTranslator: () => (/* reexport safe */ _plugins__rspack_import_7.DataTranslator),
14193
+ EXECUTED_QUERIES_UNSUPPORTED: () => (/* reexport safe */ _plugins__rspack_import_7.EXECUTED_QUERIES_UNSUPPORTED),
13746
14194
  EXPRESSION_TYPES: () => (/* reexport safe */ _expressions__rspack_import_4.EXPRESSION_TYPES),
13747
14195
  EmptyExpression: () => (/* reexport safe */ _expressions__rspack_import_4.EmptyExpression),
13748
14196
  EphemeralDataPlugin: () => (/* reexport safe */ _plugins__rspack_import_7.EphemeralDataPlugin),
@@ -13754,6 +14202,7 @@ __webpack_require__.d(__webpack_exports__, {
13754
14202
  IfBuilder: () => (/* reexport safe */ _codegen__rspack_import_1.IfBuilder),
13755
14203
  JsonTranslator: () => (/* reexport safe */ _plugins__rspack_import_7.JsonTranslator),
13756
14204
  LOG_LEVELS: () => (/* reexport safe */ _utilities__rspack_import_10.LOG_LEVELS),
14205
+ MEMORY_EXECUTION_EXPLANATIONS: () => (/* reexport safe */ _plugins__rspack_import_7.MEMORY_EXECUTION_EXPLANATIONS),
13757
14206
  MemoryDataCollection: () => (/* reexport safe */ _collections__rspack_import_2.MemoryDataCollection),
13758
14207
  NotParsableExpression: () => (/* reexport safe */ _expressions__rspack_import_4.NotParsableExpression),
13759
14208
  ObjectBuilder: () => (/* reexport safe */ _codegen__rspack_import_1.ObjectBuilder),
@@ -13839,9 +14288,11 @@ __webpack_require__.d(__webpack_exports__, {
13839
14288
  distinctJoinKeys: () => (/* reexport safe */ _plugins__rspack_import_7.distinctJoinKeys),
13840
14289
  evaluate: () => (/* reexport safe */ _expressions__rspack_import_4.evaluate),
13841
14290
  executeJoin: () => (/* reexport safe */ _plugins__rspack_import_7.executeJoin),
14291
+ explainQuery: () => (/* reexport safe */ _plugins__rspack_import_7.explainQuery),
13842
14292
  extractTypeInfo: () => (/* reexport safe */ _schema__rspack_import_9.extractTypeInfo),
13843
14293
  fastHash: () => (/* reexport safe */ _utilities__rspack_import_10.fastHash),
13844
14294
  forEach: () => (/* reexport safe */ _expressions__rspack_import_4.forEach),
14295
+ formatExplanation: () => (/* reexport safe */ _plugins__rspack_import_7.formatExplanation),
13845
14296
  getLogLevel: () => (/* reexport safe */ _utilities__rspack_import_10.getLogLevel),
13846
14297
  getProperties: () => (/* reexport safe */ _expressions__rspack_import_4.getProperties),
13847
14298
  hasPrimitiveElements: () => (/* reexport safe */ _schema__rspack_import_9.hasPrimitiveElements),
@@ -13889,7 +14340,8 @@ __webpack_require__.d(__webpack_exports__, {
13889
14340
  toStrictPredicate: () => (/* reexport safe */ _expressions__rspack_import_4.toStrictPredicate),
13890
14341
  unsafeCast: () => (/* reexport safe */ _utilities__rspack_import_10.unsafeCast),
13891
14342
  uuid: () => (/* reexport safe */ _utilities__rspack_import_10.uuid),
13892
- uuidv4: () => (/* reexport safe */ _utilities__rspack_import_10.uuidv4)
14343
+ uuidv4: () => (/* reexport safe */ _utilities__rspack_import_10.uuidv4),
14344
+ withExecutedQueries: () => (/* reexport safe */ _plugins__rspack_import_7.withExecutedQueries)
13893
14345
  });
13894
14346
  /* import */ var _assertions__rspack_import_0 = __webpack_require__(126);
13895
14347
  /* import */ var _codegen__rspack_import_1 = __webpack_require__(80);
@@ -13898,7 +14350,7 @@ __webpack_require__.d(__webpack_exports__, {
13898
14350
  /* import */ var _expressions__rspack_import_4 = __webpack_require__(138);
13899
14351
  /* import */ var _performance__rspack_import_5 = __webpack_require__(971);
13900
14352
  /* import */ var _pipeline__rspack_import_6 = __webpack_require__(314);
13901
- /* import */ var _plugins__rspack_import_7 = __webpack_require__(771);
14353
+ /* import */ var _plugins__rspack_import_7 = __webpack_require__(301);
13902
14354
  /* import */ var _results__rspack_import_8 = __webpack_require__(264);
13903
14355
  /* import */ var _schema__rspack_import_9 = __webpack_require__(755);
13904
14356
  /* import */ var _utilities__rspack_import_10 = __webpack_require__(222);