关于 cdc
cdc 是 Turso 内部变更数据捕获功能的说明,按连接记录表变更及事务边界。同步引擎读取 CDC 表中的记录,将本地变更推送到远程端。
主要功能
- 01捕获 INSERT、UPDATE 和 DELETE 变更
- 02保存变更前后的记录映像
- 03在 v2 中记录事务边界
- 04通过 PRAGMA 启用或关闭捕获
- 05向同步引擎提供捕获的变更
适用场景
- 01跟踪本地数据库变更以供同步
- 02在重放时按事务归组变更
- 03检查捕获的行数据和架构变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tursodatabase/ turso --skill cdc安装后在 Claude Code 中输入/cdc确认已加载。
详细介绍
CDC(更改数据捕获)- 内部功能图
概述
CDC 会跟踪数据库表上的 INSERT/UPDATE/DELETE 更改,并将更改记录写入专用 CDC 表(默认名为 turso_cdc)。CDC 按连接启用,通过 PRAGMA 开启,并在字节码生成(转换)层运行。同步引擎会使用 CDC 记录,将本地更改推送到远程端。
架构图
User SQL (INSERT/UPDATE/DELETE/DDL)
|
v
┌─────────────────────────────────────────────────┐
│ Translate layer (core/translate/) │
│ ┌───────────────────────────────────────────┐ │
│ │ prepare_cdc_if_necessary() │ │
│ │ - checks CaptureDataChangesInfo │ │
│ │ - opens CDC table cursor (OpenWrite) │ │
│ │ - skips if target == CDC table itself │ │
│ └───────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────┐ │
│ │ emit_cdc_insns() │ │
│ │ - writes (change_id, change_time, │ │
│ │ change_type, table_name, id, │ │
│ │ before, after, updates) into CDC tbl │ │
│ └───────────────────────────────────────────┘ │
│ + emit_cdc_full_record() / emit_cdc_patch_record() │
└─────────────────────────────────────────────────┘
|
v
CDC table (turso_cdc or custom name)
|
v
┌─────────────────────────────────────────────────┐
│ Sync engine (sync/engine/) │
│ DatabaseTape reads CDC table → DatabaseChange │
│ → apply/revert → push to remote │
└─────────────────────────────────────────────────┘
核心数据类型
CaptureDataChangesMode + CaptureDataChangesInfo — core/lib.rs
CDC 的行为由两种类型控制:
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
#[repr(u8)]
enum CdcVersion {
V1 = 1,
V2 = 2,
}
const CDC_VERSION_CURRENT: CdcVersion = CdcVersion::V2;
enum CaptureDataChangesMode {
Id, // capture only rowid
Before, // capture before-image
After, // capture after-image
Full, // before + after + updates
}
struct CaptureDataChangesInfo {
mode: CaptureDataChangesMode,
table: String, // CDC table name
version: Option<CdcVersion>, // schema version (V1 or V2)
}
连接会存储 Option<CaptureDataChangesInfo>:None 表示 CDC 已关闭。
CdcVersion 的关键方法:
has_commit_record()—self >= V2,用于控制是否发出 COMMIT 记录Display/FromStr— 在"v1"↔V1、"v2"↔V2之间进行往返转换
CaptureDataChangesInfo 的关键方法:
parse(value: &str, version: Option<CdcVersion>)— 解析 PRAGMA 参数"<mode>[,<table_name>]";如果为 "off",则返回Nonecdc_version()— 返回CdcVersion(如果版本为 None,则会 panic)。这是替代旧is_v1()/is_v2()/version()方法的单一访问器。has_before()/has_after()/has_updates()— 检查模式支持的功能mode_name()— 以字符串形式返回模式
Option<CaptureDataChangesInfo> 上的便捷 trait CaptureDataChangesExt 提供:
has_before()/has_after()/has_updates()— 委托给内部值;如果为 None,则返回 falsetable()— 返回Option<&str>;如果 CDC 已关闭,则返回 None
CDC 表架构 v1
默认表名:turso_cdc(常量 TURSO_CDC_DEFAULT_TABLE_NAME)
CREATE TABLE turso_cdc (
change_id INTEGER PRIMARY KEY AUTOINCREMENT,
change_time INTEGER, -- unixepoch()
change_type INTEGER, -- 1=INSERT, 0=UPDATE, -1=DELETE
table_name TEXT,
id <untyped>, -- rowid of changed row
before BLOB, -- binary record (before-image)
after BLOB, -- binary record (after-image)
updates BLOB -- binary record of per-column changes
);
CDC 表架构 v2(当前版本)
CREATE TABLE turso_cdc (
change_id INTEGER PRIMARY KEY AUTOINCREMENT,
change_time INTEGER, -- unixepoch()
change_type INTEGER, -- 1=INSERT, 0=UPDATE, -1=DELETE, 2=COMMIT
table_name TEXT,
id <untyped>, -- rowid of changed row
before BLOB, -- binary record (before-image)
after BLOB, -- binary record (after-image)
updates BLOB, -- binary record of per-column changes
change_txn_id INTEGER -- transaction ID (groups rows into transactions)
);
v2 新增:
change_txn_id列 — 按事务对 CDC 行分组。通过conn_txn_id(candidate)操作码分配事务 ID,该操作码会获取或设置每连接的事务 ID。change_type=2(COMMIT)记录 — 标记事务边界。在自动提交模式下,每条语句发出一次;也会在显式执行COMMIT时发出。
CDC 表由 InitCdcVersion 操作码在运行时通过 CREATE TABLE IF NOT EXISTS 创建。
CDC 版本表
首次启用 CDC 时,会创建一个版本跟踪表:
CREATE TABLE turso_cdc_version (
table_name TEXT PRIMARY KEY,
version TEXT NOT NULL
);
当前版本:CDC_VERSION_CURRENT = CdcVersion::V2(定义于 core/lib.rs,并从 core/translate/pragma.rs 重新导出)
InitCdcVersion 中的版本检测
InitCdcVersion 操作码会在创建 CDC 表之前检查该表是否已存在,以检测 v1 或 v2:
- 如果 CDC 表已存在,但没有版本行 → v1(版本跟踪功能推出前已存在的表)
- 如果 CDC 表不存在 → 使用当前版本(v2)创建
- 如果版本行已存在 → 按原样使用该版本
DatabaseChange — sync/engine/src/types.rs:229-249
同步引擎中 CDC 行的 Rust 表示。具有用于正向和反向重放的 into_apply() 和 into_revert() 方法。
OperationMode — core/translate/emitter.rs
emit_cdc_insns() 使用它来确定 change_type 的值:
INSERT→ 1UPDATE/SELECT→ 0DELETE→ -1COMMIT→ 2(仅限 v2,由emit_cdc_commit_insns发出)
入口点
1. PRAGMA — 启用/禁用 CDC
设置: core/translate/pragma.rs
- 检查是否未启用 MVCC(CDC 和 MVCC 互斥)
- 使用
CDC_VERSION_CURRENT通过CaptureDataChangesInfo::parse()解析模式字符串 - 发出单个
InitCdcVersion操作码 — 所有 CDC 设置(表创建、版本跟踪、状态更改)都在执行时进行
获取(读取当前模式): core/translate/pragma.rs
- 返回 3 列:
mode、table、version - 关闭时:返回
("off", NULL, NULL) - 启用时:返回
(mode_name, table, version)
Pragma 注册: core/pragma.rs — CaptureDataChangesConn(以及已弃用的别名 UnstableCaptureDataChangesConn),列为 ["mode", "table", "version"]
2. 连接状态
字段: core/connection.rs — capture_data_changes: RwLock<Option<CaptureDataChangesInfo>>
Getter: get_capture_data_changes_info() — 返回读锁
Setter: set_capture_data_changes_info(opts: Option<CaptureDataChangesInfo>)
默认值: 初始化为 None(CDC 关闭)
3. ProgramBuilder 集成
字段: core/vdbe/builder.rs — capture_data_changes_info: Option<CaptureDataChangesInfo>
访问器: capture_data_changes_info() — 返回 &Option<CaptureDataChangesInfo>
传入来源: core/translate/mod.rs — 创建 builder 时从连接中读取
4. PrepareContext
字段: core/vdbe/mod.rs — capture_data_changes: Option<CaptureDataChangesInfo>
设置来源: PrepareContext::from_connection() — 从 connection.get_capture_data_changes_info() 克隆
5. InitCdcVersion 操作码 — core/vdbe/execute.rs
始终由 PRAGMA SET 发出。在执行时处理所有 CDC 设置:
- 对于“off”:在
state.pending_cdc_info中存储None,并提前返回 - 检查 CDC 表是否已存在(用于向后兼容 v1)
- 创建 CDC 表(
CREATE TABLE IF NOT EXISTS <cdc_table_name> ...)— 使用带有change_txn_id列的 v2 架构 - 创建版本表(
CREATE TABLE IF NOT EXISTS turso_cdc_version ...) - 插入版本行:如果 CDC 表此前已存在 → “v1”,否则 → 当前版本(“v2”)。使用
INSERT OR IGNORE保留现有版本行。 - 从表中读回实际版本
- 将计算出的
CaptureDataChangesInfo存入state.pending_cdc_info
连接的 CDC 状态不会在操作码中应用。相反,只有在事务成功提交后,pending_cdc_info 才会在 halt() 中应用。这确保了原子性:如果任何步骤失败并导致事务回滚,连接的 CDC 状态将保持不变。
所有表的创建都通过嵌套的 conn.prepare()/run_ignore_rows() 调用完成,而不是通过发出字节码,因为 PRAGMA 计划无法包含针对架构中尚不存在的表执行的 DML。
字节码生成(core/translate/emitter.rs)
以下是 CDC 代码生成的核心函数:
| 函数 | 用途 |
|---|---|
prepare_cdc_if_necessary() | 如果 CDC 已启用且目标不是 CDC 表,则打开 CDC 表游标 |
emit_cdc_full_record() | 从游标读取所有列并放入 MakeRecord(用于变更前/后镜像) |
emit_cdc_patch_record() | 根据正在使用的寄存器值构建记录(用于 INSERT/UPDATE 的变更后镜像) |
emit_cdc_insns() | 为每个已变更的行(INSERT/UPDATE/DELETE)写入一条 CDC 行。在 DML 循环中逐行调用。 |
emit_cdc_commit_insns() | 将 COMMIT 记录(change_type=2)写入 CDC 表(仅限 v2)。直接生成,不检查自动提交状态。 |
emit_cdc_autocommit_commit() | 在语句结束时生成 COMMIT。运行时检查 is_autocommit() — 仅在自动提交模式下生成。仅限 v2。 |
COMMIT 生成策略(v2)
逐行调用点使用 emit_cdc_insns()(不生成 COMMIT)。语句结束时的调用点会调用 emit_cdc_autocommit_commit(),该函数会在运行时检查 is_autocommit():
- 自动提交模式: 语句完成后生成一条 COMMIT 记录
- 显式事务(
BEGIN...COMMIT): 跳过每条语句后的 COMMIT;显式的COMMIT语句通过emit_cdc_commit_insns()生成 COMMIT 记录
这样可以确保像 INSERT INTO t VALUES (1),(2),(3) 这样的多行语句只在最后生成一条 COMMIT,而不是每行一条。
集成点 — CDC 记录的生成位置
INSERT — core/translate/insert.rs
- 逐行: 插入后调用
emit_cdc_insns();对于 REPLACE/冲突,在删除前调用 - 语句结束时: 插入循环后,在
emit_epilogue()中调用emit_cdc_autocommit_commit()
UPDATE — core/translate/emitter.rs
- 逐行: 捕获变更前镜像,通过 patch record 捕获变更后镜像,并生成
emit_cdc_insns() - 语句结束时: 更新循环后调用
emit_cdc_autocommit_commit()
DELETE — core/translate/emitter.rs
- 每行: 捕获前像并发出
emit_cdc_insns() - 语句结束时: 删除循环后调用
emit_cdc_autocommit_commit()
UPSERT (ON CONFLICT DO UPDATE) — core/translate/upsert.rs
- 每行: 在以下三种情况下均调用
emit_cdc_insns():纯插入、冲突后更新、替换 - 语句结束时不执行 COMMIT — upsert 与 INSERT 共用其收尾逻辑
Schema 变更(DDL)— core/translate/schema.rs
- CREATE TABLE: 调用
emit_cdc_insns()(插入sqlite_schema)+emit_cdc_autocommit_commit() - DROP TABLE: 在元数据循环中逐行调用
emit_cdc_insns()+ 循环后调用emit_cdc_autocommit_commit() - CREATE INDEX: 调用
emit_cdc_insns()+emit_cdc_autocommit_commit()(core/translate/schema.rs) - DROP INDEX: 逐行调用
emit_cdc_insns()+ 循环后调用emit_cdc_autocommit_commit()(core/translate/index.rs)
显式事务中的 DDL(BEGIN; CREATE TABLE t(x); COMMIT)不会为每条语句发出 COMMIT — 自动提交检查会阻止这一操作。
ALTER TABLE — core/translate/update.rs
- 当 CDC 处于 updates 模式时,在更新计划上设置
cdc_update_alter_statement
视图/触发器 — 明确排除
core/translate/view.rs— 为 CDC 游标传入Nonecore/translate/trigger.rs— 为 CDC 游标传入None
子查询 — 不启用 CDC
core/translate/subquery.rs—cdc_cursor_id: None
辅助函数(用于读取 CDC 数据)
table_columns_json_array(table_name) — core/function.rs、core/vdbe/execute.rs
返回表的列名 JSON 数组。用于解析二进制记录。
bin_record_json_object(columns_json, blob) — core/function.rs、core/vdbe/execute.rs
使用列名将二进制记录(来自 before/after/updates 列)解码为 JSON 对象。
同步引擎集成
同步引擎是 CDC 数据的主要使用方。
DatabaseTape — sync/engine/src/database_tape.rs
- CDC 配置:
DEFAULT_CDC_TABLE_NAME = "turso_cdc"、DEFAULT_CDC_MODE = "full" - PRAGMA 名称:
CDC_PRAGMA_NAME = "capture_data_changes_conn" - 初始化:
connect()设置 CDC pragma,并从turso_cdc_version表缓存cdc_version。必须在调用iterate_changes()之前调用。 - 版本缓存:
cdc_version: RwLock<Option<CdcVersion>>— 由connect()设置,由iterate_changes()读取。若未设置则触发 panic。 - 迭代器:
DatabaseChangesIterator分批读取 CDC 表,并生成DatabaseTapeOperation。对于 v2,会从表中生成真实的 COMMIT 记录;对于 v1,会在批次末尾追加一个合成的 Commit。ignore_schema_changes: true(默认值)会过滤掉sqlite_schema行变更,但不会过滤 COMMIT 记录。
同步操作 — sync/engine/src/database_sync_operations.rs
- 变更计数:
SELECT COUNT(*) FROM turso_cdc WHERE change_id > ?
同步引擎 — sync/engine/src/database_sync_engine.rs
- 初始化:
open_db()会调用main_tape.connect(coro),确保在任何iterate_changes()调用前设置好 CDC 并缓存版本。 - 在
apply_changes期间,检查 CDC 表是否存在,并在同步后重新创建该表
重放生成器 — sync/engine/src/database_replay_generator.rs
- 要求填充
updates列(full 模式)
绑定中的 CDC 接口
所有绑定都将 cdc_operations 作为同步统计信息的一部分公开:
| 绑定 | 文件 |
|---|---|
| Python | bindings/python/src/turso_sync.rs |
| JavaScript | bindings/javascript/sync/src/lib.rs |
| JS(生成器) | bindings/javascript/sync/src/generator.rs |
| Go | bindings/go/bindings_sync.go |
| React Native | bindings/react-native/src/types.ts |
| SDK Kit(C 头文件) | sync/sdk-kit/turso_sync.h |
| SDK Kit(Rust) | sync/sdk-kit/src/bindings.rs |
测试
- 集成测试:
tests/integration/functions/test_cdc.rs— 覆盖所有模式、CRUD、事务、架构变更、版本表和向后兼容性。已在tests/integration/functions/mod.rs中注册。 - 同步引擎测试:
sync/engine/src/database_tape.rs— CDC 表读取、磁带迭代、架构变更重放。 - JS 绑定测试:
bindings/javascript/sync/packages/{wasm,native}/promise.test.ts
运行:cargo test -- test_cdc(集成测试)或 cargo test -p turso_sync_engine -- database_tape(同步引擎)。
面向用户的文档
- CLI 手册页:
cli/manuals/cdc.md— 可在 REPL 中通过.manual cdc访问 - 数据库手册:
docs/manual.md— CDC 章节已链接到目录中
关键设计决策
- 按连接而非按数据库。 每个连接都有自己的 CDC 模式,并且可以指定不同的表。
- 在字节码层实现。 翻译期间,CDC 指令与实际的 DML 字节码一起生成,不使用运行时钩子或触发器。
- 排除自身变更。 对 CDC 表和
turso_cdc_version表的变更不会被捕获(在prepare_cdc_if_necessary中检查)。 - 跟踪架构变更。 DDL 操作会记录为对
sqlite_schema表的变更。 - 二进制记录格式。 before/after/updates 列使用 SQLite 的 MakeRecord 格式(与 B-tree 负载相同)。
- 感知事务。 CDC 写入与 DML 在同一事务中执行,因此回滚会自然丢弃 CDC 条目。
- 版本跟踪。 CDC 架构版本记录在
turso_cdc_version表中,并存储在CaptureDataChangesInfo.version中,以支持未来的架构演进。 - 原子 PRAGMA。 连接 CDC 状态通过
ProgramState中的pending_cdc_info延迟应用,并且仅在 Halt 时生效。如果 PRAGMA 的磁盘写入失败且事务回滚,连接状态将保持不变。 - 按语句提交(v2)。 每条语句只生成一次 COMMIT 记录(而非每行一次),使用
emit_cdc_autocommit_commit()在运行时检查is_autocommit()。在显式事务中,只有最终的COMMIT才会生成 COMMIT CDC 记录。 - 向后兼容的版本检测。 通过在创建表之前检查表是否存在,检测预先存在的 v1 CDC 表(没有
turso_cdc_version)。现有表会在版本表中插入CdcVersion::V1。 - 类型化版本枚举。 带有
#[repr(u8)]和Ord/PartialOrd的CdcVersion枚举支持通过整数比较进行功能门控(has_commit_record()=self >= V2)。Display/FromStr可处理数据库往返转换。 - CDC 与 MVCC 互斥。 在 MVCC 已启用时启用 CDC(或反之)会返回错误。在设置 PRAGMA 和切换日志模式时都会进行检查。