import type { Pool as MysqlPool } from "mysql2/promise"; import type { Pool as PgPool } from "pg"; /** 迁移读数(启动日志一行写全;测试按它断言,不靠日志文本)。 */ export interface OneTimeMigrationReport { /** M1:退役快照索引表 —— `dropped` = 本次真 drop 了,`absent` = 早已不在(第二跑/全新库)。 */ readonly retiredSnapshotTable: "dropped" | "absent"; /** M1:drop 之前表上的行数(`absent` 时恒 0)。非零 = 这次迁移真删掉了存量行,必须让运维看见。 */ readonly retiredSnapshotRows: number; /** M2:退役单座列 —— `dropped` = 本次真 DROP COLUMN,`absent` = 早已不在。 */ readonly retiredSteerSeatColumn: "dropped" | "absent"; /** M2:DROP COLUMN 随手丢掉的**陈旧**格数(行已不是 `pending` ⇒ 那条指令再也送不出去;`absent` 时恒 0)。 * 非零 = 这次迁移真丢了存量字节,必须让运维看见 —— 与 M1 的 `retiredSnapshotRows` 逐字同一条纪律。 */ readonly retiredSteerSeatStaleRows: number; /** M3:改名的行数(账本 `tool_end` 行 + 已决 checkpoint 的执行判词列,两者相加)。 */ readonly askOriginRowsMigrated: number; } /** 三条迁移都不适用的载体(local 车道没有 SQL 表)。 */ export declare const NO_SQL_MIGRATIONS: Omit; /** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */ export interface MigrationLogSink { warn(msg: string, meta?: Record): void; info(msg: string, meta?: Record): void; } /** * M2 的拒启。**具名类型**:`openStoreBackendWithFallback` 对默认推导的 local 有降级臂,而本拒启若与 * 普通 Error 同形会被吃掉 —— operator 面对的就会是「服务起来了,但那条指令悄悄没了」 * (`RunStoreStrictHydrateError` 的同族先例,锚类型不锚文案)。 */ export declare class RetiredSteerSeatError extends Error { readonly rows: number; /** 结构化事件名(日志消费端按它分派;文案会变,这个不会)。 */ readonly code = "checkpoint.pending_steer_seat_retired"; constructor(rows: number); } /** * SQL 两后端的一次性迁移。**调用点只有一个**(boot/stores.ts,在 `ensureSchema` 之后、任何路由装配 * 之前)—— 拒启在那一刻仍然是拒启(还没开始服务),而 `ensureSchema` 那条通道的契约是「只发 CREATE」 * (`test/ensure-schema.test.ts` 的运行时门),增量语句一条都不许进去。 * * ## 并发(codex r1 [medium],亲核复现后修) * 一次滚动升级会让 N 个副本同时跑到这里。整段因此跑在**一条连接**上、握着**与 `ensureSchema` 同一把** * advisory 锁(MySQL 具名锁 / PG `pg_try_advisory_lock`,两方言都是非阻塞轮询形):本段是「启动期改 schema 的活」,与 ensure 属同一类, * 复用同一把锁 ⇒ 本仓这一类工作只有**一个**锁身份,而且本段也不会与另一个副本的 ensure 交错。 * 锁拿不到时按既有先例**降级**(不阻塞启动),但降级不等于放任:每条破坏性 DDL 的成功判据是它的 * **后置条件**({@link execUntil}),所以「另一个副本抢先删掉了同一列」不会把滚动升级变成启动失败循环。 * * ## 次序(codex r1 [medium] 同轮) * 读 → 非破坏改写 → 破坏性 DDL。①拒启臂是纯读,拒的时候盘上一个字节没动;②M3 是改值不丢信息的 * UPDATE,它是最可能超时的一步,放在删表删列**之前**;③真正的破坏排在最后,且失败时把**已完成的 * 破坏性步骤及其读数**写进拒因 —— 否则「删完表再失败」会让重试看到一张不存在的表、把行数报成 0, * 证据永久消失。 * * @throws {RetiredSteerSeatError} M2 探到非空的退役席位(唯一的拒启臂)。 */ export declare function runSqlOneTimeMigrations(dialect: "tidb" | "pg", pool: MysqlPool | PgPool): Promise; /** * M3 的**纯**改写(local 车道的账本行逐条走它;SQL 两腿在服务端用等价的 JSON 路径表达同一件事)。 * 不改原对象(调用方与账本共享引用):有变更时换一只信封,原记录一个字节不动;无变更 ⇒ 返回 * `undefined`,调用方据此**不写盘**(第二跑因此零 I/O)。 */ export declare function migrateRetiredAskOrigin(type: string, data: unknown): Record | undefined; /** * 启动日志的读数行。零命中(第二跑起的常态)也照发 —— 「跑过了、什么都没改」与「压根没跑」必须可分辨。 * * 🔴 **级别按「这次真动了没有」分**:动了 ⇒ `warn`(有人的持久数据被改写/删除了,运维该看见一次); * 没动 ⇒ `info`。把恒常的空转也发成 warn 会训练运维忽略 warn —— 那是在为了本段的可见性去损耗整条告警轴。 */ export declare function logOneTimeMigrations(log: MigrationLogSink, backend: string, report: OneTimeMigrationReport): void; //# sourceMappingURL=one-time-migrations.d.ts.map