import mysql from "mysql2/promise"; import type { Pool, PoolConnection } from "mysql2/promise"; import type { ServiceConfig } from "../config-types.js"; import { type IndexSpec } from "./ensure-index.js"; /** * Shared TiDB (MySQL wire-compatible) connection pool for the durable Session center (L1). * Owned by `main.ts`; closed on shutdown. * * Read/write pool — this is the persistence center. (The SQL *tool* given to the model must use a * separate read-only pool/credential; never hand this pool to a tool.) */ /** Build the mysql2 pool options from TiDB config (pure → unit-testable apart from the live connection). */ export declare function tidbPoolOptions(tidb: NonNullable): mysql.PoolOptions; /** ── [ref] 同查结论:mysql2 与 pg-pool **不同病**,本池不需要(也不应)挂进程保命监听 ──────────────── * pg 侧的病是「idle client 错误 emit 到 Pool 且无监听 ⇒ 未捕获 ⇒ 整进程 exit 1」(修在 pg-pool.ts * createPgPool 头注)。mysql2@3.22.6 亲读(不信文档,以实现为准): * · `lib/pool_connection.js` —— PoolConnection 构造器**恒挂** `once('error', () => * this._removeFromPool())`:每条池连接天生有 error 监听(**借出期同在**——不像 pg-pool 借出时 * 摘监听,mysql2 的 checked-out 连接也无裸奔窗),EventEmitter 不会把 idle 死转 throw, * 坏连接就地淘汰; * · `lib/base/connection.js` `_notifyError` —— fatal 闩(`if (this._fatalError) return`)挡同一 * 连接的二次 emit;`_handleFatalError` 摘 stream data 监听 ⇒ 闩后 `protocolError` 结构不可达; * · `lib/base/pool.js` / `lib/promise/pool.js` —— 池层与 promise 包装层都**不**把连接 error 再 * emit 到池对象上(池层只 emit acquire/connection/enqueue/release)。 * ⇒ 空闲连接被 KILL / 故障切换杀掉 = 库内吞并淘汰,进程不死、池自愈。真 TiDB KILL 钉在 * `test/pool-idle-error-db-integration.test.ts` 第③格。升 mysql2 大版时按上面三坐标复核。 */ export declare function createTidbPool(config: ServiceConfig): Pool; /** * Idempotent schema. `session_event` is the append-only L1 event log (positional slicing by * `(session_id, seq)`); `session_meta` holds the branch leaf + the optimistic-lock counter * (`leaf_seq`) + tenant `owner`. * * ── SCHEMA POLICY (clay 裁,2026-07-26) ───────────────────────────────────────────────────────────── * 本仓 schema 的唯一真源 = **这份代码**(此数组 + `pg-pool.ts` 的 PG 孪生)。口径: * * 1. **删库重建,不做增量。** 尚未上生产、单人本地,存量库一律 DROP DATABASE 重建;因此这里**不再保留 * 任何 `ALTER TABLE` 增量 seam**。历史上的 21 条 ADD COLUMN / 3 条 CREATE INDEX / 1 条 MODIFY COLUMN * 已于 2026-07-26 全部折进下面的 `CREATE TABLE`(它们承载的语义已逐条搬成列上的行内注释 —— 那些注释 * 是 pre-migration 行为契约的**唯一书面记录**,删注释 = 丢语义)。 * 2. **不许再新增 `ALTER TABLE` seam。** 要改列/加列/改可空性 ⇒ 直接改下面的 `CREATE TABLE`,然后重建库。 * (再开增量通道就等于把「代码是真源」变回「代码 + 一串按时间排的补丁才是真源」。) * 3. 生成的全集 SQL 基线导出到 `docs/schema/baseline-mysql.sql`(由本数组机器生成;它是**产物**,不是真源 * —— 冲突时以本文件为准并重新生成)。 * * 注意:`ensureSchema` 仍然保留(建表 + advisory lock + image_bake_admit 种子行),只是语句数组现在是纯 * CREATE,不再有 information_schema 探针。 */ export declare const SCHEMA_STATEMENTS: readonly string[]; /** Named MySQL/TiDB advisory lock that serializes the whole `ensureSchema` DDL run (see below). */ export declare const ENSURE_SCHEMA_LOCK = "sema_ensure_schema"; /** Acquire {@link ENSURE_SCHEMA_LOCK} on `conn` (the lock is CONNECTION-level, so the caller MUST then run all the * serialized work on this SAME connection and release with {@link releaseEnsureSchemaLock}). Returns whether it was * acquired. Exported so test setup that does ad-hoc DDL (e.g. a DROP-then-recreate) can serialize against * ensureSchema runs using the IDENTICAL lock. * * Under heavy concurrency (the integration suite fans ~20 callers at the SAME named lock; or a fleet boots N * replicas at once) a BLOCKING GET_LOCK(name, 30) would park ~19 sessions in a pessimistic WAIT on TiDB's advisory- * lock row — which both exhausts TiDB's "pessimistic lock retry limit" (surfacing as a thrown error) AND starves the * app's OWN `SELECT … FOR UPDATE` paths (image-bake admission/lease) of pessimistic-lock budget. So we acquire * NON-BLOCKING (GET_LOCK(name, 0) returns 0 immediately if held) and POLL with jittered backoff: one holder, the * rest sleeping (not blocked in-DB). Polls until acquired (7.83.0: never degrades to unserialized — the S-287 * `ADD INDEX` leg is not race-safe); a connection whose probe keeps throwing surfaces as a boot failure. */ export declare function acquireEnsureSchemaLock(conn: PoolConnection, tag?: string): Promise; /** Release {@link ENSURE_SCHEMA_LOCK} on `conn` (best-effort — the lock also auto-frees when the connection closes). */ export declare function releaseEnsureSchemaLock(conn: PoolConnection, tag?: string): Promise; /** * S-287:`SCHEMA_STATEMENTS` 建的那批表**在存量库上可能缺的索引**(声明与 PG 侧逐条共用同一份 —— 见 * `plugins/ensure-index.ts` 头注与 `plugins/schema-indexes.ts`)。 * * 组合口径与 `SCHEMA_STATEMENTS` **一一对应**:凡是它建的表,索引声明就进这个数组;它不建的表 * (device / roster / background-agent / mailbox / task-attachment / memory 两族……由 `boot/stores.ts` * 各自的 `ensureTiDB*Schema` 腿建)由那条腿自己应用自己的声明 —— 否则这里会对一张还不存在的表发 ALTER。 * * 新建库上这批索引早已随 `CREATE TABLE` 的内联 `KEY` 到货 ⇒ `ensureIndex` 的探测全部命中、**零 DDL**。 */ export declare const SCHEMA_INDEXES: readonly IndexSpec[]; /** Create tables if absent (+ seed the image-bake admission sentinel). Safe to call on every startup. * * Per the SCHEMA POLICY on {@link SCHEMA_STATEMENTS} this is a pure CREATE run — there are NO additive * ALTER/CREATE-INDEX seams and no information_schema probes any more (2026-07-26): the schema contract is * drop-and-recreate, so every column / index / nullability lives in its `CREATE TABLE`. Do not reintroduce a * migration loop here; change the CREATE and rebuild the database. * * Concurrency: callers run this concurrently — tests fan ALL integration suites out in parallel (each * beforeAll calls ensureSchema), and in production every replica calls it at boot (`--scale service=4`). * Concurrent DDL on the SAME table makes TiDB race in its schema catalog. We serialize the entire run * behind {@link ENSURE_SCHEMA_LOCK} (a named GET_LOCK), which is CONNECTION-level — so the lock AND every * DDL statement run on the one connection we hold here (pooled `pool.query` could land on a different * connection and silently void the lock). The DDL semantics are unchanged; only the timing is serialized. */ export declare function ensureSchema(pool: Pool): Promise; //# sourceMappingURL=tidb-pool.d.ts.map