← 项目列表
课程已完成2026年6月

MiniSQL — 纯 Java SQL 引擎

一个从零实现的轻量级关系型数据库引擎——词法分析→语法分析→查询执行→存储管理全链路手工实现,零外部依赖。

JavaSQL数据库编译器Java纯 JDK无外部依赖
📋 项目概览
技术栈
Java纯 JDK无外部依赖
功能特性
  • 手写词法分析器(Tokenizer),支持 SQL 关键字、字符串、整数、注释
  • 递归下降语法分析器(Parser),构建 AST 语法树
  • 查询执行引擎(Engine),支持 SELECT/INSERT/DELETE/CREATE TABLE
  • 自定义页式文件存储 + B+ 树索引
  • WAL 日志 + 基础事务支持
  • 内嵌 HTTP Server Web UI
📖 技术分析报告在 GitHub 查看 ↗

MiniSQL — 纯 Java 自研 SQL 引擎

项目概述

MiniSQL 是一个从零实现的关系型数据库引擎,覆盖 SQL 处理的完整链路:词法分析 → 语法分析 → 查询执行 → 存储管理,零外部依赖,仅使用纯 JDK 标准库。

架构设计

                            ┌─────────────┐
   SQL String ─────────────►│  Tokenizer   │──► Token 流
                            └─────────────┘
                                     ▼
                            ┌─────────────┐
                            │   Parser     │──► AST 语法树
                            └─────────────┘
                                     ▼
                            ┌─────────────┐
                            │   Engine     │──► 查询执行
                            └─────────────┘
                                     ▼
                            ┌─────────────┐
                            │   Storage    │──► 页式文件
                            └─────────────┘

模块说明

模块 路径 职责
Tokenizer sql/tokenizer/ 字符串→Token 流。支持关键字、字符串、整数、注释(行/块)
Parser sql/parser/ Token 流→AST。递归下降解析,支持 CREATE/INSERT/SELECT/DELETE
Engine sql/engine/ AST→执行。表扫描、条件过滤、投影、类型强制转换
Storage sql/storage/ 页式文件存储、行序列化、B+ 树索引
Web UI web/ 内嵌 HTTP Server,浏览器内交互式 SQL 执行

技术亮点

1. 手写词法分析器

Tokenizer 从零手写,不依赖任何解析器生成器:

  • 支持 -- 行注释和 /* */ 块注释
  • 关键字识别(CREATE, TABLE, INSERT, SELECT, DELETE 等)
  • 字符串字面量处理(含转义字符)
  • 整数、标识符、操作符的精确定位

2. 递归下降解析器

Parser 使用标准的递归下降解析模式:

  • 每个语法产生式对应一个方法(parseCreateTable, parseInsert 等)
  • 支持 AND/OR 逻辑表达式的优先级解析
  • 错误位置精确定位

3. 内嵌 HTTP 服务器

无需 Tomcat 或 Spring Boot,通过 com.sun.net.httpserver.HttpServer 内嵌 Web 服务器:

  • 浏览器中输入 SQL → 服务端执行 → 表格化结果返回
  • 完整的 RESTful 接口设计

设计决策

决策 方案 理由
零外部依赖 纯 JDK 教学项目,避免配置复杂性
存储格式 定长页式 简化磁盘管理,便于理解数据库内核
输出格式 Unicode 框线表格 终端可读性好,支持管道重定向
Web 接口 内嵌 HTTP Server 零部署成本,开箱即用

构建与运行

# 编译
javac -d out src/minisql/**/*.java

# 运行 REPL
java -cp out minisql.MiniSQL

# 启动 Web UI
java -cp out minisql.web.MiniQLServer
# 浏览器打开 http://localhost:8080

与网站演示的关系

网站上的 MiniSQL 在线演示 是一个纯前端的 SQL 模拟器,实现了与 MiniSQL 兼容的 SQL 语法和交互体验。后端 Java 引擎的完整源码在本目录的 src/ 下。

🚀 在线演示新窗口打开 ↗

浏览器内 SQL 引擎(纯 JS 实现,模拟 MiniSQL 语法和交互风格)

💻 核心代码Java · 纯 JDK · 无外部依赖
java词法分析器(Tokenizer)Tokenizer.java · 89 行

手写的 SQL 词法分析器,将原始 SQL 字符串拆分为 Token 流。支持关键字识别、字符串字面量、整数、注释跳过。

package minisql.sql.tokenizer;

public class Tokenizer {
    private final String input;
    private int pos;
    private List<Token> tokens;

    public Tokenizer(String input) { this.input = input; this.pos = 0; }

    public List<Token> tokenize() {
        tokens = new ArrayList<>();
        while (pos < input.length()) {
            char c = input.charAt(pos);
            if (Character.isWhitespace(c)) { pos++; continue; }

            // Skip line comments --
            if (c == '-' && pos + 1 < input.length() && input.charAt(pos + 1) == '-') {
                while (pos < input.length() && input.charAt(pos) != '\n') pos++;
                continue;
            }
            // Skip block comments /* */
            if (c == '/' && pos + 1 < input.length() && input.charAt(pos + 1) == '*') {
                pos += 2;
                while (pos + 1 < input.length() && !(input.charAt(pos) == '*' && input.charAt(pos + 1) == '/')) pos++;
                pos += 2;
                continue;
            }

            // Single-char tokens
            switch (c) {
                case '*': add(TokenType.STAR, "*"); continue;
                case ',': add(TokenType.COMMA, ","); continue;
                case ';': add(TokenType.SEMICOLON, ";"); continue;
                case '(': add(TokenType.LPAREN, "("); continue;
                case ')': add(TokenType.RPAREN, ")"); continue;
                case '=': add(TokenType.EQ, "="); continue;
            }

            // Literals & identifiers
            if (c == '\'') { readString(); continue; }
            if (Character.isDigit(c)) { readInteger(); continue; }
            if (Character.isLetter(c) || c == '_') { readIdentifier(); continue; }

            add(TokenType.UNKNOWN, String.valueOf(c));
        }
        add(TokenType.EOF, "");
        return tokens;
    }

    private void readString() {
        int start = pos; pos++;
        StringBuilder sb = new StringBuilder();
        while (pos < input.length() && input.charAt(pos) != '\'') {
            if (input.charAt(pos) == '\\') { pos++; if (pos < input.length()) sb.append(input.charAt(pos)); }
            else sb.append(input.charAt(pos));
            pos++;
        }
        if (pos < input.length()) pos++;
        tokens.add(new Token(TokenType.STRING_LITERAL, sb.toString(), start));
    }

    private void readIdentifier() {
        int start = pos;
        while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) pos++;
        String word = input.substring(start, pos);
        TokenType keyword = keywordOf(word);
        tokens.add(new Token(keyword, word, start));
    }

    private TokenType keywordOf(String word) {
        switch (word.toUpperCase()) {
            case "CREATE": return TokenType.CREATE;
            case "TABLE": return TokenType.TABLE;
            case "INSERT": return TokenType.INSERT;
            case "INTO": return TokenType.INTO;
            case "VALUES": return TokenType.VALUES;
            case "SELECT": return TokenType.SELECT;
            case "FROM": return TokenType.FROM;
            case "WHERE": return TokenType.WHERE;
            case "DELETE": return TokenType.DELETE;
            case "AND": return TokenType.AND;
            case "OR": return TokenType.OR;
            case "NOT": return TokenType.NOT;
            case "INT": return TokenType.INT;
            case "STRING": return TokenType.STRING;
            default: return TokenType.IDENTIFIER;
        }
    }
}
java语法分析器(Parser)Parser.java · 89 行

递归下降语法分析器,将 Token 流解析为 AST 节点。支持 CREATE TABLE、INSERT、SELECT、DELETE 四种语句。

public class Parser {
    private final List<Token> tokens;
    private int pos;

    public Parser(List<Token> tokens) { this.tokens = tokens; this.pos = 0; }

    public ASTNode parse() {
        Token t = peek();
        switch (t.type) {
            case CREATE: return parseCreateTable();
            case INSERT: return parseInsert();
            case SELECT: return parseSelect();
            case DELETE: return parseDelete();
            default: throw new RuntimeException("Unexpected token: " + t.type);
        }
    }

    // CREATE TABLE name (col1 TYPE, col2 TYPE, ...)
    private ASTNode.CreateTableStmt parseCreateTable() {
        consume(TokenType.CREATE); consume(TokenType.TABLE);
        String name = consume(TokenType.IDENTIFIER).lexeme;
        consume(TokenType.LPAREN);
        List<ASTNode.ColumnDef> columns = new ArrayList<>();
        while (!check(TokenType.RPAREN)) {
            String colName = consume(TokenType.IDENTIFIER).lexeme;
            TokenType typeTok = consumeAny(TokenType.INT, TokenType.STRING).type;
            Value.Type colType = typeTok == TokenType.INT ? Value.Type.INT : Value.Type.STRING;
            columns.add(new ASTNode.ColumnDef(colName, colType));
            if (check(TokenType.COMMA)) consume(TokenType.COMMA);
        }
        consume(TokenType.RPAREN);
        return new ASTNode.CreateTableStmt(name, columns);
    }

    // INSERT INTO name VALUES (v1, v2), ...
    private ASTNode.InsertStmt parseInsert() {
        consume(TokenType.INSERT); consume(TokenType.INTO);
        String name = consume(TokenType.IDENTIFIER).lexeme;
        consume(TokenType.VALUES);
        List<List<Value>> rows = new ArrayList<>();
        do {
            consume(TokenType.LPAREN);
            List<Value> row = new ArrayList<>();
            while (!check(TokenType.RPAREN)) {
                if (check(TokenType.INTEGER_LITERAL))
                    row.add(new Value(Integer.parseInt(consume(TokenType.INTEGER_LITERAL).lexeme)));
                else if (check(TokenType.STRING_LITERAL))
                    row.add(new Value(consume(TokenType.STRING_LITERAL).lexeme));
                if (check(TokenType.COMMA)) consume(TokenType.COMMA);
            }
            consume(TokenType.RPAREN);
            rows.add(row);
        } while (check(TokenType.COMMA));
        return new ASTNode.InsertStmt(name, rows);
    }

    // SELECT col1, ... FROM name WHERE expr
    private ASTNode.SelectStmt parseSelect() {
        consume(TokenType.SELECT);
        List<String> columns = new ArrayList<>();
        if (check(TokenType.STAR)) { consume(TokenType.STAR); columns.add("*"); }
        else {
            columns.add(consume(TokenType.IDENTIFIER).lexeme);
            while (check(TokenType.COMMA)) {
                consume(TokenType.COMMA);
                columns.add(consume(TokenType.IDENTIFIER).lexeme);
            }
        }
        consume(TokenType.FROM);
        String name = consume(TokenType.IDENTIFIER).lexeme;
        ASTNode.Expr where = check(TokenType.WHERE) ? (consume(TokenType.WHERE), parseExpr()) : null;
        return new ASTNode.SelectStmt(columns, name, where);
    }

    // Expression parsing: expr → AND-expr (OR AND-expr)*
    private ASTNode.Expr parseExpr() {
        ASTNode.Expr left = parseAnd();
        while (check(TokenType.OR)) { consume(TokenType.OR);
            left = new ASTNode.Expr.BinOp("OR", left, parseAnd()); }
        return left;
    }

    private ASTNode.Expr parseAnd() {
        ASTNode.Expr left = parseComparison();
        while (check(TokenType.AND)) { consume(TokenType.AND);
            left = new ASTNode.Expr.BinOp("AND", left, parseComparison()); }
        return left;
    }
}
java查询执行引擎(Engine)Engine.java · 66 行

查询执行引擎,接收 AST 节点并执行对应的数据库操作。支持类型强制转换、表格格式化输出。

public class Engine {
    private final StorageManager storage;

    public String execute(ASTNode stmt) {
        if (stmt instanceof ASTNode.CreateTableStmt) return execCreate((ASTNode.CreateTableStmt) stmt);
        if (stmt instanceof ASTNode.InsertStmt) return execInsert((ASTNode.InsertStmt) stmt);
        if (stmt instanceof ASTNode.SelectStmt) return execSelect((ASTNode.SelectStmt) stmt);
        if (stmt instanceof ASTNode.DeleteStmt) return execDelete((ASTNode.DeleteStmt) stmt);
        return "Unknown statement";
    }

    private String execSelect(ASTNode.SelectStmt stmt) {
        if (!storage.tableExists(stmt.tableName))
            return "✗ Table '" + stmt.tableName + "' not found";
        List<ASTNode.ColumnDef> schema = storage.getSchema(stmt.tableName);
        List<List<Value>> allRows = storage.scanTable(stmt.tableName);

        // Determine columns to show
        List<String> colNames = (stmt.columns.size() == 1 && stmt.columns.get(0).equals("*"))
            ? schema.stream().map(c -> c.name).toList()
            : stmt.columns;

        // Build column index
        Map<String, Integer> colIndex = new HashMap<>();
        for (int i = 0; i < schema.size(); i++) colIndex.put(schema.get(i).name, i);

        // Filter & project
        List<List<String>> resultRows = new ArrayList<>();
        for (List<Value> row : allRows) {
            Map<String, Value> rowMap = new HashMap<>();
            for (int i = 0; i < schema.size(); i++) rowMap.put(schema.get(i).name, row.get(i));

            if (!new WhereEvaluator(rowMap).evaluate(stmt.where)) continue;

            List<String> projected = new ArrayList<>();
            for (String col : colNames)
                projected.add(colIndex.containsKey(col) ? row.get(colIndex.get(col)).asString() : "NULL");
            resultRows.add(projected);
        }
        return formatTable(colNames, resultRows, resultRows.size() + " row(s)");
    }

    // Unicode box-drawing table output
    private String formatTable(List<String> headers, List<List<String>> rows, String footer) {
        if (rows.isEmpty()) return "  (empty)";
        int[] widths = new int[headers.size()];
        for (int i = 0; i < headers.size(); i++) {
            widths[i] = headers.get(i).length();
            for (List<String> row : rows)
                widths[i] = Math.max(widths[i], i < row.size() ? row.get(i).length() : 0);
            widths[i] = Math.min(widths[i], 40);
        }
        StringBuilder sb = new StringBuilder();
        sb.append("┌"); for (int i = 0; i < headers.size(); i++) {
            if (i > 0) sb.append("┬");
            for (int j = 0; j < widths[i] + 2; j++) sb.append("─");
        } sb.append("┐\n");
        // ... rows with box drawing ...
        sb.append("└"); for (int i = 0; i < headers.size(); i++) {
            if (i > 0) sb.append("┴");
            for (int j = 0; j < widths[i] + 2; j++) sb.append("─");
        } sb.append("┘\n");
        sb.append(footer);
        return sb.toString();
    }
}
📁 源文件清单GitHub 仓库 ↗
路径说明行数
src/minisql/sql/tokenizer/Tokenizer.javaSQL 词法分析器114
src/minisql/sql/tokenizer/Token.javaToken 数据类型18
src/minisql/sql/tokenizer/TokenType.javaToken 类型枚举33
src/minisql/sql/parser/Parser.java递归下降语法分析器170
src/minisql/sql/parser/ASTNode.javaAST 语法树节点定义92
src/minisql/sql/engine/Engine.java查询执行引擎177
src/minisql/sql/engine/WhereEvaluator.javaWHERE 条件求值器60
src/minisql/sql/storage/StorageManager.java页式文件存储管理器210
src/minisql/sql/storage/RowSerializer.java行数据序列化95
src/minisql/MiniSQL.java主入口 & REPL85
src/minisql/web/MiniQLServer.java内嵌 HTTP Server120
src/minisql/type/Value.java值类型系统56
网站智能助手
💬 和我聊聊
🤖
你好呀 👋 有什么想聊的?