22 KiB
架构与组件
本页内容:
本页面向需要理解或修改 ChuSQL 的开发者,说明组件职责、查询执行过程及存储边界。SQL 的使用方法见 SQL 语法。
ChuSQL 采用客户端 / 服务端架构。正常运行时,只有数据库服务直接访问数据文件;离线初始化与恢复由 bootstrap 完成。
阅读实现细节前,先了解本文使用的术语:
| 术语 | 含义 |
|---|---|
| schema(表结构) | 列名、类型和约束等定义;语义检查与优化以此为依据 |
| catalog(系统目录) | 记录数据库对象及其结构的元数据;判断表是否存在以此为准 |
| 绑定与槽位 | 绑定将列引用对应到输入行的位置,这个位置称为槽位 |
| 结构守卫 | 在执行前和存储操作锁内复查依赖,拒绝使用已经失效的表结构 |
| DOMAIN(领域类型) | 为基础类型命名,并可附加默认值和约束的自定义类型 |
| WAL / LSN | WAL 是恢复用的预写日志,LSN 是标识日志进度的递增序号 |
| DML / DDL | DML 包括插入、更新和删除数据;DDL 包括创建、修改和删除结构 |
组件与访问路径
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。配置位置和平台数据目录见配置选项。
存储名称校验
存储层解析库限定名后,物理表名、声明列名和写入行顶层字段名使用同一规则:名称非空,只含 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 行为见 复合值的比较、排序与分组。
绑定查询与结构复查
绑定查询保存依赖对象编号、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 的校验与提交按以下顺序执行:
- 在存储库读锁内取得完整快照,校验直接及间接依赖和已有行。
- 以库身份和 WAL 检查点作为令牌,在写锁内再次比较。
- 令牌一致时,通过
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 语法。
快照事务与行锁
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 与崩溃恢复
预写日志记录已提交的修改,检查点记录已完成刷盘的位置。写入和恢复遵循以下顺序:
- 一组写操作与提交标记写入 WAL,每条记录带递增 LSN。
- 数据页和索引页覆盖前,完整页镜像与校验和先同步到对应
.page-wal文件。 - 数据文件同步后,原子替换
.pages中的页 LSN 和写序号,再清理镜像日志。 - 数据、页版本和 catalog 刷盘后,同步并原子发布行版本目录,再将库检查点推进到已完成提交的 LSN。
- 按检查点截断日志前缀,保留尚未确认刷盘的记录。
- 重启时先按有效全页镜像修复撕裂写,再重放库检查点之后的已提交组,丢弃未提交组,并进行幂等检查。
页镜像有独立递增写序号,允许同一事务多次刷新同一页;不能仅凭相同页 LSN 跳过后一次写入。未完整写出的尾帧丢弃,完整帧校验失败则明确报错并保留材料。镜像写入或检查点失败后,当前页句柄拒绝继续刷新,析构也不能绕过恢复。
已提交组恢复前丢弃受影响表的派生索引,再从堆数据重建,避免残缺索引根阻断逻辑重放。整表日志直接重写,不先依赖可能不完整的行集判断是否已应用。
建表、删表、建索引、删索引、账号和数据修改均进入带提交点的 WAL。LSN 跨重启续号,SystemStatus 的 last_lsn 表示检查点 LSN。
若系统目录损坏导致服务无法启动,使用离线 bootstrap。repair、recover 与 reset 的范围及操作步骤见系统引导与恢复。
页面与空间回收
- 删除行先将槽位标记为墓碑。
- 死空间达到页大小的四分之一时,随删除整理页内活行。
- 整理保留槽位编号,索引中的页号与槽号仍有效。
- 显式 compact 在线复制存活行、补齐并发修改并经 WAL 切换文件代次,回收跨页空洞及隐藏列字节,同时重建索引位置。
跨页整理的复制阶段允许并发数据写入,最终追平与切换仍独占库门。旧快照引用阻止退役文件回收。整理保留切换时的逻辑行序;应用应使用 ORDER BY 获得确定结果。
身份、授权与系统目录
系统表统一使用 _system 前缀,身份与凭据目录对普通语句隐藏。SUPERUSER 拥有最高权限,system_catalog_manager 提供受限目录管理能力;两者不通过角色成员关系继承。
| 机制 | 行为 |
|---|---|
| 角色继承 | 沿成员关系取得权限并集;拒绝自继承与环 |
| 对象授权 | 数据库 CONNECT / CREATE,表 SELECT / INSERT / UPDATE / DELETE;ALL 按作用域展开 |
| 转授权 | WITH GRANT OPTION 允许将持有的权限授予其他身份 |
| 撤销 | 授予者撤销自身来源;所有者或最高权限身份管理对象授权,级联清理无合法来源的下游 |
| 稳定对象 | catalog 分配数据库及表编号,授权与所有者绑定对象和身份编号,删除后不复用 |
| 所有者 | 建表者获得表所有权;启用角色可继承所有权,数据库所有权不隐含他人表的数据权限 |
对象权限目录 __system_object_acl 对普通语句和存储请求隐藏。
权限快照按字符分块,在系统库写锁内比较版本并通过同一 WAL 组替换;服务启动恢复建表前持久化的创建者声明,旧名称授权只迁移一次到现存稳定对象。
类型目录说明见类型与类型目录,身份字段与权限边界见用户、角色与属性。
权限变更与会话失效
账号、属性和授权变化会标记受影响的在线连接。角色变化先展开成员及转授权来源关系,确定涉及的登录身份。共享读写门允许查询并行,并将数据写入、提交、授权及对象结构变更与查询排序;事务提交再次验证权限和稳定对象编号。
TCP 连接在下一次请求时返回 unauthorized 并关闭;空闲连接不被强行中断。权限逐条查询,避免继续使用旧授权。Web 会话还通过账号版本校验,口令或身份变更使旧凭据失效。
扩展方向
Note
以下是规划,不能作为当前 SQL、事务或恢复能力使用。进度与验收见
.agents/context/progress.txt。
查询内部表示的后续完善
已有列槽位、类型化函数与 BoundQuery;后续补齐多级词法作用域、来源/列身份,以及写语句与约束的统一绑定。迁移保留同名遮蔽、NULL / DOMAIN、输出布局和错误时序,结构改写应在绑定前完成或重新绑定。
查询扩展
| 方向 | 前置条件与边界 |
|---|---|
| 物化资源控制 | 在查询内物化基础上增加内存预算、磁盘溢出及取消;与完整查询快照协调 |
| 递归 CTE | 先实现类型一致的迭代关系,明确去重、循环终止、轮次与行数预算;不隐含支持写入型 CTE |
存储扩展
行版本、行锁、同库数据准备与在线整理的边界见上文。后续优化须根据测量减少整库物化、版本文件写放大和最终切换窗口;WAL 发布顺序、版本可见性与恢复约束保持一致。