7.5 KiB
架构与组件
ChuSQL 采用客户端 / 服务端架构。数据库文件仅由 chusql-server 直接访问,其他组件通过网络接口与数据库服务通信。
主要组件
| 组件 | 作用 |
|---|---|
chusql-server |
数据库服务,负责 SQL 执行、权限检查、事务与数据访问 |
chusql-web |
Web 管理端,通过数据库服务访问数据 |
csql |
命令行客户端,通过数据库服务执行 SQL |
chusql-core/engine |
SQL 解析、语义检查、优化与执行 |
chusql-core/storage |
页面、索引、日志与持久化存储 |
数据流
典型访问路径为:
Web / CLI / 其他客户端
↓
TCP
↓
chusql-server
↓
SQL 引擎与存储层
↓
磁盘
所有 SQL 和权限检查均由数据库服务统一处理。Web 和 CLI 不直接访问数据目录,因此多个客户端可以同时连接同一数据库服务。
数据目录
数据保存在 [storage] data_dir 指定的位置,主要包含:
| 目录 | 内容 |
|---|---|
system/ |
系统数据库,包括账号和权限等系统信息 |
databases/<数据库名>/ |
用户创建的数据库 |
安装完成后只有系统数据库,不自动创建业务数据库。业务数据库需通过 CREATE DATABASE 创建。
数据库服务
chusql-server 默认监听 127.0.0.1:7777,客户端建立连接后完成登录、数据库选择和 SQL 执行。
每条连接拥有独立会话状态,例如当前数据库选择不会影响其他连接。不同连接可以同时执行语句:读请求之间可以并行,写请求之间由存储层排队。
语句里的库前缀和表限定符由引擎在语义层解析:存储层只接受精确的表名键,因此 FROM sales.orders 与选定 sales 后的 FROM orders 指向同一张表。FROM 的来源是一棵树:单表、派生表(括号里的子查询,必须带别名)与连接共用同一个 FromClause,所以 JOIN 两侧都能是派生表。解析派生表时先把内层语句在空作用域里解析成一张“表”——内层不能引用外层列,输出列统一改成裸列名(内层 u.id 在外层用 d.id 引用),重名直接报错;执行侧对应一个 Derived 算子,把内层计划的结果按别名加前缀。派生表不参与连接顺序重排(叶子的行数未知),投影也不下推进去,但内层计划照常做谓词下推与索引改写。投影项的 AS 别名与 SELECT 的输出列名一起在语义层解析,派生表与最外层查询都按别名暴露列名。
WITH 不新增语法树节点:解析器在读到一个 WITH 时就把每个 CTE 的名字与语句记进一张环境表,遇到 FROM 里出现的 CTE 名(不分大小写,只认前面已定义的)就换成 M11 的派生表节点,主语句与 CTE 内部都能再写 WITH。这样展开出来的语句对语义层、执行器与鉴权完全等价于手写派生表:CTE 用列清单改名走派生表的投影别名,来源树与权限覆盖不必另做一套。代价是 CTE 不做物化(引用两次就执行两次)、不支持递归,且前向引用会当成普通表名去查。
量词比较走另一个真节点 QuantCmp:解析时普通比较运算符先读最长符号,发现后面是 ANY / ALL 就让给量词解析,因此 age > ANY (SELECT …) 与 age > 25 共用同一层优先级。求值时先把子查询的行取成字面量,再摊成 AND(ALL)/ OR(ANY)链交给既有三值逻辑,于是空子查询 ANY 为假、ALL 为真,子查询里的 NULL 按三值规则传播。节点不带取反位(本方言没有前缀 NOT),取反写相反的运算符,如 <> ALL 与 NOT IN 同义。
优化器的成本估算用存储维护的统计:行数、列级不同值数,以及数值列的列级等宽直方图。直方图是 16 个桶加区间下界 / 上界,只统计 int 与 float 列(NULL 与非数值不进直方图);存储侧随插入累计、随删除递减,值跑出区间就把旧桶按中点并进新区间,删到表空时清空。这些统计随 schema 下沉到引擎,Cost.rangeRows 有直方图就按桶与区间的交集估命中行数(端点按闭区间算),没有直方图退回原来的四分之一;Cost.selectivityOf 把这个比例交给连接顺序估算与索引改写的成本把关,因此一条覆盖大半张表的范围条件不会再被改写成索引点查。在线协议里每条列统计多出 lo / hi / hist 三个可选字段,缺省即「没有直方图」,老客户端与老字典都能照常读。
显式事务在会话层实现:BEGIN 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;SAVEPOINT 把当前快照压进会话里的保存点栈,ROLLBACK TO 退回该点并保留它,RELEASE 丢掉该点与其后的保存点;COMMIT 先把快照、基线与库里现值按行 id 逐键比对,检测到写写冲突就报 serialization_failure 并保留事务;没有冲突才把差分用一条 apply_transaction 请求交给存储层应用;ROLLBACK 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。
WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 wal.checkpoint 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,SystemStatus 应答里的 last_lsn 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。
系统数据库及部分管理操作仅允许管理员访问。系统表统一用 __system_ 前缀:账号表 __system_users 对语句通道隐藏,角色、授权、带 option 的授权、成员四张表(__system_roles / __system_grants / __system_grant_options / __system_members)只在 system 库里可见,权限服务自身也通过 SQL 读写它们;存储启动时把旧名 sys_roles / sys_grants / sys_members 连同数据与索引文件一起迁移过来。角色支持互相继承:GRANT 角色 A TO 角色 B 记一条 B 属于 A 的成员关系,判定权限时沿成员关系算出向上闭包再取并集;写入时拒绝成环与自继承,删角色会清掉它两个方向的成员关系。带 WITH GRANT OPTION 的授权记在第四张表 __system_grant_options 里(同样的角色、权限、对象三列,另开一张表省掉给 __system_grants 加列带来的系统表结构迁移):普通身份只要手里那条授权带 grant option 就能把同一项权限转授给别的角色,REVOKE 仍然只允许管理员,收回授权时连它的 grant option 一起收掉,但不级联收回别人已经转授出去的授权。授权命令由权限服务在会话层之外直接执行,GRANT 命令会先按当前库把对象补全成 库.表 再落盘,因此判断转授权资格与实际写入用的是同一个对象名。
服务监听地址、端口和请求限制可在 chusql.toml 的 [server] 分区中配置。