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

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

1. SQL解析器原理与流程

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

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

实际解析流程示例

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

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

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

Token序列:
SELECT → id → , → name → FROM → users → WHERE → age → > → 18 → ;

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

// 创建词法分析器
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语法规则构建抽象语法树:

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

// 解析入口
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语句的关键代码

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的基本节点类型

// 所有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如下:

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不是一次性构建完成的,而是随着解析过程逐步构建:

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解析的核心代码

// 表达式解析的核心函数
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
}

运算符优先级处理

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

// 优先级常量
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

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结构

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,还有嵌套查询这些功能的解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值