feat: SQL 语法支持 BETWEEN AND、DROP TABLE IF EXISTS
CI / test (18.x) (push) Waiting to run
CI / test (20.x) (push) Waiting to run
CI / test (22.x) (push) Waiting to run
CI / test (24.x) (push) Waiting to run

This commit is contained in:
thzxx
2026-07-26 17:55:45 +08:00
parent 287f2a2b22
commit 1f8e11bf72
14 changed files with 181 additions and 13 deletions
+40 -1
View File
@@ -1614,6 +1614,11 @@ class QueryExecutor {
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
} }
async executeDropTable(stmt) { async executeDropTable(stmt) {
if (stmt.ifExists) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
return; // IF EXISTS: 表不存在时静默返回
}
return this.engine.dropTable(stmt.name); return this.engine.dropTable(stmt.name);
} }
getEngine() { return this.engine; } getEngine() { return this.engine; }
@@ -1766,6 +1771,9 @@ var TokenType;
TokenType["TRUE"] = "TRUE"; TokenType["TRUE"] = "TRUE";
TokenType["REFERENCES"] = "REFERENCES"; TokenType["REFERENCES"] = "REFERENCES";
TokenType["CASCADE"] = "CASCADE"; TokenType["CASCADE"] = "CASCADE";
TokenType["BETWEEN"] = "BETWEEN";
TokenType["IF"] = "IF";
TokenType["EXISTS"] = "EXISTS";
TokenType["FALSE"] = "FALSE"; TokenType["FALSE"] = "FALSE";
// JOIN 相关 // JOIN 相关
TokenType["INNER"] = "INNER"; TokenType["INNER"] = "INNER";
@@ -1842,6 +1850,9 @@ const KEYWORDS = {
'FALSE': TokenType.FALSE, 'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES, 'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE, 'CASCADE': TokenType.CASCADE,
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
// JOIN // JOIN
'INNER': TokenType.INNER, 'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT, 'LEFT': TokenType.LEFT,
@@ -2451,8 +2462,15 @@ class Parser {
parseDropTable() { parseDropTable() {
this.expect(TokenType.DROP); this.expect(TokenType.DROP);
this.expect(TokenType.TABLE); this.expect(TokenType.TABLE);
// IF EXISTS(可选)
let ifExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.EXISTS);
ifExists = true;
}
const tableName = this.expectIdentifier('table name'); const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName }; return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
} }
// =================================================================== // ===================================================================
// 条件表达式 // 条件表达式
@@ -2503,6 +2521,27 @@ class Parser {
result[column] = isNot ? { $ne: null } : { $eq: null }; result[column] = isNot ? { $ne: null } : { $eq: null };
return result; return result;
} }
// BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.BETWEEN)) {
this.nextToken();
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $gte: low, $lte: high };
return result;
}
// NOT BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.NOT) && this.peekTokenIs(TokenType.BETWEEN)) {
this.nextToken(); // skip NOT
this.nextToken(); // skip BETWEEN
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $not: { $gte: low, $lte: high } };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN // NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) { if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) { if (this.peekTokenIs(TokenType.IN)) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -228,6 +228,8 @@ interface CreateTableStatement {
interface DropTableStatement { interface DropTableStatement {
type: 'DROP_TABLE'; type: 'DROP_TABLE';
name: string; name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
} }
interface InsertStatement { interface InsertStatement {
type: 'INSERT'; type: 'INSERT';
@@ -696,6 +698,9 @@ declare enum TokenType {
TRUE = "TRUE", TRUE = "TRUE",
REFERENCES = "REFERENCES", REFERENCES = "REFERENCES",
CASCADE = "CASCADE", CASCADE = "CASCADE",
BETWEEN = "BETWEEN",
IF = "IF",
EXISTS = "EXISTS",
FALSE = "FALSE", FALSE = "FALSE",
INNER = "INNER", INNER = "INNER",
LEFT = "LEFT", LEFT = "LEFT",
+40 -1
View File
@@ -1610,6 +1610,11 @@ class QueryExecutor {
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
} }
async executeDropTable(stmt) { async executeDropTable(stmt) {
if (stmt.ifExists) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
return; // IF EXISTS: 表不存在时静默返回
}
return this.engine.dropTable(stmt.name); return this.engine.dropTable(stmt.name);
} }
getEngine() { return this.engine; } getEngine() { return this.engine; }
@@ -1762,6 +1767,9 @@ var TokenType;
TokenType["TRUE"] = "TRUE"; TokenType["TRUE"] = "TRUE";
TokenType["REFERENCES"] = "REFERENCES"; TokenType["REFERENCES"] = "REFERENCES";
TokenType["CASCADE"] = "CASCADE"; TokenType["CASCADE"] = "CASCADE";
TokenType["BETWEEN"] = "BETWEEN";
TokenType["IF"] = "IF";
TokenType["EXISTS"] = "EXISTS";
TokenType["FALSE"] = "FALSE"; TokenType["FALSE"] = "FALSE";
// JOIN 相关 // JOIN 相关
TokenType["INNER"] = "INNER"; TokenType["INNER"] = "INNER";
@@ -1838,6 +1846,9 @@ const KEYWORDS = {
'FALSE': TokenType.FALSE, 'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES, 'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE, 'CASCADE': TokenType.CASCADE,
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
// JOIN // JOIN
'INNER': TokenType.INNER, 'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT, 'LEFT': TokenType.LEFT,
@@ -2447,8 +2458,15 @@ class Parser {
parseDropTable() { parseDropTable() {
this.expect(TokenType.DROP); this.expect(TokenType.DROP);
this.expect(TokenType.TABLE); this.expect(TokenType.TABLE);
// IF EXISTS(可选)
let ifExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.EXISTS);
ifExists = true;
}
const tableName = this.expectIdentifier('table name'); const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName }; return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
} }
// =================================================================== // ===================================================================
// 条件表达式 // 条件表达式
@@ -2499,6 +2517,27 @@ class Parser {
result[column] = isNot ? { $ne: null } : { $eq: null }; result[column] = isNot ? { $ne: null } : { $eq: null };
return result; return result;
} }
// BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.BETWEEN)) {
this.nextToken();
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $gte: low, $lte: high };
return result;
}
// NOT BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.NOT) && this.peekTokenIs(TokenType.BETWEEN)) {
this.nextToken(); // skip NOT
this.nextToken(); // skip BETWEEN
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $not: { $gte: low, $lte: high } };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN // NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) { if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) { if (this.peekTokenIs(TokenType.IN)) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+40 -1
View File
@@ -1616,6 +1616,11 @@
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
} }
async executeDropTable(stmt) { async executeDropTable(stmt) {
if (stmt.ifExists) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
return; // IF EXISTS: 表不存在时静默返回
}
return this.engine.dropTable(stmt.name); return this.engine.dropTable(stmt.name);
} }
getEngine() { return this.engine; } getEngine() { return this.engine; }
@@ -1768,6 +1773,9 @@
TokenType["TRUE"] = "TRUE"; TokenType["TRUE"] = "TRUE";
TokenType["REFERENCES"] = "REFERENCES"; TokenType["REFERENCES"] = "REFERENCES";
TokenType["CASCADE"] = "CASCADE"; TokenType["CASCADE"] = "CASCADE";
TokenType["BETWEEN"] = "BETWEEN";
TokenType["IF"] = "IF";
TokenType["EXISTS"] = "EXISTS";
TokenType["FALSE"] = "FALSE"; TokenType["FALSE"] = "FALSE";
// JOIN 相关 // JOIN 相关
TokenType["INNER"] = "INNER"; TokenType["INNER"] = "INNER";
@@ -1844,6 +1852,9 @@
'FALSE': TokenType.FALSE, 'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES, 'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE, 'CASCADE': TokenType.CASCADE,
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
// JOIN // JOIN
'INNER': TokenType.INNER, 'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT, 'LEFT': TokenType.LEFT,
@@ -2453,8 +2464,15 @@
parseDropTable() { parseDropTable() {
this.expect(TokenType.DROP); this.expect(TokenType.DROP);
this.expect(TokenType.TABLE); this.expect(TokenType.TABLE);
// IF EXISTS(可选)
let ifExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.EXISTS);
ifExists = true;
}
const tableName = this.expectIdentifier('table name'); const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName }; return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
} }
// =================================================================== // ===================================================================
// 条件表达式 // 条件表达式
@@ -2505,6 +2523,27 @@
result[column] = isNot ? { $ne: null } : { $eq: null }; result[column] = isNot ? { $ne: null } : { $eq: null };
return result; return result;
} }
// BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.BETWEEN)) {
this.nextToken();
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $gte: low, $lte: high };
return result;
}
// NOT BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.NOT) && this.peekTokenIs(TokenType.BETWEEN)) {
this.nextToken(); // skip NOT
this.nextToken(); // skip BETWEEN
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $not: { $gte: low, $lte: high } };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN // NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) { if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) { if (this.peekTokenIs(TokenType.IN)) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -422,7 +422,7 @@ FROM users u INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.name GROUP BY u.name
HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders);`, HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders);`,
cascade: `-- 外键级联:重建 orders 表带 CASCADE cascade: `-- 外键级联:重建 orders 表带 CASCADE
DROP TABLE orders; DROP TABLE IF EXISTS orders;
CREATE TABLE orders ( CREATE TABLE orders (
id STRING PRIMARY KEY, id STRING PRIMARY KEY,
@@ -447,9 +447,9 @@ DELETE FROM users WHERE id = '1';
-- 确认级联结果 -- 确认级联结果
SELECT u.name, o.product, o.amount SELECT u.name, o.product, o.amount
FROM users u LEFT JOIN orders o ON u.id = o.user_id;`, FROM users u LEFT JOIN orders o ON u.id = o.user_id;`,
adv: `-- 复杂条件:年龄 20-30 且不是 Bob adv: `-- BETWEEN 范围查询
SELECT * FROM users SELECT * FROM users
WHERE age >= 20 AND age <= 30 AND name != 'Bob' WHERE age BETWEEN 20 AND 30 AND name != 'Bob'
ORDER BY age DESC; ORDER BY age DESC;
-- DISTINCT 去重 -- DISTINCT 去重
+2
View File
@@ -103,6 +103,8 @@ export interface CreateTableStatement {
export interface DropTableStatement { export interface DropTableStatement {
type: 'DROP_TABLE'; type: 'DROP_TABLE';
name: string; name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+4
View File
@@ -234,6 +234,10 @@ export class QueryExecutor {
} }
private async executeDropTable(stmt: DropTableStatement): Promise<void> { private async executeDropTable(stmt: DropTableStatement): Promise<void> {
if (stmt.ifExists) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists) return; // IF EXISTS: 表不存在时静默返回
}
return this.engine.dropTable(stmt.name); return this.engine.dropTable(stmt.name);
} }
+31 -1
View File
@@ -423,8 +423,15 @@ export class Parser {
private parseDropTable(): DropTableStatement { private parseDropTable(): DropTableStatement {
this.expect(TokenType.DROP); this.expect(TokenType.DROP);
this.expect(TokenType.TABLE); this.expect(TokenType.TABLE);
// IF EXISTS(可选)
let ifExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.EXISTS);
ifExists = true;
}
const tableName = this.expectIdentifier('table name'); const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName }; return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
} }
// =================================================================== // ===================================================================
@@ -483,6 +490,29 @@ export class Parser {
return result; return result;
} }
// BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.BETWEEN)) {
this.nextToken();
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result: WhereCondition = {};
result[column] = { $gte: low, $lte: high };
return result;
}
// NOT BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.NOT) && this.peekTokenIs(TokenType.BETWEEN)) {
this.nextToken(); // skip NOT
this.nextToken(); // skip BETWEEN
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result: WhereCondition = {};
result[column] = { $not: { $gte: low, $lte: high } };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN // NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) { if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) { if (this.peekTokenIs(TokenType.IN)) {
+6
View File
@@ -40,6 +40,9 @@ export enum TokenType {
TRUE = 'TRUE', TRUE = 'TRUE',
REFERENCES = 'REFERENCES', REFERENCES = 'REFERENCES',
CASCADE = 'CASCADE', CASCADE = 'CASCADE',
BETWEEN = 'BETWEEN',
IF = 'IF',
EXISTS = 'EXISTS',
FALSE = 'FALSE', FALSE = 'FALSE',
// JOIN 相关 // JOIN 相关
@@ -133,6 +136,9 @@ export const KEYWORDS: Record<string, TokenType> = {
'FALSE': TokenType.FALSE, 'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES, 'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE, 'CASCADE': TokenType.CASCADE,
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
// JOIN // JOIN
'INNER': TokenType.INNER, 'INNER': TokenType.INNER,
+6 -2
View File
@@ -130,8 +130,12 @@ describe('Parser 边缘场景', () => {
expect(ast.columns[3]).toMatchObject({ name: 'active', default: true }); expect(ast.columns[3]).toMatchObject({ name: 'active', default: true });
}); });
it('支持 IF EXISTS 语法(表名被解析为 IF', () => { it('支持 DROP TABLE IF EXISTS 语法', () => {
// 解析器把 IF 当作表名,EXISTS 作为后续 token 导致错误 const ast = parse('DROP TABLE IF EXISTS users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users', ifExists: true });
});
it('DROP TABLE 不带 IF EXISTS', () => {
const ast = parse('DROP TABLE users'); const ast = parse('DROP TABLE users');
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' }); expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' });
}); });