@routier/core 0.3.0 → 0.5.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 (35) hide show
  1. package/README.md +2 -2
  2. package/dist/index.cjs +549 -462
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +0 -1
  5. package/dist/index.js +554 -462
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/TelemetryDbPlugin.d.ts +43 -0
  8. package/dist/plugins/index.cjs +538 -24
  9. package/dist/plugins/index.cjs.map +1 -1
  10. package/dist/plugins/index.d.ts +1 -0
  11. package/dist/plugins/index.js +544 -22
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/plugins/query/QueryOptionsCollection.d.ts +13 -0
  14. package/dist/plugins/query/explain.d.ts +82 -0
  15. package/dist/plugins/query/formatExplanation.d.ts +9 -0
  16. package/dist/plugins/query/index.d.ts +2 -0
  17. package/dist/plugins/query/types.d.ts +11 -0
  18. package/dist/plugins/types.d.ts +43 -1
  19. package/dist/plugins/wire/types.d.ts +17 -1
  20. package/dist/utilities/index.cjs +43 -12
  21. package/dist/utilities/index.cjs.map +1 -1
  22. package/dist/utilities/index.js +43 -12
  23. package/dist/utilities/index.js.map +1 -1
  24. package/package.json +6 -10
  25. package/dist/capabilities/Capability.d.ts +0 -11
  26. package/dist/capabilities/PerformanceCapability.d.ts +0 -13
  27. package/dist/capabilities/TracingCapability.d.ts +0 -11
  28. package/dist/capabilities/index.cjs +0 -820
  29. package/dist/capabilities/index.cjs.map +0 -1
  30. package/dist/capabilities/index.d.ts +0 -4
  31. package/dist/capabilities/index.js +0 -808
  32. package/dist/capabilities/index.js.map +0 -1
  33. package/dist/capabilities/performance/PerformanceTracker.d.ts +0 -11
  34. package/dist/capabilities/tracing/CallTraceManager.d.ts +0 -12
  35. package/dist/capabilities/types.d.ts +0 -17
@@ -6,8 +6,21 @@ export type QueryCollectionItem<T, K extends QueryOptionName> = {
6
6
  export declare class QueryOptionsCollection<T> {
7
7
  private options;
8
8
  private nextExecutionTarget;
9
+ private nextExecutionReason;
9
10
  private nextIndex;
10
11
  private enumeratedItems;
12
+ /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */
13
+ private cutOverToMemory;
14
+ /**
15
+ * True when `split()` or `splitAt()` produced this collection.
16
+ *
17
+ * Those rebuild each half by re-adding its options, which re-derives execution targets
18
+ * without the options that caused them — a post-join filter alone in the memory half
19
+ * derives back to `"database"`. Anything reading `target` as a report of where work runs
20
+ * has to reject a derived collection; see `explainQuery`.
21
+ */
22
+ private derived;
23
+ get isDerived(): boolean;
11
24
  get items(): Map<keyof QueryOptionValueMap<unknown>, QueryCollectionItem<any, any>[]>;
12
25
  get isEmpty(): boolean;
13
26
  static EMPTY<R>(): QueryOptionsCollection<R>;
@@ -0,0 +1,82 @@
1
+ import { MemoryExecutionReason, QueryOptionExecutionTarget, QueryOptionName } from "./types";
2
+ import { QueryOptionsCollection } from "./QueryOptionsCollection";
3
+ /**
4
+ * One sentence per reason code, written for someone meeting pushdown for the first time.
5
+ *
6
+ * Beside the codes rather than in the formatter, so console output, a failing test and the
7
+ * docs all say the same thing.
8
+ */
9
+ export declare const MEMORY_EXECUTION_EXPLANATIONS: Record<MemoryExecutionReason, string>;
10
+ /**
11
+ * One thing a backend actually executed, in the backend's own language.
12
+ *
13
+ * A plugin pushes these onto `DbPluginQueryEvent.executedQueries` as it runs them, so a join —
14
+ * which reads twice — reports both, in execution order. `text` is not required to be SQL: a
15
+ * key-value store describes what it did in whatever terms it has.
16
+ */
17
+ export type ExecutedQuery = {
18
+ text: string;
19
+ parameters?: unknown[];
20
+ };
21
+ export type ExplainedOption = {
22
+ index: number;
23
+ name: QueryOptionName;
24
+ detail?: Record<string, unknown>;
25
+ };
26
+ export declare const EXECUTED_QUERIES_UNSUPPORTED = "This plugin did not report what it executed. It may not support explain.";
27
+ export type ExecutionStep = {
28
+ step: number;
29
+ of: number;
30
+ executedIn: QueryOptionExecutionTarget;
31
+ description: string;
32
+ options: ExplainedOption[];
33
+ /** Set on database steps once the plugin has reported. */
34
+ executedQueries?: ExecutedQuery[];
35
+ /** Set on the first database step instead, when the plugin reported nothing. */
36
+ executedQueriesUnsupported?: string;
37
+ /** Set on memory steps only. */
38
+ reason?: MemoryExecutionReason;
39
+ explanation?: string;
40
+ };
41
+ export type QueryExplanationSummary = {
42
+ database: number;
43
+ memory: number;
44
+ /** Deduped, in first-seen order. Empty when the whole query pushed down. */
45
+ reasons: MemoryExecutionReason[];
46
+ explanation: string;
47
+ };
48
+ export type QueryExplanation = {
49
+ collection: string;
50
+ database: string;
51
+ summary: QueryExplanationSummary;
52
+ executionSteps: ExecutionStep[];
53
+ plugin: {
54
+ kind: string;
55
+ };
56
+ };
57
+ export type ExplainContext = {
58
+ collection: string;
59
+ database: string;
60
+ pluginKind: string;
61
+ };
62
+ /**
63
+ * Builds the explanation from the resolved options, with no plugin involvement.
64
+ *
65
+ * Takes the collection BEFORE `split()`, and throws otherwise. Splitting re-adds each half
66
+ * into a fresh collection, which re-derives targets without the options that caused them — a
67
+ * post-join filter alone in the memory half derives back to `"database"`, and the document
68
+ * would report memory work as having run in the database.
69
+ */
70
+ export declare const explainQuery: (options: QueryOptionsCollection<any>, context: ExplainContext) => QueryExplanation;
71
+ /**
72
+ * Attaches what the backend reported to the step that was sent to it.
73
+ *
74
+ * Reporting is optional for a plugin, so an empty report is not an error: the step is marked
75
+ * `executedQueriesUnsupported` instead, and the rest of the explanation stands — the pushdown
76
+ * analysis comes from the options and is correct with or without the plugin's statements.
77
+ *
78
+ * Copies the steps rather than writing into them, so the explanation a caller already holds
79
+ * does not gain statements after the fact. Options and their details are shared with the
80
+ * original — nothing mutates them, and copying deeper would only look safer than it is.
81
+ */
82
+ export declare const withExecutedQueries: (explanation: QueryExplanation, executedQueries: ExecutedQuery[]) => QueryExplanation;
@@ -0,0 +1,9 @@
1
+ import { QueryExplanation } from "./explain";
2
+ /**
3
+ * Renders an explanation for a terminal.
4
+ *
5
+ * The STEP headers carry the whole lesson: a reader who has never heard of pushdown still sees
6
+ * that the statement in step 1 is not the entire query. Nobody should have to notice a missing
7
+ * ORDER BY to work that out.
8
+ */
9
+ export declare const formatExplanation: (explanation: QueryExplanation) => string;
@@ -1,3 +1,5 @@
1
+ export * from './explain';
2
+ export * from './formatExplanation';
1
3
  export * from './join';
2
4
  export * from './Query';
3
5
  export * from './QueryOptionsCollection';
@@ -19,10 +19,21 @@ export type QueryField = {
19
19
  };
20
20
  export type QueryOptionExecutionTarget = "database" | "memory";
21
21
  export type QueryOptionName = keyof QueryOptionValueMap<unknown>;
22
+ /**
23
+ * Why an option runs in memory rather than in the database.
24
+ *
25
+ * A code rather than a sentence, so a test can assert on it — the sentences live in
26
+ * `MEMORY_EXECUTION_EXPLANATIONS`. Every cause is a ratchet, because `nextExecutionTarget`
27
+ * never returns to `"database"`, so the code recorded is the FIRST cause and it stays on every
28
+ * option after it. Reporting a later one would name a symptom of this one.
29
+ */
30
+ export type MemoryExecutionReason = "not-parsable" | "unmapped-property" | "renamed-property" | "map-rename" | "after-nearest" | "after-join" | "cross-plugin-join";
22
31
  export type QueryOption<T, K extends QueryOptionName> = {
23
32
  name: QueryOptionName;
24
33
  value: QueryOptionValueMap<T>[K];
25
34
  target: QueryOptionExecutionTarget;
35
+ /** Set only when `target` is `"memory"`. */
36
+ reason?: MemoryExecutionReason;
26
37
  };
27
38
  export type QueryOptionValueMap<T extends {}> = {
28
39
  skip: number;
@@ -1,4 +1,5 @@
1
1
  import { PluginEventCallbackPartialResult, PluginEventCallbackResult } from "../results";
2
+ import { ExecutedQuery } from "./query/explain";
2
3
  import { QueryOptionsCollection } from "./query/QueryOptionsCollection";
3
4
  import { CompiledSchema, InferType } from '../schema';
4
5
  import { BulkPersistChanges, BulkPersistResult, SchemaCollection } from "../collections";
@@ -50,7 +51,48 @@ export interface IDbPlugin {
50
51
  /**
51
52
  * Event for a query operation, including schema, parent, and the query operation.
52
53
  */
53
- export type DbPluginQueryEvent<TRoot extends {}, TShape> = DbPluginOperationEvent<IQuery<TRoot, TShape>>;
54
+ export type DbPluginQueryEvent<TRoot extends {}, TShape> = DbPluginOperationEvent<IQuery<TRoot, TShape>> & {
55
+ /**
56
+ * Whether the caller asked for an explanation. Required, never optional: a query is
57
+ * either explained or it is not, and "unset" is not a third state.
58
+ *
59
+ * A plugin is free to ignore it. One that reports unconditionally is correct; one that
60
+ * checks the flag to skip building report strings is also correct. What a plugin must
61
+ * NOT do is treat `true` as an instruction it has to obey — a plugin that cannot report
62
+ * simply doesn't, and the datastore marks the step as not reported.
63
+ */
64
+ explain: boolean;
65
+ /**
66
+ * Where a plugin reports what it executed. Pushing to it is how a plugin supports
67
+ * `.explain()` — a plugin that never pushes still answers queries, and its explanations
68
+ * mark the database step as not reported (`executedQueriesUnsupported`) instead of
69
+ * showing statements.
70
+ *
71
+ * The datastore decides whether anyone sees it: with `explain` on it reads the array and
72
+ * an empty one means "not supported"; with `explain` off it takes no action either way.
73
+ *
74
+ * An array the DATASTORE creates and the plugin pushes into, rather than a value the plugin
75
+ * returns. The result envelope is rebuilt in at least six places between a plugin and the
76
+ * caller — the memory half re-translates, joins build fresh tuple values, the cache
77
+ * reconstructs from stored entries — so anything carried on it is discarded before arrival.
78
+ * The event is not rebuilt, and an array survives the shallow spread in `ConcurrencyDbPlugin`
79
+ * because both sides then hold the same array. Assigning a new one would not.
80
+ *
81
+ * Push once per query actually executed, in execution order, so a join reports both reads.
82
+ * `text` is whatever the backend runs — SQL for a SQL engine, a description of the access
83
+ * path for a store that has no statement. A plugin that answered without touching its
84
+ * backend pushes a description of that instead — `CacheDbPlugin` pushes "cache hit" —
85
+ * because pushing nothing reads as "this plugin does not report".
86
+ *
87
+ * Push AFTER the query runs, not before. `RetryDbPlugin` re-invokes with the same event, so
88
+ * a plugin that pushes first reports an entry per failed attempt.
89
+ *
90
+ * The array accumulates for as long as the event lives. That is why `.explain()` is not
91
+ * offered on a subscribed queryable: `subscribeQuery` builds its event once and re-issues it
92
+ * on every change notification, which would grow this without bound.
93
+ */
94
+ executedQueries: ExecutedQuery[];
95
+ };
54
96
  /**
55
97
  * Event for bulk operations, including schema, parent, and the entity changes.
56
98
  */
@@ -1,5 +1,6 @@
1
1
  import { SerializedExpression } from "../../expressions";
2
2
  import { JoinKind } from "../query/join";
3
+ import { ExecutedQuery } from "../query/explain";
3
4
  import { QueryOrdering } from "../query/types";
4
5
  /**
5
6
  * The wire format for a whole Routier operation.
@@ -75,6 +76,12 @@ export type SerializedQueryRequest = {
75
76
  kind: "query";
76
77
  collectionName: string;
77
78
  options: SerializedQueryOption[];
79
+ /**
80
+ * Whether the caller wants the response to say what the server ran. Required — a query is
81
+ * either explained or it is not. A server whose plugin does not report answers `true` the
82
+ * same as `false`, and the caller's explanation marks the remote step as not reported.
83
+ */
84
+ explain: boolean;
78
85
  };
79
86
  /** One entity update, as `EntityUpdateInfo` minus nothing — every field of it is already JSON. */
80
87
  export type SerializedUpdate = {
@@ -101,10 +108,19 @@ export type SerializedDestroyRequest = {
101
108
  };
102
109
  export type SerializedRequest = SerializedQueryRequest | SerializedPersistRequest | SerializedDestroyRequest;
103
110
  /** What a receiver sends back. Errors are a value, not a transport status. */
104
- export type SerializedResponse = {
111
+ export type SerializedResponse =
112
+ /**
113
+ * `executedQueries` carries what the SERVER's plugin ran, so `.explain()` on a client sees
114
+ * through the wire rather than reporting a blank. Optional on the response, unlike on a
115
+ * local event: a plugin that does not report has nothing to send, and the client's
116
+ * explanation then marks the remote step as not reported. There is no flag on either end —
117
+ * the wire forwards whatever the plugin pushed, or nothing.
118
+ */
119
+ {
105
120
  ok: true;
106
121
  kind: "query";
107
122
  value: unknown;
123
+ executedQueries?: ExecutedQuery[];
108
124
  } | {
109
125
  ok: true;
110
126
  kind: "persist";
@@ -150,8 +150,26 @@ __webpack_require__.d(__webpack_exports__, {
150
150
  class QueryOptionsCollection {
151
151
  options = new Map();
152
152
  nextExecutionTarget = "database";
153
+ nextExecutionReason = null;
153
154
  nextIndex = 0;
154
155
  enumeratedItems = [];
156
+ /** Cuts over to memory execution, keeping the first cause. See `MemoryExecutionReason`. */ cutOverToMemory(reason) {
157
+ this.nextExecutionTarget = "memory";
158
+ if (this.nextExecutionReason == null) {
159
+ this.nextExecutionReason = reason;
160
+ }
161
+ }
162
+ /**
163
+ * True when `split()` or `splitAt()` produced this collection.
164
+ *
165
+ * Those rebuild each half by re-adding its options, which re-derives execution targets
166
+ * without the options that caused them — a post-join filter alone in the memory half
167
+ * derives back to `"database"`. Anything reading `target` as a report of where work runs
168
+ * has to reject a derived collection; see `explainQuery`.
169
+ */ derived = false;
170
+ get isDerived() {
171
+ return this.derived;
172
+ }
155
173
  get items() {
156
174
  return this.options;
157
175
  }
@@ -172,7 +190,7 @@ class QueryOptionsCollection {
172
190
  if (mapValue.fields.some((x)=>x.isRename === true) || mapValue.fields.some((x)=>x.property?.isUnmapped === true)) {
173
191
  // Cut over to memory execution since we are renaming a property with .map
174
192
  // We do not want to figure out how the new name flows through the entire query
175
- this.nextExecutionTarget = "memory";
193
+ this.cutOverToMemory("map-rename");
176
194
  }
177
195
  }
178
196
  if (name === "filter") {
@@ -184,13 +202,13 @@ class QueryOptionsCollection {
184
202
  return;
185
203
  }
186
204
  if (filterValue.expression.type === "not-parsable") {
187
- this.nextExecutionTarget = "memory";
205
+ this.cutOverToMemory("not-parsable");
188
206
  } else {
189
207
  (0,_expressions_utils__rspack_import_0/* .forEach */.j)(filterValue.expression, (expression)=>{
190
208
  if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.isUnmapped) {
191
209
  // Cut over to memory execution, unmapped properties are not in the database and
192
210
  // cannot be queried
193
- this.nextExecutionTarget = "memory";
211
+ this.cutOverToMemory("unmapped-property");
194
212
  return false;
195
213
  }
196
214
  if ((0,_assertions__rspack_import_1.isPropertyExpression)(expression) && expression.property.hasRenamedSegments) {
@@ -198,7 +216,7 @@ class QueryOptionsCollection {
198
216
  // `from` (storage) names, but filter selectors reference the
199
217
  // in-memory names. Memory execution runs after deserialization,
200
218
  // where the in-memory names exist
201
- this.nextExecutionTarget = "memory";
219
+ this.cutOverToMemory("renamed-property");
202
220
  return false;
203
221
  }
204
222
  return true;
@@ -209,8 +227,10 @@ class QueryOptionsCollection {
209
227
  const sortValue = value;
210
228
  // Same rule as filters: sort selectors reference in-memory names, which
211
229
  // only exist after deserialization when the property is renamed or unmapped
212
- if (sortValue.property != null && (sortValue.property.isUnmapped || sortValue.property.hasRenamedSegments)) {
213
- this.nextExecutionTarget = "memory";
230
+ if (sortValue.property != null && sortValue.property.isUnmapped) {
231
+ this.cutOverToMemory("unmapped-property");
232
+ } else if (sortValue.property != null && sortValue.property.hasRenamedSegments) {
233
+ this.cutOverToMemory("renamed-property");
214
234
  }
215
235
  }
216
236
  if (name === "nearest") {
@@ -221,8 +241,10 @@ class QueryOptionsCollection {
221
241
  //
222
242
  // This is also what lets every translator's in-memory fallback read the column by
223
243
  // its resolved name — anything whose storage name differs never reaches them.
224
- if (nearestValue.property != null && (nearestValue.property.isUnmapped || nearestValue.property.hasRenamedSegments)) {
225
- this.nextExecutionTarget = "memory";
244
+ if (nearestValue.property != null && nearestValue.property.isUnmapped) {
245
+ this.cutOverToMemory("unmapped-property");
246
+ } else if (nearestValue.property != null && nearestValue.property.hasRenamedSegments) {
247
+ this.cutOverToMemory("renamed-property");
226
248
  }
227
249
  }
228
250
  if (name === "join") {
@@ -234,7 +256,7 @@ class QueryOptionsCollection {
234
256
  // Set BEFORE the item is created, unlike `nearest`'s ratchet below, because this
235
257
  // moves the join option itself rather than everything after it.
236
258
  if (joinValue.crossPlugin === true) {
237
- this.nextExecutionTarget = "memory";
259
+ this.cutOverToMemory("cross-plugin-join");
238
260
  }
239
261
  }
240
262
  const item = {
@@ -242,7 +264,10 @@ class QueryOptionsCollection {
242
264
  option: {
243
265
  name,
244
266
  target: this.nextExecutionTarget,
245
- value
267
+ value,
268
+ ...this.nextExecutionReason == null ? {} : {
269
+ reason: this.nextExecutionReason
270
+ }
246
271
  }
247
272
  };
248
273
  this.nextIndex++;
@@ -265,7 +290,7 @@ class QueryOptionsCollection {
265
290
  //
266
291
  // A plugin that DID push the search down loses nothing but the chance to also
267
292
  // push down what follows it, which is a limit over ten rows.
268
- this.nextExecutionTarget = "memory";
293
+ this.cutOverToMemory("after-nearest");
269
294
  }
270
295
  if (name === "join") {
271
296
  // Everything AFTER a join runs in memory, for the same reason as `nearest`: this
@@ -276,7 +301,7 @@ class QueryOptionsCollection {
276
301
  // OUTER rows read, not the pairs produced — a plausible-looking result with the
277
302
  // wrong number of rows in it. Conjuncts that can safely run earlier are split off
278
303
  // by the query builder BEFORE dispatch, which is the only exception.
279
- this.nextExecutionTarget = "memory";
304
+ this.cutOverToMemory("after-join");
280
305
  }
281
306
  }
282
307
  /**
@@ -290,6 +315,8 @@ class QueryOptionsCollection {
290
315
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
291
316
  const before = new QueryOptionsCollection();
292
317
  const after = new QueryOptionsCollection();
318
+ before.derived = true;
319
+ after.derived = true;
293
320
  let at = null;
294
321
  for(let i = 0, length = sortedItems.length; i < length; i++){
295
322
  const { option } = sortedItems[i];
@@ -323,10 +350,12 @@ class QueryOptionsCollection {
323
350
  ]
324
351
  ]));
325
352
  const nextExecutionTarget = this.nextExecutionTarget;
353
+ const nextExecutionReason = this.nextExecutionReason;
326
354
  const nextIndex = this.nextIndex;
327
355
  return ()=>{
328
356
  this.options = new Map(options);
329
357
  this.nextExecutionTarget = nextExecutionTarget;
358
+ this.nextExecutionReason = nextExecutionReason;
330
359
  this.nextIndex = nextIndex;
331
360
  this.enumeratedItems = [];
332
361
  };
@@ -336,6 +365,8 @@ class QueryOptionsCollection {
336
365
  const sortedItems = this.enumeratedItems.toSorted((a, b)=>a.index - b.index);
337
366
  const memoryQueryOptionsCollection = new QueryOptionsCollection();
338
367
  const databaseQueryOptionsCollection = new QueryOptionsCollection();
368
+ memoryQueryOptionsCollection.derived = true;
369
+ databaseQueryOptionsCollection.derived = true;
339
370
  for(let i = 0, length = sortedItems.length; i < length; i++){
340
371
  const sortedItem = sortedItems[i];
341
372
  if (sortedItem.option.target === "database") {