[手写系列]Go手写db — — 第五版(实现数据库操作模块)
[手写系列]Go手写db — — 第五版(实现数据库操作模块)
第一版文章:[手写系列]Go手写db — — 完整教程_go手写数据库-CSDN博客
第二版文章:[手写系列]Go手写db — — 第二版-CSDN博客
第三版文章:[手写系列]Go手写db — — 第三版(实现分组、排序、聚合函数等)-CSDN博客
第四版文章:[手写系列]Go手写db — — 第四版(实现事务、网络模块)
整体项目Github地址:https://github.com/ziyifast/ZiyiDB
- 请大家多多支持,也欢迎大家star⭐️和共同维护这个项目~
本文主要介绍如何在 ZiyiDB 第四版的基础上,实现数据库层面的操作,包括创建、删除、切换数据库以及数据库与表的关联管理等,通过这些功能,将使得ZiyiDB成为一个更完整的数据库系统。
一、功能列表
- 新增对数据库的创建(CREATE DATABASE)支持
- 新增对数据库的删除(DROP DATABASE)支持
- 新增对数据库列表的展示(SHOW DATABASES)支持
- 新增对表列表的展示(SHOW TABELS)支持
- 新增对数据库切换(USE DATABASE)支持
- 实现数据库与表的关联管理。存储引擎操作数据库,然后由数据库结构体间接操作表
二、实现细节
功能点一:实现数据库的创建、删除
实现思路
- internal/lexer/token.go新增DATABASE关键字,因为drop和create关键字之前已经有了
- internal/lexer/lexer.go中的lookupIdentifier方法新增一个case返回,用于将用户输入的字符转换为TokenType
- internal/ast/ast.go语法树中新增create database和drop database语法树结构
- internal/parser/parser.go语法解析器中的create和drop case中新增对database的处理
然后分别实现parseCreateDatabaseStatement、parseDropDatabaseStatement方法,方便后续构建抽象语法树
- internal/storage/memory.go存储引擎
- 新增数据库结构体的定义,同时存储引擎直接管理数据库,数据表交给Database结构体管理
- 实现底层对数据库创建、删除的方法
- cmd/main.go中executor方法新增case处理对用户对数据库创建、删除的操作
PS:network/server.go网络服务端修改同理,这里不做赘述
代码实现
1. 词法分析器调整
internal/lexer/token.go:
// internal/lexer/token.go
package lexer// TokenType 表示词法单元类型
type TokenType stringconst (...DATABASE TokenType = "DATABASE"...
)// Token 词法单元
// Type:标记的类型(如 SELECT、IDENT 等)
// Literal:标记的实际值(如具体的列名、数字等)
type Token struct {Type TokenType // 标记类型Literal string // 标记的实际值
}
internal/lexer/lexer.go:
// internal/lexer/lexer.go
package lexerimport ("bufio""bytes""io""strings""time""unicode"
)...// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "DATABASE":return DATABASE...default:return IDENT}
}...
2. 抽象语法树调整
internal/ast/ast.go:
...// CreateDatabaseStatement 表示CREATE DATABASE语句type CreateDatabaseStatement struct {Token lexer.TokenName string}func (cds *CreateDatabaseStatement) statementNode() {}func (cds *CreateDatabaseStatement) TokenLiteral() string { return cds.Token.Literal }// DropDatabaseStatementtype DropDatabaseStatement struct {Token lexer.TokenName string}func (dds *DropDatabaseStatement) statementNode() {}func (dds *DropDatabaseStatement) TokenLiteral() string { return dds.Token.Literal }...
3. 语法解析器调整
// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {//需要区分是创建表还是创建数据库case lexer.CREATE:if p.peekTokenIs(lexer.TABLE) {return p.parseCreateTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseCreateDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after CREATE")...case lexer.DROP:if p.peekTokenIs(lexer.TABLE) {return p.parseDropTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseDropDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after DROP")case lexer.SEMI:return nil, nildefault: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.Type)}
}// parseDropDatabaseStatement 解析DROP DATABASE语句
func (p *Parser) parseDropDatabaseStatement() (*ast.DropDatabaseStatement, error) {stmt := &ast.DropDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASE) {return nil, fmt.Errorf("expected DATABASE keyword")}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}// parseCreateDatabaseStatement 解析CREATE DATABASE语句
func (p *Parser) parseCreateDatabaseStatement() (*ast.CreateDatabaseStatement, error) {stmt := &ast.CreateDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASE) {return nil, fmt.Errorf("expected DATABASE keyword")}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}
4. 存储引擎实现操作
// Database 表示数据库
type Database struct {Name stringTables map[string]*Tablemu sync.RWMutex
}// MemoryBackend 内存存储引擎,管理所有数据库
type MemoryBackend struct {Databases map[string]*DatabasetxnMgr *TransactionManagerMu sync.RWMutex
}// Table 数据表,包含列定义、数据行和索引
type Table struct {Name stringColumns []ast.ColumnDefinitionRows [][]VersionedCell // 保持为 VersionedCellIndexes map[string]*IndexRowLocks map[int]*sync.RWMutex // 行级锁mu sync.RWMutex
}...// CreateDatabase 创建数据库
func (b *MemoryBackend) CreateDatabase(stmt *ast.CreateDatabaseStatement) error {b.Mu.Lock()defer b.Mu.Unlock()if _, exists := b.Databases[stmt.Name]; exists {return fmt.Errorf("database '%s' already exists", stmt.Name)}b.Databases[stmt.Name] = &Database{Name: stmt.Name,Tables: make(map[string]*Table),}return nil
}// DropDatabase 删除数据库
func (b *MemoryBackend) DropDatabase(stmt *ast.DropDatabaseStatement) error {b.Mu.Lock()defer b.Mu.Unlock()if _, exists := b.Databases[stmt.Name]; !exists {return fmt.Errorf("database '%s' does not exist", stmt.Name)}delete(b.Databases, stmt.Name)return nil
}
5. 程序入口处调整
需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)
- cmd/main.go:
func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {case *ast.CreateDatabaseStatement:if err := backend.CreateDatabase(s); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Println("Database created successfully")}case *ast.DropDatabaseStatement:if err := backend.DropDatabase(s); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Println("Database dropped successfully")}...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
- network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {//检查是否选择了数据库if connCtx.GetDBName() == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {result += "No database selected. Use 'USE database_name' to select a database."continue}}switch stmt := statement.(type) {case *ast.CreateDatabaseStatement:if err := s.backend.CreateDatabase(stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Database created successfully\n"}case *ast.DropDatabaseStatement:if err := s.backend.DropDatabase(stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Database dropped successfully\n"}case *ast.DropTableStatement:if err := s.backend.DropTable(connCtx.db, stmt); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += "Table dropped successfully\n"}default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}
测试
测试命令:
-- 创建test数据库
create database test;
-- 删除test数据库
drop database test;
效果:
功能点二:use选择数据库
实现思路
-
新增internal/context/context.go文件,定义DBContext接口
-
internal/lexer/token.go新增USE关键字:
-
internal/lexer/lexer.go中lookupIdentifier新增case:
-
抽象语法树internal/ast/ast.go新增UseDatabaseStatement
-
语法解析器internal/parser/parser.go的parseStatement新增case,并实现对应case逻辑
-
存储引擎实现UseDatabase选择数据库逻辑
-
网络服务端、程序入口调用UseDatabase方法
代码实现
1. 词法分析器调整
- internal/lexer/token.go:
const(USE TokenType = "USE"...
)
...
- internal/lexer/lexer.go:
...
// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "USE":return USEdefault:return IDENT}
}
2. 抽象语法树调整
internal/ast/ast.go:
...
type UseDatabaseStatement struct {Token lexer.TokenName string
}func (uds *UseDatabaseStatement) statementNode() {}
func (uds *UseDatabaseStatement) TokenLiteral() string { return uds.Token.Literal }
3. 语法解析器调整
internal/parser/parser.go:
// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {...case lexer.USE:return p.parseUseDatabaseStatement()...default: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.Type)}
}func (p *Parser) parseUseDatabaseStatement() (*ast.UseDatabaseStatement, error) {stmt := &ast.UseDatabaseStatement{Token: p.curToken}if !p.expectPeek(lexer.IDENT) {return nil, fmt.Errorf("expected database name")}stmt.Name = p.curToken.Literalreturn stmt, nil
}...
4. 存储引擎实现操作
internal/storage/memory.go:
// UseDatabase 使用数据库
func (b *MemoryBackend) UseDatabase(stmt *ast.UseDatabaseStatement, connCtx context.DBContext) error {b.Mu.RLock()defer b.Mu.RUnlock()if _, exists := b.Databases[stmt.Name]; !exists {return fmt.Errorf("database '%s' does not exist", stmt.Name)}// 更新连接上下文中的当前数据库connCtx.SetDBName(stmt.Name)return nil
}
5. 程序入口处调整
需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)
- cmd/main.go:
func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isDropDB {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {...case *ast.UseDatabaseStatement:if err := backend.UseDatabase(s, &dbContextAdapter{¤tDatabase}); err != nil {fmt.Printf("Error: %v\n", err)} else {fmt.Printf("Database changed to '%s'\n", currentDatabase)}...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
- network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {...switch stmt := statement.(type) {case *ast.UseDatabaseStatement:if err := s.backend.UseDatabase(stmt, connCtx); err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += fmt.Sprintf("Database changed to '%s'\n", stmt.Name)}...default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}
测试
测试命令:
-- 创建两个数据库
create database test;
create database test2;
-- 使用数据库test并创建表
use test;
create table users (id INT PRIMARY KEY,name text,age INT);
INSERT INTO users VALUES (1, 'Alice', 20);
select * from users;
-- 预期test2数据库中没有users表
use test2;
select * from users;
效果:
功能点三:实现数据库、表列表展示
实现思路
- internal/lexer/token.go新增关键字
- internal/lexer/lexer.go lookupIdentifier方法新增一个case返回,用于将用户输入的字符转换为TokenType
- internal/ast/ast.go抽象语法树新增ShowDatabasesStatement、ShowTablesStatement,方便后续语法解析器构建抽象语法树
- internal/parser/parser.go语法解析器中实现对ShowDatabasesStatement、ShowTablesStatement抽象语法树的构建
同时在parseStatement方法中新增一个case,用于解析show相关的SQL语句
- internal/storage/memory.go存储引擎中实现show databases、show tables,底层其实就是range遍历,然后将结果放在一个切片中
- 程序入口处调整,新增对应的case,判断到对应Statement之后,通过存储引擎调用对应方法即可
- cmd/main.go:
- network/server.go:
代码实现
1. 词法分析器调整
- internal/lexer/token.go:
const(...DATABASES TokenType = "DATABASES"SHOW TokenType = "SHOW"TABLES TokenType = "TABLES"
)
- internal/lexer/lexer.go:
// lookupIdentifier 查找标识符类型
// 将标识符转换为对应的标记类型
// 识别 SQL 关键字
func (l *Lexer) lookupIdentifier(ident string) TokenType {switch strings.ToUpper(ident) {...case "DATABASES":return DATABASEScase "SHOW":return SHOWcase "TABLES":return TABLESdefault:return IDENT}
}
...
2. 抽象语法树调整
internal/ast/ast.go:
// ShowDatabasesStatement
type ShowDatabasesStatement struct {Token lexer.Token
}func (sds *ShowDatabasesStatement) statementNode() {}
func (sds *ShowDatabasesStatement) TokenLiteral() string { return sds.Token.Literal }// ShowTablesStatement
type ShowTablesStatement struct {Token lexer.Token
}func (sds *ShowTablesStatement) statementNode() {}
func (sds *ShowTablesStatement) TokenLiteral() string { return sds.Token.Literal }
...
3. 语法解析器调整
internal/parser/parser.go:
// parseStatement 解析语句
// 根据当前标记类型选择相应的解析方法
func (p *Parser) parseStatement() (ast.Statement, error) {// 跳过注释for p.curToken.Type == lexer.COMMENT {p.nextToken()}switch p.curToken.Type {//需要区分是创建表还是创建数据库case lexer.CREATE:if p.peekTokenIs(lexer.TABLE) {return p.parseCreateTableStatement()} else if p.peekTokenIs(lexer.DATABASE) {return p.parseCreateDatabaseStatement()}return nil, fmt.Errorf("expected TABLE or DATABASE after CREATE")case lexer.SHOW:if p.peekTokenIs(lexer.DATABASES) {return p.parseShowDatabasesStatement()}if p.peekTokenIs(lexer.TABLES) {return p.parseShowTablesStatement()}return nil, fmt.Errorf("expected DATABASES after SHOW")...default: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.Type)}
}// parseShowDatabasesStatement 解析SHOW DATABASES语句
func (p *Parser) parseShowDatabasesStatement() (*ast.ShowDatabasesStatement, error) {stmt := &ast.ShowDatabasesStatement{Token: p.curToken}if !p.expectPeek(lexer.DATABASES) {return nil, fmt.Errorf("expected DATABASES keyword")}return stmt, nil
}// parseShowTablesStatement 解析SHOW TABLES语句
func (p *Parser) parseShowTablesStatement() (*ast.ShowTablesStatement, error) {stmt := &ast.ShowTablesStatement{Token: p.curToken}if !p.expectPeek(lexer.TABLES) {return nil, fmt.Errorf("expected TABLES keyword")}return stmt, nil
}...
4. 存储引擎实现操作
internal/storage/memory.go:
// ShowDatabases 显示所有数据库
func (b *MemoryBackend) ShowDatabases() *Results {b.Mu.RLock()defer b.Mu.RUnlock()results := &Results{Columns: []ResultColumn{{Name: "Database", Type: "TEXT"},},Rows: make([][]Cell, 0),}for dbName := range b.Databases {results.Rows = append(results.Rows, []Cell{{Type: CellTypeText, TextValue: dbName},})}// 按名称排序sort.Slice(results.Rows, func(i, j int) bool {return results.Rows[i][0].TextValue < results.Rows[j][0].TextValue})return results
}// ShowTables 显示数据库中的所有表
func (b *MemoryBackend) ShowTables(connCtx context.DBContext) *Results {b.Mu.RLock()defer b.Mu.RUnlock()results := &Results{Columns: []ResultColumn{{Name: "Tables", Type: "TEXT"},},Rows: make([][]Cell, 0),}dbName := connCtx.GetDBName()if dbName == "" {return results}database := b.Databases[dbName]for tableName := range database.Tables {results.Rows = append(results.Rows, []Cell{{Type: CellTypeText, TextValue: tableName},})}// 按名称排序sort.Slice(results.Rows, func(i, j int) bool {return results.Rows[i][0].TextValue < results.Rows[j][0].TextValue})return results
}...
5. 程序入口处调整
需要调整两个地方,一个是本地命令行版(cmd/main.go),一个是网络版(network/server.go)
- cmd/main.go:
func executor(t string) {// 分割多个SQL语句(用分号分隔)statements := strings.Split(t, ";")for _, stmt := range statements {...// 创建词法分析器l := lexer.NewLexer(strings.NewReader(stmt))// 创建语法分析器p := parser.NewParser(l)// 解析SQL语句parsedStmt, err := p.ParseProgram()if err != nil {fmt.Printf("Parse error: %v\n", err)continue}// 执行SQL语句for _, statement := range parsedStmt.Statements {if currentDatabase == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isShowDBs := statement.(*ast.ShowDatabasesStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)_, isUseDB := statement.(*ast.UseDatabaseStatement)_, isShowTables := statement.(*ast.ShowTablesStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isShowDBs && !isDropDB && !isUseDB && !isShowTables {fmt.Println("No database selected. Use 'USE database_name' to select a database.")continue}}switch s := statement.(type) {case *ast.ShowDatabasesStatement:result := backend.ShowDatabases()printResults(result)case *ast.ShowTablesStatement:result := backend.ShowTables(&dbContextAdapter{¤tDatabase})printResults(result)...default:fmt.Printf("Unsupported statement type: %T\n", s)}}}
}
- network/server.go:
// network/server.go
package networkimport ("bufio""fmt""net""strings""sync""ziyi.db.com/internal/ast""ziyi.db.com/internal/lexer""ziyi.db.com/internal/parser""ziyi.db.com/internal/storage"
)const DefaultPort = "3118"...func (s *Server) executeCommand(conn net.Conn, command string) string {...var result stringfor _, statement := range parsedStmt.Statements {//检查是否选择了数据库if connCtx.GetDBName() == "" {// 检查是否是非数据库操作语句_, isCreateDB := statement.(*ast.CreateDatabaseStatement)_, isShowDBs := statement.(*ast.ShowDatabasesStatement)_, isDropDB := statement.(*ast.DropDatabaseStatement)_, isUseDB := statement.(*ast.UseDatabaseStatement)_, isShowTables := statement.(*ast.ShowTablesStatement)// 如果不是允许的语句类型,则提示需要选择数据库if !isCreateDB && !isShowDBs && !isDropDB && !isUseDB && !isShowTables {result += "No database selected. Use 'USE database_name' to select a database."continue}}switch stmt := statement.(type) {case *ast.ShowDatabasesStatement:results := s.backend.ShowDatabases()if err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += formatResults(results) + "\n"}case *ast.ShowTablesStatement:results := s.backend.ShowTables(connCtx)if err != nil {result += fmt.Sprintf("Error: %v\n", err)} else {result += formatResults(results) + "\n"}...default:result += fmt.Sprintf("Unsupported statement type: %T\n", stmt)}}return strings.TrimSpace(result)
}
测试
测试命令:
-- 测试show databases:
create database test;
create database test2;
show databases;-- 测试show tables:
use test2;
create table users (id INT PRIMARY KEY,name text,age INT);
create table products (id INT PRIMARY KEY,name text,price FLOAT);
show tables;
效果: