@prisma/streams-server 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime/host_runtime.ts +5 -0
  45. package/src/runtime_memory.ts +200 -0
  46. package/src/runtime_memory_sampler.ts +235 -0
  47. package/src/schema/read_json.ts +43 -0
  48. package/src/schema/registry.ts +563 -59
  49. package/src/search/agg_format.ts +638 -0
  50. package/src/search/aggregate.ts +389 -0
  51. package/src/search/binary/codec.ts +162 -0
  52. package/src/search/binary/docset.ts +67 -0
  53. package/src/search/binary/restart_strings.ts +181 -0
  54. package/src/search/binary/varint.ts +34 -0
  55. package/src/search/bitset.ts +19 -0
  56. package/src/search/col_format.ts +382 -0
  57. package/src/search/col_runtime.ts +59 -0
  58. package/src/search/column_encoding.ts +43 -0
  59. package/src/search/companion_file_cache.ts +319 -0
  60. package/src/search/companion_format.ts +313 -0
  61. package/src/search/companion_manager.ts +1086 -0
  62. package/src/search/companion_plan.ts +218 -0
  63. package/src/search/fts_format.ts +423 -0
  64. package/src/search/fts_runtime.ts +333 -0
  65. package/src/search/query.ts +875 -0
  66. package/src/search/schema.ts +245 -0
  67. package/src/segment/cache.ts +93 -2
  68. package/src/segment/cached_segment.ts +89 -0
  69. package/src/segment/format.ts +108 -36
  70. package/src/segment/segmenter.ts +79 -5
  71. package/src/segment/segmenter_worker.ts +35 -6
  72. package/src/segment/segmenter_workers.ts +42 -12
  73. package/src/server.ts +150 -36
  74. package/src/sqlite/adapter.ts +185 -14
  75. package/src/sqlite/runtime_stats.ts +163 -0
  76. package/src/stats.ts +3 -3
  77. package/src/stream_size_reconciler.ts +100 -0
  78. package/src/touch/canonical_change.ts +7 -0
  79. package/src/touch/live_metrics.ts +94 -64
  80. package/src/touch/live_templates.ts +15 -1
  81. package/src/touch/manager.ts +166 -88
  82. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
  83. package/src/touch/spec.ts +95 -92
  84. package/src/touch/touch_journal.ts +4 -0
  85. package/src/touch/worker_pool.ts +8 -14
  86. package/src/touch/worker_protocol.ts +3 -3
  87. package/src/uploader.ts +77 -6
  88. package/src/util/bloom256.ts +2 -2
  89. package/src/util/byte_lru.ts +73 -0
  90. package/src/util/lru.ts +8 -0
  91. package/src/util/stream_paths.ts +19 -0
package/src/db/db.ts CHANGED
@@ -8,9 +8,9 @@ export const STREAM_FLAG_TOUCH = 1 << 1;
8
8
 
9
9
  const BASE_WAL_GC_CHUNK_OFFSETS = (() => {
10
10
  const raw = process.env.DS_BASE_WAL_GC_CHUNK_OFFSETS;
11
- if (raw == null || raw.trim() === "") return 100_000;
11
+ if (raw == null || raw.trim() === "") return 1_000_000;
12
12
  const n = Number(raw);
13
- if (!Number.isFinite(n) || n <= 0) return 100_000;
13
+ if (!Number.isFinite(n) || n <= 0) return 1_000_000;
14
14
  return Math.floor(n);
15
15
  })();
16
16
 
@@ -19,6 +19,7 @@ export type StreamRow = {
19
19
  created_at_ms: bigint;
20
20
  updated_at_ms: bigint;
21
21
  content_type: string;
22
+ profile: string | null;
22
23
  stream_seq: string | null;
23
24
  closed: number;
24
25
  closed_producer_id: string | null;
@@ -32,6 +33,7 @@ export type StreamRow = {
32
33
  uploaded_segment_count: number;
33
34
  pending_rows: bigint;
34
35
  pending_bytes: bigint;
36
+ logical_size_bytes: bigint;
35
37
  wal_rows: bigint;
36
38
  wal_bytes: bigint;
37
39
  last_append_ms: bigint;
@@ -49,6 +51,7 @@ export type SegmentRow = {
49
51
  end_offset: bigint;
50
52
  block_count: number;
51
53
  last_append_ms: bigint;
54
+ payload_bytes: bigint;
52
55
  size_bytes: number;
53
56
  local_path: string;
54
57
  created_at_ms: bigint;
@@ -78,12 +81,81 @@ export type IndexRunRow = {
78
81
  start_segment: number;
79
82
  end_segment: number;
80
83
  object_key: string;
84
+ size_bytes: number;
85
+ filter_len: number;
86
+ record_count: number;
87
+ retired_gen: number | null;
88
+ retired_at_ms: bigint | null;
89
+ };
90
+
91
+ export type SecondaryIndexStateRow = {
92
+ stream: string;
93
+ index_name: string;
94
+ index_secret: Uint8Array;
95
+ config_hash: string;
96
+ indexed_through: number;
97
+ updated_at_ms: bigint;
98
+ };
99
+
100
+ export type SecondaryIndexRunRow = {
101
+ run_id: string;
102
+ stream: string;
103
+ index_name: string;
104
+ level: number;
105
+ start_segment: number;
106
+ end_segment: number;
107
+ object_key: string;
108
+ size_bytes: number;
81
109
  filter_len: number;
82
110
  record_count: number;
83
111
  retired_gen: number | null;
84
112
  retired_at_ms: bigint | null;
85
113
  };
86
114
 
115
+ export type LexiconIndexStateRow = {
116
+ stream: string;
117
+ source_kind: string;
118
+ source_name: string;
119
+ indexed_through: number;
120
+ updated_at_ms: bigint;
121
+ };
122
+
123
+ export type LexiconIndexRunRow = {
124
+ run_id: string;
125
+ stream: string;
126
+ source_kind: string;
127
+ source_name: string;
128
+ level: number;
129
+ start_segment: number;
130
+ end_segment: number;
131
+ object_key: string;
132
+ size_bytes: number;
133
+ record_count: number;
134
+ retired_gen: number | null;
135
+ retired_at_ms: bigint | null;
136
+ };
137
+
138
+ export type SearchCompanionPlanRow = {
139
+ stream: string;
140
+ generation: number;
141
+ plan_hash: string;
142
+ plan_json: string;
143
+ updated_at_ms: bigint;
144
+ };
145
+
146
+ export type SearchSegmentCompanionRow = {
147
+ stream: string;
148
+ segment_index: number;
149
+ object_key: string;
150
+ plan_generation: number;
151
+ sections_json: string;
152
+ section_sizes_json: string;
153
+ size_bytes: number;
154
+ primary_timestamp_min_ms: bigint | null;
155
+ primary_timestamp_max_ms: bigint | null;
156
+ updated_at_ms: bigint;
157
+ };
158
+
87
159
  export class SqliteDurableStore {
88
160
  public readonly db: SqliteDatabase;
89
161
  private dbstatReady: boolean | null = null;
@@ -93,7 +165,9 @@ export class SqliteDurableStore {
93
165
  getStream: SqliteStatement;
94
166
  upsertStream: SqliteStatement;
95
167
  listStreams: SqliteStatement;
168
+ listDeletedStreams: SqliteStatement;
96
169
  setDeleted: SqliteStatement;
170
+ setStreamProfile: SqliteStatement;
97
171
 
98
172
  insertWal: SqliteStatement;
99
173
 
@@ -106,6 +180,8 @@ export class SqliteDurableStore {
106
180
 
107
181
  streamWalRange: SqliteStatement;
108
182
  streamWalRangeByKey: SqliteStatement;
183
+ streamWalRangeDesc: SqliteStatement;
184
+ streamWalRangeDescByKey: SqliteStatement;
109
185
 
110
186
  createSegment: SqliteStatement;
111
187
  listSegmentsForStream: SqliteStatement;
@@ -113,13 +189,16 @@ export class SqliteDurableStore {
113
189
  findSegmentForOffset: SqliteStatement;
114
190
  nextSegmentIndex: SqliteStatement;
115
191
  markSegmentUploaded: SqliteStatement;
116
- pendingUploadSegments: SqliteStatement;
192
+ pendingUploadHeads: SqliteStatement;
193
+ recentSegmentCompressionWindow: SqliteStatement;
117
194
  countPendingSegments: SqliteStatement;
118
195
  tryClaimSegment: SqliteStatement;
119
196
  countSegmentsForStream: SqliteStatement;
120
197
 
121
198
  getManifest: SqliteStatement;
122
199
  upsertManifest: SqliteStatement;
200
+ setSchemaUploadedSize: SqliteStatement;
201
+ recordObjectStoreRequest: SqliteStatement;
123
202
 
124
203
  getIndexState: SqliteStatement;
125
204
  upsertIndexState: SqliteStatement;
@@ -130,6 +209,46 @@ export class SqliteDurableStore {
130
209
  insertIndexRun: SqliteStatement;
131
210
  retireIndexRun: SqliteStatement;
132
211
  deleteIndexRun: SqliteStatement;
212
+ deleteIndexStateForStream: SqliteStatement;
213
+ deleteIndexRunsForStream: SqliteStatement;
214
+ getSecondaryIndexState: SqliteStatement;
215
+ listSecondaryIndexStates: SqliteStatement;
216
+ upsertSecondaryIndexState: SqliteStatement;
217
+ updateSecondaryIndexedThrough: SqliteStatement;
218
+ listSecondaryIndexRuns: SqliteStatement;
219
+ listSecondaryIndexRunsAll: SqliteStatement;
220
+ listRetiredSecondaryIndexRuns: SqliteStatement;
221
+ insertSecondaryIndexRun: SqliteStatement;
222
+ retireSecondaryIndexRun: SqliteStatement;
223
+ deleteSecondaryIndexRun: SqliteStatement;
224
+ deleteSecondaryIndexState: SqliteStatement;
225
+ deleteSecondaryIndexRunsForIndex: SqliteStatement;
226
+ deleteSecondaryIndexStatesForStream: SqliteStatement;
227
+ deleteSecondaryIndexRunsForStream: SqliteStatement;
228
+ getLexiconIndexState: SqliteStatement;
229
+ listLexiconIndexStates: SqliteStatement;
230
+ upsertLexiconIndexState: SqliteStatement;
231
+ updateLexiconIndexedThrough: SqliteStatement;
232
+ listLexiconIndexRuns: SqliteStatement;
233
+ listLexiconIndexRunsAll: SqliteStatement;
234
+ listRetiredLexiconIndexRuns: SqliteStatement;
235
+ insertLexiconIndexRun: SqliteStatement;
236
+ retireLexiconIndexRun: SqliteStatement;
237
+ deleteLexiconIndexRun: SqliteStatement;
238
+ deleteLexiconIndexState: SqliteStatement;
239
+ deleteLexiconIndexRunsForSource: SqliteStatement;
240
+ deleteLexiconIndexStatesForStream: SqliteStatement;
241
+ deleteLexiconIndexRunsForStream: SqliteStatement;
242
+ getSearchCompanionPlan: SqliteStatement;
243
+ listSearchCompanionPlanStreams: SqliteStatement;
244
+ upsertSearchCompanionPlan: SqliteStatement;
245
+ deleteSearchCompanionPlan: SqliteStatement;
246
+ listSearchSegmentCompanions: SqliteStatement;
247
+ getSearchSegmentCompanion: SqliteStatement;
248
+ upsertSearchSegmentCompanion: SqliteStatement;
249
+ deleteSearchSegmentCompanionsFromGeneration: SqliteStatement;
250
+ deleteSearchSegmentCompanionsFromIndex: SqliteStatement;
251
+ deleteSearchSegmentCompanions: SqliteStatement;
133
252
  countUploadedSegments: SqliteStatement;
134
253
  getSegmentMeta: SqliteStatement;
135
254
  ensureSegmentMeta: SqliteStatement;
@@ -138,14 +257,17 @@ export class SqliteDurableStore {
138
257
  setUploadedSegmentCount: SqliteStatement;
139
258
 
140
259
  advanceUploadedThrough: SqliteStatement;
141
- deleteWalBeforeOffset: SqliteStatement;
142
260
 
143
261
  getSchemaRegistry: SqliteStatement;
144
262
  upsertSchemaRegistry: SqliteStatement;
145
- getStreamInterpreter: SqliteStatement;
146
- upsertStreamInterpreter: SqliteStatement;
147
- deleteStreamInterpreter: SqliteStatement;
148
- listStreamInterpreters: SqliteStatement;
263
+ getStreamProfile: SqliteStatement;
264
+ upsertStreamProfile: SqliteStatement;
265
+ deleteStreamProfile: SqliteStatement;
266
+ getStreamTouchState: SqliteStatement;
267
+ upsertStreamTouchState: SqliteStatement;
268
+ deleteStreamTouchState: SqliteStatement;
269
+ listStreamTouchStates: SqliteStatement;
270
+ listStreamsByProfile: SqliteStatement;
149
271
  countStreams: SqliteStatement;
150
272
  sumPendingBytes: SqliteStatement;
151
273
  sumPendingSegmentBytes: SqliteStatement;
@@ -162,31 +284,32 @@ export class SqliteDurableStore {
162
284
  this.stmts = {
163
285
  getStream: this.db.query(
164
286
  `SELECT stream, created_at_ms, updated_at_ms,
165
- content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
287
+ content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
166
288
  epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
167
- pending_rows, pending_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
289
+ pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
168
290
  expires_at_ms, stream_flags
169
291
  FROM streams WHERE stream = ? LIMIT 1;`
170
292
  ),
171
293
  upsertStream: this.db.query(
172
294
  `INSERT INTO streams(stream, created_at_ms, updated_at_ms,
173
- content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
295
+ content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
174
296
  epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
175
- pending_rows, pending_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
297
+ pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
176
298
  expires_at_ms, stream_flags)
177
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
299
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
178
300
  ON CONFLICT(stream) DO UPDATE SET
179
301
  updated_at_ms=excluded.updated_at_ms,
180
302
  expires_at_ms=excluded.expires_at_ms,
181
303
  ttl_seconds=excluded.ttl_seconds,
182
304
  content_type=excluded.content_type,
305
+ profile=excluded.profile,
183
306
  stream_flags=excluded.stream_flags;`
184
307
  ),
185
308
  listStreams: this.db.query(
186
309
  `SELECT stream, created_at_ms, updated_at_ms,
187
- content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
310
+ content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
188
311
  epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
189
- pending_rows, pending_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
312
+ pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
190
313
  expires_at_ms, stream_flags
191
314
  FROM streams
192
315
  WHERE (stream_flags & ?) = 0
@@ -194,7 +317,15 @@ export class SqliteDurableStore {
194
317
  ORDER BY stream
195
318
  LIMIT ? OFFSET ?;`
196
319
  ),
320
+ listDeletedStreams: this.db.query(
321
+ `SELECT stream
322
+ FROM streams
323
+ WHERE (stream_flags & ?) != 0
324
+ ORDER BY stream
325
+ LIMIT ? OFFSET ?;`
326
+ ),
197
327
  setDeleted: this.db.query(`UPDATE streams SET stream_flags = (stream_flags | ?), updated_at_ms=? WHERE stream=?;`),
328
+ setStreamProfile: this.db.query(`UPDATE streams SET profile=?, updated_at_ms=? WHERE stream=?;`),
198
329
 
199
330
  insertWal: this.db.query(
200
331
  `INSERT INTO wal(stream, offset, ts_ms, payload, payload_len, routing_key, content_type, flags)
@@ -205,6 +336,7 @@ export class SqliteDurableStore {
205
336
  `UPDATE streams
206
337
  SET next_offset = ?, updated_at_ms = ?, last_append_ms = ?,
207
338
  pending_rows = pending_rows + ?, pending_bytes = pending_bytes + ?,
339
+ logical_size_bytes = logical_size_bytes + ?,
208
340
  wal_rows = wal_rows + ?, wal_bytes = wal_bytes + ?
209
341
  WHERE stream = ? AND (stream_flags & ?) = 0;`
210
342
  ),
@@ -212,6 +344,7 @@ export class SqliteDurableStore {
212
344
  `UPDATE streams
213
345
  SET next_offset = ?, updated_at_ms = ?, last_append_ms = ?,
214
346
  pending_rows = pending_rows + ?, pending_bytes = pending_bytes + ?,
347
+ logical_size_bytes = logical_size_bytes + ?,
215
348
  wal_rows = wal_rows + ?, wal_bytes = wal_bytes + ?
216
349
  WHERE stream = ? AND (stream_flags & ?) = 0 AND next_offset = ?;`
217
350
  ),
@@ -256,24 +389,36 @@ export class SqliteDurableStore {
256
389
  WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?
257
390
  ORDER BY offset ASC;`
258
391
  ),
392
+ streamWalRangeDesc: this.db.query(
393
+ `SELECT offset, ts_ms, routing_key, content_type, payload
394
+ FROM wal
395
+ WHERE stream = ? AND offset >= ? AND offset <= ?
396
+ ORDER BY offset DESC;`
397
+ ),
398
+ streamWalRangeDescByKey: this.db.query(
399
+ `SELECT offset, ts_ms, routing_key, content_type, payload
400
+ FROM wal
401
+ WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?
402
+ ORDER BY offset DESC;`
403
+ ),
259
404
 
260
405
  createSegment: this.db.query(
261
406
  `INSERT INTO segments(segment_id, stream, segment_index, start_offset, end_offset, block_count,
262
- last_append_ms, size_bytes, local_path, created_at_ms, uploaded_at_ms, r2_etag)
263
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
407
+ last_append_ms, payload_bytes, size_bytes, local_path, created_at_ms, uploaded_at_ms, r2_etag)
408
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
264
409
  ),
265
410
  listSegmentsForStream: this.db.query(
266
- `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, size_bytes,
411
+ `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
267
412
  local_path, created_at_ms, uploaded_at_ms, r2_etag
268
413
  FROM segments WHERE stream=? ORDER BY segment_index ASC;`
269
414
  ),
270
415
  getSegmentByIndex: this.db.query(
271
- `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, size_bytes,
416
+ `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
272
417
  local_path, created_at_ms, uploaded_at_ms, r2_etag
273
418
  FROM segments WHERE stream=? AND segment_index=? LIMIT 1;`
274
419
  ),
275
420
  findSegmentForOffset: this.db.query(
276
- `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, size_bytes,
421
+ `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
277
422
  local_path, created_at_ms, uploaded_at_ms, r2_etag
278
423
  FROM segments
279
424
  WHERE stream=? AND start_offset <= ? AND end_offset >= ?
@@ -286,10 +431,31 @@ export class SqliteDurableStore {
286
431
  markSegmentUploaded: this.db.query(
287
432
  `UPDATE segments SET r2_etag=?, uploaded_at_ms=? WHERE segment_id=?;`
288
433
  ),
289
- pendingUploadSegments: this.db.query(
290
- `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, size_bytes,
434
+ pendingUploadHeads: this.db.query(
435
+ `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms, payload_bytes, size_bytes,
291
436
  local_path, created_at_ms, uploaded_at_ms, r2_etag
292
- FROM segments WHERE uploaded_at_ms IS NULL ORDER BY created_at_ms ASC LIMIT ?;`
437
+ FROM segments s
438
+ WHERE s.uploaded_at_ms IS NULL
439
+ AND s.segment_index = (
440
+ SELECT MIN(s2.segment_index)
441
+ FROM segments s2
442
+ WHERE s2.stream = s.stream AND s2.uploaded_at_ms IS NULL
443
+ )
444
+ ORDER BY s.created_at_ms ASC, s.stream ASC
445
+ LIMIT ?;`
446
+ ),
447
+ recentSegmentCompressionWindow: this.db.query(
448
+ `SELECT
449
+ COALESCE(SUM(payload_bytes), 0) AS payload_total,
450
+ COALESCE(SUM(size_bytes), 0) AS size_total,
451
+ COUNT(*) AS cnt
452
+ FROM (
453
+ SELECT payload_bytes, size_bytes
454
+ FROM segments
455
+ WHERE stream=? AND payload_bytes > 0
456
+ ORDER BY segment_index DESC
457
+ LIMIT ?
458
+ );`
293
459
  ),
294
460
  countPendingSegments: this.db.query(`SELECT COUNT(*) as cnt FROM segments WHERE uploaded_at_ms IS NULL;`),
295
461
  countSegmentsForStream: this.db.query(`SELECT COUNT(*) as cnt FROM segments WHERE stream=?;`),
@@ -297,15 +463,19 @@ export class SqliteDurableStore {
297
463
  `UPDATE streams SET segment_in_progress=1, updated_at_ms=? WHERE stream=? AND segment_in_progress=0;`
298
464
  ),
299
465
 
300
- getManifest: this.db.query(`SELECT stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag FROM manifests WHERE stream=? LIMIT 1;`),
466
+ getManifest: this.db.query(
467
+ `SELECT stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes
468
+ FROM manifests WHERE stream=? LIMIT 1;`
469
+ ),
301
470
  upsertManifest: this.db.query(
302
- `INSERT INTO manifests(stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag)
303
- VALUES(?, ?, ?, ?, ?)
471
+ `INSERT INTO manifests(stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes)
472
+ VALUES(?, ?, ?, ?, ?, ?)
304
473
  ON CONFLICT(stream) DO UPDATE SET
305
474
  generation=excluded.generation,
306
475
  uploaded_generation=excluded.uploaded_generation,
307
476
  last_uploaded_at_ms=excluded.last_uploaded_at_ms,
308
- last_uploaded_etag=excluded.last_uploaded_etag;`
477
+ last_uploaded_etag=excluded.last_uploaded_etag,
478
+ last_uploaded_size_bytes=excluded.last_uploaded_size_bytes;`
309
479
  ),
310
480
 
311
481
  getIndexState: this.db.query(
@@ -324,23 +494,23 @@ export class SqliteDurableStore {
324
494
  `UPDATE index_state SET indexed_through=?, updated_at_ms=? WHERE stream=?;`
325
495
  ),
326
496
  listIndexRuns: this.db.query(
327
- `SELECT run_id, stream, level, start_segment, end_segment, object_key, filter_len, record_count, retired_gen, retired_at_ms
497
+ `SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
328
498
  FROM index_runs WHERE stream=? AND retired_gen IS NULL
329
499
  ORDER BY start_segment ASC, level ASC;`
330
500
  ),
331
501
  listIndexRunsAll: this.db.query(
332
- `SELECT run_id, stream, level, start_segment, end_segment, object_key, filter_len, record_count, retired_gen, retired_at_ms
502
+ `SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
333
503
  FROM index_runs WHERE stream=?
334
504
  ORDER BY start_segment ASC, level ASC;`
335
505
  ),
336
506
  listRetiredIndexRuns: this.db.query(
337
- `SELECT run_id, stream, level, start_segment, end_segment, object_key, filter_len, record_count, retired_gen, retired_at_ms
507
+ `SELECT run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
338
508
  FROM index_runs WHERE stream=? AND retired_gen IS NOT NULL
339
509
  ORDER BY retired_at_ms ASC;`
340
510
  ),
341
511
  insertIndexRun: this.db.query(
342
- `INSERT OR IGNORE INTO index_runs(run_id, stream, level, start_segment, end_segment, object_key, filter_len, record_count, retired_gen, retired_at_ms)
343
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
512
+ `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)
513
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
344
514
  ),
345
515
  retireIndexRun: this.db.query(
346
516
  `UPDATE index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
@@ -348,6 +518,174 @@ export class SqliteDurableStore {
348
518
  deleteIndexRun: this.db.query(
349
519
  `DELETE FROM index_runs WHERE run_id=?;`
350
520
  ),
521
+ deleteIndexStateForStream: this.db.query(`DELETE FROM index_state WHERE stream=?;`),
522
+ deleteIndexRunsForStream: this.db.query(`DELETE FROM index_runs WHERE stream=?;`),
523
+ getSecondaryIndexState: this.db.query(
524
+ `SELECT stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms
525
+ FROM secondary_index_state WHERE stream=? AND index_name=? LIMIT 1;`
526
+ ),
527
+ listSecondaryIndexStates: this.db.query(
528
+ `SELECT stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms
529
+ FROM secondary_index_state WHERE stream=?
530
+ ORDER BY index_name ASC;`
531
+ ),
532
+ upsertSecondaryIndexState: this.db.query(
533
+ `INSERT INTO secondary_index_state(stream, index_name, index_secret, config_hash, indexed_through, updated_at_ms)
534
+ VALUES(?, ?, ?, ?, ?, ?)
535
+ ON CONFLICT(stream, index_name) DO UPDATE SET
536
+ index_secret=excluded.index_secret,
537
+ config_hash=excluded.config_hash,
538
+ indexed_through=excluded.indexed_through,
539
+ updated_at_ms=excluded.updated_at_ms;`
540
+ ),
541
+ updateSecondaryIndexedThrough: this.db.query(
542
+ `UPDATE secondary_index_state
543
+ SET indexed_through=?, updated_at_ms=?
544
+ WHERE stream=? AND index_name=?;`
545
+ ),
546
+ listSecondaryIndexRuns: this.db.query(
547
+ `SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
548
+ FROM secondary_index_runs
549
+ WHERE stream=? AND index_name=? AND retired_gen IS NULL
550
+ ORDER BY start_segment ASC, level ASC;`
551
+ ),
552
+ listSecondaryIndexRunsAll: this.db.query(
553
+ `SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
554
+ FROM secondary_index_runs
555
+ WHERE stream=? AND index_name=?
556
+ ORDER BY start_segment ASC, level ASC;`
557
+ ),
558
+ listRetiredSecondaryIndexRuns: this.db.query(
559
+ `SELECT run_id, stream, index_name, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count, retired_gen, retired_at_ms
560
+ FROM secondary_index_runs
561
+ WHERE stream=? AND index_name=? AND retired_gen IS NOT NULL
562
+ ORDER BY retired_at_ms ASC;`
563
+ ),
564
+ insertSecondaryIndexRun: this.db.query(
565
+ `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)
566
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
567
+ ),
568
+ retireSecondaryIndexRun: this.db.query(
569
+ `UPDATE secondary_index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
570
+ ),
571
+ deleteSecondaryIndexRun: this.db.query(
572
+ `DELETE FROM secondary_index_runs WHERE run_id=?;`
573
+ ),
574
+ deleteSecondaryIndexState: this.db.query(`DELETE FROM secondary_index_state WHERE stream=? AND index_name=?;`),
575
+ deleteSecondaryIndexRunsForIndex: this.db.query(`DELETE FROM secondary_index_runs WHERE stream=? AND index_name=?;`),
576
+ deleteSecondaryIndexStatesForStream: this.db.query(`DELETE FROM secondary_index_state WHERE stream=?;`),
577
+ deleteSecondaryIndexRunsForStream: this.db.query(`DELETE FROM secondary_index_runs WHERE stream=?;`),
578
+ getLexiconIndexState: this.db.query(
579
+ `SELECT stream, source_kind, source_name, indexed_through, updated_at_ms
580
+ FROM lexicon_index_state
581
+ WHERE stream=? AND source_kind=? AND source_name=?
582
+ LIMIT 1;`
583
+ ),
584
+ listLexiconIndexStates: this.db.query(
585
+ `SELECT stream, source_kind, source_name, indexed_through, updated_at_ms
586
+ FROM lexicon_index_state
587
+ WHERE stream=?
588
+ ORDER BY source_kind ASC, source_name ASC;`
589
+ ),
590
+ upsertLexiconIndexState: this.db.query(
591
+ `INSERT INTO lexicon_index_state(stream, source_kind, source_name, indexed_through, updated_at_ms)
592
+ VALUES(?, ?, ?, ?, ?)
593
+ ON CONFLICT(stream, source_kind, source_name) DO UPDATE SET
594
+ indexed_through=excluded.indexed_through,
595
+ updated_at_ms=excluded.updated_at_ms;`
596
+ ),
597
+ updateLexiconIndexedThrough: this.db.query(
598
+ `UPDATE lexicon_index_state
599
+ SET indexed_through=?, updated_at_ms=?
600
+ WHERE stream=? AND source_kind=? AND source_name=?;`
601
+ ),
602
+ listLexiconIndexRuns: this.db.query(
603
+ `SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
604
+ FROM lexicon_index_runs
605
+ WHERE stream=? AND source_kind=? AND source_name=? AND retired_gen IS NULL
606
+ ORDER BY start_segment ASC, level ASC;`
607
+ ),
608
+ listLexiconIndexRunsAll: this.db.query(
609
+ `SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
610
+ FROM lexicon_index_runs
611
+ WHERE stream=? AND source_kind=? AND source_name=?
612
+ ORDER BY start_segment ASC, level ASC;`
613
+ ),
614
+ listRetiredLexiconIndexRuns: this.db.query(
615
+ `SELECT run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count, retired_gen, retired_at_ms
616
+ FROM lexicon_index_runs
617
+ WHERE stream=? AND source_kind=? AND source_name=? AND retired_gen IS NOT NULL
618
+ ORDER BY retired_at_ms ASC;`
619
+ ),
620
+ insertLexiconIndexRun: this.db.query(
621
+ `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)
622
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL);`
623
+ ),
624
+ retireLexiconIndexRun: this.db.query(
625
+ `UPDATE lexicon_index_runs SET retired_gen=?, retired_at_ms=? WHERE run_id=?;`
626
+ ),
627
+ deleteLexiconIndexRun: this.db.query(
628
+ `DELETE FROM lexicon_index_runs WHERE run_id=?;`
629
+ ),
630
+ deleteLexiconIndexState: this.db.query(
631
+ `DELETE FROM lexicon_index_state WHERE stream=? AND source_kind=? AND source_name=?;`
632
+ ),
633
+ deleteLexiconIndexRunsForSource: this.db.query(
634
+ `DELETE FROM lexicon_index_runs WHERE stream=? AND source_kind=? AND source_name=?;`
635
+ ),
636
+ deleteLexiconIndexStatesForStream: this.db.query(`DELETE FROM lexicon_index_state WHERE stream=?;`),
637
+ deleteLexiconIndexRunsForStream: this.db.query(`DELETE FROM lexicon_index_runs WHERE stream=?;`),
638
+ getSearchCompanionPlan: this.db.query(
639
+ `SELECT stream, generation, plan_hash, plan_json, updated_at_ms
640
+ FROM search_companion_plans WHERE stream=? LIMIT 1;`
641
+ ),
642
+ listSearchCompanionPlanStreams: this.db.query(
643
+ `SELECT stream FROM search_companion_plans ORDER BY stream ASC;`
644
+ ),
645
+ upsertSearchCompanionPlan: this.db.query(
646
+ `INSERT INTO search_companion_plans(stream, generation, plan_hash, plan_json, updated_at_ms)
647
+ VALUES(?, ?, ?, ?, ?)
648
+ ON CONFLICT(stream) DO UPDATE SET
649
+ generation=excluded.generation,
650
+ plan_hash=excluded.plan_hash,
651
+ plan_json=excluded.plan_json,
652
+ updated_at_ms=excluded.updated_at_ms;`
653
+ ),
654
+ deleteSearchCompanionPlan: this.db.query(`DELETE FROM search_companion_plans WHERE stream=?;`),
655
+ listSearchSegmentCompanions: this.db.query(
656
+ `SELECT stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
657
+ primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms
658
+ FROM search_segment_companions
659
+ WHERE stream=?
660
+ ORDER BY segment_index ASC;`
661
+ ),
662
+ getSearchSegmentCompanion: this.db.query(
663
+ `SELECT stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
664
+ primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms
665
+ FROM search_segment_companions
666
+ WHERE stream=? AND segment_index=? LIMIT 1;`
667
+ ),
668
+ upsertSearchSegmentCompanion: this.db.query(
669
+ `INSERT INTO search_segment_companions(stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json, size_bytes,
670
+ primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms)
671
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
672
+ ON CONFLICT(stream, segment_index) DO UPDATE SET
673
+ object_key=excluded.object_key,
674
+ plan_generation=excluded.plan_generation,
675
+ sections_json=excluded.sections_json,
676
+ section_sizes_json=excluded.section_sizes_json,
677
+ size_bytes=excluded.size_bytes,
678
+ primary_timestamp_min_ms=excluded.primary_timestamp_min_ms,
679
+ primary_timestamp_max_ms=excluded.primary_timestamp_max_ms,
680
+ updated_at_ms=excluded.updated_at_ms;`
681
+ ),
682
+ deleteSearchSegmentCompanionsFromGeneration: this.db.query(
683
+ `DELETE FROM search_segment_companions WHERE stream=? AND plan_generation < ?;`
684
+ ),
685
+ deleteSearchSegmentCompanionsFromIndex: this.db.query(
686
+ `DELETE FROM search_segment_companions WHERE stream=? AND segment_index >= ?;`
687
+ ),
688
+ deleteSearchSegmentCompanions: this.db.query(`DELETE FROM search_segment_companions WHERE stream=?;`),
351
689
  countUploadedSegments: this.db.query(
352
690
  `SELECT COALESCE(MAX(segment_index), -1) as max_idx
353
691
  FROM segments WHERE stream=? AND r2_etag IS NOT NULL;`
@@ -385,35 +723,48 @@ export class SqliteDurableStore {
385
723
  advanceUploadedThrough: this.db.query(
386
724
  `UPDATE streams SET uploaded_through=?, updated_at_ms=? WHERE stream=?;`
387
725
  ),
388
- deleteWalBeforeOffset: this.db.query(
389
- `DELETE FROM wal WHERE stream=? AND offset <= ?;`
390
- ),
391
726
 
392
- getSchemaRegistry: this.db.query(`SELECT stream, schema_json, updated_at_ms FROM schemas WHERE stream=? LIMIT 1;`),
727
+ getSchemaRegistry: this.db.query(`SELECT stream, schema_json, updated_at_ms, uploaded_size_bytes FROM schemas WHERE stream=? LIMIT 1;`),
393
728
  upsertSchemaRegistry: this.db.query(
394
729
  `INSERT INTO schemas(stream, schema_json, updated_at_ms) VALUES(?, ?, ?)
395
730
  ON CONFLICT(stream) DO UPDATE SET schema_json=excluded.schema_json, updated_at_ms=excluded.updated_at_ms;`
396
731
  ),
397
- getStreamInterpreter: this.db.query(
398
- `SELECT stream, interpreted_through, updated_at_ms
399
- FROM stream_interpreters WHERE stream=? LIMIT 1;`
732
+ setSchemaUploadedSize: this.db.query(`UPDATE schemas SET uploaded_size_bytes=?, updated_at_ms=? WHERE stream=?;`),
733
+ getStreamProfile: this.db.query(`SELECT stream, profile_json, updated_at_ms FROM stream_profiles WHERE stream=? LIMIT 1;`),
734
+ upsertStreamProfile: this.db.query(
735
+ `INSERT INTO stream_profiles(stream, profile_json, updated_at_ms) VALUES(?, ?, ?)
736
+ ON CONFLICT(stream) DO UPDATE SET profile_json=excluded.profile_json, updated_at_ms=excluded.updated_at_ms;`
737
+ ),
738
+ deleteStreamProfile: this.db.query(`DELETE FROM stream_profiles WHERE stream=?;`),
739
+ getStreamTouchState: this.db.query(
740
+ `SELECT stream, processed_through, updated_at_ms
741
+ FROM stream_touch_state WHERE stream=? LIMIT 1;`
400
742
  ),
401
- upsertStreamInterpreter: this.db.query(
402
- `INSERT INTO stream_interpreters(stream, interpreted_through, updated_at_ms)
743
+ upsertStreamTouchState: this.db.query(
744
+ `INSERT INTO stream_touch_state(stream, processed_through, updated_at_ms)
403
745
  VALUES(?, ?, ?)
404
746
  ON CONFLICT(stream) DO UPDATE SET
405
- interpreted_through=excluded.interpreted_through,
747
+ processed_through=excluded.processed_through,
406
748
  updated_at_ms=excluded.updated_at_ms;`
407
749
  ),
408
- deleteStreamInterpreter: this.db.query(`DELETE FROM stream_interpreters WHERE stream=?;`),
409
- listStreamInterpreters: this.db.query(
410
- `SELECT stream, interpreted_through, updated_at_ms
411
- FROM stream_interpreters
750
+ deleteStreamTouchState: this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`),
751
+ listStreamTouchStates: this.db.query(
752
+ `SELECT stream, processed_through, updated_at_ms
753
+ FROM stream_touch_state
412
754
  ORDER BY stream ASC;`
413
755
  ),
756
+ listStreamsByProfile: this.db.query(`SELECT stream FROM streams WHERE profile=? ORDER BY stream ASC;`),
414
757
  countStreams: this.db.query(`SELECT COUNT(*) as cnt FROM streams WHERE (stream_flags & ?) = 0;`),
415
758
  sumPendingBytes: this.db.query(`SELECT COALESCE(SUM(pending_bytes), 0) as total FROM streams;`),
416
759
  sumPendingSegmentBytes: this.db.query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE uploaded_at_ms IS NULL;`),
760
+ recordObjectStoreRequest: this.db.query(
761
+ `INSERT INTO objectstore_request_counts(stream_hash, artifact, op, count, bytes, updated_at_ms)
762
+ VALUES(?, ?, ?, ?, ?, ?)
763
+ ON CONFLICT(stream_hash, artifact, op) DO UPDATE SET
764
+ count=objectstore_request_counts.count + excluded.count,
765
+ bytes=objectstore_request_counts.bytes + excluded.bytes,
766
+ updated_at_ms=excluded.updated_at_ms;`
767
+ ),
417
768
  };
418
769
  }
419
770
 
@@ -428,6 +779,54 @@ export class SqliteDurableStore {
428
779
  return v.toString();
429
780
  }
430
781
 
782
+ private deleteWalThroughWithStats(
783
+ stream: string,
784
+ through: bigint,
785
+ opts?: { maxRows?: number }
786
+ ): { deletedRows: bigint; deletedBytes: bigint } {
787
+ if (through < 0n) return { deletedRows: 0n, deletedBytes: 0n };
788
+ const bound = this.bindInt(through);
789
+ const maxRows = opts?.maxRows;
790
+ const useChunkedDelete = typeof maxRows === "number" && Number.isFinite(maxRows) && maxRows > 0;
791
+ const stmt = useChunkedDelete
792
+ ? this.db.prepare(
793
+ `DELETE FROM wal
794
+ WHERE rowid IN (
795
+ SELECT rowid
796
+ FROM wal
797
+ WHERE stream=? AND offset <= ?
798
+ ORDER BY offset ASC
799
+ LIMIT ?
800
+ )
801
+ RETURNING payload_len;`
802
+ )
803
+ : this.db.prepare(
804
+ `DELETE FROM wal
805
+ WHERE stream=? AND offset <= ?
806
+ RETURNING payload_len;`
807
+ );
808
+
809
+ try {
810
+ const rows = useChunkedDelete
811
+ ? stmt.iterate(stream, bound, Math.max(1, Math.floor(maxRows!)))
812
+ : stmt.iterate(stream, bound);
813
+
814
+ let deletedRows = 0n;
815
+ let deletedBytes = 0n;
816
+ for (const row of rows as any) {
817
+ deletedRows += 1n;
818
+ deletedBytes += this.toBigInt(row?.payload_len ?? 0);
819
+ }
820
+ return { deletedRows, deletedBytes };
821
+ } finally {
822
+ try {
823
+ stmt.finalize?.();
824
+ } catch {
825
+ // ignore
826
+ }
827
+ }
828
+ }
829
+
431
830
  private encodeU64Le(value: bigint): Uint8Array {
432
831
  const buf = new Uint8Array(8);
433
832
  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
@@ -448,6 +847,7 @@ export class SqliteDurableStore {
448
847
  created_at_ms: this.toBigInt(row.created_at_ms),
449
848
  updated_at_ms: this.toBigInt(row.updated_at_ms),
450
849
  content_type: String(row.content_type),
850
+ profile: row.profile == null ? null : String(row.profile),
451
851
  stream_seq: row.stream_seq == null ? null : String(row.stream_seq),
452
852
  closed: Number(row.closed),
453
853
  closed_producer_id: row.closed_producer_id == null ? null : String(row.closed_producer_id),
@@ -461,6 +861,7 @@ export class SqliteDurableStore {
461
861
  uploaded_segment_count: Number(row.uploaded_segment_count ?? 0),
462
862
  pending_rows: this.toBigInt(row.pending_rows),
463
863
  pending_bytes: this.toBigInt(row.pending_bytes),
864
+ logical_size_bytes: this.toBigInt(row.logical_size_bytes ?? 0),
464
865
  wal_rows: this.toBigInt(row.wal_rows ?? 0),
465
866
  wal_bytes: this.toBigInt(row.wal_bytes ?? 0),
466
867
  last_append_ms: this.toBigInt(row.last_append_ms),
@@ -480,6 +881,7 @@ export class SqliteDurableStore {
480
881
  end_offset: this.toBigInt(row.end_offset),
481
882
  block_count: Number(row.block_count),
482
883
  last_append_ms: this.toBigInt(row.last_append_ms),
884
+ payload_bytes: this.toBigInt(row.payload_bytes ?? 0),
483
885
  size_bytes: Number(row.size_bytes),
484
886
  local_path: String(row.local_path),
485
887
  created_at_ms: this.toBigInt(row.created_at_ms),
@@ -505,10 +907,45 @@ export class SqliteDurableStore {
505
907
  return row ? this.coerceStreamRow(row) : null;
506
908
  }
507
909
 
910
+ setStreamLogicalSizeBytes(stream: string, logicalSizeBytes: bigint): void {
911
+ this.db
912
+ .query(`UPDATE streams SET logical_size_bytes=?, updated_at_ms=? WHERE stream=?;`)
913
+ .run(this.bindInt(logicalSizeBytes), this.nowMs(), stream);
914
+ }
915
+
916
+ listStreamsMissingLogicalSize(limit: number): string[] {
917
+ const now = this.nowMs();
918
+ const rows = this.db
919
+ .query(
920
+ `SELECT stream
921
+ FROM streams
922
+ WHERE (stream_flags & ?) = 0
923
+ AND (expires_at_ms IS NULL OR expires_at_ms > ?)
924
+ AND next_offset > 0
925
+ AND logical_size_bytes = 0
926
+ ORDER BY updated_at_ms ASC
927
+ LIMIT ?;`
928
+ )
929
+ .all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, now, limit) as any[];
930
+ return rows.map((row) => String(row.stream));
931
+ }
932
+
933
+ getWalBytesAfterOffset(stream: string, offset: bigint): bigint {
934
+ const row = this.db
935
+ .query(
936
+ `SELECT COALESCE(SUM(payload_len), 0) as bytes
937
+ FROM wal
938
+ WHERE stream=? AND offset > ?;`
939
+ )
940
+ .get(stream, this.bindInt(offset)) as any;
941
+ return this.toBigInt(row?.bytes ?? 0);
942
+ }
943
+
508
944
  ensureStream(
509
945
  stream: string,
510
946
  opts?: {
511
947
  contentType?: string;
948
+ profile?: string | null;
512
949
  expiresAtMs?: bigint | null;
513
950
  ttlSeconds?: number | null;
514
951
  closed?: boolean;
@@ -523,6 +960,7 @@ export class SqliteDurableStore {
523
960
  const epoch = 0;
524
961
  const nextOffset = 0n;
525
962
  const contentType = opts?.contentType ?? "application/octet-stream";
963
+ const profile = opts?.profile ?? "generic";
526
964
  const closed = opts?.closed ? 1 : 0;
527
965
  const closedProducer = opts?.closedProducer ?? null;
528
966
  const expiresAtMs = opts?.expiresAtMs ?? null;
@@ -533,18 +971,19 @@ export class SqliteDurableStore {
533
971
  .query(
534
972
  `INSERT INTO streams(
535
973
  stream, created_at_ms, updated_at_ms,
536
- content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
974
+ content_type, profile, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
537
975
  epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
538
- pending_rows, pending_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
976
+ pending_rows, pending_bytes, logical_size_bytes, last_append_ms, last_segment_cut_ms, segment_in_progress,
539
977
  expires_at_ms, stream_flags
540
978
  )
541
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
979
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
542
980
  )
543
981
  .run(
544
982
  stream,
545
983
  now,
546
984
  now,
547
985
  contentType,
986
+ profile,
548
987
  null,
549
988
  closed,
550
989
  closedProducer ? closedProducer.id : null,
@@ -558,6 +997,7 @@ export class SqliteDurableStore {
558
997
  0,
559
998
  0n,
560
999
  0n,
1000
+ 0n,
561
1001
  now,
562
1002
  now,
563
1003
  0,
@@ -565,7 +1005,7 @@ export class SqliteDurableStore {
565
1005
  streamFlags
566
1006
  );
567
1007
 
568
- this.stmts.upsertManifest.run(stream, 0, 0, null, null);
1008
+ this.stmts.upsertManifest.run(stream, 0, 0, null, null, null);
569
1009
  this.ensureSegmentMeta(stream);
570
1010
  return this.getStream(stream)!;
571
1011
  }
@@ -576,6 +1016,7 @@ export class SqliteDurableStore {
576
1016
  row.created_at_ms,
577
1017
  row.updated_at_ms,
578
1018
  row.content_type,
1019
+ row.profile,
579
1020
  row.stream_seq,
580
1021
  row.closed,
581
1022
  row.closed_producer_id,
@@ -589,6 +1030,7 @@ export class SqliteDurableStore {
589
1030
  row.uploaded_segment_count,
590
1031
  row.pending_rows,
591
1032
  row.pending_bytes,
1033
+ row.logical_size_bytes,
592
1034
  row.wal_rows,
593
1035
  row.wal_bytes,
594
1036
  row.last_append_ms,
@@ -605,20 +1047,55 @@ export class SqliteDurableStore {
605
1047
  return rows.map((r) => this.coerceStreamRow(r));
606
1048
  }
607
1049
 
1050
+ listDeletedStreams(limit: number, offset: number): string[] {
1051
+ const rows = this.stmts.listDeletedStreams.all(STREAM_FLAG_DELETED, limit, offset) as any[];
1052
+ return rows.map((row) => String(row.stream));
1053
+ }
1054
+
608
1055
  listExpiredStreams(limit: number): string[] {
609
1056
  const now = this.nowMs();
610
1057
  const rows = this.stmts.listExpiredStreams.all(STREAM_FLAG_DELETED | STREAM_FLAG_TOUCH, now, limit) as any[];
611
1058
  return rows.map((r) => String(r.stream));
612
1059
  }
613
1060
 
1061
+ deleteAccelerationState(stream: string): void {
1062
+ const tx = this.db.transaction(() => {
1063
+ this.stmts.deleteIndexRunsForStream.run(stream);
1064
+ this.stmts.deleteIndexStateForStream.run(stream);
1065
+ this.stmts.deleteSecondaryIndexRunsForStream.run(stream);
1066
+ this.stmts.deleteSecondaryIndexStatesForStream.run(stream);
1067
+ this.stmts.deleteLexiconIndexRunsForStream.run(stream);
1068
+ this.stmts.deleteLexiconIndexStatesForStream.run(stream);
1069
+ this.stmts.deleteSearchSegmentCompanions.run(stream);
1070
+ this.stmts.deleteSearchCompanionPlan.run(stream);
1071
+ });
1072
+ tx();
1073
+ }
1074
+
614
1075
  deleteStream(stream: string): boolean {
615
1076
  const existing = this.getStream(stream);
616
1077
  if (!existing) return false;
617
1078
  const now = this.nowMs();
618
- this.stmts.setDeleted.run(STREAM_FLAG_DELETED, now, stream);
1079
+ const tx = this.db.transaction(() => {
1080
+ this.stmts.setDeleted.run(STREAM_FLAG_DELETED, now, stream);
1081
+ this.stmts.deleteIndexRunsForStream.run(stream);
1082
+ this.stmts.deleteIndexStateForStream.run(stream);
1083
+ this.stmts.deleteSecondaryIndexRunsForStream.run(stream);
1084
+ this.stmts.deleteSecondaryIndexStatesForStream.run(stream);
1085
+ this.stmts.deleteLexiconIndexRunsForStream.run(stream);
1086
+ this.stmts.deleteLexiconIndexStatesForStream.run(stream);
1087
+ this.stmts.deleteSearchSegmentCompanions.run(stream);
1088
+ this.stmts.deleteSearchCompanionPlan.run(stream);
1089
+ });
1090
+ tx();
619
1091
  return true;
620
1092
  }
621
1093
 
1094
+ updateStreamProfile(stream: string, profile: string | null): StreamRow | null {
1095
+ this.stmts.setStreamProfile.run(profile, this.nowMs(), stream);
1096
+ return this.getStream(stream);
1097
+ }
1098
+
622
1099
  hardDeleteStream(stream: string): boolean {
623
1100
  const tx = this.db.transaction(() => {
624
1101
  const existing = this.getStream(stream);
@@ -627,11 +1104,18 @@ export class SqliteDurableStore {
627
1104
  this.db.query(`DELETE FROM segments WHERE stream=?;`).run(stream);
628
1105
  this.db.query(`DELETE FROM manifests WHERE stream=?;`).run(stream);
629
1106
  this.db.query(`DELETE FROM schemas WHERE stream=?;`).run(stream);
630
- this.db.query(`DELETE FROM stream_interpreters WHERE stream=?;`).run(stream);
1107
+ this.db.query(`DELETE FROM stream_profiles WHERE stream=?;`).run(stream);
1108
+ this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`).run(stream);
631
1109
  this.db.query(`DELETE FROM live_templates WHERE stream=?;`).run(stream);
632
1110
  this.db.query(`DELETE FROM producer_state WHERE stream=?;`).run(stream);
633
1111
  this.db.query(`DELETE FROM index_state WHERE stream=?;`).run(stream);
634
1112
  this.db.query(`DELETE FROM index_runs WHERE stream=?;`).run(stream);
1113
+ this.db.query(`DELETE FROM secondary_index_state WHERE stream=?;`).run(stream);
1114
+ this.db.query(`DELETE FROM secondary_index_runs WHERE stream=?;`).run(stream);
1115
+ this.db.query(`DELETE FROM lexicon_index_state WHERE stream=?;`).run(stream);
1116
+ this.db.query(`DELETE FROM lexicon_index_runs WHERE stream=?;`).run(stream);
1117
+ this.db.query(`DELETE FROM search_companion_plans WHERE stream=?;`).run(stream);
1118
+ this.db.query(`DELETE FROM search_segment_companions WHERE stream=?;`).run(stream);
635
1119
  this.db.query(`DELETE FROM stream_segment_meta WHERE stream=?;`).run(stream);
636
1120
  this.db.query(`DELETE FROM streams WHERE stream=?;`).run(stream);
637
1121
  return true;
@@ -639,49 +1123,81 @@ export class SqliteDurableStore {
639
1123
  return tx();
640
1124
  }
641
1125
 
642
- getSchemaRegistry(stream: string): { stream: string; registry_json: string; updated_at_ms: bigint } | null {
1126
+ getSchemaRegistry(stream: string): { stream: string; registry_json: string; updated_at_ms: bigint; uploaded_size_bytes: bigint } | null {
643
1127
  const row = this.stmts.getSchemaRegistry.get(stream) as any;
644
1128
  if (!row) return null;
645
- return { stream: String(row.stream), registry_json: String(row.schema_json), updated_at_ms: this.toBigInt(row.updated_at_ms) };
1129
+ return {
1130
+ stream: String(row.stream),
1131
+ registry_json: String(row.schema_json),
1132
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
1133
+ uploaded_size_bytes: this.toBigInt(row.uploaded_size_bytes ?? 0),
1134
+ };
646
1135
  }
647
1136
 
648
1137
  upsertSchemaRegistry(stream: string, registryJson: string): void {
649
1138
  this.stmts.upsertSchemaRegistry.run(stream, registryJson, this.nowMs());
650
1139
  }
651
1140
 
652
- getStreamInterpreter(stream: string): { stream: string; interpreted_through: bigint; updated_at_ms: bigint } | null {
653
- const row = this.stmts.getStreamInterpreter.get(stream) as any;
1141
+ setSchemaUploadedSizeBytes(stream: string, sizeBytes: number): void {
1142
+ this.stmts.setSchemaUploadedSize.run(sizeBytes, this.nowMs(), stream);
1143
+ }
1144
+
1145
+ getStreamProfile(stream: string): { stream: string; profile_json: string; updated_at_ms: bigint } | null {
1146
+ const row = this.stmts.getStreamProfile.get(stream) as any;
1147
+ if (!row) return null;
1148
+ return {
1149
+ stream: String(row.stream),
1150
+ profile_json: String(row.profile_json),
1151
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
1152
+ };
1153
+ }
1154
+
1155
+ upsertStreamProfile(stream: string, profileJson: string): void {
1156
+ this.stmts.upsertStreamProfile.run(stream, profileJson, this.nowMs());
1157
+ }
1158
+
1159
+ deleteStreamProfile(stream: string): void {
1160
+ this.stmts.deleteStreamProfile.run(stream);
1161
+ }
1162
+
1163
+ getStreamTouchState(stream: string): { stream: string; processed_through: bigint; updated_at_ms: bigint } | null {
1164
+ const row = this.stmts.getStreamTouchState.get(stream) as any;
654
1165
  if (!row) return null;
655
1166
  return {
656
1167
  stream: String(row.stream),
657
- interpreted_through: this.toBigInt(row.interpreted_through),
1168
+ processed_through: this.toBigInt(row.processed_through),
658
1169
  updated_at_ms: this.toBigInt(row.updated_at_ms),
659
1170
  };
660
1171
  }
661
1172
 
662
- listStreamInterpreters(): Array<{ stream: string; interpreted_through: bigint; updated_at_ms: bigint }> {
663
- const rows = this.stmts.listStreamInterpreters.all() as any[];
1173
+ listStreamTouchStates(): Array<{ stream: string; processed_through: bigint; updated_at_ms: bigint }> {
1174
+ const rows = this.stmts.listStreamTouchStates.all() as any[];
664
1175
  return rows.map((row) => ({
665
1176
  stream: String(row.stream),
666
- interpreted_through: this.toBigInt(row.interpreted_through),
1177
+ processed_through: this.toBigInt(row.processed_through),
667
1178
  updated_at_ms: this.toBigInt(row.updated_at_ms),
668
1179
  }));
669
1180
  }
670
1181
 
671
- ensureStreamInterpreter(stream: string): void {
672
- const existing = this.getStreamInterpreter(stream);
1182
+ listStreamsByProfile(kind: string): string[] {
1183
+ const rows = this.stmts.listStreamsByProfile.all(kind) as any[];
1184
+ return rows.map((row) => String(row.stream));
1185
+ }
1186
+
1187
+ ensureStreamTouchState(stream: string): void {
1188
+ const existing = this.getStreamTouchState(stream);
673
1189
  if (existing) return;
674
1190
  const srow = this.getStream(stream);
675
1191
  const initialThrough = srow ? srow.next_offset - 1n : -1n;
676
- this.stmts.upsertStreamInterpreter.run(stream, this.bindInt(initialThrough), this.nowMs());
1192
+ this.stmts.upsertStreamTouchState.run(stream, this.bindInt(initialThrough), this.nowMs());
677
1193
  }
678
1194
 
679
- updateStreamInterpreterThrough(stream: string, interpretedThrough: bigint): void {
680
- this.stmts.upsertStreamInterpreter.run(stream, this.bindInt(interpretedThrough), this.nowMs());
1195
+ updateStreamTouchStateThrough(stream: string, processedThrough: bigint): void {
1196
+ this.stmts.upsertStreamTouchState.run(stream, this.bindInt(processedThrough), this.nowMs());
681
1197
  }
682
1198
 
683
- deleteStreamInterpreter(stream: string): void {
684
- this.stmts.deleteStreamInterpreter.run(stream);
1199
+ deleteStreamTouchState(stream: string): void {
1200
+ this.stmts.deleteStreamTouchState.run(stream);
685
1201
  }
686
1202
 
687
1203
  addStreamFlags(stream: string, flags: number): void {
@@ -695,6 +1211,12 @@ export class SqliteDurableStore {
695
1211
  return this.toBigInt(row.min_off);
696
1212
  }
697
1213
 
1214
+ getWalOldestTimestampMs(stream: string): bigint | null {
1215
+ const row = this.db.query(`SELECT MIN(ts_ms) as min_ts FROM wal WHERE stream=?;`).get(stream) as any;
1216
+ if (!row || row.min_ts == null) return null;
1217
+ return this.toBigInt(row.min_ts);
1218
+ }
1219
+
698
1220
  /**
699
1221
  * Trim a WAL-only stream by age (in ms), leaving at least 1 record if the stream is non-empty.
700
1222
  *
@@ -723,18 +1245,9 @@ export class SqliteDurableStore {
723
1245
 
724
1246
  if (keepFromOffset <= 0n) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
725
1247
 
726
- const stats = this.db
727
- .query(
728
- `SELECT COALESCE(SUM(payload_len), 0) as bytes, COUNT(*) as rows
729
- FROM wal WHERE stream=? AND offset < ?;`
730
- )
731
- .get(stream, this.bindInt(keepFromOffset)) as any;
732
- const bytes = this.toBigInt(stats?.bytes ?? 0);
733
- const rows = this.toBigInt(stats?.rows ?? 0);
1248
+ const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, keepFromOffset - 1n);
734
1249
  if (rows <= 0n) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
735
1250
 
736
- this.db.query(`DELETE FROM wal WHERE stream=? AND offset < ?;`).run(stream, this.bindInt(keepFromOffset));
737
-
738
1251
  // Touch streams are WAL-only: pending_* tracks WAL payload bytes/rows. Keep it consistent for stats/backpressure.
739
1252
  const now = this.nowMs();
740
1253
  this.db.query(
@@ -922,6 +1435,7 @@ export class SqliteDurableStore {
922
1435
  lastAppend,
923
1436
  pendingRows,
924
1437
  totalBytes,
1438
+ totalBytes,
925
1439
  pendingRows,
926
1440
  totalBytes,
927
1441
  stream,
@@ -942,16 +1456,50 @@ export class SqliteDurableStore {
942
1456
  const start = this.bindInt(startOffset);
943
1457
  const end = this.bindInt(endOffset);
944
1458
  const stmt = routingKey
945
- ? this.db.query(
946
- `SELECT offset, ts_ms, routing_key, content_type, payload\n FROM wal\n WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?\n ORDER BY offset ASC;`
1459
+ ? this.db.prepare(
1460
+ `SELECT offset, ts_ms, routing_key, content_type, payload
1461
+ FROM wal
1462
+ WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?
1463
+ ORDER BY offset ASC;`
947
1464
  )
948
- : this.db.query(
949
- `SELECT offset, ts_ms, routing_key, content_type, payload\n FROM wal\n WHERE stream = ? AND offset >= ? AND offset <= ?\n ORDER BY offset ASC;`
1465
+ : this.db.prepare(
1466
+ `SELECT offset, ts_ms, routing_key, content_type, payload
1467
+ FROM wal
1468
+ WHERE stream = ? AND offset >= ? AND offset <= ?
1469
+ ORDER BY offset ASC;`
950
1470
  );
951
1471
  try {
952
- const it = routingKey
953
- ? (stmt.iterate(stream, start, end, routingKey) as any)
954
- : (stmt.iterate(stream, start, end) as any);
1472
+ const it = routingKey ? (stmt.iterate(stream, start, end, routingKey) as any) : (stmt.iterate(stream, start, end) as any);
1473
+ for (const row of it) {
1474
+ yield row;
1475
+ }
1476
+ } finally {
1477
+ try {
1478
+ stmt.finalize?.();
1479
+ } catch {
1480
+ // ignore
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ *iterWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): Generator<any, void, void> {
1486
+ const start = this.bindInt(startOffset);
1487
+ const end = this.bindInt(endOffset);
1488
+ const stmt = routingKey
1489
+ ? this.db.prepare(
1490
+ `SELECT offset, ts_ms, routing_key, content_type, payload
1491
+ FROM wal
1492
+ WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?
1493
+ ORDER BY offset DESC;`
1494
+ )
1495
+ : this.db.prepare(
1496
+ `SELECT offset, ts_ms, routing_key, content_type, payload
1497
+ FROM wal
1498
+ WHERE stream = ? AND offset >= ? AND offset <= ?
1499
+ ORDER BY offset DESC;`
1500
+ );
1501
+ try {
1502
+ const it = routingKey ? (stmt.iterate(stream, start, end, routingKey) as any) : (stmt.iterate(stream, start, end) as any);
955
1503
  for (const row of it) {
956
1504
  yield row;
957
1505
  }
@@ -977,6 +1525,7 @@ export class SqliteDurableStore {
977
1525
  endOffset: bigint;
978
1526
  blockCount: number;
979
1527
  lastAppendMs: bigint;
1528
+ payloadBytes: bigint;
980
1529
  sizeBytes: number;
981
1530
  localPath: string;
982
1531
  }): void {
@@ -988,6 +1537,7 @@ export class SqliteDurableStore {
988
1537
  row.endOffset,
989
1538
  row.blockCount,
990
1539
  row.lastAppendMs,
1540
+ row.payloadBytes,
991
1541
  row.sizeBytes,
992
1542
  row.localPath,
993
1543
  this.nowMs()
@@ -1002,9 +1552,9 @@ export class SqliteDurableStore {
1002
1552
  endOffset: bigint;
1003
1553
  blockCount: number;
1004
1554
  lastAppendMs: bigint;
1555
+ payloadBytes: bigint;
1005
1556
  sizeBytes: number;
1006
1557
  localPath: string;
1007
- payloadBytes: bigint;
1008
1558
  rowsSealed: bigint;
1009
1559
  }): void {
1010
1560
  const tx = this.db.transaction(() => {
@@ -1031,11 +1581,21 @@ export class SqliteDurableStore {
1031
1581
  return row ? this.coerceSegmentRow(row) : null;
1032
1582
  }
1033
1583
 
1034
- pendingUploadSegments(limit: number): SegmentRow[] {
1035
- const rows = this.stmts.pendingUploadSegments.all(limit) as any[];
1584
+ pendingUploadHeads(limit: number): SegmentRow[] {
1585
+ const rows = this.stmts.pendingUploadHeads.all(limit) as any[];
1036
1586
  return rows.map((r) => this.coerceSegmentRow(r));
1037
1587
  }
1038
1588
 
1589
+ recentSegmentCompressionRatio(stream: string, limit = 8): number | null {
1590
+ const row = this.stmts.recentSegmentCompressionWindow.get(stream, Math.max(1, limit)) as any;
1591
+ const count = Number(row?.cnt ?? 0);
1592
+ if (!Number.isFinite(count) || count <= 0) return null;
1593
+ const payloadTotal = this.toBigInt(row?.payload_total ?? 0);
1594
+ const sizeTotal = this.toBigInt(row?.size_total ?? 0);
1595
+ if (payloadTotal <= 0n || sizeTotal <= 0n) return null;
1596
+ return Number(sizeTotal) / Number(payloadTotal);
1597
+ }
1598
+
1039
1599
  countPendingSegments(): number {
1040
1600
  const row = this.stmts.countPendingSegments.get() as any;
1041
1601
  return row ? Number(row.cnt) : 0;
@@ -1158,20 +1718,10 @@ export class SqliteDurableStore {
1158
1718
  }
1159
1719
 
1160
1720
  deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number } {
1161
- const through = this.bindInt(uploadedThrough);
1162
1721
  const tx = this.db.transaction(() => {
1163
- const stats = this.db
1164
- .query(
1165
- `SELECT COALESCE(SUM(payload_len), 0) as bytes, COUNT(*) as rows
1166
- FROM wal WHERE stream=? AND offset <= ?;`
1167
- )
1168
- .get(stream, through) as any;
1169
- const bytes = this.toBigInt(stats?.bytes ?? 0);
1170
- const rows = this.toBigInt(stats?.rows ?? 0);
1722
+ const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, uploadedThrough);
1171
1723
  if (rows <= 0n) return { deletedRows: 0, deletedBytes: 0 };
1172
1724
 
1173
- this.stmts.deleteWalBeforeOffset.run(stream, through);
1174
-
1175
1725
  const now = this.nowMs();
1176
1726
  this.db.query(
1177
1727
  `UPDATE streams
@@ -1188,10 +1738,17 @@ export class SqliteDurableStore {
1188
1738
  return tx();
1189
1739
  }
1190
1740
 
1191
- getManifestRow(stream: string): { stream: string; generation: number; uploaded_generation: number; last_uploaded_at_ms: bigint | null; last_uploaded_etag: string | null } {
1741
+ getManifestRow(stream: string): {
1742
+ stream: string;
1743
+ generation: number;
1744
+ uploaded_generation: number;
1745
+ last_uploaded_at_ms: bigint | null;
1746
+ last_uploaded_etag: string | null;
1747
+ last_uploaded_size_bytes: bigint | null;
1748
+ } {
1192
1749
  const row = this.stmts.getManifest.get(stream) as any;
1193
1750
  if (!row) {
1194
- this.stmts.upsertManifest.run(stream, 0, 0, null, null);
1751
+ this.stmts.upsertManifest.run(stream, 0, 0, null, null, null);
1195
1752
  const fresh = this.stmts.getManifest.get(stream) as any;
1196
1753
  return {
1197
1754
  stream: String(fresh.stream),
@@ -1199,6 +1756,7 @@ export class SqliteDurableStore {
1199
1756
  uploaded_generation: Number(fresh.uploaded_generation),
1200
1757
  last_uploaded_at_ms: fresh.last_uploaded_at_ms == null ? null : this.toBigInt(fresh.last_uploaded_at_ms),
1201
1758
  last_uploaded_etag: fresh.last_uploaded_etag == null ? null : String(fresh.last_uploaded_etag),
1759
+ last_uploaded_size_bytes: fresh.last_uploaded_size_bytes == null ? null : this.toBigInt(fresh.last_uploaded_size_bytes),
1202
1760
  };
1203
1761
  }
1204
1762
  return {
@@ -1207,11 +1765,19 @@ export class SqliteDurableStore {
1207
1765
  uploaded_generation: Number(row.uploaded_generation),
1208
1766
  last_uploaded_at_ms: row.last_uploaded_at_ms == null ? null : this.toBigInt(row.last_uploaded_at_ms),
1209
1767
  last_uploaded_etag: row.last_uploaded_etag == null ? null : String(row.last_uploaded_etag),
1768
+ last_uploaded_size_bytes: row.last_uploaded_size_bytes == null ? null : this.toBigInt(row.last_uploaded_size_bytes),
1210
1769
  };
1211
1770
  }
1212
1771
 
1213
- upsertManifestRow(stream: string, generation: number, uploadedGeneration: number, uploadedAtMs: bigint | null, etag: string | null): void {
1214
- this.stmts.upsertManifest.run(stream, generation, uploadedGeneration, uploadedAtMs, etag);
1772
+ upsertManifestRow(
1773
+ stream: string,
1774
+ generation: number,
1775
+ uploadedGeneration: number,
1776
+ uploadedAtMs: bigint | null,
1777
+ etag: string | null,
1778
+ sizeBytes: number | null
1779
+ ): void {
1780
+ this.stmts.upsertManifest.run(stream, generation, uploadedGeneration, uploadedAtMs, etag, sizeBytes);
1215
1781
  }
1216
1782
 
1217
1783
  getIndexState(stream: string): IndexStateRow | null {
@@ -1242,6 +1808,7 @@ export class SqliteDurableStore {
1242
1808
  start_segment: Number(r.start_segment),
1243
1809
  end_segment: Number(r.end_segment),
1244
1810
  object_key: String(r.object_key),
1811
+ size_bytes: Number(r.size_bytes ?? 0),
1245
1812
  filter_len: Number(r.filter_len),
1246
1813
  record_count: Number(r.record_count),
1247
1814
  retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
@@ -1258,6 +1825,7 @@ export class SqliteDurableStore {
1258
1825
  start_segment: Number(r.start_segment),
1259
1826
  end_segment: Number(r.end_segment),
1260
1827
  object_key: String(r.object_key),
1828
+ size_bytes: Number(r.size_bytes ?? 0),
1261
1829
  filter_len: Number(r.filter_len),
1262
1830
  record_count: Number(r.record_count),
1263
1831
  retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
@@ -1274,6 +1842,7 @@ export class SqliteDurableStore {
1274
1842
  start_segment: Number(r.start_segment),
1275
1843
  end_segment: Number(r.end_segment),
1276
1844
  object_key: String(r.object_key),
1845
+ size_bytes: Number(r.size_bytes ?? 0),
1277
1846
  filter_len: Number(r.filter_len),
1278
1847
  record_count: Number(r.record_count),
1279
1848
  retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
@@ -1289,6 +1858,7 @@ export class SqliteDurableStore {
1289
1858
  row.start_segment,
1290
1859
  row.end_segment,
1291
1860
  row.object_key,
1861
+ row.size_bytes,
1292
1862
  row.filter_len,
1293
1863
  row.record_count
1294
1864
  );
@@ -1314,49 +1884,403 @@ export class SqliteDurableStore {
1314
1884
  tx();
1315
1885
  }
1316
1886
 
1887
+ deleteIndex(stream: string): void {
1888
+ const tx = this.db.transaction(() => {
1889
+ this.db.query(`DELETE FROM index_runs WHERE stream=?;`).run(stream);
1890
+ this.db.query(`DELETE FROM index_state WHERE stream=?;`).run(stream);
1891
+ });
1892
+ tx();
1893
+ }
1894
+
1317
1895
  countUploadedSegments(stream: string): number {
1318
1896
  const row = this.stmts.countUploadedSegments.get(stream) as any;
1319
1897
  const maxIdx = row ? Number(row.max_idx) : -1;
1320
1898
  return maxIdx >= 0 ? maxIdx + 1 : 0;
1321
1899
  }
1322
1900
 
1323
- commitManifest(stream: string, generation: number, etag: string, uploadedAtMs: bigint, uploadedThrough: bigint): void {
1901
+ getSecondaryIndexState(stream: string, indexName: string): SecondaryIndexStateRow | null {
1902
+ const row = this.stmts.getSecondaryIndexState.get(stream, indexName) as any;
1903
+ if (!row) return null;
1904
+ return {
1905
+ stream: String(row.stream),
1906
+ index_name: String(row.index_name),
1907
+ index_secret: row.index_secret instanceof Uint8Array ? row.index_secret : new Uint8Array(row.index_secret),
1908
+ config_hash: String(row.config_hash ?? ""),
1909
+ indexed_through: Number(row.indexed_through),
1910
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
1911
+ };
1912
+ }
1913
+
1914
+ listSecondaryIndexStates(stream: string): SecondaryIndexStateRow[] {
1915
+ const rows = this.stmts.listSecondaryIndexStates.all(stream) as any[];
1916
+ return rows.map((row) => ({
1917
+ stream: String(row.stream),
1918
+ index_name: String(row.index_name),
1919
+ index_secret: row.index_secret instanceof Uint8Array ? row.index_secret : new Uint8Array(row.index_secret),
1920
+ config_hash: String(row.config_hash ?? ""),
1921
+ indexed_through: Number(row.indexed_through),
1922
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
1923
+ }));
1924
+ }
1925
+
1926
+ upsertSecondaryIndexState(
1927
+ stream: string,
1928
+ indexName: string,
1929
+ indexSecret: Uint8Array,
1930
+ configHash: string,
1931
+ indexedThrough: number
1932
+ ): void {
1933
+ this.stmts.upsertSecondaryIndexState.run(stream, indexName, indexSecret, configHash, indexedThrough, this.nowMs());
1934
+ }
1935
+
1936
+ updateSecondaryIndexedThrough(stream: string, indexName: string, indexedThrough: number): void {
1937
+ this.stmts.updateSecondaryIndexedThrough.run(indexedThrough, this.nowMs(), stream, indexName);
1938
+ }
1939
+
1940
+ listSecondaryIndexRuns(stream: string, indexName: string): SecondaryIndexRunRow[] {
1941
+ const rows = this.stmts.listSecondaryIndexRuns.all(stream, indexName) as any[];
1942
+ return rows.map((r) => ({
1943
+ run_id: String(r.run_id),
1944
+ stream: String(r.stream),
1945
+ index_name: String(r.index_name),
1946
+ level: Number(r.level),
1947
+ start_segment: Number(r.start_segment),
1948
+ end_segment: Number(r.end_segment),
1949
+ object_key: String(r.object_key),
1950
+ size_bytes: Number(r.size_bytes ?? 0),
1951
+ filter_len: Number(r.filter_len),
1952
+ record_count: Number(r.record_count),
1953
+ retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
1954
+ retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
1955
+ }));
1956
+ }
1957
+
1958
+ listSecondaryIndexRunsAll(stream: string, indexName: string): SecondaryIndexRunRow[] {
1959
+ const rows = this.stmts.listSecondaryIndexRunsAll.all(stream, indexName) as any[];
1960
+ return rows.map((r) => ({
1961
+ run_id: String(r.run_id),
1962
+ stream: String(r.stream),
1963
+ index_name: String(r.index_name),
1964
+ level: Number(r.level),
1965
+ start_segment: Number(r.start_segment),
1966
+ end_segment: Number(r.end_segment),
1967
+ object_key: String(r.object_key),
1968
+ size_bytes: Number(r.size_bytes ?? 0),
1969
+ filter_len: Number(r.filter_len),
1970
+ record_count: Number(r.record_count),
1971
+ retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
1972
+ retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
1973
+ }));
1974
+ }
1975
+
1976
+ listRetiredSecondaryIndexRuns(stream: string, indexName: string): SecondaryIndexRunRow[] {
1977
+ const rows = this.stmts.listRetiredSecondaryIndexRuns.all(stream, indexName) as any[];
1978
+ return rows.map((r) => ({
1979
+ run_id: String(r.run_id),
1980
+ stream: String(r.stream),
1981
+ index_name: String(r.index_name),
1982
+ level: Number(r.level),
1983
+ start_segment: Number(r.start_segment),
1984
+ end_segment: Number(r.end_segment),
1985
+ object_key: String(r.object_key),
1986
+ size_bytes: Number(r.size_bytes ?? 0),
1987
+ filter_len: Number(r.filter_len),
1988
+ record_count: Number(r.record_count),
1989
+ retired_gen: r.retired_gen == null ? null : Number(r.retired_gen),
1990
+ retired_at_ms: r.retired_at_ms == null ? null : this.toBigInt(r.retired_at_ms),
1991
+ }));
1992
+ }
1993
+
1994
+ insertSecondaryIndexRun(row: Omit<SecondaryIndexRunRow, "retired_gen" | "retired_at_ms">): void {
1995
+ this.stmts.insertSecondaryIndexRun.run(
1996
+ row.run_id,
1997
+ row.stream,
1998
+ row.index_name,
1999
+ row.level,
2000
+ row.start_segment,
2001
+ row.end_segment,
2002
+ row.object_key,
2003
+ row.size_bytes,
2004
+ row.filter_len,
2005
+ row.record_count
2006
+ );
2007
+ }
2008
+
2009
+ retireSecondaryIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): void {
2010
+ if (runIds.length === 0) return;
2011
+ const tx = this.db.transaction(() => {
2012
+ for (const runId of runIds) {
2013
+ this.stmts.retireSecondaryIndexRun.run(retiredGen, retiredAtMs, runId);
2014
+ }
2015
+ });
2016
+ tx();
2017
+ }
2018
+
2019
+ deleteSecondaryIndexRuns(runIds: string[]): void {
2020
+ if (runIds.length === 0) return;
2021
+ const tx = this.db.transaction(() => {
2022
+ for (const runId of runIds) {
2023
+ this.stmts.deleteSecondaryIndexRun.run(runId);
2024
+ }
2025
+ });
2026
+ tx();
2027
+ }
2028
+
2029
+ deleteSecondaryIndex(stream: string, indexName: string): void {
1324
2030
  const tx = this.db.transaction(() => {
1325
- this.stmts.upsertManifest.run(stream, generation, generation, uploadedAtMs, etag);
2031
+ this.stmts.deleteSecondaryIndexRunsForIndex.run(stream, indexName);
2032
+ this.stmts.deleteSecondaryIndexState.run(stream, indexName);
2033
+ });
2034
+ tx();
2035
+ }
2036
+
2037
+ getLexiconIndexState(stream: string, sourceKind: string, sourceName: string): LexiconIndexStateRow | null {
2038
+ const row = this.stmts.getLexiconIndexState.get(stream, sourceKind, sourceName) as any;
2039
+ if (!row) return null;
2040
+ return {
2041
+ stream: String(row.stream),
2042
+ source_kind: String(row.source_kind),
2043
+ source_name: String(row.source_name),
2044
+ indexed_through: Number(row.indexed_through),
2045
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
2046
+ };
2047
+ }
2048
+
2049
+ listLexiconIndexStates(stream: string): LexiconIndexStateRow[] {
2050
+ const rows = this.stmts.listLexiconIndexStates.all(stream) as any[];
2051
+ return rows.map((row) => ({
2052
+ stream: String(row.stream),
2053
+ source_kind: String(row.source_kind),
2054
+ source_name: String(row.source_name),
2055
+ indexed_through: Number(row.indexed_through),
2056
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
2057
+ }));
2058
+ }
2059
+
2060
+ upsertLexiconIndexState(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): void {
2061
+ this.stmts.upsertLexiconIndexState.run(stream, sourceKind, sourceName, indexedThrough, this.nowMs());
2062
+ }
2063
+
2064
+ updateLexiconIndexedThrough(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): void {
2065
+ this.stmts.updateLexiconIndexedThrough.run(indexedThrough, this.nowMs(), stream, sourceKind, sourceName);
2066
+ }
2067
+
2068
+ listLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
2069
+ const rows = this.stmts.listLexiconIndexRuns.all(stream, sourceKind, sourceName) as any[];
2070
+ return rows.map((row) => ({
2071
+ run_id: String(row.run_id),
2072
+ stream: String(row.stream),
2073
+ source_kind: String(row.source_kind),
2074
+ source_name: String(row.source_name),
2075
+ level: Number(row.level),
2076
+ start_segment: Number(row.start_segment),
2077
+ end_segment: Number(row.end_segment),
2078
+ object_key: String(row.object_key),
2079
+ size_bytes: Number(row.size_bytes ?? 0),
2080
+ record_count: Number(row.record_count ?? 0),
2081
+ retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
2082
+ retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
2083
+ }));
2084
+ }
2085
+
2086
+ listLexiconIndexRunsAll(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
2087
+ const rows = this.stmts.listLexiconIndexRunsAll.all(stream, sourceKind, sourceName) as any[];
2088
+ return rows.map((row) => ({
2089
+ run_id: String(row.run_id),
2090
+ stream: String(row.stream),
2091
+ source_kind: String(row.source_kind),
2092
+ source_name: String(row.source_name),
2093
+ level: Number(row.level),
2094
+ start_segment: Number(row.start_segment),
2095
+ end_segment: Number(row.end_segment),
2096
+ object_key: String(row.object_key),
2097
+ size_bytes: Number(row.size_bytes ?? 0),
2098
+ record_count: Number(row.record_count ?? 0),
2099
+ retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
2100
+ retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
2101
+ }));
2102
+ }
2103
+
2104
+ listRetiredLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): LexiconIndexRunRow[] {
2105
+ const rows = this.stmts.listRetiredLexiconIndexRuns.all(stream, sourceKind, sourceName) as any[];
2106
+ return rows.map((row) => ({
2107
+ run_id: String(row.run_id),
2108
+ stream: String(row.stream),
2109
+ source_kind: String(row.source_kind),
2110
+ source_name: String(row.source_name),
2111
+ level: Number(row.level),
2112
+ start_segment: Number(row.start_segment),
2113
+ end_segment: Number(row.end_segment),
2114
+ object_key: String(row.object_key),
2115
+ size_bytes: Number(row.size_bytes ?? 0),
2116
+ record_count: Number(row.record_count ?? 0),
2117
+ retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
2118
+ retired_at_ms: row.retired_at_ms == null ? null : this.toBigInt(row.retired_at_ms),
2119
+ }));
2120
+ }
2121
+
2122
+ insertLexiconIndexRun(row: Omit<LexiconIndexRunRow, "retired_gen" | "retired_at_ms">): void {
2123
+ this.stmts.insertLexiconIndexRun.run(
2124
+ row.run_id,
2125
+ row.stream,
2126
+ row.source_kind,
2127
+ row.source_name,
2128
+ row.level,
2129
+ row.start_segment,
2130
+ row.end_segment,
2131
+ row.object_key,
2132
+ row.size_bytes,
2133
+ row.record_count
2134
+ );
2135
+ }
2136
+
2137
+ retireLexiconIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): void {
2138
+ if (runIds.length === 0) return;
2139
+ const tx = this.db.transaction(() => {
2140
+ for (const runId of runIds) {
2141
+ this.stmts.retireLexiconIndexRun.run(retiredGen, retiredAtMs, runId);
2142
+ }
2143
+ });
2144
+ tx();
2145
+ }
2146
+
2147
+ deleteLexiconIndexRuns(runIds: string[]): void {
2148
+ if (runIds.length === 0) return;
2149
+ const tx = this.db.transaction(() => {
2150
+ for (const runId of runIds) {
2151
+ this.stmts.deleteLexiconIndexRun.run(runId);
2152
+ }
2153
+ });
2154
+ tx();
2155
+ }
2156
+
2157
+ deleteLexiconIndexSource(stream: string, sourceKind: string, sourceName: string): void {
2158
+ const tx = this.db.transaction(() => {
2159
+ this.stmts.deleteLexiconIndexRunsForSource.run(stream, sourceKind, sourceName);
2160
+ this.stmts.deleteLexiconIndexState.run(stream, sourceKind, sourceName);
2161
+ });
2162
+ tx();
2163
+ }
2164
+
2165
+ getSearchCompanionPlan(stream: string): SearchCompanionPlanRow | null {
2166
+ const row = this.stmts.getSearchCompanionPlan.get(stream) as any;
2167
+ if (!row) return null;
2168
+ return {
2169
+ stream: String(row.stream),
2170
+ generation: Number(row.generation),
2171
+ plan_hash: String(row.plan_hash),
2172
+ plan_json: String(row.plan_json),
2173
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
2174
+ };
2175
+ }
2176
+
2177
+ listSearchCompanionPlanStreams(): string[] {
2178
+ const rows = this.stmts.listSearchCompanionPlanStreams.all() as any[];
2179
+ return rows.map((row) => String(row.stream));
2180
+ }
2181
+
2182
+ upsertSearchCompanionPlan(stream: string, generation: number, planHash: string, planJson: string): void {
2183
+ this.stmts.upsertSearchCompanionPlan.run(stream, generation, planHash, planJson, this.nowMs());
2184
+ }
2185
+
2186
+ deleteSearchCompanionPlan(stream: string): void {
2187
+ this.stmts.deleteSearchCompanionPlan.run(stream);
2188
+ }
2189
+
2190
+ listSearchSegmentCompanions(stream: string): SearchSegmentCompanionRow[] {
2191
+ const rows = this.stmts.listSearchSegmentCompanions.all(stream) as any[];
2192
+ return rows.map((row) => ({
2193
+ stream: String(row.stream),
2194
+ segment_index: Number(row.segment_index),
2195
+ object_key: String(row.object_key),
2196
+ plan_generation: Number(row.plan_generation),
2197
+ sections_json: String(row.sections_json),
2198
+ section_sizes_json: String(row.section_sizes_json ?? "{}"),
2199
+ size_bytes: Number(row.size_bytes ?? 0),
2200
+ primary_timestamp_min_ms: row.primary_timestamp_min_ms == null ? null : this.toBigInt(row.primary_timestamp_min_ms),
2201
+ primary_timestamp_max_ms: row.primary_timestamp_max_ms == null ? null : this.toBigInt(row.primary_timestamp_max_ms),
2202
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
2203
+ }));
2204
+ }
2205
+
2206
+ getSearchSegmentCompanion(stream: string, segmentIndex: number): SearchSegmentCompanionRow | null {
2207
+ const row = this.stmts.getSearchSegmentCompanion.get(stream, segmentIndex) as any;
2208
+ if (!row) return null;
2209
+ return {
2210
+ stream: String(row.stream),
2211
+ segment_index: Number(row.segment_index),
2212
+ object_key: String(row.object_key),
2213
+ plan_generation: Number(row.plan_generation),
2214
+ sections_json: String(row.sections_json),
2215
+ section_sizes_json: String(row.section_sizes_json ?? "{}"),
2216
+ size_bytes: Number(row.size_bytes ?? 0),
2217
+ primary_timestamp_min_ms: row.primary_timestamp_min_ms == null ? null : this.toBigInt(row.primary_timestamp_min_ms),
2218
+ primary_timestamp_max_ms: row.primary_timestamp_max_ms == null ? null : this.toBigInt(row.primary_timestamp_max_ms),
2219
+ updated_at_ms: this.toBigInt(row.updated_at_ms),
2220
+ };
2221
+ }
2222
+
2223
+ upsertSearchSegmentCompanion(
2224
+ stream: string,
2225
+ segmentIndex: number,
2226
+ objectKey: string,
2227
+ planGeneration: number,
2228
+ sectionsJson: string,
2229
+ sectionSizesJson: string,
2230
+ sizeBytes: number,
2231
+ primaryTimestampMinMs: bigint | null,
2232
+ primaryTimestampMaxMs: bigint | null
2233
+ ): void {
2234
+ this.stmts.upsertSearchSegmentCompanion.run(
2235
+ stream,
2236
+ segmentIndex,
2237
+ objectKey,
2238
+ planGeneration,
2239
+ sectionsJson,
2240
+ sectionSizesJson,
2241
+ sizeBytes,
2242
+ primaryTimestampMinMs,
2243
+ primaryTimestampMaxMs,
2244
+ this.nowMs()
2245
+ );
2246
+ }
2247
+
2248
+ deleteSearchSegmentCompanionsBeforeGeneration(stream: string, generation: number): void {
2249
+ this.stmts.deleteSearchSegmentCompanionsFromGeneration.run(stream, generation);
2250
+ }
2251
+
2252
+ deleteSearchSegmentCompanionsFrom(stream: string, segmentIndex: number): void {
2253
+ this.stmts.deleteSearchSegmentCompanionsFromIndex.run(stream, segmentIndex);
2254
+ }
2255
+
2256
+ deleteSearchSegmentCompanions(stream: string): void {
2257
+ this.stmts.deleteSearchSegmentCompanions.run(stream);
2258
+ }
2259
+
2260
+ commitManifest(
2261
+ stream: string,
2262
+ generation: number,
2263
+ etag: string,
2264
+ uploadedAtMs: bigint,
2265
+ uploadedThrough: bigint,
2266
+ sizeBytes: number
2267
+ ): void {
2268
+ const tx = this.db.transaction(() => {
2269
+ this.stmts.upsertManifest.run(stream, generation, generation, uploadedAtMs, etag, sizeBytes);
1326
2270
  this.stmts.advanceUploadedThrough.run(uploadedThrough, this.nowMs(), stream);
1327
2271
  let gcThrough = uploadedThrough;
1328
- const interp = this.stmts.getStreamInterpreter.get(stream) as any;
1329
- if (interp) {
1330
- const interpretedThrough = this.toBigInt(interp.interpreted_through);
1331
- gcThrough = interpretedThrough < gcThrough ? interpretedThrough : gcThrough;
2272
+ const touchState = this.stmts.getStreamTouchState.get(stream) as any;
2273
+ if (touchState) {
2274
+ const processedThrough = this.toBigInt(touchState.processed_through);
2275
+ gcThrough = processedThrough < gcThrough ? processedThrough : gcThrough;
1332
2276
  }
1333
2277
  if (gcThrough < 0n) return;
1334
2278
 
1335
- // Chunk deletes to avoid large event-loop stalls on catch-up uploads.
1336
- // (Periodic GC in touch/manager.ts handles interpreter-gated cleanup too.)
1337
- let deleteThrough = gcThrough;
1338
- if (BASE_WAL_GC_CHUNK_OFFSETS > 0) {
1339
- const oldest = this.getWalOldestOffset(stream);
1340
- if (oldest != null) {
1341
- const maxThrough = oldest + BigInt(BASE_WAL_GC_CHUNK_OFFSETS) - 1n;
1342
- if (deleteThrough > maxThrough) deleteThrough = maxThrough;
1343
- }
1344
- }
1345
- if (deleteThrough < 0n) return;
1346
-
1347
- const bound = this.bindInt(deleteThrough);
1348
- const stats = this.db
1349
- .query(
1350
- `SELECT COALESCE(SUM(payload_len), 0) as bytes, COUNT(*) as rows
1351
- FROM wal WHERE stream=? AND offset <= ?;`
1352
- )
1353
- .get(stream, bound) as any;
1354
- const bytes = this.toBigInt(stats?.bytes ?? 0);
1355
- const rows = this.toBigInt(stats?.rows ?? 0);
2279
+ const { deletedRows: rows, deletedBytes: bytes } = this.deleteWalThroughWithStats(stream, gcThrough, {
2280
+ maxRows: BASE_WAL_GC_CHUNK_OFFSETS,
2281
+ });
1356
2282
  if (rows <= 0n) return;
1357
2283
 
1358
- this.stmts.deleteWalBeforeOffset.run(stream, bound);
1359
-
1360
2284
  // Keep retained-WAL counters consistent for metrics/debugging.
1361
2285
  const now = this.nowMs();
1362
2286
  this.db.query(
@@ -1370,6 +2294,152 @@ export class SqliteDurableStore {
1370
2294
  tx();
1371
2295
  }
1372
2296
 
2297
+ recordObjectStoreRequestByHash(streamHash: string, artifact: string, op: string, bytes = 0, count = 1): void {
2298
+ if (!streamHash || !artifact || !op) return;
2299
+ this.stmts.recordObjectStoreRequest.run(streamHash, artifact, op, count, bytes, this.nowMs());
2300
+ }
2301
+
2302
+ getObjectStoreRequestSummaryByHash(streamHash: string): {
2303
+ puts: bigint;
2304
+ reads: bigint;
2305
+ gets: bigint;
2306
+ heads: bigint;
2307
+ lists: bigint;
2308
+ deletes: bigint;
2309
+ by_artifact: Array<{ artifact: string; puts: bigint; gets: bigint; heads: bigint; lists: bigint; deletes: bigint; reads: bigint }>;
2310
+ } {
2311
+ const rows = this.db
2312
+ .query(
2313
+ `SELECT artifact, op, count
2314
+ FROM objectstore_request_counts
2315
+ WHERE stream_hash=?
2316
+ ORDER BY artifact ASC, op ASC;`
2317
+ )
2318
+ .all(streamHash) as any[];
2319
+ const byArtifact = new Map<string, { puts: bigint; gets: bigint; heads: bigint; lists: bigint; deletes: bigint; reads: bigint }>();
2320
+ let puts = 0n;
2321
+ let gets = 0n;
2322
+ let heads = 0n;
2323
+ let lists = 0n;
2324
+ let deletes = 0n;
2325
+ for (const row of rows) {
2326
+ const artifact = String(row.artifact);
2327
+ const op = String(row.op);
2328
+ const count = this.toBigInt(row.count ?? 0);
2329
+ const entry = byArtifact.get(artifact) ?? { puts: 0n, gets: 0n, heads: 0n, lists: 0n, deletes: 0n, reads: 0n };
2330
+ if (op === "put") {
2331
+ entry.puts += count;
2332
+ puts += count;
2333
+ } else if (op === "get") {
2334
+ entry.gets += count;
2335
+ entry.reads += count;
2336
+ gets += count;
2337
+ } else if (op === "head") {
2338
+ entry.heads += count;
2339
+ entry.reads += count;
2340
+ heads += count;
2341
+ } else if (op === "list") {
2342
+ entry.lists += count;
2343
+ entry.reads += count;
2344
+ lists += count;
2345
+ } else if (op === "delete") {
2346
+ entry.deletes += count;
2347
+ deletes += count;
2348
+ }
2349
+ byArtifact.set(artifact, entry);
2350
+ }
2351
+ return {
2352
+ puts,
2353
+ reads: gets + heads + lists,
2354
+ gets,
2355
+ heads,
2356
+ lists,
2357
+ deletes,
2358
+ by_artifact: Array.from(byArtifact.entries()).map(([artifact, entry]) => ({ artifact, ...entry })),
2359
+ };
2360
+ }
2361
+
2362
+ getUploadedSegmentBytes(stream: string): bigint {
2363
+ const row = this.db
2364
+ .query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE stream=? AND r2_etag IS NOT NULL;`)
2365
+ .get(stream) as any;
2366
+ return this.toBigInt(row?.total ?? 0);
2367
+ }
2368
+
2369
+ getPendingSealedSegmentBytes(stream: string): bigint {
2370
+ const row = this.db
2371
+ .query(`SELECT COALESCE(SUM(size_bytes), 0) as total FROM segments WHERE stream=? AND uploaded_at_ms IS NULL;`)
2372
+ .get(stream) as any;
2373
+ return this.toBigInt(row?.total ?? 0);
2374
+ }
2375
+
2376
+ getRoutingIndexStorage(stream: string): { object_count: number; bytes: bigint } {
2377
+ const row = this.db
2378
+ .query(`SELECT COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total FROM index_runs WHERE stream=?;`)
2379
+ .get(stream) as any;
2380
+ return {
2381
+ object_count: Number(row?.cnt ?? 0),
2382
+ bytes: this.toBigInt(row?.total ?? 0),
2383
+ };
2384
+ }
2385
+
2386
+ getSecondaryIndexStorage(stream: string): Array<{ index_name: string; object_count: number; bytes: bigint }> {
2387
+ const rows = this.db
2388
+ .query(
2389
+ `SELECT index_name, COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total
2390
+ FROM secondary_index_runs
2391
+ WHERE stream=?
2392
+ GROUP BY index_name
2393
+ ORDER BY index_name ASC;`
2394
+ )
2395
+ .all(stream) as any[];
2396
+ return rows.map((row) => ({
2397
+ index_name: String(row.index_name),
2398
+ object_count: Number(row.cnt ?? 0),
2399
+ bytes: this.toBigInt(row.total ?? 0),
2400
+ }));
2401
+ }
2402
+
2403
+ getLexiconIndexStorage(
2404
+ stream: string
2405
+ ): Array<{ source_kind: string; source_name: string; object_count: number; bytes: bigint }> {
2406
+ const rows = this.db
2407
+ .query(
2408
+ `SELECT source_kind, source_name, COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total
2409
+ FROM lexicon_index_runs
2410
+ WHERE stream=?
2411
+ GROUP BY source_kind, source_name
2412
+ ORDER BY source_kind ASC, source_name ASC;`
2413
+ )
2414
+ .all(stream) as any[];
2415
+ return rows.map((row) => ({
2416
+ source_kind: String(row.source_kind),
2417
+ source_name: String(row.source_name),
2418
+ object_count: Number(row.cnt ?? 0),
2419
+ bytes: this.toBigInt(row.total ?? 0),
2420
+ }));
2421
+ }
2422
+
2423
+ getBundledCompanionStorage(stream: string): { object_count: number; bytes: bigint } {
2424
+ const row = this.db
2425
+ .query(`SELECT COUNT(*) as cnt, COALESCE(SUM(size_bytes), 0) as total FROM search_segment_companions WHERE stream=?;`)
2426
+ .get(stream) as any;
2427
+ return {
2428
+ object_count: Number(row?.cnt ?? 0),
2429
+ bytes: this.toBigInt(row?.total ?? 0),
2430
+ };
2431
+ }
2432
+
2433
+ getSegmentLastAppendMsFromMeta(stream: string, segmentIndex: number): bigint | null {
2434
+ const meta = this.getSegmentMeta(stream);
2435
+ if (!meta) return null;
2436
+ if (segmentIndex < 0 || segmentIndex >= meta.segment_count) return null;
2437
+ const off = segmentIndex * 8;
2438
+ if (off + 8 > meta.segment_last_ts.byteLength) return null;
2439
+ const dv = new DataView(meta.segment_last_ts.buffer, meta.segment_last_ts.byteOffset, meta.segment_last_ts.byteLength);
2440
+ return dv.getBigUint64(off, true) / 1_000_000n;
2441
+ }
2442
+
1373
2443
  /** Find candidates by bytes/rows/interval. */
1374
2444
  candidates(
1375
2445
  minPendingBytes: bigint,