@routier/core 0.2.1 → 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.
Files changed (161) hide show
  1. package/LICENSE +21 -0
  2. package/{readme.md → README.md} +3 -3
  3. package/dist/assertions/index.cjs +191 -0
  4. package/dist/assertions/index.cjs.map +1 -0
  5. package/dist/assertions/index.js +43 -63
  6. package/dist/assertions/index.js.map +1 -1
  7. package/dist/capabilities/index.cjs +820 -0
  8. package/dist/capabilities/index.cjs.map +1 -0
  9. package/dist/capabilities/index.js +584 -558
  10. package/dist/capabilities/index.js.map +1 -1
  11. package/dist/codegen/handlers/CloneHandlerBuilder.d.ts +10 -1
  12. package/dist/codegen/handlers/clone/CloneArrayHandler.d.ts +3 -0
  13. package/dist/codegen/handlers/clone/CloneDateHandler.d.ts +13 -0
  14. package/dist/codegen/handlers/clone/CloneValueHandler.d.ts +16 -0
  15. package/dist/codegen/handlers/compare/CompareComputedHandler.d.ts +10 -0
  16. package/dist/codegen/handlers/compare/CompareFunctionHandler.d.ts +10 -0
  17. package/dist/codegen/handlers/deserialize/DeserializeArrayHandler.d.ts +14 -0
  18. package/dist/codegen/handlers/enableChangeTracking/EnableChangeTrackingArrayHandler.d.ts +11 -0
  19. package/dist/codegen/handlers/enrichment/EnrichmentArrayHandler.d.ts +11 -0
  20. package/dist/codegen/handlers/freeze/FreezeArrayHandler.d.ts +6 -0
  21. package/dist/codegen/handlers/hash/HashArrayHandler.d.ts +10 -0
  22. package/dist/codegen/handlers/hash/HashFileHandler.d.ts +24 -0
  23. package/dist/codegen/handlers/hash/HashObjectHandler.d.ts +10 -0
  24. package/dist/codegen/handlers/hash/HashTransformHandler.d.ts +23 -0
  25. package/dist/codegen/handlers/merge/MergeArrayHandler.d.ts +20 -0
  26. package/dist/codegen/handlers/merge/MergeObjectHandler.d.ts +10 -0
  27. package/dist/codegen/handlers/serialize/SerializeArrayHandler.d.ts +19 -0
  28. package/dist/codegen/handlers/strip/StripComputedHandler.d.ts +10 -0
  29. package/dist/codegen/handlers/strip/StripFunctionHandler.d.ts +9 -0
  30. package/dist/codegen/handlers/types.d.ts +55 -1
  31. package/dist/codegen/index.cjs +618 -0
  32. package/dist/codegen/index.cjs.map +1 -0
  33. package/dist/codegen/index.js +99 -127
  34. package/dist/codegen/index.js.map +1 -1
  35. package/dist/collections/MemoryDataCollection.d.ts +15 -1
  36. package/dist/collections/TagCollection.d.ts +0 -1
  37. package/dist/collections/index.cjs +702 -0
  38. package/dist/collections/index.cjs.map +1 -0
  39. package/dist/collections/index.js +282 -276
  40. package/dist/collections/index.js.map +1 -1
  41. package/dist/errors/OptimisticConcurrencyError.d.ts +18 -0
  42. package/dist/errors/PluginDestroyedError.d.ts +10 -0
  43. package/dist/errors/index.cjs +132 -0
  44. package/dist/errors/index.cjs.map +1 -0
  45. package/dist/errors/index.d.ts +2 -0
  46. package/dist/errors/index.js +61 -19
  47. package/dist/errors/index.js.map +1 -1
  48. package/dist/expressions/evaluate.d.ts +53 -0
  49. package/dist/expressions/index.cjs +2087 -0
  50. package/dist/expressions/index.cjs.map +1 -0
  51. package/dist/expressions/index.d.ts +1 -1
  52. package/dist/expressions/index.js +1727 -1082
  53. package/dist/expressions/index.js.map +1 -1
  54. package/dist/expressions/parser.d.ts +19 -0
  55. package/dist/expressions/types.d.ts +87 -2
  56. package/dist/index.cjs +14377 -0
  57. package/dist/index.cjs.map +1 -0
  58. package/dist/index.js +12374 -7931
  59. package/dist/index.js.map +1 -1
  60. package/dist/performance/index.cjs +217 -0
  61. package/dist/performance/index.cjs.map +1 -0
  62. package/dist/performance/index.js +120 -55
  63. package/dist/performance/index.js.map +1 -1
  64. package/dist/pipeline/index.cjs +576 -0
  65. package/dist/pipeline/index.cjs.map +1 -0
  66. package/dist/pipeline/index.js +223 -161
  67. package/dist/pipeline/index.js.map +1 -1
  68. package/dist/plugins/BatchingDbPlugin.d.ts +139 -0
  69. package/dist/plugins/CacheDbPlugin.d.ts +70 -0
  70. package/dist/plugins/ConcurrencyDbPlugin.d.ts +78 -0
  71. package/dist/plugins/EphemeralDataPlugin.d.ts +57 -1
  72. package/dist/plugins/RetryDbPlugin.d.ts +77 -0
  73. package/dist/plugins/index.cjs +6551 -0
  74. package/dist/plugins/index.cjs.map +1 -0
  75. package/dist/plugins/index.d.ts +5 -0
  76. package/dist/plugins/index.js +6271 -1170
  77. package/dist/plugins/index.js.map +1 -1
  78. package/dist/plugins/query/QueryOptionsCollection.d.ts +34 -0
  79. package/dist/plugins/query/explain.d.ts +82 -0
  80. package/dist/plugins/query/formatExplanation.d.ts +9 -0
  81. package/dist/plugins/query/index.d.ts +4 -0
  82. package/dist/plugins/query/join.d.ts +210 -0
  83. package/dist/plugins/query/similarity.d.ts +38 -0
  84. package/dist/plugins/query/types.d.ts +75 -1
  85. package/dist/plugins/translators/DataTranslator.d.ts +28 -0
  86. package/dist/plugins/translators/JsonTranslator.d.ts +27 -0
  87. package/dist/plugins/translators/SqlTranslator.d.ts +45 -0
  88. package/dist/plugins/translators/TupleTranslator.d.ts +45 -0
  89. package/dist/plugins/translators/index.d.ts +1 -0
  90. package/dist/plugins/types.d.ts +125 -3
  91. package/dist/plugins/wire/handler.d.ts +122 -0
  92. package/dist/plugins/wire/index.d.ts +4 -0
  93. package/dist/plugins/wire/persist.d.ts +43 -0
  94. package/dist/plugins/wire/query.d.ts +49 -0
  95. package/dist/plugins/wire/types.d.ts +139 -0
  96. package/dist/results/index.cjs +174 -0
  97. package/dist/results/index.cjs.map +1 -0
  98. package/dist/results/index.js +29 -44
  99. package/dist/results/index.js.map +1 -1
  100. package/dist/schema/PropertyInfo.d.ts +31 -2
  101. package/dist/schema/SchemaDefinition.d.ts +23 -1
  102. package/dist/schema/builder.d.ts +35 -1
  103. package/dist/schema/communication/broadcast.d.ts +17 -1
  104. package/dist/schema/index.cjs +6911 -0
  105. package/dist/schema/index.cjs.map +1 -0
  106. package/dist/schema/index.d.ts +2 -0
  107. package/dist/schema/index.js +5330 -4541
  108. package/dist/schema/index.js.map +1 -1
  109. package/dist/schema/property/base/SchemaBase.d.ts +48 -2
  110. package/dist/schema/property/modifiers/SchemaNullable.d.ts +10 -0
  111. package/dist/schema/property/modifiers/SchemaOptional.d.ts +19 -0
  112. package/dist/schema/property/modifiers/SchemaSearchable.d.ts +34 -0
  113. package/dist/schema/property/modifiers/index.d.ts +1 -0
  114. package/dist/schema/property/types/SchemaFile.d.ts +60 -0
  115. package/dist/schema/property/types/SchemaString.d.ts +21 -1
  116. package/dist/schema/property/types/SchemaVector.d.ts +61 -0
  117. package/dist/schema/property/types/index.d.ts +2 -0
  118. package/dist/schema/table/SchemaTransform.d.ts +40 -0
  119. package/dist/schema/table/index.d.ts +1 -0
  120. package/dist/schema/types.d.ts +166 -8
  121. package/dist/schema/utils/propertyKind.d.ts +4 -0
  122. package/dist/types/index.cjs +23 -0
  123. package/dist/types/index.cjs.map +1 -0
  124. package/dist/types/index.js +0 -14
  125. package/dist/types/index.js.map +1 -1
  126. package/dist/utilities/index.cjs +1013 -0
  127. package/dist/utilities/index.cjs.map +1 -0
  128. package/dist/utilities/index.js +541 -379
  129. package/dist/utilities/index.js.map +1 -1
  130. package/dist/utilities/logger.d.ts +48 -0
  131. package/package.json +156 -98
  132. package/dist/codegen/SlotPath.test.d.ts +0 -1
  133. package/dist/codegen/blocks.test.d.ts +0 -1
  134. package/dist/codegen/utils.test.d.ts +0 -1
  135. package/dist/collections/Changes.test.d.ts +0 -1
  136. package/dist/collections/IdSet.test.d.ts +0 -1
  137. package/dist/collections/MemoryDataCollection.test.d.ts +0 -1
  138. package/dist/collections/TagCollection.contract.test.d.ts +0 -1
  139. package/dist/collections/TagCollection.test.d.ts +0 -1
  140. package/dist/expressions/parser.test.d.ts +0 -1
  141. package/dist/expressions/sql.d.ts +0 -28
  142. package/dist/expressions/sql.test.d.ts +0 -1
  143. package/dist/expressions/utils.test.d.ts +0 -1
  144. package/dist/pipeline/SyncronousQueue.test.d.ts +0 -1
  145. package/dist/plugins/EphemeralDataPlugin.test.d.ts +0 -1
  146. package/dist/plugins/query/Query.test.d.ts +0 -1
  147. package/dist/plugins/query/QueryOptionsCollection.test.d.ts +0 -1
  148. package/dist/plugins/translators/JsonTranslator.test.d.ts +0 -1
  149. package/dist/plugins/translators/SqlTranslator.test.d.ts +0 -1
  150. package/dist/plugins/translators/TranslatedValues.test.d.ts +0 -1
  151. package/dist/results/Result.test.d.ts +0 -1
  152. package/dist/results/utils.test.d.ts +0 -1
  153. package/dist/schema/PropertyInfo.test.d.ts +0 -1
  154. package/dist/schema/communication/broadcast.test.d.ts +0 -1
  155. package/dist/schema/schemaGeneration.test.d.ts +0 -1
  156. package/dist/schema/utils/standardJsonSchema.test.d.ts +0 -1
  157. package/dist/utilities/arrays.test.d.ts +0 -1
  158. package/dist/utilities/dates.test.d.ts +0 -1
  159. package/dist/utilities/runtime.test.d.ts +0 -1
  160. package/dist/utilities/strings.test.d.ts +0 -1
  161. package/dist/utilities/uuid.test.d.ts +0 -1
@@ -6,13 +6,47 @@ 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>;
14
27
  static isEmpty<T>(options: QueryOptionsCollection<T>): boolean;
15
28
  add<K extends QueryOptionName>(name: K, value: QueryOption<T, K>["value"]): void;
29
+ /**
30
+ * Splits the collection around the FIRST occurrence of `name`, preserving order.
31
+ *
32
+ * For a join: the options recorded before it operate on entity rows, the option itself
33
+ * produces tuples, and the ones after it operate on tuples. Three different shapes, so the
34
+ * caller has to run them in three steps rather than one pass.
35
+ */
36
+ splitAt<K extends QueryOptionName>(name: K): {
37
+ before: QueryOptionsCollection<T>;
38
+ at: QueryOption<T, K> | null;
39
+ after: QueryOptionsCollection<T>;
40
+ };
41
+ /**
42
+ * Captures the collection's current state and returns a function that restores it.
43
+ *
44
+ * Terminal queryable operations (count, first, aggregates, …) record their option on
45
+ * the shared collection before executing. Without restoring, a re-executed terminal —
46
+ * the whole point of a subscribed queryable — stacks its option a second time and
47
+ * runs it over the first execution's scalar result.
48
+ */
49
+ snapshot(): () => void;
16
50
  split(): {
17
51
  memory: QueryOptionsCollection<T>;
18
52
  database: QueryOptionsCollection<T>;
@@ -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,7 @@
1
+ export * from './explain';
2
+ export * from './formatExplanation';
3
+ export * from './join';
1
4
  export * from './Query';
2
5
  export * from './QueryOptionsCollection';
3
6
  export * from './types';
7
+ export * from './similarity';
@@ -0,0 +1,210 @@
1
+ import { CompiledSchemaCore, PropertyInfo } from "../../schema";
2
+ import { UnknownRecord } from "../../utilities";
3
+ import { PluginEventCallbackResult } from "../../results";
4
+ import type { DbPluginQueryEvent } from "../types";
5
+ import type { ITranslatedValue } from "../translators/types";
6
+ import { QueryOptionsCollection } from "./QueryOptionsCollection";
7
+ import { QueryOptionValueMap } from "./types";
8
+ export type JoinKind = "inner" | "left";
9
+ /**
10
+ * One side's join key, as a property path plus the resolved property.
11
+ *
12
+ * The path is what survives serialization; the `PropertyInfo` is the live handle that reads
13
+ * the value and resolves a `from`-renamed storage name. Both, because a key read has to work
14
+ * on either side of the wire.
15
+ */
16
+ export type JoinKeyReference = {
17
+ propertyName: string;
18
+ property: PropertyInfo<any> | null;
19
+ };
20
+ export type JoinQueryOptionValue = QueryOptionValueMap<any>["join"];
21
+ /**
22
+ * The inner side of a join, as the PLUGIN sees it.
23
+ *
24
+ * Supplied by the plugin rather than fetched by the translator, because loading rows is the one
25
+ * part of a join that is backend-specific: a memory plugin resolves a collection, Dexie opens a
26
+ * store, Mongo reads a collection. Everything after that — deserializing, applying the inner
27
+ * scopes, pairing — is identical, so it lives here and runs once.
28
+ *
29
+ * Rows arrive in STORAGE shape, exactly as the plugin holds them; the translator deserializes
30
+ * them with `innerSchema`, which the outer query's own deserialization would never do.
31
+ */
32
+ export type JoinInnerSide = {
33
+ innerSchema: CompiledSchemaCore<any>;
34
+ innerRows: readonly unknown[];
35
+ };
36
+ /**
37
+ * `undefined` when the query has no join at all, which is the common case and not an error.
38
+ */
39
+ export type JoinInnerSideResult = {
40
+ ok: "success";
41
+ innerSide?: JoinInnerSide;
42
+ } | {
43
+ ok: "error";
44
+ error: unknown;
45
+ };
46
+ /**
47
+ * A joined pair, each half fully deserialized into its own schema's ENTITY shape.
48
+ *
49
+ * The wire contract for every interpretation of a join: a native SQL join, an in-plugin hash
50
+ * join, and the datastore's cross-plugin join all produce exactly this. Flat combined rows
51
+ * never leave a translator — see `specs/joins.md`.
52
+ */
53
+ export type JoinTuple = [UnknownRecord, UnknownRecord | undefined];
54
+ /**
55
+ * Turns storage-shape records into entity-shape values, one side of a join at a time.
56
+ *
57
+ * The inner side does NOT pass through the outer query's normal deserialization —
58
+ * `DatabaseDataAccessStrategy.query` transforms against the outer schema only — so each side
59
+ * is deserialized with its own schema here, which is what makes the two halves of a tuple
60
+ * readable by the same property names the caller wrote in the selectors.
61
+ *
62
+ * `"diff"` rather than `"proxy"`: join results are read-only projections and never attach to
63
+ * the change tracker, so there is nothing for a tracking proxy to record.
64
+ */
65
+ export declare const toEntityShape: (schema: CompiledSchemaCore<any>, rows: readonly unknown[]) => UnknownRecord[];
66
+ /**
67
+ * Reads a join key off an entity-shape row.
68
+ *
69
+ * Through the `PropertyInfo` when there is one, so a nested path (`a.b.id`) resolves the same
70
+ * way every other option resolves it. The string fallback exists for an option that crossed a
71
+ * wire without its schema; it walks the same path by name.
72
+ */
73
+ export declare const readJoinKey: (row: UnknownRecord | undefined, reference: JoinKeyReference) => unknown;
74
+ /**
75
+ * Applies an inner side's own filters to its rows.
76
+ *
77
+ * **This is the correctness trap of the whole feature.** Every interpretation of a join
78
+ * bypasses the inner collection's normal datastore read path, so the inner side's soft-delete
79
+ * scope and `.scope()` filters exist ONLY because `innerOptions` carries them. An interpreter
80
+ * that skips this returns soft-deleted rows.
81
+ *
82
+ * Filters only. Nothing else reaches `innerOptions` today — scopes are filters — and applying
83
+ * a `skip`/`take` recorded against the inner collection to the rows feeding a join would
84
+ * change which pairs exist rather than which rows are visible.
85
+ */
86
+ export declare const applyInnerOptions: (rows: UnknownRecord[], innerOptions: QueryOptionsCollection<any>) => UnknownRecord[];
87
+ /**
88
+ * The join itself: one hash join, written once, called from every interpreter.
89
+ *
90
+ * O(n + m) rather than a nested loop, which is the whole reason the API takes explicit key
91
+ * selectors instead of a free-form predicate. Both key properties are `string` or `number` by
92
+ * a build-time rule, so the keys are hashable and compare the same way in JS and in SQL.
93
+ *
94
+ * Semantics, exactly as `specs/joins.md` states them:
95
+ *
96
+ * - **Null keys** never match. Under `left` the outer row still appears, paired with
97
+ * `undefined`.
98
+ * - **Duplicates** produce every pair: the full cross product per key group.
99
+ * - **Ordering** is outer order, then inner order within a key group. Undefined by contract —
100
+ * a caller who cares sorts.
101
+ */
102
+ export declare const hashJoin: (options: {
103
+ kind: JoinKind;
104
+ outerRows: UnknownRecord[];
105
+ innerRows: UnknownRecord[];
106
+ outerKey: JoinKeyReference;
107
+ innerKey: JoinKeyReference;
108
+ }) => JoinTuple[];
109
+ /**
110
+ * How many distinct outer keys are still worth turning into an `IN (...)` prefilter.
111
+ *
112
+ * A cost decision, never a correctness one: above the threshold the inner side is read under its
113
+ * own scopes and the hash join discards the surplus, which is the same answer by a slower route.
114
+ * 500 because a bound-parameter list is cheap in the hundreds and starts costing more than the
115
+ * scan it saves in the thousands — and some engines refuse a list that long outright.
116
+ */
117
+ export declare const DEFAULT_SEMI_JOIN_KEY_THRESHOLD = 500;
118
+ /**
119
+ * The distinct join keys of the outer rows, or `null` when there are too many to be worth sending.
120
+ *
121
+ * `null` means "do not prefilter", not "no keys" — an empty SET is a real answer meaning the inner
122
+ * side cannot match anything.
123
+ *
124
+ * @param column The key's STORAGE column name when `rows` are storage-shaped, or its property name
125
+ * when they are entity-shaped. The caller knows which it holds.
126
+ */
127
+ export declare const distinctJoinKeys: (rows: readonly UnknownRecord[], reference: JoinKeyReference, threshold?: number, options?: {
128
+ storageShape?: boolean;
129
+ }) => Set<unknown> | null;
130
+ /**
131
+ * A filter restricting the inner side to rows whose key is one the outer side actually has.
132
+ *
133
+ * Built as an expression tree by hand rather than parsed from generated source, for the reason
134
+ * `softDeleteScope` gives: generating source needs `new Function`, which a Content-Security-Policy
135
+ * blocks, and the shape is known here so there is nothing to parse. An `includes` comparator over
136
+ * an array value is what every translator already turns into `IN (...)` or `$in`, so this pushes
137
+ * down on the backends that can take it and runs as the closure on the ones that cannot.
138
+ *
139
+ * Cost only. Every pair it removes from the inner read is one the hash join would have discarded.
140
+ */
141
+ export declare const semiJoinFilter: (reference: JoinKeyReference, keys: ReadonlySet<unknown>) => QueryOptionValueMap<any>["filter"];
142
+ /**
143
+ * Loads a join's inner side by asking the plugin to run an ORDINARY query for it.
144
+ *
145
+ * The generic way for a plugin to interpret a join — one call, no join-specific reading code.
146
+ * The inner side is just "this collection, under these filters", which is a query every plugin
147
+ * already knows how to answer, through whatever indexes and scoping it normally applies.
148
+ *
149
+ * Only the DATABASE half of `innerOptions` is sent. The memory half would mean nothing to the
150
+ * plugin, and `executeJoin` re-applies every filter regardless — filters are pure, so the second
151
+ * pass costs a walk over the survivors and guarantees the inner scopes are honoured even if the
152
+ * plugin silently ignored them.
153
+ *
154
+ * The inner query carries no `join` option of its own, so this cannot recurse.
155
+ *
156
+ * @param query How this plugin runs a query. **Not necessarily `plugin.query`**: a plugin that
157
+ * serializes queries through a work queue must pass its UN-QUEUED path, or this call waits behind
158
+ * the outer query that is still holding the queue and the plugin deadlocks.
159
+ */
160
+ export declare const loadJoinInnerSide: <TRoot extends {}, TShape>(event: DbPluginQueryEvent<TRoot, TShape>, query: (innerEvent: DbPluginQueryEvent<UnknownRecord, UnknownRecord>, done: PluginEventCallbackResult<ITranslatedValue<UnknownRecord>>) => void, done: (result: JoinInnerSideResult) => void,
161
+ /**
162
+ * The outer side's distinct keys, when the caller already has them.
163
+ *
164
+ * Only a plugin that runs its outer query FIRST can supply these, and most run this loader
165
+ * before anything else — so it is optional, and its absence costs a wider inner read rather
166
+ * than a wrong one.
167
+ */
168
+ outerKeys?: ReadonlySet<unknown> | null) => void;
169
+ /**
170
+ * Interprets a join by running TWO ordinary queries through the plugin's own read path.
171
+ *
172
+ * The whole of interpretation 2 for a plugin that has no reason to do anything cleverer, and the
173
+ * shape every non-SQL backend should prefer:
174
+ *
175
+ * ```ts
176
+ * query(event, done) {
177
+ * if (event.operation.options.has("join")) {
178
+ * joinInPlugin(event, (e, d) => this.query(e, d), done);
179
+ * return;
180
+ * }
181
+ * // ...the ordinary single-collection path
182
+ * }
183
+ * ```
184
+ *
185
+ * **The outer side runs FIRST, and that ordering is the optimization.** Its keys are what narrow
186
+ * the inner read to rows that can actually pair — and they do not exist until the outer filters
187
+ * have run. Loading the inner side first, which is what a naive implementation does, means reading
188
+ * and materializing a whole collection to pair it with three rows.
189
+ *
190
+ * Neither query carries the join option, so both take the plugin's normal path: its indexes, its
191
+ * pushdown decisions, its retries. Nothing recurses, because the option is stripped before either
192
+ * goes out.
193
+ *
194
+ * @param query How this plugin runs a query. NOT necessarily `plugin.query` — a plugin that
195
+ * serializes queries through a work queue must pass its UN-QUEUED path, or the two reads below
196
+ * wait on the slot this one is holding.
197
+ */
198
+ export declare const joinInPlugin: <TRoot extends {}, TShape>(event: DbPluginQueryEvent<TRoot, TShape>, query: (innerEvent: DbPluginQueryEvent<UnknownRecord, UnknownRecord>, done: PluginEventCallbackResult<ITranslatedValue<UnknownRecord>>) => void, done: PluginEventCallbackResult<ITranslatedValue<TShape>>) => void;
199
+ /**
200
+ * A join over rows both sides have already deserialized, with the inner side's scopes applied.
201
+ *
202
+ * The one entry point every interpreter uses: `JsonTranslator.join` inside a plugin, and the
203
+ * datastore's own memory half for a cross-plugin join. Callers hand over ENTITY-shape rows —
204
+ * `toEntityShape` is separate because a caller may already have paid for it.
205
+ */
206
+ export declare const executeJoin: (options: {
207
+ option: JoinQueryOptionValue;
208
+ outerRows: UnknownRecord[];
209
+ innerRows: UnknownRecord[];
210
+ }) => JoinTuple[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cosine distance, and the ordering rules that go with it.
3
+ *
4
+ * One implementation, shared by every in-memory path, because the promise the feature makes
5
+ * is that a backend with no vector support returns the SAME ROWS IN THE SAME ORDER as one
6
+ * that pushes the search down. Two copies of this arithmetic would drift, and the drift would
7
+ * only ever show up as a conformance failure nobody could localise.
8
+ *
9
+ * The tie-breaking and degenerate cases below are chosen to match pgvector rather than to be
10
+ * independently reasonable — parity is the point.
11
+ */
12
+ /**
13
+ * Distance in `[0, 2]`, or `Infinity` for a value that cannot be compared.
14
+ *
15
+ * `Infinity` covers three cases, and they all sort LAST, which is what PostgreSQL does too:
16
+ * a missing value (`NULL` sorts last under `ASC` by default), a zero-magnitude vector
17
+ * (pgvector's `<=>` yields `NaN`, which PostgreSQL orders after every real number), and a
18
+ * stored vector of the wrong width.
19
+ *
20
+ * The width case cannot happen on a native `vector(n)` column — the engine rejects the write —
21
+ * so it only arises on a backend storing JSON, where the data was written by something other
22
+ * than this schema. Sorting it last rather than throwing keeps one corrupt row from failing a
23
+ * query that is otherwise answerable.
24
+ */
25
+ export declare const cosineDistance: (left: readonly number[] | null | undefined, right: readonly number[]) => number;
26
+ /**
27
+ * The `count` rows closest to `vector`, nearest first.
28
+ *
29
+ * Distances are computed once per row rather than inside the comparator: a comparison-time
30
+ * computation runs O(n log n) times over vectors that are commonly 1536 wide, which turns an
31
+ * ordering into the dominant cost of the query.
32
+ *
33
+ * The sort is stable, so rows at equal distance keep the order the backend returned them in.
34
+ * That is not a guarantee worth relying on across backends — two engines can hand back the
35
+ * same rows in different orders — but it does mean this function never introduces a
36
+ * difference of its own.
37
+ */
38
+ export declare const nearestBy: <T>(rows: readonly T[], vector: number[], count: number, select: (row: T) => readonly number[] | null | undefined) => T[];
@@ -1,6 +1,8 @@
1
1
  import { Expression, Filter, ParamsFilter } from "../../expressions";
2
- import { PropertyInfo } from "../../schema";
2
+ import { PropertyInfo, SchemaId } from "../../schema";
3
3
  import { GenericFunction } from "../../types";
4
+ import type { JoinKeyReference, JoinKind } from "./join";
5
+ import type { QueryOptionsCollection } from "./QueryOptionsCollection";
4
6
  export declare enum QueryOrdering {
5
7
  Descending = "desc",
6
8
  Ascending = "asc"
@@ -17,10 +19,21 @@ export type QueryField = {
17
19
  };
18
20
  export type QueryOptionExecutionTarget = "database" | "memory";
19
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";
20
31
  export type QueryOption<T, K extends QueryOptionName> = {
21
32
  name: QueryOptionName;
22
33
  value: QueryOptionValueMap<T>[K];
23
34
  target: QueryOptionExecutionTarget;
35
+ /** Set only when `target` is `"memory"`. */
36
+ reason?: MemoryExecutionReason;
24
37
  };
25
38
  export type QueryOptionValueMap<T extends {}> = {
26
39
  skip: number;
@@ -29,6 +42,7 @@ export type QueryOptionValueMap<T extends {}> = {
29
42
  selector: GenericFunction<T, T[keyof T]>;
30
43
  direction: QueryOrdering;
31
44
  propertyName: string;
45
+ property?: PropertyInfo<T> | null;
32
46
  };
33
47
  map: {
34
48
  selector: GenericFunction<T, any>;
@@ -44,6 +58,66 @@ export type QueryOptionValueMap<T extends {}> = {
44
58
  filter: ParamsFilter<T, {}> | Filter<T>;
45
59
  expression: Expression;
46
60
  };
61
+ /**
62
+ * Similarity search: an ordering plus a limit, never a filter.
63
+ *
64
+ * `count` is part of the option rather than a separate `take` because the two are one
65
+ * operation to a backend that can push this down — `ORDER BY ... LIMIT n` is what makes an
66
+ * approximate index usable, and splitting them would order every row before limiting.
67
+ */
68
+ nearest: {
69
+ selector: GenericFunction<T, T[keyof T]>;
70
+ propertyName: string;
71
+ property?: PropertyInfo<T> | null;
72
+ vector: number[];
73
+ count: number;
74
+ };
75
+ /**
76
+ * An equi-join against a second collection, interpreted by whoever receives it.
77
+ *
78
+ * A first-class query option rather than a datastore side-path: a SQL backend emits a real
79
+ * `INNER JOIN`/`LEFT JOIN`, every other backend loads the rows it needs and the shared hash
80
+ * join runs inside the plugin, and a cross-plugin join runs in the datastore's memory half.
81
+ * All three produce the same pairs — see `specs/joins.md`.
82
+ *
83
+ * Serializable by construction: property paths and a schema id, never live rows, with any
84
+ * filter's values travelling in its params object. That is what lets the whole option be
85
+ * forwarded to a server once expression-tree serialization lands.
86
+ */
87
+ join: {
88
+ kind: JoinKind;
89
+ /** Resolved through `event.schemas`, which already carries every schema in the store. */
90
+ innerSchemaId: SchemaId;
91
+ outerKey: JoinKeyReference;
92
+ innerKey: JoinKeyReference;
93
+ /**
94
+ * The inner side's own filters — INCLUDING its soft-delete scope and `.scope()`
95
+ * filters. Every interpreter must apply these: it is the only place they exist, because
96
+ * a join bypasses the inner collection's normal read path.
97
+ */
98
+ innerOptions: QueryOptionsCollection<any>;
99
+ /**
100
+ * Whether the two sides live on DIFFERENT plugin instances, in which case no plugin can
101
+ * receive the option and the datastore is the interpreter.
102
+ *
103
+ * Decided by plugin instance identity at build time, never by comparing database names —
104
+ * two plugins over one database are still two interpreters, and one name can front two
105
+ * databases.
106
+ */
107
+ crossPlugin: boolean;
108
+ /**
109
+ * How many distinct outer keys are still worth turning into an `IN (...)` prefilter on the
110
+ * inner read — the datastore's `semiJoinKeyThreshold`, default 500.
111
+ *
112
+ * Carried in the option because the decision is made where the join executes, which is
113
+ * usually inside a plugin, and a plugin cannot see a datastore's configuration. A number
114
+ * serializes; a reference to the store would not.
115
+ *
116
+ * Cost only. Above the threshold the inner side is read under its own scopes and the hash
117
+ * join discards the surplus — the same answer by a slower route.
118
+ */
119
+ semiJoinKeyThreshold: number;
120
+ };
47
121
  min: true;
48
122
  max: true;
49
123
  count: true;
@@ -16,5 +16,33 @@ export declare abstract class DataTranslator<TRoot extends {}, TShape> {
16
16
  abstract sort(data: unknown, option: QueryOption<TShape, "sort">): TShape;
17
17
  abstract map(data: unknown, option: QueryOption<TShape, "map">): TShape;
18
18
  abstract group(data: unknown, option: QueryOption<TShape, "group">): TShape;
19
+ /**
20
+ * Abstract on purpose, unlike the pass-throughs a storage translator can usually inherit.
21
+ *
22
+ * Every other shaper degrades safely when a backend ignores it — an unsorted result is
23
+ * still the right rows. A similarity search is not: it is the only option whose value is
24
+ * ENTIRELY in the ordering and the limit, so a translator that quietly passes the data
25
+ * through returns every row in insertion order and calls it the ten nearest.
26
+ *
27
+ * Requiring an answer here means a new translator cannot be written without deciding
28
+ * whether its backend performed the search, and the compiler asks the question.
29
+ */
30
+ abstract nearest(data: unknown, option: QueryOption<TShape, "nearest">): TShape;
31
+ /**
32
+ * Abstract for the same reason as `nearest`, one step further.
33
+ *
34
+ * A translator that quietly passed a join through would not return an unsorted or unlimited
35
+ * result — it would return the OUTER rows, one object each where the contract says tuples,
36
+ * and every `([outer, inner]) => ...` lambda downstream would destructure the outer entity
37
+ * instead. Nothing errors; the answer is simply a different query's answer.
38
+ *
39
+ * So the compiler asks the question. Every translator must state how its backend joins:
40
+ * natively (pass through the rows the SQL already paired), in memory (the shared hash join
41
+ * over rows the plugin supplies), or not at all (throw, loudly, naming the backend).
42
+ *
43
+ * The output contract, whichever answer: an array of `[outer, inner]` tuples, each half
44
+ * fully deserialized into its OWN schema's entity shape.
45
+ */
46
+ abstract join(data: unknown, option: QueryOption<TShape, "join">): TShape;
19
47
  translate(data: unknown): ITranslatedValue<TShape>;
20
48
  }
@@ -1,6 +1,24 @@
1
1
  import { DataTranslator } from "./DataTranslator";
2
2
  import { QueryOption } from "../query/types";
3
+ import { JoinInnerSide } from "../query/join";
4
+ import { IQuery } from "../types";
3
5
  export declare class JsonTranslator<TRoot extends {}, TShape> extends DataTranslator<TRoot, TShape> {
6
+ private readonly innerSide?;
7
+ /**
8
+ * @param innerSide The inner collection's rows, when this query carries a `join` option.
9
+ * A plugin that omits it for a query that HAS a join gets a throw from `join()` rather than
10
+ * a silently un-joined result.
11
+ */
12
+ constructor(query: IQuery<TRoot, TShape>, innerSide?: JoinInnerSide);
13
+ /**
14
+ * The hash join itself, over rows already in memory — the floor every non-SQL backend
15
+ * stands on.
16
+ *
17
+ * Both halves are deserialized here, each with its own schema, because that is where the
18
+ * `===` on key values is specified to happen: in entity shape, by the property names the
19
+ * caller wrote in the key selectors. A `from`-renamed column reads correctly for free.
20
+ */
21
+ join<TResult>(data: unknown, option: QueryOption<TShape, "join">): TResult;
4
22
  filter<TResult>(data: unknown, option: QueryOption<TShape, "filter">): TResult;
5
23
  map<T>(data: unknown, option: QueryOption<T, "map">): T;
6
24
  group<T>(data: unknown, option: QueryOption<T, "group">): T;
@@ -8,6 +26,15 @@ export declare class JsonTranslator<TRoot extends {}, TShape> extends DataTransl
8
26
  min<TResult extends string | number | Date>(data: unknown, _: QueryOption<TShape, "min">): TResult;
9
27
  max<TResult extends string | number | Date>(data: unknown, _: QueryOption<TShape, "max">): TResult;
10
28
  sort<TResult>(data: unknown, option: QueryOption<TShape, "sort">): TResult;
29
+ /**
30
+ * The similarity search itself, over values already in memory.
31
+ *
32
+ * This is the floor the whole feature stands on: it is reached whenever the backend did
33
+ * not do the search, which is every backend except the ones with a native vector index.
34
+ * It reads the property through the option's selector, so it works on any shape the rows
35
+ * arrive in.
36
+ */
37
+ nearest<TResult>(data: unknown, option: QueryOption<TShape, "nearest">): TResult;
11
38
  sum<TResult extends number>(data: unknown, _: QueryOption<TShape, "sum">): TResult;
12
39
  distinct<TResult>(data: unknown, _: QueryOption<TShape, "distinct">): TResult;
13
40
  skip<TResult>(data: unknown, option: QueryOption<TShape, "skip">): TResult;
@@ -1,6 +1,21 @@
1
1
  import { QueryOption } from "../query/types";
2
+ import { IQuery } from "../types";
2
3
  import { DataTranslator } from "./DataTranslator";
4
+ /**
5
+ * What the statement that produced these rows actually did.
6
+ *
7
+ * Supplied by the plugin because only its query builder knows: an extension may be missing, a
8
+ * window may have made a pushdown unsafe, an inner filter may have had no column to compare
9
+ * against. Every flag defaults to false — the safe direction, since doing the work twice is slow
10
+ * and skipping it is wrong.
11
+ */
12
+ export type SqlPushdown = {
13
+ /** The statement contained a real `INNER JOIN`/`LEFT JOIN` and its rows are already tuples. */
14
+ join?: boolean;
15
+ };
3
16
  export declare class SqlTranslator<TRoot extends {}, TShape> extends DataTranslator<TRoot, TShape> {
17
+ protected readonly pushedDown: SqlPushdown;
18
+ constructor(query: IQuery<TRoot, TShape>, pushedDown?: SqlPushdown);
4
19
  count<TResult extends number>(data: unknown, _: QueryOption<TShape, "count">): TResult;
5
20
  min<TResult extends string | number | Date>(data: unknown, _: QueryOption<TShape, "min">): TResult;
6
21
  max<TResult extends string | number | Date>(data: unknown, _: QueryOption<TShape, "max">): TResult;
@@ -11,6 +26,36 @@ export declare class SqlTranslator<TRoot extends {}, TShape> extends DataTransla
11
26
  skip(data: unknown, _: QueryOption<TShape, "skip">): TShape;
12
27
  take(data: unknown, _: QueryOption<TShape, "take">): TShape;
13
28
  sort(data: unknown, _: QueryOption<TShape, "sort">): TShape;
29
+ /**
30
+ * Scores in memory, unlike every other shaper here.
31
+ *
32
+ * The pass-throughs above are safe because the SQL that produced these rows contained the
33
+ * corresponding clause. No `sql-core` statement contains a similarity ordering — engines
34
+ * that can express one are the exception, not the rule — so passing the data through
35
+ * would return whatever order the engine happened to produce.
36
+ *
37
+ * A plugin whose engine DID push the search down overrides this with a pass-through,
38
+ * gated on `option.target`. Postgres is the only one today.
39
+ *
40
+ * Rows arrive keyed by storage column name and are read that way rather than through the
41
+ * option's selector, because the selector is written against the entity shape and these
42
+ * rows have not been deserialized into it yet.
43
+ */
44
+ nearest(data: unknown, option: QueryOption<TShape, "nearest">): TShape;
45
+ /**
46
+ * Passes through only when the statement really did contain the `JOIN`, and refuses otherwise.
47
+ *
48
+ * The pass-throughs above are safe unconditionally because the SQL that produced these rows
49
+ * contained the corresponding clause. A join is not like that: if the plugin did not emit one,
50
+ * these rows are the outer side alone, and passing them through hands the caller entities
51
+ * where the contract says tuples — every `([outer, inner]) => ...` lambda downstream then
52
+ * destructures the wrong object, and nothing errors.
53
+ *
54
+ * So the plugin has to say, and the default is to refuse. A plugin that DID emit the join has
55
+ * already split each flat row into its two deserialized halves (`splitJoinRows` in
56
+ * `@routier/sql-plugin-core`), so by the time the option is walked the work is done.
57
+ */
58
+ join(data: unknown, option: QueryOption<TShape, "join">): TShape;
14
59
  group<T>(data: unknown, option: QueryOption<T, "group">): T;
15
60
  map(data: unknown, option: QueryOption<TShape, "map">): TShape;
16
61
  }