@prisma/streams-server 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- package/src/util/stream_paths.ts +19 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import type { SchemaRegistry, SearchFieldConfig } from "./schema/registry";
|
|
3
|
+
import { extractSearchExactValuesResult, resolveSearchAlias } from "./search/schema";
|
|
4
|
+
|
|
5
|
+
type Token =
|
|
6
|
+
| { kind: "word"; value: string }
|
|
7
|
+
| { kind: "string"; value: string }
|
|
8
|
+
| { kind: "lparen" }
|
|
9
|
+
| { kind: "rparen" }
|
|
10
|
+
| { kind: "colon" }
|
|
11
|
+
| { kind: "op"; value: "=" | ">" | ">=" | "<" | "<=" }
|
|
12
|
+
| { kind: "minus" };
|
|
13
|
+
|
|
14
|
+
export type ReadFilterComparisonOp = "eq" | "gt" | "gte" | "lt" | "lte";
|
|
15
|
+
|
|
16
|
+
type FilterExpr =
|
|
17
|
+
| { kind: "and"; left: FilterExpr; right: FilterExpr }
|
|
18
|
+
| { kind: "or"; left: FilterExpr; right: FilterExpr }
|
|
19
|
+
| { kind: "not"; expr: FilterExpr }
|
|
20
|
+
| { kind: "has"; field: string }
|
|
21
|
+
| { kind: "compare"; field: string; op: ReadFilterComparisonOp; rawValue: string };
|
|
22
|
+
|
|
23
|
+
export type CompiledReadFilterClause = {
|
|
24
|
+
kind: "has" | "compare";
|
|
25
|
+
field: string;
|
|
26
|
+
index: SearchFieldConfig;
|
|
27
|
+
op?: ReadFilterComparisonOp;
|
|
28
|
+
canonicalValue?: string;
|
|
29
|
+
compareValue?: bigint | number | boolean | string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CompiledReadFilter =
|
|
33
|
+
| { kind: "and"; left: CompiledReadFilter; right: CompiledReadFilter }
|
|
34
|
+
| { kind: "or"; left: CompiledReadFilter; right: CompiledReadFilter }
|
|
35
|
+
| { kind: "not"; expr: CompiledReadFilter }
|
|
36
|
+
| ({ kind: "has" } & CompiledReadFilterClause)
|
|
37
|
+
| ({ kind: "compare" } & CompiledReadFilterClause);
|
|
38
|
+
|
|
39
|
+
export type ReadFilterExactClause = {
|
|
40
|
+
field: string;
|
|
41
|
+
canonicalValue: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ReadFilterColumnClause = {
|
|
45
|
+
field: string;
|
|
46
|
+
op: ReadFilterComparisonOp | "has";
|
|
47
|
+
compareValue?: bigint | number | boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function tokenizeResult(input: string): Result<Token[], { message: string }> {
|
|
51
|
+
const tokens: Token[] = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < input.length) {
|
|
54
|
+
const ch = input[i];
|
|
55
|
+
if (/\s/.test(ch)) {
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === "(") {
|
|
60
|
+
tokens.push({ kind: "lparen" });
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === ")") {
|
|
65
|
+
tokens.push({ kind: "rparen" });
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === ":") {
|
|
70
|
+
tokens.push({ kind: "colon" });
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "-") {
|
|
75
|
+
tokens.push({ kind: "minus" });
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === ">" || ch === "<" || ch === "=") {
|
|
80
|
+
if ((ch === ">" || ch === "<") && input[i + 1] === "=") {
|
|
81
|
+
tokens.push({ kind: "op", value: `${ch}=` as ">=" | "<=" });
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
tokens.push({ kind: "op", value: ch as "=" | ">" | "<" });
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === "\"") {
|
|
90
|
+
let out = "";
|
|
91
|
+
i += 1;
|
|
92
|
+
while (i < input.length) {
|
|
93
|
+
const cur = input[i];
|
|
94
|
+
if (cur === "\\") {
|
|
95
|
+
if (i + 1 >= input.length) return Result.err({ message: "unterminated escape in filter string" });
|
|
96
|
+
out += input[i + 1];
|
|
97
|
+
i += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (cur === "\"") break;
|
|
101
|
+
out += cur;
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
if (i >= input.length || input[i] !== "\"") return Result.err({ message: "unterminated quoted string in filter" });
|
|
105
|
+
i += 1;
|
|
106
|
+
tokens.push({ kind: "string", value: out });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let j = i;
|
|
110
|
+
while (j < input.length) {
|
|
111
|
+
const cur = input[j];
|
|
112
|
+
if (/\s/.test(cur) || cur === "(" || cur === ")" || cur === ":" || cur === ">" || cur === "<" || cur === "=") break;
|
|
113
|
+
j += 1;
|
|
114
|
+
}
|
|
115
|
+
const word = input.slice(i, j);
|
|
116
|
+
if (word === "") return Result.err({ message: "invalid filter syntax" });
|
|
117
|
+
tokens.push({ kind: "word", value: word });
|
|
118
|
+
i = j;
|
|
119
|
+
}
|
|
120
|
+
return Result.ok(tokens);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class Parser {
|
|
124
|
+
constructor(private readonly tokens: Token[], private pos = 0) {}
|
|
125
|
+
|
|
126
|
+
parseResult(): Result<FilterExpr, { message: string }> {
|
|
127
|
+
const exprRes = this.parseOrResult();
|
|
128
|
+
if (Result.isError(exprRes)) return exprRes;
|
|
129
|
+
if (!this.isAtEnd()) return Result.err({ message: "unexpected token in filter" });
|
|
130
|
+
return exprRes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private parseOrResult(): Result<FilterExpr, { message: string }> {
|
|
134
|
+
let leftRes = this.parseAndResult();
|
|
135
|
+
if (Result.isError(leftRes)) return leftRes;
|
|
136
|
+
let left = leftRes.value;
|
|
137
|
+
while (this.peekWord("OR")) {
|
|
138
|
+
this.pos += 1;
|
|
139
|
+
const rightRes = this.parseAndResult();
|
|
140
|
+
if (Result.isError(rightRes)) return rightRes;
|
|
141
|
+
left = { kind: "or", left, right: rightRes.value };
|
|
142
|
+
}
|
|
143
|
+
return Result.ok(left);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private parseAndResult(): Result<FilterExpr, { message: string }> {
|
|
147
|
+
let leftRes = this.parseUnaryResult();
|
|
148
|
+
if (Result.isError(leftRes)) return leftRes;
|
|
149
|
+
let left = leftRes.value;
|
|
150
|
+
while (!this.isAtEnd() && !this.peekKind("rparen") && !this.peekWord("OR")) {
|
|
151
|
+
if (this.peekWord("AND")) this.pos += 1;
|
|
152
|
+
const rightRes = this.parseUnaryResult();
|
|
153
|
+
if (Result.isError(rightRes)) return rightRes;
|
|
154
|
+
left = { kind: "and", left, right: rightRes.value };
|
|
155
|
+
}
|
|
156
|
+
return Result.ok(left);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private parseUnaryResult(): Result<FilterExpr, { message: string }> {
|
|
160
|
+
if (this.peekWord("NOT")) {
|
|
161
|
+
this.pos += 1;
|
|
162
|
+
const innerRes = this.parseUnaryResult();
|
|
163
|
+
if (Result.isError(innerRes)) return innerRes;
|
|
164
|
+
return Result.ok({ kind: "not", expr: innerRes.value });
|
|
165
|
+
}
|
|
166
|
+
if (this.peekKind("minus")) {
|
|
167
|
+
this.pos += 1;
|
|
168
|
+
const innerRes = this.parseUnaryResult();
|
|
169
|
+
if (Result.isError(innerRes)) return innerRes;
|
|
170
|
+
return Result.ok({ kind: "not", expr: innerRes.value });
|
|
171
|
+
}
|
|
172
|
+
return this.parsePrimaryResult();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private parsePrimaryResult(): Result<FilterExpr, { message: string }> {
|
|
176
|
+
if (this.peekKind("lparen")) {
|
|
177
|
+
this.pos += 1;
|
|
178
|
+
const exprRes = this.parseOrResult();
|
|
179
|
+
if (Result.isError(exprRes)) return exprRes;
|
|
180
|
+
if (!this.peekKind("rparen")) return Result.err({ message: "missing ')' in filter" });
|
|
181
|
+
this.pos += 1;
|
|
182
|
+
return exprRes;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const token = this.consumeWordOrString();
|
|
186
|
+
if (!token) return Result.err({ message: "expected clause in filter" });
|
|
187
|
+
if (token.kind !== "word") return Result.err({ message: "expected field name in filter" });
|
|
188
|
+
const fieldOrKeyword = token.value;
|
|
189
|
+
|
|
190
|
+
if (!this.peekKind("colon")) return Result.err({ message: "expected ':' in filter clause" });
|
|
191
|
+
this.pos += 1;
|
|
192
|
+
|
|
193
|
+
if (fieldOrKeyword === "has") {
|
|
194
|
+
const fieldToken = this.consumeWordOrString();
|
|
195
|
+
if (!fieldToken || fieldToken.kind !== "word") return Result.err({ message: "has: requires a field name" });
|
|
196
|
+
return Result.ok({ kind: "has", field: fieldToken.value });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let op: ReadFilterComparisonOp = "eq";
|
|
200
|
+
if (this.peekKind("op")) {
|
|
201
|
+
const raw = (this.tokens[this.pos] as Extract<Token, { kind: "op" }>).value;
|
|
202
|
+
this.pos += 1;
|
|
203
|
+
op = raw === "=" ? "eq" : raw === ">" ? "gt" : raw === ">=" ? "gte" : raw === "<" ? "lt" : "lte";
|
|
204
|
+
}
|
|
205
|
+
const valueToken = this.consumeWordOrString();
|
|
206
|
+
if (!valueToken) return Result.err({ message: "expected value in filter clause" });
|
|
207
|
+
return Result.ok({
|
|
208
|
+
kind: "compare",
|
|
209
|
+
field: fieldOrKeyword,
|
|
210
|
+
op,
|
|
211
|
+
rawValue: valueToken.value,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private consumeWordOrString(): Extract<Token, { kind: "word" | "string" }> | null {
|
|
216
|
+
const token = this.tokens[this.pos];
|
|
217
|
+
if (!token || (token.kind !== "word" && token.kind !== "string")) return null;
|
|
218
|
+
this.pos += 1;
|
|
219
|
+
return token;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private peekKind(kind: Token["kind"]): boolean {
|
|
223
|
+
return this.tokens[this.pos]?.kind === kind;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private peekWord(value: string): boolean {
|
|
227
|
+
const token = this.tokens[this.pos];
|
|
228
|
+
return token?.kind === "word" && token.value.toUpperCase() === value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private isAtEnd(): boolean {
|
|
232
|
+
return this.pos >= this.tokens.length;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function canonicalizeFilterValue(index: SearchFieldConfig, rawValue: string): string | null {
|
|
237
|
+
switch (index.kind) {
|
|
238
|
+
case "keyword":
|
|
239
|
+
return index.normalizer === "lowercase_v1" ? rawValue.toLowerCase() : rawValue;
|
|
240
|
+
case "integer":
|
|
241
|
+
return /^-?(0|[1-9][0-9]*)$/.test(rawValue.trim()) ? String(BigInt(rawValue.trim())) : null;
|
|
242
|
+
case "float": {
|
|
243
|
+
const parsed = Number(rawValue);
|
|
244
|
+
return Number.isFinite(parsed) ? String(parsed) : null;
|
|
245
|
+
}
|
|
246
|
+
case "date": {
|
|
247
|
+
if (rawValue.trim() === "") return null;
|
|
248
|
+
const parsed = Date.parse(rawValue);
|
|
249
|
+
if (Number.isFinite(parsed)) return String(Math.trunc(parsed));
|
|
250
|
+
return /^-?(0|[1-9][0-9]*)$/.test(rawValue.trim()) ? String(BigInt(rawValue.trim())) : null;
|
|
251
|
+
}
|
|
252
|
+
case "bool": {
|
|
253
|
+
const lowered = rawValue.trim().toLowerCase();
|
|
254
|
+
return lowered === "true" || lowered === "false" ? lowered : null;
|
|
255
|
+
}
|
|
256
|
+
default:
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compileCompareValueResult(index: SearchFieldConfig, rawValue: string, op: ReadFilterComparisonOp): Result<{
|
|
262
|
+
canonicalValue: string;
|
|
263
|
+
compareValue: bigint | number | boolean | string;
|
|
264
|
+
}, { message: string }> {
|
|
265
|
+
const canonical = canonicalizeFilterValue(index, rawValue);
|
|
266
|
+
if (canonical == null) return Result.err({ message: "invalid value for filter field" });
|
|
267
|
+
if (op === "eq") {
|
|
268
|
+
if (index.kind === "integer" || index.kind === "date") {
|
|
269
|
+
return Result.ok({ canonicalValue: canonical, compareValue: BigInt(canonical) });
|
|
270
|
+
}
|
|
271
|
+
if (index.kind === "float") {
|
|
272
|
+
const parsed = Number(canonical);
|
|
273
|
+
if (!Number.isFinite(parsed)) return Result.err({ message: "invalid numeric value for filter field" });
|
|
274
|
+
return Result.ok({ canonicalValue: canonical, compareValue: parsed });
|
|
275
|
+
}
|
|
276
|
+
if (index.kind === "bool") {
|
|
277
|
+
return Result.ok({ canonicalValue: canonical, compareValue: canonical === "true" });
|
|
278
|
+
}
|
|
279
|
+
return Result.ok({ canonicalValue: canonical, compareValue: canonical });
|
|
280
|
+
}
|
|
281
|
+
if (index.kind === "integer" || index.kind === "date") {
|
|
282
|
+
return Result.ok({ canonicalValue: canonical, compareValue: BigInt(canonical) });
|
|
283
|
+
}
|
|
284
|
+
if (index.kind === "float") {
|
|
285
|
+
const parsed = Number(canonical);
|
|
286
|
+
if (!Number.isFinite(parsed)) return Result.err({ message: "invalid numeric value for filter field" });
|
|
287
|
+
return Result.ok({ canonicalValue: canonical, compareValue: parsed });
|
|
288
|
+
}
|
|
289
|
+
return Result.err({ message: "comparison operator not supported for filter field" });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function compileExprResult(
|
|
293
|
+
expr: FilterExpr,
|
|
294
|
+
indexByName: Map<string, { field: string; config: SearchFieldConfig }>
|
|
295
|
+
): Result<CompiledReadFilter, { message: string }> {
|
|
296
|
+
if (expr.kind === "and") {
|
|
297
|
+
const leftRes = compileExprResult(expr.left, indexByName);
|
|
298
|
+
if (Result.isError(leftRes)) return leftRes;
|
|
299
|
+
const rightRes = compileExprResult(expr.right, indexByName);
|
|
300
|
+
if (Result.isError(rightRes)) return rightRes;
|
|
301
|
+
return Result.ok({ kind: "and", left: leftRes.value, right: rightRes.value });
|
|
302
|
+
}
|
|
303
|
+
if (expr.kind === "or") {
|
|
304
|
+
const leftRes = compileExprResult(expr.left, indexByName);
|
|
305
|
+
if (Result.isError(leftRes)) return leftRes;
|
|
306
|
+
const rightRes = compileExprResult(expr.right, indexByName);
|
|
307
|
+
if (Result.isError(rightRes)) return rightRes;
|
|
308
|
+
return Result.ok({ kind: "or", left: leftRes.value, right: rightRes.value });
|
|
309
|
+
}
|
|
310
|
+
if (expr.kind === "not") {
|
|
311
|
+
const innerRes = compileExprResult(expr.expr, indexByName);
|
|
312
|
+
if (Result.isError(innerRes)) return innerRes;
|
|
313
|
+
return Result.ok({ kind: "not", expr: innerRes.value });
|
|
314
|
+
}
|
|
315
|
+
const resolved = indexByName.get(expr.field);
|
|
316
|
+
if (!resolved) return Result.err({ message: `filter field ${expr.field} is not indexed` });
|
|
317
|
+
const index = resolved.config;
|
|
318
|
+
if (expr.kind === "has" && !index.exists && !index.exact && !index.column) {
|
|
319
|
+
return Result.err({ message: `filter field ${expr.field} does not support has:` });
|
|
320
|
+
}
|
|
321
|
+
if (expr.kind === "has") {
|
|
322
|
+
return Result.ok({ kind: "has", field: resolved.field, index });
|
|
323
|
+
}
|
|
324
|
+
const compareRes = compileCompareValueResult(index, expr.rawValue, expr.op);
|
|
325
|
+
if (Result.isError(compareRes)) return compareRes;
|
|
326
|
+
if (expr.op !== "eq" && !index.column) {
|
|
327
|
+
return Result.err({ message: `filter field ${expr.field} does not support comparisons` });
|
|
328
|
+
}
|
|
329
|
+
if (expr.op === "eq" && !index.exact && !index.column) {
|
|
330
|
+
return Result.err({ message: `filter field ${expr.field} does not support equality filters` });
|
|
331
|
+
}
|
|
332
|
+
return Result.ok({
|
|
333
|
+
kind: "compare",
|
|
334
|
+
field: resolved.field,
|
|
335
|
+
index,
|
|
336
|
+
op: expr.op,
|
|
337
|
+
canonicalValue: compareRes.value.canonicalValue,
|
|
338
|
+
compareValue: compareRes.value.compareValue,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function parseReadFilterResult(
|
|
343
|
+
registry: SchemaRegistry,
|
|
344
|
+
input: string
|
|
345
|
+
): Result<CompiledReadFilter, { message: string }> {
|
|
346
|
+
const trimmed = input.trim();
|
|
347
|
+
if (trimmed === "") return Result.err({ message: "filter must not be empty" });
|
|
348
|
+
const tokensRes = tokenizeResult(trimmed);
|
|
349
|
+
if (Result.isError(tokensRes)) return tokensRes;
|
|
350
|
+
const parser = new Parser(tokensRes.value);
|
|
351
|
+
const exprRes = parser.parseResult();
|
|
352
|
+
if (Result.isError(exprRes)) return exprRes;
|
|
353
|
+
const fields = registry.search?.fields ?? {};
|
|
354
|
+
const indexByName = new Map<string, { field: string; config: SearchFieldConfig }>();
|
|
355
|
+
for (const [fieldName, config] of Object.entries(fields)) indexByName.set(fieldName, { field: fieldName, config });
|
|
356
|
+
for (const alias of Object.keys(registry.search?.aliases ?? {})) {
|
|
357
|
+
const resolved = resolveSearchAlias(registry.search, alias);
|
|
358
|
+
const config = fields[resolved];
|
|
359
|
+
if (config) indexByName.set(alias, { field: resolved, config });
|
|
360
|
+
}
|
|
361
|
+
return compileExprResult(exprRes.value, indexByName);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function compareCanonicals(left: string, right: bigint | number | boolean | string, kind: SearchFieldConfig["kind"]): number {
|
|
365
|
+
if (kind === "integer" || kind === "date") {
|
|
366
|
+
const l = BigInt(left);
|
|
367
|
+
const r = right as bigint;
|
|
368
|
+
return l < r ? -1 : l > r ? 1 : 0;
|
|
369
|
+
}
|
|
370
|
+
if (kind === "float") {
|
|
371
|
+
const l = Number(left);
|
|
372
|
+
const r = right as number;
|
|
373
|
+
return l < r ? -1 : l > r ? 1 : 0;
|
|
374
|
+
}
|
|
375
|
+
if (kind === "bool") {
|
|
376
|
+
const l = left === "true";
|
|
377
|
+
const r = right === true || right === "true";
|
|
378
|
+
return l === r ? 0 : l ? 1 : -1;
|
|
379
|
+
}
|
|
380
|
+
const r = String(right);
|
|
381
|
+
return left < r ? -1 : left > r ? 1 : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function evaluateClause(filter: CompiledReadFilterClause, values: string[] | undefined): boolean {
|
|
385
|
+
if (filter.kind === "has") return !!values && values.length > 0;
|
|
386
|
+
if (!values || values.length === 0) return false;
|
|
387
|
+
if (filter.op === "eq") return values.includes(filter.canonicalValue!);
|
|
388
|
+
for (const value of values) {
|
|
389
|
+
const cmp = compareCanonicals(value, filter.compareValue!, filter.index.kind);
|
|
390
|
+
if (filter.op === "gt" && cmp > 0) return true;
|
|
391
|
+
if (filter.op === "gte" && cmp >= 0) return true;
|
|
392
|
+
if (filter.op === "lt" && cmp < 0) return true;
|
|
393
|
+
if (filter.op === "lte" && cmp <= 0) return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function evaluateReadFilterResult(
|
|
399
|
+
registry: SchemaRegistry,
|
|
400
|
+
offset: bigint,
|
|
401
|
+
filter: CompiledReadFilter,
|
|
402
|
+
value: unknown
|
|
403
|
+
): Result<boolean, { message: string }> {
|
|
404
|
+
const valuesRes = extractSearchExactValuesResult(registry, offset, value);
|
|
405
|
+
if (Result.isError(valuesRes)) return valuesRes;
|
|
406
|
+
const values = valuesRes.value;
|
|
407
|
+
const evalNode = (node: CompiledReadFilter): boolean => {
|
|
408
|
+
if (node.kind === "and") return evalNode(node.left) && evalNode(node.right);
|
|
409
|
+
if (node.kind === "or") return evalNode(node.left) || evalNode(node.right);
|
|
410
|
+
if (node.kind === "not") return !evalNode(node.expr);
|
|
411
|
+
return evaluateClause(node, values.get(node.field));
|
|
412
|
+
};
|
|
413
|
+
return Result.ok(evalNode(filter));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function collectPositiveExactFilterClauses(filter: CompiledReadFilter): ReadFilterExactClause[] {
|
|
417
|
+
const out: ReadFilterExactClause[] = [];
|
|
418
|
+
const visit = (node: CompiledReadFilter, negated: boolean): void => {
|
|
419
|
+
if (node.kind === "and") {
|
|
420
|
+
visit(node.left, negated);
|
|
421
|
+
visit(node.right, negated);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (node.kind === "not") {
|
|
425
|
+
visit(node.expr, !negated);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (negated || node.kind !== "compare" || node.op !== "eq" || !node.canonicalValue) return;
|
|
429
|
+
out.push({ field: node.field, canonicalValue: node.canonicalValue });
|
|
430
|
+
};
|
|
431
|
+
visit(filter, false);
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function collectPositiveColumnFilterClauses(filter: CompiledReadFilter): ReadFilterColumnClause[] {
|
|
436
|
+
const out: ReadFilterColumnClause[] = [];
|
|
437
|
+
const supportsColumn = (node: CompiledReadFilterClause): boolean =>
|
|
438
|
+
node.index.column === true &&
|
|
439
|
+
(node.index.kind === "integer" || node.index.kind === "float" || node.index.kind === "date" || node.index.kind === "bool");
|
|
440
|
+
const visit = (node: CompiledReadFilter, negated: boolean): void => {
|
|
441
|
+
if (node.kind === "and") {
|
|
442
|
+
visit(node.left, negated);
|
|
443
|
+
visit(node.right, negated);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (node.kind === "not") {
|
|
447
|
+
visit(node.expr, !negated);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (negated) return;
|
|
451
|
+
if (node.kind === "has") {
|
|
452
|
+
if (supportsColumn(node)) out.push({ field: node.field, op: "has" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (node.kind === "compare" && supportsColumn(node)) {
|
|
456
|
+
out.push({
|
|
457
|
+
field: node.field,
|
|
458
|
+
op: node.op!,
|
|
459
|
+
compareValue:
|
|
460
|
+
typeof node.compareValue === "bigint" || typeof node.compareValue === "number" || typeof node.compareValue === "boolean"
|
|
461
|
+
? node.compareValue
|
|
462
|
+
: undefined,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
visit(filter, false);
|
|
467
|
+
return out;
|
|
468
|
+
}
|