@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,132 @@
1
+ import type { Pool } from "pg";
2
+ import type { ManifestGenerationRow } from "../store/index_store";
3
+ import type { SegmentRow, StreamRow } from "../store/rows";
4
+ import type { PgExecutor, PgStreamRow } from "./types";
5
+
6
+ export class PostgresIndexSharedStore {
7
+ constructor(protected readonly pool: Pool, private readonly currentTimeMs: () => bigint) {}
8
+
9
+ nowMs(): bigint {
10
+ return this.currentTimeMs();
11
+ }
12
+
13
+ async getStream(stream: string): Promise<StreamRow | null> {
14
+ return getStreamWithExecutor(this.pool, stream);
15
+ }
16
+
17
+ async getSegmentByIndex(stream: string, segmentIndex: number): Promise<SegmentRow | null> {
18
+ return getSegmentByIndexWithExecutor(this.pool, stream, segmentIndex);
19
+ }
20
+
21
+ async countUploadedSegments(stream: string): Promise<number> {
22
+ const res = await this.pool.query<{ max_idx: number | string | null }>(
23
+ `SELECT MAX(segment_index) AS max_idx FROM segments WHERE stream = $1 AND r2_etag IS NOT NULL;`,
24
+ [stream]
25
+ );
26
+ const maxIdx = res.rows[0]?.max_idx == null ? -1 : Number(res.rows[0]!.max_idx);
27
+ return maxIdx >= 0 ? maxIdx + 1 : 0;
28
+ }
29
+
30
+ async countSegmentsForStream(stream: string): Promise<number> {
31
+ const res = await this.pool.query<{ count: string }>(`SELECT COUNT(*) AS count FROM segments WHERE stream = $1;`, [stream]);
32
+ return Number(res.rows[0]?.count ?? 0);
33
+ }
34
+
35
+ async getManifestRow(stream: string): Promise<ManifestGenerationRow> {
36
+ return getManifestGenerationWithExecutor(this.pool, stream);
37
+ }
38
+ }
39
+
40
+ export async function getStreamWithExecutor(executor: PgExecutor, stream: string): Promise<StreamRow | null> {
41
+ const res = await executor.query<PgStreamRow>(`SELECT * FROM streams WHERE stream = $1;`, [stream]);
42
+ return res.rows[0] ? coerceStreamRow(res.rows[0]) : null;
43
+ }
44
+
45
+ export async function getSegmentByIndexWithExecutor(executor: PgExecutor, stream: string, segmentIndex: number): Promise<SegmentRow | null> {
46
+ const res = await executor.query(
47
+ `SELECT segment_id, stream, segment_index, start_offset, end_offset, block_count, last_append_ms,
48
+ payload_bytes, size_bytes, local_path, created_at_ms, uploaded_at_ms, r2_etag
49
+ FROM segments
50
+ WHERE stream = $1 AND segment_index = $2
51
+ LIMIT 1;`,
52
+ [stream, segmentIndex]
53
+ );
54
+ return res.rows[0] ? coerceSegmentRow(res.rows[0]) : null;
55
+ }
56
+
57
+ async function getManifestGenerationWithExecutor(executor: PgExecutor, stream: string): Promise<ManifestGenerationRow> {
58
+ const res = await executor.query<{ generation: number | string }>(`SELECT generation FROM manifests WHERE stream = $1 LIMIT 1;`, [stream]);
59
+ if (res.rows[0]) return { generation: Number(res.rows[0].generation) };
60
+ await executor.query(
61
+ `INSERT INTO manifests(stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes)
62
+ VALUES($1, 0, 0, NULL, NULL, NULL)
63
+ ON CONFLICT(stream) DO NOTHING;`,
64
+ [stream]
65
+ );
66
+ return { generation: 0 };
67
+ }
68
+
69
+ export function coerceSegmentRow(row: any): SegmentRow {
70
+ return {
71
+ segment_id: String(row.segment_id),
72
+ stream: String(row.stream),
73
+ segment_index: Number(row.segment_index),
74
+ start_offset: toBigInt(row.start_offset),
75
+ end_offset: toBigInt(row.end_offset),
76
+ block_count: Number(row.block_count),
77
+ last_append_ms: toBigInt(row.last_append_ms),
78
+ payload_bytes: toBigInt(row.payload_bytes),
79
+ size_bytes: Number(row.size_bytes),
80
+ local_path: String(row.local_path),
81
+ created_at_ms: toBigInt(row.created_at_ms),
82
+ uploaded_at_ms: row.uploaded_at_ms == null ? null : toBigInt(row.uploaded_at_ms),
83
+ r2_etag: row.r2_etag == null ? null : String(row.r2_etag),
84
+ };
85
+ }
86
+
87
+ function coerceStreamRow(row: PgStreamRow): StreamRow {
88
+ const walRows = toBigInt(row.wal_rows);
89
+ const walBytes = toBigInt(row.wal_bytes);
90
+ const lastAppendMs = toBigInt(row.last_append_ms);
91
+ return {
92
+ stream: String(row.stream),
93
+ created_at_ms: toBigInt(row.created_at_ms),
94
+ updated_at_ms: toBigInt(row.updated_at_ms),
95
+ content_type: String(row.content_type),
96
+ profile: row.profile == null ? null : String(row.profile),
97
+ stream_seq: row.stream_seq == null ? null : String(row.stream_seq),
98
+ closed: Number(row.closed),
99
+ closed_producer_id: row.closed_producer_id == null ? null : String(row.closed_producer_id),
100
+ closed_producer_epoch: row.closed_producer_epoch == null ? null : Number(row.closed_producer_epoch),
101
+ closed_producer_seq: row.closed_producer_seq == null ? null : Number(row.closed_producer_seq),
102
+ ttl_seconds: row.ttl_seconds == null ? null : Number(row.ttl_seconds),
103
+ epoch: Number(row.epoch),
104
+ next_offset: toBigInt(row.next_offset),
105
+ sealed_through: row.sealed_through == null ? -1n : toBigInt(row.sealed_through),
106
+ uploaded_through: row.uploaded_through == null ? -1n : toBigInt(row.uploaded_through),
107
+ uploaded_segment_count: row.uploaded_segment_count == null ? 0 : Number(row.uploaded_segment_count),
108
+ pending_rows: row.pending_rows == null ? walRows : toBigInt(row.pending_rows),
109
+ pending_bytes: row.pending_bytes == null ? walBytes : toBigInt(row.pending_bytes),
110
+ logical_size_bytes: toBigInt(row.logical_size_bytes),
111
+ wal_rows: walRows,
112
+ wal_bytes: walBytes,
113
+ last_append_ms: lastAppendMs,
114
+ last_segment_cut_ms: row.last_segment_cut_ms == null ? lastAppendMs : toBigInt(row.last_segment_cut_ms),
115
+ segment_in_progress: row.segment_in_progress == null ? 0 : Number(row.segment_in_progress),
116
+ expires_at_ms: row.expires_at_ms == null ? null : toBigInt(row.expires_at_ms),
117
+ stream_flags: Number(row.stream_flags),
118
+ };
119
+ }
120
+
121
+ export function pgInt(value: bigint): string {
122
+ return value.toString();
123
+ }
124
+
125
+ export function toBigInt(value: unknown): bigint {
126
+ return typeof value === "bigint" ? value : BigInt(String(value));
127
+ }
128
+
129
+ export function toBytes(value: unknown): Uint8Array {
130
+ if (value instanceof Uint8Array) return value;
131
+ return new Uint8Array(value as ArrayBuffer);
132
+ }
@@ -0,0 +1,355 @@
1
+ import type { Pool } from "pg";
2
+ import { dsError } from "../util/ds_error";
3
+
4
+ export const POSTGRES_SCHEMA_VERSION = 1;
5
+
6
+ type PgSchemaExecutor = Pick<Pool, "query">;
7
+
8
+ export type PostgresMigrationOptions = {
9
+ fullMode?: boolean;
10
+ };
11
+
12
+ export async function migratePostgresStore(pool: Pool, opts: PostgresMigrationOptions = {}): Promise<void> {
13
+ const client = await pool.connect();
14
+ try {
15
+ await client.query("BEGIN");
16
+ await ensureSchemaVersionTable(client);
17
+ const currentVersion = await readPostgresSchemaVersion(client);
18
+ if (currentVersion != null && currentVersion !== POSTGRES_SCHEMA_VERSION) {
19
+ throw dsError(`postgres schema version ${currentVersion} is not supported by version ${POSTGRES_SCHEMA_VERSION}`);
20
+ }
21
+ await installWalControlPlaneSchema(client);
22
+ if (opts.fullMode) {
23
+ await installFullModeSegmentSchema(client);
24
+ await installFullModeIndexSchema(client);
25
+ await installFullModeTouchSchema(client);
26
+ await installFullModeStatsSchema(client);
27
+ }
28
+ await setPostgresSchemaVersion(client, POSTGRES_SCHEMA_VERSION);
29
+ await client.query("COMMIT");
30
+ } catch (error) {
31
+ await client.query("ROLLBACK").catch(() => {});
32
+ throw error;
33
+ } finally {
34
+ client.release();
35
+ }
36
+ }
37
+
38
+ export async function readPostgresSchemaVersion(executor: PgSchemaExecutor): Promise<number | null> {
39
+ const table = await executor.query<{ exists: string | null }>(`SELECT to_regclass('schema_version')::text AS exists;`);
40
+ if (!table.rows[0]?.exists) return null;
41
+ const rows = await executor.query<{ version: number | string }>(`SELECT version FROM schema_version;`);
42
+ if (rows.rows.length === 0) return null;
43
+ if (rows.rows.length > 1) throw dsError("postgres schema_version must contain exactly one row");
44
+ return Number(rows.rows[0]!.version);
45
+ }
46
+
47
+ async function ensureSchemaVersionTable(executor: PgSchemaExecutor): Promise<void> {
48
+ await executor.query(`
49
+ CREATE TABLE IF NOT EXISTS schema_version (
50
+ version integer NOT NULL
51
+ );
52
+ `);
53
+ }
54
+
55
+ async function setPostgresSchemaVersion(executor: PgSchemaExecutor, version: number): Promise<void> {
56
+ await executor.query(`DELETE FROM schema_version;`);
57
+ await executor.query(`INSERT INTO schema_version(version) VALUES($1);`, [version]);
58
+ }
59
+
60
+ async function installWalControlPlaneSchema(executor: PgSchemaExecutor): Promise<void> {
61
+ await executor.query(`
62
+ CREATE TABLE IF NOT EXISTS streams (
63
+ stream text PRIMARY KEY,
64
+ created_at_ms bigint NOT NULL,
65
+ updated_at_ms bigint NOT NULL,
66
+ content_type text NOT NULL,
67
+ profile text NULL,
68
+ stream_seq text NULL,
69
+ closed integer NOT NULL DEFAULT 0,
70
+ closed_producer_id text NULL,
71
+ closed_producer_epoch integer NULL,
72
+ closed_producer_seq integer NULL,
73
+ ttl_seconds integer NULL,
74
+ epoch integer NOT NULL DEFAULT 0,
75
+ next_offset bigint NOT NULL DEFAULT 0,
76
+ logical_size_bytes bigint NOT NULL DEFAULT 0,
77
+ wal_rows bigint NOT NULL DEFAULT 0,
78
+ wal_bytes bigint NOT NULL DEFAULT 0,
79
+ last_append_ms bigint NOT NULL,
80
+ expires_at_ms bigint NULL,
81
+ stream_flags integer NOT NULL DEFAULT 0
82
+ );
83
+ `);
84
+ await executor.query(`
85
+ CREATE TABLE IF NOT EXISTS wal (
86
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
87
+ "offset" bigint NOT NULL,
88
+ ts_ms bigint NOT NULL,
89
+ payload bytea NOT NULL,
90
+ payload_len integer NOT NULL,
91
+ routing_key bytea NULL,
92
+ content_type text NULL,
93
+ flags integer NOT NULL DEFAULT 0,
94
+ PRIMARY KEY (stream, "offset")
95
+ );
96
+ `);
97
+ await executor.query(`
98
+ CREATE TABLE IF NOT EXISTS schemas (
99
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
100
+ schema_json text NOT NULL,
101
+ updated_at_ms bigint NOT NULL
102
+ );
103
+ `);
104
+ await executor.query(`
105
+ CREATE TABLE IF NOT EXISTS stream_profiles (
106
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
107
+ profile_json text NOT NULL,
108
+ updated_at_ms bigint NOT NULL
109
+ );
110
+ `);
111
+ await executor.query(`
112
+ CREATE TABLE IF NOT EXISTS producer_state (
113
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
114
+ producer_id text NOT NULL,
115
+ epoch integer NOT NULL,
116
+ last_seq integer NOT NULL,
117
+ updated_at_ms bigint NOT NULL,
118
+ PRIMARY KEY (stream, producer_id)
119
+ );
120
+ `);
121
+ await executor.query(`CREATE INDEX IF NOT EXISTS wal_stream_offset_idx ON wal(stream, "offset");`);
122
+ await executor.query(`CREATE INDEX IF NOT EXISTS wal_stream_routing_offset_idx ON wal(stream, routing_key, "offset");`);
123
+ await executor.query(`CREATE INDEX IF NOT EXISTS streams_updated_at_idx ON streams(updated_at_ms);`);
124
+ }
125
+
126
+ async function installFullModeSegmentSchema(executor: PgSchemaExecutor): Promise<void> {
127
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS sealed_through bigint NOT NULL DEFAULT -1;`);
128
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS uploaded_through bigint NOT NULL DEFAULT -1;`);
129
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS uploaded_segment_count integer NOT NULL DEFAULT 0;`);
130
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS pending_rows bigint NOT NULL DEFAULT 0;`);
131
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS pending_bytes bigint NOT NULL DEFAULT 0;`);
132
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS last_segment_cut_ms bigint NOT NULL DEFAULT 0;`);
133
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS segment_in_progress integer NOT NULL DEFAULT 0;`);
134
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS segment_claim_token text NULL;`);
135
+ await executor.query(`ALTER TABLE streams ADD COLUMN IF NOT EXISTS segment_claimed_at_ms bigint NULL;`);
136
+ await executor.query(`
137
+ UPDATE streams
138
+ SET pending_rows = wal_rows,
139
+ pending_bytes = wal_bytes,
140
+ last_segment_cut_ms = last_append_ms
141
+ WHERE sealed_through = -1
142
+ AND uploaded_through = -1
143
+ AND uploaded_segment_count = 0
144
+ AND segment_in_progress = 0
145
+ AND segment_claim_token IS NULL
146
+ AND pending_rows = 0
147
+ AND pending_bytes = 0;
148
+ `);
149
+ await executor.query(`
150
+ CREATE TABLE IF NOT EXISTS segments (
151
+ segment_id text PRIMARY KEY,
152
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
153
+ segment_index integer NOT NULL,
154
+ start_offset bigint NOT NULL,
155
+ end_offset bigint NOT NULL,
156
+ block_count integer NOT NULL,
157
+ last_append_ms bigint NOT NULL,
158
+ payload_bytes bigint NOT NULL DEFAULT 0,
159
+ size_bytes integer NOT NULL,
160
+ local_path text NOT NULL,
161
+ created_at_ms bigint NOT NULL,
162
+ uploaded_at_ms bigint NULL,
163
+ r2_etag text NULL,
164
+ UNIQUE(stream, segment_index)
165
+ );
166
+ `);
167
+ await executor.query(`
168
+ CREATE TABLE IF NOT EXISTS stream_segment_meta (
169
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
170
+ segment_count integer NOT NULL,
171
+ segment_offsets bytea NOT NULL,
172
+ segment_blocks bytea NOT NULL,
173
+ segment_last_ts bytea NOT NULL
174
+ );
175
+ `);
176
+ await executor.query(`
177
+ CREATE TABLE IF NOT EXISTS manifests (
178
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
179
+ generation integer NOT NULL,
180
+ uploaded_generation integer NOT NULL,
181
+ last_uploaded_at_ms bigint NULL,
182
+ last_uploaded_etag text NULL,
183
+ last_uploaded_size_bytes bigint NULL
184
+ );
185
+ `);
186
+ await executor.query(`ALTER TABLE schemas ADD COLUMN IF NOT EXISTS uploaded_size_bytes bigint NOT NULL DEFAULT 0;`);
187
+ await executor.query(`CREATE INDEX IF NOT EXISTS streams_pending_bytes_idx ON streams(pending_bytes);`);
188
+ await executor.query(`CREATE INDEX IF NOT EXISTS streams_last_cut_idx ON streams(last_segment_cut_ms);`);
189
+ await executor.query(`CREATE INDEX IF NOT EXISTS streams_inprog_pending_idx ON streams(segment_in_progress, pending_bytes, last_segment_cut_ms);`);
190
+ await executor.query(`CREATE INDEX IF NOT EXISTS segments_stream_start_idx ON segments(stream, start_offset);`);
191
+ await executor.query(`CREATE INDEX IF NOT EXISTS segments_pending_upload_idx ON segments(uploaded_at_ms);`);
192
+ }
193
+
194
+ async function installFullModeIndexSchema(executor: PgSchemaExecutor): Promise<void> {
195
+ await executor.query(`
196
+ CREATE TABLE IF NOT EXISTS index_state (
197
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
198
+ index_secret bytea NOT NULL,
199
+ indexed_through integer NOT NULL,
200
+ updated_at_ms bigint NOT NULL
201
+ );
202
+ `);
203
+ await executor.query(`
204
+ CREATE TABLE IF NOT EXISTS index_runs (
205
+ run_id text PRIMARY KEY,
206
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
207
+ level integer NOT NULL,
208
+ start_segment integer NOT NULL,
209
+ end_segment integer NOT NULL,
210
+ object_key text NOT NULL,
211
+ size_bytes bigint NOT NULL,
212
+ filter_len integer NOT NULL,
213
+ record_count integer NOT NULL,
214
+ retired_gen integer NULL,
215
+ retired_at_ms bigint NULL
216
+ );
217
+ `);
218
+ await executor.query(`
219
+ CREATE TABLE IF NOT EXISTS secondary_index_state (
220
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
221
+ index_name text NOT NULL,
222
+ index_secret bytea NOT NULL,
223
+ config_hash text NOT NULL,
224
+ indexed_through integer NOT NULL,
225
+ updated_at_ms bigint NOT NULL,
226
+ PRIMARY KEY(stream, index_name)
227
+ );
228
+ `);
229
+ await executor.query(`
230
+ CREATE TABLE IF NOT EXISTS secondary_index_runs (
231
+ run_id text PRIMARY KEY,
232
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
233
+ index_name text NOT NULL,
234
+ level integer NOT NULL,
235
+ start_segment integer NOT NULL,
236
+ end_segment integer NOT NULL,
237
+ object_key text NOT NULL,
238
+ size_bytes bigint NOT NULL,
239
+ filter_len integer NOT NULL,
240
+ record_count integer NOT NULL,
241
+ retired_gen integer NULL,
242
+ retired_at_ms bigint NULL
243
+ );
244
+ `);
245
+ await executor.query(`
246
+ CREATE TABLE IF NOT EXISTS lexicon_index_state (
247
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
248
+ source_kind text NOT NULL,
249
+ source_name text NOT NULL,
250
+ indexed_through integer NOT NULL,
251
+ updated_at_ms bigint NOT NULL,
252
+ PRIMARY KEY(stream, source_kind, source_name)
253
+ );
254
+ `);
255
+ await executor.query(`
256
+ CREATE TABLE IF NOT EXISTS lexicon_index_runs (
257
+ run_id text PRIMARY KEY,
258
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
259
+ source_kind text NOT NULL,
260
+ source_name text NOT NULL,
261
+ level integer NOT NULL,
262
+ start_segment integer NOT NULL,
263
+ end_segment integer NOT NULL,
264
+ object_key text NOT NULL,
265
+ size_bytes bigint NOT NULL,
266
+ record_count integer NOT NULL,
267
+ retired_gen integer NULL,
268
+ retired_at_ms bigint NULL
269
+ );
270
+ `);
271
+ await executor.query(`
272
+ CREATE TABLE IF NOT EXISTS search_companion_plans (
273
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
274
+ generation integer NOT NULL,
275
+ plan_hash text NOT NULL,
276
+ plan_json text NOT NULL,
277
+ updated_at_ms bigint NOT NULL
278
+ );
279
+ `);
280
+ await executor.query(`
281
+ CREATE TABLE IF NOT EXISTS search_segment_companions (
282
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
283
+ segment_index integer NOT NULL,
284
+ object_key text NOT NULL,
285
+ plan_generation integer NOT NULL,
286
+ sections_json text NOT NULL,
287
+ section_sizes_json text NOT NULL,
288
+ size_bytes bigint NOT NULL,
289
+ primary_timestamp_min_ms bigint NULL,
290
+ primary_timestamp_max_ms bigint NULL,
291
+ updated_at_ms bigint NOT NULL,
292
+ PRIMARY KEY(stream, segment_index)
293
+ );
294
+ `);
295
+ await executor.query(`CREATE INDEX IF NOT EXISTS index_runs_active_idx ON index_runs(stream, retired_gen, level, start_segment);`);
296
+ await executor.query(`CREATE INDEX IF NOT EXISTS index_runs_retired_idx ON index_runs(stream, retired_gen, retired_at_ms);`);
297
+ await executor.query(`CREATE INDEX IF NOT EXISTS secondary_index_runs_active_idx ON secondary_index_runs(stream, index_name, retired_gen, level, start_segment);`);
298
+ await executor.query(`CREATE INDEX IF NOT EXISTS secondary_index_runs_retired_idx ON secondary_index_runs(stream, index_name, retired_gen, retired_at_ms);`);
299
+ await executor.query(`CREATE INDEX IF NOT EXISTS lexicon_index_runs_active_idx ON lexicon_index_runs(stream, source_kind, source_name, retired_gen, level, start_segment);`);
300
+ await executor.query(`CREATE INDEX IF NOT EXISTS lexicon_index_runs_retired_idx ON lexicon_index_runs(stream, source_kind, source_name, retired_gen, retired_at_ms);`);
301
+ await executor.query(`CREATE INDEX IF NOT EXISTS search_segment_companions_plan_idx ON search_segment_companions(stream, plan_generation, segment_index);`);
302
+ }
303
+
304
+ async function installFullModeTouchSchema(executor: PgSchemaExecutor): Promise<void> {
305
+ await executor.query(`
306
+ CREATE TABLE IF NOT EXISTS stream_touch_state (
307
+ stream text PRIMARY KEY REFERENCES streams(stream) ON DELETE CASCADE,
308
+ processed_through bigint NOT NULL,
309
+ updated_at_ms bigint NOT NULL
310
+ );
311
+ `);
312
+ await executor.query(`
313
+ CREATE TABLE IF NOT EXISTS live_templates (
314
+ stream text NOT NULL REFERENCES streams(stream) ON DELETE CASCADE,
315
+ template_id text NOT NULL,
316
+ entity text NOT NULL,
317
+ fields_json text NOT NULL,
318
+ encodings_json text NOT NULL,
319
+ state text NOT NULL,
320
+ created_at_ms bigint NOT NULL,
321
+ last_seen_at_ms bigint NOT NULL,
322
+ inactivity_ttl_ms bigint NOT NULL,
323
+ active_from_source_offset bigint NOT NULL,
324
+ retired_at_ms bigint NULL,
325
+ retired_reason text NULL,
326
+ PRIMARY KEY(stream, template_id)
327
+ );
328
+ `);
329
+ await executor.query(`
330
+ CREATE INDEX IF NOT EXISTS live_templates_stream_entity_state_last_seen_idx
331
+ ON live_templates(stream, entity, state, last_seen_at_ms);
332
+ `);
333
+ await executor.query(`
334
+ CREATE INDEX IF NOT EXISTS live_templates_stream_state_last_seen_idx
335
+ ON live_templates(stream, state, last_seen_at_ms);
336
+ `);
337
+ }
338
+
339
+ async function installFullModeStatsSchema(executor: PgSchemaExecutor): Promise<void> {
340
+ await executor.query(`
341
+ CREATE TABLE IF NOT EXISTS objectstore_request_counts (
342
+ stream_hash text NOT NULL,
343
+ artifact text NOT NULL,
344
+ op text NOT NULL,
345
+ count bigint NOT NULL DEFAULT 0,
346
+ bytes bigint NOT NULL DEFAULT 0,
347
+ updated_at_ms bigint NOT NULL,
348
+ PRIMARY KEY(stream_hash, artifact, op)
349
+ );
350
+ `);
351
+ await executor.query(`
352
+ CREATE INDEX IF NOT EXISTS objectstore_request_counts_stream_hash_idx
353
+ ON objectstore_request_counts(stream_hash, updated_at_ms);
354
+ `);
355
+ }