Files
ChuSQL/docs/INTRODUCTION.md
T

88 lines
4.3 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.
# 实现要点
完整的实现原理相对复杂,故不多赘述,这里阐述一些关键的实现要点
## Haskell -- 处理引擎
### 词法分析
词法层与语法层同处 `chusql-engine/src/ChuSQL/SQLSyntax/Parser.hs`,基于 **megaparsec** 实现。解析器类型为
```haskell
type Parser = Parsec Void String
```
即输入是 `String`,词法错误类型取 `Void`(不自定义错误类型),失败信息统一由 `errorBundlePretty` 渲染成人可读的报文。
#### 空白与词法单元骨架
| 组合子 | 定义 | 作用 |
| -------- | ------------------------------ | ------------------------------------ |
| `sc` | `Lxr.space space1 empty empty` | 跳过空白(含换行);暂不解析注释 |
| `lexeme` | `Lxr.lexeme sc` | 解析一个词法单元,并吃掉其后的空白 |
| `symbol` | `Lxr.symbol sc` | 固定符号 `*` `,` `=` `>` `<` `(` `)` |
所有词法单元都从 `lexeme` 出口,游标返回时必定停在下一个 token 的起始位置,因此语法层不需要感知空白。
#### 已实现的 token
| token | 组合子 | 词法规则 | 例 |
| -------- | ------------ | ------------------------------------------ | ------------------------ |
| 关键字 | `keyword` | 大小写不敏感;其后不能紧跟标识符字符 | `SELECT` / `select` |
| 标识符 | `identifier` | 首字符为字母或 `_`,其后为字母、数字或 `_` | `users`、`user1`、`_tmp` |
| 整数 | `integer` | 十进制,委托 `Lxr.decimal` | `18` |
| 字符串 | `stringLit` | 单引号包裹,遵循标准 SQL 转义 | `'Alice'` |
| 符号 | `symbol` | 字面匹配 | `>`、`(` |
| 排序方向 | `sortDir` | `ASC` / `DESC`,缺省 `Asc` | `DESC` |
目前已接入语法的关键字:`SELECT`、`FROM`、`WHERE`、`ORDER`、`BY`、`LIMIT`、`AND`、`OR`、`ASC`、`DESC`、`INSERT`、`INTO`、`VALUES`、`DELETE`、`UPDATE`、`SET`。
#### 三个关键实现点
**1. 关键字要防前缀**
`keyword` 匹配完关键字后追加 `notFollowedBy (satisfy isIdentChar)`,否则 `selection` 会被读成 `SELECT`、`fromage` 会被读成 `FROM`。
**2. 关键字必须可回溯(`try`)**
```haskell
keyword k = lexeme $ try $ do
_ <- string' k
notFollowedBy (satisfy isIdentChar)
```
`string'` 大小写不敏感,匹配 `ORDER` 时会先吃掉前两个字符 `OR`;紧接着的前缀守卫因为遇到 `D` 而失败,此时输入已被消耗,这个失败就成了**已消耗失败**,调用方的 `many`(`AND` / `OR` 运算符层)不会回溯而是直接报错:
```text
SELECT name FROM users WHERE age > 18 ORDER BY age DESC
^ unexpected 'D'
```
把整个 `keyword` 用 `try` 包住,失败即复位,`ORDER BY` 才能被后续子句正常识别。这同时消掉了一整类隐患:任何以 `AND` / `OR` 为前缀的子句关键字跟在表达式后面都不会再炸。
**3. 字符串字面量不能用 `manyTill`**
`manyTill p end` 每一步都先尝试结束符,`'It''s ok'` 会在 `''` 的第一个引号处提前收尾。正确做法是先读内容、再吃结尾引号:
```haskell
stringLit = lexeme $ do
_ <- char '\''
s <- many stringChar
_ <- char '\''
return s
stringChar = choice [ '\'' <$ try (string "''"), satisfy (/= '\'') ]
```
`stringChar` 的两个分支在失败时都不消耗输入,因此可以安全地放进 `many`。
#### 字符串字面量与标准 SQL 转义
| 写法 | 含义 |
| --------- | ------------------------------------------------------- |
| `'it''s'` | `''` 折叠为一个单引号,值为 `it's` |
| `'a\b'` | 反斜杠是普通字符,值为 `a\b`(**不**做 `\n`/`\t` 转义) |
| `''` | 空字符串 |
| `''''` | 只含一个单引号的字符串 |
| `'oops` | 未闭合,解析失败 |