@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
@@ -0,0 +1,925 @@
1
+ import type { SqliteDatabase } from "../sqlite/adapter.ts";
2
+ import { dsError } from "../util/ds_error.ts";
3
+
4
+ /**
5
+ * SQLite schema + migrations.
6
+ *
7
+ * This rewrite uses SQLite as:
8
+ * - WAL (durable append log)
9
+ * - local metadata store (streams/segments/manifests/schemas)
10
+ */
11
+
12
+ export const SCHEMA_VERSION = 24;
13
+
14
+ export const DEFAULT_PRAGMAS_SQL = `
15
+ PRAGMA journal_mode = WAL;
16
+ PRAGMA synchronous = FULL;
17
+ PRAGMA foreign_keys = ON;
18
+ PRAGMA busy_timeout = 5000;
19
+ PRAGMA temp_store = MEMORY;
20
+ `;
21
+
22
+ const CREATE_TABLES_V4_SQL = `
23
+ CREATE TABLE IF NOT EXISTS streams (
24
+ stream TEXT PRIMARY KEY,
25
+ created_at_ms INTEGER NOT NULL,
26
+ updated_at_ms INTEGER NOT NULL,
27
+
28
+ content_type TEXT NOT NULL,
29
+ profile TEXT NULL,
30
+ stream_seq TEXT NULL,
31
+ closed INTEGER NOT NULL DEFAULT 0,
32
+ closed_producer_id TEXT NULL,
33
+ closed_producer_epoch INTEGER NULL,
34
+ closed_producer_seq INTEGER NULL,
35
+ ttl_seconds INTEGER NULL,
36
+
37
+ epoch INTEGER NOT NULL,
38
+ next_offset INTEGER NOT NULL,
39
+ sealed_through INTEGER NOT NULL,
40
+ uploaded_through INTEGER NOT NULL,
41
+ uploaded_segment_count INTEGER NOT NULL DEFAULT 0,
42
+
43
+ pending_rows INTEGER NOT NULL,
44
+ pending_bytes INTEGER NOT NULL,
45
+
46
+ -- Logical payload bytes ever appended to this stream and still part of its
47
+ -- visible history on this node. This is the constant-time source for
48
+ -- management endpoints such as /_details.
49
+ logical_size_bytes INTEGER NOT NULL DEFAULT 0,
50
+
51
+ -- Logical size of retained rows in the wal table for this stream (payload-only bytes).
52
+ -- This is explicitly tracked because SQLite file size is high-water and does not shrink
53
+ -- deterministically after DELETE-based GC/retention trimming.
54
+ wal_rows INTEGER NOT NULL DEFAULT 0,
55
+ wal_bytes INTEGER NOT NULL DEFAULT 0,
56
+
57
+ last_append_ms INTEGER NOT NULL,
58
+ last_segment_cut_ms INTEGER NOT NULL,
59
+ segment_in_progress INTEGER NOT NULL,
60
+
61
+ expires_at_ms INTEGER NULL,
62
+ stream_flags INTEGER NOT NULL DEFAULT 0
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS streams_pending_bytes_idx ON streams(pending_bytes);
66
+ CREATE INDEX IF NOT EXISTS streams_last_cut_idx ON streams(last_segment_cut_ms);
67
+ CREATE INDEX IF NOT EXISTS streams_inprog_pending_idx ON streams(segment_in_progress, pending_bytes, last_segment_cut_ms);
68
+
69
+ CREATE TABLE IF NOT EXISTS wal (
70
+ id INTEGER PRIMARY KEY,
71
+ stream TEXT NOT NULL,
72
+ offset INTEGER NOT NULL,
73
+ ts_ms INTEGER NOT NULL,
74
+ payload BLOB NOT NULL,
75
+ payload_len INTEGER NOT NULL,
76
+ routing_key BLOB NULL,
77
+ content_type TEXT NULL,
78
+ flags INTEGER NOT NULL DEFAULT 0
79
+ );
80
+
81
+ CREATE UNIQUE INDEX IF NOT EXISTS wal_stream_offset_uniq ON wal(stream, offset);
82
+ CREATE INDEX IF NOT EXISTS wal_ts_idx ON wal(ts_ms);
83
+
84
+ CREATE TABLE IF NOT EXISTS segments (
85
+ segment_id TEXT PRIMARY KEY,
86
+ stream TEXT NOT NULL,
87
+ segment_index INTEGER NOT NULL,
88
+ start_offset INTEGER NOT NULL,
89
+ end_offset INTEGER NOT NULL,
90
+ block_count INTEGER NOT NULL,
91
+ last_append_ms INTEGER NOT NULL,
92
+ payload_bytes INTEGER NOT NULL DEFAULT 0,
93
+ size_bytes INTEGER NOT NULL,
94
+ local_path TEXT NOT NULL,
95
+ created_at_ms INTEGER NOT NULL,
96
+ uploaded_at_ms INTEGER NULL,
97
+ r2_etag TEXT NULL
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS stream_segment_meta (
101
+ stream TEXT PRIMARY KEY,
102
+ segment_count INTEGER NOT NULL,
103
+ segment_offsets BLOB NOT NULL,
104
+ segment_blocks BLOB NOT NULL,
105
+ segment_last_ts BLOB NOT NULL
106
+ );
107
+
108
+ CREATE UNIQUE INDEX IF NOT EXISTS segments_stream_index_uniq ON segments(stream, segment_index);
109
+ CREATE INDEX IF NOT EXISTS segments_stream_start_idx ON segments(stream, start_offset);
110
+ CREATE INDEX IF NOT EXISTS segments_pending_upload_idx ON segments(uploaded_at_ms);
111
+
112
+ CREATE TABLE IF NOT EXISTS manifests (
113
+ stream TEXT PRIMARY KEY,
114
+ generation INTEGER NOT NULL,
115
+ uploaded_generation INTEGER NOT NULL,
116
+ last_uploaded_at_ms INTEGER NULL,
117
+ last_uploaded_etag TEXT NULL,
118
+ last_uploaded_size_bytes INTEGER NULL
119
+ );
120
+
121
+ CREATE TABLE IF NOT EXISTS schemas (
122
+ stream TEXT PRIMARY KEY,
123
+ schema_json TEXT NOT NULL,
124
+ updated_at_ms INTEGER NOT NULL,
125
+ uploaded_size_bytes INTEGER NOT NULL DEFAULT 0
126
+ );
127
+
128
+ CREATE TABLE IF NOT EXISTS stream_profiles (
129
+ stream TEXT PRIMARY KEY,
130
+ profile_json TEXT NOT NULL,
131
+ updated_at_ms INTEGER NOT NULL
132
+ );
133
+
134
+ CREATE TABLE IF NOT EXISTS producer_state (
135
+ stream TEXT NOT NULL,
136
+ producer_id TEXT NOT NULL,
137
+ epoch INTEGER NOT NULL,
138
+ last_seq INTEGER NOT NULL,
139
+ updated_at_ms INTEGER NOT NULL,
140
+ PRIMARY KEY (stream, producer_id)
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS stream_touch_state (
144
+ stream TEXT PRIMARY KEY,
145
+ processed_through INTEGER NOT NULL,
146
+ updated_at_ms INTEGER NOT NULL
147
+ );
148
+
149
+ -- Live dynamic template registry (per base stream).
150
+ CREATE TABLE IF NOT EXISTS live_templates (
151
+ stream TEXT NOT NULL,
152
+ template_id TEXT NOT NULL,
153
+ entity TEXT NOT NULL,
154
+ fields_json TEXT NOT NULL,
155
+ encodings_json TEXT NOT NULL,
156
+ state TEXT NOT NULL,
157
+ created_at_ms INTEGER NOT NULL,
158
+ last_seen_at_ms INTEGER NOT NULL,
159
+ inactivity_ttl_ms INTEGER NOT NULL,
160
+ active_from_source_offset INTEGER NOT NULL,
161
+ retired_at_ms INTEGER NULL,
162
+ retired_reason TEXT NULL,
163
+ PRIMARY KEY (stream, template_id)
164
+ );
165
+
166
+ CREATE INDEX IF NOT EXISTS live_templates_stream_entity_state_last_seen_idx
167
+ ON live_templates(stream, entity, state, last_seen_at_ms);
168
+ CREATE INDEX IF NOT EXISTS live_templates_stream_state_last_seen_idx
169
+ ON live_templates(stream, state, last_seen_at_ms);
170
+ `;
171
+
172
+ const CREATE_INDEX_TABLES_SQL = `
173
+ CREATE TABLE IF NOT EXISTS index_state (
174
+ stream TEXT PRIMARY KEY,
175
+ index_secret BLOB NOT NULL,
176
+ indexed_through INTEGER NOT NULL,
177
+ updated_at_ms INTEGER NOT NULL
178
+ );
179
+
180
+ CREATE TABLE IF NOT EXISTS index_runs (
181
+ run_id TEXT PRIMARY KEY,
182
+ stream TEXT NOT NULL,
183
+ level INTEGER NOT NULL,
184
+ start_segment INTEGER NOT NULL,
185
+ end_segment INTEGER NOT NULL,
186
+ object_key TEXT NOT NULL,
187
+ size_bytes INTEGER NOT NULL DEFAULT 0,
188
+ filter_len INTEGER NOT NULL,
189
+ record_count INTEGER NOT NULL,
190
+ retired_gen INTEGER NULL,
191
+ retired_at_ms INTEGER NULL
192
+ );
193
+
194
+ CREATE INDEX IF NOT EXISTS index_runs_stream_idx ON index_runs(stream, level, start_segment);
195
+ `;
196
+
197
+ const CREATE_SECONDARY_INDEX_TABLES_SQL = `
198
+ CREATE TABLE IF NOT EXISTS secondary_index_state (
199
+ stream TEXT NOT NULL,
200
+ index_name TEXT NOT NULL,
201
+ index_secret BLOB NOT NULL,
202
+ config_hash TEXT NOT NULL,
203
+ indexed_through INTEGER NOT NULL,
204
+ updated_at_ms INTEGER NOT NULL,
205
+ PRIMARY KEY (stream, index_name)
206
+ );
207
+
208
+ CREATE TABLE IF NOT EXISTS secondary_index_runs (
209
+ run_id TEXT PRIMARY KEY,
210
+ stream TEXT NOT NULL,
211
+ index_name TEXT NOT NULL,
212
+ level INTEGER NOT NULL,
213
+ start_segment INTEGER NOT NULL,
214
+ end_segment INTEGER NOT NULL,
215
+ object_key TEXT NOT NULL,
216
+ size_bytes INTEGER NOT NULL DEFAULT 0,
217
+ filter_len INTEGER NOT NULL,
218
+ record_count INTEGER NOT NULL,
219
+ retired_gen INTEGER NULL,
220
+ retired_at_ms INTEGER NULL
221
+ );
222
+
223
+ CREATE INDEX IF NOT EXISTS secondary_index_runs_stream_idx
224
+ ON secondary_index_runs(stream, index_name, level, start_segment);
225
+ `;
226
+
227
+ const CREATE_LEXICON_INDEX_TABLES_SQL = `
228
+ CREATE TABLE IF NOT EXISTS lexicon_index_state (
229
+ stream TEXT NOT NULL,
230
+ source_kind TEXT NOT NULL,
231
+ source_name TEXT NOT NULL,
232
+ indexed_through INTEGER NOT NULL,
233
+ updated_at_ms INTEGER NOT NULL,
234
+ PRIMARY KEY (stream, source_kind, source_name)
235
+ );
236
+
237
+ CREATE TABLE IF NOT EXISTS lexicon_index_runs (
238
+ run_id TEXT PRIMARY KEY,
239
+ stream TEXT NOT NULL,
240
+ source_kind TEXT NOT NULL,
241
+ source_name TEXT NOT NULL,
242
+ level INTEGER NOT NULL,
243
+ start_segment INTEGER NOT NULL,
244
+ end_segment INTEGER NOT NULL,
245
+ object_key TEXT NOT NULL,
246
+ size_bytes INTEGER NOT NULL DEFAULT 0,
247
+ record_count INTEGER NOT NULL,
248
+ retired_gen INTEGER NULL,
249
+ retired_at_ms INTEGER NULL
250
+ );
251
+
252
+ CREATE INDEX IF NOT EXISTS lexicon_index_runs_stream_idx
253
+ ON lexicon_index_runs(stream, source_kind, source_name, level, start_segment);
254
+ `;
255
+
256
+ const CREATE_SEARCH_COMPANION_TABLES_SQL = `
257
+ CREATE TABLE IF NOT EXISTS search_companion_plans (
258
+ stream TEXT PRIMARY KEY,
259
+ generation INTEGER NOT NULL,
260
+ plan_hash TEXT NOT NULL,
261
+ plan_json TEXT NOT NULL,
262
+ updated_at_ms INTEGER NOT NULL
263
+ );
264
+
265
+ CREATE TABLE IF NOT EXISTS search_segment_companions (
266
+ stream TEXT NOT NULL,
267
+ segment_index INTEGER NOT NULL,
268
+ object_key TEXT NOT NULL,
269
+ plan_generation INTEGER NOT NULL,
270
+ sections_json TEXT NOT NULL,
271
+ section_sizes_json TEXT NOT NULL DEFAULT '{}',
272
+ size_bytes INTEGER NOT NULL DEFAULT 0,
273
+ primary_timestamp_min_ms INTEGER NULL,
274
+ primary_timestamp_max_ms INTEGER NULL,
275
+ updated_at_ms INTEGER NOT NULL,
276
+ PRIMARY KEY (stream, segment_index)
277
+ );
278
+
279
+ CREATE INDEX IF NOT EXISTS search_segment_companions_stream_plan_idx
280
+ ON search_segment_companions(stream, plan_generation, segment_index);
281
+ `;
282
+
283
+ const CREATE_OBJECTSTORE_REQUEST_TABLES_SQL = `
284
+ CREATE TABLE IF NOT EXISTS objectstore_request_counts (
285
+ stream_hash TEXT NOT NULL,
286
+ artifact TEXT NOT NULL,
287
+ op TEXT NOT NULL,
288
+ count INTEGER NOT NULL DEFAULT 0,
289
+ bytes INTEGER NOT NULL DEFAULT 0,
290
+ updated_at_ms INTEGER NOT NULL,
291
+ PRIMARY KEY (stream_hash, artifact, op)
292
+ );
293
+
294
+ CREATE INDEX IF NOT EXISTS objectstore_request_counts_stream_hash_idx
295
+ ON objectstore_request_counts(stream_hash, updated_at_ms);
296
+ `;
297
+
298
+ const CREATE_TABLES_V4_SUFFIX_SQL = (suffix: string): string => `
299
+ CREATE TABLE streams_${suffix} (
300
+ stream TEXT PRIMARY KEY,
301
+ created_at_ms INTEGER NOT NULL,
302
+ updated_at_ms INTEGER NOT NULL,
303
+
304
+ content_type TEXT NOT NULL,
305
+ stream_seq TEXT NULL,
306
+ closed INTEGER NOT NULL DEFAULT 0,
307
+ closed_producer_id TEXT NULL,
308
+ closed_producer_epoch INTEGER NULL,
309
+ closed_producer_seq INTEGER NULL,
310
+ ttl_seconds INTEGER NULL,
311
+
312
+ epoch INTEGER NOT NULL,
313
+ next_offset INTEGER NOT NULL,
314
+ sealed_through INTEGER NOT NULL,
315
+ uploaded_through INTEGER NOT NULL,
316
+ uploaded_segment_count INTEGER NOT NULL DEFAULT 0,
317
+
318
+ pending_rows INTEGER NOT NULL,
319
+ pending_bytes INTEGER NOT NULL,
320
+ logical_size_bytes INTEGER NOT NULL DEFAULT 0,
321
+
322
+ last_append_ms INTEGER NOT NULL,
323
+ last_segment_cut_ms INTEGER NOT NULL,
324
+ segment_in_progress INTEGER NOT NULL,
325
+
326
+ expires_at_ms INTEGER NULL,
327
+ stream_flags INTEGER NOT NULL DEFAULT 0
328
+ );
329
+
330
+ CREATE TABLE wal_${suffix} (
331
+ id INTEGER PRIMARY KEY,
332
+ stream TEXT NOT NULL,
333
+ offset INTEGER NOT NULL,
334
+ ts_ms INTEGER NOT NULL,
335
+ payload BLOB NOT NULL,
336
+ payload_len INTEGER NOT NULL,
337
+ routing_key BLOB NULL,
338
+ content_type TEXT NULL,
339
+ flags INTEGER NOT NULL DEFAULT 0
340
+ );
341
+
342
+ CREATE TABLE segments_${suffix} (
343
+ segment_id TEXT PRIMARY KEY,
344
+ stream TEXT NOT NULL,
345
+ segment_index INTEGER NOT NULL,
346
+ start_offset INTEGER NOT NULL,
347
+ end_offset INTEGER NOT NULL,
348
+ block_count INTEGER NOT NULL,
349
+ last_append_ms INTEGER NOT NULL,
350
+ size_bytes INTEGER NOT NULL,
351
+ local_path TEXT NOT NULL,
352
+ created_at_ms INTEGER NOT NULL,
353
+ uploaded_at_ms INTEGER NULL,
354
+ r2_etag TEXT NULL
355
+ );
356
+
357
+ CREATE TABLE manifests_${suffix} (
358
+ stream TEXT PRIMARY KEY,
359
+ generation INTEGER NOT NULL,
360
+ uploaded_generation INTEGER NOT NULL,
361
+ last_uploaded_at_ms INTEGER NULL,
362
+ last_uploaded_etag TEXT NULL
363
+ );
364
+
365
+ CREATE TABLE schemas_${suffix} (
366
+ stream TEXT PRIMARY KEY,
367
+ schema_json TEXT NOT NULL,
368
+ updated_at_ms INTEGER NOT NULL
369
+ );
370
+
371
+ CREATE TABLE producer_state_${suffix} (
372
+ stream TEXT NOT NULL,
373
+ producer_id TEXT NOT NULL,
374
+ epoch INTEGER NOT NULL,
375
+ last_seq INTEGER NOT NULL,
376
+ updated_at_ms INTEGER NOT NULL,
377
+ PRIMARY KEY (stream, producer_id)
378
+ );
379
+ `;
380
+
381
+ const CREATE_INDEXES_V4_SQL = `
382
+ CREATE UNIQUE INDEX IF NOT EXISTS wal_stream_offset_uniq ON wal(stream, offset);
383
+ CREATE INDEX IF NOT EXISTS wal_ts_idx ON wal(ts_ms);
384
+
385
+ CREATE INDEX IF NOT EXISTS streams_pending_bytes_idx ON streams(pending_bytes);
386
+ CREATE INDEX IF NOT EXISTS streams_last_cut_idx ON streams(last_segment_cut_ms);
387
+ CREATE INDEX IF NOT EXISTS streams_inprog_pending_idx ON streams(segment_in_progress, pending_bytes, last_segment_cut_ms);
388
+
389
+ CREATE UNIQUE INDEX IF NOT EXISTS segments_stream_index_uniq ON segments(stream, segment_index);
390
+ CREATE INDEX IF NOT EXISTS segments_stream_start_idx ON segments(stream, start_offset);
391
+ CREATE INDEX IF NOT EXISTS segments_pending_upload_idx ON segments(uploaded_at_ms);
392
+ `;
393
+
394
+ export function initSchema(db: SqliteDatabase, opts: { skipMigrations?: boolean } = {}): void {
395
+ db.exec(DEFAULT_PRAGMAS_SQL);
396
+
397
+ // Some worker processes only need read/write access to existing tables and
398
+ // should avoid concurrent schema init/migration work.
399
+ if (opts.skipMigrations) return;
400
+
401
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);`);
402
+
403
+ const readSchemaVersion = (): number | null => {
404
+ const row = db.query("SELECT version FROM schema_version LIMIT 1;").get() as any;
405
+ if (!row) return null;
406
+ const raw = row.version;
407
+ if (typeof raw === "bigint") return Number(raw);
408
+ if (typeof raw === "number") return raw;
409
+ return Number(raw);
410
+ };
411
+
412
+ const version0 = readSchemaVersion();
413
+ if (version0 == null) {
414
+ db.exec(CREATE_TABLES_V4_SQL);
415
+ db.exec(CREATE_INDEX_TABLES_SQL);
416
+ db.exec(CREATE_SECONDARY_INDEX_TABLES_SQL);
417
+ db.exec(CREATE_LEXICON_INDEX_TABLES_SQL);
418
+ db.exec(CREATE_SEARCH_COMPANION_TABLES_SQL);
419
+ db.exec(CREATE_OBJECTSTORE_REQUEST_TABLES_SQL);
420
+ db.query("INSERT INTO schema_version(version) VALUES (?);").run(SCHEMA_VERSION);
421
+ return;
422
+ }
423
+
424
+ if (version0 === SCHEMA_VERSION) return;
425
+
426
+ let version = version0;
427
+ while (version !== SCHEMA_VERSION) {
428
+ if (version === 1) {
429
+ migrateV1ToV4(db);
430
+ } else if (version === 2) {
431
+ migrateV2ToV4(db);
432
+ } else if (version === 3) {
433
+ migrateV3ToV4(db);
434
+ } else if (version === 4) {
435
+ migrateV4ToV5(db);
436
+ } else if (version === 5) {
437
+ migrateV5ToV6(db);
438
+ } else if (version === 6) {
439
+ migrateV6ToV7(db);
440
+ } else if (version === 7) {
441
+ migrateV7ToV8(db);
442
+ } else if (version === 8) {
443
+ migrateV8ToV9(db);
444
+ } else if (version === 9) {
445
+ migrateV9ToV10(db);
446
+ } else if (version === 10) {
447
+ migrateV10ToV11(db);
448
+ } else if (version === 11) {
449
+ migrateV11ToV12(db);
450
+ } else if (version === 12) {
451
+ migrateV12ToV13(db);
452
+ } else if (version === 13) {
453
+ migrateV13ToV14(db);
454
+ } else if (version === 14) {
455
+ migrateV14ToV15(db);
456
+ } else if (version === 15) {
457
+ migrateV15ToV16(db);
458
+ } else if (version === 16) {
459
+ migrateV16ToV17(db);
460
+ } else if (version === 17) {
461
+ migrateV17ToV18(db);
462
+ } else if (version === 18) {
463
+ migrateV18ToV19(db);
464
+ } else if (version === 19) {
465
+ migrateV19ToV20(db);
466
+ } else if (version === 20) {
467
+ migrateV20ToV21(db);
468
+ } else if (version === 21) {
469
+ migrateV21ToV22(db);
470
+ } else if (version === 22) {
471
+ migrateV22ToV23(db);
472
+ } else if (version === 23) {
473
+ migrateV23ToV24(db);
474
+ } else {
475
+ throw dsError(`unexpected schema version: ${version} (expected ${SCHEMA_VERSION})`);
476
+ }
477
+ const next = readSchemaVersion();
478
+ if (next == null) throw dsError("schema_version row missing after migration");
479
+ version = next;
480
+ }
481
+ }
482
+
483
+ function migrateV1ToV4(db: SqliteDatabase): void {
484
+ const tx = db.transaction(() => {
485
+ db.exec(CREATE_TABLES_V4_SUFFIX_SQL("v4"));
486
+
487
+ // Streams
488
+ db.exec(`
489
+ INSERT INTO streams_v4(
490
+ stream, created_at_ms, updated_at_ms,
491
+ content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
492
+ epoch,
493
+ next_offset, sealed_through, uploaded_through,
494
+ pending_rows, pending_bytes,
495
+ last_append_ms, last_segment_cut_ms, segment_in_progress,
496
+ expires_at_ms, stream_flags
497
+ )
498
+ SELECT
499
+ stream,
500
+ CAST(created_at_ns / 1000000 AS INTEGER),
501
+ CAST(updated_at_ns / 1000000 AS INTEGER),
502
+ 'application/octet-stream',
503
+ NULL,
504
+ 0,
505
+ NULL,
506
+ NULL,
507
+ NULL,
508
+ NULL,
509
+ epoch,
510
+ next_seq,
511
+ sealed_through_seq,
512
+ uploaded_through_seq,
513
+ pending_rows,
514
+ pending_bytes,
515
+ CAST(last_append_ns / 1000000 AS INTEGER),
516
+ CAST(last_segment_cut_ns / 1000000 AS INTEGER),
517
+ segment_in_progress,
518
+ CASE WHEN expires_at_ns IS NULL THEN NULL ELSE CAST(expires_at_ns / 1000000 AS INTEGER) END,
519
+ CASE WHEN deleted != 0 THEN 1 ELSE 0 END
520
+ FROM streams;
521
+ `);
522
+
523
+ // WAL
524
+ db.exec(`
525
+ INSERT INTO wal_v4(
526
+ stream, offset, ts_ms, payload, payload_len, routing_key, content_type, flags
527
+ )
528
+ SELECT
529
+ stream,
530
+ seq,
531
+ CAST(append_ns / 1000000 AS INTEGER),
532
+ payload,
533
+ payload_len,
534
+ CASE WHEN routing_key IS NULL THEN NULL ELSE CAST(routing_key AS BLOB) END,
535
+ CASE WHEN is_json != 0 THEN 'application/json' ELSE NULL END,
536
+ 0
537
+ FROM wal;
538
+ `);
539
+
540
+ // Segments
541
+ db.exec(`
542
+ INSERT INTO segments_v4(
543
+ segment_id, stream, segment_index, start_offset, end_offset, block_count,
544
+ last_append_ms, size_bytes, local_path, created_at_ms, uploaded_at_ms, r2_etag
545
+ )
546
+ SELECT
547
+ segment_id,
548
+ stream,
549
+ segment_index,
550
+ start_seq,
551
+ end_seq,
552
+ block_count,
553
+ CAST(last_append_ns / 1000000 AS INTEGER),
554
+ size_bytes,
555
+ local_path,
556
+ CAST(created_at_ns / 1000000 AS INTEGER),
557
+ CASE WHEN uploaded_at_ns IS NULL THEN NULL ELSE CAST(uploaded_at_ns / 1000000 AS INTEGER) END,
558
+ NULL
559
+ FROM segments;
560
+ `);
561
+
562
+ // Manifests
563
+ db.exec(`
564
+ INSERT INTO manifests_v4(
565
+ stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag
566
+ )
567
+ SELECT
568
+ stream,
569
+ generation,
570
+ uploaded_generation,
571
+ CASE WHEN last_uploaded_at_ns IS NULL THEN NULL ELSE CAST(last_uploaded_at_ns / 1000000 AS INTEGER) END,
572
+ last_uploaded_etag
573
+ FROM manifests;
574
+ `);
575
+
576
+ // Schemas
577
+ db.exec(`
578
+ INSERT INTO schemas_v4(stream, schema_json, updated_at_ms)
579
+ SELECT stream, schema_json, CAST(updated_at_ns / 1000000 AS INTEGER)
580
+ FROM schemas;
581
+ `);
582
+
583
+ db.exec(`DROP TABLE wal;`);
584
+ db.exec(`DROP TABLE streams;`);
585
+ db.exec(`DROP TABLE segments;`);
586
+ db.exec(`DROP TABLE manifests;`);
587
+ db.exec(`DROP TABLE schemas;`);
588
+
589
+ db.exec(`ALTER TABLE streams_v4 RENAME TO streams;`);
590
+ db.exec(`ALTER TABLE wal_v4 RENAME TO wal;`);
591
+ db.exec(`ALTER TABLE segments_v4 RENAME TO segments;`);
592
+ db.exec(`ALTER TABLE manifests_v4 RENAME TO manifests;`);
593
+ db.exec(`ALTER TABLE schemas_v4 RENAME TO schemas;`);
594
+ db.exec(`ALTER TABLE producer_state_v4 RENAME TO producer_state;`);
595
+
596
+ db.exec(CREATE_INDEXES_V4_SQL);
597
+
598
+ db.exec(CREATE_INDEX_TABLES_SQL);
599
+ db.exec(`UPDATE schema_version SET version = 4;`);
600
+ });
601
+
602
+ tx();
603
+ }
604
+
605
+ function migrateV2ToV4(db: SqliteDatabase): void {
606
+ const tx = db.transaction(() => {
607
+ db.exec(`ALTER TABLE segments ADD COLUMN block_count INTEGER NOT NULL DEFAULT 0;`);
608
+ db.exec(`ALTER TABLE segments ADD COLUMN last_append_ms INTEGER NOT NULL DEFAULT 0;`);
609
+
610
+ db.exec(`ALTER TABLE streams ADD COLUMN content_type TEXT NOT NULL DEFAULT 'application/octet-stream';`);
611
+ db.exec(`ALTER TABLE streams ADD COLUMN stream_seq TEXT NULL;`);
612
+ db.exec(`ALTER TABLE streams ADD COLUMN closed INTEGER NOT NULL DEFAULT 0;`);
613
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_id TEXT NULL;`);
614
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_epoch INTEGER NULL;`);
615
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_seq INTEGER NULL;`);
616
+ db.exec(`ALTER TABLE streams ADD COLUMN ttl_seconds INTEGER NULL;`);
617
+
618
+ db.exec(`
619
+ CREATE TABLE IF NOT EXISTS producer_state (
620
+ stream TEXT NOT NULL,
621
+ producer_id TEXT NOT NULL,
622
+ epoch INTEGER NOT NULL,
623
+ last_seq INTEGER NOT NULL,
624
+ updated_at_ms INTEGER NOT NULL,
625
+ PRIMARY KEY (stream, producer_id)
626
+ );
627
+ `);
628
+ db.exec(CREATE_INDEX_TABLES_SQL);
629
+ db.exec(`UPDATE schema_version SET version = 4;`);
630
+ });
631
+
632
+ tx();
633
+ }
634
+
635
+ function migrateV3ToV4(db: SqliteDatabase): void {
636
+ const tx = db.transaction(() => {
637
+ db.exec(`ALTER TABLE streams ADD COLUMN content_type TEXT NOT NULL DEFAULT 'application/octet-stream';`);
638
+ db.exec(`ALTER TABLE streams ADD COLUMN stream_seq TEXT NULL;`);
639
+ db.exec(`ALTER TABLE streams ADD COLUMN closed INTEGER NOT NULL DEFAULT 0;`);
640
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_id TEXT NULL;`);
641
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_epoch INTEGER NULL;`);
642
+ db.exec(`ALTER TABLE streams ADD COLUMN closed_producer_seq INTEGER NULL;`);
643
+ db.exec(`ALTER TABLE streams ADD COLUMN ttl_seconds INTEGER NULL;`);
644
+
645
+ db.exec(`
646
+ CREATE TABLE IF NOT EXISTS producer_state (
647
+ stream TEXT NOT NULL,
648
+ producer_id TEXT NOT NULL,
649
+ epoch INTEGER NOT NULL,
650
+ last_seq INTEGER NOT NULL,
651
+ updated_at_ms INTEGER NOT NULL,
652
+ PRIMARY KEY (stream, producer_id)
653
+ );
654
+ `);
655
+ db.exec(CREATE_INDEX_TABLES_SQL);
656
+ db.exec(`UPDATE schema_version SET version = 4;`);
657
+ });
658
+
659
+ tx();
660
+ }
661
+
662
+ function migrateV4ToV5(db: SqliteDatabase): void {
663
+ const tx = db.transaction(() => {
664
+ db.exec(CREATE_INDEX_TABLES_SQL);
665
+ db.exec(`UPDATE schema_version SET version = 5;`);
666
+ });
667
+ tx();
668
+ }
669
+
670
+ function migrateV5ToV6(db: SqliteDatabase): void {
671
+ const tx = db.transaction(() => {
672
+ db.exec(`ALTER TABLE streams ADD COLUMN uploaded_segment_count INTEGER NOT NULL DEFAULT 0;`);
673
+ db.exec(`
674
+ CREATE TABLE IF NOT EXISTS stream_segment_meta (
675
+ stream TEXT PRIMARY KEY,
676
+ segment_count INTEGER NOT NULL,
677
+ segment_offsets BLOB NOT NULL,
678
+ segment_blocks BLOB NOT NULL,
679
+ segment_last_ts BLOB NOT NULL
680
+ );
681
+ `);
682
+ db.exec(`UPDATE schema_version SET version = 6;`);
683
+ });
684
+ tx();
685
+ }
686
+
687
+ function migrateV6ToV7(db: SqliteDatabase): void {
688
+ const tx = db.transaction(() => {
689
+ db.exec(`
690
+ CREATE TABLE IF NOT EXISTS stream_touch_state (
691
+ stream TEXT PRIMARY KEY,
692
+ processed_through INTEGER NOT NULL,
693
+ updated_at_ms INTEGER NOT NULL
694
+ );
695
+ `);
696
+ db.exec(`UPDATE schema_version SET version = 7;`);
697
+ });
698
+ tx();
699
+ }
700
+
701
+ function migrateV7ToV8(db: SqliteDatabase): void {
702
+ const tx = db.transaction(() => {
703
+ db.exec(`UPDATE schema_version SET version = 8;`);
704
+ });
705
+ tx();
706
+ }
707
+
708
+ function migrateV8ToV9(db: SqliteDatabase): void {
709
+ const tx = db.transaction(() => {
710
+ db.exec(`
711
+ CREATE TABLE IF NOT EXISTS live_templates (
712
+ stream TEXT NOT NULL,
713
+ template_id TEXT NOT NULL,
714
+ entity TEXT NOT NULL,
715
+ fields_json TEXT NOT NULL,
716
+ encodings_json TEXT NOT NULL,
717
+ state TEXT NOT NULL,
718
+ created_at_ms INTEGER NOT NULL,
719
+ last_seen_at_ms INTEGER NOT NULL,
720
+ inactivity_ttl_ms INTEGER NOT NULL,
721
+ active_from_source_offset INTEGER NOT NULL,
722
+ retired_at_ms INTEGER NULL,
723
+ retired_reason TEXT NULL,
724
+ PRIMARY KEY (stream, template_id)
725
+ );
726
+ `);
727
+ db.exec(`
728
+ CREATE INDEX IF NOT EXISTS live_templates_stream_entity_state_last_seen_idx
729
+ ON live_templates(stream, entity, state, last_seen_at_ms);
730
+ `);
731
+ db.exec(`
732
+ CREATE INDEX IF NOT EXISTS live_templates_stream_state_last_seen_idx
733
+ ON live_templates(stream, state, last_seen_at_ms);
734
+ `);
735
+ db.exec(`UPDATE schema_version SET version = 9;`);
736
+ });
737
+ tx();
738
+ }
739
+
740
+ function migrateV9ToV10(db: SqliteDatabase): void {
741
+ const tx = db.transaction(() => {
742
+ db.exec(`ALTER TABLE streams ADD COLUMN wal_rows INTEGER NOT NULL DEFAULT 0;`);
743
+ db.exec(`ALTER TABLE streams ADD COLUMN wal_bytes INTEGER NOT NULL DEFAULT 0;`);
744
+
745
+ // Backfill current retained WAL rows/bytes per stream so metrics are correct after upgrade.
746
+ db.exec(`DROP TABLE IF EXISTS temp.wal_stats;`);
747
+ db.exec(`
748
+ CREATE TEMP TABLE wal_stats AS
749
+ SELECT stream, COUNT(*) as rows, COALESCE(SUM(payload_len), 0) as bytes
750
+ FROM wal
751
+ GROUP BY stream;
752
+ `);
753
+ db.exec(`
754
+ UPDATE streams
755
+ SET wal_rows = COALESCE((SELECT rows FROM wal_stats WHERE wal_stats.stream = streams.stream), 0),
756
+ wal_bytes = COALESCE((SELECT bytes FROM wal_stats WHERE wal_stats.stream = streams.stream), 0);
757
+ `);
758
+ db.exec(`DROP TABLE wal_stats;`);
759
+
760
+ db.exec(`UPDATE schema_version SET version = 10;`);
761
+ });
762
+ tx();
763
+ }
764
+
765
+ function migrateV10ToV11(db: SqliteDatabase): void {
766
+ const tx = db.transaction(() => {
767
+ db.exec(`DROP INDEX IF EXISTS wal_touch_stream_rk_offset_idx;`);
768
+ db.exec(`UPDATE schema_version SET version = 11;`);
769
+ });
770
+ tx();
771
+ }
772
+
773
+ function migrateV11ToV12(db: SqliteDatabase): void {
774
+ const tx = db.transaction(() => {
775
+ db.exec(`ALTER TABLE streams ADD COLUMN profile TEXT NULL;`);
776
+ db.exec(`UPDATE schema_version SET version = 12;`);
777
+ });
778
+ tx();
779
+ }
780
+
781
+ function migrateV12ToV13(db: SqliteDatabase): void {
782
+ const tx = db.transaction(() => {
783
+ db.exec(`
784
+ CREATE TABLE IF NOT EXISTS stream_profiles (
785
+ stream TEXT PRIMARY KEY,
786
+ profile_json TEXT NOT NULL,
787
+ updated_at_ms INTEGER NOT NULL
788
+ );
789
+ `);
790
+ db.exec(`UPDATE schema_version SET version = 13;`);
791
+ });
792
+ tx();
793
+ }
794
+
795
+ function migrateV13ToV14(db: SqliteDatabase): void {
796
+ const tx = db.transaction(() => {
797
+ db.exec(`
798
+ CREATE TABLE IF NOT EXISTS stream_touch_state (
799
+ stream TEXT PRIMARY KEY,
800
+ processed_through INTEGER NOT NULL,
801
+ updated_at_ms INTEGER NOT NULL
802
+ );
803
+ `);
804
+
805
+ const hasLegacy = !!db
806
+ .query(`SELECT name FROM sqlite_master WHERE type='table' AND name='stream_interpreters' LIMIT 1;`)
807
+ .get();
808
+ if (hasLegacy) {
809
+ db.exec(`
810
+ INSERT OR REPLACE INTO stream_touch_state(stream, processed_through, updated_at_ms)
811
+ SELECT stream, interpreted_through, updated_at_ms
812
+ FROM stream_interpreters;
813
+ `);
814
+ db.exec(`DROP TABLE stream_interpreters;`);
815
+ }
816
+
817
+ db.exec(`UPDATE schema_version SET version = ${SCHEMA_VERSION};`);
818
+ });
819
+ tx();
820
+ }
821
+
822
+ function migrateV14ToV15(db: SqliteDatabase): void {
823
+ const tx = db.transaction(() => {
824
+ db.exec(CREATE_SECONDARY_INDEX_TABLES_SQL);
825
+ db.exec(`UPDATE schema_version SET version = 15;`);
826
+ });
827
+ tx();
828
+ }
829
+
830
+ function migrateV15ToV16(db: SqliteDatabase): void {
831
+ const tx = db.transaction(() => {
832
+ db.exec(`UPDATE schema_version SET version = 16;`);
833
+ });
834
+ tx();
835
+ }
836
+
837
+ function migrateV16ToV17(db: SqliteDatabase): void {
838
+ const tx = db.transaction(() => {
839
+ db.exec(`ALTER TABLE streams ADD COLUMN logical_size_bytes INTEGER NOT NULL DEFAULT 0;`);
840
+
841
+ // Streams that still live entirely in the retained WAL can be backfilled
842
+ // cheaply here. Streams with published segments are repaired asynchronously
843
+ // at runtime from segment objects if this value is still missing.
844
+ db.exec(`
845
+ UPDATE streams
846
+ SET logical_size_bytes = wal_bytes
847
+ WHERE next_offset = wal_rows;
848
+ `);
849
+
850
+ db.exec(`UPDATE schema_version SET version = 17;`);
851
+ });
852
+ tx();
853
+ }
854
+
855
+ function migrateV17ToV18(db: SqliteDatabase): void {
856
+ const tx = db.transaction(() => {
857
+ db.exec(CREATE_SEARCH_COMPANION_TABLES_SQL);
858
+ db.exec(`UPDATE schema_version SET version = 18;`);
859
+ });
860
+ tx();
861
+ }
862
+
863
+ function migrateV18ToV19(db: SqliteDatabase): void {
864
+ const tx = db.transaction(() => {
865
+ db.exec(`ALTER TABLE secondary_index_state ADD COLUMN config_hash TEXT NOT NULL DEFAULT '';`);
866
+ db.exec(`DROP INDEX IF EXISTS search_family_segments_stream_idx;`);
867
+ db.exec(`DROP TABLE IF EXISTS search_family_segments;`);
868
+ db.exec(`DROP TABLE IF EXISTS search_family_state;`);
869
+ db.exec(`UPDATE schema_version SET version = 19;`);
870
+ });
871
+ tx();
872
+ }
873
+
874
+ function migrateV19ToV20(db: SqliteDatabase): void {
875
+ const tx = db.transaction(() => {
876
+ db.exec(`ALTER TABLE manifests ADD COLUMN last_uploaded_size_bytes INTEGER NULL;`);
877
+ db.exec(`ALTER TABLE schemas ADD COLUMN uploaded_size_bytes INTEGER NOT NULL DEFAULT 0;`);
878
+ db.exec(`ALTER TABLE index_runs ADD COLUMN size_bytes INTEGER NOT NULL DEFAULT 0;`);
879
+ db.exec(`ALTER TABLE secondary_index_runs ADD COLUMN size_bytes INTEGER NOT NULL DEFAULT 0;`);
880
+ db.exec(`ALTER TABLE search_segment_companions ADD COLUMN section_sizes_json TEXT NOT NULL DEFAULT '{}';`);
881
+ db.exec(`ALTER TABLE search_segment_companions ADD COLUMN size_bytes INTEGER NOT NULL DEFAULT 0;`);
882
+ db.exec(CREATE_OBJECTSTORE_REQUEST_TABLES_SQL);
883
+ db.exec(`UPDATE schema_version SET version = 20;`);
884
+ });
885
+ tx();
886
+ }
887
+
888
+ function migrateV20ToV21(db: SqliteDatabase): void {
889
+ const tx = db.transaction(() => {
890
+ db.exec(`ALTER TABLE search_segment_companions ADD COLUMN primary_timestamp_min_ms INTEGER NULL;`);
891
+ db.exec(`ALTER TABLE search_segment_companions ADD COLUMN primary_timestamp_max_ms INTEGER NULL;`);
892
+ db.exec(`UPDATE schema_version SET version = 21;`);
893
+ });
894
+ tx();
895
+ }
896
+
897
+ function migrateV21ToV22(db: SqliteDatabase): void {
898
+ const tx = db.transaction(() => {
899
+ db.exec(CREATE_LEXICON_INDEX_TABLES_SQL);
900
+ db.exec(`UPDATE schema_version SET version = 22;`);
901
+ });
902
+ tx();
903
+ }
904
+
905
+ function migrateV22ToV23(db: SqliteDatabase): void {
906
+ const tx = db.transaction(() => {
907
+ db.exec(`DROP INDEX IF EXISTS wal_stream_offset_idx;`);
908
+ db.exec(`UPDATE schema_version SET version = 23;`);
909
+ });
910
+ tx();
911
+ }
912
+
913
+ function migrateV23ToV24(db: SqliteDatabase): void {
914
+ const tx = db.transaction(() => {
915
+ const hasPayloadBytes = db
916
+ .query(`PRAGMA table_info(segments);`)
917
+ .all()
918
+ .some((row: any) => String(row.name) === "payload_bytes");
919
+ if (!hasPayloadBytes) {
920
+ db.exec(`ALTER TABLE segments ADD COLUMN payload_bytes INTEGER NOT NULL DEFAULT 0;`);
921
+ }
922
+ db.exec(`UPDATE schema_version SET version = 24;`);
923
+ });
924
+ tx();
925
+ }