@tungthedev/streams-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
package/src/offset.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { decodeCrockfordBase32Fixed26Result, encodeCrockfordBase32Fixed26Result } from "./util/base32_crockford";
|
|
2
|
+
import { readU32BE, writeU32BE } from "./util/endian";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
import { dsError } from "./util/ds_error.ts";
|
|
5
|
+
|
|
6
|
+
export type ParsedOffset =
|
|
7
|
+
| { kind: "start" }
|
|
8
|
+
| { kind: "seq"; epoch: number; seq: bigint; inBlock: number };
|
|
9
|
+
|
|
10
|
+
const DEFAULT_EPOCH = 0;
|
|
11
|
+
|
|
12
|
+
export function parseOffsetResult(input: string | null | undefined): Result<ParsedOffset, { kind: "invalid_offset"; message: string }> {
|
|
13
|
+
if (input == null || input === "") {
|
|
14
|
+
return Result.err({ kind: "invalid_offset", message: "missing offset" });
|
|
15
|
+
}
|
|
16
|
+
if (input === "-1") return Result.ok({ kind: "start" });
|
|
17
|
+
|
|
18
|
+
if (input.length !== 26) {
|
|
19
|
+
return Result.err({ kind: "invalid_offset", message: `invalid offset length: ${input.length}` });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const bytesRes = decodeCrockfordBase32Fixed26Result(input);
|
|
23
|
+
if (Result.isError(bytesRes)) return Result.err({ kind: "invalid_offset", message: bytesRes.error.message });
|
|
24
|
+
const bytes = bytesRes.value;
|
|
25
|
+
const epoch = readU32BE(bytes, 0);
|
|
26
|
+
const hi = readU32BE(bytes, 4);
|
|
27
|
+
const lo = readU32BE(bytes, 8);
|
|
28
|
+
const inBlock = readU32BE(bytes, 12);
|
|
29
|
+
// Protocol offsets are shifted by +1 so we never emit reserved "-1".
|
|
30
|
+
const rawSeq = (BigInt(hi) << 32n) | BigInt(lo);
|
|
31
|
+
const seq = rawSeq - 1n;
|
|
32
|
+
return Result.ok({ kind: "seq", epoch, seq, inBlock });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseOffset(input: string | null | undefined): ParsedOffset {
|
|
36
|
+
const res = parseOffsetResult(input);
|
|
37
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
38
|
+
return res.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function encodeOffsetResult(epoch: number, seq: bigint, inBlock = 0): Result<string, { kind: "invalid_offset"; message: string }> {
|
|
42
|
+
if (seq < -1n) return Result.err({ kind: "invalid_offset", message: "invalid offset" });
|
|
43
|
+
const bytes = new Uint8Array(16);
|
|
44
|
+
writeU32BE(bytes, 0, epoch >>> 0);
|
|
45
|
+
const rawSeq = seq + 1n;
|
|
46
|
+
const hi = Number((rawSeq >> 32n) & 0xffffffffn);
|
|
47
|
+
const lo = Number(rawSeq & 0xffffffffn);
|
|
48
|
+
writeU32BE(bytes, 4, hi);
|
|
49
|
+
writeU32BE(bytes, 8, lo);
|
|
50
|
+
writeU32BE(bytes, 12, inBlock >>> 0);
|
|
51
|
+
const encodedRes = encodeCrockfordBase32Fixed26Result(bytes);
|
|
52
|
+
if (Result.isError(encodedRes)) return Result.err({ kind: "invalid_offset", message: encodedRes.error.message });
|
|
53
|
+
return Result.ok(encodedRes.value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function encodeOffset(epoch: number, seq: bigint, inBlock = 0): string {
|
|
57
|
+
const res = encodeOffsetResult(epoch, seq, inBlock);
|
|
58
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
59
|
+
return res.value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function canonicalizeOffset(input: string): string {
|
|
63
|
+
const p = parseOffset(input);
|
|
64
|
+
if (p.kind === "start") return encodeOffset(DEFAULT_EPOCH, -1n, 0);
|
|
65
|
+
return encodeOffset(p.epoch, p.seq, p.inBlock);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function offsetToSeqOrNeg1(p: ParsedOffset): bigint {
|
|
69
|
+
return p.kind === "start" ? -1n : p.seq;
|
|
70
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { Pool, type PoolClient } from "pg";
|
|
3
|
+
import type { Config } from "../config";
|
|
4
|
+
import { bootstrapObjectStoreIntoRestoreStore } from "../bootstrap";
|
|
5
|
+
import type { ObjectStore } from "../objectstore/interface";
|
|
6
|
+
import type { BootstrapRestoreStore } from "../store/bootstrap_restore_store";
|
|
7
|
+
import type { ProfileTouchControlStore } from "../store/touch_store";
|
|
8
|
+
import type { IndexRunRow, LexiconIndexRunRow, SecondaryIndexRunRow } from "../store/rows";
|
|
9
|
+
import type { StreamReadRow } from "../store/segment_read_store";
|
|
10
|
+
import { migratePostgresStore } from "./schema";
|
|
11
|
+
import type { PgExecutor } from "./types";
|
|
12
|
+
import { dsError } from "../util/ds_error";
|
|
13
|
+
import {
|
|
14
|
+
deletePostgresStreamProfile,
|
|
15
|
+
restorePostgresStreamRow,
|
|
16
|
+
upsertPostgresSchemaRegistry,
|
|
17
|
+
upsertPostgresStreamProfile,
|
|
18
|
+
} from "./control_restore";
|
|
19
|
+
import {
|
|
20
|
+
markPostgresSegmentUploaded,
|
|
21
|
+
restorePostgresManifestRow,
|
|
22
|
+
restorePostgresSegmentMeta,
|
|
23
|
+
restorePostgresSegmentRow,
|
|
24
|
+
setPostgresSchemaUploadedSizeBytes,
|
|
25
|
+
} from "./segments";
|
|
26
|
+
import { insertPostgresIndexRun, retirePostgresIndexRuns, upsertPostgresIndexState } from "./routing_index";
|
|
27
|
+
import {
|
|
28
|
+
insertPostgresSecondaryIndexRun,
|
|
29
|
+
retirePostgresSecondaryIndexRuns,
|
|
30
|
+
upsertPostgresSecondaryIndexState,
|
|
31
|
+
} from "./secondary_index";
|
|
32
|
+
import {
|
|
33
|
+
insertPostgresLexiconIndexRun,
|
|
34
|
+
retirePostgresLexiconIndexRuns,
|
|
35
|
+
upsertPostgresLexiconIndexState,
|
|
36
|
+
} from "./lexicon_index";
|
|
37
|
+
import { upsertPostgresSearchCompanionPlan, upsertPostgresSearchSegmentCompanion } from "./companions";
|
|
38
|
+
import { deletePostgresStreamTouchState, ensurePostgresStreamTouchStateFromStream } from "./touch";
|
|
39
|
+
|
|
40
|
+
export async function bootstrapPostgresFromR2(
|
|
41
|
+
cfg: Config,
|
|
42
|
+
objectStore: ObjectStore,
|
|
43
|
+
connectionString: string,
|
|
44
|
+
opts: { clearLocal?: boolean } = {}
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
if (opts.clearLocal !== false) {
|
|
47
|
+
rmSync(`${cfg.rootDir}/local`, { recursive: true, force: true });
|
|
48
|
+
rmSync(`${cfg.rootDir}/cache`, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
mkdirSync(cfg.rootDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const pool = new Pool({ connectionString });
|
|
53
|
+
try {
|
|
54
|
+
await migratePostgresStore(pool, { fullMode: true });
|
|
55
|
+
if (opts.clearLocal !== false) await clearPostgresRestoreTarget(pool);
|
|
56
|
+
else await assertPostgresRestoreTargetEmpty(pool);
|
|
57
|
+
await bootstrapObjectStoreIntoRestoreStore(cfg, objectStore, new PostgresBootstrapRestoreStore(pool));
|
|
58
|
+
} finally {
|
|
59
|
+
await pool.end();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function clearPostgresRestoreTarget(executor: PgExecutor): Promise<void> {
|
|
64
|
+
await executor.query(`TRUNCATE TABLE objectstore_request_counts, streams RESTART IDENTITY CASCADE;`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function assertPostgresRestoreTargetEmpty(executor: PgExecutor): Promise<void> {
|
|
68
|
+
const res = await executor.query<{ count: string | number | bigint }>(
|
|
69
|
+
`SELECT
|
|
70
|
+
(SELECT COUNT(*) FROM streams) +
|
|
71
|
+
(SELECT COUNT(*) FROM objectstore_request_counts) AS count;`
|
|
72
|
+
);
|
|
73
|
+
if (BigInt(res.rows[0]?.count ?? 0) !== 0n) {
|
|
74
|
+
throw dsError("postgres bootstrap target is not empty; rerun with clearLocal enabled");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class PostgresBootstrapRestoreStore implements BootstrapRestoreStore {
|
|
79
|
+
private tx: PoolClient | null = null;
|
|
80
|
+
|
|
81
|
+
readonly touch: ProfileTouchControlStore = {
|
|
82
|
+
ensureStreamTouchState: (stream) => this.ensureStreamTouchState(stream),
|
|
83
|
+
deleteStreamTouchState: (stream) => this.deleteStreamTouchState(stream),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
constructor(private readonly pool: Pool) {}
|
|
87
|
+
|
|
88
|
+
nowMs(): bigint {
|
|
89
|
+
return BigInt(Date.now());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async close(): Promise<void> {
|
|
93
|
+
await this.rollbackActiveTransaction();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async beginRestoreStream(_stream: string): Promise<void> {
|
|
97
|
+
if (this.tx) throw dsError("postgres bootstrap restore transaction is already active");
|
|
98
|
+
this.tx = await this.pool.connect();
|
|
99
|
+
await this.tx.query("BEGIN");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async commitRestoreStream(_stream: string): Promise<void> {
|
|
103
|
+
if (!this.tx) return;
|
|
104
|
+
const client = this.tx;
|
|
105
|
+
try {
|
|
106
|
+
await client.query("COMMIT");
|
|
107
|
+
this.tx = null;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
110
|
+
this.tx = null;
|
|
111
|
+
throw error;
|
|
112
|
+
} finally {
|
|
113
|
+
client.release();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async rollbackRestoreStream(_stream: string): Promise<void> {
|
|
118
|
+
await this.rollbackActiveTransaction();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async restoreStreamRow(row: StreamReadRow): Promise<void> {
|
|
122
|
+
await restorePostgresStreamRow(this.executor, row);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async upsertStreamProfile(stream: string, profileJson: string): Promise<void> {
|
|
126
|
+
await upsertPostgresStreamProfile(this.executor, this.nowMs(), stream, profileJson);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async deleteStreamProfile(stream: string): Promise<void> {
|
|
130
|
+
await deletePostgresStreamProfile(this.executor, stream);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async upsertSegmentMeta(stream: string, count: number, offsets: Uint8Array, blocks: Uint8Array, lastTs: Uint8Array): Promise<void> {
|
|
134
|
+
await restorePostgresSegmentMeta(this.executor, stream, count, offsets, blocks, lastTs);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async upsertManifestRow(
|
|
138
|
+
stream: string,
|
|
139
|
+
generation: number,
|
|
140
|
+
uploadedGeneration: number,
|
|
141
|
+
uploadedAtMs: bigint | null,
|
|
142
|
+
etag: string | null,
|
|
143
|
+
sizeBytes: number | null
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
await restorePostgresManifestRow(this.executor, stream, generation, uploadedGeneration, uploadedAtMs, etag, sizeBytes);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async createSegmentRow(row: {
|
|
149
|
+
segmentId: string;
|
|
150
|
+
stream: string;
|
|
151
|
+
segmentIndex: number;
|
|
152
|
+
startOffset: bigint;
|
|
153
|
+
endOffset: bigint;
|
|
154
|
+
blockCount: number;
|
|
155
|
+
lastAppendMs: bigint;
|
|
156
|
+
payloadBytes: bigint;
|
|
157
|
+
sizeBytes: number;
|
|
158
|
+
localPath: string;
|
|
159
|
+
}): Promise<void> {
|
|
160
|
+
await restorePostgresSegmentRow(this.executor, this.nowMs(), row);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async markSegmentUploaded(segmentId: string, etag: string, uploadedAtMs: bigint): Promise<void> {
|
|
164
|
+
await markPostgresSegmentUploaded(this.executor, segmentId, etag, uploadedAtMs);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async upsertIndexState(stream: string, indexSecret: Uint8Array, indexedThrough: number): Promise<void> {
|
|
168
|
+
await upsertPostgresIndexState(this.executor, this.nowMs(), stream, indexSecret, indexedThrough);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async insertIndexRun(row: Omit<IndexRunRow, "retired_gen" | "retired_at_ms">): Promise<void> {
|
|
172
|
+
await insertPostgresIndexRun(this.executor, row, { idempotent: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async retireIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): Promise<void> {
|
|
176
|
+
await retirePostgresIndexRuns(this.executor, runIds, retiredGen, retiredAtMs);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async upsertSecondaryIndexState(
|
|
180
|
+
stream: string,
|
|
181
|
+
indexName: string,
|
|
182
|
+
indexSecret: Uint8Array,
|
|
183
|
+
configHash: string,
|
|
184
|
+
indexedThrough: number
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
await upsertPostgresSecondaryIndexState(this.executor, this.nowMs(), stream, indexName, indexSecret, configHash, indexedThrough);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async insertSecondaryIndexRun(row: Omit<SecondaryIndexRunRow, "retired_gen" | "retired_at_ms">): Promise<void> {
|
|
190
|
+
await insertPostgresSecondaryIndexRun(this.executor, row, { idempotent: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async retireSecondaryIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): Promise<void> {
|
|
194
|
+
await retirePostgresSecondaryIndexRuns(this.executor, runIds, retiredGen, retiredAtMs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async upsertLexiconIndexState(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): Promise<void> {
|
|
198
|
+
await upsertPostgresLexiconIndexState(this.executor, this.nowMs(), stream, sourceKind, sourceName, indexedThrough);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async insertLexiconIndexRun(row: Omit<LexiconIndexRunRow, "retired_gen" | "retired_at_ms">): Promise<void> {
|
|
202
|
+
await insertPostgresLexiconIndexRun(this.executor, row, { idempotent: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async retireLexiconIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): Promise<void> {
|
|
206
|
+
await retirePostgresLexiconIndexRuns(this.executor, runIds, retiredGen, retiredAtMs);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async upsertSearchCompanionPlan(stream: string, generation: number, planHash: string, planJson: string): Promise<void> {
|
|
210
|
+
await upsertPostgresSearchCompanionPlan(this.executor, this.nowMs(), stream, generation, planHash, planJson);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async upsertSearchSegmentCompanion(
|
|
214
|
+
stream: string,
|
|
215
|
+
segmentIndex: number,
|
|
216
|
+
objectKey: string,
|
|
217
|
+
planGeneration: number,
|
|
218
|
+
sectionsJson: string,
|
|
219
|
+
sectionSizesJson: string,
|
|
220
|
+
sizeBytes: number,
|
|
221
|
+
primaryTimestampMinMs: bigint | null,
|
|
222
|
+
primaryTimestampMaxMs: bigint | null
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
await upsertPostgresSearchSegmentCompanion(
|
|
225
|
+
this.executor,
|
|
226
|
+
this.nowMs(),
|
|
227
|
+
stream,
|
|
228
|
+
segmentIndex,
|
|
229
|
+
objectKey,
|
|
230
|
+
planGeneration,
|
|
231
|
+
sectionsJson,
|
|
232
|
+
sectionSizesJson,
|
|
233
|
+
sizeBytes,
|
|
234
|
+
primaryTimestampMinMs,
|
|
235
|
+
primaryTimestampMaxMs
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async upsertSchemaRegistry(stream: string, registryJson: string): Promise<void> {
|
|
240
|
+
await upsertPostgresSchemaRegistry(this.executor, this.nowMs(), stream, registryJson);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async setSchemaUploadedSizeBytes(stream: string, sizeBytes: number): Promise<void> {
|
|
244
|
+
await setPostgresSchemaUploadedSizeBytes(this.executor, this.nowMs(), stream, sizeBytes);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async ensureStreamTouchState(stream: string): Promise<void> {
|
|
248
|
+
await ensurePostgresStreamTouchStateFromStream(this.executor, this.nowMs(), stream);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async deleteStreamTouchState(stream: string): Promise<void> {
|
|
252
|
+
await deletePostgresStreamTouchState(this.executor, stream);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private get executor(): PgExecutor {
|
|
256
|
+
return this.tx ?? this.pool;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async rollbackActiveTransaction(): Promise<void> {
|
|
260
|
+
if (!this.tx) return;
|
|
261
|
+
const client = this.tx;
|
|
262
|
+
this.tx = null;
|
|
263
|
+
try {
|
|
264
|
+
await client.query("ROLLBACK");
|
|
265
|
+
} finally {
|
|
266
|
+
client.release();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { CompanionProgressStore, SearchCompanionIndexStore } from "../store/index_store";
|
|
3
|
+
import type { SearchCompanionPlanRow, SearchSegmentCompanionRow } from "../store/rows";
|
|
4
|
+
import type { PgExecutor } from "./types";
|
|
5
|
+
import { getSegmentByIndexWithExecutor, pgInt, toBigInt } from "./rows";
|
|
6
|
+
|
|
7
|
+
export class PostgresCompanionIndexStore implements SearchCompanionIndexStore, CompanionProgressStore {
|
|
8
|
+
constructor(private readonly pool: Pool, private readonly currentTimeMs: () => bigint) {}
|
|
9
|
+
|
|
10
|
+
async getSegmentByIndex(stream: string, segmentIndex: number) {
|
|
11
|
+
return getSegmentByIndexWithExecutor(this.pool, stream, segmentIndex);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async countUploadedSegments(stream: string): Promise<number> {
|
|
15
|
+
const res = await this.pool.query<{ max_idx: number | string | null }>(
|
|
16
|
+
`SELECT MAX(segment_index) AS max_idx FROM segments WHERE stream = $1 AND r2_etag IS NOT NULL;`,
|
|
17
|
+
[stream]
|
|
18
|
+
);
|
|
19
|
+
const maxIdx = res.rows[0]?.max_idx == null ? -1 : Number(res.rows[0]!.max_idx);
|
|
20
|
+
return maxIdx >= 0 ? maxIdx + 1 : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getSearchCompanionPlan(stream: string): Promise<SearchCompanionPlanRow | null> {
|
|
24
|
+
return getPostgresSearchCompanionPlan(this.pool, stream);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async listSearchCompanionPlanStreams(): Promise<string[]> {
|
|
28
|
+
const res = await this.pool.query<{ stream: string }>(`SELECT stream FROM search_companion_plans ORDER BY stream ASC;`);
|
|
29
|
+
return res.rows.map((row) => String(row.stream));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async upsertSearchCompanionPlan(stream: string, generation: number, planHash: string, planJson: string): Promise<void> {
|
|
33
|
+
await upsertPostgresSearchCompanionPlan(this.pool, this.currentTimeMs(), stream, generation, planHash, planJson);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async deleteSearchCompanionPlan(stream: string): Promise<void> {
|
|
37
|
+
await this.pool.query(`DELETE FROM search_companion_plans WHERE stream = $1;`, [stream]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getSearchSegmentCompanion(stream: string, segmentIndex: number): Promise<SearchSegmentCompanionRow | null> {
|
|
41
|
+
return getPostgresSearchSegmentCompanion(this.pool, stream, segmentIndex);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async listSearchSegmentCompanions(stream: string): Promise<SearchSegmentCompanionRow[]> {
|
|
45
|
+
return listPostgresSearchSegmentCompanions(this.pool, stream);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async upsertSearchSegmentCompanion(
|
|
49
|
+
stream: string,
|
|
50
|
+
segmentIndex: number,
|
|
51
|
+
objectKey: string,
|
|
52
|
+
planGeneration: number,
|
|
53
|
+
sectionsJson: string,
|
|
54
|
+
sectionSizesJson: string,
|
|
55
|
+
sizeBytes: number,
|
|
56
|
+
primaryTimestampMinMs: bigint | null,
|
|
57
|
+
primaryTimestampMaxMs: bigint | null
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
await upsertPostgresSearchSegmentCompanion(
|
|
60
|
+
this.pool,
|
|
61
|
+
this.currentTimeMs(),
|
|
62
|
+
stream,
|
|
63
|
+
segmentIndex,
|
|
64
|
+
objectKey,
|
|
65
|
+
planGeneration,
|
|
66
|
+
sectionsJson,
|
|
67
|
+
sectionSizesJson,
|
|
68
|
+
sizeBytes,
|
|
69
|
+
primaryTimestampMinMs,
|
|
70
|
+
primaryTimestampMaxMs
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async deleteSearchSegmentCompanions(stream: string): Promise<void> {
|
|
75
|
+
await this.pool.query(`DELETE FROM search_segment_companions WHERE stream = $1;`, [stream]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function upsertPostgresSearchCompanionPlan(
|
|
80
|
+
executor: PgExecutor,
|
|
81
|
+
nowMs: bigint,
|
|
82
|
+
stream: string,
|
|
83
|
+
generation: number,
|
|
84
|
+
planHash: string,
|
|
85
|
+
planJson: string
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
await executor.query(
|
|
88
|
+
`INSERT INTO search_companion_plans(stream, generation, plan_hash, plan_json, updated_at_ms)
|
|
89
|
+
VALUES($1, $2, $3, $4, $5)
|
|
90
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
91
|
+
generation = excluded.generation,
|
|
92
|
+
plan_hash = excluded.plan_hash,
|
|
93
|
+
plan_json = excluded.plan_json,
|
|
94
|
+
updated_at_ms = excluded.updated_at_ms;`,
|
|
95
|
+
[stream, generation, planHash, planJson, pgInt(nowMs)]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function upsertPostgresSearchSegmentCompanion(
|
|
100
|
+
executor: PgExecutor,
|
|
101
|
+
nowMs: bigint,
|
|
102
|
+
stream: string,
|
|
103
|
+
segmentIndex: number,
|
|
104
|
+
objectKey: string,
|
|
105
|
+
planGeneration: number,
|
|
106
|
+
sectionsJson: string,
|
|
107
|
+
sectionSizesJson: string,
|
|
108
|
+
sizeBytes: number,
|
|
109
|
+
primaryTimestampMinMs: bigint | null,
|
|
110
|
+
primaryTimestampMaxMs: bigint | null
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
await executor.query(
|
|
113
|
+
`INSERT INTO search_segment_companions(
|
|
114
|
+
stream, segment_index, object_key, plan_generation, sections_json, section_sizes_json,
|
|
115
|
+
size_bytes, primary_timestamp_min_ms, primary_timestamp_max_ms, updated_at_ms
|
|
116
|
+
)
|
|
117
|
+
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
118
|
+
ON CONFLICT(stream, segment_index) DO UPDATE SET
|
|
119
|
+
object_key = excluded.object_key,
|
|
120
|
+
plan_generation = excluded.plan_generation,
|
|
121
|
+
sections_json = excluded.sections_json,
|
|
122
|
+
section_sizes_json = excluded.section_sizes_json,
|
|
123
|
+
size_bytes = excluded.size_bytes,
|
|
124
|
+
primary_timestamp_min_ms = excluded.primary_timestamp_min_ms,
|
|
125
|
+
primary_timestamp_max_ms = excluded.primary_timestamp_max_ms,
|
|
126
|
+
updated_at_ms = excluded.updated_at_ms;`,
|
|
127
|
+
[
|
|
128
|
+
stream,
|
|
129
|
+
segmentIndex,
|
|
130
|
+
objectKey,
|
|
131
|
+
planGeneration,
|
|
132
|
+
sectionsJson,
|
|
133
|
+
sectionSizesJson,
|
|
134
|
+
sizeBytes,
|
|
135
|
+
primaryTimestampMinMs == null ? null : pgInt(primaryTimestampMinMs),
|
|
136
|
+
primaryTimestampMaxMs == null ? null : pgInt(primaryTimestampMaxMs),
|
|
137
|
+
pgInt(nowMs),
|
|
138
|
+
]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function loadPostgresSearchCompanionManifest(
|
|
143
|
+
executor: PgExecutor,
|
|
144
|
+
stream: string
|
|
145
|
+
): Promise<{ searchCompanionPlan: SearchCompanionPlanRow | null; searchSegmentCompanions: SearchSegmentCompanionRow[] }> {
|
|
146
|
+
return {
|
|
147
|
+
searchCompanionPlan: await getPostgresSearchCompanionPlan(executor, stream),
|
|
148
|
+
searchSegmentCompanions: await listPostgresSearchSegmentCompanions(executor, stream),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function getPostgresSearchCompanionPlan(executor: PgExecutor, stream: string): Promise<SearchCompanionPlanRow | null> {
|
|
153
|
+
const res = await executor.query(`SELECT * FROM search_companion_plans WHERE stream = $1 LIMIT 1;`, [stream]);
|
|
154
|
+
return res.rows[0] ? coerceSearchCompanionPlanRow(res.rows[0]) : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getPostgresSearchSegmentCompanion(
|
|
158
|
+
executor: PgExecutor,
|
|
159
|
+
stream: string,
|
|
160
|
+
segmentIndex: number
|
|
161
|
+
): Promise<SearchSegmentCompanionRow | null> {
|
|
162
|
+
const res = await executor.query(`SELECT * FROM search_segment_companions WHERE stream = $1 AND segment_index = $2 LIMIT 1;`, [
|
|
163
|
+
stream,
|
|
164
|
+
segmentIndex,
|
|
165
|
+
]);
|
|
166
|
+
return res.rows[0] ? coerceSearchSegmentCompanionRow(res.rows[0]) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function listPostgresSearchSegmentCompanions(executor: PgExecutor, stream: string): Promise<SearchSegmentCompanionRow[]> {
|
|
170
|
+
const res = await executor.query(`SELECT * FROM search_segment_companions WHERE stream = $1 ORDER BY segment_index ASC;`, [stream]);
|
|
171
|
+
return res.rows.map(coerceSearchSegmentCompanionRow);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function coerceSearchCompanionPlanRow(row: any): SearchCompanionPlanRow {
|
|
175
|
+
return {
|
|
176
|
+
stream: String(row.stream),
|
|
177
|
+
generation: Number(row.generation),
|
|
178
|
+
plan_hash: String(row.plan_hash),
|
|
179
|
+
plan_json: String(row.plan_json),
|
|
180
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function coerceSearchSegmentCompanionRow(row: any): SearchSegmentCompanionRow {
|
|
185
|
+
return {
|
|
186
|
+
stream: String(row.stream),
|
|
187
|
+
segment_index: Number(row.segment_index),
|
|
188
|
+
object_key: String(row.object_key),
|
|
189
|
+
plan_generation: Number(row.plan_generation),
|
|
190
|
+
sections_json: String(row.sections_json),
|
|
191
|
+
section_sizes_json: String(row.section_sizes_json),
|
|
192
|
+
size_bytes: Number(row.size_bytes),
|
|
193
|
+
primary_timestamp_min_ms: row.primary_timestamp_min_ms == null ? null : toBigInt(row.primary_timestamp_min_ms),
|
|
194
|
+
primary_timestamp_max_ms: row.primary_timestamp_max_ms == null ? null : toBigInt(row.primary_timestamp_max_ms),
|
|
195
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { StreamReadRow } from "../store/segment_read_store";
|
|
2
|
+
import type { PgExecutor } from "./types";
|
|
3
|
+
import { pgInt } from "./rows";
|
|
4
|
+
|
|
5
|
+
export async function restorePostgresStreamRow(executor: PgExecutor, row: StreamReadRow): Promise<void> {
|
|
6
|
+
await executor.query(
|
|
7
|
+
`INSERT INTO streams(
|
|
8
|
+
stream, created_at_ms, updated_at_ms, content_type, profile, stream_seq, closed,
|
|
9
|
+
closed_producer_id, closed_producer_epoch, closed_producer_seq, ttl_seconds,
|
|
10
|
+
epoch, next_offset, sealed_through, uploaded_through, uploaded_segment_count,
|
|
11
|
+
pending_rows, pending_bytes, logical_size_bytes, wal_rows, wal_bytes,
|
|
12
|
+
last_append_ms, last_segment_cut_ms, segment_in_progress, expires_at_ms, stream_flags
|
|
13
|
+
)
|
|
14
|
+
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
|
|
15
|
+
$17, $18, $19, $20, $21, $22, $23, $24, $25, $26)
|
|
16
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
17
|
+
created_at_ms = excluded.created_at_ms,
|
|
18
|
+
updated_at_ms = excluded.updated_at_ms,
|
|
19
|
+
content_type = excluded.content_type,
|
|
20
|
+
profile = excluded.profile,
|
|
21
|
+
stream_seq = excluded.stream_seq,
|
|
22
|
+
closed = excluded.closed,
|
|
23
|
+
closed_producer_id = excluded.closed_producer_id,
|
|
24
|
+
closed_producer_epoch = excluded.closed_producer_epoch,
|
|
25
|
+
closed_producer_seq = excluded.closed_producer_seq,
|
|
26
|
+
ttl_seconds = excluded.ttl_seconds,
|
|
27
|
+
epoch = excluded.epoch,
|
|
28
|
+
next_offset = excluded.next_offset,
|
|
29
|
+
sealed_through = excluded.sealed_through,
|
|
30
|
+
uploaded_through = excluded.uploaded_through,
|
|
31
|
+
uploaded_segment_count = excluded.uploaded_segment_count,
|
|
32
|
+
pending_rows = excluded.pending_rows,
|
|
33
|
+
pending_bytes = excluded.pending_bytes,
|
|
34
|
+
logical_size_bytes = excluded.logical_size_bytes,
|
|
35
|
+
wal_rows = excluded.wal_rows,
|
|
36
|
+
wal_bytes = excluded.wal_bytes,
|
|
37
|
+
last_append_ms = excluded.last_append_ms,
|
|
38
|
+
last_segment_cut_ms = excluded.last_segment_cut_ms,
|
|
39
|
+
segment_in_progress = excluded.segment_in_progress,
|
|
40
|
+
segment_claim_token = NULL,
|
|
41
|
+
segment_claimed_at_ms = NULL,
|
|
42
|
+
expires_at_ms = excluded.expires_at_ms,
|
|
43
|
+
stream_flags = excluded.stream_flags;`,
|
|
44
|
+
[
|
|
45
|
+
row.stream,
|
|
46
|
+
pgInt(row.created_at_ms),
|
|
47
|
+
pgInt(row.updated_at_ms),
|
|
48
|
+
row.content_type,
|
|
49
|
+
row.profile,
|
|
50
|
+
row.stream_seq,
|
|
51
|
+
row.closed,
|
|
52
|
+
row.closed_producer_id,
|
|
53
|
+
row.closed_producer_epoch,
|
|
54
|
+
row.closed_producer_seq,
|
|
55
|
+
row.ttl_seconds,
|
|
56
|
+
row.epoch,
|
|
57
|
+
pgInt(row.next_offset),
|
|
58
|
+
pgInt(row.sealed_through),
|
|
59
|
+
pgInt(row.uploaded_through),
|
|
60
|
+
row.uploaded_segment_count ?? 0,
|
|
61
|
+
pgInt(row.pending_rows),
|
|
62
|
+
pgInt(row.pending_bytes),
|
|
63
|
+
pgInt(row.logical_size_bytes),
|
|
64
|
+
pgInt(row.wal_rows),
|
|
65
|
+
pgInt(row.wal_bytes),
|
|
66
|
+
pgInt(row.last_append_ms),
|
|
67
|
+
pgInt(row.last_segment_cut_ms),
|
|
68
|
+
row.segment_in_progress ?? 0,
|
|
69
|
+
row.expires_at_ms == null ? null : pgInt(row.expires_at_ms),
|
|
70
|
+
row.stream_flags,
|
|
71
|
+
]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function upsertPostgresStreamProfile(
|
|
76
|
+
executor: PgExecutor,
|
|
77
|
+
nowMs: bigint,
|
|
78
|
+
stream: string,
|
|
79
|
+
profileJson: string
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
await executor.query(
|
|
82
|
+
`INSERT INTO stream_profiles(stream, profile_json, updated_at_ms)
|
|
83
|
+
VALUES($1, $2, $3)
|
|
84
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
85
|
+
profile_json = excluded.profile_json,
|
|
86
|
+
updated_at_ms = excluded.updated_at_ms;`,
|
|
87
|
+
[stream, profileJson, pgInt(nowMs)]
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deletePostgresStreamProfile(executor: PgExecutor, stream: string): Promise<void> {
|
|
92
|
+
await executor.query(`DELETE FROM stream_profiles WHERE stream = $1;`, [stream]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function upsertPostgresSchemaRegistry(
|
|
96
|
+
executor: PgExecutor,
|
|
97
|
+
nowMs: bigint,
|
|
98
|
+
stream: string,
|
|
99
|
+
registryJson: string
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
await executor.query(
|
|
102
|
+
`INSERT INTO schemas(stream, schema_json, updated_at_ms)
|
|
103
|
+
VALUES($1, $2, $3)
|
|
104
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
105
|
+
schema_json = excluded.schema_json,
|
|
106
|
+
updated_at_ms = excluded.updated_at_ms;`,
|
|
107
|
+
[stream, registryJson, pgInt(nowMs)]
|
|
108
|
+
);
|
|
109
|
+
}
|