MiniSQL — 纯 Java SQL 引擎
一个从零实现的轻量级关系型数据库引擎——词法分析→语法分析→查询执行→存储管理全链路手工实现,零外部依赖。
📋 项目概览
技术栈
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)
手写的 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)
递归下降语法分析器,将 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)
查询执行引擎,接收 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.java | SQL 词法分析器 | 114 |
src/minisql/sql/tokenizer/Token.java | Token 数据类型 | 18 |
src/minisql/sql/tokenizer/TokenType.java | Token 类型枚举 | 33 |
src/minisql/sql/parser/Parser.java | 递归下降语法分析器 | 170 |
src/minisql/sql/parser/ASTNode.java | AST 语法树节点定义 | 92 |
src/minisql/sql/engine/Engine.java | 查询执行引擎 | 177 |
src/minisql/sql/engine/WhereEvaluator.java | WHERE 条件求值器 | 60 |
src/minisql/sql/storage/StorageManager.java | 页式文件存储管理器 | 210 |
src/minisql/sql/storage/RowSerializer.java | 行数据序列化 | 95 |
src/minisql/MiniSQL.java | 主入口 & REPL | 85 |
src/minisql/web/MiniQLServer.java | 内嵌 HTTP Server | 120 |
src/minisql/type/Value.java | 值类型系统 | 56 |