@routier/core 0.2.0 → 0.3.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/{readme.md → README.md} +2 -2
  3. package/dist/assertions/index.cjs +191 -0
  4. package/dist/assertions/index.cjs.map +1 -0
  5. package/dist/assertions/index.d.ts +30 -0
  6. package/dist/assertions/index.js +94 -30
  7. package/dist/assertions/index.js.map +1 -1
  8. package/dist/capabilities/index.cjs +820 -0
  9. package/dist/capabilities/index.cjs.map +1 -0
  10. package/dist/capabilities/index.js +580 -545
  11. package/dist/capabilities/index.js.map +1 -1
  12. package/dist/codegen/handlers/CloneHandlerBuilder.d.ts +10 -1
  13. package/dist/codegen/handlers/SetHandlerBuilder.d.ts +4 -0
  14. package/dist/codegen/handlers/clone/CloneArrayHandler.d.ts +3 -0
  15. package/dist/codegen/handlers/clone/CloneDateHandler.d.ts +13 -0
  16. package/dist/codegen/handlers/clone/CloneValueHandler.d.ts +16 -0
  17. package/dist/codegen/handlers/compare/CompareComputedHandler.d.ts +10 -0
  18. package/dist/codegen/handlers/compare/CompareFunctionHandler.d.ts +10 -0
  19. package/dist/codegen/handlers/deserialize/DeserializeArrayHandler.d.ts +14 -0
  20. package/dist/codegen/handlers/enableChangeTracking/EnableChangeTrackingArrayHandler.d.ts +11 -0
  21. package/dist/codegen/handlers/enrichment/EnrichmentArrayHandler.d.ts +11 -0
  22. package/dist/codegen/handlers/freeze/FreezeArrayHandler.d.ts +6 -0
  23. package/dist/codegen/handlers/hash/HashArrayHandler.d.ts +10 -0
  24. package/dist/codegen/handlers/hash/HashFileHandler.d.ts +24 -0
  25. package/dist/codegen/handlers/hash/HashObjectHandler.d.ts +10 -0
  26. package/dist/codegen/handlers/hash/HashTransformHandler.d.ts +23 -0
  27. package/dist/codegen/handlers/index.d.ts +1 -0
  28. package/dist/codegen/handlers/merge/MergeArrayHandler.d.ts +20 -0
  29. package/dist/codegen/handlers/merge/MergeObjectHandler.d.ts +10 -0
  30. package/dist/codegen/handlers/serialize/SerializeArrayHandler.d.ts +19 -0
  31. package/dist/codegen/handlers/serialize/SerializeComputedHandler.d.ts +6 -0
  32. package/dist/codegen/handlers/set/SetComplexHandler.d.ts +6 -0
  33. package/dist/codegen/handlers/strip/StripComputedHandler.d.ts +10 -0
  34. package/dist/codegen/handlers/strip/StripFunctionHandler.d.ts +9 -0
  35. package/dist/codegen/handlers/types.d.ts +55 -1
  36. package/dist/codegen/index.cjs +618 -0
  37. package/dist/codegen/index.cjs.map +1 -0
  38. package/dist/codegen/index.js +100 -127
  39. package/dist/codegen/index.js.map +1 -1
  40. package/dist/collections/Changes.d.ts +2 -2
  41. package/dist/collections/MemoryDataCollection.d.ts +15 -1
  42. package/dist/collections/TagCollection.d.ts +7 -8
  43. package/dist/collections/index.cjs +702 -0
  44. package/dist/collections/index.cjs.map +1 -0
  45. package/dist/collections/index.js +282 -276
  46. package/dist/collections/index.js.map +1 -1
  47. package/dist/errors/OptimisticConcurrencyError.d.ts +18 -0
  48. package/dist/errors/PluginDestroyedError.d.ts +10 -0
  49. package/dist/errors/index.cjs +132 -0
  50. package/dist/errors/index.cjs.map +1 -0
  51. package/dist/errors/index.d.ts +2 -0
  52. package/dist/errors/index.js +61 -19
  53. package/dist/errors/index.js.map +1 -1
  54. package/dist/expressions/constants.d.ts +2 -0
  55. package/dist/expressions/evaluate.d.ts +53 -0
  56. package/dist/expressions/index.cjs +2087 -0
  57. package/dist/expressions/index.cjs.map +1 -0
  58. package/dist/expressions/index.d.ts +2 -0
  59. package/dist/expressions/index.js +1784 -772
  60. package/dist/expressions/index.js.map +1 -1
  61. package/dist/expressions/parser.d.ts +19 -0
  62. package/dist/expressions/types.d.ts +87 -2
  63. package/dist/expressions/utils.d.ts +1 -2
  64. package/dist/index.cjs +13925 -0
  65. package/dist/index.cjs.map +1 -0
  66. package/dist/index.js +12094 -7049
  67. package/dist/index.js.map +1 -1
  68. package/dist/performance/index.cjs +217 -0
  69. package/dist/performance/index.cjs.map +1 -0
  70. package/dist/performance/index.js +126 -50
  71. package/dist/performance/index.js.map +1 -1
  72. package/dist/pipeline/index.cjs +576 -0
  73. package/dist/pipeline/index.cjs.map +1 -0
  74. package/dist/pipeline/index.js +229 -156
  75. package/dist/pipeline/index.js.map +1 -1
  76. package/dist/plugins/BatchingDbPlugin.d.ts +139 -0
  77. package/dist/plugins/CacheDbPlugin.d.ts +70 -0
  78. package/dist/plugins/ConcurrencyDbPlugin.d.ts +78 -0
  79. package/dist/plugins/EphemeralDataPlugin.d.ts +57 -1
  80. package/dist/plugins/RetryDbPlugin.d.ts +77 -0
  81. package/dist/plugins/index.cjs +6104 -0
  82. package/dist/plugins/index.cjs.map +1 -0
  83. package/dist/plugins/index.d.ts +5 -1
  84. package/dist/plugins/index.js +5572 -1491
  85. package/dist/plugins/index.js.map +1 -1
  86. package/dist/plugins/query/QueryOptionsCollection.d.ts +22 -0
  87. package/dist/plugins/query/index.d.ts +2 -0
  88. package/dist/plugins/query/join.d.ts +210 -0
  89. package/dist/plugins/query/similarity.d.ts +38 -0
  90. package/dist/plugins/query/types.d.ts +64 -1
  91. package/dist/plugins/translators/DataTranslator.d.ts +28 -0
  92. package/dist/plugins/translators/JsonTranslator.d.ts +27 -0
  93. package/dist/plugins/translators/SqlTranslator.d.ts +45 -0
  94. package/dist/plugins/translators/TranslatedArrayValue.d.ts +3 -1
  95. package/dist/plugins/translators/TranslatedGroupValue.d.ts +3 -1
  96. package/dist/plugins/translators/TranslatedSingleValue.d.ts +3 -1
  97. package/dist/plugins/translators/TupleTranslator.d.ts +45 -0
  98. package/dist/plugins/translators/index.d.ts +1 -0
  99. package/dist/plugins/translators/types.d.ts +8 -1
  100. package/dist/plugins/types.d.ts +87 -11
  101. package/dist/plugins/wire/handler.d.ts +122 -0
  102. package/dist/plugins/wire/index.d.ts +4 -0
  103. package/dist/plugins/wire/persist.d.ts +43 -0
  104. package/dist/plugins/wire/query.d.ts +49 -0
  105. package/dist/plugins/wire/types.d.ts +123 -0
  106. package/dist/results/Result.d.ts +2 -2
  107. package/dist/results/index.cjs +174 -0
  108. package/dist/results/index.cjs.map +1 -0
  109. package/dist/results/index.js +29 -44
  110. package/dist/results/index.js.map +1 -1
  111. package/dist/schema/PropertyInfo.d.ts +35 -2
  112. package/dist/schema/SchemaDefinition.d.ts +51 -1
  113. package/dist/schema/builder.d.ts +35 -1
  114. package/dist/schema/communication/broadcast.d.ts +17 -1
  115. package/dist/schema/index.cjs +6911 -0
  116. package/dist/schema/index.cjs.map +1 -0
  117. package/dist/schema/index.d.ts +4 -0
  118. package/dist/schema/index.js +5781 -3973
  119. package/dist/schema/index.js.map +1 -1
  120. package/dist/schema/property/base/SchemaBase.d.ts +50 -2
  121. package/dist/schema/property/modifiers/SchemaDeserialize.d.ts +5 -1
  122. package/dist/schema/property/modifiers/SchemaForeignKey.d.ts +15 -0
  123. package/dist/schema/property/modifiers/SchemaKey.d.ts +2 -0
  124. package/dist/schema/property/modifiers/SchemaNullable.d.ts +15 -1
  125. package/dist/schema/property/modifiers/SchemaOptional.d.ts +22 -1
  126. package/dist/schema/property/modifiers/SchemaSearchable.d.ts +34 -0
  127. package/dist/schema/property/modifiers/SchemaTag.d.ts +27 -0
  128. package/dist/schema/property/modifiers/SchemaTracked.d.ts +5 -1
  129. package/dist/schema/property/modifiers/index.d.ts +3 -0
  130. package/dist/schema/property/types/SchemaArray.d.ts +2 -0
  131. package/dist/schema/property/types/SchemaBoolean.d.ts +2 -0
  132. package/dist/schema/property/types/SchemaDate.d.ts +2 -0
  133. package/dist/schema/property/types/SchemaFile.d.ts +60 -0
  134. package/dist/schema/property/types/SchemaNumber.d.ts +5 -1
  135. package/dist/schema/property/types/SchemaObject.d.ts +2 -0
  136. package/dist/schema/property/types/SchemaString.d.ts +25 -1
  137. package/dist/schema/property/types/SchemaVector.d.ts +61 -0
  138. package/dist/schema/property/types/index.d.ts +2 -0
  139. package/dist/schema/table/SchemaTransform.d.ts +40 -0
  140. package/dist/schema/table/index.d.ts +1 -0
  141. package/dist/schema/types.d.ts +200 -32
  142. package/dist/schema/utils/propertyKind.d.ts +4 -0
  143. package/dist/schema/utils/standardJsonSchema.d.ts +90 -0
  144. package/dist/types/index.cjs +23 -0
  145. package/dist/types/index.cjs.map +1 -0
  146. package/dist/types/index.js +0 -14
  147. package/dist/types/index.js.map +1 -1
  148. package/dist/utilities/functions.d.ts +1 -1
  149. package/dist/utilities/index.cjs +982 -0
  150. package/dist/utilities/index.cjs.map +1 -0
  151. package/dist/utilities/index.js +590 -321
  152. package/dist/utilities/index.js.map +1 -1
  153. package/dist/utilities/logger.d.ts +48 -0
  154. package/package.json +152 -98
  155. package/dist/collections/Changes.test.d.ts +0 -1
  156. package/dist/collections/MemoryDataCollection.test.d.ts +0 -1
  157. package/dist/collections/TagCollection.test.d.ts +0 -1
  158. package/dist/expressions/parser.test.d.ts +0 -1
  159. package/dist/expressions/utils.test.d.ts +0 -1
  160. package/dist/pipeline/SyncronousQueue.test.d.ts +0 -1
  161. package/dist/pipeline/WorkPipeline.test.d.ts +0 -1
  162. package/dist/plugins/EphemeralDataPlugin.test.d.ts +0 -1
  163. package/dist/plugins/query/QueryOptionsCollection.test.d.ts +0 -1
  164. package/dist/plugins/replication/OptimisticReplicationDbPlugin.d.ts +0 -23
  165. package/dist/plugins/replication/ReplicationDbPlugin.d.ts +0 -22
  166. package/dist/plugins/replication/index.d.ts +0 -3
  167. package/dist/plugins/replication/types.d.ts +0 -5
  168. package/dist/plugins/translators/JsonTranslator.test.d.ts +0 -1
  169. package/dist/schema/PropertyInfo.test.d.ts +0 -1
  170. package/dist/schema/schemaGeneration.test.d.ts +0 -1
  171. package/dist/schema/testSchemas.test.d.ts +0 -314
  172. package/dist/utilities/arrays.test.d.ts +0 -1
  173. package/dist/utilities/dates.test.d.ts +0 -1
  174. package/dist/utilities/strings.test.d.ts +0 -1
@@ -13,10 +13,32 @@ export declare class QueryOptionsCollection<T> {
13
13
  static EMPTY<R>(): QueryOptionsCollection<R>;
14
14
  static isEmpty<T>(options: QueryOptionsCollection<T>): boolean;
15
15
  add<K extends QueryOptionName>(name: K, value: QueryOption<T, K>["value"]): void;
16
+ /**
17
+ * Splits the collection around the FIRST occurrence of `name`, preserving order.
18
+ *
19
+ * For a join: the options recorded before it operate on entity rows, the option itself
20
+ * produces tuples, and the ones after it operate on tuples. Three different shapes, so the
21
+ * caller has to run them in three steps rather than one pass.
22
+ */
23
+ splitAt<K extends QueryOptionName>(name: K): {
24
+ before: QueryOptionsCollection<T>;
25
+ at: QueryOption<T, K> | null;
26
+ after: QueryOptionsCollection<T>;
27
+ };
28
+ /**
29
+ * Captures the collection's current state and returns a function that restores it.
30
+ *
31
+ * Terminal queryable operations (count, first, aggregates, …) record their option on
32
+ * the shared collection before executing. Without restoring, a re-executed terminal —
33
+ * the whole point of a subscribed queryable — stacks its option a second time and
34
+ * runs it over the first execution's scalar result.
35
+ */
36
+ snapshot(): () => void;
16
37
  split(): {
17
38
  memory: QueryOptionsCollection<T>;
18
39
  database: QueryOptionsCollection<T>;
19
40
  };
41
+ hasTransformations(): boolean;
20
42
  has<K extends QueryOptionName>(name: K): boolean;
21
43
  get<K extends QueryOptionName>(name: K): QueryCollectionItem<T, K>[];
22
44
  getLast<K extends QueryOptionName>(name: K): QueryOption<T, K> | null;
@@ -1,3 +1,5 @@
1
+ export * from './join';
1
2
  export * from './Query';
2
3
  export * from './QueryOptionsCollection';
3
4
  export * from './types';
5
+ 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"
@@ -29,6 +31,7 @@ export type QueryOptionValueMap<T extends {}> = {
29
31
  selector: GenericFunction<T, T[keyof T]>;
30
32
  direction: QueryOrdering;
31
33
  propertyName: string;
34
+ property?: PropertyInfo<T> | null;
32
35
  };
33
36
  map: {
34
37
  selector: GenericFunction<T, any>;
@@ -44,6 +47,66 @@ export type QueryOptionValueMap<T extends {}> = {
44
47
  filter: ParamsFilter<T, {}> | Filter<T>;
45
48
  expression: Expression;
46
49
  };
50
+ /**
51
+ * Similarity search: an ordering plus a limit, never a filter.
52
+ *
53
+ * `count` is part of the option rather than a separate `take` because the two are one
54
+ * operation to a backend that can push this down — `ORDER BY ... LIMIT n` is what makes an
55
+ * approximate index usable, and splitting them would order every row before limiting.
56
+ */
57
+ nearest: {
58
+ selector: GenericFunction<T, T[keyof T]>;
59
+ propertyName: string;
60
+ property?: PropertyInfo<T> | null;
61
+ vector: number[];
62
+ count: number;
63
+ };
64
+ /**
65
+ * An equi-join against a second collection, interpreted by whoever receives it.
66
+ *
67
+ * A first-class query option rather than a datastore side-path: a SQL backend emits a real
68
+ * `INNER JOIN`/`LEFT JOIN`, every other backend loads the rows it needs and the shared hash
69
+ * join runs inside the plugin, and a cross-plugin join runs in the datastore's memory half.
70
+ * All three produce the same pairs — see `specs/joins.md`.
71
+ *
72
+ * Serializable by construction: property paths and a schema id, never live rows, with any
73
+ * filter's values travelling in its params object. That is what lets the whole option be
74
+ * forwarded to a server once expression-tree serialization lands.
75
+ */
76
+ join: {
77
+ kind: JoinKind;
78
+ /** Resolved through `event.schemas`, which already carries every schema in the store. */
79
+ innerSchemaId: SchemaId;
80
+ outerKey: JoinKeyReference;
81
+ innerKey: JoinKeyReference;
82
+ /**
83
+ * The inner side's own filters — INCLUDING its soft-delete scope and `.scope()`
84
+ * filters. Every interpreter must apply these: it is the only place they exist, because
85
+ * a join bypasses the inner collection's normal read path.
86
+ */
87
+ innerOptions: QueryOptionsCollection<any>;
88
+ /**
89
+ * Whether the two sides live on DIFFERENT plugin instances, in which case no plugin can
90
+ * receive the option and the datastore is the interpreter.
91
+ *
92
+ * Decided by plugin instance identity at build time, never by comparing database names —
93
+ * two plugins over one database are still two interpreters, and one name can front two
94
+ * databases.
95
+ */
96
+ crossPlugin: boolean;
97
+ /**
98
+ * How many distinct outer keys are still worth turning into an `IN (...)` prefilter on the
99
+ * inner read — the datastore's `semiJoinKeyThreshold`, default 500.
100
+ *
101
+ * Carried in the option because the decision is made where the join executes, which is
102
+ * usually inside a plugin, and a plugin cannot see a datastore's configuration. A number
103
+ * serializes; a reference to the store would not.
104
+ *
105
+ * Cost only. Above the threshold the inner side is read under its own scopes and the hash
106
+ * join discards the surplus — the same answer by a slower route.
107
+ */
108
+ semiJoinKeyThreshold: number;
109
+ };
47
110
  min: true;
48
111
  max: true;
49
112
  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
  }
@@ -1,6 +1,8 @@
1
1
  import { ITranslatedValue } from './types';
2
2
  export declare class TranslatedArrayValue<T> implements ITranslatedValue<T> {
3
3
  readonly value: T;
4
- constructor(value: unknown);
4
+ readonly isTransformed: boolean;
5
+ readonly isEmpty: boolean;
6
+ constructor(value: unknown, isTransformed: boolean);
5
7
  forEach(callback: (item: unknown) => unknown): void;
6
8
  }
@@ -1,6 +1,8 @@
1
1
  import { ITranslatedValue } from './types';
2
2
  export declare class TranslatedGroupValue<T> implements ITranslatedValue<T> {
3
3
  readonly value: T;
4
- constructor(value: unknown);
4
+ readonly isTransformed: boolean;
5
+ readonly isEmpty: boolean;
6
+ constructor(value: unknown, isTransformed: boolean);
5
7
  forEach(callback: (item: unknown) => unknown): void;
6
8
  }
@@ -1,6 +1,8 @@
1
1
  import { ITranslatedValue } from './types';
2
2
  export declare class TranslatedSingleValue<T> implements ITranslatedValue<T> {
3
3
  value: T;
4
- constructor(value: unknown);
4
+ readonly isTransformed: boolean;
5
+ readonly isEmpty: boolean;
6
+ constructor(value: unknown, isTransformed: boolean);
5
7
  forEach(callback: (item: unknown) => unknown): void;
6
8
  }
@@ -0,0 +1,45 @@
1
+ import { DataTranslator } from "./DataTranslator";
2
+ import { QueryOption } from "../query/types";
3
+ /**
4
+ * The memory half's pass over JOIN TUPLES.
5
+ *
6
+ * `JsonTranslator` cannot do this job, and the reason is one line of it: `map` and `group` walk
7
+ * `option.value.fields` and deserialize each field through its `PropertyInfo`. A tuple has no
8
+ * schema and no fields — `getFields` over `([p, m]) => ({ ... })` extracts nothing meaningful —
9
+ * so that loop would either no-op or write properties onto a two-element array.
10
+ *
11
+ * Everything a join query is allowed to do after the join is expressible as a plain closure
12
+ * over the tuple, and that is all this does. The lambdas the caller wrote (`([p, m]) => ...`)
13
+ * are applied as-is, which is also why the result is identical whichever backend produced the
14
+ * pairs.
15
+ *
16
+ * Both halves are already in entity shape when the tuples arrive here (the wire contract), so
17
+ * there is nothing left to deserialize — the reason the missing field loop costs nothing.
18
+ *
19
+ * The absent operations are absent by design, not by omission: `sum`/`min`/`max`/`distinct` and
20
+ * `group` are not declared on the tuple queryable's type, so reaching them means something
21
+ * built an option this API cannot express, and a throw naming it is the honest answer.
22
+ */
23
+ export declare class TupleTranslator<TRoot extends {}, TShape> extends DataTranslator<TRoot, TShape> {
24
+ filter<TResult>(data: unknown, option: QueryOption<TShape, "filter">): TResult;
25
+ map<TResult>(data: unknown, option: QueryOption<TShape, "map">): TResult;
26
+ sort<TResult>(data: unknown, option: QueryOption<TShape, "sort">): TResult;
27
+ skip<TResult>(data: unknown, option: QueryOption<TShape, "skip">): TResult;
28
+ take<TResult>(data: unknown, option: QueryOption<TShape, "take">): TResult;
29
+ count<TResult extends number>(data: unknown, _: QueryOption<TShape, "count">): TResult;
30
+ /**
31
+ * Already joined by the time anything reaches here.
32
+ *
33
+ * The pairs were produced either by the plugin (its translator's `join`) or by the datastore
34
+ * before this pass ran, so the option is a record of what happened rather than work to do.
35
+ */
36
+ join(data: unknown, _: QueryOption<TShape, "join">): TShape;
37
+ group<TResult>(_: unknown, __: QueryOption<TShape, "group">): TResult;
38
+ sum<TResult extends number>(_: unknown, __: QueryOption<TShape, "sum">): TResult;
39
+ min<TResult extends string | number | Date>(_: unknown, __: QueryOption<TShape, "min">): TResult;
40
+ max<TResult extends string | number | Date>(_: unknown, __: QueryOption<TShape, "max">): TResult;
41
+ distinct<TResult>(_: unknown, __: QueryOption<TShape, "distinct">): TResult;
42
+ nearest(_: unknown, __: QueryOption<TShape, "nearest">): TShape;
43
+ private notSupported;
44
+ private notAnArray;
45
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './DataTranslator';
2
2
  export * from './JsonTranslator';
3
3
  export * from './SqlTranslator';
4
+ export * from './TupleTranslator';
4
5
  export * from './types';
5
6
  export * from './TranslatedArrayValue';
6
7
  export * from './TranslatedGroupValue';
@@ -2,9 +2,16 @@ export interface ITranslatedValue<T> {
2
2
  readonly value: T;
3
3
  /**
4
4
  * Iterates over items in the collection, calling the callback for each item.
5
- * If the callback returns a value, that value will replace the original item in the collection.
5
+ * If the callback returns a value, that value replaces the original item (e.g. so
6
+ * changeTracker.resolve() can swap in attached/merged entities).
6
7
  *
7
8
  * @param callback Function called for each item. If it returns a value, that value replaces the original item.
8
9
  */
9
10
  forEach(callback: (item: unknown) => unknown): void;
11
+ /**
12
+ * True if the translator remapped or transformed the data from the database shape;
13
+ * false if the data is unchanged and in the same shape as the database.
14
+ */
15
+ readonly isTransformed: boolean;
16
+ readonly isEmpty: boolean;
10
17
  }