Files
ChuSQL/docs/architecture.md
T
ChuNa7a 6996e19694
build / build (push) Blocked by required conditions
tests / build (push) Waiting to run
tests / tests (push) Waiting to run
修bug,完善功能
2026-10-16 23:52:12 +08:00

324 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构与组件
[← 返回 README](../README.md)
本页内容:
- [组件与访问路径](#组件与访问路径)
- [数据目录](#数据目录)
- [会话与并发](#会话与并发)
- [SQL 查询处理](#sql-查询处理)
- [快照事务](#快照事务)
- [WAL 与崩溃恢复](#wal-与崩溃恢复)
- [页面与空间回收](#页面与空间回收)
- [身份、授权与系统目录](#身份授权与系统目录)
- [权限变更与会话失效](#权限变更与会话失效)
- [扩展方向](#扩展方向)
本页面向需要理解或修改 ChuSQL 的开发者,说明组件职责、查询执行过程及存储边界。SQL 的使用方法见 [SQL 语法](syntax.md)。
ChuSQL 采用客户端 / 服务端架构。正常运行时,只有数据库服务直接访问数据文件;离线初始化与恢复由 bootstrap 完成。
阅读实现细节前,先了解本文使用的术语:
| 术语 | 含义 |
| --- | --- |
| schema(表结构) | 列名、类型和约束等定义;语义检查与优化以此为依据 |
| catalog(系统目录) | 记录数据库对象及其结构的元数据;判断表是否存在以此为准 |
| 绑定与槽位 | 绑定将列引用对应到输入行的位置,这个位置称为槽位 |
| 结构守卫 | 在执行前和存储操作锁内复查依赖,拒绝使用已经失效的表结构 |
| DOMAIN(领域类型) | 为基础类型命名,并可附加默认值和约束的自定义类型 |
| WAL / LSN | WAL 是恢复用的预写日志,LSN 是标识日志进度的递增序号 |
| DML / DDL | DML 包括插入、更新和删除数据;DDL 包括创建、修改和删除结构 |
## 组件与访问路径
```mermaid
flowchart LR
CLI[CLI / 其他客户端] -->|TCP| Server[chusql-server]
Browser[浏览器] -->|HTTP| Web[chusql-web]
Web -->|TCP| Server
Server --> Engine[Haskell SQL 引擎]
Engine --> Storage[Rust 存储层]
Storage --> Disk[(磁盘)]
```
| 组件 | 职责 |
| --- | --- |
| chusql-server | 登录、会话、鉴权、SQL 执行和事务 |
| chusql-web | Web 管理界面,通过 TCP 访问服务 |
| csql | 命令行客户端,通过 TCP 执行 SQL |
| chusql-core/engine | 解析、语义检查、优化及执行 |
| chusql-core/storage | catalog、页面、B+ 树、缓存、WAL 与持久化 |
| csql-bootstrap | 安装引导及离线系统目录修复、恢复、重置 |
服务默认监听 127.0.0.1:7777,Web 默认监听 127.0.0.1:7778。配置位置和平台数据目录见[配置选项](config.md)。
### 存储名称校验
存储层解析库限定名后,物理表名、声明列名和写入行顶层字段名使用同一规则:名称非空,只含 ASCII 字母、数字和下划线。SQL 标识符另遵守解析器规则。
单行、批量、整表替换、结构替换及事务载荷在处理请求时拒绝非法字段名,不修改数据和 WAL。校验不遍历文本内容或复合值内部的键。
旧数据不自动改名,原始扫描与历史 WAL 重放保留兼容;后续重写需显式提供合法字段名。
## 数据目录
`[storage] data_dir` 保存持久化数据,`log_files` 单独指定诊断日志目录。
| 位置 | 内容 |
| --- | --- |
| system/ | 身份、凭据、角色、授权和预定义类型目录 |
| databases/<数据库名>/ | 业务数据库的 catalog、数据与索引 |
| WAL 与检查点文件 | 已提交写入的恢复记录及刷盘进度 |
安装只初始化系统数据库,业务数据库由用户创建。表存在性以 catalog 为准,不能仅凭数据文件判断。
## 会话与并发
- 每条 TCP 连接拥有独立会话,当前数据库和事务互不影响。
- 服务端业务请求共享全局权限门,数据写语句和提交独占目标数据库的执行门,不同数据库可并行写;权限、DDL 和管理员存储请求独占全局门。库门按名称排序获取并在取消时释放,避免跨库等待环。存储层仍维护每库读写门及表锁。
- Web 与 CLI 共享服务端的权限和执行规则,不直接读写数据目录。
- Web 登录会话分别持有普通 API / SQL 连接与表格编辑连接,编辑事务按页面标识固定数据库。表格读取携带编辑标识时使用该事务快照;普通查询不进入编辑事务。
- 同一 Web Cookie 的请求串行执行。编辑提交以固定保存点重放本地编辑集,失败整体回退;成功回执按提交标识去重。SQL 控制台事务另按页面与标签标识限制连接访问。
## SQL 查询处理
查询依次经过解析、基于 schema 的语义检查、优化和执行。库名、表名、列名及别名在语义层解析;存储层使用精确的表名键。
### 解析后的查询表示
解析器将 SQL 转为抽象语法树(AST)。其中 `Select` 的字段与辅助函数分工如下:
| 名称 | 用途 |
| --- | --- |
| `selectItems :: [(String, Expr)]` | 保存输出名称与投影表达式,即查询要返回的列或计算结果 |
| `selectHaving` | 保存分组条件 |
| `OrderKey` | 区分按名称、输出序号和表达式排序 |
| `Col` | 表示普通列;别名、计算和聚合使用相同的投影项结构 |
| `makeSelect` | 接受列名列表并构造投影项 |
### 类型能力与复合值运算
运行时类型描述保存相等比较(Eq)、排序(Ord)和哈希(Hash)能力,复合类型按参数递归派生。语义检查按运算要求能力,DOMAIN 沿用基础类型。值排序和哈希按结构执行,不使用显示文本;嵌套浮点正负零保持相等与哈希一致。
值与类型编码保持版本 1,能力由代码中的类型描述提供,不写入已有值载荷。索引键编码是独立约束,运行时复合键不进入索引探测。SQL 行为见 [复合值的比较、排序与分组](types.md#比较排序与分组)。
### 绑定查询与结构复查
绑定查询保存依赖对象编号、schema 版本、列定义和直接及间接依赖的 DOMAIN。执行前复查,真实存储的每次读取还在相关库读锁内验证全部依赖;结构改变或对象替换明确报错。守卫不提供整条多请求查询的数据快照;旧目录的未知身份/版本保留兼容校验,旧存储未确认守卫则拒绝执行。
查询由 `Runtime.Query.bindQuery` 统一准备并绑定,输出按位置校验,保留重复列名。优化完成后将表达式、排序及分组键绑定到输入布局;物理列顺序不同会在存储边界整理,缺列或位置不符明确报错。
`ORDER BY` 输出序号保留到计划生成,选择对应投影表达式或 DISTINCT 输出槽位,不通过别名再次查找。非排序投影仍在 LIMIT 后求值。
### 聚合与排序的求值顺序
聚合计划合并投影、HAVING 和排序表达式中的当前层聚合,重复调用只计算一次。内部列使用 SQL 标识符无法声明的名称。
HAVING 强制形成分组,无分组键时将输入作为整体组。HAVING 过滤位于聚合算子 Aggregate 之后、去重与排序截断之前。
表达式排序先计算隐藏排序键并保留投影所需列,再排序、截断和投影。DISTINCT 排序映射到已有输出槽位。HAVING 和排序子查询同样参与结构依赖守卫与 SELECT 权限检查。
### 相关子查询的列查找
相关子查询会引用外层查询的列,因此列查找必须区分内外层作用域。
相关子查询按最近到最远的独立列作用域查找,最近层歧义不向外回退;二段限定引用只在最近声明该来源的层解析,裸列可继续向外查找。
`BoundSubquery` 保存父布局中的取值表达式,缓存按槽位和值区分;重绑定保留局部和外层身份,原位置失效时只允许唯一同名列重定位。
执行期外层行仍扁平化,尚无独立作用域或来源编号,结构改写须在绑定前完成。
### 写入求值与约束检查
写语句按目标 schema 绑定输入表达式和赋值目标,求值与校验遵循以下规则:
- INSERT 拒绝重复目标列,按输入顺序求值,再按 schema 顺序补默认值、NULL 和自增值。
- UPDATE 的重复赋值读取前一次修改结果。全部赋值后检查 CHECK,再检查最终行集的 NOT NULL、UNIQUE 和 PRIMARY KEY。
- 唯一性检查保留整数与整值浮点的一致语义,多个 NULL 不冲突。检查通过前不发布修改。
- CHECK、非空和唯一性按列槽位取值。INSERT 已有行及引擎 ALTER 输入行按原 schema 整理,缺列不跳过。
### 可复用的写计划
INSERT、UPDATE、DELETE 和已有表的结构变更由 `Engine.bindWrite` 生成不保存数据行的 `BoundWrite`,`executeWriteM` 在执行前复查结构,允许跨数据及统计变化复用。
DML 在绑定阶段固定 INSERT 输入槽位、UPDATE 目标槽位、DELETE 行布局和 CHECK / NOT NULL / UNIQUE / PRIMARY KEY 约束;复用时不重新解析或绑定这些内容,自增起点与行集约束仍取当前数据。
结构变更则在执行时对变更后的 schema 绑定并检查约束。
计划携带目标表、嵌套查询及传递 DOMAIN 依赖;ADD COLUMN / ALTER TYPE 还包含新声明类型的 DOMAIN。
建表、建删库、DOMAIN 管理和会话命令继续走各自入口,不属于已有表写计划。
### DOMAIN 约束的变更
DOMAIN 的 CHECK 在独立 VALUE 槽位绑定,继承默认值和非空限制按当前领域目录合成;表列保留原始声明。创建依赖 DOMAIN 的表或领域也通过结构守卫复查定义。
ALTER DOMAIN 的校验与提交按以下顺序执行:
1. 在存储库读锁内取得完整快照,校验直接及间接依赖和已有行。
2. 以库身份和 WAL 检查点作为令牌,在写锁内再次比较。
3. 令牌一致时,通过 `ReplaceSchema` 提交;期间任何普通提交都使令牌失效,并返回 `serialization_failure`。
约束变更复用现有目录版本、WAL 和恢复路径,不引入通用结构事务。
### 存储操作的结构守卫
写入前先发送只读的 `validate_write` 请求,确认存储支持写守卫并校验依赖;未确认时不发送实际修改请求。
实际读取在相关库共享门内复查,实际写入在目标库独占门及其他依赖库共享门内复查,跨库按库名排序取门。
对象替换或结构版本变化使旧绑定失效,包括结构改变后恢复原定义。
守卫保证结构校验与单次存储操作之间不发生 DDL,不提供整条写语句的数据快照或数据冲突检测。
### 修改表结构时的约束校验
CHECK 定义和执行共用类型、函数及列槽位绑定入口,拒绝聚合、子查询和非布尔条件。修改表结构时还需遵守以下规则:
- DROP COLUMN 校验保留下来的 CHECK。
- RENAME COLUMN 不自动改写约束文本,引用旧名称的 CHECK 会使改名被拒绝。
- ALTER TYPE 依据新 schema 校验 CHECK、默认值和键能力,空表也不跳过定义校验。通过定义校验后仍检查已有行,再发布结构变更。
### 查询优化的能力与边界
查询优化支持以下能力,统计信息只影响计划选择,优化不能改变查询结果语义:
| 能力 | 实现与边界 |
| --- | --- |
| 派生表 | 内层独立解析,必须有别名,不能引用外层列;输出列按别名暴露 |
| WITH / CTE | 解析时展开为派生表;相同执行子树在查询内物化复用,不支持递归 |
| ANY / ALL | 按 OR / AND 的三值逻辑求值;空集的 ANY 为假、ALL 为真 |
| 子查询复用 | 按查询定义及外层绑定值物化结果,跨过滤、投影、排序与 JOIN 复用 |
| 半连接 / 反连接 | 独立 IN / NOT IN 与简单等值相关 EXISTS / NOT EXISTS 转为哈希存在性匹配,只输出左行 |
| 成本估算 | 使用行数、不同值数和数值列的 16 桶等宽直方图 |
| 索引选择 | 优化器判断适用条件与成本,存储层管理实际索引 |
#### 连接重排与投影下推
派生表可根据 schema 和透明算子估计行数并参与安全的连接重排;缺统计或可能报错时保守处理。输出列投影可下推,但保留所有计算项的求值,避免隐藏错误。内存存储提供实时行数和封顶去重统计。
#### 子查询结果的复用
执行器用查询内状态保存首次使用时成功求出的子查询行;键包含已解析查询定义与具名外层绑定值,跨不同父布局复用,原始名称调用保守包含整个外层行。
派生表按执行子树和外层行缓存,应用来源别名前复用,优化后子树不同则分别执行。标量、IN、EXISTS 和 ANY / ALL 各自处理共享行集,保留基数错误和三值逻辑。
缓存在每次执行时清空,不跨会话或数据修改保留;既有表达式分组求值顺序保持,JOIN 空行对仍不触发子查询。
尚无磁盘溢出或物化内存预算,也不提供整条查询的数据快照。
#### 用存在性匹配加速子查询
半连接与反连接根据右侧是否存在匹配行来筛选左行,只输出左侧列。`SemiJoin` 分别绑定两侧键,只暴露左侧布局;执行左侧为空时不求右侧,其他情况完整求右侧并构建键集合,按原左行顺序过滤,右侧重复键不会放大结果。
独立 IN 保存原子查询身份,继续共享其他消费者的物化结果;NOT IN 跟踪右侧 NULL,NOT EXISTS 的空值键则视作不匹配。
大整数与浮点混合比较超出安全哈希范围时退回既有逐值比较。
重写只覆盖单独过滤谓词:IN 左键限普通列或简单字面量;相关 EXISTS 限单表、唯一外层等值引用和普通列/简单字面量投影。
组合谓词、复杂相关条件、聚合、排序截断及可能报错的相关投影继续走原路径,避免改变错误时序和词法作用域。
完整 SQL 写法及限制见 [SQL 语法](syntax.md)。
## 快照事务与行锁
MVCC 是多版本并发控制:每条逻辑行有不复用的内部编号和版本链,事务读取开始时可见的版本及自身修改。库内提交序号使用 WAL 提交 LSN;`mvcc.json` 保存版本目录,在堆与索引同步后原子发布,再推进库检查点。旧目录首次写入或开始事务时导入版本,原页格式保留。
| 阶段 | 行为 |
| --- | --- |
| BEGIN | 注册存储事务和快照引用,向引擎返回一致的结构与行集 |
| 事务内执行 | 基于开始快照执行,修改行和唯一键在语句成功前取得锁 |
| SAVEPOINT | 保存工作行集和读依赖;回滚同步存储计划,已有锁保留到事务结束 |
| COMMIT | 在短提交门内合并最新版本、校验权限、对象、读写依赖及最终约束,再经 WAL 发布 |
| ROLLBACK / 断线 | 清除计划、快照引用、锁和等待边;保存点不能跨连接恢复 |
行锁覆盖内部行编号及旧、新唯一值,NULL 不争用唯一值锁。同一库中不同行可以同时准备修改,等待行锁不占库提交门。等待图检测环并中止形成环的等待者;总等待上限为 5 秒,分别返回 `deadlock_detected` 或 `lock_timeout`。版本冲突返回 `serialization_failure`。调用方须 ROLLBACK,再从 BEGIN 重做,不能只重发 COMMIT。保存点回滚不释放此前取得的锁。
默认 BEGIN 提供快照隔离,复查写入行的内容版本。SERIALIZABLE 写事务另外按表复查所有读依赖,包括子查询及幻行,因此不同记录的变化也可能导致保守冲突。只读事务可完成原快照读取。事务内禁止 DDL 和切换数据库。
自动提交的查询与数据写语句也使用存储事务。权限和 DDL 使用全局独占门;数据准备使用共享门,同库提交校验和 WAL 发布仍按顺序完成,以保留连续的已应用提交前缀。原始管理员存储写入不取得 SQL 事务行锁,但会发布版本,旧事务随后按版本冲突规则拒绝提交。
SQL 工作行集仍在 Haskell 中物化;版本目录发布也会读取整库并写出版本文件。行版本支持隔离与回收,不表示已有按需取行、增量版本日志或吞吐量保证。结束事务释放内存中的旧版本,后续发布写回回收结果;旧版本及整库快照会增加内存、磁盘和 I/O 成本。
### 在线空间整理
显式 `compact` 将快照中的存活行复制到独立代次的影子堆与索引,复制期间继续接受 SQL 数据写入。切换前短暂独占库门,保留最新行集的有序前缀,删除影子中的过期行并按最新顺序追加其余行;并发变化较多时,切换窗口可能重写较长后缀。
影子文件同步完成后,以 WAL 的 Migrate 操作发布 catalog 中的 `file_generation`,逻辑对象编号和结构版本不变。旧快照结束后删除退役文件;重启先恢复切换日志,再清理未发布与退役代次。启动时隐藏列整理仍使用离线重写。备份须包含整个库目录及页侧文件,不能只复制 catalog 指向的新堆。
## WAL 与崩溃恢复
预写日志记录已提交的修改,检查点记录已完成刷盘的位置。写入和恢复遵循以下顺序:
1. 一组写操作与提交标记写入 WAL,每条记录带递增 LSN。
2. 数据页和索引页覆盖前,完整页镜像与校验和先同步到对应 `.page-wal` 文件。
3. 数据文件同步后,原子替换 `.pages` 中的页 LSN 和写序号,再清理镜像日志。
4. 数据、页版本和 catalog 刷盘后,同步并原子发布行版本目录,再将库检查点推进到已完成提交的 LSN。
5. 按检查点截断日志前缀,保留尚未确认刷盘的记录。
6. 重启时先按有效全页镜像修复撕裂写,再重放库检查点之后的已提交组,丢弃未提交组,并进行幂等检查。
页镜像有独立递增写序号,允许同一事务多次刷新同一页;不能仅凭相同页 LSN 跳过后一次写入。未完整写出的尾帧丢弃,完整帧校验失败则明确报错并保留材料。镜像写入或检查点失败后,当前页句柄拒绝继续刷新,析构也不能绕过恢复。
已提交组恢复前丢弃受影响表的派生索引,再从堆数据重建,避免残缺索引根阻断逻辑重放。整表日志直接重写,不先依赖可能不完整的行集判断是否已应用。
建表、删表、建索引、删索引、账号和数据修改均进入带提交点的 WAL。LSN 跨重启续号,SystemStatus 的 last_lsn 表示检查点 LSN。
若系统目录损坏导致服务无法启动,使用离线 bootstrap。repair、recover 与 reset 的范围及操作步骤见[系统引导与恢复](bootstrap.md)。
## 页面与空间回收
- 删除行先将槽位标记为墓碑。
- 死空间达到页大小的四分之一时,随删除整理页内活行。
- 整理保留槽位编号,索引中的页号与槽号仍有效。
- 显式 compact 在线复制存活行、补齐并发修改并经 WAL 切换文件代次,回收跨页空洞及隐藏列字节,同时重建索引位置。
跨页整理的复制阶段允许并发数据写入,最终追平与切换仍独占库门。旧快照引用阻止退役文件回收。整理保留切换时的逻辑行序;应用应使用 ORDER BY 获得确定结果。
## 身份、授权与系统目录
系统表统一使用 __system_ 前缀,身份与凭据目录对普通语句隐藏。SUPERUSER 拥有最高权限,system_catalog_manager 提供受限目录管理能力;两者不通过角色成员关系继承。
| 机制 | 行为 |
| --- | --- |
| 角色继承 | 沿成员关系取得权限并集;拒绝自继承与环 |
| 对象授权 | 数据库 CONNECT / CREATE,表 SELECT / INSERT / UPDATE / DELETE;ALL 按作用域展开 |
| 转授权 | WITH GRANT OPTION 允许将持有的权限授予其他身份 |
| 撤销 | 授予者撤销自身来源;所有者或最高权限身份管理对象授权,级联清理无合法来源的下游 |
| 稳定对象 | catalog 分配数据库及表编号,授权与所有者绑定对象和身份编号,删除后不复用 |
| 所有者 | 建表者获得表所有权;启用角色可继承所有权,数据库所有权不隐含他人表的数据权限 |
对象权限目录 __system_object_acl 对普通语句和存储请求隐藏。
权限快照按字符分块,在系统库写锁内比较版本并通过同一 WAL 组替换;服务启动恢复建表前持久化的创建者声明,旧名称授权只迁移一次到现存稳定对象。
类型目录说明见[类型与类型目录](types.md),身份字段与权限边界见[用户、角色与属性](users-and-roles.md)。
## 权限变更与会话失效
账号、属性和授权变化会标记受影响的在线连接。角色变化先展开成员及转授权来源关系,确定涉及的登录身份。共享读写门允许查询并行,并将数据写入、提交、授权及对象结构变更与查询排序;事务提交再次验证权限和稳定对象编号。
TCP 连接在下一次请求时返回 unauthorized 并关闭;空闲连接不被强行中断。权限逐条查询,避免继续使用旧授权。Web 会话还通过账号版本校验,口令或身份变更使旧凭据失效。
## 扩展方向
> [!NOTE]
> 以下是规划,不能作为当前 SQL、事务或恢复能力使用。进度与验收见 `.agents/context/progress.txt`。
### 查询内部表示的后续完善
已有列槽位、类型化函数与 `BoundQuery`;后续补齐多级词法作用域、来源/列身份,以及写语句与约束的统一绑定。迁移保留同名遮蔽、NULL / DOMAIN、输出布局和错误时序,结构改写应在绑定前完成或重新绑定。
### 查询扩展
| 方向 | 前置条件与边界 |
| --- | --- |
| 物化资源控制 | 在查询内物化基础上增加内存预算、磁盘溢出及取消;与完整查询快照协调 |
| 递归 CTE | 先实现类型一致的迭代关系,明确去重、循环终止、轮次与行数预算;不隐含支持写入型 CTE |
### 存储扩展
行版本、行锁、同库数据准备与在线整理的边界见上文。后续优化须根据测量减少整库物化、版本文件写放大和最终切换窗口;WAL 发布顺序、版本可见性与恢复约束保持一致。