/** * **启动期「库 ≟ 代码」列集对账探针**(S-547)—— 存量库缺列/多列一律**响亮拒启**。 * * ── 病(test T-47 三库一致的真读数)─────────────────────────────────────────────────────────────── * 7.91.2 建的 `session_policy`(那时还没有 `gen` 列)+ 一行数据,7.92.0 的二进制接上去:`CREATE TABLE * IF NOT EXISTS` 对存量表是**空操作** ⇒ 启动全绿、日志零提 `gen`、健康检查 healthy,直到**用户路径上** * 第一次读写策略行才炸 —— `PUT/GET /v1/sessions/:id/policy` 500,`POST /v1/tasks` 的 run 终局对外 * `errorCode` 直接是裸 `42703` / `ER_BAD_FIELD_ERROR`。 * * ── 与 SCHEMA POLICY(`tidb-pool.ts` 顶注,clay 裁 2026-07-26)不冲突 ───────────────────────────── * 那条口径禁的是 **ALTER seam**:「代码是 schema 的唯一真源、存量库删库重建、`ensure*` 里不许再出现 * `ALTER TABLE`」。本探针**一条 DDL 都不发**,它是只读的 information_schema 对账 —— 恰恰是那条口径的 * **执行面**:口径说「库必须等于代码」,在此之前没有任何东西检查过这句话是不是真的。两者一个是禁令 * (不许代码偷改库)、一个是对账(库偏离了就别起),同向不同面。 * * ── 为什么两个方向都拒(缺列 **和** 多列)────────────────────────────────────────────────────── * 缺列 = 库比代码旧(升级没重建库):读写会在用户路径上炸。多列 = 库比代码**新** —— 滚动升级窗里一个 * **旧副本**连上了已被新副本重建过的库;它按旧列集读写,新列上的不变量(本例:`gen` 血统)在它眼里 * 根本不存在,于是它会悄悄写出违反新契约的行。后者比前者更隐蔽,所以不做「幽灵列只警告」。 * ⇒ 运维顺序因此是**先升库再升副本**(见 `docs/DEPLOY-PREREQS.md` 滚动升级窗那一节)。 * * ── 期望列集从哪来:**机器派生,不许手抄**([ref])──────────────────────────────────────────── * 期望值 = `schema-ensure-legs.ts` 的录制驱动跑一遍**真的** `ensure*Schema`,把录到的 `CREATE TABLE` * 语句文本解析出列集。于是「代码里的 schema」与「探针拿来对账的 schema」是**同一份字节**,不存在第二 * 处清单会腐掉。(更好的形是把语句数组本身改成结构化再生成 SQL —— 那要改 70 张表的 DDL 写法,爆炸 * 半径远大于本批;先解析文本,理由写在这里。)拒启文案里的那句 `ALTER TABLE … ADD COLUMN …` 也由 * **同一份解析结果**生成,不是手写的。 * * ── 失败方向:读不到就拒,不跳过 ───────────────────────────────────────────────────────────── * information_schema 查询抛错、或整库一行都读不出来 ⇒ **拒启**。探针的语义是「我证明了库等于代码」, * 证不出来时放行等于把一条保护性断言静默降级成祝福([ref]:保护型缺席必须 fail-closed)。 * `recordFailOpen` 在这条腿上**不适用** —— 它是 F 类(装饰/缓存)的登记口,不是 P 类保护臂的逃生门。 */ import { type SchemaDialect } from "./schema-ensure-legs.js"; /** 一列:名字 + 它在 `CREATE TABLE` 里的**原样定义文本**(生成 ADD COLUMN 指路用,不另手写)。 */ export interface ParsedColumn { readonly name: string; readonly ddl: string; } /** 一张表的列形。 */ export interface ParsedTable { readonly table: string; readonly columns: readonly ParsedColumn[]; } /** * 解析一条 `CREATE TABLE`。不是建表语句(CREATE INDEX / INSERT / SELECT / CREATE EXTENSION)⇒ `undefined`。 * * 认识面窄得刻意:只取**表名 + 顶层列名 + 该列的定义文本**。类型、默认值、可空性一律**不比对** —— * 本探针守的是「列在不在」这一类(T-47 的真病),把类型也拉进来会让方言差异(`DATETIME(3)` ⇄ * `TIMESTAMPTZ(3)`、`vector(d)` ⇄ `jsonb` 的运行期参数化)变成一堆假红,而那是另一条判据另一台车。 */ export declare function parseCreateTable(stmt: string): ParsedTable | undefined; /** 启动时真会建出来的**全部**表形(录制 → 解析)。同名表二次出现以**先到**为准(`IF NOT EXISTS` 的真语义)。 */ export declare function expectedTableShapes(dialect: SchemaDialect): Promise>; /** 拒启码(机读)。附录 A / 拒启码表的登记名。 */ export declare const SCHEMA_MISMATCH_ERROR_CODE = "config.schema_mismatch"; /** 一张表的对账结论。 */ export interface TableMismatch { readonly table: string; /** 代码有、库没有(库比代码**旧**)。 */ readonly missing: readonly ParsedColumn[]; /** 库有、代码没有(库比代码**新** —— 滚动窗里的旧副本)。 */ readonly ghost: readonly string[]; } /** 探针读口:一条 SQL → 行数组(两方言由调用方各自绑)。 */ export type SchemaProbeRead = (sql: string) => Promise>>; /** * 库里实际的列集(表名 → 列名集合)。 * * 🔴 **PG 侧读的是「查询真正会解析到的那个关系」,不是 `current_schema()`**(codex 对抗复审本轮 [medium] * 采纳):店里的 SQL 全是**不限定 schema** 的裸表名,它按 `search_path` **逐个** schema 解析,命中第一个; * 而 `current_schema()` 只是路径的第一段。两者不等的部署是真的存在(角色的 `search_path` 配了多段),那时 * 只读第一段会让探针对「表其实住在路径后段、而且缺列」这一形**完全沉默**。⇒ 按 `current_schemas(false)` * 的**顺序**取每张表的第一次命中 —— 探针于是与执行器看同一张表。MySQL 协议没有这个概念(库 = 连接上的 * `DATABASE()`),那条臂一个字不变。 */ export declare function readLiveColumns(read: SchemaProbeRead, dialect: SchemaDialect): Promise>>; /** 对账(纯函数:期望 × 实况 → 不符表清单)。 */ export declare function diffSchema(expected: Map, live: Map>): TableMismatch[]; /** 拒启文案(表名 + 缺/多列 + 两条路;ADD COLUMN 句由解析结果**生成**)。 */ export declare function describeSchemaMismatch(dialect: SchemaDialect, mismatches: readonly TableMismatch[]): string; /** * 启动期对账:**建表之后**跑,不符即抛(= 拒启)。两方言 + InnoDB 同腿(MySQL 协议共用 `mysql` 臂)。 * * 失败方向见文件头注:读抛 / 整库零行 ⇒ 拒。 */ export declare function assertSchemaMatchesCode(read: SchemaProbeRead, dialect: SchemaDialect): Promise; //# sourceMappingURL=schema-consistency.d.ts.map