/** * PostgreSQL pool + central schema — the PG sibling of tidb-pool.ts, for the second run-store backend * (clay 2026-06-21). Schema ownership is CENTRAL (one ensurePgSchema, like tidb-pool's SCHEMA_STATEMENTS) — PG * stores assume the tables exist and NEVER create their own (a per-store `CREATE TABLE IF NOT EXISTS` is * first-writer-wins, so a trimmed table would shadow the full one and drop columns). * * Dialect deltas vs the MySQL DDL in tidb-pool.ts: JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3), TINYINT(1)→SMALLINT, * inline `KEY …`→separate `CREATE INDEX`. checkpoint timestamps stay epoch-ms BIGINT (core's Checkpoint uses ms), * matching the TiDB schema byte-for-byte semantically. Only the tables PG backends implement so far are listed * (run-store + checkpoint); the remaining tidb-* tables are added as their PG stores land (tracked). * * ─────────────────────────────── SCHEMA POLICY (clay 裁 2026-07-26) ─────────────────────────────── * THIS CODE IS THE SINGLE SOURCE OF TRUTH for the PG schema. The stack is not yet in production and runs * against a single local operator's database, so a schema change is DROP-AND-RECREATE (drop the database, * boot, `ensurePgSchema` rebuilds it) — NOT an in-place migration. * * Consequence, and it is a hard rule: **do NOT add new `ALTER TABLE` seams here.** Every column / default / * constraint change is folded into its own `CREATE TABLE`, and this array stays pure `CREATE TABLE` + * `CREATE INDEX`. The historical additive-ALTER seams were folded in on 2026-07-26 (they had all decayed * into permanent no-ops — the CREATEs already carried the columns). * * Because the seams are gone, the semantics they used to document now live as inline `--` comments ON THE * COLUMN inside the CREATE, not as TypeScript comments outside the template literal: the full-set SQL * baseline `docs/schema/baseline-pg.sql` is GENERATED from these statement strings, so only what is inside * the SQL survives the export. The baseline is a derived artifact — regenerate it, never hand-edit it. */ import pg from "pg"; import type { ServiceConfig } from "../config-types.js"; export declare const PG_SCHEMA_STATEMENTS: readonly string[]; /** Build the pg PoolConfig from PG config (pure → unit-testable apart from the live connection) — the PG twin of * tidbPoolOptions. `dbQueryTimeoutMs` (DB_QUERY_TIMEOUT_MS, S9 deeper fix) arms a POOL-WIDE per-query ceiling on * every client the pool hands out, BOTH sides: * - `statement_timeout` = T: SERVER-side cancel (sent as a startup parameter) — frees the server's resources * and surfaces the canonical 57014 error on any statement that runs past T; * - `query_timeout` = T + 5s: CLIENT-side read-timeout backstop for the cases the server can't answer at all * (dead network / hung server — the S9 hang class). Slightly ABOVE statement_timeout on purpose, so the * server's clean cancel normally wins and the client timer only fires when no response ever arrives (a * query_timeout rejection makes pg-pool DESTROY that client, discarding the poisoned connection). * Unset = both off (byte-compat: legit heavyweight statements — boot DDL, ≤64 MiB snapshot-blob rows — make a * universal default unsafe; the S9 write-behind counter face carries its own always-on per-query timeout). */ export declare function pgPoolOptions(pgCfg: NonNullable, dbQueryTimeoutMs?: number): pg.PoolConfig; /** 池连接错误的结构化摘要([ref];idle 态与 checked-out 态两监听共用)。纯函数,单测直打。字段 * **诚实缺席**:code/severity 只在错误对象上真有 string 值时才出键(pg 的 DatabaseError 把两者 * 平铺在错误对象上;裸网络错误没有)—— 不 String() 硬铸,「不知道 ≠ 铸一个假值」([ref] 同族纪律)。 */ export interface PgPoolClientErrorMeta { message: string; code?: string; severity?: string; } export declare function pgPoolClientErrorMeta(err: unknown): PgPoolClientErrorMeta; /** `createPgPool` 的 warn 座(形=store-backend 的 `BootLogSink`;不 import 它——那是反向依赖)。 */ export type PgPoolWarnSink = (msg: string, meta?: Record) => void; /** Create the PG pool from a connection string (or pg.PoolConfig). Thin — production backend-selection wiring * (a ServiceConfig.pg sibling of .tidb) is a tracked follow-on. * * ── [ref](DEBTS;test [ref] 独立复现 FATAL 57P01→exit)——池必须挂 'error' 监听 ───────────────── * node-postgres 语义(pg-pool@3.14.0 `makeIdleListener`):一条**空闲**连接出错 * (`pg_terminate_backend` / 云端故障切换 / LB 空闲回收 / 网络断)⇒ 池先 `_remove(client)` 再 * `pool.emit('error', err, client)`。Pool 是 EventEmitter ⇒ 无 'error' 监听时 Node 把这枚事件转 * throw(ERR_UNHANDLED_ERROR)⇒ `boot/shutdown.ts` 的 uncaughtException 钩无条件 exit 1 ⇒ * **一条空闲连接的死换整进程的死**。修在源头监听,三件明确**不做**: * · 不 exit —— 坏连接在 emit **之前**已被池淘汰,下次 acquire 发新连接,在飞查询(在别的 * client 上)零影响,这不是进程级故障; * · 不静默 —— 每次事件一条结构化 warn(含 code/severity)+ `recordFailOpen`(F 类,census 第 * 52 行):「连接不断被外部杀」(故障切换风暴 / 回收策略过激)与「一切正常」在服务面同形, * 不留痕则只能靠间接症状发现; * · 不动 shutdown.ts 的 uncaughtException 钩 —— exit 1 对**真**未捕获仍是正确姿势,旁路兜底 * 会吞掉别人的崩溃([ref] 纪律:修源头,不下游兜)。 * `onIdleClientError` 缺省 console.warn(与本文件 `ensurePgSchema` 的既有告警姿势一致)——生产 * 装配(store-backend `createStoreBackend`)递结构化 logger 的 warn 座。 */ export declare function createPgPool(opts: string | pg.PoolConfig, onIdleClientError?: PgPoolWarnSink): pg.Pool; /** A fixed bigint key for the `pg_advisory_lock` that serializes the whole `ensurePgSchema` DDL run (see below). * Arbitrary constant; only its uniqueness within this app matters (advisory-lock keys are app-private). * Exported so **boot-time schema-shaped work outside this function** serializes against it under the SAME key — * one lock identity for the whole class (the one-time migration段 is the current user; it runs after ensure and * must not interleave with another replica's ensure). */ export declare const ENSURE_SCHEMA_LOCK_KEY = 4906; /** * {@link ENSURE_SCHEMA_LOCK_KEY} 的**非阻塞**获取(轮询 + 抖动退避)—— MySQL 孪生 * `tidb-pool.ts acquireEnsureSchemaLock` 的逐条同形(循环共用 `ensure-schema-lock-policy.ts`)。轮到拿到为止;坏连接连续抛上抛 * (每条 DDL 本来就各自幂等),而不是把启动挂死。 * * 🔴 **为什么不能再用阻塞的 `pg_advisory_lock()`**(S-287 复审 H1,对 7.82.1 的回归):本仓自 S-287 起在 * 锁窗内发 `CREATE INDEX CONCURRENTLY`,而 CIC 的末相要**等掉同库里所有 xmin 比它老的运行中后端** * (`WaitForOlderSnapshots`)。一条卡在 `SELECT pg_advisory_lock($1)` 里的副本**正是**一条持着快照的运行中 * 查询 —— 它等锁、锁等 CIC、CIC 等它:三方互等。命中形是「全新 PG 库 + 多副本同时起」(表刚建完 ⇒ 索引 * 必缺 ⇒ 必走 CIC)。非阻塞轮询把等待挪到**库外**:每次尝试都是一条立刻返回的独立语句,轮询间隙进程 * 不持任何快照,CIC 因此永远等得到头。旧的普通 `CREATE INDEX` 不做 WaitForOlderSnapshots,所以这条病 * 是随 CIC 一起进来的,修也必须和它同批。 * * ⚠️ 锁是 **session(连接)级**:调用方必须把锁与其后**全部**被串行化的语句跑在**同一条** client 上 * (走 `pool.query` 可能落到别的池连接,那把锁就静默作废了)。 */ export declare function acquirePgEnsureSchemaLock(client: pg.PoolClient, tag: string): Promise; /** {@link acquirePgEnsureSchemaLock} 的释放(尽力而为 —— 锁随连接归还自动释放)。 */ export declare function releasePgEnsureSchemaLock(client: pg.PoolClient, tag: string): Promise; /** Idempotent central schema apply (the PG twin of tidb-pool.ts ensureSchema): the run/checkpoint tables here + * every PG store's own (disjoint) tables. Order is irrelevant (CREATE IF NOT EXISTS, no cross-table FKs). * * Concurrency: callers run this concurrently — tests fan ALL integration suites out in parallel (each beforeAll * calls ensurePgSchema), and in production every replica calls it at boot. Concurrent DDL on the SAME table makes * PG raise `tuple concurrently updated` in its catalog. We serialize the entire run behind a session-level * advisory lock, acquired NON-BLOCKINGLY with polling ({@link acquirePgEnsureSchemaLock} — 阻塞形会与本函数 * 自 S-287 起发的 `CREATE INDEX CONCURRENTLY` 三方互等,理由写在那只函数的头注里): the first caller does * the real CREATE/index work, the rest poll from OUTSIDE the database then find every statement already * satisfied (IF NOT EXISTS ⇒ no-op), so there is no catalog race. The DDL semantics are * unchanged — only its timing is serialized. The advisory lock is SESSION (connection)-level, so the lock and * ALL the DDL MUST run on the one client we hold; routing DDL through `pool.query` could land on a different * pooled connection and silently void the lock. */ export declare function ensurePgSchema(pool: pg.Pool): Promise; //# sourceMappingURL=pg-pool.d.ts.map