@tungthedev/streams-server 0.2.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.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -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 +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -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 +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -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 +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
package/src/db/db.ts
ADDED
|
@@ -0,0 +1,2424 @@
|
|
|
1
|
+
import { initSchema } from "./schema.ts";
|
|
2
|
+
import { openSqliteDatabase, type SqliteDatabase, type SqliteStatement } from "../sqlite/adapter.ts";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
import type { StoreAppendBatch, StoreAppendTask } from "../store/append";
|
|
5
|
+
import type { WalReadRow, WalStore } from "../store/wal_store";
|
|
6
|
+
import type { SegmentReadStore, StreamReadStore } from "../store/segment_read_store";
|
|
7
|
+
import type { ManifestPublicationSnapshot, ManifestStore, SegmentStore } from "../store/segment_manifest_store";
|
|
8
|
+
import type {
|
|
9
|
+
IndexRunRow,
|
|
10
|
+
IndexStateRow,
|
|
11
|
+
LexiconIndexRunRow,
|
|
12
|
+
LexiconIndexStateRow,
|
|
13
|
+
SearchCompanionPlanRow,
|
|
14
|
+
SearchSegmentCompanionRow,
|
|
15
|
+
SecondaryIndexRunRow,
|
|
16
|
+
SecondaryIndexStateRow,
|
|
17
|
+
SegmentMetaRow,
|
|
18
|
+
SegmentRow,
|
|
19
|
+
StreamRow,
|
|
20
|
+
} from "../store/rows";
|
|
21
|
+
import { STREAM_FLAG_DELETED, STREAM_FLAG_TOUCH } from "../store/rows";
|
|
22
|
+
import type {
|
|
23
|
+
ProfileMetadataCommit,
|
|
24
|
+
ProfileMetadataMutationContext,
|
|
25
|
+
ProfileMetadataMutationPlan,
|
|
26
|
+
ProfileStore,
|
|
27
|
+
SchemaMetadataCommit,
|
|
28
|
+
SchemaMetadataMutationContext,
|
|
29
|
+
SchemaMetadataMutationPlan,
|
|
30
|
+
SchemaStore,
|
|
31
|
+
} from "../store/schema_profile_store";
|
|
32
|
+
import type { WalControlPlaneStore, DurableStoreCapabilities } from "../store/capabilities";
|
|
33
|
+
import type {
|
|
34
|
+
ObjectStoreAccountingStore,
|
|
35
|
+
ObjectStoreRequestCountRow,
|
|
36
|
+
ObjectStoreRequestSummary,
|
|
37
|
+
StorageStatsStore,
|
|
38
|
+
} from "../store/stats_accounting_store";
|
|
39
|
+
import { summarizeObjectStoreRequestCounts } from "../store/stats_accounting_store";
|
|
40
|
+
import type {
|
|
41
|
+
FullModeDetailsSnapshot,
|
|
42
|
+
FullModeDetailsSnapshotRequest,
|
|
43
|
+
FullModeDetailsStore,
|
|
44
|
+
FullModeLagSnapshotRequest,
|
|
45
|
+
} from "../store/full_mode_details_store";
|
|
46
|
+
import { SqliteWalStore } from "./sqlite_wal_store";
|
|
47
|
+
import { loadSqliteManifestPublicationSnapshot } from "./sqlite_manifest_snapshot";
|
|
48
|
+
import { SqliteTouchStore } from "./sqlite_touch_store";
|
|
49
|
+
|
|
50
|
+
export { STREAM_FLAG_DELETED, STREAM_FLAG_TOUCH } from "../store/rows";
|
|
51
|
+
|
|
52
|
+
const BASE_WAL_GC_CHUNK_OFFSETS = (() => {
|
|
53
|
+
const raw = process.env.DS_BASE_WAL_GC_CHUNK_OFFSETS;
|
|
54
|
+
if (raw == null || raw.trim() === "") return 1_000_000;
|
|
55
|
+
const n = Number(raw);
|
|
56
|
+
if (!Number.isFinite(n) || n <= 0) return 1_000_000;
|
|
57
|
+
return Math.floor(n);
|
|
58
|
+
})();
|
|
59
|
+
|
|
60
|
+
export type {
|
|
61
|
+
IndexRunRow,
|
|
62
|
+
IndexStateRow,
|
|
63
|
+
LexiconIndexRunRow,
|
|
64
|
+
LexiconIndexStateRow,
|
|
65
|
+
SearchCompanionPlanRow,
|
|
66
|
+
SearchSegmentCompanionRow,
|
|
67
|
+
SecondaryIndexRunRow,
|
|
68
|
+
SecondaryIndexStateRow,
|
|
69
|
+
SegmentMetaRow,
|
|
70
|
+
SegmentRow,
|
|
71
|
+
StreamRow,
|
|
72
|
+
} from "../store/rows";
|
|
73
|
+
|
|
74
|
+
function legacyWalReadRow(row: WalReadRow): {
|
|
75
|
+
offset: bigint;
|
|
76
|
+
ts_ms: bigint;
|
|
77
|
+
routing_key: Uint8Array | null;
|
|
78
|
+
content_type: string | null;
|
|
79
|
+
payload: Uint8Array;
|
|
80
|
+
} {
|
|
81
|
+
return {
|
|
82
|
+
offset: row.offset,
|
|
83
|
+
ts_ms: row.tsMs,
|
|
84
|
+
routing_key: row.routingKey,
|
|
85
|
+
content_type: row.contentType,
|
|
86
|
+
payload: row.payload,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class SqliteDurableStore
|
|
91
|
+
implements
|
|
92
|
+
WalControlPlaneStore,
|
|
93
|
+
WalStore,
|
|
94
|
+
SegmentReadStore,
|
|
95
|
+
StreamReadStore,
|
|
96
|
+
SegmentStore,
|
|
97
|
+
ManifestStore,
|
|
98
|
+
SchemaStore,
|
|
99
|
+
ProfileStore,
|
|
100
|
+
StorageStatsStore,
|
|
101
|
+
ObjectStoreAccountingStore,
|
|
102
|
+
FullModeDetailsStore
|
|
103
|
+
{
|
|
104
|
+
readonly kind = "sqlite" as const;
|
|
105
|
+
readonly capabilities: DurableStoreCapabilities = {
|
|
106
|
+
wal: true,
|
|
107
|
+
schemas: true,
|
|
108
|
+
profiles: true,
|
|
109
|
+
streamLifecycle: true,
|
|
110
|
+
segmentReads: true,
|
|
111
|
+
indexes: true,
|
|
112
|
+
manifests: true,
|
|
113
|
+
objectStoreAccounting: true,
|
|
114
|
+
storageStats: true,
|
|
115
|
+
schemaPublication: true,
|
|
116
|
+
builtinProfiles: true,
|
|
117
|
+
internalMetrics: true,
|
|
118
|
+
touch: true,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
public readonly db: SqliteDatabase;
|
|
122
|
+
public readonly touch: SqliteTouchStore;
|
|
123
|
+
private readonly walStore: SqliteWalStore;
|
|
124
|
+
private dbstatReady: boolean | null = null;
|
|
125
|
+
|
|
126
|
+
// Prepared statements.
|
|
127
|
+
private readonly stmts: {
|
|
128
|
+
getStream: SqliteStatement;
|
|
129
|
+
upsertStream: SqliteStatement;
|
|
130
|
+
listStreams: SqliteStatement;
|
|
131
|
+
listDeletedStreams: SqliteStatement;
|
|
132
|
+
setDeleted: SqliteStatement;
|
|
133
|
+
setStreamProfile: SqliteStatement;
|
|
134
|
+
|
|
135
|
+
insertWal: SqliteStatement;
|
|
136
|
+
|
|
137
|
+
updateStreamAppend: SqliteStatement;
|
|
138
|
+
updateStreamAppendSeqCheck: SqliteStatement;
|
|
139
|
+
|
|
140
|
+
candidateStreams: SqliteStatement;
|
|
141
|
+
candidateStreamsNoInterval: SqliteStatement;
|
|
142
|
+
listExpiredStreams: SqliteStatement;
|
|
143
|
+
|
|
144
|
+
createSegment: SqliteStatement;
|
|
145
|
+
listSegmentsForStream: SqliteStatement;
|
|
146
|
+
getSegmentByIndex: SqliteStatement;
|
|
147
|
+
findSegmentForOffset: SqliteStatement;
|
|
148
|
+
nextSegmentIndex: SqliteStatement;
|
|
149
|
+
markSegmentUploaded: SqliteStatement;
|
|
150
|
+
pendingUploadHeads: SqliteStatement;
|
|
151
|
+
recentSegmentCompressionWindow: SqliteStatement;
|
|
152
|
+
countPendingSegments: SqliteStatement;
|
|
153
|
+
tryClaimSegment: SqliteStatement;
|
|
154
|
+
countSegmentsForStream: SqliteStatement;
|
|
155
|
+
|
|
156
|
+
getManifest: SqliteStatement;
|
|
157
|
+
upsertManifest: SqliteStatement;
|
|
158
|
+
setSchemaUploadedSize: SqliteStatement;
|
|
159
|
+
recordObjectStoreRequest: SqliteStatement;
|
|
160
|
+
|
|
161
|
+
getIndexState: SqliteStatement;
|
|
162
|
+
upsertIndexState: SqliteStatement;
|
|
163
|
+
updateIndexedThrough: SqliteStatement;
|
|
164
|
+
listIndexRuns: SqliteStatement;
|
|
165
|
+
listIndexRunsAll: SqliteStatement;
|
|
166
|
+
listRetiredIndexRuns: SqliteStatement;
|
|
167
|
+
insertIndexRun: SqliteStatement;
|
|
168
|
+
retireIndexRun: SqliteStatement;
|
|
169
|
+
deleteIndexRun: SqliteStatement;
|
|
170
|
+
deleteIndexStateForStream: SqliteStatement;
|
|
171
|
+
deleteIndexRunsForStream: SqliteStatement;
|
|
172
|
+
getSecondaryIndexState: SqliteStatement;
|
|
173
|
+
listSecondaryIndexStates: SqliteStatement;
|
|
174
|
+
upsertSecondaryIndexState: SqliteStatement;
|
|
175
|
+
updateSecondaryIndexedThrough: SqliteStatement;
|
|
176
|
+
listSecondaryIndexRuns: SqliteStatement;
|
|
177
|
+
listSecondaryIndexRunsAll: SqliteStatement;
|
|
178
|
+
listRetiredSecondaryIndexRuns: SqliteStatement;
|
|
179
|
+
insertSecondaryIndexRun: SqliteStatement;
|
|
180
|
+
retireSecondaryIndexRun: SqliteStatement;
|
|
181
|
+
deleteSecondaryIndexRun: SqliteStatement;
|
|
182
|
+
deleteSecondaryIndexState: SqliteStatement;
|
|
183
|
+
deleteSecondaryIndexRunsForIndex: SqliteStatement;
|
|
184
|
+
deleteSecondaryIndexStatesForStream: SqliteStatement;
|
|
185
|
+
deleteSecondaryIndexRunsForStream: SqliteStatement;
|
|
186
|
+
getLexiconIndexState: SqliteStatement;
|
|
187
|
+
listLexiconIndexStates: SqliteStatement;
|
|
188
|
+
upsertLexiconIndexState: SqliteStatement;
|
|
189
|
+
updateLexiconIndexedThrough: SqliteStatement;
|
|
190
|
+
listLexiconIndexRuns: SqliteStatement;
|
|
191
|
+
listLexiconIndexRunsAll: SqliteStatement;
|
|
192
|
+
listRetiredLexiconIndexRuns: SqliteStatement;
|
|
193
|
+
insertLexiconIndexRun: SqliteStatement;
|
|
194
|
+
retireLexiconIndexRun: SqliteStatement;
|
|
195
|
+
deleteLexiconIndexRun: SqliteStatement;
|
|
196
|
+
deleteLexiconIndexState: SqliteStatement;
|
|
197
|
+
deleteLexiconIndexRunsForSource: SqliteStatement;
|
|
198
|
+
deleteLexiconIndexStatesForStream: SqliteStatement;
|
|
199
|
+
deleteLexiconIndexRunsForStream: SqliteStatement;
|
|
200
|
+
getSearchCompanionPlan: SqliteStatement;
|
|
201
|
+
listSearchCompanionPlanStreams: SqliteStatement;
|
|
202
|
+
upsertSearchCompanionPlan: SqliteStatement;
|
|
203
|
+
deleteSearchCompanionPlan: SqliteStatement;
|
|
204
|
+
listSearchSegmentCompanions: SqliteStatement;
|
|
205
|
+
getSearchSegmentCompanion: SqliteStatement;
|
|
206
|
+
upsertSearchSegmentCompanion: SqliteStatement;
|
|
207
|
+
deleteSearchSegmentCompanionsFromGeneration: SqliteStatement;
|
|
208
|
+
deleteSearchSegmentCompanionsFromIndex: SqliteStatement;
|
|
209
|
+
deleteSearchSegmentCompanions: SqliteStatement;
|
|
210
|
+
countUploadedSegments: SqliteStatement;
|
|
211
|
+
getSegmentMeta: SqliteStatement;
|
|
212
|
+
ensureSegmentMeta: SqliteStatement;
|
|
213
|
+
appendSegmentMeta: SqliteStatement;
|
|
214
|
+
upsertSegmentMeta: SqliteStatement;
|
|
215
|
+
setUploadedSegmentCount: SqliteStatement;
|
|
216
|
+
|
|
217
|
+
advanceUploadedThrough: SqliteStatement;
|
|
218
|
+
|
|
219
|
+
getSchemaRegistry: SqliteStatement;
|
|
220
|
+
upsertSchemaRegistry: SqliteStatement;
|
|
221
|
+
getStreamProfile: SqliteStatement;
|
|
222
|
+
upsertStreamProfile: SqliteStatement;
|
|
223
|
+
deleteStreamProfile: SqliteStatement;
|
|
224
|
+
countStreams: SqliteStatement;
|
|
225
|
+
sumPendingBytes: SqliteStatement;
|
|
226
|
+
sumPendingSegmentBytes: SqliteStatement;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
constructor(path: string, opts: { cacheBytes?: number; skipMigrations?: boolean } = {}) {
|
|
230
|
+
this.db = openSqliteDatabase(path);
|
|
231
|
+
initSchema(this.db, { skipMigrations: opts.skipMigrations });
|
|
232
|
+
if (opts.cacheBytes && opts.cacheBytes > 0) {
|
|
233
|
+
const kb = Math.max(1, Math.floor(opts.cacheBytes / 1024));
|
|
234
|
+
this.db.exec(`PRAGMA cache_size = -${kb};`);
|
|
235
|
+
}
|
|
236
|
+
this.walStore = new SqliteWalStore(this.db, () => this.nowMs(), STREAM_FLAG_DELETED);
|
|
237
|
+
this.touch = new SqliteTouchStore(this.db, {
|
|
238
|
+
nowMs: () => this.nowMs(),
|
|
239
|
+
getStream: (stream) => this.getStream(stream),
|
|
240
|
+
ensureStream: (stream, ensureOpts) => this.ensureStream(stream, ensureOpts),
|
|
241
|
+
addStreamFlags: (stream, flags) => this.addStreamFlags(stream, flags),
|
|
242
|
+
isDeleted: (row) => this.isDeleted(row),
|
|
243
|
+
readWalRange: (stream, startOffset, endOffset, routingKey) => this.readWalRange(stream, startOffset, endOffset, routingKey),
|
|
244
|
+
deleteWalThrough: (stream, uploadedThrough) => this.deleteWalThrough(stream, uploadedThrough),
|
|
245
|
+
getWalOldestOffset: (stream) => this.getWalOldestOffset(stream),
|
|
246
|
+
trimWalByAge: (stream, maxAgeMs) => this.trimWalByAge(stream, maxAgeMs),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this.stmts = {
|
|
250
|
+
getStream: this.db.query(
|
|
251
|
+
`SELECT stream, created_at_ms, updated_at_ms,
|
|
252
|
+
content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
|
|
253
|
+
epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
|
|
254
|
+
pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
|
|
255
|
+
expires_at_ms, stream_flags
|
|
256
|
+
FROM streams WHERE stream = ? LIMIT 1;`
|
|
257
|
+
),
|
|
258
|
+
upsertStream: this.db.query(
|
|
259
|
+
`INSERT INTO streams(stream, created_at_ms, updated_at_ms,
|
|
260
|
+
content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
|
|
261
|
+
epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
|
|
262
|
+
pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
|
|
263
|
+
expires_at_ms, stream_flags)
|
|
264
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
265
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
266
|
+
updated_at_ms=excluded.updated_at_ms,
|
|
267
|
+
expires_at_ms=excluded.expires_at_ms,
|
|
268
|
+
ttl_seconds=excluded.ttl_seconds,
|
|
269
|
+
content_type=excluded.content_type,
|
|
270
|
+
profile=excluded.profile,
|
|
271
|
+
stream_flags=excluded.stream_flags;`
|
|
272
|
+
),
|
|
273
|
+
listStreams: this.db.query(
|
|
274
|
+
`SELECT stream, created_at_ms, updated_at_ms,
|
|
275
|
+
content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
|
|
276
|
+
epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
|
|
277
|
+
pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
|
|
278
|
+
expires_at_ms, stream_flags
|
|
279
|
+
FROM streams
|
|
280
|
+
WHERE (stream_flags & ?) = 0
|
|
281
|
+
AND (expires_at_ms IS NULL OR expires_at_ms > ?)
|
|
282
|
+
ORDER BY stream
|
|
283
|
+
LIMIT ? OFFSET ?;`
|
|
284
|
+
),
|
|
285
|
+
listDeletedStreams: this.db.query(
|
|
286
|
+
`SELECT stream
|
|
287
|
+
FROM streams
|
|
288
|
+
WHERE (stream_flags & ?) != 0
|
|
289
|
+
ORDER BY stream
|
|
290
|
+
LIMIT ? OFFSET ?;`
|
|
291
|
+
),
|
|
292
|
+
setDeleted: this.db.query(`UPDATE streams SET stream_flags = (stream_flags | ?), updated_at_ms=? WHERE stream=?;`),
|
|
293
|
+
setStreamProfile: this.db.query(`UPDATE streams SET profile=?, updated_at_ms=? WHERE stream=?;`),
|
|
294
|
+
|
|
295
|
+
insertWal: this.db.query(
|
|
296
|
+
`INSERT INTO wal(stream, offset, ts_ms, payload, payload_len, routing_key, content_type, flags)
|
|
297
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`
|
|
298
|
+
),
|
|
299
|
+
|
|
300
|
+
updateStreamAppend: this.db.query(
|
|
301
|
+
`UPDATE streams
|
|
302
|
+
SET next_offset = ?, updated_at_ms = ?, last_append_ms = ?,
|
|
303
|
+
pending_rows = pending_rows + ?, pending_bytes = pending_bytes + ?,
|
|
304
|
+
logical_size_bytes = logical_size_bytes + ?,
|
|
305
|
+
wal_rows = wal_rows + ?, wal_bytes = wal_bytes + ?
|
|
306
|
+
WHERE stream = ? AND (stream_flags & ?) = 0;`
|
|
307
|
+
),
|
|
308
|
+
updateStreamAppendSeqCheck: this.db.query(
|
|
309
|
+
`UPDATE streams
|
|
310
|
+
SET next_offset = ?, updated_at_ms = ?, last_append_ms = ?,
|
|
311
|
+
pending_rows = pending_rows + ?, pending_bytes = pending_bytes + ?,
|
|
312
|
+
logical_size_bytes = logical_size_bytes + ?,
|
|
313
|
+
wal_rows = wal_rows + ?, wal_bytes = wal_bytes + ?
|
|
314
|
+
WHERE stream = ? AND (stream_flags & ?) = 0 AND next_offset = ?;`
|
|
315
|
+
),
|
|
316
|
+
|
|
317
|
+
candidateStreams: this.db.query(
|
|
318
|
+
`SELECT stream, pending_bytes, pending_rows, last_segment_cut_ms, sealed_through, next_offset, epoch
|
|
319
|
+
FROM streams
|
|
320
|
+
WHERE (stream_flags & ?) = 0
|
|
321
|
+
AND segment_in_progress = 0
|
|
322
|
+
AND (pending_bytes >= ? OR pending_rows >= ? OR (? - last_segment_cut_ms) >= ?)
|
|
323
|
+
ORDER BY pending_bytes DESC
|
|
324
|
+
LIMIT ?;`
|
|
325
|
+
),
|
|
326
|
+
candidateStreamsNoInterval: this.db.query(
|
|
327
|
+
`SELECT stream, pending_bytes, pending_rows, last_segment_cut_ms, sealed_through, next_offset, epoch
|
|
328
|
+
FROM streams
|
|
329
|
+
WHERE (stream_flags & ?) = 0
|
|
330
|
+
AND segment_in_progress = 0
|
|
331
|
+
AND (pending_bytes >= ? OR pending_rows >= ?)
|
|
332
|
+
ORDER BY pending_bytes DESC
|
|
333
|
+
LIMIT ?;`
|
|
334
|
+
),
|
|
335
|
+
listExpiredStreams: this.db.query(
|
|
336
|
+
`SELECT stream
|
|
337
|
+
FROM streams
|
|
338
|
+
WHERE (stream_flags & ?) = 0
|
|
339
|
+
AND expires_at_ms IS NOT NULL
|
|
340
|
+
AND expires_at_ms <= ?
|
|
341
|
+
ORDER BY expires_at_ms ASC
|
|
342
|
+
LIMIT ?;`
|
|
343
|
+
),
|
|
344
|
+
|
|
345
|
+
createSegment: this.db.query(
|
|
346
|
+
`INSERT INTO segments(segment_id, stream, segment_index, start_offset, end_offset, block_count,
|
|
347
|
+
last_append_ms, payload_bytes, size_bytes, local_path, created_at_ms, uploaded_at_ms, r2_etag)
|
|
348
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
|
|
349
|
+
),
|
|
350
|
+
listSegmentsForStream: this.db.query(
|
|
351
|
+
`SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
|
|
352
|
+
local_path, created_at_ms, uploaded_at_ms, r2_etag
|
|
353
|
+
FROM segments WHERE stream=? ORDER BY segment_index ASC;`
|
|
354
|
+
),
|
|
355
|
+
getSegmentByIndex: this.db.query(
|
|
356
|
+
`SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
|
|
357
|
+
local_path, created_at_ms, uploaded_at_ms, r2_etag
|
|
358
|
+
FROM segments WHERE stream=? AND segment_index=? LIMIT 1;`
|
|
359
|
+
),
|
|
360
|
+
findSegmentForOffset: this.db.query(
|
|
361
|
+
`SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
|
|
362
|
+
local_path, created_at_ms, uploaded_at_ms, r2_etag
|
|
363
|
+
FROM segments
|
|
364
|
+
WHERE stream=? AND start_offset <= ? AND end_offset >= ?
|
|
365
|
+
ORDER BY segment_index DESC
|
|
366
|
+
LIMIT 1;`
|
|
367
|
+
),
|
|
368
|
+
nextSegmentIndex: this.db.query(
|
|
369
|
+
`SELECT COALESCE(MAX(segment_index)+1, 0) as next_idx FROM segments WHERE stream=?;`
|
|
370
|
+
),
|
|
371
|
+
markSegmentUploaded: this.db.query(
|
|
372
|
+
`UPDATE segments SET r2_etag=?, uploaded_at_ms=? WHERE segment_id=?;`
|
|
373
|
+
),
|
|
374
|
+
pendingUploadHeads: this.db.query(
|
|
375
|
+
`SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
|
|
376
|
+
local_path, created_at_ms, uploaded_at_ms, r2_etag
|
|
377
|
+
FROM segments s
|
|
378
|
+
WHERE s.uploaded_at_ms IS NULL
|
|
379
|
+
AND s.segment_index = (
|
|
380
|
+
SELECT MIN(s2.segment_index)
|
|
381
|
+
FROM segments s2
|
|
382
|
+
WHERE s2.stream = s.stream AND s2.uploaded_at_ms IS NULL
|
|
383
|
+
)
|
|
384
|
+
ORDER BY s.created_at_ms ASC, s.stream ASC
|
|
385
|
+
LIMIT ?;`
|
|
386
|
+
),
|
|
387
|
+
recentSegmentCompressionWindow: this.db.query(
|
|
388
|
+
`SELECT
|
|
389
|
+
COALESCE(SUM(payload_bytes), 0) AS payload_total,
|
|
390
|
+
COALESCE(SUM(size_bytes), 0) AS size_total,
|
|
391
|
+
COUNT(*) AS cnt
|
|
392
|
+
FROM (
|
|
393
|
+
SELECT payload_bytes, size_bytes
|
|
394
|
+
FROM segments
|
|
395
|
+
WHERE stream=? AND payload_bytes > 0
|
|
396
|
+
ORDER BY segment_index DESC
|
|
397
|
+
LIMIT ?
|
|
398
|
+
);`
|
|
399
|
+
),
|
|
400
|
+
countPendingSegments: this.db.query(`SELECT COUNT(*) as cnt FROM segments WHERE uploaded_at_ms IS NULL;`),
|
|
401
|
+
countSegmentsForStream: this.db.query(`SELECT COUNT(*) as cnt FROM segments WHERE stream=?;`),
|
|
402
|
+
tryClaimSegment: this.db.query(
|
|
403
|
+
`UPDATE streams SET segment_in_progress=1, updated_at_ms=? WHERE stream=? AND segment_in_progress=0;`
|
|
404
|
+
),
|
|
405
|
+
|
|
406
|
+
getManifest: this.db.query(
|
|
407
|
+
`SELECT stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes
|
|
408
|
+
FROM manifests WHERE stream=? LIMIT 1;`
|
|
409
|
+
),
|
|
410
|
+
upsertManifest: this.db.query(
|
|
411
|
+
`INSERT INTO manifests(stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes)
|
|
412
|
+
VALUES(?, ?, ?, ?, ?, ?)
|
|
413
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
414
|
+
generation=excluded.generation,
|
|
415
|
+
uploaded_generation=excluded.uploaded_generation,
|
|
416
|
+
last_uploaded_at_ms=excluded.last_uploaded_at_ms,
|
|
417
|
+
last_uploaded_etag=excluded.last_uploaded_etag,
|
|
418
|
+
last_uploaded_size_bytes=excluded.last_uploaded_size_bytes;`
|
|
419
|
+
),
|
|
420
|
+
|
|
421
|
+
getIndexState: this.db.query(
|
|
422
|
+
`SELECT stream, index_secret, indexed_through, updated_at_ms
|
|
423
|
+
FROM index_state WHERE stream=? LIMIT 1;`
|
|
424
|
+
),
|
|
425
|
+
upsertIndexState: this.db.query(
|
|
426
|
+
`INSERT INTO index_state(stream, index_secret, indexed_through, updated_at_ms)
|
|
427
|
+
VALUES(?, ?, ?, ?)
|
|
428
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
429
|
+
index_secret=excluded.index_secret,
|
|
430
|
+
indexed_through=excluded.indexed_through,
|
|
431
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
432
|
+
),
|
|
433
|
+
updateIndexedThrough: this.db.query(
|
|
434
|
+
`UPDATE index_state SET indexed_through=?, updated_at_ms=? WHERE stream=?;`
|
|
435
|
+
),
|
|
436
|
+
listIndexRuns: this.db.query(
|
|
437
|
+
`SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
438
|
+
FROM index_runs WHERE stream=? AND retired_gen IS NULL
|
|
439
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
440
|
+
),
|
|
441
|
+
listIndexRunsAll: this.db.query(
|
|
442
|
+
`SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
443
|
+
FROM index_runs WHERE stream=?
|
|
444
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
445
|
+
),
|
|
446
|
+
listRetiredIndexRuns: this.db.query(
|
|
447
|
+
`SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
448
|
+
FROM index_runs WHERE stream=? AND retired_gen IS NOT NULL
|
|
449
|
+
ORDER BY retired_at_ms ASC;`
|
|
450
|
+
),
|
|
451
|
+
insertIndexRun: this.db.query(
|
|
452
|
+
`INSERT OR IGNORE INTO index_runs(run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms)
|
|
453
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
|
|
454
|
+
),
|
|
455
|
+
retireIndexRun: this.db.query(
|
|
456
|
+
`UPDATE index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
|
|
457
|
+
),
|
|
458
|
+
deleteIndexRun: this.db.query(
|
|
459
|
+
`DELETE FROM index_runs WHERE run_id=?;`
|
|
460
|
+
),
|
|
461
|
+
deleteIndexStateForStream: this.db.query(`DELETE FROM index_state WHERE stream=?;`),
|
|
462
|
+
deleteIndexRunsForStream: this.db.query(`DELETE FROM index_runs WHERE stream=?;`),
|
|
463
|
+
getSecondaryIndexState: this.db.query(
|
|
464
|
+
`SELECT stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms
|
|
465
|
+
FROM secondary_index_state WHERE stream=? AND index_name=? LIMIT 1;`
|
|
466
|
+
),
|
|
467
|
+
listSecondaryIndexStates: this.db.query(
|
|
468
|
+
`SELECT stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms
|
|
469
|
+
FROM secondary_index_state WHERE stream=?
|
|
470
|
+
ORDER BY index_name ASC;`
|
|
471
|
+
),
|
|
472
|
+
upsertSecondaryIndexState: this.db.query(
|
|
473
|
+
`INSERT INTO secondary_index_state(stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms)
|
|
474
|
+
VALUES(?, ?, ?, ?, ?, ?)
|
|
475
|
+
ON CONFLICT(stream, index_name) DO UPDATE SET
|
|
476
|
+
index_secret=excluded.index_secret,
|
|
477
|
+
config_hash=excluded.config_hash,
|
|
478
|
+
indexed_through=excluded.indexed_through,
|
|
479
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
480
|
+
),
|
|
481
|
+
updateSecondaryIndexedThrough: this.db.query(
|
|
482
|
+
`UPDATE secondary_index_state
|
|
483
|
+
SET indexed_through=?, updated_at_ms=?
|
|
484
|
+
WHERE stream=? AND index_name=?;`
|
|
485
|
+
),
|
|
486
|
+
listSecondaryIndexRuns: this.db.query(
|
|
487
|
+
`SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
488
|
+
FROM secondary_index_runs
|
|
489
|
+
WHERE stream=? AND index_name=? AND retired_gen IS NULL
|
|
490
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
491
|
+
),
|
|
492
|
+
listSecondaryIndexRunsAll: this.db.query(
|
|
493
|
+
`SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
494
|
+
FROM secondary_index_runs
|
|
495
|
+
WHERE stream=? AND index_name=?
|
|
496
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
497
|
+
),
|
|
498
|
+
listRetiredSecondaryIndexRuns: this.db.query(
|
|
499
|
+
`SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
|
|
500
|
+
FROM secondary_index_runs
|
|
501
|
+
WHERE stream=? AND index_name=? AND retired_gen IS NOT NULL
|
|
502
|
+
ORDER BY retired_at_ms ASC;`
|
|
503
|
+
),
|
|
504
|
+
insertSecondaryIndexRun: this.db.query(
|
|
505
|
+
`INSERT OR IGNORE INTO secondary_index_runs(run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms)
|
|
506
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
|
|
507
|
+
),
|
|
508
|
+
retireSecondaryIndexRun: this.db.query(
|
|
509
|
+
`UPDATE secondary_index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
|
|
510
|
+
),
|
|
511
|
+
deleteSecondaryIndexRun: this.db.query(
|
|
512
|
+
`DELETE FROM secondary_index_runs WHERE run_id=?;`
|
|
513
|
+
),
|
|
514
|
+
deleteSecondaryIndexState: this.db.query(`DELETE FROM secondary_index_state WHERE stream=? AND index_name=?;`),
|
|
515
|
+
deleteSecondaryIndexRunsForIndex: this.db.query(`DELETE FROM secondary_index_runs WHERE stream=? AND index_name=?;`),
|
|
516
|
+
deleteSecondaryIndexStatesForStream: this.db.query(`DELETE FROM secondary_index_state WHERE stream=?;`),
|
|
517
|
+
deleteSecondaryIndexRunsForStream: this.db.query(`DELETE FROM secondary_index_runs WHERE stream=?;`),
|
|
518
|
+
getLexiconIndexState: this.db.query(
|
|
519
|
+
`SELECT stream, source_kind, source_name, indexed_through, updated_at_ms
|
|
520
|
+
FROM lexicon_index_state
|
|
521
|
+
WHERE stream=? AND source_kind=? AND source_name=?
|
|
522
|
+
LIMIT 1;`
|
|
523
|
+
),
|
|
524
|
+
listLexiconIndexStates: this.db.query(
|
|
525
|
+
`SELECT stream, source_kind, source_name, indexed_through, updated_at_ms
|
|
526
|
+
FROM lexicon_index_state
|
|
527
|
+
WHERE stream=?
|
|
528
|
+
ORDER BY source_kind ASC, source_name ASC;`
|
|
529
|
+
),
|
|
530
|
+
upsertLexiconIndexState: this.db.query(
|
|
531
|
+
`INSERT INTO lexicon_index_state(stream, source_kind, source_name, indexed_through, updated_at_ms)
|
|
532
|
+
VALUES(?, ?, ?, ?, ?)
|
|
533
|
+
ON CONFLICT(stream, source_kind, source_name) DO UPDATE SET
|
|
534
|
+
indexed_through=excluded.indexed_through,
|
|
535
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
536
|
+
),
|
|
537
|
+
updateLexiconIndexedThrough: this.db.query(
|
|
538
|
+
`UPDATE lexicon_index_state
|
|
539
|
+
SET indexed_through=?, updated_at_ms=?
|
|
540
|
+
WHERE stream=? AND source_kind=? AND source_name=?;`
|
|
541
|
+
),
|
|
542
|
+
listLexiconIndexRuns: this.db.query(
|
|
543
|
+
`SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
|
|
544
|
+
FROM lexicon_index_runs
|
|
545
|
+
WHERE stream=? AND source_kind=? AND source_name=? AND retired_gen IS NULL
|
|
546
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
547
|
+
),
|
|
548
|
+
listLexiconIndexRunsAll: this.db.query(
|
|
549
|
+
`SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
|
|
550
|
+
FROM lexicon_index_runs
|
|
551
|
+
WHERE stream=? AND source_kind=? AND source_name=?
|
|
552
|
+
ORDER BY start_segment ASC, level ASC;`
|
|
553
|
+
),
|
|
554
|
+
listRetiredLexiconIndexRuns: this.db.query(
|
|
555
|
+
`SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
|
|
556
|
+
FROM lexicon_index_runs
|
|
557
|
+
WHERE stream=? AND source_kind=? AND source_name=? AND retired_gen IS NOT NULL
|
|
558
|
+
ORDER BY retired_at_ms ASC;`
|
|
559
|
+
),
|
|
560
|
+
insertLexiconIndexRun: this.db.query(
|
|
561
|
+
`INSERT OR IGNORE INTO lexicon_index_runs(run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms)
|
|
562
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
|
|
563
|
+
),
|
|
564
|
+
retireLexiconIndexRun: this.db.query(
|
|
565
|
+
`UPDATE lexicon_index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
|
|
566
|
+
),
|
|
567
|
+
deleteLexiconIndexRun: this.db.query(
|
|
568
|
+
`DELETE FROM lexicon_index_runs WHERE run_id=?;`
|
|
569
|
+
),
|
|
570
|
+
deleteLexiconIndexState: this.db.query(
|
|
571
|
+
`DELETE FROM lexicon_index_state WHERE stream=? AND source_kind=? AND source_name=?;`
|
|
572
|
+
),
|
|
573
|
+
deleteLexiconIndexRunsForSource: this.db.query(
|
|
574
|
+
`DELETE FROM lexicon_index_runs WHERE stream=? AND source_kind=? AND source_name=?;`
|
|
575
|
+
),
|
|
576
|
+
deleteLexiconIndexStatesForStream: this.db.query(`DELETE FROM lexicon_index_state WHERE stream=?;`),
|
|
577
|
+
deleteLexiconIndexRunsForStream: this.db.query(`DELETE FROM lexicon_index_runs WHERE stream=?;`),
|
|
578
|
+
getSearchCompanionPlan: this.db.query(
|
|
579
|
+
`SELECT stream, generation, plan_hash, plan_json, updated_at_ms
|
|
580
|
+
FROM search_companion_plans WHERE stream=? LIMIT 1;`
|
|
581
|
+
),
|
|
582
|
+
listSearchCompanionPlanStreams: this.db.query(
|
|
583
|
+
`SELECT stream FROM search_companion_plans ORDER BY stream ASC;`
|
|
584
|
+
),
|
|
585
|
+
upsertSearchCompanionPlan: this.db.query(
|
|
586
|
+
`INSERT INTO search_companion_plans(stream, generation, plan_hash, plan_json, updated_at_ms)
|
|
587
|
+
VALUES(?, ?, ?, ?, ?)
|
|
588
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
589
|
+
generation=excluded.generation,
|
|
590
|
+
plan_hash=excluded.plan_hash,
|
|
591
|
+
plan_json=excluded.plan_json,
|
|
592
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
593
|
+
),
|
|
594
|
+
deleteSearchCompanionPlan: this.db.query(`DELETE FROM search_companion_plans WHERE stream=?;`),
|
|
595
|
+
listSearchSegmentCompanions: this.db.query(
|
|
596
|
+
`SELECT stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
|
|
597
|
+
primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms
|
|
598
|
+
FROM search_segment_companions
|
|
599
|
+
WHERE stream=?
|
|
600
|
+
ORDER BY segment_index ASC;`
|
|
601
|
+
),
|
|
602
|
+
getSearchSegmentCompanion: this.db.query(
|
|
603
|
+
`SELECT stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
|
|
604
|
+
primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms
|
|
605
|
+
FROM search_segment_companions
|
|
606
|
+
WHERE stream=? AND segment_index=? LIMIT 1;`
|
|
607
|
+
),
|
|
608
|
+
upsertSearchSegmentCompanion: this.db.query(
|
|
609
|
+
`INSERT INTO search_segment_companions(stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
|
|
610
|
+
primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms)
|
|
611
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
612
|
+
ON CONFLICT(stream, segment_index) DO UPDATE SET
|
|
613
|
+
object_key=excluded.object_key,
|
|
614
|
+
plan_generation=excluded.plan_generation,
|
|
615
|
+
sections_json=excluded.sections_json,
|
|
616
|
+
section_sizes_json=excluded.section_sizes_json,
|
|
617
|
+
size_bytes=excluded.size_bytes,
|
|
618
|
+
primary_timestamp_min_ms=excluded.primary_timestamp_min_ms,
|
|
619
|
+
primary_timestamp_max_ms=excluded.primary_timestamp_max_ms,
|
|
620
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
621
|
+
),
|
|
622
|
+
deleteSearchSegmentCompanionsFromGeneration: this.db.query(
|
|
623
|
+
`DELETE FROM search_segment_companions WHERE stream=? AND plan_generation < ?;`
|
|
624
|
+
),
|
|
625
|
+
deleteSearchSegmentCompanionsFromIndex: this.db.query(
|
|
626
|
+
`DELETE FROM search_segment_companions WHERE stream=? AND segment_index >= ?;`
|
|
627
|
+
),
|
|
628
|
+
deleteSearchSegmentCompanions: this.db.query(`DELETE FROM search_segment_companions WHERE stream=?;`),
|
|
629
|
+
countUploadedSegments: this.db.query(
|
|
630
|
+
`SELECT COALESCE(MAX(segment_index), -1) as max_idx
|
|
631
|
+
FROM segments WHERE stream=? AND r2_etag IS NOT NULL;`
|
|
632
|
+
),
|
|
633
|
+
getSegmentMeta: this.db.query(
|
|
634
|
+
`SELECT stream, segment_count, segment_offsets, segment_blocks, segment_last_ts
|
|
635
|
+
FROM stream_segment_meta WHERE stream=? LIMIT 1;`
|
|
636
|
+
),
|
|
637
|
+
ensureSegmentMeta: this.db.query(
|
|
638
|
+
`INSERT INTO stream_segment_meta(stream, segment_count, segment_offsets, segment_blocks, segment_last_ts)
|
|
639
|
+
VALUES(?, 0, x'', x'', x'')
|
|
640
|
+
ON CONFLICT(stream) DO NOTHING;`
|
|
641
|
+
),
|
|
642
|
+
appendSegmentMeta: this.db.query(
|
|
643
|
+
`UPDATE stream_segment_meta
|
|
644
|
+
SET segment_count = segment_count + 1,
|
|
645
|
+
segment_offsets = segment_offsets || ?,
|
|
646
|
+
segment_blocks = segment_blocks || ?,
|
|
647
|
+
segment_last_ts = segment_last_ts || ?
|
|
648
|
+
WHERE stream = ?;`
|
|
649
|
+
),
|
|
650
|
+
upsertSegmentMeta: this.db.query(
|
|
651
|
+
`INSERT INTO stream_segment_meta(stream, segment_count, segment_offsets, segment_blocks, segment_last_ts)
|
|
652
|
+
VALUES(?, ?, ?, ?, ?)
|
|
653
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
654
|
+
segment_count=excluded.segment_count,
|
|
655
|
+
segment_offsets=excluded.segment_offsets,
|
|
656
|
+
segment_blocks=excluded.segment_blocks,
|
|
657
|
+
segment_last_ts=excluded.segment_last_ts;`
|
|
658
|
+
),
|
|
659
|
+
setUploadedSegmentCount: this.db.query(
|
|
660
|
+
`UPDATE streams SET uploaded_segment_count=?, updated_at_ms=? WHERE stream=?;`
|
|
661
|
+
),
|
|
662
|
+
|
|
663
|
+
advanceUploadedThrough: this.db.query(
|
|
664
|
+
`UPDATE streams SET uploaded_through=?, updated_at_ms=? WHERE stream=?;`
|
|
665
|
+
),
|
|
666
|
+
|
|
667
|
+
getSchemaRegistry: this.db.query(`SELECT stream, schema_json, updated_at_ms, uploaded_size_bytes FROM schemas WHERE stream=? LIMIT 1;`),
|
|
668
|
+
upsertSchemaRegistry: this.db.query(
|
|
669
|
+
`INSERT INTO schemas(stream, schema_json, updated_at_ms) VALUES(?, ?, ?)
|
|
670
|
+
ON CONFLICT(stream) DO UPDATE SET schema_json=excluded.schema_json, updated_at_ms=excluded.updated_at_ms;`
|
|
671
|
+
),
|
|
672
|
+
setSchemaUploadedSize: this.db.query(`UPDATE schemas SET uploaded_size_bytes=?, updated_at_ms=? WHERE stream=?;`),
|
|
673
|
+
getStreamProfile: this.db.query(`SELECT stream, profile_json, updated_at_ms FROM stream_profiles WHERE stream=? LIMIT 1;`),
|
|
674
|
+
upsertStreamProfile: this.db.query(
|
|
675
|
+
`INSERT INTO stream_profiles(stream, profile_json, updated_at_ms) VALUES(?, ?, ?)
|
|
676
|
+
ON CONFLICT(stream) DO UPDATE SET profile_json=excluded.profile_json, updated_at_ms=excluded.updated_at_ms;`
|
|
677
|
+
),
|
|
678
|
+
deleteStreamProfile: this.db.query(`DELETE FROM stream_profiles WHERE stream=?;`),
|
|
679
|
+
countStreams: this.db.query(`SELECT COUNT(*) as cnt FROM streams WHERE (stream_flags & ?) = 0;`),
|
|
680
|
+
sumPendingBytes: this.db.query(`SELECT COALESCE(SUM(pending_bytes), 0) as total FROM streams;`),
|
|
681
|
+
sumPendingSegmentBytes: this.db.query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE uploaded_at_ms IS NULL;`),
|
|
682
|
+
recordObjectStoreRequest: this.db.query(
|
|
683
|
+
`INSERT INTO objectstore_request_counts(stream_hash, artifact, op, count, bytes, updated_at_ms)
|
|
684
|
+
VALUES(?, ?, ?, ?, ?, ?)
|
|
685
|
+
ON CONFLICT(stream_hash, artifact, op) DO UPDATE SET
|
|
686
|
+
count=objectstore_request_counts.count + excluded.count,
|
|
687
|
+
bytes=objectstore_request_counts.bytes + excluded.bytes,
|
|
688
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
689
|
+
),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private toBigInt(v: any): bigint {
|
|
694
|
+
return typeof v === "bigint" ? v : BigInt(v);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private bindInt(v: bigint): number | string {
|
|
698
|
+
const max = BigInt(Number.MAX_SAFE_INTEGER);
|
|
699
|
+
const min = BigInt(Number.MIN_SAFE_INTEGER);
|
|
700
|
+
if (v <= max && v >= min) return Number(v);
|
|
701
|
+
return v.toString();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private deleteWalThroughWithStats(
|
|
705
|
+
stream: string,
|
|
706
|
+
through: bigint,
|
|
707
|
+
opts?: { maxRows?: number }
|
|
708
|
+
): { deletedRows: bigint; deletedBytes: bigint } {
|
|
709
|
+
if (through < 0n) return { deletedRows: 0n, deletedBytes: 0n };
|
|
710
|
+
const bound = this.bindInt(through);
|
|
711
|
+
const maxRows = opts?.maxRows;
|
|
712
|
+
const useChunkedDelete = typeof maxRows === "number" && Number.isFinite(maxRows) && maxRows > 0;
|
|
713
|
+
const stmt = useChunkedDelete
|
|
714
|
+
? this.db.prepare(
|
|
715
|
+
`DELETE FROM wal
|
|
716
|
+
WHERE rowid IN (
|
|
717
|
+
SELECT rowid
|
|
718
|
+
FROM wal
|
|
719
|
+
WHERE stream=? AND offset <= ?
|
|
720
|
+
ORDER BY offset ASC
|
|
721
|
+
LIMIT ?
|
|
722
|
+
)
|
|
723
|
+
RETURNING payload_len;`
|
|
724
|
+
)
|
|
725
|
+
: this.db.prepare(
|
|
726
|
+
`DELETE FROM wal
|
|
727
|
+
WHERE stream=? AND offset <= ?
|
|
728
|
+
RETURNING payload_len;`
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const rows = useChunkedDelete
|
|
733
|
+
? stmt.iterate(stream, bound, Math.max(1, Math.floor(maxRows!)))
|
|
734
|
+
: stmt.iterate(stream, bound);
|
|
735
|
+
|
|
736
|
+
let deletedRows = 0n;
|
|
737
|
+
let deletedBytes = 0n;
|
|
738
|
+
for (const row of rows as any) {
|
|
739
|
+
deletedRows += 1n;
|
|
740
|
+
deletedBytes += this.toBigInt(row?.payload_len ?? 0);
|
|
741
|
+
}
|
|
742
|
+
return { deletedRows, deletedBytes };
|
|
743
|
+
} finally {
|
|
744
|
+
try {
|
|
745
|
+
stmt.finalize?.();
|
|
746
|
+
} catch {
|
|
747
|
+
// ignore
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private encodeU64Le(value: bigint): Uint8Array {
|
|
753
|
+
const buf = new Uint8Array(8);
|
|
754
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
755
|
+
dv.setBigUint64(0, value, true);
|
|
756
|
+
return buf;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private encodeU32Le(value: number): Uint8Array {
|
|
760
|
+
const buf = new Uint8Array(4);
|
|
761
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
762
|
+
dv.setUint32(0, value >>> 0, true);
|
|
763
|
+
return buf;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private coerceStreamRow(row: any): StreamRow {
|
|
767
|
+
return {
|
|
768
|
+
stream: String(row.stream),
|
|
769
|
+
created_at_ms: this.toBigInt(row.created_at_ms),
|
|
770
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
771
|
+
content_type: String(row.content_type),
|
|
772
|
+
profile: row.profile == null ? null : String(row.profile),
|
|
773
|
+
stream_seq: row.stream_seq == null ? null : String(row.stream_seq),
|
|
774
|
+
closed: Number(row.closed),
|
|
775
|
+
closed_producer_id: row.closed_producer_id == null ? null : String(row.closed_producer_id),
|
|
776
|
+
closed_producer_epoch: row.closed_producer_epoch == null ? null : Number(row.closed_producer_epoch),
|
|
777
|
+
closed_producer_seq: row.closed_producer_seq == null ? null : Number(row.closed_producer_seq),
|
|
778
|
+
ttl_seconds: row.ttl_seconds == null ? null : Number(row.ttl_seconds),
|
|
779
|
+
epoch: Number(row.epoch),
|
|
780
|
+
next_offset: this.toBigInt(row.next_offset),
|
|
781
|
+
sealed_through: this.toBigInt(row.sealed_through),
|
|
782
|
+
uploaded_through: this.toBigInt(row.uploaded_through),
|
|
783
|
+
uploaded_segment_count: Number(row.uploaded_segment_count ?? 0),
|
|
784
|
+
pending_rows: this.toBigInt(row.pending_rows),
|
|
785
|
+
pending_bytes: this.toBigInt(row.pending_bytes),
|
|
786
|
+
logical_size_bytes: this.toBigInt(row.logical_size_bytes ?? 0),
|
|
787
|
+
wal_rows: this.toBigInt(row.wal_rows ?? 0),
|
|
788
|
+
wal_bytes: this.toBigInt(row.wal_bytes ?? 0),
|
|
789
|
+
last_append_ms: this.toBigInt(row.last_append_ms),
|
|
790
|
+
last_segment_cut_ms: this.toBigInt(row.last_segment_cut_ms),
|
|
791
|
+
segment_in_progress: Number(row.segment_in_progress),
|
|
792
|
+
expires_at_ms: row.expires_at_ms == null ? null : this.toBigInt(row.expires_at_ms),
|
|
793
|
+
stream_flags: Number(row.stream_flags),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private coerceSegmentRow(row: any): SegmentRow {
|
|
798
|
+
return {
|
|
799
|
+
segment_id: String(row.segment_id),
|
|
800
|
+
stream: String(row.stream),
|
|
801
|
+
segment_index: Number(row.segment_index),
|
|
802
|
+
start_offset: this.toBigInt(row.start_offset),
|
|
803
|
+
end_offset: this.toBigInt(row.end_offset),
|
|
804
|
+
block_count: Number(row.block_count),
|
|
805
|
+
last_append_ms: this.toBigInt(row.last_append_ms),
|
|
806
|
+
payload_bytes: this.toBigInt(row.payload_bytes ?? 0),
|
|
807
|
+
size_bytes: Number(row.size_bytes),
|
|
808
|
+
local_path: String(row.local_path),
|
|
809
|
+
created_at_ms: this.toBigInt(row.created_at_ms),
|
|
810
|
+
uploaded_at_ms: row.uploaded_at_ms == null ? null : this.toBigInt(row.uploaded_at_ms),
|
|
811
|
+
r2_etag: row.r2_etag == null ? null : String(row.r2_etag),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
close(): void {
|
|
816
|
+
this.db.close();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
nowMs(): bigint {
|
|
820
|
+
return BigInt(Date.now());
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
isDeleted(row: StreamRow): boolean {
|
|
824
|
+
return (row.stream_flags & STREAM_FLAG_DELETED) !== 0;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
getStream(stream: string): StreamRow | null {
|
|
828
|
+
const row = this.stmts.getStream.get(stream) as any;
|
|
829
|
+
return row ? this.coerceStreamRow(row) : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
setStreamLogicalSizeBytes(stream: string, logicalSizeBytes: bigint): void {
|
|
833
|
+
this.db
|
|
834
|
+
.query(`UPDATE streams SET logical_size_bytes=?, updated_at_ms=? WHERE stream=?;`)
|
|
835
|
+
.run(this.bindInt(logicalSizeBytes), this.nowMs(), stream);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
listStreamsMissingLogicalSize(limit: number): string[] {
|
|
839
|
+
const now = this.nowMs();
|
|
840
|
+
const rows = this.db
|
|
841
|
+
.query(
|
|
842
|
+
`SELECT stream
|
|
843
|
+
FROM streams
|
|
844
|
+
WHERE (stream_flags & ?) = 0
|
|
845
|
+
AND (expires_at_ms IS NULL OR expires_at_ms > ?)
|
|
846
|
+
AND next_offset > 0
|
|
847
|
+
AND logical_size_bytes = 0
|
|
848
|
+
ORDER BY updated_at_ms ASC
|
|
849
|
+
LIMIT ?;`
|
|
850
|
+
)
|
|
851
|
+
.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, now, limit) as any[];
|
|
852
|
+
return rows.map((row) => String(row.stream));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
getWalBytesAfterOffset(stream: string, offset: bigint): bigint {
|
|
856
|
+
const row = this.db
|
|
857
|
+
.query(
|
|
858
|
+
`SELECT COALESCE(SUM(payload_len), 0) as bytes
|
|
859
|
+
FROM wal
|
|
860
|
+
WHERE stream=? AND offset > ?;`
|
|
861
|
+
)
|
|
862
|
+
.get(stream, this.bindInt(offset)) as any;
|
|
863
|
+
return this.toBigInt(row?.bytes ?? 0);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
ensureStream(
|
|
867
|
+
stream: string,
|
|
868
|
+
opts?: {
|
|
869
|
+
contentType?: string;
|
|
870
|
+
profile?: string | null;
|
|
871
|
+
expiresAtMs?: bigint | null;
|
|
872
|
+
ttlSeconds?: number | null;
|
|
873
|
+
closed?: boolean;
|
|
874
|
+
closedProducer?: { id: string; epoch: number; seq: number } | null;
|
|
875
|
+
streamFlags?: number;
|
|
876
|
+
}
|
|
877
|
+
): StreamRow {
|
|
878
|
+
const existing = this.getStream(stream);
|
|
879
|
+
if (existing) return existing;
|
|
880
|
+
|
|
881
|
+
const now = this.nowMs();
|
|
882
|
+
const epoch = 0;
|
|
883
|
+
const nextOffset = 0n;
|
|
884
|
+
const contentType = opts?.contentType ?? "application/octet-stream";
|
|
885
|
+
const profile = opts?.profile ?? "generic";
|
|
886
|
+
const closed = opts?.closed ? 1 : 0;
|
|
887
|
+
const closedProducer = opts?.closedProducer ?? null;
|
|
888
|
+
const expiresAtMs = opts?.expiresAtMs ?? null;
|
|
889
|
+
const ttlSeconds = opts?.ttlSeconds ?? null;
|
|
890
|
+
const streamFlags = opts?.streamFlags ?? 0;
|
|
891
|
+
|
|
892
|
+
this.db
|
|
893
|
+
.query(
|
|
894
|
+
`INSERT INTO streams(
|
|
895
|
+
stream, created_at_ms, updated_at_ms,
|
|
896
|
+
content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
|
|
897
|
+
epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
|
|
898
|
+
pending_rows, pending_bytes, logical_size_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
|
|
899
|
+
expires_at_ms, stream_flags
|
|
900
|
+
)
|
|
901
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
|
902
|
+
)
|
|
903
|
+
.run(
|
|
904
|
+
stream,
|
|
905
|
+
now,
|
|
906
|
+
now,
|
|
907
|
+
contentType,
|
|
908
|
+
profile,
|
|
909
|
+
null,
|
|
910
|
+
closed,
|
|
911
|
+
closedProducer ? closedProducer.id : null,
|
|
912
|
+
closedProducer ? closedProducer.epoch : null,
|
|
913
|
+
closedProducer ? closedProducer.seq : null,
|
|
914
|
+
ttlSeconds,
|
|
915
|
+
epoch,
|
|
916
|
+
nextOffset,
|
|
917
|
+
-1n,
|
|
918
|
+
-1n,
|
|
919
|
+
0,
|
|
920
|
+
0n,
|
|
921
|
+
0n,
|
|
922
|
+
0n,
|
|
923
|
+
now,
|
|
924
|
+
now,
|
|
925
|
+
0,
|
|
926
|
+
expiresAtMs,
|
|
927
|
+
streamFlags
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
this.stmts.upsertManifest.run(stream, 0, 0, null, null, null);
|
|
931
|
+
this.ensureSegmentMeta(stream);
|
|
932
|
+
return this.getStream(stream)!;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
restoreStreamRow(row: StreamRow): void {
|
|
936
|
+
this.stmts.upsertStream.run(
|
|
937
|
+
row.stream,
|
|
938
|
+
row.created_at_ms,
|
|
939
|
+
row.updated_at_ms,
|
|
940
|
+
row.content_type,
|
|
941
|
+
row.profile,
|
|
942
|
+
row.stream_seq,
|
|
943
|
+
row.closed,
|
|
944
|
+
row.closed_producer_id,
|
|
945
|
+
row.closed_producer_epoch,
|
|
946
|
+
row.closed_producer_seq,
|
|
947
|
+
row.ttl_seconds,
|
|
948
|
+
row.epoch,
|
|
949
|
+
row.next_offset,
|
|
950
|
+
row.sealed_through,
|
|
951
|
+
row.uploaded_through,
|
|
952
|
+
row.uploaded_segment_count,
|
|
953
|
+
row.pending_rows,
|
|
954
|
+
row.pending_bytes,
|
|
955
|
+
row.logical_size_bytes,
|
|
956
|
+
row.wal_rows,
|
|
957
|
+
row.wal_bytes,
|
|
958
|
+
row.last_append_ms,
|
|
959
|
+
row.last_segment_cut_ms,
|
|
960
|
+
row.segment_in_progress,
|
|
961
|
+
row.expires_at_ms,
|
|
962
|
+
row.stream_flags
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
listStreams(limit: number, offset: number): StreamRow[] {
|
|
967
|
+
const now = this.nowMs();
|
|
968
|
+
const rows = this.stmts.listStreams.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, now, limit, offset) as any[];
|
|
969
|
+
return rows.map((r) => this.coerceStreamRow(r));
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
listDeletedStreams(limit: number, offset: number): string[] {
|
|
973
|
+
const rows = this.stmts.listDeletedStreams.all(STREAM_FLAG_DELETED, limit, offset) as any[];
|
|
974
|
+
return rows.map((row) => String(row.stream));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
listExpiredStreams(limit: number): string[] {
|
|
978
|
+
const now = this.nowMs();
|
|
979
|
+
const rows = this.stmts.listExpiredStreams.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, now, limit) as any[];
|
|
980
|
+
return rows.map((r) => String(r.stream));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
deleteAccelerationState(stream: string): void {
|
|
984
|
+
const tx = this.db.transaction(() => {
|
|
985
|
+
this.stmts.deleteIndexRunsForStream.run(stream);
|
|
986
|
+
this.stmts.deleteIndexStateForStream.run(stream);
|
|
987
|
+
this.stmts.deleteSecondaryIndexRunsForStream.run(stream);
|
|
988
|
+
this.stmts.deleteSecondaryIndexStatesForStream.run(stream);
|
|
989
|
+
this.stmts.deleteLexiconIndexRunsForStream.run(stream);
|
|
990
|
+
this.stmts.deleteLexiconIndexStatesForStream.run(stream);
|
|
991
|
+
this.stmts.deleteSearchSegmentCompanions.run(stream);
|
|
992
|
+
this.stmts.deleteSearchCompanionPlan.run(stream);
|
|
993
|
+
});
|
|
994
|
+
tx();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
deleteStream(stream: string): boolean {
|
|
998
|
+
const existing = this.getStream(stream);
|
|
999
|
+
if (!existing) return false;
|
|
1000
|
+
const now = this.nowMs();
|
|
1001
|
+
const tx = this.db.transaction(() => {
|
|
1002
|
+
this.stmts.setDeleted.run(STREAM_FLAG_DELETED, now, stream);
|
|
1003
|
+
this.stmts.deleteIndexRunsForStream.run(stream);
|
|
1004
|
+
this.stmts.deleteIndexStateForStream.run(stream);
|
|
1005
|
+
this.stmts.deleteSecondaryIndexRunsForStream.run(stream);
|
|
1006
|
+
this.stmts.deleteSecondaryIndexStatesForStream.run(stream);
|
|
1007
|
+
this.stmts.deleteLexiconIndexRunsForStream.run(stream);
|
|
1008
|
+
this.stmts.deleteLexiconIndexStatesForStream.run(stream);
|
|
1009
|
+
this.stmts.deleteSearchSegmentCompanions.run(stream);
|
|
1010
|
+
this.stmts.deleteSearchCompanionPlan.run(stream);
|
|
1011
|
+
});
|
|
1012
|
+
tx();
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
updateStreamProfile(stream: string, profile: string | null): StreamRow | null {
|
|
1017
|
+
this.stmts.setStreamProfile.run(profile, this.nowMs(), stream);
|
|
1018
|
+
return this.getStream(stream);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
hardDeleteStream(stream: string): boolean {
|
|
1022
|
+
const tx = this.db.transaction(() => {
|
|
1023
|
+
const existing = this.getStream(stream);
|
|
1024
|
+
if (!existing) return false;
|
|
1025
|
+
this.db.query(`DELETE FROM wal WHERE stream=?;`).run(stream);
|
|
1026
|
+
this.db.query(`DELETE FROM segments WHERE stream=?;`).run(stream);
|
|
1027
|
+
this.db.query(`DELETE FROM manifests WHERE stream=?;`).run(stream);
|
|
1028
|
+
this.db.query(`DELETE FROM schemas WHERE stream=?;`).run(stream);
|
|
1029
|
+
this.db.query(`DELETE FROM stream_profiles WHERE stream=?;`).run(stream);
|
|
1030
|
+
this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`).run(stream);
|
|
1031
|
+
this.db.query(`DELETE FROM live_templates WHERE stream=?;`).run(stream);
|
|
1032
|
+
this.db.query(`DELETE FROM producer_state WHERE stream=?;`).run(stream);
|
|
1033
|
+
this.db.query(`DELETE FROM index_state WHERE stream=?;`).run(stream);
|
|
1034
|
+
this.db.query(`DELETE FROM index_runs WHERE stream=?;`).run(stream);
|
|
1035
|
+
this.db.query(`DELETE FROM secondary_index_state WHERE stream=?;`).run(stream);
|
|
1036
|
+
this.db.query(`DELETE FROM secondary_index_runs WHERE stream=?;`).run(stream);
|
|
1037
|
+
this.db.query(`DELETE FROM lexicon_index_state WHERE stream=?;`).run(stream);
|
|
1038
|
+
this.db.query(`DELETE FROM lexicon_index_runs WHERE stream=?;`).run(stream);
|
|
1039
|
+
this.db.query(`DELETE FROM search_companion_plans WHERE stream=?;`).run(stream);
|
|
1040
|
+
this.db.query(`DELETE FROM search_segment_companions WHERE stream=?;`).run(stream);
|
|
1041
|
+
this.db.query(`DELETE FROM stream_segment_meta WHERE stream=?;`).run(stream);
|
|
1042
|
+
this.db.query(`DELETE FROM streams WHERE stream=?;`).run(stream);
|
|
1043
|
+
return true;
|
|
1044
|
+
});
|
|
1045
|
+
return tx();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
getSchemaRegistry(stream: string): { stream: string; registry_json: string; updated_at_ms: bigint; uploaded_size_bytes: bigint } | null {
|
|
1049
|
+
const row = this.stmts.getSchemaRegistry.get(stream) as any;
|
|
1050
|
+
if (!row) return null;
|
|
1051
|
+
return {
|
|
1052
|
+
stream: String(row.stream),
|
|
1053
|
+
registry_json: String(row.schema_json),
|
|
1054
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
1055
|
+
uploaded_size_bytes: this.toBigInt(row.uploaded_size_bytes ?? 0),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async getSchemaRegistryForRead(
|
|
1060
|
+
stream: string
|
|
1061
|
+
): Promise<{ stream: string; registry_json: string; updated_at_ms: bigint; uploaded_size_bytes: bigint } | null> {
|
|
1062
|
+
return this.getSchemaRegistry(stream);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async commitSchemaMetadataMutation<T, E>(
|
|
1066
|
+
stream: string,
|
|
1067
|
+
mutation: (ctx: SchemaMetadataMutationContext) => Result<SchemaMetadataMutationPlan<T>, E>
|
|
1068
|
+
): Promise<Result<SchemaMetadataCommit<T>, E>> {
|
|
1069
|
+
const tx = this.db.transaction(() => {
|
|
1070
|
+
const streamRow = this.getStream(stream);
|
|
1071
|
+
const registryRow = this.getSchemaRegistry(stream);
|
|
1072
|
+
const mutationRes = mutation({ streamRow, registryRow });
|
|
1073
|
+
if (Result.isError(mutationRes)) return mutationRes;
|
|
1074
|
+
const updatedAtMs = this.nowMs();
|
|
1075
|
+
this.stmts.upsertSchemaRegistry.run(stream, mutationRes.value.registryJson, updatedAtMs);
|
|
1076
|
+
return Result.ok({
|
|
1077
|
+
registry: mutationRes.value.registry,
|
|
1078
|
+
updatedAtMs,
|
|
1079
|
+
value: mutationRes.value.value,
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
return tx();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
upsertSchemaRegistry(stream: string, registryJson: string): void {
|
|
1086
|
+
this.stmts.upsertSchemaRegistry.run(stream, registryJson, this.nowMs());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
setSchemaUploadedSizeBytes(stream: string, sizeBytes: number): void {
|
|
1090
|
+
this.stmts.setSchemaUploadedSize.run(sizeBytes, this.nowMs(), stream);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
getStreamProfile(stream: string): { stream: string; profile_json: string; updated_at_ms: bigint } | null {
|
|
1094
|
+
const row = this.stmts.getStreamProfile.get(stream) as any;
|
|
1095
|
+
if (!row) return null;
|
|
1096
|
+
return {
|
|
1097
|
+
stream: String(row.stream),
|
|
1098
|
+
profile_json: String(row.profile_json),
|
|
1099
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async getStreamProfileForRead(stream: string): Promise<{ stream: string; profile_json: string; updated_at_ms: bigint } | null> {
|
|
1104
|
+
return this.getStreamProfile(stream);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async commitProfileMetadataMutation<T, E>(
|
|
1108
|
+
stream: string,
|
|
1109
|
+
mutation: (ctx: ProfileMetadataMutationContext) => Result<ProfileMetadataMutationPlan<T>, E>
|
|
1110
|
+
): Promise<Result<ProfileMetadataCommit<T>, E>> {
|
|
1111
|
+
const tx = this.db.transaction(() => {
|
|
1112
|
+
const streamRow = this.getStream(stream);
|
|
1113
|
+
const profileRow = this.getStreamProfile(stream);
|
|
1114
|
+
const mutationRes = mutation({ streamRow, profileRow });
|
|
1115
|
+
if (Result.isError(mutationRes)) return mutationRes;
|
|
1116
|
+
|
|
1117
|
+
const updatedAtMs = this.nowMs();
|
|
1118
|
+
const metadata = mutationRes.value.metadata;
|
|
1119
|
+
this.stmts.setStreamProfile.run(metadata.streamProfile, updatedAtMs, stream);
|
|
1120
|
+
if (metadata.schemaRegistry) {
|
|
1121
|
+
this.stmts.upsertSchemaRegistry.run(stream, JSON.stringify(metadata.schemaRegistry), updatedAtMs);
|
|
1122
|
+
}
|
|
1123
|
+
if (metadata.profileJson == null) this.stmts.deleteStreamProfile.run(stream);
|
|
1124
|
+
else this.stmts.upsertStreamProfile.run(stream, metadata.profileJson, updatedAtMs);
|
|
1125
|
+
if (metadata.touchState === "ensure" && streamRow) {
|
|
1126
|
+
this.db
|
|
1127
|
+
.query(
|
|
1128
|
+
`INSERT OR IGNORE INTO stream_touch_state(stream, processed_through, updated_at_ms)
|
|
1129
|
+
VALUES(?, ?, ?);`
|
|
1130
|
+
)
|
|
1131
|
+
.run(stream, this.bindInt(streamRow.next_offset - 1n), updatedAtMs);
|
|
1132
|
+
} else if (metadata.touchState === "delete") {
|
|
1133
|
+
this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`).run(stream);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return Result.ok({
|
|
1137
|
+
schemaRegistry: metadata.schemaRegistry,
|
|
1138
|
+
profileUpdatedAtMs: updatedAtMs,
|
|
1139
|
+
value: mutationRes.value.value,
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
return tx();
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
upsertStreamProfile(stream: string, profileJson: string): void {
|
|
1146
|
+
this.stmts.upsertStreamProfile.run(stream, profileJson, this.nowMs());
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
deleteStreamProfile(stream: string): void {
|
|
1150
|
+
this.stmts.deleteStreamProfile.run(stream);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
addStreamFlags(stream: string, flags: number): void {
|
|
1154
|
+
if (!Number.isFinite(flags) || flags <= 0) return;
|
|
1155
|
+
this.db.query(`UPDATE streams SET stream_flags = (stream_flags | ?), updated_at_ms=? WHERE stream=?;`).run(flags, this.nowMs(), stream);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
getWalOldestOffset(stream: string): bigint | null {
|
|
1159
|
+
const row = this.db.query(`SELECT MIN(offset) as min_off FROM wal WHERE stream=?;`).get(stream) as any;
|
|
1160
|
+
if (!row || row.min_off == null) return null;
|
|
1161
|
+
return this.toBigInt(row.min_off);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
getWalOldestTimestampMs(stream: string): bigint | null {
|
|
1165
|
+
const row = this.db.query(`SELECT MIN(ts_ms) as min_ts FROM wal WHERE stream=?;`).get(stream) as any;
|
|
1166
|
+
if (!row || row.min_ts == null) return null;
|
|
1167
|
+
return this.toBigInt(row.min_ts);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Trim a WAL-only stream by age (in ms), leaving at least 1 record if the stream is non-empty.
|
|
1172
|
+
*
|
|
1173
|
+
* This is primarily intended for internal companion touch streams which are not segmented/uploaded.
|
|
1174
|
+
*/
|
|
1175
|
+
trimWalByAge(stream: string, maxAgeMs: number): { trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null } {
|
|
1176
|
+
const ageMs = Math.max(0, Math.floor(maxAgeMs));
|
|
1177
|
+
if (!Number.isFinite(ageMs)) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: null };
|
|
1178
|
+
|
|
1179
|
+
const tx = this.db.transaction(() => {
|
|
1180
|
+
const lastRow = this.db.query(`SELECT offset, ts_ms FROM wal WHERE stream=? ORDER BY offset DESC LIMIT 1;`).get(stream) as any;
|
|
1181
|
+
if (!lastRow || lastRow.offset == null) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: null };
|
|
1182
|
+
const lastOffset = this.toBigInt(lastRow.offset);
|
|
1183
|
+
|
|
1184
|
+
let keepFromOffset: bigint;
|
|
1185
|
+
if (ageMs === 0) {
|
|
1186
|
+
// maxAgeMs=0 means "keep only the newest row" (still leaving 1 record).
|
|
1187
|
+
keepFromOffset = lastOffset;
|
|
1188
|
+
} else {
|
|
1189
|
+
const cutoff = this.nowMs() - BigInt(ageMs);
|
|
1190
|
+
const keepRow = this.db
|
|
1191
|
+
.query(`SELECT offset FROM wal WHERE stream=? AND ts_ms >= ? ORDER BY offset ASC LIMIT 1;`)
|
|
1192
|
+
.get(stream, this.bindInt(cutoff)) as any;
|
|
1193
|
+
keepFromOffset = keepRow && keepRow.offset != null ? this.toBigInt(keepRow.offset) : lastOffset;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (keepFromOffset <= 0n) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
|
|
1197
|
+
|
|
1198
|
+
const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, keepFromOffset - 1n);
|
|
1199
|
+
if (rows <= 0n) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
|
|
1200
|
+
|
|
1201
|
+
// Touch streams are WAL-only: pending_* tracks WAL payload bytes/rows. Keep it consistent for stats/backpressure.
|
|
1202
|
+
const now = this.nowMs();
|
|
1203
|
+
this.db.query(
|
|
1204
|
+
`UPDATE streams
|
|
1205
|
+
SET pending_bytes = CASE WHEN pending_bytes >= ? THEN pending_bytes - ? ELSE 0 END,
|
|
1206
|
+
pending_rows = CASE WHEN pending_rows >= ? THEN pending_rows - ? ELSE 0 END,
|
|
1207
|
+
wal_bytes = CASE WHEN wal_bytes >= ? THEN wal_bytes - ? ELSE 0 END,
|
|
1208
|
+
wal_rows = CASE WHEN wal_rows >= ? THEN wal_rows - ? ELSE 0 END,
|
|
1209
|
+
updated_at_ms = ?
|
|
1210
|
+
WHERE stream = ?;`
|
|
1211
|
+
).run(bytes, bytes, rows, rows, bytes, bytes, rows, rows, now, stream);
|
|
1212
|
+
|
|
1213
|
+
const trimmedBytes = bytes <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bytes) : Number.MAX_SAFE_INTEGER;
|
|
1214
|
+
const trimmedRows = rows <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(rows) : Number.MAX_SAFE_INTEGER;
|
|
1215
|
+
return { trimmedRows, trimmedBytes, keptFromOffset: keepFromOffset };
|
|
1216
|
+
});
|
|
1217
|
+
return tx();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
countStreams(): number {
|
|
1221
|
+
const row = this.stmts.countStreams.get(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH) as any;
|
|
1222
|
+
return row ? Number(row.cnt) : 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
sumPendingBytes(): number {
|
|
1226
|
+
const row = this.stmts.sumPendingBytes.get() as any;
|
|
1227
|
+
const total = row?.total ?? 0;
|
|
1228
|
+
return Number(this.toBigInt(total));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
sumPendingSegmentBytes(): number {
|
|
1232
|
+
const row = this.stmts.sumPendingSegmentBytes.get() as any;
|
|
1233
|
+
const total = row?.total ?? 0;
|
|
1234
|
+
return Number(this.toBigInt(total));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private ensureDbStat(): boolean {
|
|
1238
|
+
if (this.dbstatReady != null) return this.dbstatReady;
|
|
1239
|
+
try {
|
|
1240
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS temp.dbstat USING dbstat;");
|
|
1241
|
+
this.dbstatReady = true;
|
|
1242
|
+
} catch {
|
|
1243
|
+
this.dbstatReady = false;
|
|
1244
|
+
}
|
|
1245
|
+
return this.dbstatReady;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private estimateWalBytes(): number {
|
|
1249
|
+
try {
|
|
1250
|
+
const row = this.db.query(
|
|
1251
|
+
`SELECT
|
|
1252
|
+
COALESCE(SUM(payload_len), 0) as payload,
|
|
1253
|
+
COALESCE(SUM(LENGTH(routing_key)), 0) as rk,
|
|
1254
|
+
COALESCE(SUM(LENGTH(content_type)), 0) as ct
|
|
1255
|
+
FROM wal;`
|
|
1256
|
+
).get() as any;
|
|
1257
|
+
return Number(row?.payload ?? 0) + Number(row?.rk ?? 0) + Number(row?.ct ?? 0);
|
|
1258
|
+
} catch {
|
|
1259
|
+
return 0;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private estimateMetaBytes(): number {
|
|
1264
|
+
try {
|
|
1265
|
+
const streams = this.db.query(
|
|
1266
|
+
`SELECT
|
|
1267
|
+
COALESCE(SUM(LENGTH(stream)), 0) as stream,
|
|
1268
|
+
COALESCE(SUM(LENGTH(content_type)), 0) as content_type,
|
|
1269
|
+
COALESCE(SUM(LENGTH(stream_seq)), 0) as stream_seq,
|
|
1270
|
+
COALESCE(SUM(LENGTH(closed_producer_id)), 0) as closed_producer_id
|
|
1271
|
+
FROM streams;`
|
|
1272
|
+
).get() as any;
|
|
1273
|
+
const segments = this.db.query(
|
|
1274
|
+
`SELECT
|
|
1275
|
+
COALESCE(SUM(LENGTH(segment_id)), 0) as segment_id,
|
|
1276
|
+
COALESCE(SUM(LENGTH(stream)), 0) as stream,
|
|
1277
|
+
COALESCE(SUM(LENGTH(local_path)), 0) as local_path,
|
|
1278
|
+
COALESCE(SUM(LENGTH(r2_etag)), 0) as r2_etag
|
|
1279
|
+
FROM segments;`
|
|
1280
|
+
).get() as any;
|
|
1281
|
+
const manifests = this.db.query(
|
|
1282
|
+
`SELECT
|
|
1283
|
+
COALESCE(SUM(LENGTH(stream)), 0) as stream,
|
|
1284
|
+
COALESCE(SUM(LENGTH(last_uploaded_etag)), 0) as last_uploaded_etag
|
|
1285
|
+
FROM manifests;`
|
|
1286
|
+
).get() as any;
|
|
1287
|
+
const schemas = this.db.query(`SELECT COALESCE(SUM(LENGTH(schema_json)), 0) as schema_json FROM schemas;`).get() as any;
|
|
1288
|
+
const producers = this.db.query(
|
|
1289
|
+
`SELECT
|
|
1290
|
+
COALESCE(SUM(LENGTH(stream)), 0) as stream,
|
|
1291
|
+
COALESCE(SUM(LENGTH(producer_id)), 0) as producer_id
|
|
1292
|
+
FROM producer_state;`
|
|
1293
|
+
).get() as any;
|
|
1294
|
+
const total =
|
|
1295
|
+
Number(streams?.stream ?? 0) +
|
|
1296
|
+
Number(streams?.content_type ?? 0) +
|
|
1297
|
+
Number(streams?.stream_seq ?? 0) +
|
|
1298
|
+
Number(streams?.closed_producer_id ?? 0) +
|
|
1299
|
+
Number(segments?.segment_id ?? 0) +
|
|
1300
|
+
Number(segments?.stream ?? 0) +
|
|
1301
|
+
Number(segments?.local_path ?? 0) +
|
|
1302
|
+
Number(segments?.r2_etag ?? 0) +
|
|
1303
|
+
Number(manifests?.stream ?? 0) +
|
|
1304
|
+
Number(manifests?.last_uploaded_etag ?? 0) +
|
|
1305
|
+
Number(schemas?.schema_json ?? 0) +
|
|
1306
|
+
Number(producers?.stream ?? 0) +
|
|
1307
|
+
Number(producers?.producer_id ?? 0);
|
|
1308
|
+
return total;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return 0;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
getWalDbSizeBytes(): number {
|
|
1315
|
+
if (this.ensureDbStat()) {
|
|
1316
|
+
try {
|
|
1317
|
+
const row = this.db.query(`SELECT COALESCE(SUM(pgsize), 0) as total FROM temp.dbstat WHERE name = 'wal';`).get() as any;
|
|
1318
|
+
return Number(row?.total ?? 0);
|
|
1319
|
+
} catch {
|
|
1320
|
+
// fall through
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return this.estimateWalBytes();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
getMetaDbSizeBytes(): number {
|
|
1327
|
+
if (this.ensureDbStat()) {
|
|
1328
|
+
try {
|
|
1329
|
+
const row = this.db
|
|
1330
|
+
.query(`SELECT COALESCE(SUM(pgsize), 0) as total FROM temp.dbstat WHERE name != 'wal';`)
|
|
1331
|
+
.get() as any;
|
|
1332
|
+
return Number(row?.total ?? 0);
|
|
1333
|
+
} catch {
|
|
1334
|
+
// fall through
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return this.estimateMetaBytes();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Append rows into WAL inside a transaction.
|
|
1342
|
+
*
|
|
1343
|
+
* Returns the last offset written.
|
|
1344
|
+
*/
|
|
1345
|
+
appendWalRows(args: {
|
|
1346
|
+
stream: string;
|
|
1347
|
+
startOffset: bigint;
|
|
1348
|
+
expectedOffset?: bigint;
|
|
1349
|
+
baseAppendMs: bigint;
|
|
1350
|
+
rows: Array<{ routingKey: Uint8Array | null; contentType: string | null; payload: Uint8Array; appendMs: bigint }>;
|
|
1351
|
+
}): Result<
|
|
1352
|
+
{ lastOffset: bigint },
|
|
1353
|
+
{ kind: "no_rows" | "stream_missing" | "stream_expired" } | { kind: "seq_mismatch"; expectedNext: bigint }
|
|
1354
|
+
> {
|
|
1355
|
+
const { stream, startOffset, expectedOffset, rows } = args;
|
|
1356
|
+
if (rows.length === 0) return Result.err({ kind: "no_rows" });
|
|
1357
|
+
|
|
1358
|
+
const tx = this.db.transaction(() => {
|
|
1359
|
+
const st = this.getStream(stream);
|
|
1360
|
+
if (!st || this.isDeleted(st)) return Result.err({ kind: "stream_missing" as const });
|
|
1361
|
+
if (st.expires_at_ms != null && this.nowMs() > st.expires_at_ms) return Result.err({ kind: "stream_expired" as const });
|
|
1362
|
+
|
|
1363
|
+
if (expectedOffset !== undefined && st.next_offset !== expectedOffset) {
|
|
1364
|
+
return Result.err({ kind: "seq_mismatch" as const, expectedNext: st.next_offset });
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
let totalBytes = 0n;
|
|
1368
|
+
let offset = startOffset;
|
|
1369
|
+
for (const r of rows) {
|
|
1370
|
+
const payloadLen = r.payload.byteLength;
|
|
1371
|
+
totalBytes += BigInt(payloadLen);
|
|
1372
|
+
this.stmts.insertWal.run(stream, offset, r.appendMs, r.payload, payloadLen, r.routingKey, r.contentType, 0);
|
|
1373
|
+
offset += 1n;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const lastOffset = offset - 1n;
|
|
1377
|
+
const newNextOffset = lastOffset + 1n;
|
|
1378
|
+
const now = this.nowMs();
|
|
1379
|
+
const pendingRows = BigInt(rows.length);
|
|
1380
|
+
const lastAppend = rows[rows.length - 1].appendMs;
|
|
1381
|
+
|
|
1382
|
+
this.stmts.updateStreamAppend.run(
|
|
1383
|
+
newNextOffset,
|
|
1384
|
+
now,
|
|
1385
|
+
lastAppend,
|
|
1386
|
+
pendingRows,
|
|
1387
|
+
totalBytes,
|
|
1388
|
+
totalBytes,
|
|
1389
|
+
pendingRows,
|
|
1390
|
+
totalBytes,
|
|
1391
|
+
stream,
|
|
1392
|
+
STREAM_FLAG_DELETED
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
return Result.ok({ lastOffset });
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
return tx();
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async appendBatch(batch: StoreAppendTask[]): Promise<StoreAppendBatch> {
|
|
1402
|
+
return this.walStore.appendBatch(batch);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array) {
|
|
1406
|
+
return this.walStore.readWalRange(stream, startOffset, endOffset, routingKey);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
readWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array) {
|
|
1410
|
+
return this.walStore.readWalRangeDesc(stream, startOffset, endOffset, routingKey);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async getWalOldestTimestampMsForRead(stream: string): Promise<bigint | null> {
|
|
1414
|
+
return this.walStore.getWalOldestTimestampMsForRead(stream);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async nowMsForRead(): Promise<bigint> {
|
|
1418
|
+
return this.nowMs();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async getStreamForRead(stream: string): Promise<StreamRow | null> {
|
|
1422
|
+
return this.getStream(stream);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
getSegmentStreamState(stream: string): StreamRow | null {
|
|
1426
|
+
return this.getStream(stream);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async listSegmentsForRead(stream: string): Promise<SegmentRow[]> {
|
|
1430
|
+
return this.listSegmentsForStream(stream);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async getSegmentByIndexForRead(stream: string, segmentIndex: number): Promise<SegmentRow | null> {
|
|
1434
|
+
return this.getSegmentByIndex(stream, segmentIndex);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async findSegmentForOffsetForRead(stream: string, offset: bigint): Promise<SegmentRow | null> {
|
|
1438
|
+
return this.findSegmentForOffset(stream, offset);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
async countSegmentsForRead(stream: string): Promise<number> {
|
|
1442
|
+
return this.countSegmentsForStream(stream);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
async getSearchCompanionPlanForRead(stream: string): Promise<SearchCompanionPlanRow | null> {
|
|
1446
|
+
return this.getSearchCompanionPlan(stream);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async listSearchSegmentCompanionsForRead(stream: string): Promise<SearchSegmentCompanionRow[]> {
|
|
1450
|
+
return this.listSearchSegmentCompanions(stream);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
async getSearchSegmentCompanionForRead(stream: string, segmentIndex: number): Promise<SearchSegmentCompanionRow | null> {
|
|
1454
|
+
return this.getSearchSegmentCompanion(stream, segmentIndex);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Query WAL rows within a range.
|
|
1459
|
+
* Uses iterate() for bounded memory.
|
|
1460
|
+
*/
|
|
1461
|
+
*iterWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): Generator<any, void, void> {
|
|
1462
|
+
for (const row of this.walStore.iterWalRange(stream, startOffset, endOffset, routingKey)) {
|
|
1463
|
+
yield legacyWalReadRow(row);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
*iterWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): Generator<any, void, void> {
|
|
1468
|
+
for (const row of this.walStore.iterWalRangeDesc(stream, startOffset, endOffset, routingKey)) {
|
|
1469
|
+
yield legacyWalReadRow(row);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
nextSegmentIndexForStream(stream: string): number {
|
|
1474
|
+
const row = this.stmts.nextSegmentIndex.get(stream) as any;
|
|
1475
|
+
return Number(row?.next_idx ?? 0);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
createSegmentRow(row: {
|
|
1479
|
+
segmentId: string;
|
|
1480
|
+
stream: string;
|
|
1481
|
+
segmentIndex: number;
|
|
1482
|
+
startOffset: bigint;
|
|
1483
|
+
endOffset: bigint;
|
|
1484
|
+
blockCount: number;
|
|
1485
|
+
lastAppendMs: bigint;
|
|
1486
|
+
payloadBytes: bigint;
|
|
1487
|
+
sizeBytes: number;
|
|
1488
|
+
localPath: string;
|
|
1489
|
+
}): void {
|
|
1490
|
+
this.stmts.createSegment.run(
|
|
1491
|
+
row.segmentId,
|
|
1492
|
+
row.stream,
|
|
1493
|
+
row.segmentIndex,
|
|
1494
|
+
row.startOffset,
|
|
1495
|
+
row.endOffset,
|
|
1496
|
+
row.blockCount,
|
|
1497
|
+
row.lastAppendMs,
|
|
1498
|
+
row.payloadBytes,
|
|
1499
|
+
row.sizeBytes,
|
|
1500
|
+
row.localPath,
|
|
1501
|
+
this.nowMs()
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
commitSealedSegment(row: {
|
|
1506
|
+
segmentId: string;
|
|
1507
|
+
stream: string;
|
|
1508
|
+
segmentIndex: number;
|
|
1509
|
+
startOffset: bigint;
|
|
1510
|
+
endOffset: bigint;
|
|
1511
|
+
blockCount: number;
|
|
1512
|
+
lastAppendMs: bigint;
|
|
1513
|
+
payloadBytes: bigint;
|
|
1514
|
+
sizeBytes: number;
|
|
1515
|
+
localPath: string;
|
|
1516
|
+
rowsSealed: bigint;
|
|
1517
|
+
}): void {
|
|
1518
|
+
const tx = this.db.transaction(() => {
|
|
1519
|
+
this.createSegmentRow(row);
|
|
1520
|
+
this.appendSegmentMeta(row.stream, row.endOffset + 1n, row.blockCount, row.lastAppendMs * 1_000_000n);
|
|
1521
|
+
this.setStreamSealedThrough(row.stream, row.endOffset, row.payloadBytes, row.rowsSealed);
|
|
1522
|
+
});
|
|
1523
|
+
tx();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
listSegmentsForStream(stream: string): SegmentRow[] {
|
|
1527
|
+
const rows = this.stmts.listSegmentsForStream.all(stream) as any[];
|
|
1528
|
+
return rows.map((r) => this.coerceSegmentRow(r));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
getSegmentByIndex(stream: string, segmentIndex: number): SegmentRow | null {
|
|
1532
|
+
const row = this.stmts.getSegmentByIndex.get(stream, segmentIndex) as any;
|
|
1533
|
+
return row ? this.coerceSegmentRow(row) : null;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
findSegmentForOffset(stream: string, offset: bigint): SegmentRow | null {
|
|
1537
|
+
const bound = this.bindInt(offset);
|
|
1538
|
+
const row = this.stmts.findSegmentForOffset.get(stream, bound, bound) as any;
|
|
1539
|
+
return row ? this.coerceSegmentRow(row) : null;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
pendingUploadHeads(limit: number): SegmentRow[] {
|
|
1543
|
+
const rows = this.stmts.pendingUploadHeads.all(limit) as any[];
|
|
1544
|
+
return rows.map((r) => this.coerceSegmentRow(r));
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
recentSegmentCompressionRatio(stream: string, limit = 8): number | null {
|
|
1548
|
+
const row = this.stmts.recentSegmentCompressionWindow.get(stream, Math.max(1, limit)) as any;
|
|
1549
|
+
const count = Number(row?.cnt ?? 0);
|
|
1550
|
+
if (!Number.isFinite(count) || count <= 0) return null;
|
|
1551
|
+
const payloadTotal = this.toBigInt(row?.payload_total ?? 0);
|
|
1552
|
+
const sizeTotal = this.toBigInt(row?.size_total ?? 0);
|
|
1553
|
+
if (payloadTotal <= 0n || sizeTotal <= 0n) return null;
|
|
1554
|
+
return Number(sizeTotal) / Number(payloadTotal);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
countPendingSegments(): number {
|
|
1558
|
+
const row = this.stmts.countPendingSegments.get() as any;
|
|
1559
|
+
return row ? Number(row.cnt) : 0;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
countSegmentsForStream(stream: string): number {
|
|
1563
|
+
const row = this.stmts.countSegmentsForStream.get(stream) as any;
|
|
1564
|
+
return row ? Number(row.cnt) : 0;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
getSegmentMeta(stream: string): SegmentMetaRow | null {
|
|
1568
|
+
const row = this.stmts.getSegmentMeta.get(stream) as any;
|
|
1569
|
+
if (!row) return null;
|
|
1570
|
+
const offsets = row.segment_offsets instanceof Uint8Array ? row.segment_offsets : new Uint8Array(row.segment_offsets);
|
|
1571
|
+
const blocks = row.segment_blocks instanceof Uint8Array ? row.segment_blocks : new Uint8Array(row.segment_blocks);
|
|
1572
|
+
const lastTs = row.segment_last_ts instanceof Uint8Array ? row.segment_last_ts : new Uint8Array(row.segment_last_ts);
|
|
1573
|
+
return {
|
|
1574
|
+
stream: String(row.stream),
|
|
1575
|
+
segment_count: Number(row.segment_count),
|
|
1576
|
+
segment_offsets: offsets,
|
|
1577
|
+
segment_blocks: blocks,
|
|
1578
|
+
segment_last_ts: lastTs,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
ensureSegmentMeta(stream: string): void {
|
|
1583
|
+
this.stmts.ensureSegmentMeta.run(stream);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
appendSegmentMeta(stream: string, offsetPlusOne: bigint, blockCount: number, lastAppendNs: bigint): void {
|
|
1587
|
+
this.ensureSegmentMeta(stream);
|
|
1588
|
+
const offsetBytes = this.encodeU64Le(offsetPlusOne);
|
|
1589
|
+
const blockBytes = this.encodeU32Le(blockCount);
|
|
1590
|
+
const tsBytes = this.encodeU64Le(lastAppendNs);
|
|
1591
|
+
this.stmts.appendSegmentMeta.run(offsetBytes, blockBytes, tsBytes, stream);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
upsertSegmentMeta(stream: string, count: number, offsets: Uint8Array, blocks: Uint8Array, lastTs: Uint8Array): void {
|
|
1595
|
+
this.stmts.upsertSegmentMeta.run(stream, count, offsets, blocks, lastTs);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
rebuildSegmentMeta(stream: string): SegmentMetaRow {
|
|
1599
|
+
const rows = this.db
|
|
1600
|
+
.query(
|
|
1601
|
+
`SELECT end_offset, block_count, last_append_ms
|
|
1602
|
+
FROM segments WHERE stream=? ORDER BY segment_index ASC;`
|
|
1603
|
+
)
|
|
1604
|
+
.all(stream) as any[];
|
|
1605
|
+
const count = rows.length;
|
|
1606
|
+
const offsets = new Uint8Array(count * 8);
|
|
1607
|
+
const blocks = new Uint8Array(count * 4);
|
|
1608
|
+
const lastTs = new Uint8Array(count * 8);
|
|
1609
|
+
const dvOffsets = new DataView(offsets.buffer, offsets.byteOffset, offsets.byteLength);
|
|
1610
|
+
const dvBlocks = new DataView(blocks.buffer, blocks.byteOffset, blocks.byteLength);
|
|
1611
|
+
const dvLastTs = new DataView(lastTs.buffer, lastTs.byteOffset, lastTs.byteLength);
|
|
1612
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1613
|
+
const endOffset = this.toBigInt(rows[i].end_offset);
|
|
1614
|
+
const blockCount = Number(rows[i].block_count);
|
|
1615
|
+
const lastAppendMs = this.toBigInt(rows[i].last_append_ms);
|
|
1616
|
+
dvOffsets.setBigUint64(i * 8, endOffset + 1n, true);
|
|
1617
|
+
dvBlocks.setUint32(i * 4, blockCount >>> 0, true);
|
|
1618
|
+
dvLastTs.setBigUint64(i * 8, lastAppendMs * 1_000_000n, true);
|
|
1619
|
+
}
|
|
1620
|
+
this.upsertSegmentMeta(stream, count, offsets, blocks, lastTs);
|
|
1621
|
+
return { stream, segment_count: count, segment_offsets: offsets, segment_blocks: blocks, segment_last_ts: lastTs };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
setUploadedSegmentCount(stream: string, count: number): void {
|
|
1625
|
+
this.stmts.setUploadedSegmentCount.run(count, this.nowMs(), stream);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
advanceUploadedSegmentCount(stream: string): number {
|
|
1629
|
+
const row = this.getStream(stream);
|
|
1630
|
+
if (!row) return 0;
|
|
1631
|
+
let count = row.uploaded_segment_count ?? 0;
|
|
1632
|
+
for (;;) {
|
|
1633
|
+
const seg = this.getSegmentByIndex(stream, count);
|
|
1634
|
+
if (!seg || !seg.r2_etag) break;
|
|
1635
|
+
count += 1;
|
|
1636
|
+
}
|
|
1637
|
+
if (count !== row.uploaded_segment_count) {
|
|
1638
|
+
this.stmts.setUploadedSegmentCount.run(count, this.nowMs(), stream);
|
|
1639
|
+
}
|
|
1640
|
+
return count;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
markSegmentUploaded(segmentId: string, etag: string, uploadedAtMs: bigint): void {
|
|
1644
|
+
this.stmts.markSegmentUploaded.run(etag, uploadedAtMs, segmentId);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
setStreamSealedThrough(stream: string, sealedThrough: bigint, bytesSealed: bigint, rowsSealed: bigint): void {
|
|
1648
|
+
const now = this.nowMs();
|
|
1649
|
+
this.db.query(
|
|
1650
|
+
`UPDATE streams
|
|
1651
|
+
SET sealed_through = ?,
|
|
1652
|
+
pending_bytes = CASE WHEN pending_bytes >= ? THEN pending_bytes - ? ELSE 0 END,
|
|
1653
|
+
pending_rows = CASE WHEN pending_rows >= ? THEN pending_rows - ? ELSE 0 END,
|
|
1654
|
+
last_segment_cut_ms = ?,
|
|
1655
|
+
updated_at_ms = ?
|
|
1656
|
+
WHERE stream = ?;`
|
|
1657
|
+
).run(sealedThrough, bytesSealed, bytesSealed, rowsSealed, rowsSealed, now, now, stream);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
setSegmentInProgress(stream: string, inProgress: number): void {
|
|
1661
|
+
this.db.query(`UPDATE streams SET segment_in_progress=?, updated_at_ms=? WHERE stream=?;`).run(inProgress, this.nowMs(), stream);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
tryClaimSegment(stream: string): { token: string } | null {
|
|
1665
|
+
const res = this.stmts.tryClaimSegment.run(this.nowMs(), stream) as any;
|
|
1666
|
+
const changes = typeof res?.changes === "bigint" ? res.changes : BigInt(Number(res?.changes ?? 0));
|
|
1667
|
+
return changes > 0n ? { token: "sqlite" } : null;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
resetSegmentInProgress(): void {
|
|
1671
|
+
this.db.query(`UPDATE streams SET segment_in_progress=0 WHERE segment_in_progress != 0;`).run();
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
advanceUploadedThrough(stream: string, uploadedThrough: bigint): void {
|
|
1675
|
+
this.stmts.advanceUploadedThrough.run(uploadedThrough, this.nowMs(), stream);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number } {
|
|
1679
|
+
const tx = this.db.transaction(() => {
|
|
1680
|
+
const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, uploadedThrough);
|
|
1681
|
+
if (rows <= 0n) return { deletedRows: 0, deletedBytes: 0 };
|
|
1682
|
+
|
|
1683
|
+
const now = this.nowMs();
|
|
1684
|
+
this.db.query(
|
|
1685
|
+
`UPDATE streams
|
|
1686
|
+
SET wal_bytes = CASE WHEN wal_bytes >= ? THEN wal_bytes - ? ELSE 0 END,
|
|
1687
|
+
wal_rows = CASE WHEN wal_rows >= ? THEN wal_rows - ? ELSE 0 END,
|
|
1688
|
+
updated_at_ms = ?
|
|
1689
|
+
WHERE stream = ?;`
|
|
1690
|
+
).run(bytes, bytes, rows, rows, now, stream);
|
|
1691
|
+
|
|
1692
|
+
const deletedBytes = bytes <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bytes) : Number.MAX_SAFE_INTEGER;
|
|
1693
|
+
const deletedRows = rows <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(rows) : Number.MAX_SAFE_INTEGER;
|
|
1694
|
+
return { deletedRows, deletedBytes };
|
|
1695
|
+
});
|
|
1696
|
+
return tx();
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
getManifestRow(stream: string): {
|
|
1700
|
+
stream: string;
|
|
1701
|
+
generation: number;
|
|
1702
|
+
uploaded_generation: number;
|
|
1703
|
+
last_uploaded_at_ms: bigint | null;
|
|
1704
|
+
last_uploaded_etag: string | null;
|
|
1705
|
+
last_uploaded_size_bytes: bigint | null;
|
|
1706
|
+
} {
|
|
1707
|
+
const row = this.stmts.getManifest.get(stream) as any;
|
|
1708
|
+
if (!row) {
|
|
1709
|
+
this.stmts.upsertManifest.run(stream, 0, 0, null, null, null);
|
|
1710
|
+
const fresh = this.stmts.getManifest.get(stream) as any;
|
|
1711
|
+
return {
|
|
1712
|
+
stream: String(fresh.stream),
|
|
1713
|
+
generation: Number(fresh.generation),
|
|
1714
|
+
uploaded_generation: Number(fresh.uploaded_generation),
|
|
1715
|
+
last_uploaded_at_ms: fresh.last_uploaded_at_ms == null ? null : this.toBigInt(fresh.last_uploaded_at_ms),
|
|
1716
|
+
last_uploaded_etag: fresh.last_uploaded_etag == null ? null : String(fresh.last_uploaded_etag),
|
|
1717
|
+
last_uploaded_size_bytes: fresh.last_uploaded_size_bytes == null ? null : this.toBigInt(fresh.last_uploaded_size_bytes),
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
stream: String(row.stream),
|
|
1722
|
+
generation: Number(row.generation),
|
|
1723
|
+
uploaded_generation: Number(row.uploaded_generation),
|
|
1724
|
+
last_uploaded_at_ms: row.last_uploaded_at_ms == null ? null : this.toBigInt(row.last_uploaded_at_ms),
|
|
1725
|
+
last_uploaded_etag: row.last_uploaded_etag == null ? null : String(row.last_uploaded_etag),
|
|
1726
|
+
last_uploaded_size_bytes: row.last_uploaded_size_bytes == null ? null : this.toBigInt(row.last_uploaded_size_bytes),
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
upsertManifestRow(
|
|
1731
|
+
stream: string,
|
|
1732
|
+
generation: number,
|
|
1733
|
+
uploadedGeneration: number,
|
|
1734
|
+
uploadedAtMs: bigint | null,
|
|
1735
|
+
etag: string | null,
|
|
1736
|
+
sizeBytes: number | null
|
|
1737
|
+
): void {
|
|
1738
|
+
this.stmts.upsertManifest.run(stream, generation, uploadedGeneration, uploadedAtMs, etag, sizeBytes);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
loadManifestPublicationSnapshot(stream: string): ManifestPublicationSnapshot | null {
|
|
1742
|
+
return loadSqliteManifestPublicationSnapshot(this, stream);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
getSegmentForManifestCleanup(stream: string, segmentIndex: number): SegmentRow | null {
|
|
1746
|
+
return this.getSegmentByIndex(stream, segmentIndex);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
getIndexState(stream: string): IndexStateRow | null {
|
|
1750
|
+
const row = this.stmts.getIndexState.get(stream) as any;
|
|
1751
|
+
if (!row) return null;
|
|
1752
|
+
return {
|
|
1753
|
+
stream: String(row.stream),
|
|
1754
|
+
index_secret: row.index_secret instanceof Uint8Array ? row.index_secret : new Uint8Array(row.index_secret),
|
|
1755
|
+
indexed_through: Number(row.indexed_through),
|
|
1756
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
upsertIndexState(stream: string, indexSecret: Uint8Array, indexedThrough: number): void {
|
|
1761
|
+
this.stmts.upsertIndexState.run(stream, indexSecret, indexedThrough, this.nowMs());
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
updateIndexedThrough(stream: string, indexedThrough: number): void {
|
|
1765
|
+
this.stmts.updateIndexedThrough.run(indexedThrough, this.nowMs(), stream);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
listIndexRuns(stream: string): IndexRunRow[] {
|
|
1769
|
+
const rows = this.stmts.listIndexRuns.all(stream) as any[];
|
|
1770
|
+
return rows.map((r) => ({
|
|
1771
|
+
run_id: String(r.run_id),
|
|
1772
|
+
stream: String(r.stream),
|
|
1773
|
+
level: Number(r.level),
|
|
1774
|
+
start_segment: Number(r.start_segment),
|
|
1775
|
+
end_segment: Number(r.end_segment),
|
|
1776
|
+
object_key: String(r.object_key),
|
|
1777
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1778
|
+
filter_len: Number(r.filter_len),
|
|
1779
|
+
record_count: Number(r.record_count),
|
|
1780
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1781
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1782
|
+
}));
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
listIndexRunsAll(stream: string): IndexRunRow[] {
|
|
1786
|
+
const rows = this.stmts.listIndexRunsAll.all(stream) as any[];
|
|
1787
|
+
return rows.map((r) => ({
|
|
1788
|
+
run_id: String(r.run_id),
|
|
1789
|
+
stream: String(r.stream),
|
|
1790
|
+
level: Number(r.level),
|
|
1791
|
+
start_segment: Number(r.start_segment),
|
|
1792
|
+
end_segment: Number(r.end_segment),
|
|
1793
|
+
object_key: String(r.object_key),
|
|
1794
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1795
|
+
filter_len: Number(r.filter_len),
|
|
1796
|
+
record_count: Number(r.record_count),
|
|
1797
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1798
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1799
|
+
}));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
listRetiredIndexRuns(stream: string): IndexRunRow[] {
|
|
1803
|
+
const rows = this.stmts.listRetiredIndexRuns.all(stream) as any[];
|
|
1804
|
+
return rows.map((r) => ({
|
|
1805
|
+
run_id: String(r.run_id),
|
|
1806
|
+
stream: String(r.stream),
|
|
1807
|
+
level: Number(r.level),
|
|
1808
|
+
start_segment: Number(r.start_segment),
|
|
1809
|
+
end_segment: Number(r.end_segment),
|
|
1810
|
+
object_key: String(r.object_key),
|
|
1811
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1812
|
+
filter_len: Number(r.filter_len),
|
|
1813
|
+
record_count: Number(r.record_count),
|
|
1814
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1815
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1816
|
+
}));
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
insertIndexRun(row: Omit<IndexRunRow, "retired_gen" | "retired_at_ms">): void {
|
|
1820
|
+
this.stmts.insertIndexRun.run(
|
|
1821
|
+
row.run_id,
|
|
1822
|
+
row.stream,
|
|
1823
|
+
row.level,
|
|
1824
|
+
row.start_segment,
|
|
1825
|
+
row.end_segment,
|
|
1826
|
+
row.object_key,
|
|
1827
|
+
row.size_bytes,
|
|
1828
|
+
row.filter_len,
|
|
1829
|
+
row.record_count
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
retireIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): void {
|
|
1834
|
+
if (runIds.length === 0) return;
|
|
1835
|
+
const tx = this.db.transaction(() => {
|
|
1836
|
+
for (const runId of runIds) {
|
|
1837
|
+
this.stmts.retireIndexRun.run(retiredGen, retiredAtMs, runId);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
tx();
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
deleteIndexRuns(runIds: string[]): void {
|
|
1844
|
+
if (runIds.length === 0) return;
|
|
1845
|
+
const tx = this.db.transaction(() => {
|
|
1846
|
+
for (const runId of runIds) {
|
|
1847
|
+
this.stmts.deleteIndexRun.run(runId);
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
tx();
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
deleteIndex(stream: string): void {
|
|
1854
|
+
const tx = this.db.transaction(() => {
|
|
1855
|
+
this.db.query(`DELETE FROM index_runs WHERE stream=?;`).run(stream);
|
|
1856
|
+
this.db.query(`DELETE FROM index_state WHERE stream=?;`).run(stream);
|
|
1857
|
+
});
|
|
1858
|
+
tx();
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
countUploadedSegments(stream: string): number {
|
|
1862
|
+
const row = this.stmts.countUploadedSegments.get(stream) as any;
|
|
1863
|
+
const maxIdx = row ? Number(row.max_idx) : -1;
|
|
1864
|
+
return maxIdx >= 0 ? maxIdx + 1 : 0;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
getSecondaryIndexState(stream: string, indexName: string): SecondaryIndexStateRow | null {
|
|
1868
|
+
const row = this.stmts.getSecondaryIndexState.get(stream, indexName) as any;
|
|
1869
|
+
if (!row) return null;
|
|
1870
|
+
return {
|
|
1871
|
+
stream: String(row.stream),
|
|
1872
|
+
index_name: String(row.index_name),
|
|
1873
|
+
index_secret: row.index_secret instanceof Uint8Array ? row.index_secret : new Uint8Array(row.index_secret),
|
|
1874
|
+
config_hash: String(row.config_hash ?? ""),
|
|
1875
|
+
indexed_through: Number(row.indexed_through),
|
|
1876
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
listSecondaryIndexStates(stream: string): SecondaryIndexStateRow[] {
|
|
1881
|
+
const rows = this.stmts.listSecondaryIndexStates.all(stream) as any[];
|
|
1882
|
+
return rows.map((row) => ({
|
|
1883
|
+
stream: String(row.stream),
|
|
1884
|
+
index_name: String(row.index_name),
|
|
1885
|
+
index_secret: row.index_secret instanceof Uint8Array ? row.index_secret : new Uint8Array(row.index_secret),
|
|
1886
|
+
config_hash: String(row.config_hash ?? ""),
|
|
1887
|
+
indexed_through: Number(row.indexed_through),
|
|
1888
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
1889
|
+
}));
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
upsertSecondaryIndexState(
|
|
1893
|
+
stream: string,
|
|
1894
|
+
indexName: string,
|
|
1895
|
+
indexSecret: Uint8Array,
|
|
1896
|
+
configHash: string,
|
|
1897
|
+
indexedThrough: number
|
|
1898
|
+
): void {
|
|
1899
|
+
this.stmts.upsertSecondaryIndexState.run(stream, indexName, indexSecret, configHash, indexedThrough, this.nowMs());
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
updateSecondaryIndexedThrough(stream: string, indexName: string, indexedThrough: number): void {
|
|
1903
|
+
this.stmts.updateSecondaryIndexedThrough.run(indexedThrough, this.nowMs(), stream, indexName);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
listSecondaryIndexRuns(stream: string, indexName: string): SecondaryIndexRunRow[] {
|
|
1907
|
+
const rows = this.stmts.listSecondaryIndexRuns.all(stream, indexName) as any[];
|
|
1908
|
+
return rows.map((r) => ({
|
|
1909
|
+
run_id: String(r.run_id),
|
|
1910
|
+
stream: String(r.stream),
|
|
1911
|
+
index_name: String(r.index_name),
|
|
1912
|
+
level: Number(r.level),
|
|
1913
|
+
start_segment: Number(r.start_segment),
|
|
1914
|
+
end_segment: Number(r.end_segment),
|
|
1915
|
+
object_key: String(r.object_key),
|
|
1916
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1917
|
+
filter_len: Number(r.filter_len),
|
|
1918
|
+
record_count: Number(r.record_count),
|
|
1919
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1920
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1921
|
+
}));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
listSecondaryIndexRunsAll(stream: string, indexName: string): SecondaryIndexRunRow[] {
|
|
1925
|
+
const rows = this.stmts.listSecondaryIndexRunsAll.all(stream, indexName) as any[];
|
|
1926
|
+
return rows.map((r) => ({
|
|
1927
|
+
run_id: String(r.run_id),
|
|
1928
|
+
stream: String(r.stream),
|
|
1929
|
+
index_name: String(r.index_name),
|
|
1930
|
+
level: Number(r.level),
|
|
1931
|
+
start_segment: Number(r.start_segment),
|
|
1932
|
+
end_segment: Number(r.end_segment),
|
|
1933
|
+
object_key: String(r.object_key),
|
|
1934
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1935
|
+
filter_len: Number(r.filter_len),
|
|
1936
|
+
record_count: Number(r.record_count),
|
|
1937
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1938
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1939
|
+
}));
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
listRetiredSecondaryIndexRuns(stream: string, indexName: string): SecondaryIndexRunRow[] {
|
|
1943
|
+
const rows = this.stmts.listRetiredSecondaryIndexRuns.all(stream, indexName) as any[];
|
|
1944
|
+
return rows.map((r) => ({
|
|
1945
|
+
run_id: String(r.run_id),
|
|
1946
|
+
stream: String(r.stream),
|
|
1947
|
+
index_name: String(r.index_name),
|
|
1948
|
+
level: Number(r.level),
|
|
1949
|
+
start_segment: Number(r.start_segment),
|
|
1950
|
+
end_segment: Number(r.end_segment),
|
|
1951
|
+
object_key: String(r.object_key),
|
|
1952
|
+
size_bytes: Number(r.size_bytes ?? 0),
|
|
1953
|
+
filter_len: Number(r.filter_len),
|
|
1954
|
+
record_count: Number(r.record_count),
|
|
1955
|
+
retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
|
|
1956
|
+
retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
|
|
1957
|
+
}));
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
insertSecondaryIndexRun(row: Omit<SecondaryIndexRunRow, "retired_gen" | "retired_at_ms">): void {
|
|
1961
|
+
this.stmts.insertSecondaryIndexRun.run(
|
|
1962
|
+
row.run_id,
|
|
1963
|
+
row.stream,
|
|
1964
|
+
row.index_name,
|
|
1965
|
+
row.level,
|
|
1966
|
+
row.start_segment,
|
|
1967
|
+
row.end_segment,
|
|
1968
|
+
row.object_key,
|
|
1969
|
+
row.size_bytes,
|
|
1970
|
+
row.filter_len,
|
|
1971
|
+
row.record_count
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
retireSecondaryIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): void {
|
|
1976
|
+
if (runIds.length === 0) return;
|
|
1977
|
+
const tx = this.db.transaction(() => {
|
|
1978
|
+
for (const runId of runIds) {
|
|
1979
|
+
this.stmts.retireSecondaryIndexRun.run(retiredGen, retiredAtMs, runId);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
tx();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
deleteSecondaryIndexRuns(runIds: string[]): void {
|
|
1986
|
+
if (runIds.length === 0) return;
|
|
1987
|
+
const tx = this.db.transaction(() => {
|
|
1988
|
+
for (const runId of runIds) {
|
|
1989
|
+
this.stmts.deleteSecondaryIndexRun.run(runId);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
tx();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
deleteSecondaryIndex(stream: string, indexName: string): void {
|
|
1996
|
+
const tx = this.db.transaction(() => {
|
|
1997
|
+
this.stmts.deleteSecondaryIndexRunsForIndex.run(stream, indexName);
|
|
1998
|
+
this.stmts.deleteSecondaryIndexState.run(stream, indexName);
|
|
1999
|
+
});
|
|
2000
|
+
tx();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
getLexiconIndexState(stream: string, sourceKind: string, sourceName: string): LexiconIndexStateRow | null {
|
|
2004
|
+
const row = this.stmts.getLexiconIndexState.get(stream, sourceKind, sourceName) as any;
|
|
2005
|
+
if (!row) return null;
|
|
2006
|
+
return {
|
|
2007
|
+
stream: String(row.stream),
|
|
2008
|
+
source_kind: String(row.source_kind),
|
|
2009
|
+
source_name: String(row.source_name),
|
|
2010
|
+
indexed_through: Number(row.indexed_through),
|
|
2011
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
listLexiconIndexStates(stream: string): LexiconIndexStateRow[] {
|
|
2016
|
+
const rows = this.stmts.listLexiconIndexStates.all(stream) as any[];
|
|
2017
|
+
return rows.map((row) => ({
|
|
2018
|
+
stream: String(row.stream),
|
|
2019
|
+
source_kind: String(row.source_kind),
|
|
2020
|
+
source_name: String(row.source_name),
|
|
2021
|
+
indexed_through: Number(row.indexed_through),
|
|
2022
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
2023
|
+
}));
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
upsertLexiconIndexState(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): void {
|
|
2027
|
+
this.stmts.upsertLexiconIndexState.run(stream, sourceKind, sourceName, indexedThrough, this.nowMs());
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
updateLexiconIndexedThrough(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): void {
|
|
2031
|
+
this.stmts.updateLexiconIndexedThrough.run(indexedThrough, this.nowMs(), stream, sourceKind, sourceName);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
listLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
|
|
2035
|
+
const rows = this.stmts.listLexiconIndexRuns.all(stream, sourceKind, sourceName) as any[];
|
|
2036
|
+
return rows.map((row) => ({
|
|
2037
|
+
run_id: String(row.run_id),
|
|
2038
|
+
stream: String(row.stream),
|
|
2039
|
+
source_kind: String(row.source_kind),
|
|
2040
|
+
source_name: String(row.source_name),
|
|
2041
|
+
level: Number(row.level),
|
|
2042
|
+
start_segment: Number(row.start_segment),
|
|
2043
|
+
end_segment: Number(row.end_segment),
|
|
2044
|
+
object_key: String(row.object_key),
|
|
2045
|
+
size_bytes: Number(row.size_bytes ?? 0),
|
|
2046
|
+
record_count: Number(row.record_count ?? 0),
|
|
2047
|
+
retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
|
|
2048
|
+
retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
listLexiconIndexRunsAll(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
|
|
2053
|
+
const rows = this.stmts.listLexiconIndexRunsAll.all(stream, sourceKind, sourceName) as any[];
|
|
2054
|
+
return rows.map((row) => ({
|
|
2055
|
+
run_id: String(row.run_id),
|
|
2056
|
+
stream: String(row.stream),
|
|
2057
|
+
source_kind: String(row.source_kind),
|
|
2058
|
+
source_name: String(row.source_name),
|
|
2059
|
+
level: Number(row.level),
|
|
2060
|
+
start_segment: Number(row.start_segment),
|
|
2061
|
+
end_segment: Number(row.end_segment),
|
|
2062
|
+
object_key: String(row.object_key),
|
|
2063
|
+
size_bytes: Number(row.size_bytes ?? 0),
|
|
2064
|
+
record_count: Number(row.record_count ?? 0),
|
|
2065
|
+
retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
|
|
2066
|
+
retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
|
|
2067
|
+
}));
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
listRetiredLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
|
|
2071
|
+
const rows = this.stmts.listRetiredLexiconIndexRuns.all(stream, sourceKind, sourceName) as any[];
|
|
2072
|
+
return rows.map((row) => ({
|
|
2073
|
+
run_id: String(row.run_id),
|
|
2074
|
+
stream: String(row.stream),
|
|
2075
|
+
source_kind: String(row.source_kind),
|
|
2076
|
+
source_name: String(row.source_name),
|
|
2077
|
+
level: Number(row.level),
|
|
2078
|
+
start_segment: Number(row.start_segment),
|
|
2079
|
+
end_segment: Number(row.end_segment),
|
|
2080
|
+
object_key: String(row.object_key),
|
|
2081
|
+
size_bytes: Number(row.size_bytes ?? 0),
|
|
2082
|
+
record_count: Number(row.record_count ?? 0),
|
|
2083
|
+
retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
|
|
2084
|
+
retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
|
|
2085
|
+
}));
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
insertLexiconIndexRun(row: Omit<LexiconIndexRunRow, "retired_gen" | "retired_at_ms">): void {
|
|
2089
|
+
this.stmts.insertLexiconIndexRun.run(
|
|
2090
|
+
row.run_id,
|
|
2091
|
+
row.stream,
|
|
2092
|
+
row.source_kind,
|
|
2093
|
+
row.source_name,
|
|
2094
|
+
row.level,
|
|
2095
|
+
row.start_segment,
|
|
2096
|
+
row.end_segment,
|
|
2097
|
+
row.object_key,
|
|
2098
|
+
row.size_bytes,
|
|
2099
|
+
row.record_count
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
retireLexiconIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): void {
|
|
2104
|
+
if (runIds.length === 0) return;
|
|
2105
|
+
const tx = this.db.transaction(() => {
|
|
2106
|
+
for (const runId of runIds) {
|
|
2107
|
+
this.stmts.retireLexiconIndexRun.run(retiredGen, retiredAtMs, runId);
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
tx();
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
deleteLexiconIndexRuns(runIds: string[]): void {
|
|
2114
|
+
if (runIds.length === 0) return;
|
|
2115
|
+
const tx = this.db.transaction(() => {
|
|
2116
|
+
for (const runId of runIds) {
|
|
2117
|
+
this.stmts.deleteLexiconIndexRun.run(runId);
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
tx();
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
deleteLexiconIndexSource(stream: string, sourceKind: string, sourceName: string): void {
|
|
2124
|
+
const tx = this.db.transaction(() => {
|
|
2125
|
+
this.stmts.deleteLexiconIndexRunsForSource.run(stream, sourceKind, sourceName);
|
|
2126
|
+
this.stmts.deleteLexiconIndexState.run(stream, sourceKind, sourceName);
|
|
2127
|
+
});
|
|
2128
|
+
tx();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
getSearchCompanionPlan(stream: string): SearchCompanionPlanRow | null {
|
|
2132
|
+
const row = this.stmts.getSearchCompanionPlan.get(stream) as any;
|
|
2133
|
+
if (!row) return null;
|
|
2134
|
+
return {
|
|
2135
|
+
stream: String(row.stream),
|
|
2136
|
+
generation: Number(row.generation),
|
|
2137
|
+
plan_hash: String(row.plan_hash),
|
|
2138
|
+
plan_json: String(row.plan_json),
|
|
2139
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
listSearchCompanionPlanStreams(): string[] {
|
|
2144
|
+
const rows = this.stmts.listSearchCompanionPlanStreams.all() as any[];
|
|
2145
|
+
return rows.map((row) => String(row.stream));
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
upsertSearchCompanionPlan(stream: string, generation: number, planHash: string, planJson: string): void {
|
|
2149
|
+
this.stmts.upsertSearchCompanionPlan.run(stream, generation, planHash, planJson, this.nowMs());
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
deleteSearchCompanionPlan(stream: string): void {
|
|
2153
|
+
this.stmts.deleteSearchCompanionPlan.run(stream);
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
listSearchSegmentCompanions(stream: string): SearchSegmentCompanionRow[] {
|
|
2157
|
+
const rows = this.stmts.listSearchSegmentCompanions.all(stream) as any[];
|
|
2158
|
+
return rows.map((row) => ({
|
|
2159
|
+
stream: String(row.stream),
|
|
2160
|
+
segment_index: Number(row.segment_index),
|
|
2161
|
+
object_key: String(row.object_key),
|
|
2162
|
+
plan_generation: Number(row.plan_generation),
|
|
2163
|
+
sections_json: String(row.sections_json),
|
|
2164
|
+
section_sizes_json: String(row.section_sizes_json ?? "{}"),
|
|
2165
|
+
size_bytes: Number(row.size_bytes ?? 0),
|
|
2166
|
+
primary_timestamp_min_ms: row.primary_timestamp_min_ms == null ? null : this.toBigInt(row.primary_timestamp_min_ms),
|
|
2167
|
+
primary_timestamp_max_ms: row.primary_timestamp_max_ms == null ? null : this.toBigInt(row.primary_timestamp_max_ms),
|
|
2168
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
2169
|
+
}));
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
getSearchSegmentCompanion(stream: string, segmentIndex: number): SearchSegmentCompanionRow | null {
|
|
2173
|
+
const row = this.stmts.getSearchSegmentCompanion.get(stream, segmentIndex) as any;
|
|
2174
|
+
if (!row) return null;
|
|
2175
|
+
return {
|
|
2176
|
+
stream: String(row.stream),
|
|
2177
|
+
segment_index: Number(row.segment_index),
|
|
2178
|
+
object_key: String(row.object_key),
|
|
2179
|
+
plan_generation: Number(row.plan_generation),
|
|
2180
|
+
sections_json: String(row.sections_json),
|
|
2181
|
+
section_sizes_json: String(row.section_sizes_json ?? "{}"),
|
|
2182
|
+
size_bytes: Number(row.size_bytes ?? 0),
|
|
2183
|
+
primary_timestamp_min_ms: row.primary_timestamp_min_ms == null ? null : this.toBigInt(row.primary_timestamp_min_ms),
|
|
2184
|
+
primary_timestamp_max_ms: row.primary_timestamp_max_ms == null ? null : this.toBigInt(row.primary_timestamp_max_ms),
|
|
2185
|
+
updated_at_ms: this.toBigInt(row.updated_at_ms),
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
upsertSearchSegmentCompanion(
|
|
2190
|
+
stream: string,
|
|
2191
|
+
segmentIndex: number,
|
|
2192
|
+
objectKey: string,
|
|
2193
|
+
planGeneration: number,
|
|
2194
|
+
sectionsJson: string,
|
|
2195
|
+
sectionSizesJson: string,
|
|
2196
|
+
sizeBytes: number,
|
|
2197
|
+
primaryTimestampMinMs: bigint | null,
|
|
2198
|
+
primaryTimestampMaxMs: bigint | null
|
|
2199
|
+
): void {
|
|
2200
|
+
this.stmts.upsertSearchSegmentCompanion.run(
|
|
2201
|
+
stream,
|
|
2202
|
+
segmentIndex,
|
|
2203
|
+
objectKey,
|
|
2204
|
+
planGeneration,
|
|
2205
|
+
sectionsJson,
|
|
2206
|
+
sectionSizesJson,
|
|
2207
|
+
sizeBytes,
|
|
2208
|
+
primaryTimestampMinMs,
|
|
2209
|
+
primaryTimestampMaxMs,
|
|
2210
|
+
this.nowMs()
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
deleteSearchSegmentCompanionsBeforeGeneration(stream: string, generation: number): void {
|
|
2215
|
+
this.stmts.deleteSearchSegmentCompanionsFromGeneration.run(stream, generation);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
deleteSearchSegmentCompanionsFrom(stream: string, segmentIndex: number): void {
|
|
2219
|
+
this.stmts.deleteSearchSegmentCompanionsFromIndex.run(stream, segmentIndex);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
deleteSearchSegmentCompanions(stream: string): void {
|
|
2223
|
+
this.stmts.deleteSearchSegmentCompanions.run(stream);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
commitManifest(
|
|
2227
|
+
stream: string,
|
|
2228
|
+
generation: number,
|
|
2229
|
+
etag: string,
|
|
2230
|
+
uploadedAtMs: bigint,
|
|
2231
|
+
uploadedThrough: bigint,
|
|
2232
|
+
sizeBytes: number
|
|
2233
|
+
): void {
|
|
2234
|
+
const tx = this.db.transaction(() => {
|
|
2235
|
+
this.stmts.upsertManifest.run(stream, generation, generation, uploadedAtMs, etag, sizeBytes);
|
|
2236
|
+
this.stmts.advanceUploadedThrough.run(uploadedThrough, this.nowMs(), stream);
|
|
2237
|
+
let gcThrough = uploadedThrough;
|
|
2238
|
+
const touchState = this.touch.getStreamTouchState(stream);
|
|
2239
|
+
if (touchState) {
|
|
2240
|
+
const processedThrough = touchState.processed_through;
|
|
2241
|
+
gcThrough = processedThrough < gcThrough ? processedThrough : gcThrough;
|
|
2242
|
+
}
|
|
2243
|
+
if (gcThrough < 0n) return;
|
|
2244
|
+
|
|
2245
|
+
const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, gcThrough, {
|
|
2246
|
+
maxRows: BASE_WAL_GC_CHUNK_OFFSETS,
|
|
2247
|
+
});
|
|
2248
|
+
if (rows <= 0n) return;
|
|
2249
|
+
|
|
2250
|
+
// Keep retained-WAL counters consistent for metrics/debugging.
|
|
2251
|
+
const now = this.nowMs();
|
|
2252
|
+
this.db.query(
|
|
2253
|
+
`UPDATE streams
|
|
2254
|
+
SET wal_bytes = CASE WHEN wal_bytes >= ? THEN wal_bytes - ? ELSE 0 END,
|
|
2255
|
+
wal_rows = CASE WHEN wal_rows >= ? THEN wal_rows - ? ELSE 0 END,
|
|
2256
|
+
updated_at_ms = ?
|
|
2257
|
+
WHERE stream = ?;`
|
|
2258
|
+
).run(bytes, bytes, rows, rows, now, stream);
|
|
2259
|
+
});
|
|
2260
|
+
tx();
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
recordObjectStoreRequestByHash(streamHash: string, artifact: string, op: string, bytes = 0, count = 1): void {
|
|
2264
|
+
if (!streamHash || !artifact || !op) return;
|
|
2265
|
+
this.stmts.recordObjectStoreRequest.run(streamHash, artifact, op, count, bytes, this.nowMs());
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
getObjectStoreRequestSummaryByHash(streamHash: string): ObjectStoreRequestSummary {
|
|
2269
|
+
const rows = this.db
|
|
2270
|
+
.query(
|
|
2271
|
+
`SELECT artifact, op, count
|
|
2272
|
+
FROM objectstore_request_counts
|
|
2273
|
+
WHERE stream_hash=?
|
|
2274
|
+
ORDER BY artifact ASC, op ASC;`
|
|
2275
|
+
)
|
|
2276
|
+
.all(streamHash) as any[];
|
|
2277
|
+
return summarizeObjectStoreRequestCounts(
|
|
2278
|
+
rows.map(
|
|
2279
|
+
(row): ObjectStoreRequestCountRow => ({
|
|
2280
|
+
artifact: String(row.artifact),
|
|
2281
|
+
op: String(row.op),
|
|
2282
|
+
count: this.toBigInt(row.count ?? 0),
|
|
2283
|
+
})
|
|
2284
|
+
)
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
getUploadedSegmentBytes(stream: string): bigint {
|
|
2289
|
+
const row = this.db
|
|
2290
|
+
.query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE stream=? AND r2_etag IS NOT NULL;`)
|
|
2291
|
+
.get(stream) as any;
|
|
2292
|
+
return this.toBigInt(row?.total ?? 0);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
getPendingSealedSegmentBytes(stream: string): bigint {
|
|
2296
|
+
const row = this.db
|
|
2297
|
+
.query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE stream=? AND uploaded_at_ms IS NULL;`)
|
|
2298
|
+
.get(stream) as any;
|
|
2299
|
+
return this.toBigInt(row?.total ?? 0);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
getRoutingIndexStorage(stream: string): { object_count: number; bytes: bigint } {
|
|
2303
|
+
const row = this.db
|
|
2304
|
+
.query(`SELECT COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total FROM index_runs WHERE stream=?;`)
|
|
2305
|
+
.get(stream) as any;
|
|
2306
|
+
return {
|
|
2307
|
+
object_count: Number(row?.cnt ?? 0),
|
|
2308
|
+
bytes: this.toBigInt(row?.total ?? 0),
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
getSecondaryIndexStorage(stream: string): Array<{ index_name: string; object_count: number; bytes: bigint }> {
|
|
2313
|
+
const rows = this.db
|
|
2314
|
+
.query(
|
|
2315
|
+
`SELECT index_name, COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total
|
|
2316
|
+
FROM secondary_index_runs
|
|
2317
|
+
WHERE stream=?
|
|
2318
|
+
GROUP BY index_name
|
|
2319
|
+
ORDER BY index_name ASC;`
|
|
2320
|
+
)
|
|
2321
|
+
.all(stream) as any[];
|
|
2322
|
+
return rows.map((row) => ({
|
|
2323
|
+
index_name: String(row.index_name),
|
|
2324
|
+
object_count: Number(row.cnt ?? 0),
|
|
2325
|
+
bytes: this.toBigInt(row.total ?? 0),
|
|
2326
|
+
}));
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
getLexiconIndexStorage(
|
|
2330
|
+
stream: string
|
|
2331
|
+
): Array<{ source_kind: string; source_name: string; object_count: number; bytes: bigint }> {
|
|
2332
|
+
const rows = this.db
|
|
2333
|
+
.query(
|
|
2334
|
+
`SELECT source_kind, source_name, COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total
|
|
2335
|
+
FROM lexicon_index_runs
|
|
2336
|
+
WHERE stream=?
|
|
2337
|
+
GROUP BY source_kind, source_name
|
|
2338
|
+
ORDER BY source_kind ASC, source_name ASC;`
|
|
2339
|
+
)
|
|
2340
|
+
.all(stream) as any[];
|
|
2341
|
+
return rows.map((row) => ({
|
|
2342
|
+
source_kind: String(row.source_kind),
|
|
2343
|
+
source_name: String(row.source_name),
|
|
2344
|
+
object_count: Number(row.cnt ?? 0),
|
|
2345
|
+
bytes: this.toBigInt(row.total ?? 0),
|
|
2346
|
+
}));
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
getBundledCompanionStorage(stream: string): { object_count: number; bytes: bigint } {
|
|
2350
|
+
const row = this.db
|
|
2351
|
+
.query(`SELECT COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total FROM search_segment_companions WHERE stream=?;`)
|
|
2352
|
+
.get(stream) as any;
|
|
2353
|
+
return {
|
|
2354
|
+
object_count: Number(row?.cnt ?? 0),
|
|
2355
|
+
bytes: this.toBigInt(row?.total ?? 0),
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
getSegmentLastAppendMsFromMeta(stream: string, segmentIndex: number): bigint | null {
|
|
2360
|
+
const meta = this.getSegmentMeta(stream);
|
|
2361
|
+
if (!meta) return null;
|
|
2362
|
+
if (segmentIndex < 0 || segmentIndex >= meta.segment_count) return null;
|
|
2363
|
+
const off = segmentIndex * 8;
|
|
2364
|
+
if (off + 8 > meta.segment_last_ts.byteLength) return null;
|
|
2365
|
+
const dv = new DataView(meta.segment_last_ts.buffer, meta.segment_last_ts.byteOffset, meta.segment_last_ts.byteLength);
|
|
2366
|
+
return dv.getBigUint64(off, true) / 1_000_000n;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
getFullModeDetailsSnapshot(request: FullModeDetailsSnapshotRequest): FullModeDetailsSnapshot {
|
|
2370
|
+
const stream = request.stream;
|
|
2371
|
+
const segmentCount = this.countSegmentsForStream(stream);
|
|
2372
|
+
const uploadedSegmentCount = this.countUploadedSegments(stream);
|
|
2373
|
+
return {
|
|
2374
|
+
segmentCount,
|
|
2375
|
+
uploadedSegmentCount,
|
|
2376
|
+
manifest: this.getManifestRow(stream),
|
|
2377
|
+
schemaRow: this.getSchemaRegistry(stream),
|
|
2378
|
+
uploadedSegmentBytes: this.getUploadedSegmentBytes(stream),
|
|
2379
|
+
pendingSealedSegmentBytes: this.getPendingSealedSegmentBytes(stream),
|
|
2380
|
+
routingIndexStorage: this.getRoutingIndexStorage(stream),
|
|
2381
|
+
secondaryIndexStorage: this.getSecondaryIndexStorage(stream),
|
|
2382
|
+
lexiconIndexStorage: this.getLexiconIndexStorage(stream),
|
|
2383
|
+
bundledCompanionStorage: this.getBundledCompanionStorage(stream),
|
|
2384
|
+
routingState: this.getIndexState(stream),
|
|
2385
|
+
routingRuns: this.listIndexRuns(stream),
|
|
2386
|
+
retiredRoutingRuns: this.listRetiredIndexRuns(stream),
|
|
2387
|
+
exactIndexes: request.exactIndexNames.map((indexName) => ({
|
|
2388
|
+
indexName,
|
|
2389
|
+
state: this.getSecondaryIndexState(stream, indexName),
|
|
2390
|
+
activeRuns: this.listSecondaryIndexRuns(stream, indexName),
|
|
2391
|
+
retiredRuns: this.listRetiredSecondaryIndexRuns(stream, indexName),
|
|
2392
|
+
})),
|
|
2393
|
+
routingLexiconState: this.getLexiconIndexState(stream, "routing_key", ""),
|
|
2394
|
+
routingLexiconRuns: this.listLexiconIndexRuns(stream, "routing_key", ""),
|
|
2395
|
+
retiredRoutingLexiconRuns: this.listRetiredLexiconIndexRuns(stream, "routing_key", ""),
|
|
2396
|
+
companionPlan: this.getSearchCompanionPlan(stream),
|
|
2397
|
+
companionRows: this.listSearchSegmentCompanions(stream),
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
getFullModeLagSnapshot(request: FullModeLagSnapshotRequest): Map<number, bigint> {
|
|
2402
|
+
const out = new Map<number, bigint>();
|
|
2403
|
+
const sorted = Array.from(new Set(request.segmentIndexes.filter((index) => Number.isInteger(index) && index >= 0))).sort((a, b) => a - b);
|
|
2404
|
+
for (const index of sorted) {
|
|
2405
|
+
const lastAppendMs = this.getSegmentLastAppendMsFromMeta(request.stream, index);
|
|
2406
|
+
if (lastAppendMs != null) out.set(index, lastAppendMs);
|
|
2407
|
+
}
|
|
2408
|
+
return out;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/** Find candidates by bytes/rows/interval. */
|
|
2412
|
+
candidates(
|
|
2413
|
+
minPendingBytes: bigint,
|
|
2414
|
+
minPendingRows: bigint,
|
|
2415
|
+
maxIntervalMs: bigint,
|
|
2416
|
+
limit: number
|
|
2417
|
+
): Array<{ stream: string; pending_bytes: bigint; pending_rows: bigint; last_segment_cut_ms: bigint; sealed_through: bigint; next_offset: bigint; epoch: number }> {
|
|
2418
|
+
if (maxIntervalMs <= 0n) {
|
|
2419
|
+
return this.stmts.candidateStreamsNoInterval.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, minPendingBytes, minPendingRows, limit) as any;
|
|
2420
|
+
}
|
|
2421
|
+
const now = this.nowMs();
|
|
2422
|
+
return this.stmts.candidateStreams.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, minPendingBytes, minPendingRows, now, maxIntervalMs, limit) as any;
|
|
2423
|
+
}
|
|
2424
|
+
}
|