[手写系列]Go手写db — — 第二版

[手写系列]Go手写db --- --- 第二版

第一版文章:[手写系列]Go手写db --- --- 完整教程

  • 整体项目Github地址:https://github.com/ziyifast/ZiyiDB
  • 请大家多多支持,也欢迎大家star⭐️和共同维护这个项目~

本文主要介绍如何在 ZiyiDB 第一版的基础上,实现更多新功能,给大家提供更多的实现思路,以及实现的流程,后续更多关键字以及字段类型的支持,大家可以参考着实现。

一、功能列表

  1. 新增对 FLOAT 和 DATETIME 字段类型的支持
  2. 新增对 BETWEEN AND 关键字的支持
  3. 新增对注释符的支持
  4. 新增对 >=、<=、!= 操作符的支持

二、实现细节

1. 新增对 FLOAT 和 DATETIME 字段类型的支持

实现思路

为了支持 FLOAT 和 DATETIME 类型,我们需要在词法分析器、语法分析器和存储引擎中进行相应修改。

token.go新增常量 -> lexer.go词法分析器新增对Float、时间的解析 -> ast.go抽象语法树新增FloatLiteral、DatetimeLiteral -> parser.go语法解析器中新增对Float、时间类型数据的解析,并构建为语法树 -> memory.go存储引擎新增对Float、时间类型的insert、update操作(解析语法树)

  1. lexer/token.go中新增FLOAT、DATETIME const常量
  2. lexer/lexer.go的NextToken中新增对时间、数字的解析
  • 对时间的解析:是字符串,且能被转换为时间格式
  • 对浮点数数字的解析:是数字,且包含.
  1. internal/lexer/lexer.go的lookupIdentifier方法中新增标识符类型,用于解析关键字
  2. internal/ast/ast.go中为后续构建抽象语法树,新增FloatLiteral、DateTimeLiteral以及在Cell结构体中新增FloatValue和TimeValue存储该类型值

  3. internal/parser/parser.go解析器中新增对Float浮点数、DATETIME时间的解析,将解析出来的数据构建为抽象语法树的结构
  4. internal/storage/memory.go存储引擎中新增对*ast.FloatLiteral、*ast.DateTimeLiteral的解析,将其转换为原始类型
  • 更新evaluateExpression函数:
  • 更新Update函数:将抽象语法树中的datetime、float Literal字面量解析至原始类型并更新至Cell中
  • 更新Insert函数:将抽象语法树中的datetime、float Literal字面量解析至原始类型并添加至Cell中。

PS:

①ast.Literal 是用户输入的表达,ast.Cell 是实际存储的数据,将Cell struct放在ast.go中不合理,后续会进行优化

②各类Literal,比如:FloatLiteral这里的Value可以存储原本的值,而非统一用String存储,避免重复转换,增加开销

  • 大家可以自己尝试在原代码的基础上进行优化,当做练手

代码实现

①在词法分析器中添加新类型
go 复制代码
// internal/lexer/token.go
const (
    // ... 其他标记类型
    FLOAT    TokenType = "FLOAT"
    DATETIME TokenType = "DATETIME"
    // ... 其他标记类型
)

// internal/lexer/lexer.go
func (l *Lexer) NextToken() Token {
    var tok Token
    l.skipWhitespace()
    
    // ... 其他代码
    
    switch l.ch {
    // ... 其他case
    case '\'':
        tok.Type = STRING
        tok.Literal = l.readString()
        // 检查是否为时间格式
        if _, err := time.Parse("2006-01-02 15:04:05", tok.Literal); err == nil {
            tok.Type = DATETIME
        }
        return tok
    // ... 其他case
    }
    
    // 处理数字(包括浮点数)
    if isDigit(l.ch) || l.ch == '.' {
        num := l.readNumber()
        if strings.Contains(num, ".") {
            tok.Type = FLOAT
        } else {
            tok.Type = INT
        }
        tok.Literal = num
        return tok
    }
    
    // ... 其他代码
}

// internal/lexer/lexer.go
func (l *Lexer) readNumber() string {
    var num bytes.Buffer
    hasDecimal := false
    for (isDigit(l.ch) || (l.ch == '.' && !hasDecimal)) && l.ch != 0 {
        if l.ch == '.' {
            hasDecimal = true
        }
        num.WriteRune(l.ch)
        l.readChar()
    }
    return num.String()
}

// internal/lexer/lexer.go
func (l *Lexer) lookupIdentifier(ident string) TokenType {
    switch strings.ToUpper(ident) {
    // ... 其他case
    case "FLOAT":
        return FLOAT
    case "DATETIME":
        return DATETIME
    // ... 其他case
    }
    return IDENT
}
②在 AST 中添加新表达式类型
go 复制代码
// internal/ast/ast.go
// 新增FloatLiteral表达式类型
type FloatLiteral struct {
    BaseExpression
    Token lexer.Token
    Value string
}

// 新增DateTimeLiteral表达式类型
type DateTimeLiteral struct {
    BaseExpression
    Token lexer.Token
    Value string
}

// 在 Cell 结构体中添加新字段
type Cell struct {
    Type       CellType
    IntValue   int32
    TextValue  string
    FloatValue float32
    TimeValue  string
}

const (
    CellTypeInt CellType = iota
    CellTypeText
    CellTypeFloat
    CellTypeDateTime
)
③在解析器中处理新类型
go 复制代码
// internal/parser/parser.go
func (p *Parser) parseExpression() (ast.Expression, error) {
    switch p.curToken.Type {
    // ... 其他case
    case lexer.FLOAT:
        return &ast.FloatLiteral{
            Token: p.curToken,
            Value: p.curToken.Literal,
        }, nil
    case lexer.DATETIME:
        return &ast.DateTimeLiteral{
            Token: p.curToken,
            Value: p.curToken.Literal,
        }, nil
    // ... 其他case
    }
}
④在存储引擎中处理新数据类型
go 复制代码
// internal/storage/memory.go
// 修改 evaluateExpression 函数以处理新类型
func evaluateExpression(expr ast.Expression) (interface{}, error) {
    switch e := expr.(type) {
    // ... 其他case
    case *ast.FloatLiteral:
        val, err := strconv.ParseFloat(e.Value, 32)
        if err != nil {
            return nil, fmt.Errorf("Incorrect float value: '%s'", e.Value)
        }
        return float32(val), nil
    case *ast.DateTimeLiteral:
        t, err := time.Parse("2006-01-02 15:04:05", e.Value)
        if err != nil {
            return nil, fmt.Errorf("Incorrect datetime value: '%s'", e.Value)
        }
        return t, nil
    // ... 其他case
    }
}

// 在 Insert 方法中处理新类型
func (b *MemoryBackend) Insert(stmt *ast.InsertStatement) error {
    // ... 其他代码
    
    // 类型转换(保持原有逻辑)
    switch v := value.(type) {
    // ... 其他case
    case float32:
        row[tableColIdx] = ast.Cell{Type: ast.CellTypeFloat, FloatValue: v}
    case time.Time:
        row[tableColIdx] = ast.Cell{Type: ast.CellTypeDateTime, TimeValue: v.Format("2006-01-02 15:04:05")}
    // ... 其他case
    }
    
    // ... 其他代码
}

// 在 getColumnValue 中处理新类型
func getColumnValue(expr ast.Expression, row []ast.Cell, columns []ast.ColumnDefinition) (interface{}, error) {
    switch e := expr.(type) {
    // ... 其他case
    case *ast.FloatLiteral:
        val, err := strconv.ParseFloat(e.Value, 32)
        if err != nil {
            return nil, fmt.Errorf("Incorrect float value: '%s'", e.Value)
        }
        return float32(val), nil
    case *ast.DateTimeLiteral:
        val, err := time.Parse("2006-01-02 15:04:05", e.Value)
        if err != nil {
            return nil, fmt.Errorf("Incorrect datetime value: '%s'", e.Value)
        }
        return val, nil
    // ... 其他case
    }
}

测试

测试SQL:

sql 复制代码
CREATE TABLE users (id INT PRIMARY KEY,name text,age INT,score FLOAT, ctime DATETIME ); 
INSERT INTO users VALUES (1, 'Alice', 20,89.0, '2023-07-01 12:00:00');
INSERT INTO users VALUES (2, 'Bob', 25,98.3, '2023-07-04 12:00:00');

select * from users where score < 90.0;
select * from users where ctime >  '2023-07-02 12:00:00';
drop table users;

效果:

2. 新增对 BETWEEN AND 关键字的支持

实现思路

BETWEEN AND 是一个用于范围查询的操作符。我们需要在词法分析器中识别这两个关键字,在 AST 中添加新的表达式类型,在解析器中处理这种语法结构,并在存储引擎中实现相应的查询逻辑。

  1. lexer/token.go中新增BETWEEN、AND的TokenType常量
  2. lexer/lexer.go中的lookupIdentifier方法新增对BETWEEN、AND关键字的读取,将用户的字符输入转换为TokenType
  3. ast/ast.go抽象语法树中新增BetweenExpression struct,用于将between and内容构建到语法树中
  4. parser/parser.go语法解析器:
  • 新增parseBetweenExpression函数,用于将用户输入的between and部分解析为ast.BetweenExpression
  • 各语法的Statement部分新增对between and的处理(比如select、delete、update语句...)
    例:parseSelectStatement部分(构建select的抽象语法树)新增对between and关键字的处理,将其转换为where条件部分
  1. storage/memory.go中新增对*ast.BetweenExpression的处理,判断当前数据是否满足between and过滤条件

代码实现

①在词法分析器中添加关键字
go 复制代码
// internal/lexer/token.go
const (
    // ... 其他标记类型
    BETWEEN TokenType = "BETWEEN"
    AND     TokenType = "AND"
    // ... 其他标记类型
)

// internal/lexer/lexer.go
func (l *Lexer) lookupIdentifier(ident string) TokenType {
    switch strings.ToUpper(ident) {
    // ... 其他case
    case "BETWEEN":
        return BETWEEN
    case "AND":
        return AND
    // ... 其他case
    }
    return IDENT
}
②在 AST 中添加 BETWEEN 表达式
go 复制代码
// internal/ast/ast.go
// BetweenExpression 表示 BETWEEN AND 表达式
type BetweenExpression struct {
    Token lexer.Token // BETWEEN 标记
    Left  Expression  // 左操作数(列名或表达式)
    Low   Expression  // 下限值
    High  Expression  // 上限值
}

func (be *BetweenExpression) expressionNode()      {}
func (be *BetweenExpression) TokenLiteral() string { return be.Token.Literal }
③在解析器中处理 BETWEEN 语法
go 复制代码
// internal/parser/parser.go
// 在 parseSelectStatement、parseUpdateStatement 和 parseDeleteStatement 中添加处理逻辑
func (p *Parser) parseSelectStatement() (*ast.SelectStatement, error) {
    // ... 其他代码
    
    // 解析WHERE子句
    if p.peekTokenIs(lexer.WHERE) {
        p.nextToken()
        p.nextToken()

        // 解析左操作数(列名)
        if !p.curTokenIs(lexer.IDENT) {
            return nil, fmt.Errorf("You have an error in your SQL syntax; check the manual that corresponds to your db server version for the right syntax to use near '%s'", p.curToken.Literal)
        }
        left := &ast.Identifier{
            Token: p.curToken,
            Value: p.curToken.Literal,
        }

        // 解析操作符
        p.nextToken()
        operator := p.curToken

        // 处理BETWEEN操作符
        if p.curTokenIs(lexer.BETWEEN) {
            expr, err := p.parseBetweenExpression(left)
            if err != nil {
                return nil, err
            }
            stmt.Where = expr
            return stmt, nil
        }

        // ... 其他代码
    }
    
    // ... 其他代码
}

// 解析BETWEEN表达式
func (p *Parser) parseBetweenExpression(left ast.Expression) (ast.Expression, error) {
    expr := &ast.BetweenExpression{
        Token: p.curToken,
        Left:  left,
    }

    p.nextToken() // 跳过BETWEEN

    // 解析下限值
    lower, err := p.parseExpression()
    if err != nil {
        return nil, err
    }
    expr.Low = lower

    if !p.expectPeek(lexer.AND) {
        return nil, fmt.Errorf("expected AND after BETWEEN expression")
    }

    p.nextToken() // 跳过AND

    // 解析上限值
    upper, err := p.parseExpression()
    if err != nil {
        return nil, err
    }
    expr.High = upper

    return expr, nil
}
④在存储引擎中实现 BETWEEN 查询逻辑
go 复制代码
// internal/storage/memory.go
// 在 evaluateWhereCondition 中处理 BETWEEN 表达式
func evaluateWhereCondition(expr ast.Expression, row []ast.Cell, columns []ast.ColumnDefinition) (bool, error) {
    switch e := expr.(type) {
    // ... 其他case
    case *ast.BetweenExpression:
        // 获取列值
        colIndex, err := getColumnIndex(e.Left.(*ast.Identifier).Value, columns)
        if err != nil {
            return false, err
        }
        
        left := row[colIndex]
        lower, err := evaluateExpression(e.Low)
        if err != nil {
            return false, err
        }
        
        upper, err := evaluateExpression(e.High)
        if err != nil {
            return false, err
        }
        
        switch left.Type {
        case ast.CellTypeInt:
            leftVal := left.IntValue
            lowerVal, lok := lower.(int32)
            upperVal, uok := upper.(int32)
            if !lok || !uok {
                return false, fmt.Errorf("type mismatch in BETWEEN expression")
            }
            return leftVal >= lowerVal && leftVal <= upperVal, nil
        case ast.CellTypeFloat:
            leftVal := left.FloatValue
            lowerVal, lok := lower.(float32)
            upperVal, uok := upper.(float32)
            if !lok || !uok {
                return false, fmt.Errorf("type mismatch in BETWEEN expression")
            }
            return leftVal >= lowerVal && leftVal <= upperVal, nil
        case ast.CellTypeDateTime:
            val := left.TimeValue
            leftVal, err := time.Parse("2006-01-02 15:04:05", val)
            if err != nil {
                return false, err
            }
            lowerVal, lok := lower.(time.Time)
            upperVal, uok := upper.(time.Time)
            if !lok || !uok {
                return false, fmt.Errorf("type mismatch in BETWEEN expression")
            }
            return (leftVal.After(lowerVal) || leftVal.Equal(lowerVal)) &&
                (leftVal.Before(upperVal) || leftVal.Equal(upperVal)), nil
        default:
            return false, fmt.Errorf("unsupported type in BETWEEN expression")
        }
    // ... 其他case
    }
}

测试

测试SQL:

sql 复制代码
CREATE TABLE users (id INT PRIMARY KEY,name text,age INT,score FLOAT, ctime DATETIME ); 

INSERT INTO users VALUES (1, 'Alice', 20,89.0, '2023-07-01 12:00:00');
INSERT INTO users VALUES (2, 'Bob', 25,98.3, '2023-07-04 12:00:00');

select * from users where age between 21 and 28;
select * from users where ctime between '2021-07-02 12:00:00' and '2023-07-05 12:00:00';
select * from users where ctime between '2023-07-02 12:00:00' and '2023-07-05 12:00:00';
drop table users;

效果:

3. 新增对注释符的支持

实现思路

SQL 注释以--开头,直到行尾。我们需要在词法分析器中识别注释并跳过它们,在解析器中忽略注释标记。

  1. lexer/token.go中新增注释符标识--,用于匹配用户输入。
  2. lexer/lexer.go词法分析器中,读取注释即注释后的内容


    效果:
  3. parser/parser.go语法解析器中的ParseProgram、parseStatement忽略注释符之后的内容,遇到注释符即跳过

代码实现

①在词法分析器中添加注释支持
go 复制代码
// internal/lexer/token.go
const (
    // ... 其他标记类型
    COMMENT TokenType = "--"
    // ... 其他标记类型
)

// internal/lexer/lexer.go
func (l *Lexer) NextToken() Token {
    var tok Token
    l.skipWhitespace()
    
    // 检查是否为注释
    if l.ch == '-' && l.peekChar() == '-' {
        return l.readComment()
    }
    
    // ... 其他代码
}

// 读取注释内容
func (l *Lexer) readComment() Token {
    var comment bytes.Buffer
    l.readChar() // 跳过第一个 '-'
    l.readChar() // 跳过第二个 '-'
    
    // 读取直到行尾
    for l.ch != '\n' && l.ch != 0 && l.ch != '\r' {
        comment.WriteRune(l.ch)
        l.readChar()
    }
    return Token{Type: COMMENT, Literal: comment.String()}
}
②在解析器中忽略注释
go 复制代码
// internal/parser/parser.go
func (p *Parser) ParseProgram() (*ast.Program, error) {
    program := &ast.Program{
        Statements: []ast.Statement{},
    }

    for p.curToken.Type != lexer.EOF {
        // 跳过注释
        if p.curToken.Type == lexer.COMMENT {
            p.nextToken()
            continue
        }
        stmt, err := p.parseStatement()
        if err != nil {
            return nil, err
        }
        if stmt != nil {
            program.Statements = append(program.Statements, stmt)
        }
        p.nextToken()
    }

    return program, nil
}

// parseStatement 中也跳过注释
func (p *Parser) parseStatement() (ast.Statement, error) {
    // 跳过注释
    for p.curToken.Type == lexer.COMMENT {
        p.nextToken()
    }
    // ... 其他代码
}

测试

测试SQL:

sql 复制代码
CREATE TABLE users (id INT PRIMARY KEY,name TEXT,age INT); -- 建表
INSERT INTO users VALUES (1, 'Alice', 20); -- 插入用户数据
SELECT * FROM users; -- 查询数据
drop table users; -- 删除表

效果:

4. 新增对 >=、<=、!= 操作符的支持

实现思路

我们需要在词法分析器中识别这些新的操作符,在解析器中处理它们,并在存储引擎中实现相应的比较逻辑。

  1. lexer/token.go中新增>=、<=、!=
  2. lexer/lexer.go词法分析器中新增对!=、>=、<=操作符的识别,将字符映射为对应的token
  3. parser/parser.go语法解析器中isBasicOperator操作符判断中新增<=、>=、!=,用于检查是否属于SQL操作符
  4. storage/memory.go存储引擎中的evaluateWhereCondition、compareValues方法中实现对>=、<=、!=的底层逻辑

代码实现

①在词法分析器中添加新操作符
go 复制代码
// internal/lexer/token.go
const (
    // ... 其他标记类型
    GTE TokenType = ">="  // >=
    LTE TokenType = "<="  // <=
    NEQ TokenType = "!="  // !=
    // ... 其他标记类型
)

// internal/lexer/lexer.go
var operatorMap = map[string]TokenType{
    "=":  EQ,
    ">":  GT,
    "<":  LT,
    ">=": GTE,
    "<=": LTE,
    "!=": NEQ,
}

func (l *Lexer) readOperator() Token {
    var op bytes.Buffer
    op.WriteRune(l.ch)

    // 检查是否为双字符操作符
    if l.peekChar() == '=' {
        op.WriteRune(l.peekChar())
        l.readChar() // 消费 '='
    }

    literal := op.String()
    if tokenType, exists := operatorMap[literal]; exists {
        l.readChar() // 消费当前字符
        return Token{Type: tokenType, Literal: literal}
    }

    // 如果不是有效的操作符,回退并返回错误
    l.readChar()
    return Token{Type: ERROR, Literal: literal}
}
②在解析器中处理新操作符
go 复制代码
// internal/parser/parser.go
// isBasicOperator 检查是否为操作符,比如 =、>、<、>=、<=、!=、like等
func (p *Parser) isBasicOperator() bool {
    return p.curTokenIs(lexer.EQ) ||
       p.curTokenIs(lexer.GT) ||
       p.curTokenIs(lexer.LT) ||
       p.curTokenIs(lexer.GTE) ||
       p.curTokenIs(lexer.LTE) ||
       p.curTokenIs(lexer.NEQ) ||
       p.curTokenIs(lexer.LIKE)
}
③在存储引擎中实现新操作符的比较逻辑
go 复制代码
// internal/storage/memory.go
// 在 evaluateWhereCondition 中处理新操作符
func evaluateWhereCondition(expr ast.Expression, row []ast.Cell, columns []ast.ColumnDefinition) (bool, error) {
    switch e := expr.(type) {
    case *ast.BinaryExpression:
        // 获取左操作数的值
        leftValue, err := getColumnValue(e.Left, row, columns)
        if err != nil {
            return false, err
        }

        // 获取右操作数的值
        rightValue, err := getColumnValue(e.Right, row, columns)
        if err != nil {
            return false, err
        }

        // 根据操作符比较值
        switch e.Operator {
        case "=":
            return compareValues(leftValue, rightValue, "=")
        case ">":
            return compareValues(leftValue, rightValue, ">")
        case "<":
            return compareValues(leftValue, rightValue, "<")
        case ">=":
            return compareValues(leftValue, rightValue, ">=")
        case "<=":
            return compareValues(leftValue, rightValue, "<=")
        case "!=":
            result, err := compareValues(leftValue, rightValue, "=")
            if err != nil {
                return false, err
            }
            return !result, nil // 返回相反的结果
        default:
            return false, fmt.Errorf("Unknown operator: '%s'", e.Operator)
        }
    // ... 其他case
    }
}

// 在 compareValues 中处理新操作符
func compareValues(left, right interface{}, operator string) (bool, error) {
    // ... 其他代码
    
    switch operator {
    case "=":
        return isEqual(left, right)
    case ">":
        return isGreater(left, right)
    case "<":
        return isLess(left, right)
    case ">=":
        equal, _ := isEqual(left, right)
        greater, _ := isGreater(left, right)
        return equal || greater, nil
    case "<=":
        equal, _ := isEqual(left, right)
        less, _ := isLess(left, right)
        return equal || less, nil
    case "!=":
        equal, err := isEqual(left, right)
        if err != nil {
            return false, err
        }
        return !equal, nil
    default:
        return false, fmt.Errorf("Unknown operator: '%s'", operator)
    }
}

测试

测试SQL:

sql 复制代码
CREATE TABLE users (id INT PRIMARY KEY,name TEXT,age INT);

INSERT INTO users VALUES (1, 'Alice', 20);
INSERT INTO users VALUES (2, 'Bob', 25);
INSERT INTO users VALUES (3, 'Charlie', 30);

select name, age from users where age >= 24;
select name, age from users where age <= 20;
select name, age from users where name != 'Charlie';

drop table users;

效果:

相关推荐
九皇叔叔6 分钟前
【7】SQL 语句基础应用
数据库·sql·mysql
阿拉丁的梦8 分钟前
教程1:用vscode->ptvsd-创建和调试一个UI(python)-转载官方翻译(有修正)
开发语言·python
木宇(记得热爱生活)16 分钟前
一键搭建开发环境:制作bash shell脚本
开发语言·bash
Cisyam^23 分钟前
Go环境搭建实战:告别Java环境配置的复杂
java·开发语言·golang
IAR Systems1 小时前
在IAR Embedded Workbench for Arm中实现Infineon TRAVEO™ T2G安全调试
开发语言·arm开发·安全·嵌入式软件开发·iar
jayzhang_2 小时前
SPARK入门
大数据·开发语言
蹦极的考拉2 小时前
网站日志里面老是出现{pboot:if((\x22file_put_co\x22.\x22ntents\x22)(\x22temp.php\x22.....
android·开发语言·php
麦聪聊数据2 小时前
能源行业数据库远程运维安全合规实践:Web化平台的落地经验
运维·数据库·sql·安全·数据服务
fured2 小时前
[调试][实现][原理]用Golang实现建议断点调试器
开发语言·后端·golang
chenglin0163 小时前
阿里云——云存储与数据库服务
数据库·阿里云·云计算