SQL解析器:实现进阶功能
在上一篇文章中,我们介绍了SQL解析器的基础架构和核心功能实现,包括基本的SELECT、INSERT、UPDATE语句解析。本文将深入探讨SQL解析器的进阶功能实现,重点关注我们新增的DROP、JOIN、DELETE语句解析以及嵌套查询功能。
项目结构回顾
我们的SQL解析器遵循经典的编译器前端设计,分为以下几个核心模块:
bash
internal/parser/
├── ast/ - 抽象语法树定义
├── lexer/ - 词法分析器
├── parser/ - 语法分析器
└── test/ - 测试用例
这种分层结构使我们能够清晰地分离关注点,提高代码的可维护性和扩展性。
下面的图表展示了SQL解析的基本流程:
新增功能实现
1. DELETE语句解析
DELETE语句是数据操作语言(DML)的重要组成部分,用于从表中删除数据。其基本语法为:
sql
DELETE FROM table_name [WHERE condition];
在实现中,我们创建了 DeleteStatement
结构来表示DELETE语句:
go
// DeleteStatement 表示DELETE语句
type DeleteStatement struct {
TableName string // 要删除数据的表名
Where Expression // WHERE条件,如 id = 1
}
解析过程主要包括:
- 识别DELETE关键字
- 期望下一个token是FROM
- 解析表名
- 可选地解析WHERE子句
- 处理可选的分号
DELETE语句的实现流程如下:
实现代码关键部分:
go
// parseDeleteStatement 解析DELETE语句
func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) {
stmt := &ast.DeleteStatement{}
// 跳过DELETE关键字
p.nextToken()
// 期望下一个Token是FROM
if !p.currTokenIs(lexer.FROM) {
return nil, fmt.Errorf("期望FROM,但得到%s", p.currToken.Literal)
}
// 跳过FROM关键字
p.nextToken()
// 解析表名
if !p.currTokenIs(lexer.IDENTIFIER) {
return nil, fmt.Errorf("期望表名,但得到%s", p.currToken.Literal)
}
stmt.TableName = p.currToken.Literal
// 解析WHERE子句(可选)
p.nextToken()
if p.currTokenIs(lexer.WHERE) {
p.nextToken() // 跳过WHERE关键字
where, err := p.parseExpression(LOWEST)
if err != nil {
return nil, err
}
stmt.Where = where
}
// 检查可选的分号
if p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // 消费分号
}
return stmt, nil
}
DELETE语句的实现相对简单,但它是数据操作的基础功能之一。
2. JOIN操作的解析
关系型数据库的核心优势之一是能够通过JOIN操作关联多个表的数据。我们实现了多种JOIN类型的支持:
go
// JoinType 表示连接类型
type JoinType int
const (
INNER JoinType = iota
LEFT
RIGHT
FULL
)
JOIN子句的解析需要处理表名、可选的表别名以及ON条件:
go
// JoinClause 表示JOIN子句
type JoinClause struct {
JoinType JoinType
TableName string
Alias string
Condition JoinCondition
}
// JoinCondition 表示JOIN条件
type JoinCondition struct {
LeftTable string
LeftColumn string
RightTable string
RightColumn string
}
JOIN操作的AST结构如下图所示:
特别复杂的是表别名处理,需要支持两种形式:
- 显式别名:
table_name AS alias
- 隐式别名:
table_name alias
我们的实现代码能够正确处理这两种形式:
go
// 解析表别名(可选)
p.nextToken()
if p.currTokenIs(lexer.AS) {
p.nextToken()
if !p.currTokenIs(lexer.IDENTIFIER) {
return nil, fmt.Errorf("期望表别名,但得到%s", p.currToken.Literal)
}
join.Alias = p.currToken.Literal
p.nextToken()
} else if p.currTokenIs(lexer.IDENTIFIER) {
// 支持不带AS的别名语法: INNER JOIN orders o
join.Alias = p.currToken.Literal
p.nextToken()
}
JOIN解析过程的复杂之处还在于需要处理通过点号(.)限定的列引用,如 users.id = orders.user_id
。这需要我们修改标识符解析逻辑:
go
// 检查是否是表名限定的列名: table.column
if p.peekTokenIs(lexer.DOT) {
p.nextToken() // 跳过点号
p.nextToken() // 移动到列名
if !p.currTokenIs(lexer.IDENTIFIER) {
return nil, fmt.Errorf("期望列名,但得到%s", p.currToken.Literal)
}
// 更新标识符的值为 "table.column"
ident.Value = ident.Value + "." + p.currToken.Literal
}
JOIN解析流程图:
3. 嵌套查询功能
嵌套查询(子查询)是SQL的高级特性,允许在一个SQL语句中嵌入另一个SELECT语句。我们通过递归设计实现了任意深度的嵌套查询支持:
go
// SubqueryExpression 表示SQL中的子查询表达式
type SubqueryExpression struct {
Query Statement // 嵌套的查询语句
}
子查询可以出现在以下位置:
- FROM子句中:
SELECT * FROM (SELECT id FROM users) AS subq
- WHERE子句中:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)
实现嵌套查询的关键是递归的解析策略。当检测到左括号后跟着SELECT关键字时,解析器会递归调用SELECT语句的解析函数:
go
// parseGroupedExpression 解析括号表达式
func (p *Parser) parseGroupedExpression() (ast.Expression, error) {
p.nextToken() // 跳过左括号
// 检查是否是子查询
if p.currTokenIs(lexer.SELECT) {
subQuery, err := p.parseSelectStatement()
if err != nil {
return nil, err
}
// 检查右括号
if !p.currTokenIs(lexer.RPAREN) {
return nil, fmt.Errorf("期望右括号')',但得到%s", p.currToken.Literal)
}
// 前进到右括号之后的token
p.nextToken()
return &ast.SubqueryExpression{Query: subQuery}, nil
}
// 不是子查询,而是普通的括号表达式
exp, err := p.parseExpression(LOWEST)
// ...
}
以下是嵌套查询解析的流程图:
在FROM子句中的子查询还需要处理别名,这在解析器中被特殊处理:
go
// 解析表名或子查询
p.nextToken()
if p.currTokenIs(lexer.LPAREN) {
// 这是一个子查询
subquery, err := p.parseSubquery()
if err != nil {
return nil, err
}
stmt.Subquery = subquery
// 检查子查询后面是否有别名(必须有AS关键字)
if p.currTokenIs(lexer.AS) {
p.nextToken() // 跳过AS
if !p.currTokenIs(lexer.IDENTIFIER) {
return nil, fmt.Errorf("期望子查询别名,但得到%s", p.currToken.Literal)
}
stmt.TableAlias = p.currToken.Literal
p.nextToken() // 跳过别名
}
}
支持多层嵌套的关键是递归处理,每当遇到新的子查询,我们就递归地解析它,这使得我们的解析器能够处理任意复杂度的嵌套查询,如:
sql
SELECT t.name FROM (SELECT u.name FROM (SELECT name FROM users) AS u) AS t
4. DROP语句支持
为了完善DDL(数据定义语言)功能,我们实现了DROP TABLE语句:
go
// DropStatement 表示DROP语句
type DropStatement struct {
ObjectType string // 对象类型,如 "TABLE"
Name string // 要删除的对象名称
}
DROP语句的解析相对简单:
go
// parseDropTableStatement 解析DROP TABLE语句
func (p *Parser) parseDropTableStatement() (*ast.DropStatement, error) {
stmt := &ast.DropStatement{
ObjectType: "TABLE",
}
// 跳过DROP
p.nextToken()
// 跳过TABLE
p.nextToken()
// 解析表名
if !p.currTokenIs(lexer.IDENTIFIER) {
return nil, fmt.Errorf("期望表名,但得到%s", p.currToken.Literal)
}
stmt.Name = p.currToken.Literal
// 处理可选的分号
if p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken()
}
return stmt, nil
}
测试机制
为了确保解析器的正确性,我们为每种语句类型都编写了详细的测试用例:
- 单元测试:验证各个解析函数的正确性
- 集成测试:验证完整SQL语句的解析结果
- AST测试:验证AST节点的String()方法生成正确的SQL
测试架构如下:
示例测试代码:
go
// TestNestedQueries 测试嵌套查询的解析
func TestNestedQueries(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "FROM子句中的子查询",
input: "SELECT subq.id, subq.name FROM (SELECT id, name FROM users WHERE age > 18) AS subq",
},
{
name: "WHERE子句中的子查询",
input: "SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders)",
},
{
name: "多层嵌套查询",
input: "SELECT t.name FROM (SELECT u.name FROM (SELECT name FROM users) AS u) AS t",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := lexer.NewLexer(tt.input)
p := parser.NewParser(l)
stmt, err := p.Parse()
if err != nil {
t.Fatalf("解析错误: %v", err)
}
// 各种验证...
})
}
}
通过这样的测试,我们可以确保:
- 解析器正确识别所有SQL语句类型
- 解析器能够正确处理各种边界情况和错误情况
- AST节点能够正确地重新生成原始SQL
设计要点与优化考虑
在实现这些功能时,我们注重以下几个设计原则:
1. 递归下降解析
我们采用Pratt解析算法(自顶向下运算符优先级解析)处理表达式,这种算法特别适合于处理具有不同优先级的运算符,如SQL中的比较运算符和逻辑运算符。
优先级常量定义示例:
go
const (
LOWEST = 1 // 最低优先级
AND_OR = 2 // 逻辑运算符: AND OR
EQUALS = 3 // 相等运算符: == !=
LESSGREATER = 4 // 比较运算符: > < >= <=
SUM = 5 // 加减运算符: + -
PRODUCT = 6 // 乘除运算符: * /
PREFIX = 7 // 前缀运算符: -X 或 !X
)
2. 模块化设计
我们将不同类型的SQL语句解析逻辑分离到不同文件中,提高了代码的可维护性:
- select.go:处理SELECT语句和相关子句(JOIN, ORDER BY, LIMIT等)
- insert.go:处理INSERT语句
- update.go:处理UPDATE语句
- delete.go:处理DELETE语句
- create.go:处理CREATE TABLE语句
- drop.go:处理DROP TABLE语句
- expression.go:处理表达式解析(包括子查询)
3. 错误处理
我们提供详细的错误信息,包括期望的token和实际的token,以及行号和列号信息:
go
func (p *Parser) peekError(t lexer.TokenType) {
msg := fmt.Sprintf("行%d列%d: 期望下一个Token是%s,但得到了%s",
p.peekToken.Line, p.peekToken.Column, t, p.peekToken.Type)
p.errors = append(p.errors, msg)
}
4. 兼容性考虑
我们支持可选的分号,兼容不同SQL方言的习惯。同时,表别名处理也支持两种不同的语法形式:
sql
-- 两种形式都支持
SELECT u.id FROM users AS u
SELECT u.id FROM users u
5. 性能优化
虽然我们的实现主要关注功能完整性,但也考虑了一些性能因素:
- 使用预分配的map存储前缀和中缀解析函数
- 避免不必要的字符串拷贝和内存分配
- 使用结构体字段而非接口字段,减少运行时开销
后续展望
虽然我们已经实现了SQL解析器的核心功能,但仍有改进空间:
-
支持更多SQL特性:
- GROUP BY和HAVING子句
- 窗口函数支持(OVER, PARTITION BY)
- 存储过程和触发器语法
- 更多数据类型和函数支持
-
优化错误恢复机制:
- 在遇到错误时能够继续解析,提供更多错误信息
- 支持语法错误提示和修复建议
-
增加语义分析:
- 检查表和列名是否存在
- 类型检查和类型推导
- 检查引用完整性
-
实现SQL执行引擎:
- 将AST转换为执行计划
- 支持基础查询执行
- 实现简单的查询优化
基于当前的解析器架构,可以向这些方向自然扩展,进一步增强我们的SQL解析与执行系统。
但是因为我们这篇文章的重点不是这个,咱们暂且就先实现这么多吧。
总结
通过实现DROP、JOIN、DELETE语句和嵌套查询功能,我们的SQL解析器已经具备了处理相当复杂SQL语句的能力。
我们下一步我们将实现基础的 ALTER TABLE功能,这也是我们sql解析器的最后一部分内容。