Files
ChuBlog/docs/INTRODUCTION.md
T

4.3 KiB
Raw Blame History

实现要点

完整的实现原理相对复杂,故不多赘述,这里阐述一些关键的实现要点

Haskell -- 处理引擎

词法分析

词法层与语法层同处 chusql-engine/src/ChuSQL/SQLSyntax/Parser.hs,基于 megaparsec 实现。解析器类型为

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)

keyword k = lexeme $ try $ do
    _ <- string' k
    notFollowedBy (satisfy isIdentChar)

string' 大小写不敏感,匹配 ORDER 时会先吃掉前两个字符 OR;紧接着的前缀守卫因为遇到 D 而失败,此时输入已被消耗,这个失败就成了已消耗失败,调用方的 many(AND / OR 运算符层)不会回溯而是直接报错:

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' 会在 '' 的第一个引号处提前收尾。正确做法是先读内容、再吃结尾引号:

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 未闭合,解析失败