@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.
Files changed (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. 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
+ }