@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,389 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import type {
|
|
3
|
+
SchemaRegistry,
|
|
4
|
+
SearchConfig,
|
|
5
|
+
SearchRollupConfig,
|
|
6
|
+
} from "../schema/registry";
|
|
7
|
+
import { parseTimestampMsResult } from "../util/time";
|
|
8
|
+
import { resolvePointerResult } from "../util/json_pointer";
|
|
9
|
+
import {
|
|
10
|
+
canonicalizeColumnValue,
|
|
11
|
+
canonicalizeExactValue,
|
|
12
|
+
extractRawSearchValuesForFieldsResult,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
import {
|
|
15
|
+
collectPositiveSearchExactClauses,
|
|
16
|
+
parseSearchQueryResult,
|
|
17
|
+
type CompiledSearchQuery,
|
|
18
|
+
} from "./query";
|
|
19
|
+
import type { AggMeasureState, AggSummaryState } from "./agg_format";
|
|
20
|
+
|
|
21
|
+
export type AggregateRequest = {
|
|
22
|
+
rollup: string;
|
|
23
|
+
fromMs: bigint;
|
|
24
|
+
toMs: bigint;
|
|
25
|
+
interval: string;
|
|
26
|
+
intervalMs: number;
|
|
27
|
+
q: CompiledSearchQuery | null;
|
|
28
|
+
groupBy: string[];
|
|
29
|
+
measures: string[] | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type RollupEligibility = {
|
|
33
|
+
eligible: boolean;
|
|
34
|
+
exactFilters: Record<string, string>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
38
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTimeValueResult(raw: unknown, path: string): Result<bigint, { message: string }> {
|
|
42
|
+
if (typeof raw === "string") {
|
|
43
|
+
const parsedRes = parseTimestampMsResult(raw);
|
|
44
|
+
if (Result.isError(parsedRes)) return Result.err({ message: `${path} must be a valid timestamp` });
|
|
45
|
+
return Result.ok(parsedRes.value);
|
|
46
|
+
}
|
|
47
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
48
|
+
return Result.ok(BigInt(Math.trunc(raw)));
|
|
49
|
+
}
|
|
50
|
+
return Result.err({ message: `${path} must be a timestamp string or unix milliseconds` });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseIntervalMsResult(search: SearchConfig, rollupName: string, interval: string): Result<number, { message: string }> {
|
|
54
|
+
const rollup = search.rollups?.[rollupName];
|
|
55
|
+
if (!rollup) return Result.err({ message: `unknown rollup ${rollupName}` });
|
|
56
|
+
if (!rollup.intervals.includes(interval)) return Result.err({ message: `interval ${interval} is not configured for rollup ${rollupName}` });
|
|
57
|
+
const parsed = interval.trim();
|
|
58
|
+
const match = /^(\d+)(ms|s|m|h|d)$/.exec(parsed);
|
|
59
|
+
if (!match) return Result.err({ message: `interval ${interval} is not a supported duration` });
|
|
60
|
+
const value = Number(match[1]);
|
|
61
|
+
const unit = match[2];
|
|
62
|
+
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
|
|
63
|
+
return Result.ok(value * multiplier);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveRollupConfigResult(
|
|
67
|
+
registry: SchemaRegistry,
|
|
68
|
+
rollupName: string
|
|
69
|
+
): Result<SearchRollupConfig, { message: string }> {
|
|
70
|
+
const search = registry.search;
|
|
71
|
+
if (!search) return Result.err({ message: "search is not configured for this stream" });
|
|
72
|
+
const rollup = search.rollups?.[rollupName];
|
|
73
|
+
if (!rollup) return Result.err({ message: `unknown rollup ${rollupName}` });
|
|
74
|
+
return Result.ok(rollup);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseAggregateRequestBodyResult(
|
|
78
|
+
registry: SchemaRegistry,
|
|
79
|
+
raw: unknown
|
|
80
|
+
): Result<AggregateRequest, { message: string }> {
|
|
81
|
+
if (!isPlainObject(raw)) return Result.err({ message: "aggregate request must be an object" });
|
|
82
|
+
const search = registry.search;
|
|
83
|
+
if (!search) return Result.err({ message: "search is not configured for this stream" });
|
|
84
|
+
if (typeof raw.rollup !== "string" || raw.rollup.trim() === "") return Result.err({ message: "rollup must be a string" });
|
|
85
|
+
const rollupName = raw.rollup.trim();
|
|
86
|
+
const rollupRes = resolveRollupConfigResult(registry, rollupName);
|
|
87
|
+
if (Result.isError(rollupRes)) return rollupRes;
|
|
88
|
+
const fromRes = parseTimeValueResult(raw.from, "from");
|
|
89
|
+
if (Result.isError(fromRes)) return fromRes;
|
|
90
|
+
const toRes = parseTimeValueResult(raw.to, "to");
|
|
91
|
+
if (Result.isError(toRes)) return toRes;
|
|
92
|
+
if (toRes.value <= fromRes.value) return Result.err({ message: "to must be greater than from" });
|
|
93
|
+
if (typeof raw.interval !== "string" || raw.interval.trim() === "") return Result.err({ message: "interval must be a string" });
|
|
94
|
+
const interval = raw.interval.trim();
|
|
95
|
+
const intervalMsRes = parseIntervalMsResult(search, rollupName, interval);
|
|
96
|
+
if (Result.isError(intervalMsRes)) return intervalMsRes;
|
|
97
|
+
|
|
98
|
+
let q: CompiledSearchQuery | null = null;
|
|
99
|
+
if (raw.q !== undefined && raw.q !== null) {
|
|
100
|
+
if (typeof raw.q !== "string") return Result.err({ message: "q must be a string" });
|
|
101
|
+
const queryRes = parseSearchQueryResult(registry, raw.q);
|
|
102
|
+
if (Result.isError(queryRes)) return queryRes;
|
|
103
|
+
q = queryRes.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configuredDimensions = new Set(rollupRes.value.dimensions ?? []);
|
|
107
|
+
let groupBy: string[] = [];
|
|
108
|
+
if (raw.group_by !== undefined) {
|
|
109
|
+
if (!Array.isArray(raw.group_by)) return Result.err({ message: "group_by must be an array of strings" });
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
for (let i = 0; i < raw.group_by.length; i++) {
|
|
112
|
+
if (typeof raw.group_by[i] !== "string") return Result.err({ message: `group_by[${i}] must be a string` });
|
|
113
|
+
const field = raw.group_by[i].trim();
|
|
114
|
+
if (!configuredDimensions.has(field)) return Result.err({ message: `group_by field ${field} is not configured on rollup ${rollupName}` });
|
|
115
|
+
if (!seen.has(field)) {
|
|
116
|
+
seen.add(field);
|
|
117
|
+
groupBy.push(field);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
groupBy.sort((a, b) => a.localeCompare(b));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let measures: string[] | null = null;
|
|
124
|
+
if (raw.measures !== undefined) {
|
|
125
|
+
if (!Array.isArray(raw.measures) || raw.measures.length === 0) {
|
|
126
|
+
return Result.err({ message: "measures must be a non-empty array of strings" });
|
|
127
|
+
}
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
measures = [];
|
|
130
|
+
for (let i = 0; i < raw.measures.length; i++) {
|
|
131
|
+
if (typeof raw.measures[i] !== "string") return Result.err({ message: `measures[${i}] must be a string` });
|
|
132
|
+
const measure = raw.measures[i].trim();
|
|
133
|
+
if (!Object.prototype.hasOwnProperty.call(rollupRes.value.measures, measure)) {
|
|
134
|
+
return Result.err({ message: `unknown measure ${measure} on rollup ${rollupName}` });
|
|
135
|
+
}
|
|
136
|
+
if (!seen.has(measure)) {
|
|
137
|
+
seen.add(measure);
|
|
138
|
+
measures.push(measure);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
measures.sort((a, b) => a.localeCompare(b));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Result.ok({
|
|
145
|
+
rollup: rollupName,
|
|
146
|
+
fromMs: fromRes.value,
|
|
147
|
+
toMs: toRes.value,
|
|
148
|
+
interval,
|
|
149
|
+
intervalMs: intervalMsRes.value,
|
|
150
|
+
q,
|
|
151
|
+
groupBy,
|
|
152
|
+
measures,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function extractRollupEligibility(query: CompiledSearchQuery | null, dimensions: Set<string>): RollupEligibility {
|
|
157
|
+
if (!query) return { eligible: true, exactFilters: {} };
|
|
158
|
+
const clauses = collectPositiveSearchExactClauses(query);
|
|
159
|
+
const exactFilters: Record<string, string> = {};
|
|
160
|
+
const visit = (node: CompiledSearchQuery): boolean => {
|
|
161
|
+
if (node.kind === "and") return visit(node.left) && visit(node.right);
|
|
162
|
+
if (node.kind === "keyword") {
|
|
163
|
+
if (node.prefix || !dimensions.has(node.field)) return false;
|
|
164
|
+
exactFilters[node.field] = node.canonicalValue;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (node.kind === "compare") {
|
|
168
|
+
if (node.op !== "eq" || !node.canonicalValue || !dimensions.has(node.field)) return false;
|
|
169
|
+
exactFilters[node.field] = node.canonicalValue;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
if (!visit(query)) return { eligible: false, exactFilters: {} };
|
|
175
|
+
for (const clause of clauses) {
|
|
176
|
+
if (!dimensions.has(clause.field)) return { eligible: false, exactFilters: {} };
|
|
177
|
+
exactFilters[clause.field] = clause.canonicalValue;
|
|
178
|
+
}
|
|
179
|
+
return { eligible: true, exactFilters };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toFiniteNumber(value: unknown): number | null {
|
|
183
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
184
|
+
if (typeof value === "bigint") return Number(value);
|
|
185
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
186
|
+
const parsed = Number(value);
|
|
187
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractHistogramObject(value: unknown): Record<string, number> | undefined {
|
|
193
|
+
if (!isPlainObject(value)) return undefined;
|
|
194
|
+
const out: Record<string, number> = {};
|
|
195
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
196
|
+
const num = toFiniteNumber(raw);
|
|
197
|
+
if (num == null) continue;
|
|
198
|
+
out[key] = (out[key] ?? 0) + num;
|
|
199
|
+
}
|
|
200
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function cloneAggMeasureState(state: AggMeasureState): AggMeasureState {
|
|
204
|
+
if (state.kind === "count") return { kind: "count", value: state.value };
|
|
205
|
+
return {
|
|
206
|
+
kind: "summary",
|
|
207
|
+
summary: {
|
|
208
|
+
count: state.summary.count,
|
|
209
|
+
sum: state.summary.sum,
|
|
210
|
+
min: state.summary.min,
|
|
211
|
+
max: state.summary.max,
|
|
212
|
+
histogram: state.summary.histogram ? { ...state.summary.histogram } : undefined,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function mergeHistogram(target: Record<string, number> | undefined, next: Record<string, number> | undefined): Record<string, number> | undefined {
|
|
218
|
+
if (!target && !next) return undefined;
|
|
219
|
+
const out: Record<string, number> = { ...(target ?? {}) };
|
|
220
|
+
for (const [bucket, value] of Object.entries(next ?? {})) {
|
|
221
|
+
out[bucket] = (out[bucket] ?? 0) + value;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function mergeAggMeasureState(target: AggMeasureState, next: AggMeasureState): AggMeasureState {
|
|
227
|
+
if (target.kind === "count" && next.kind === "count") {
|
|
228
|
+
target.value += next.value;
|
|
229
|
+
return target;
|
|
230
|
+
}
|
|
231
|
+
if (target.kind === "summary" && next.kind === "summary") {
|
|
232
|
+
target.summary.count += next.summary.count;
|
|
233
|
+
target.summary.sum += next.summary.sum;
|
|
234
|
+
target.summary.min =
|
|
235
|
+
target.summary.min == null ? next.summary.min : next.summary.min == null ? target.summary.min : Math.min(target.summary.min, next.summary.min);
|
|
236
|
+
target.summary.max =
|
|
237
|
+
target.summary.max == null ? next.summary.max : next.summary.max == null ? target.summary.max : Math.max(target.summary.max, next.summary.max);
|
|
238
|
+
target.summary.histogram = mergeHistogram(target.summary.histogram, next.summary.histogram);
|
|
239
|
+
return target;
|
|
240
|
+
}
|
|
241
|
+
return target;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function computeHistogramPercentile(histogram: Record<string, number> | undefined, percentile: number): number | null {
|
|
245
|
+
if (!histogram) return null;
|
|
246
|
+
const entries = Object.entries(histogram)
|
|
247
|
+
.map(([bucket, count]) => ({ bucket: Number(bucket), count }))
|
|
248
|
+
.filter((entry) => Number.isFinite(entry.bucket) && Number.isFinite(entry.count) && entry.count > 0)
|
|
249
|
+
.sort((a, b) => a.bucket - b.bucket);
|
|
250
|
+
if (entries.length === 0) return null;
|
|
251
|
+
const total = entries.reduce((sum, entry) => sum + entry.count, 0);
|
|
252
|
+
if (total <= 0) return null;
|
|
253
|
+
const threshold = total * percentile;
|
|
254
|
+
let seen = 0;
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
seen += entry.count;
|
|
257
|
+
if (seen >= threshold) return entry.bucket;
|
|
258
|
+
}
|
|
259
|
+
return entries[entries.length - 1]?.bucket ?? null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function formatAggMeasureState(state: AggMeasureState): unknown {
|
|
263
|
+
if (state.kind === "count") {
|
|
264
|
+
return { count: state.value };
|
|
265
|
+
}
|
|
266
|
+
const histogram = state.summary.histogram ? { ...state.summary.histogram } : undefined;
|
|
267
|
+
const avg = state.summary.count > 0 ? state.summary.sum / state.summary.count : null;
|
|
268
|
+
return {
|
|
269
|
+
count: state.summary.count,
|
|
270
|
+
sum: state.summary.sum,
|
|
271
|
+
min: state.summary.min,
|
|
272
|
+
max: state.summary.max,
|
|
273
|
+
avg,
|
|
274
|
+
p50: computeHistogramPercentile(histogram, 0.5),
|
|
275
|
+
p95: computeHistogramPercentile(histogram, 0.95),
|
|
276
|
+
p99: computeHistogramPercentile(histogram, 0.99),
|
|
277
|
+
histogram,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function rollupRequiredFieldNames(registry: SchemaRegistry, rollup: SearchRollupConfig): string[] {
|
|
282
|
+
const fields = new Set<string>();
|
|
283
|
+
const timestampField = rollup.timestampField ?? registry.search?.primaryTimestampField;
|
|
284
|
+
if (timestampField) fields.add(timestampField);
|
|
285
|
+
for (const dimension of rollup.dimensions ?? []) fields.add(dimension);
|
|
286
|
+
for (const measure of Object.values(rollup.measures)) {
|
|
287
|
+
if (measure.kind === "summary") fields.add(measure.field);
|
|
288
|
+
}
|
|
289
|
+
return Array.from(fields);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function extractRollupContributionResult(
|
|
293
|
+
registry: SchemaRegistry,
|
|
294
|
+
rollup: SearchRollupConfig,
|
|
295
|
+
offset: bigint,
|
|
296
|
+
value: unknown,
|
|
297
|
+
precomputedRawValues?: Map<string, unknown[]>
|
|
298
|
+
): Result<{ timestampMs: number; dimensions: Record<string, string | null>; measures: Record<string, AggMeasureState> } | null, { message: string }> {
|
|
299
|
+
if (!isPlainObject(value)) return Result.ok(null);
|
|
300
|
+
const rawValuesRes = precomputedRawValues
|
|
301
|
+
? Result.ok(precomputedRawValues)
|
|
302
|
+
: extractRawSearchValuesForFieldsResult(registry, offset, value, rollupRequiredFieldNames(registry, rollup));
|
|
303
|
+
if (Result.isError(rawValuesRes)) return rawValuesRes;
|
|
304
|
+
const rawValues = rawValuesRes.value;
|
|
305
|
+
|
|
306
|
+
const timestampField = rollup.timestampField ?? registry.search?.primaryTimestampField;
|
|
307
|
+
if (!timestampField) return Result.ok(null);
|
|
308
|
+
const timestampConfig = registry.search?.fields[timestampField];
|
|
309
|
+
if (!timestampConfig) return Result.ok(null);
|
|
310
|
+
const timestampValues = rawValues.get(timestampField) ?? [];
|
|
311
|
+
if (timestampValues.length !== 1) return Result.ok(null);
|
|
312
|
+
const timestampValue = canonicalizeColumnValue(timestampConfig, timestampValues[0]);
|
|
313
|
+
if (typeof timestampValue !== "bigint" && typeof timestampValue !== "number") return Result.ok(null);
|
|
314
|
+
const timestampMs = typeof timestampValue === "bigint" ? Number(timestampValue) : Math.trunc(timestampValue);
|
|
315
|
+
if (!Number.isFinite(timestampMs)) return Result.ok(null);
|
|
316
|
+
|
|
317
|
+
const dimensions: Record<string, string | null> = {};
|
|
318
|
+
for (const dimension of rollup.dimensions ?? []) {
|
|
319
|
+
const config = registry.search?.fields[dimension];
|
|
320
|
+
if (!config) return Result.ok(null);
|
|
321
|
+
const values = rawValues.get(dimension) ?? [];
|
|
322
|
+
if (values.length > 1) return Result.ok(null);
|
|
323
|
+
if (values.length === 0) {
|
|
324
|
+
dimensions[dimension] = null;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const exactCanonical = canonicalizeExactValue(config, values[0]);
|
|
328
|
+
dimensions[dimension] = exactCanonical == null ? null : exactCanonical;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const measures: Record<string, AggMeasureState> = {};
|
|
332
|
+
for (const [measureName, measure] of Object.entries(rollup.measures)) {
|
|
333
|
+
if (measure.kind === "count") {
|
|
334
|
+
measures[measureName] = { kind: "count", value: 1 };
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (measure.kind === "summary") {
|
|
338
|
+
const config = registry.search?.fields[measure.field];
|
|
339
|
+
if (!config) return Result.ok(null);
|
|
340
|
+
const values = rawValues.get(measure.field) ?? [];
|
|
341
|
+
if (values.length !== 1) return Result.ok(null);
|
|
342
|
+
const numeric = toFiniteNumber(canonicalizeColumnValue(config, values[0]));
|
|
343
|
+
if (numeric == null) return Result.ok(null);
|
|
344
|
+
measures[measureName] = {
|
|
345
|
+
kind: "summary",
|
|
346
|
+
summary: {
|
|
347
|
+
count: 1,
|
|
348
|
+
sum: numeric,
|
|
349
|
+
min: numeric,
|
|
350
|
+
max: numeric,
|
|
351
|
+
histogram: measure.histogram === "log2_v1" ? { [String(2 ** Math.floor(Math.log2(Math.max(1, Math.abs(numeric) || 1))))]: 1 } : undefined,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const countResolved = resolvePointerResult(value, measure.countJsonPointer);
|
|
357
|
+
if (Result.isError(countResolved) || !countResolved.value.exists) return Result.ok(null);
|
|
358
|
+
const sumResolved = resolvePointerResult(value, measure.sumJsonPointer);
|
|
359
|
+
if (Result.isError(sumResolved) || !sumResolved.value.exists) return Result.ok(null);
|
|
360
|
+
const minResolved = resolvePointerResult(value, measure.minJsonPointer);
|
|
361
|
+
if (Result.isError(minResolved) || !minResolved.value.exists) return Result.ok(null);
|
|
362
|
+
const maxResolved = resolvePointerResult(value, measure.maxJsonPointer);
|
|
363
|
+
if (Result.isError(maxResolved) || !maxResolved.value.exists) return Result.ok(null);
|
|
364
|
+
const count = toFiniteNumber(countResolved.value.value);
|
|
365
|
+
const sum = toFiniteNumber(sumResolved.value.value);
|
|
366
|
+
const min = toFiniteNumber(minResolved.value.value);
|
|
367
|
+
const max = toFiniteNumber(maxResolved.value.value);
|
|
368
|
+
if (count == null || sum == null || min == null || max == null) return Result.ok(null);
|
|
369
|
+
let histogram: Record<string, number> | undefined;
|
|
370
|
+
if (measure.histogramJsonPointer) {
|
|
371
|
+
const histResolved = resolvePointerResult(value, measure.histogramJsonPointer);
|
|
372
|
+
if (!Result.isError(histResolved) && histResolved.value.exists) {
|
|
373
|
+
histogram = extractHistogramObject(histResolved.value.value);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
measures[measureName] = {
|
|
377
|
+
kind: "summary",
|
|
378
|
+
summary: {
|
|
379
|
+
count,
|
|
380
|
+
sum,
|
|
381
|
+
min,
|
|
382
|
+
max,
|
|
383
|
+
histogram,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return Result.ok({ timestampMs, dimensions, measures });
|
|
389
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export class BinaryPayloadError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "BinaryPayloadError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class BinaryWriter {
|
|
9
|
+
private readonly parts: Uint8Array[] = [];
|
|
10
|
+
private lengthBytes = 0;
|
|
11
|
+
|
|
12
|
+
get length(): number {
|
|
13
|
+
return this.lengthBytes;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
writeU8(value: number): void {
|
|
17
|
+
const out = new Uint8Array(1);
|
|
18
|
+
out[0] = value & 0xff;
|
|
19
|
+
this.push(out);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
writeU16(value: number): void {
|
|
23
|
+
const out = new Uint8Array(2);
|
|
24
|
+
new DataView(out.buffer).setUint16(0, value, true);
|
|
25
|
+
this.push(out);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeU32(value: number): void {
|
|
29
|
+
const out = new Uint8Array(4);
|
|
30
|
+
new DataView(out.buffer).setUint32(0, value, true);
|
|
31
|
+
this.push(out);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
writeU64(value: bigint): void {
|
|
35
|
+
const out = new Uint8Array(8);
|
|
36
|
+
new DataView(out.buffer).setBigUint64(0, BigInt.asUintN(64, value), true);
|
|
37
|
+
this.push(out);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writeI64(value: bigint): void {
|
|
41
|
+
const out = new Uint8Array(8);
|
|
42
|
+
new DataView(out.buffer).setBigInt64(0, BigInt.asIntN(64, value), true);
|
|
43
|
+
this.push(out);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
writeF64(value: number): void {
|
|
47
|
+
const out = new Uint8Array(8);
|
|
48
|
+
new DataView(out.buffer).setFloat64(0, value, true);
|
|
49
|
+
this.push(out);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
writeBytes(value: Uint8Array): void {
|
|
53
|
+
this.push(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
finish(): Uint8Array {
|
|
57
|
+
return concatBytes(this.parts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private push(value: Uint8Array): void {
|
|
61
|
+
this.parts.push(value);
|
|
62
|
+
this.lengthBytes += value.byteLength;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class BinaryCursor {
|
|
67
|
+
private offsetBytes = 0;
|
|
68
|
+
|
|
69
|
+
constructor(private readonly bytes: Uint8Array) {}
|
|
70
|
+
|
|
71
|
+
get offset(): number {
|
|
72
|
+
return this.offsetBytes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
remaining(): number {
|
|
76
|
+
return this.bytes.byteLength - this.offsetBytes;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
readU8(): number {
|
|
80
|
+
this.ensureAvailable(1);
|
|
81
|
+
return this.bytes[this.offsetBytes++]!;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
readU16(): number {
|
|
85
|
+
const value = readU16(this.bytes, this.offsetBytes);
|
|
86
|
+
this.offsetBytes += 2;
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
readU32(): number {
|
|
91
|
+
const value = readU32(this.bytes, this.offsetBytes);
|
|
92
|
+
this.offsetBytes += 4;
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
readU64(): bigint {
|
|
97
|
+
const value = readU64(this.bytes, this.offsetBytes);
|
|
98
|
+
this.offsetBytes += 8;
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
readI64(): bigint {
|
|
103
|
+
const value = readI64(this.bytes, this.offsetBytes);
|
|
104
|
+
this.offsetBytes += 8;
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
readF64(): number {
|
|
109
|
+
const value = readF64(this.bytes, this.offsetBytes);
|
|
110
|
+
this.offsetBytes += 8;
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
readBytes(length: number): Uint8Array {
|
|
115
|
+
this.ensureAvailable(length);
|
|
116
|
+
const out = this.bytes.subarray(this.offsetBytes, this.offsetBytes + length);
|
|
117
|
+
this.offsetBytes += length;
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
skip(length: number): void {
|
|
122
|
+
this.ensureAvailable(length);
|
|
123
|
+
this.offsetBytes += length;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private ensureAvailable(length: number): void {
|
|
127
|
+
if (length < 0 || this.offsetBytes + length > this.bytes.byteLength) {
|
|
128
|
+
throw new BinaryPayloadError("unexpected end of binary payload");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
134
|
+
const total = parts.reduce((sum, part) => sum + part.byteLength, 0);
|
|
135
|
+
const out = new Uint8Array(total);
|
|
136
|
+
let offset = 0;
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
out.set(part, offset);
|
|
139
|
+
offset += part.byteLength;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function readU16(bytes: Uint8Array, offset: number): number {
|
|
145
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 2).getUint16(0, true);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function readU32(bytes: Uint8Array, offset: number): number {
|
|
149
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0, true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function readU64(bytes: Uint8Array, offset: number): bigint {
|
|
153
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 8).getBigUint64(0, true);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function readI64(bytes: Uint8Array, offset: number): bigint {
|
|
157
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 8).getBigInt64(0, true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readF64(bytes: Uint8Array, offset: number): number {
|
|
161
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 8).getFloat64(0, true);
|
|
162
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BinaryCursor, BinaryPayloadError, BinaryWriter } from "./codec";
|
|
2
|
+
import { createBitset, bitsetGet, bitsetSet } from "../bitset";
|
|
3
|
+
import { readUVarint, writeUVarint } from "./varint";
|
|
4
|
+
|
|
5
|
+
export const DOCSET_CODEC_ALL = 0;
|
|
6
|
+
export const DOCSET_CODEC_BITSET = 1;
|
|
7
|
+
export const DOCSET_CODEC_DELTA_DOCIDS = 2;
|
|
8
|
+
|
|
9
|
+
export type EncodedDocSet = {
|
|
10
|
+
codec: number;
|
|
11
|
+
payload: Uint8Array;
|
|
12
|
+
docIds: number[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function encodeDocSet(docCount: number, docIds: number[]): EncodedDocSet {
|
|
16
|
+
const sortedDocIds = [...docIds].sort((a, b) => a - b);
|
|
17
|
+
if (sortedDocIds.length === docCount) {
|
|
18
|
+
return { codec: DOCSET_CODEC_ALL, payload: new Uint8Array(), docIds: sortedDocIds };
|
|
19
|
+
}
|
|
20
|
+
const bitset = createBitset(docCount);
|
|
21
|
+
for (const docId of sortedDocIds) bitsetSet(bitset, docId);
|
|
22
|
+
const deltaWriter = new BinaryWriter();
|
|
23
|
+
let previous = 0;
|
|
24
|
+
for (let i = 0; i < sortedDocIds.length; i++) {
|
|
25
|
+
const docId = sortedDocIds[i]!;
|
|
26
|
+
writeUVarint(deltaWriter, i === 0 ? docId : docId - previous);
|
|
27
|
+
previous = docId;
|
|
28
|
+
}
|
|
29
|
+
const deltaPayload = deltaWriter.finish();
|
|
30
|
+
const bitsetPayload = new Uint8Array(bitset);
|
|
31
|
+
const useDelta = sortedDocIds.length > 0 && deltaPayload.byteLength < bitsetPayload.byteLength;
|
|
32
|
+
return {
|
|
33
|
+
codec: useDelta ? DOCSET_CODEC_DELTA_DOCIDS : DOCSET_CODEC_BITSET,
|
|
34
|
+
payload: useDelta ? deltaPayload : bitsetPayload,
|
|
35
|
+
docIds: sortedDocIds,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function decodeDocIds(docCount: number, codec: number, payload: Uint8Array): number[] {
|
|
40
|
+
if (codec === DOCSET_CODEC_ALL) {
|
|
41
|
+
return Array.from({ length: docCount }, (_, index) => index);
|
|
42
|
+
}
|
|
43
|
+
if (codec === DOCSET_CODEC_BITSET) {
|
|
44
|
+
const out: number[] = [];
|
|
45
|
+
for (let docId = 0; docId < docCount; docId++) {
|
|
46
|
+
if (bitsetGet(payload, docId)) out.push(docId);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
if (codec === DOCSET_CODEC_DELTA_DOCIDS) {
|
|
51
|
+
const cursor = new BinaryCursor(payload);
|
|
52
|
+
const out: number[] = [];
|
|
53
|
+
let previous = 0;
|
|
54
|
+
while (cursor.remaining() > 0) {
|
|
55
|
+
const delta = Number(readUVarint(cursor));
|
|
56
|
+
const next = out.length === 0 ? delta : previous + delta;
|
|
57
|
+
out.push(next);
|
|
58
|
+
previous = next;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
throw new BinaryPayloadError(`unknown docset codec ${codec}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function iterateDocIds(docCount: number, codec: number, payload: Uint8Array): Iterable<number> {
|
|
66
|
+
return decodeDocIds(docCount, codec, payload);
|
|
67
|
+
}
|