SQL解析器系列:实现ALTER TABLE语句

173 阅读7分钟

SQL解析器系列:实现ALTER TABLE语句

在SQL解析器系列的最后一篇文章中,我们将聚焦于ALTER TABLE语句的实现。ALTER TABLE是数据定义语言(DDL)中的重要组成部分,允许在不丢失数据的情况下修改表结构。实现这个功能将为我们的SQL解析器画上圆满的句号,也为下一阶段的查询执行引擎开发奠定基础。

ALTER TABLE语法与复杂性

ALTER TABLE语句的语法相对复杂,支持多种操作类型:

ALTER TABLE users 
    ADD COLUMN email VARCHAR(100) NOT NULL,
    MODIFY COLUMN name VARCHAR(50) DEFAULT 'unknown',
    DROP COLUMN age,
    ADD CONSTRAINT uk_email UNIQUE (email);

ALTER TABLE语句的基本语法结构如下图所示:

flowchart LR
    A[ALTER TABLE] --> B[表名]
    B --> C[操作1]
    C --> D{更多操作?}
    D -->|是| E[逗号]
    E --> F[操作2]
    F --> D
    D -->|否| G[分号可选]

与其他SQL语句相比,ALTER TABLE的实现面临以下挑战:

  1. 多操作支持:一条ALTER语句可以包含多个子操作,每个操作有不同的语法结构
  2. 复杂的类型定义:列类型可能包含参数,如 DECIMAL(10,2)
  3. 多种约束类型:需要支持PRIMARY KEY、UNIQUE、FOREIGN KEY等不同约束
  4. 默认值表达式:默认值可以是简单字面量或复杂表达式

各种ALTER操作类型的语法结构如下:

flowchart TD
    A[ALTER操作] --> B{操作类型}
    B -->|ADD COLUMN| C[列名 + 类型 + 属性]
    B -->|MODIFY COLUMN| D[列名 + 新类型 + 属性]
    B -->|CHANGE COLUMN| E[旧列名 + 新列名 + 类型 + 属性]
    B -->|DROP COLUMN| F[列名]
    B -->|ADD CONSTRAINT| G[约束名 + 约束类型 + 参数]
    B -->|DROP CONSTRAINT| H[约束名]
  
    C --> I[列属性]
    D --> I
    E --> I
  
    I --> I1[NOT NULL]
    I --> I2[DEFAULT 值]
    I --> I3[COMMENT '注释']
  
    G --> J{约束类型}
    J -->|UNIQUE| K[列名列表]
    J -->|PRIMARY KEY| L[列名列表]
    J -->|FOREIGN KEY| M[列名列表 + REFERENCES + 表名 + 列名列表]

数据结构设计

为了表示ALTER TABLE语句,我们设计了一个层次化的AST结构:

classDiagram
    class AlterTableStatement {
        +String TableName
        +[]AlterAction Actions
    }
  
    class AlterAction {
        +AlterActionType Type
        +*AlterColumnDef ColumnDef
        +String ColumnName
        +*Constraint Constraint
        +String ConstraintName
    }
  
    class AlterColumnDef {
        +String OldName
        +String Name
        +String Type
        +bool NotNull
        +*ColumnDefault Default
        +*ColumnComment Comment
    }
  
    class Constraint {
        +String Name
        +ConstraintType Type
        +[]String Columns
        +String ReferencedTable
        +[]String ReferencedCols
    }
  
    AlterTableStatement "1" --> "*" AlterAction : contains
    AlterAction --> AlterColumnDef : for column operations
    AlterAction --> Constraint : for constraint operations

这种设计允许我们在一个语句中表示多种ALTER操作,每种操作都有其特定的结构和属性。ALTER操作类型通过枚举定义:

// AlterActionType 表示ALTER TABLE操作的类型
type AlterActionType int

const (
    AddColumn AlterActionType = iota
    ModifyColumn
    ChangeColumn
    DropColumn
    AddConstraint
    DropConstraint
)

解析实现

主解析函数的流程

ALTER TABLE语句的解析过程可以表示为以下流程图:

flowchart TD
    A[开始解析] --> B[跳过ALTER TABLE关键字]
    B --> C[解析表名]
    C --> D[解析第一个ALTER操作]
    D --> E{当前是逗号?}
    E -->|是| F[跳过逗号]
    F --> G[解析下一个ALTER操作]
    G --> E
    E -->|否| H{当前是分号?}
    H -->|是| I[跳过分号]
    H -->|否| J[完成]
    I --> J
    J --> K[返回AlterTableStatement]

解析的核心代码如下:

func (p *Parser) parseAlterTableStatement() (*ast.AlterTableStatement, error) {
    stmt := &ast.AlterTableStatement{}

    // 跳过ALTER TABLE
    p.nextToken() // 跳过ALTER
    p.nextToken() // 跳过TABLE

    // 解析表名
    if !p.currTokenIs(lexer.IDENTIFIER) {
        return nil, fmt.Errorf("期望表名,但得到%s", p.currToken.Literal)
    }
    stmt.TableName = p.currToken.Literal
    p.nextToken()

    // 解析第一个ALTER操作
    action, err := p.parseAlterAction()
    if err != nil {
        return nil, err
    }
    stmt.Actions = append(stmt.Actions, action)

    // 如果有更多的操作(以逗号分隔),继续解析
    for p.currTokenIs(lexer.COMMA) {
        p.nextToken() // 跳过逗号
        action, err := p.parseAlterAction()
        if err != nil {
            return nil, err
        }
        stmt.Actions = append(stmt.Actions, action)
    }

    // 可选的分号
    if p.currTokenIs(lexer.SEMICOLON) {
        p.nextToken() // 跳过分号
    }

    return stmt, nil
}

操作类型解析详解

每种ALTER操作类型的解析逻辑都不同,以下是各种操作类型的解析流程:

flowchart TD
    A[parseAlterAction] --> B{当前Token类型}
  
    B -->|ADD| C[跳过ADD]
    C --> D{下一个是?}
    D -->|COLUMN| E[解析ADD COLUMN]
    D -->|CONSTRAINT| F[解析ADD CONSTRAINT]
    D -->|其他| G[错误]
  
    B -->|MODIFY| H[解析MODIFY COLUMN]
    B -->|CHANGE| I[解析CHANGE COLUMN]
  
    B -->|DROP| J[跳过DROP]
    J --> K{下一个是?}
    K -->|COLUMN| L[解析DROP COLUMN]
    K -->|CONSTRAINT| M[解析DROP CONSTRAINT]
    K -->|其他| N[错误]
  
    B -->|其他| O[错误]
  
    E --> P[返回ALTER操作]
    F --> P
    H --> P
    I --> P
    L --> P
    M --> P

特别值得注意的是ADD COLUMN操作的解析过程:

flowchart TD
    A[开始解析ADD COLUMN] --> B[解析列名]
    B --> C[解析列类型]
    C --> D{有括号参数?}
    D -->|是| E[解析类型参数]
    D -->|否| F[继续]
    E --> F
    F --> G{有列属性?}
    G -->|NOT NULL| H[设置NotNull=true]
    G -->|DEFAULT| I[解析默认值表达式]
    G -->|COMMENT| J[解析注释字符串]
    G -->|否| K[结束]
    H --> G
    I --> G
    J --> G

处理复杂类型参数

处理类型参数(如VARCHAR(100))时,我们需要特别注意嵌套括号的情况。我们使用一个计数器跟踪括号嵌套级别:

flowchart LR
    A[遇到左括号] --> B[bracketLevel = 1]
    B --> C[跳过左括号]
    C --> D[循环]
    D --> E{当前字符}
    E -->|左括号| F[bracketLevel++]
    E -->|右括号| G[bracketLevel--]
    F --> H[跳过当前字符]
    G --> I{bracketLevel > 0?}
    I -->|是| H
    I -->|否| J[跳过右括号]
    H --> D
    J --> K[继续解析]

具体实现代码:

// 处理类型后可能的括号参数,如VARCHAR(100)
if p.currTokenIs(lexer.LPAREN) {
    bracketLevel := 1
    p.nextToken() // 跳过左括号
  
    // 跳过括号内的所有token直到匹配的右括号
    for bracketLevel > 0 && !p.currTokenIs(lexer.EOF) {
        if p.currTokenIs(lexer.LPAREN) {
            bracketLevel++
        } else if p.currTokenIs(lexer.RPAREN) {
            bracketLevel--
        }
      
        if bracketLevel > 0 {
            p.nextToken()
        }
    }
  
    if p.currTokenIs(lexer.RPAREN) {
        p.nextToken() // 跳过右括号
    }
}

这种方法确保我们能正确处理嵌套括号,如 DECIMAL(12,2)或更复杂的类型定义。

表达式解析与默认值处理

默认值可以是各种表达式,例如数字、字符串甚至函数调用。我们利用之前实现的Pratt解析算法来处理这些表达式:

flowchart TD
    A[解析DEFAULT] --> B[跳过DEFAULT关键字]
    B --> C[调用parseExpression]
    C --> D[创建ColumnDefault结构]
    D --> E[返回结果]
if p.currTokenIs(lexer.DEFAULT) {
    p.nextToken() // 跳过DEFAULT
  
    // 解析默认值表达式
    expr, err := p.parseExpression(LOWEST)
    if err != nil {
        return nil, err
    }
    colDef.Default = &ast.ColumnDefault{Value: expr}
}

表达式解析的优先级流程如下:

flowchart TD
    A[parseExpression] --> B[获取前缀解析函数]
    B --> C[解析左侧表达式]
    C --> D{下一个Token优先级>当前优先级?}
    D -->|是| E[获取中缀解析函数]
    E --> F[解析二元表达式]
    F --> D
    D -->|否| G[返回表达式]

约束解析深入讲解

约束解析是ALTER TABLE中的另一个复杂部分,不同类型的约束有不同的语法结构:

flowchart TD
    A[parseConstraint] --> B{约束类型}
  
    B -->|UNIQUE| C[constraint.Type = UniqueConstraint]
    C --> D[解析列名列表]
  
    B -->|PRIMARY KEY| E[constraint.Type = PrimaryKeyConstraint]
    E --> F[跳过KEY]
    F --> D
  
    B -->|FOREIGN KEY| G[constraint.Type = ForeignKeyConstraint]
    G --> H[跳过KEY]
    H --> I[解析列名列表]
    I --> J[期望REFERENCES]
    J --> K[解析引用表名]
    K --> L[解析引用列名列表]
  
    D --> M[返回约束]
    L --> M

约束解析的一个关键部分是解析标识符列表,例如 (id, name, email)

// parseIdentifierList 解析标识符列表
func (p *Parser) parseIdentifierList() ([]string, error) {
    identifiers := []string{}

    // 跳过左括号
    p.nextToken()

    // 第一个标识符
    if !p.currTokenIs(lexer.IDENTIFIER) {
        return nil, fmt.Errorf("期望标识符,但得到%s", p.currToken.Literal)
    }

    identifiers = append(identifiers, p.currToken.Literal)

    // 解析剩余标识符
    for p.peekTokenIs(lexer.COMMA) {
        p.nextToken() // 跳过当前标识符或逗号
        p.nextToken() // 移动到下一个标识符

        if !p.currTokenIs(lexer.IDENTIFIER) {
            return nil, fmt.Errorf("期望标识符,但得到%s", p.currToken.Literal)
        }

        identifiers = append(identifiers, p.currToken.Literal)
    }

    // 期望下一个Token是右括号
    if !p.expectPeek(lexer.RPAREN) {
        return nil, fmt.Errorf("期望),但得到%s", p.peekToken.Literal)
    }

    return identifiers, nil
}

测试实现详解

测试是确保解析器正确性的关键。我们为ALTER TABLE设计了全面的测试套件:

flowchart TD
    A[TestAlterTableStatement] --> B[定义测试用例]
    B --> C[循环测试用例]
    C --> D[创建Lexer]
    D --> E[创建Parser]
    E --> F[调用Parse方法]
    F --> G{检查错误}
    G -->|有错误| H{期望错误?}
    H -->|是| I[测试通过]
    H -->|否| J[测试失败]
    G -->|无错误| K{期望无错误?}
    K -->|是| L[验证解析结果]
    K -->|否| M[测试失败]
    L --> N{验证成功?}
    N -->|是| I
    N -->|否| J

测试用例覆盖了各种ALTER TABLE操作:

{
    name:           "Add Column",
    input:          "ALTER TABLE users ADD COLUMN email VARCHAR(100) NOT NULL;",
    checkTableName: "users",
    actionCount:    1,
    expectError:    false,
},
{
    name:           "Add Column with Default",
    input:          "ALTER TABLE users ADD COLUMN score INT DEFAULT 100;",
    checkTableName: "users",
    actionCount:    1,
    expectError:    false,
},
{
    name:           "Multiple Actions",
    input:          "ALTER TABLE orders ADD COLUMN created_at TIMESTAMP, ADD CONSTRAINT pk_order PRIMARY KEY (id);",
    checkTableName: "orders",
    actionCount:    2,
    expectError:    false,
},
{
    name:           "Drop Column",
    input:          "ALTER TABLE products DROP COLUMN description;",
    checkTableName: "products",
    actionCount:    1,
    expectError:    false,
},

对于每个测试用例,我们验证:

  1. 是否正确解析表名
  2. 操作数量是否正确
  3. 每个操作的类型是否正确
  4. 操作参数(列定义、约束等)是否正确

错误处理机制

强大的错误处理对于解析器至关重要。我们采用详细的错误消息,帮助用户快速定位问题:

flowchart LR
    A[遇到错误情况] --> B[构建详细错误信息]
    B --> C[包含当前Token信息]
    C --> D[包含期望的Token]
    D --> E[包含位置信息]
    E --> F[返回错误]

错误处理示例:

if !p.currTokenIs(lexer.IDENTIFIER) {
    return nil, fmt.Errorf("期望表名,但得到%s", p.currToken.Literal)
}

if !p.currTokenIs(lexer.LPAREN) {
    return nil, fmt.Errorf("期望(,但得到%s", p.currToken.Literal)
}

这种详细的错误提示使得用户能够快速识别和修复SQL语法错误。

编译器设计模式应用

我们的SQL解析器实现展示了多种编译器设计模式:

flowchart TD
    A[编译器设计模式] --> B[词法分析模式]
    A --> C[递归下降解析]
    A --> D[访问者模式]
    A --> E[解释器模式]
  
    B --> B1[Token分类]
    B --> B2[状态转换]
  
    C --> C1[自顶向下]
    C --> C2[预测解析]
  
    D --> D1[AST遍历]
  
    E --> E1[AST解释执行]

特别值得一提的是Pratt解析算法的应用,它使我们能够轻松处理复杂表达式的优先级:

flowchart LR
    A[表达式] --> B[运算符优先级表]
    B --> C[前缀解析函数]
    B --> D[中缀解析函数]
    C --> E[parseExpression]
    D --> E
    E --> F[AST表达式节点]

技术难点与解决方案详解

1. 多操作处理

解析多个操作需要仔细处理Token序列,特别是在操作之间的逗号和语句结尾的分号:

flowchart TD
    A[解析第一个操作] --> B{当前Token}
    B -->|逗号| C[跳过逗号]
    C --> D[解析下一个操作]
    D --> B
    B -->|分号| E[跳过分号]
    B -->|其他| F[结束]
    E --> F

我们的实现确保正确处理操作序列,即使它们跨越多行:

// 如果有更多的操作(以逗号分隔),继续解析
for p.currTokenIs(lexer.COMMA) {
    p.nextToken() // 跳过逗号
    action, err := p.parseAlterAction()
    if err != nil {
        return nil, err
    }
    stmt.Actions = append(stmt.Actions, action)
}

2. 嵌套括号处理

处理类型参数中的嵌套括号是一个挑战,我们通过维护括号嵌套级别解决这个问题:

flowchart LR
    A["DECIMAL(10,2)"] --> B[bracketLevel=1]
    B --> C[处理10]
    C --> D[处理逗号]
    D --> E[处理2]
    E --> F[bracketLevel=0]
    F --> G[继续解析]

代码实现通过跟踪嵌套级别,确保只有在所有括号闭合后才结束解析:

bracketLevel := 1
p.nextToken() // 跳过左括号

for bracketLevel > 0 && !p.currTokenIs(lexer.EOF) {
    if p.currTokenIs(lexer.LPAREN) {
        bracketLevel++
    } else if p.currTokenIs(lexer.RPAREN) {
        bracketLevel--
    }
  
    if bracketLevel > 0 {
        p.nextToken()
    }
}

3. 表达式解析集成

将表达式解析集成到ALTER TABLE实现中,我们重用了现有的表达式解析功能:

flowchart TD
    A[解析DEFAULT] --> B[调用parseExpression]
    B --> C[创建表达式AST]
    C --> D[设置列默认值]

这种集成允许我们解析复杂的默认值表达式,如 DEFAULT 10 * 2 + 5

SQL解析器系列总结

至此,我们的SQL解析器已经实现了所有核心功能,形成了一个完整的系统:

flowchart LR
    SQL["SQL文本\n(SELECT * FROM users WHERE id > 10)"] --> Lexer["词法分析器"]
    Lexer --> Tokens["Token流\n[SELECT, *, FROM, users, WHERE, id, >, 10]"]
    Tokens --> Parser["语法分析器"]
    Parser --> AST["抽象语法树(AST)"]
  
    Lexer -- 步骤1 --> L1["读取字符流"]
    L1 -- 步骤2 --> L2["识别关键字/标识符"]
    L2 -- 步骤3 --> Tokens
  
    Parser -- 步骤4 --> P1["识别语句类型\n(SELECT语句)"]
    P1 -- 步骤5 --> P2["解析表达式\n(Pratt算法)"]
    P2 -- 步骤6 --> AST
  
    AST -- 步骤7 --> A1["SELECT节点"]
    A1 -- 步骤8 --> A2["列: *\n表: users\n条件: id > 10"]
  
    style SQL fill:#f5f5f5,stroke:#333,stroke-width:2px
    style Lexer fill:#d4e6f1,stroke:#333,stroke-width:2px
    style Tokens fill:#d4e6f1,stroke:#333,stroke-width:2px
    style Parser fill:#d5e8d4,stroke:#333,stroke-width:2px
    style AST fill:#d5e8d4,stroke:#333,stroke-width:2px
  
    style L1 fill:#e1f5fe,stroke:#333,stroke-width:1px
    style L2 fill:#e1f5fe,stroke:#333,stroke-width:1px
  
    style P1 fill:#e8f5e9,stroke:#333,stroke-width:1px
    style P2 fill:#e8f5e9,stroke:#333,stroke-width:1px
  
    style A1 fill:#f9fbe7,stroke:#333,stroke-width:1px
    style A2 fill:#f9fbe7,stroke:#333,stroke-width:1px

我们支持的SQL功能包括:

flowchart TD
  SQL[SQL解析器] --> DML[DML]
  SQL --> DDL[DDL]
  SQL --> ADV[高级特性]
  
  DML --> SELECT[SELECT]
  DML --> INSERT[INSERT]
  DML --> UPDATE[UPDATE]
  DML --> DELETE[DELETE]
  
  SELECT --> COL[列选择]
  SELECT --> JOIN[表连接]
  SELECT --> WHERE[条件过滤]
  SELECT --> ORDER[排序]
  
  DDL --> CREATE[CREATE TABLE]
  DDL --> DROP[DROP TABLE]
  DDL --> ALTER[ALTER TABLE]
  
  ALTER --> ADD[ADD COLUMN]
  ALTER --> MODIFY[MODIFY COLUMN]
  ALTER --> DROP_COL[DROP COLUMN]
  ALTER --> ADD_CONS[添加约束]
  ALTER --> DROP_CONS[删除约束]
  
  ADV --> NESTED[嵌套查询]
  ADV --> EXPR[表达式]
  ADV --> ALIAS[表别名]

关键技术点回顾

在整个SQL解析器实现过程中,我们应用了多种编译原理技术:

classDiagram
    class Lexer {
        +input string
        +position int
        +readPosition int
        +ch byte
        +NextToken() Token
    }
  
    class Parser {
        +lexer *Lexer
        +currToken Token
        +peekToken Token
        +Parse() Statement
    }
  
    class AST {
        <<interface>>
        +statementNode()
        +TokenLiteral() string
    }
  
    Lexer --> Parser : provides tokens
    Parser --> AST : builds
  1. 词法分析:将SQL文本分解为Token序列
  2. 递归下降解析:采用自顶向下的方式构建语法树
  3. Pratt解析算法:处理不同优先级的运算符
  4. 抽象语法树:构建SQL语句的结构化表示

下一步:查询执行引擎

完成SQL解析器后,我们将开发查询执行引擎,这包括以下主要组件:

flowchart LR
    A[SQL解析器] --> B[查询执行引擎]
    B --> C[AST转换器]
    C --> D[执行计划生成器]
    D --> E[存储引擎接口]
    E --> F[结果处理器]
  1. AST转换为执行计划:将抽象语法树转换为可执行的操作序列
  2. 数据存储接口:设计与底层存储引擎的交互方式
  3. 执行器实现:执行各种SQL操作的具体逻辑
  4. 结果集处理:处理查询结果并返回给用户

这些组件将构成一个完整的查询执行系统,使我们能够实际运行SQL查询并获取结果。


SQL解析器是数据库系统的关键组件,它将用户输入的SQL语句转换为系统可理解的结构。通过实现ALTER TABLE语句,我们完成了SQL解析器的所有核心功能,为后续开发查询执行引擎奠定了坚实基础。

在下一阶段,我们将开始构建查询执行引擎,使我们的系统能够真正执行SQL操作并返回结果。敬请期待!