Go语言从零构建SQL数据库(4)-解析器

SQL解析器:数据库的"翻译官"

1. SQL解析器原理与流程

SQL解析器是数据库系统的核心组件,负责将文本形式的SQL语句转换为系统内部可执行的结构。整个解析过程可以通过下图来表示:

sql 复制代码
+---------------+     +---------------+     +---------------+     +---------------+
|               |     |   词法分析器   |     |   语法分析器   |     |               |
| SQL文本输入    | --> |   (Lexer)     | --> |   (Parser)    | --> |  抽象语法树   |
|               |     |               |     |               |     |    (AST)     |
+---------------+     +---------------+     +---------------+     +---------------+
                            |                     |
                            v                     v
                      +---------------+    +---------------+
                      | Token序列     |    | 解析表达式     |
                      | 识别关键字     |    | 构建节点      |
                      | 识别标识符     |    | 处理优先级    |
                      | 识别操作符     |    | 错误处理      |
                      +---------------+    +---------------+

实际解析流程示例

以一个简单的查询为例:SELECT id, name FROM users WHERE age > 18;

第一步:词法分析(分词)

SQL文本首先经过词法分析器处理,被拆分成一系列Token:

sql 复制代码
Token序列:
SELECT → id → , → name → FROM → users → WHERE → age → > → 18 → ;

核心代码:词法分析器如何识别Token

go 复制代码
// 创建词法分析器
lexer := lexer.NewLexer("SELECT id, name FROM users WHERE age > 18;")

// 词法分析器核心方法
func (l *Lexer) NextToken() Token {
    // 跳过空白字符
    l.skipWhitespace()
  
    // 根据当前字符判断Token类型
    switch l.ch {
    case '=':
        return Token{Type: EQUAL, Literal: "="}
    case ',':
        return Token{Type: COMMA, Literal: ","}
    case '>':
        return Token{Type: GREATER, Literal: ">"}
    // ... 其他特殊字符处理
  
    default:
        if isLetter(l.ch) {
            // 读取标识符或关键字
            literal := l.readIdentifier()
            tokenType := lookupKeyword(literal) // 判断是否是关键字
            return Token{Type: tokenType, Literal: literal}
        } else if isDigit(l.ch) {
            // 读取数字
            return Token{Type: NUMBER, Literal: l.readNumber()}
        }
    }
}
第二步:语法分析(构建语法树)

Token序列传递给语法分析器,根据SQL语法规则构建抽象语法树:

核心代码:语法分析器如何分派处理不同语句

go 复制代码
// 解析入口
func (p *Parser) Parse() (ast.Statement, error) {
    // 根据第一个Token判断SQL语句类型
    switch p.currToken.Type {
    case lexer.SELECT:
        return p.parseSelectStatement()
    case lexer.INSERT:
        return p.parseInsertStatement()
    case lexer.UPDATE:
        return p.parseUpdateStatement()
    case lexer.CREATE:
        if p.peekTokenIs(lexer.TABLE) {
            return p.parseCreateTableStatement()
        }
        return nil, fmt.Errorf("不支持的CREATE语句")
    default:
        return nil, fmt.Errorf("不支持的SQL语句类型: %s", p.currToken.Literal)
    }
}

解析SELECT语句的关键代码

go 复制代码
func (p *Parser) parseSelectStatement() (*ast.SelectStatement, error) {
    stmt := &ast.SelectStatement{}
  
    p.nextToken() // 跳过SELECT关键字
  
    // 1. 解析列名列表
    columns, err := p.parseExpressionList(lexer.COMMA)
    if err != nil {
        return nil, err
    }
    stmt.Columns = columns
  
    // 2. 解析FROM子句和表名
    p.nextToken()
    if !p.currTokenIs(lexer.FROM) {
        return nil, fmt.Errorf("期望FROM,但得到%s", p.currToken.Literal)
    }
  
    p.nextToken() // 跳过FROM
    if !p.currTokenIs(lexer.IDENTIFIER) {
        return nil, fmt.Errorf("期望表名,但得到%s", p.currToken.Literal)
    }
    stmt.TableName = p.currToken.Literal
  
    // 3. 解析WHERE子句(可选)
    p.nextToken()
    if p.currTokenIs(lexer.WHERE) {
        p.nextToken() // 跳过WHERE
        expr, err := p.parseExpression(LOWEST) // 解析条件表达式
        if err != nil {
            return nil, err
        }
        stmt.Where = expr
    }
  
    // 4. 解析其他可选子句(ORDER BY, LIMIT等)
  
    return stmt, nil
}

2. 抽象语法树(AST)详解

抽象语法树是SQL语句的树状结构表示,每个节点代表SQL语句的一个组成部分。

AST的基本节点类型

go 复制代码
// 所有AST节点的基础接口
type Node interface {
    TokenLiteral() string // 返回节点对应的词法单元字面值
    String() string       // 返回节点的字符串表示
}

// SQL语句节点
type Statement interface {
    Node
    statementNode()
}

// 表达式节点
type Expression interface {
    Node
    expressionNode()
}

直观理解AST结构

对于查询语句 SELECT id, name FROM users WHERE age > 18;,最终构建的AST如下:

css 复制代码
SelectStatement
├── Columns: [
│   ├── Identifier{Value: "id"}
│   └── Identifier{Value: "name"}
│  ]
├── TableName: "users"
└── Where: BinaryExpression{
    ├── Left: Identifier{Value: "age"}
    ├── Operator: GREATER
    └── Right: LiteralExpression{Value: "18", Type: NUMBER}
   }

这个树状结构直观地展示了SQL语句的各个组成部分和它们之间的关系。

AST构建的渐进过程

AST不是一次性构建完成的,而是随着解析过程逐步构建:

css 复制代码
1. 初始化空的SelectStatement节点
   SelectStatement{} → 空结构

2. 解析列名列表
   SelectStatement{
     Columns: [
       Identifier{Value: "id"},
       Identifier{Value: "name"}
     ]
   }

3. 添加表名
   SelectStatement{
     Columns: [...],
     TableName: "users"
   }

4. 添加WHERE条件
   SelectStatement{
     Columns: [...],
     TableName: "users",
     Where: BinaryExpression{...}
   }

3. 表达式解析的关键技术

表达式解析是SQL解析器最复杂的部分,尤其是处理运算符优先级和嵌套表达式。我们使用Pratt解析技术来高效处理这些问题。

Pratt解析的核心代码

go 复制代码
// 表达式解析的核心函数
func (p *Parser) parseExpression(precedence int) (ast.Expression, error) {
    // 1. 获取前缀解析函数(处理标识符、字面量等)
    prefix := p.prefixParseFns[p.currToken.Type]
    if prefix == nil {
        return nil, fmt.Errorf("找不到%s的前缀解析函数", p.currToken.Literal)
    }
  
    // 2. 解析最左侧表达式
    leftExp, err := prefix()
    if err != nil {
        return nil, err
    }
  
    // 3. 根据优先级处理中缀表达式(处理运算符如>、=、AND等)
    for !p.peekTokenIs(lexer.SEMICOLON) && precedence < p.peekPrecedence() {
        infix := p.infixParseFns[p.peekToken.Type]
        if infix == nil {
            return leftExp, nil
        }
      
        p.nextToken() // 移动到运算符
      
        // 构建二元表达式,保证运算符优先级正确
        leftExp, err = infix(leftExp)
        if err != nil {
            return nil, err
        }
    }
  
    return leftExp, nil
}

运算符优先级处理

定义清晰的优先级确保表达式按照预期顺序解析:

go 复制代码
// 优先级常量
const (
    LOWEST      = 1 // 最低优先级
    AND_OR      = 2 // AND OR
    EQUALS      = 3 // == !=
    LESSGREATER = 4 // > < >= <=
    SUM         = 5 // + -
    PRODUCT     = 6 // * /
    PREFIX      = 7 // -X 或 !X
)

// 运算符优先级映射
var precedences = map[TokenType]int{
    EQUAL:         EQUALS,
    NOT_EQUAL:     EQUALS,
    LESS:          LESSGREATER,
    GREATER:       LESSGREATER,
    AND:           AND_OR,
    OR:            AND_OR,
    // ...其他运算符
}

4. 实际案例解析

让我们通过一个完整示例,来展示SQL语句从文本到AST的完整转换过程:

输入SQL

sql 复制代码
SELECT id, name FROM users WHERE age > 18 AND role = 'admin';

转换过程

  1. 词法分析:将SQL文本拆分为Token序列
  2. 语法分析:识别SELECT语句的基本结构
  3. 解析列列表 :识别 idname两个列
  4. 解析表名 :识别表名 users
  5. 解析WHERE子句
    • 解析 age > 18为一个二元表达式
    • 遇到 AND运算符,创建新的二元表达式
    • 解析 role = 'admin'作为右侧表达式
    • 最终WHERE子句表示为嵌套的二元表达式

最终AST结构

css 复制代码
SelectStatement
├── Columns: [
│   ├── Identifier{Value: "id"}
│   └── Identifier{Value: "name"}
│  ]
├── TableName: "users"
└── Where: BinaryExpression{
    ├── Left: BinaryExpression{
    │   ├── Left: Identifier{Value: "age"}
    │   ├── Operator: GREATER
    │   └── Right: LiteralExpression{Value: "18", Type: NUMBER}
    │  }
    ├── Operator: AND
    └── Right: BinaryExpression{
        ├── Left: Identifier{Value: "role"}
        ├── Operator: EQUAL
        └── Right: LiteralExpression{Value: "admin", Type: STRING}
       }
   }

小结

通过以上解析过程,我们实现了从SQL文本到内部数据结构的转换,这个结构可以被数据库引擎进一步处理。SQL解析器的质量直接影响数据库系统的稳定性和性能,一个好的解析器应当:

  1. 能够正确识别各种SQL语法
  2. 提供清晰的错误信息
  3. 构建结构良好的AST
  4. 为后续的查询计划和优化提供基础

在接下来的章节中,我们将完善这个解析器实现SQL语句更为全面的解析,包括drop关键字,xxx join,delete,还有嵌套查询这些功能的解析。

相关推荐
徐小黑ACG7 分钟前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂3 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch5 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
bobz9655 小时前
k8s 怎么提供虚拟机更好
后端
bobz9656 小时前
nova compute 如何创建 ovs 端口
后端
用键盘当武器的秋刀鱼6 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端
Asthenia04127 小时前
从迷宫到公式:为 NFA 构造正规式
后端
Asthenia04127 小时前
像整理玩具一样:DFA 化简和状态等价性
后端
Asthenia04127 小时前
编译原理:打包思维-NFA 怎么变成 DFA
后端