跳到主要内容
Markdown DB Engine

Runtime Database

运行模式

数据库核心是嵌入式本地引擎。TypeScript 应用可以直接打开数据库目录;本地 Server 也可以作为 Writer Host 打开数据库,并向 Web UI 与其他语言提供 API。

同一 Runtime Database 同一时间只有一个 Writer Host:

  • Embedded Host 持有写锁时,其他进程只能按照锁协议读取或连接该 Host;
  • Local Server 持有写锁时,其他进程必须通过 Server API 写入;
  • 非法的第二个 Writer 必须在打开数据库时失败,不能依赖冲突发生后恢复。

多 Reader 可以并行读取一致快照。

嵌入式 openDatabase() 在读取状态和执行恢复之前通过原子文件创建获取 Writer Lock。锁记录 Database 路径对应的 Token、Host、PID、获取时间和租约到期时间;本机进程仍然存活时不能只因为时间经过而抢占锁,跨主机租约由持有者定期续期。close() 会等待当前 Host 内已经排队的提交完成,再校验 Token 并释放锁。单个 Host 内的所有提交进入同一串行队列,不能使用两个旧快照互相覆盖。

物理目录

database/
└── .mddb/
    ├── data/                    # 正式 Markdown 业务 Record
    ├── schema/                  # Collection Schema
    ├── catalog/index.json       # 路径、UUID、版本、布局、分区与摘要
    ├── indexes/                 # 属性、唯一、全文和关系索引
    ├── system/                  # 用户、角色、凭据、审计与数据库配置
    ├── wal/transactions.json    # Prepared WAL Journal
    ├── snapshots/               # 可以独立打开的备份目录
    ├── state.json               # 格式版本和必要一致性元数据
    └── lock.json                # Writer Host 所有权与租约

data/ 和可导出的 Schema 使用开放格式。Catalog、Indexes、System 与 WAL 可以使用内部格式,并具有独立格式版本。

事务与隔离

运行时 Mutation 与 Workspace Merge 都转换为统一 Changeset,并由同一个 Transaction Engine 提交。

数据库保证:

  • 原子性:一个 Changeset 全部提交或全部回滚;
  • 一致性:提交后满足当前 Schema、唯一约束、Reference 和权限;
  • 隔离性:写事务按照 Serializable 语义串行提交,Reader 使用稳定快照;
  • 持久性:成功返回前,恢复所需 WAL 与正式状态已经安全持久化。

事务可以跨 Collection、文件、Layout 和索引。文件边界不能决定事务边界。

document 按 Record 文件增量替换;tablerecords 对受影响的物理 Collection 文件整体替换。Reader 查询结果使用深拷贝与内部状态隔离,Transaction Reader 持有创建事务时的结构化快照。Record 的 Replace、Patch、Delete 和 Move 都执行 optimistic version 检查。

提交流程

  1. 在当前快照上解析 Mutation 或已确认 Merge Plan;
  2. 检查权限、对象版本、Schema、约束和删除影响;
  3. 生成确定的 Record、Markdown Patch、Catalog 与 Index Changeset;
  4. 持久化 WAL;
  5. 将新文件写入临时位置并完成必要同步;
  6. 原子替换正式文件与内部状态;
  7. 标记事务已提交;
  8. 发布只包含已提交状态的事件。

任意中断点重启后只能恢复到完整提交前或完整提交后状态。

JSON、正式 Markdown、锁和备份均采用同目录临时文件/目录替换,并在文件同步后同步父目录。正常提交按照 Changeset 只同步受影响的物理文件;启动恢复和备份使用完整同步。Prepared WAL 同时进入 wal/transactions.json 和带格式版本的内部状态,恢复时按照 checksum 验证并幂等重放,成功后清除 Prepared Journal。

查询

主要查询接口是类型安全 Query API,不以 SQL 作为核心契约。Query 支持:

  • Collection 与 UUID 读取;
  • 字段相等、不等、范围和集合运算;
  • 嵌套字段与数组条件;
  • 字段投影;
  • 稳定排序与游标分页;
  • 全文搜索;
  • Reference 与反向关系查询;
  • 查询计划与索引选择诊断。

Query AST 对 Runtime Collection 与 Transaction Snapshot 使用同一套执行函数。布尔组合支持 andornot,字段谓词支持相等、范围、集合、存在性、前缀、数组包含和全文 Token 匹配。投影始终保留 _id_version。排序使用 UUID 作为最终 Tie-breaker;Cursor 同时绑定 Query 摘要与 Catalog Version,Query 或 Runtime Snapshot 改变后返回 PLAN_STALE,不能在变化后的数据集上静默继续分页。

SQL 可以作为兼容适配器存在,但不能定义内部数据模型,也不能削弱 Collection、Body、嵌套对象与 Workspace 语义。

索引

数据库支持:

  • UUID 主索引;
  • 普通字段索引;
  • 唯一索引;
  • 复合索引;
  • 全文索引;
  • Reference 与反向关系索引。

Schema 声明需要持久化的索引。每次 Schema 提交都会按照名称、类型与字段序列对账:已经删除的定义会删除对应 Index,同名但类型或字段变化的定义会使用当前正式数据重建。Query Planner 可以使用统计信息选择索引。索引不是正式业务数据,删除后可以从 Data、Schema 与 Catalog 重建,并验证查询等价性。

Index Snapshot 使用独立格式版本、Catalog Version 与 SHA-256 摘要,保存每个 Index 的 Posting、Record-to-Key 映射、条目数和不同值数量。普通提交根据 Changeset 更新受影响 Record;完整 Rebuild 从已提交 Record 与 Schema 确定性生成相同摘要。打开数据库时只接受 Catalog Version 与摘要都匹配的 Snapshot,否则 Writer 重建,Reader 使用内存重建结果。Index 文件可以损坏或删除,但不能改变正式 Markdown 数据。

Query Planner 从 and 条件中收集可以使用 Index 的等值、集合与全文谓词,优先选择覆盖字段更多的有效定义;复合 Index 只有在全部字段都有等值条件时才会参与计划。Planner 根据 Index 统计返回 scanindex 策略、Index 名称和预计行数。执行器先通过 Posting 缩小候选集合,再使用同一 Query AST 复核,因此 Index 与全表扫描具有相同结果语义。

运行时 Mutation

Mutation API 支持 Insert、Replace、Patch、Delete、Move、批量 Changeset 和 Transaction。CollectionHandle 是绑定 Runtime 与 Collection 名称的长期视图,每次操作都读取最新已提交状态,不能缓存创建时的 Record Map。Patch 只把调用者明确提供的字段写入 Changeset;Document Layout 使用 YAML AST 修改 Frontmatter,并在正常提交与 Prepared WAL 恢复时增量重放,因此保留未触及的注释、字段顺序、Body 和其他 Markdown 字节。

每次成功修改(包括 Move)增加 Record Version。Version 是 optimistic concurrency control 的整数或不可比较 Token,不是用户可浏览的历史 Revision。

提交前统一加载正式 Schema,并在同一目标状态上应用 Default,检查 strict 字段、类型、required、nullable、复合唯一和正向 Reference。删除目标 Record 时,按照反向 Reference 关系展开 restrictcascadesetNullremove,展开后的 Changeset 与用户 Changeset 一起进入同一 WAL 和 Transaction Engine。Replace 会删除未提供的旧字段,Patch 只合并明确提供的字段。

事件

Transaction Commit 后发布:

record.created
record.updated
record.deleted
schema.changed
merge.committed
index.rebuilt

事件包含事务 ID、Record UUID、Collection、变化类型和提交时间,不默认包含敏感正文。订阅者只会看到已提交状态,不能通过事件读取未提交 Workspace。

同一事务的多条 Event 共享 txId;以 txId 继续读取时跳过整笔事务,不会从事务中间继续。Event 只保存身份、Collection、类型、Actor 和时间,不保存正文或字段值。

恢复与完整性

数据库提供:

  • 启动恢复与 WAL 重放;
  • Catalog、Schema、正式 Markdown 和 Index 一致性检查;
  • 索引删除与重建;
  • 损坏文件和无法解析 Record 的诊断;
  • 数据目录备份所需的安全快照接口;
  • 内部格式升级前检查与失败恢复。

正式 Markdown 损坏时不能使用旧索引伪装成正常数据。数据库应进入可诊断的受限状态,保留未损坏数据的读取能力,并阻止可能扩大损坏的写入。

Runtime 打开时会比较正式 Markdown 与按照 Catalog 计算的 Record ID、物理路径和 SHA-256 摘要。任何缺失、额外、无法解析或摘要不一致的文件都会使数据库进入受限状态;受限状态拒绝 Mutation 和备份,且不会使用旧内部状态覆盖损坏文件。管理员修复正式文件后可以再次执行 check(),验证通过后解除受限状态。

Catalog 为每条 Record 保存 Database ID、Collection、Record ID、物理路径、Layout、Partition、Record Version 和正式文件实际字节摘要。提交先按照变化写入正式 Markdown,再从受影响文件读取真实字节并刷新 Catalog,不能使用重新渲染的理想内容代替磁盘摘要。Catalog 与 Index 不属于正式业务数据,但 Catalog 映射必须与正式 Markdown 和必要内部状态保持一致。

createSnapshot() 在 Writer Host 内创建经过完整同步的隔离数据库目录 .mddb/snapshots/{snapshotId}/,并复制正式 Schema,使备份保持相同的字段、唯一与 Reference 约束。该目录拥有自己的 .mddb 结构,可以通过 openDatabase() 独立打开并执行完整性检查;同名 JSON 文件只作为兼容的快照摘要。

提交协议的故障验证

packages/core/src/runtime/storage/commit-pipeline.ts 在提交流程的 6 个关键节点(WAL 准备前/后、正式数据同步前/后、提交标记前/后)与恢复流程的 2 个关键节点(数据同步前/后)设置了故障点;formal-data.ts 另外在每个正式文件替换或删除之后提供编号故障点。故障点可以通过 MDDB_CRASH_AT 环境变量触发真实 process.exitpackages/core/tests/storage/runtime-crash.test.ts 使用 node:child_processdist/ 编译产物上执行真实子进程崩溃,验证单文件、多文件与恢复中断只能收敛到完整提交前或提交后状态、不留临时文件、且重复重启结果幂等。packages/core/tests/storage/runtime-concurrency.test.ts 使用真实子进程通过 openDatabase() 持有 Writer Host,验证第二个 Host 在打开阶段被拒绝,并验证 Host 内并发提交串行化和 Reader Snapshot 隔离。