|
1 | 1 | # 创建表(`CREATE TABLE`)
|
2 | 2 |
|
3 |
| -在实现了 Catalog 之后,我们就可以创建第一个数据表。 |
| 3 | +在实现了 Catalog 之后,我们就可以使用 `CREATE TABLE` 语句创建数据表: |
4 | 4 |
|
5 |
| -在 SQL 语言中,创建表用 `CREATE TABLE` 语句实现。这里就需要引入 Binder 的概念。…… |
| 5 | +```sql |
| 6 | +CREATE TABLE student ( |
| 7 | + id INTEGER PRIMARY KEY, |
| 8 | + name VARCHAR NOT NULL, |
| 9 | + age INTEGER |
| 10 | +); |
| 11 | +``` |
| 12 | + |
| 13 | +这一语句除了解析和执行两个步骤以外,还需要做名称的检查,并将它们与数据库内部对象绑定起来。 |
| 14 | +这些工作一般是由一个叫做 Binder 的模块完成。 |
| 15 | + |
| 16 | +在此任务中,我们将实现一个基础的 Binder,同时拓展 Executor 实现相应的执行逻辑,最终支持 `CREATE TABLE` 语句。 |
6 | 17 |
|
7 | 18 | <!-- toc -->
|
8 | 19 |
|
9 | 20 | ## 背景知识
|
10 | 21 |
|
11 | 22 | ### Binder
|
12 | 23 |
|
13 |
| -TODO |
| 24 | +Binder 是整个数据库系统中一个不太起眼但又十分重要的模块。 |
| 25 | + |
| 26 | +它的作用是将解析后生成的 AST 和 Schema 信息绑定起来,具体包括: |
| 27 | + |
| 28 | +- 检查输入的名称是否合法、是否有重复、有歧义 |
| 29 | +- 推断表达式的返回值类型,并检查是否合法 |
| 30 | +- 将输入的名称转换成内部 ID |
| 31 | + |
| 32 | +比如,对于一个简单的创建表的命令: |
| 33 | + |
| 34 | +```sql |
| 35 | +CREATE TABLE student ( |
| 36 | + id INTEGER PRIMARY KEY, |
| 37 | + name VARCHAR NOT NULL, |
| 38 | + age INTEGER |
| 39 | +); |
| 40 | +``` |
| 41 | + |
| 42 | +Binder 会依次完成以下工作: |
| 43 | + |
| 44 | +1. 由于 SQL 对大小写不敏感,Binder 会首先将所有名称统一成小写。 |
| 45 | +2. 对于表名 `student`,自动补全省略的 schema 名。假如当前 schema 是 `school`,那么会将其补全成 `school.student`。 |
| 46 | +3. 检查 schema 是否存在,并获取它的 Schema ID。 |
| 47 | +4. 检查 schema 中是否已经存在名为 `student` 的表。 |
| 48 | +5. 检查列名 `id` `name` `age` 是否合法、是否有重复。 |
| 49 | +6. 检查列的属性是否合法,例如不能出现两个 `PRIMARY KEY`。 |
| 50 | +7. 向 AST 中填入绑定后的信息:Schema ID。 |
| 51 | + |
| 52 | +对于插入数据的 `INSERT` 语句,例如: |
| 53 | + |
| 54 | +```sql |
| 55 | +INSERT INTO student VALUES (1, 'Alice', 18) |
| 56 | +``` |
| 57 | + |
| 58 | +Binder 需要查询表中每一列的信息,推断表达式的类型,并检查它们是否相符。 |
| 59 | +换言之,Binder 应该能够识别出以下不合法的插入语句: |
| 60 | + |
| 61 | +```sql |
| 62 | +INSERT INTO student VALUES (1) -- 存在未指定的 NOT NULL 值 |
| 63 | +INSERT INTO student VALUES (1, 'Alice', 'old') -- 类型不匹配 |
| 64 | +INSERT INTO student VALUES (1, 'Alice', 18+'g') -- 表达式类型不匹配 |
| 65 | +``` |
| 66 | + |
| 67 | +对于更复杂的嵌套查询语句,Binder 还需要根据当前语境,推断出每个名称具体指代哪个对象: |
| 68 | + |
| 69 | +```sql |
| 70 | +SELECT name FROM student WHERE sid IN ( |
| 71 | +-- ^-----------^ student.sid |
| 72 | + SELECT sid FROM enrolled WHERE class = 'database' |
| 73 | +-- ^----------^ enrolled.sid |
| 74 | +) |
| 75 | +``` |
| 76 | + |
| 77 | +可以看出,Binder 干的都是一些比较繁琐的脏活累活。因此后面我们写的 Binder 代码也会比较冗长并且细节琐碎。 |
14 | 78 |
|
15 | 79 | ## 任务目标
|
16 | 80 |
|
17 |
| -能够创建数据表,支持以下 SQL: |
| 81 | +能够创建数据表,支持以下 SQL 语句: |
18 | 82 |
|
19 | 83 | ```sql
|
20 |
| -CREATE TABLE t (a INT) |
| 84 | +CREATE TABLE student ( |
| 85 | + id INTEGER PRIMARY KEY, |
| 86 | + name VARCHAR NOT NULL, |
| 87 | + age INTEGER |
| 88 | +); |
| 89 | +``` |
| 90 | + |
| 91 | +【练习】支持 `DROP TABLE` 语句,删除数据表: |
| 92 | + |
| 93 | +```sql |
| 94 | +DROP TABLE student; |
| 95 | +``` |
| 96 | + |
| 97 | +【练习】支持 `CREATE SCHEMA` 语句,创建 schema: |
| 98 | + |
| 99 | +```sql |
| 100 | +CREATE SCHEMA school; |
21 | 101 | ```
|
22 | 102 |
|
23 | 103 | ## 整体设计
|
24 | 104 |
|
25 |
| -TODO |
| 105 | +在加入 Binder 之后,RisingLight 的整个数据处理流程扩展成了这个样子: |
| 106 | + |
| 107 | + |
| 108 | + |
| 109 | +其中 Binder 插在了 Parser 和 Executor 之间。 |
| 110 | +它会将 Parser 生成的 AST 进行处理后,生成一个新的 AST 交给 Executor,在此过程中需要从 Catalog 读取 Schema 信息。 |
| 111 | +Executor 拿到绑定后的 AST 去执行,在此过程中可能也会再次修改 Catalog(比如创建一个表)。 |
| 112 | + |
| 113 | +在代码结构上,我们可能会新增以下文件: |
| 114 | + |
| 115 | +``` |
| 116 | +src |
| 117 | +├── binder |
| 118 | +│ ├── mod.rs |
| 119 | +│ └── statement |
| 120 | +│ ├── mod.rs |
| 121 | +│ ├── create.rs |
| 122 | +│ └── select.rs |
| 123 | +├── executor |
| 124 | +│ ├── mod.rs |
| 125 | +│ ├── create.rs |
| 126 | +│ └── select.rs |
| 127 | +... |
| 128 | +``` |
| 129 | + |
| 130 | +此外还需要对数据库顶层结构进行修改。 |
| 131 | + |
| 132 | +### Bound AST |
| 133 | + |
| 134 | +Binder 模块的主要任务是给 Parser 生成的 AST 绑定必要的信息。 |
| 135 | + |
| 136 | +由于我们的 Parser 使用了第三方库,不能在它的 AST 结构上扩展新的属性,所以只能定义新的结构来存放这些信息。 |
| 137 | + |
| 138 | +例如对于 `CREATE TABLE` 语句来说,绑定后的 AST 应该具有以下信息: |
| 139 | + |
| 140 | +```rust,no_run |
| 141 | +// binder/statement/create.rs |
| 142 | +
|
| 143 | +/// A bound `CREATE TABLE` statement. |
| 144 | +#[derive(Debug, PartialEq, Clone)] |
| 145 | +pub struct BoundCreateTable { |
| 146 | + pub schema_id: SchemaId, // schema name 经过向 catalog 查询转换成了 ID |
| 147 | + pub table_name: String, |
| 148 | + pub columns: Vec<(String, ColumnDesc)>, |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +类似地,对于 1.1 中的 `SELECT 1` 语句而言,我们可以只提取出必要的值来保存: |
| 153 | + |
| 154 | +```rust,no_run |
| 155 | +// binder/statement/select.rs |
| 156 | +
|
| 157 | +use crate::parser::Value; |
| 158 | +
|
| 159 | +/// A bound `SELECT` statement. |
| 160 | +#[derive(Debug, PartialEq, Clone)] |
| 161 | +pub struct BoundSelect { |
| 162 | + pub values: Vec<Value>, |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +最后,我们需要定义一个 enum 将各种不同类型的语句聚合起来: |
| 167 | + |
| 168 | +```rust,no_run |
| 169 | +// binder/mod.rs |
| 170 | +
|
| 171 | +/// A bound SQL statement. |
| 172 | +#[derive(Debug, PartialEq, Clone)] |
| 173 | +pub enum BoundStatement { |
| 174 | + CreateTable(BoundCreateTable), |
| 175 | + Select(BoundSelect), |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +这样,一个 `BoundStatement` 变量就可以表示 Binder 生成的整个 AST 了。 |
| 180 | + |
| 181 | +### Binder |
| 182 | + |
| 183 | +接下来,我们实现真正的 `Binder` 对象。它会将 Parser 生成的 AST 转换成一个新的 AST。 |
| 184 | +由于在绑定过程中会访问 Catalog 的数据,`Binder` 中需要存放一个 Catalog 对象的指针: |
| 185 | + |
| 186 | +```rust,no_run |
| 187 | +pub struct Binder { |
| 188 | + catalog: Arc<Catalog>, |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +我们在 `Binder` 对象上实现各种 `bind` 方法来完成对不同 AST 节点的处理: |
| 193 | + |
| 194 | +```rust,no_run |
| 195 | +use crate::parser::{Query, Statement}; |
| 196 | +
|
| 197 | +impl Binder { |
| 198 | + pub fn bind(&mut self, stmt: &Statement) -> Result<BoundStatement, BindError> { |
| 199 | + use Statement::*; |
| 200 | + match stmt { |
| 201 | + CreateTable { .. } => Ok(BoundStatement::CreateTable(self.bind_create_table(stmt)?)), |
| 202 | + Query(query) => Ok(BoundStatement::Select(self.bind_select(query)?)), |
| 203 | + _ => todo!("bind statement: {:#?}", stmt), |
| 204 | + } |
| 205 | + } |
| 206 | +
|
| 207 | + fn bind_create_table(&mut self, stmt: &Statement) -> Result<BoundCreateTable, BindError> { |
| 208 | + // YOUR CODE HERE |
| 209 | + } |
| 210 | +
|
| 211 | + fn bind_select(&mut self, query: &Query) -> Result<BoundSelect, BindError> { |
| 212 | + // YOUR CODE HERE |
| 213 | + } |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +注意到这些方法都使用了 `&mut self` 签名,这是因为 `Binder` 未来会有内部状态,并且在 bind 过程中还会修改这些状态。 |
| 218 | +<!-- 例如对于 `SELECT id FROM student` 语句来说,需要首先扫描 FROM 子句记录下表名 `student`,然后访问 `id` 时才能知道它是否是某个表的列。 --> |
| 219 | + |
| 220 | +另外在 bind 过程中还可能产生各种各样的错误,比如名称不存在或者重复等等。 |
| 221 | +我们将所有可能发生的错误定义在一个 `BindError` 错误类型中(参考 [1.1 错误处理](../01-01-hello-sql.md#错误处理)): |
| 222 | + |
| 223 | +```rust,no_run |
| 224 | +/// The error type of bind operations. |
| 225 | +#[derive(thiserror::Error, Debug, PartialEq)] |
| 226 | +pub enum BindError { |
| 227 | + #[error("schema not found: {0}")] |
| 228 | + SchemaNotFound(String), |
| 229 | + // ... |
| 230 | +} |
| 231 | +``` |
| 232 | + |
| 233 | +至于具体的 bind 逻辑,大家可以参考背景知识中描述的过程尝试自己实现。 |
| 234 | + |
| 235 | +### Executor |
| 236 | + |
| 237 | +在 1.1 中我们实现过一个最简单的执行器,它只是一个函数,拿到 AST 后做具体的执行。 |
| 238 | +现在我们有了更多类型的语句,并且在执行它们的过程中还需要访问 Catalog。 |
| 239 | +因此和 Binder 类似,我们现在需要将 Executor 也扩展为一个对象: |
| 240 | + |
| 241 | +```rust,no_run |
| 242 | +pub struct Executor { |
| 243 | + catalog: Arc<Catalog>, |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +然后在 `Executor` 上实现各种 `execute` 方法来对不同类型的 AST 节点做执行: |
| 248 | + |
| 249 | +```rust,no_run |
| 250 | +/// The error type of execution. |
| 251 | +#[derive(thiserror::Error, Debug)] |
| 252 | +pub enum ExecuteError {...} |
| 253 | +
|
| 254 | +impl Executor { |
| 255 | + pub fn execute(&self, stmt: BoundStatement) -> Result<String, ExecuteError> { |
| 256 | + match stmt { |
| 257 | + BoundStatement::CreateTable(stmt) => self.execute_create_table(stmt), |
| 258 | + BoundStatement::Select(stmt) => self.execute_select(stmt), |
| 259 | + } |
| 260 | + } |
| 261 | +
|
| 262 | + fn execute_create_table(&self, stmt: BoundCreateTable) -> Result<String, ExecuteError> { |
| 263 | + // YOUR CODE HERE |
| 264 | + } |
| 265 | +
|
| 266 | + fn execute_select(&self, query: BoundSelect) -> Result<String, BindError> { |
| 267 | + // YOUR CODE HERE |
| 268 | + } |
| 269 | +} |
| 270 | +``` |
| 271 | + |
| 272 | +我们暂时将 Executor 的返回值设定为 `String` 类型,表示语句的执行结果。 |
| 273 | +在下一个任务中,我们会实现更具体的内存数据类型 `Array` 和 `DataChunk`。 |
| 274 | +到那时,Executor 的输出就是一段真正的数据了。 |
0 commit comments