SQL解析器:实现进阶功能

SQL解析器:实现进阶功能

在上一篇文章中,我们介绍了SQL解析器的基础架构和核心功能实现,包括基本的SELECT、INSERT、UPDATE语句解析。本文将深入探讨SQL解析器的进阶功能实现,重点关注我们新增的DROP、JOIN、DELETE语句解析以及嵌套查询功能。

项目结构回顾

我们的SQL解析器遵循经典的编译器前端设计,分为以下几个核心模块:

bash 复制代码
internal/parser/
├── ast/       - 抽象语法树定义
├── lexer/     - 词法分析器
├── parser/    - 语法分析器
└── test/      - 测试用例

这种分层结构使我们能够清晰地分离关注点,提高代码的可维护性和扩展性。

下面的图表展示了SQL解析的基本流程:

flowchart LR SQL[SQL文本] --> Lexer[词法分析器] Lexer --> TokenStream[Token流] TokenStream --> Parser[语法分析器] Parser --> AST[抽象语法树] AST --> StringGen[SQL生成器] StringGen --> OutputSQL[生成SQL] style SQL fill:#f9f,stroke:#333,stroke-width:2px style AST fill:#bbf,stroke:#333,stroke-width:2px style OutputSQL fill:#bfb,stroke:#333,stroke-width:2px

新增功能实现

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
}

解析过程主要包括:

  1. 识别DELETE关键字
  2. 期望下一个token是FROM
  3. 解析表名
  4. 可选地解析WHERE子句
  5. 处理可选的分号

DELETE语句的实现流程如下:

flowchart TD A[检测到DELETE关键字] --> B{下一个token是FROM?} B -->|是| C[解析表名] B -->|否| Z[抛出错误] C --> D{下一个token是WHERE?} D -->|是| E[解析WHERE表达式] D -->|否| F[无WHERE条件] E --> G[构建DeleteStatement] F --> G G --> H{下一个token是分号?} H -->|是| I[消费分号] H -->|否| J[语句结束] I --> K[返回AST] J --> K style A fill:#f9f,stroke:#333,stroke-width:2px style G fill:#bbf,stroke:#333,stroke-width:2px style K fill:#bfb,stroke:#333,stroke-width:2px

实现代码关键部分:

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结构如下图所示:

classDiagram class SelectStatement { +Columns []Expression +TableName string +TableAlias string +JoinClauses []JoinClause +Where Expression +OrderBy []OrderByClause +Limit *LimitClause } class JoinClause { +JoinType JoinType +TableName string +Alias string +Condition JoinCondition } class JoinCondition { +LeftTable string +LeftColumn string +RightTable string +RightColumn string } SelectStatement --> JoinClause : contains JoinClause --> JoinCondition : contains

特别复杂的是表别名处理,需要支持两种形式:

  1. 显式别名:table_name AS alias
  2. 隐式别名: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解析流程图:

flowchart TD A[检测JOIN类型] --> B[解析表名] B --> C{有表别名?} C -->|有AS关键字| D[解析AS后的别名] C -->|隐式别名| E[解析隐式别名] C -->|无别名| F[无别名] D --> G[期望ON关键字] E --> G F --> G G --> H[解析左表.列] H --> I[期望等号] I --> J[解析右表.列] J --> K[构建JoinClause] K --> L[返回JOIN AST] style A fill:#f9f,stroke:#333,stroke-width:2px style K fill:#bbf,stroke:#333,stroke-width:2px style L fill:#bfb,stroke:#333,stroke-width:2px

3. 嵌套查询功能

嵌套查询(子查询)是SQL的高级特性,允许在一个SQL语句中嵌入另一个SELECT语句。我们通过递归设计实现了任意深度的嵌套查询支持:

go 复制代码
// SubqueryExpression 表示SQL中的子查询表达式
type SubqueryExpression struct {
    Query Statement // 嵌套的查询语句
}

子查询可以出现在以下位置:

  1. FROM子句中:SELECT * FROM (SELECT id FROM users) AS subq
  2. 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)
    // ...
}

以下是嵌套查询解析的流程图:

flowchart TD A[遇到左括号] --> B{下一个是SELECT?} B -->|是| C[递归调用parseSelectStatement] B -->|否| D[解析普通括号表达式] C --> E[创建SubqueryExpression] D --> F[返回普通表达式] E --> G[返回子查询表达式] style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#bbf,stroke:#333,stroke-width:2px style E fill:#bfb,stroke:#333,stroke-width:2px

在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
}

测试机制

为了确保解析器的正确性,我们为每种语句类型都编写了详细的测试用例:

  1. 单元测试:验证各个解析函数的正确性
  2. 集成测试:验证完整SQL语句的解析结果
  3. AST测试:验证AST节点的String()方法生成正确的SQL

测试架构如下:

flowchart TD A[SQL字符串] --> B[Lexer] B --> C[Parser] C --> D[AST] D --> E{验证节点类型} E -->|成功| F[验证节点属性] E -->|失败| G[测试失败] F -->|成功| H[验证String方法] F -->|失败| G H -->|成功| I[测试通过] H -->|失败| G style A fill:#f9f,stroke:#333,stroke-width:2px style D fill:#bbf,stroke:#333,stroke-width:2px style I fill:#bfb,stroke:#333,stroke-width:2px style G fill:#f99,stroke:#333,stroke-width:2px

示例测试代码:

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)
            }
          
            // 各种验证...
        })
    }
}

通过这样的测试,我们可以确保:

  1. 解析器正确识别所有SQL语句类型
  2. 解析器能够正确处理各种边界情况和错误情况
  3. 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解析器的核心功能,但仍有改进空间:

  1. 支持更多SQL特性

    • GROUP BY和HAVING子句
    • 窗口函数支持(OVER, PARTITION BY)
    • 存储过程和触发器语法
    • 更多数据类型和函数支持
  2. 优化错误恢复机制

    • 在遇到错误时能够继续解析,提供更多错误信息
    • 支持语法错误提示和修复建议
  3. 增加语义分析

    • 检查表和列名是否存在
    • 类型检查和类型推导
    • 检查引用完整性
  4. 实现SQL执行引擎

    • 将AST转换为执行计划
    • 支持基础查询执行
    • 实现简单的查询优化

基于当前的解析器架构,可以向这些方向自然扩展,进一步增强我们的SQL解析与执行系统。

但是因为我们这篇文章的重点不是这个,咱们暂且就先实现这么多吧。

总结

通过实现DROP、JOIN、DELETE语句和嵌套查询功能,我们的SQL解析器已经具备了处理相当复杂SQL语句的能力。

我们下一步我们将实现基础的 ALTER TABLE功能,这也是我们sql解析器的最后一部分内容。

相关推荐
掉头发的王富贵几秒前
Dockerfile不会写?于是我花十分钟看了这篇文章
后端·docker·容器
zozowind3 分钟前
1Panel快速安装Dify指南
人工智能·后端
xin4974 分钟前
Calcite 如何通过 SQL 来查询不同数据源? 有性能问题吗?
后端·源码
编程乐趣4 分钟前
MahApps.Metro:专为 WPF 应用程序设计的 UI 框架
后端
bjzhang758 分钟前
rqlite:一个基于SQLite构建的分布式数据库
数据库·分布式·rqlite
Piper蛋窝8 分钟前
Go 1.7 相比 Go 1.6 有哪些值得注意的改动?
后端·go
张哈大9 分钟前
《苍穹外卖Day2:大一菜鸟的代码升空纪实》
后端
一介输生10 分钟前
Spring Cloud实现权限管理(网关+jwt版)
java·后端
AI_Infra智塔11 分钟前
ZStack文档DevOps平台建设实践
后端
我转的头好晕13 分钟前
EF Core基本使用
数据库·c#·asp.net