feat: SQL 语法支持 BETWEEN AND、DROP TABLE IF EXISTS
This commit is contained in:
Vendored
+40
-1
@@ -1614,6 +1614,11 @@ class QueryExecutor {
|
||||
return this.engine.createTable(createSchema(stmt.name, columns));
|
||||
}
|
||||
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);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
@@ -1766,6 +1771,9 @@ var TokenType;
|
||||
TokenType["TRUE"] = "TRUE";
|
||||
TokenType["REFERENCES"] = "REFERENCES";
|
||||
TokenType["CASCADE"] = "CASCADE";
|
||||
TokenType["BETWEEN"] = "BETWEEN";
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
@@ -1842,6 +1850,9 @@ const KEYWORDS = {
|
||||
'FALSE': TokenType.FALSE,
|
||||
'REFERENCES': TokenType.REFERENCES,
|
||||
'CASCADE': TokenType.CASCADE,
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -2451,8 +2462,15 @@ class Parser {
|
||||
parseDropTable() {
|
||||
this.expect(TokenType.DROP);
|
||||
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');
|
||||
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 };
|
||||
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 IN(NOT 后紧跟 LIKE 或 IN)
|
||||
if (this.curTokenIs(TokenType.NOT)) {
|
||||
if (this.peekTokenIs(TokenType.IN)) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5
@@ -228,6 +228,8 @@ interface CreateTableStatement {
|
||||
interface DropTableStatement {
|
||||
type: 'DROP_TABLE';
|
||||
name: string;
|
||||
/** IF EXISTS — 表不存在时不报错 */
|
||||
ifExists?: boolean;
|
||||
}
|
||||
interface InsertStatement {
|
||||
type: 'INSERT';
|
||||
@@ -696,6 +698,9 @@ declare enum TokenType {
|
||||
TRUE = "TRUE",
|
||||
REFERENCES = "REFERENCES",
|
||||
CASCADE = "CASCADE",
|
||||
BETWEEN = "BETWEEN",
|
||||
IF = "IF",
|
||||
EXISTS = "EXISTS",
|
||||
FALSE = "FALSE",
|
||||
INNER = "INNER",
|
||||
LEFT = "LEFT",
|
||||
|
||||
Vendored
+40
-1
@@ -1610,6 +1610,11 @@ class QueryExecutor {
|
||||
return this.engine.createTable(createSchema(stmt.name, columns));
|
||||
}
|
||||
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);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
@@ -1762,6 +1767,9 @@ var TokenType;
|
||||
TokenType["TRUE"] = "TRUE";
|
||||
TokenType["REFERENCES"] = "REFERENCES";
|
||||
TokenType["CASCADE"] = "CASCADE";
|
||||
TokenType["BETWEEN"] = "BETWEEN";
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
@@ -1838,6 +1846,9 @@ const KEYWORDS = {
|
||||
'FALSE': TokenType.FALSE,
|
||||
'REFERENCES': TokenType.REFERENCES,
|
||||
'CASCADE': TokenType.CASCADE,
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -2447,8 +2458,15 @@ class Parser {
|
||||
parseDropTable() {
|
||||
this.expect(TokenType.DROP);
|
||||
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');
|
||||
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 };
|
||||
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 IN(NOT 后紧跟 LIKE 或 IN)
|
||||
if (this.curTokenIs(TokenType.NOT)) {
|
||||
if (this.peekTokenIs(TokenType.IN)) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+40
-1
@@ -1616,6 +1616,11 @@
|
||||
return this.engine.createTable(createSchema(stmt.name, columns));
|
||||
}
|
||||
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);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
@@ -1768,6 +1773,9 @@
|
||||
TokenType["TRUE"] = "TRUE";
|
||||
TokenType["REFERENCES"] = "REFERENCES";
|
||||
TokenType["CASCADE"] = "CASCADE";
|
||||
TokenType["BETWEEN"] = "BETWEEN";
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
@@ -1844,6 +1852,9 @@
|
||||
'FALSE': TokenType.FALSE,
|
||||
'REFERENCES': TokenType.REFERENCES,
|
||||
'CASCADE': TokenType.CASCADE,
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -2453,8 +2464,15 @@
|
||||
parseDropTable() {
|
||||
this.expect(TokenType.DROP);
|
||||
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');
|
||||
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 };
|
||||
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 IN(NOT 后紧跟 LIKE 或 IN)
|
||||
if (this.curTokenIs(TokenType.NOT)) {
|
||||
if (this.peekTokenIs(TokenType.IN)) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -422,7 +422,7 @@ FROM users u INNER JOIN orders o ON u.id = o.user_id
|
||||
GROUP BY u.name
|
||||
HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders);`,
|
||||
cascade: `-- 外键级联:重建 orders 表带 CASCADE
|
||||
DROP TABLE orders;
|
||||
DROP TABLE IF EXISTS orders;
|
||||
|
||||
CREATE TABLE orders (
|
||||
id STRING PRIMARY KEY,
|
||||
@@ -447,9 +447,9 @@ DELETE FROM users WHERE id = '1';
|
||||
-- 确认级联结果
|
||||
SELECT u.name, o.product, o.amount
|
||||
FROM users u LEFT JOIN orders o ON u.id = o.user_id;`,
|
||||
adv: `-- 复杂条件:年龄 20-30 且不是 Bob
|
||||
adv: `-- BETWEEN 范围查询
|
||||
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;
|
||||
|
||||
-- DISTINCT 去重
|
||||
|
||||
@@ -103,6 +103,8 @@ export interface CreateTableStatement {
|
||||
export interface DropTableStatement {
|
||||
type: 'DROP_TABLE';
|
||||
name: string;
|
||||
/** IF EXISTS — 表不存在时不报错 */
|
||||
ifExists?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -234,6 +234,10 @@ export class QueryExecutor {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -423,8 +423,15 @@ export class Parser {
|
||||
private parseDropTable(): DropTableStatement {
|
||||
this.expect(TokenType.DROP);
|
||||
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');
|
||||
return { type: 'DROP_TABLE', name: tableName };
|
||||
return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -483,6 +490,29 @@ export class Parser {
|
||||
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 IN(NOT 后紧跟 LIKE 或 IN)
|
||||
if (this.curTokenIs(TokenType.NOT)) {
|
||||
if (this.peekTokenIs(TokenType.IN)) {
|
||||
|
||||
@@ -40,6 +40,9 @@ export enum TokenType {
|
||||
TRUE = 'TRUE',
|
||||
REFERENCES = 'REFERENCES',
|
||||
CASCADE = 'CASCADE',
|
||||
BETWEEN = 'BETWEEN',
|
||||
IF = 'IF',
|
||||
EXISTS = 'EXISTS',
|
||||
FALSE = 'FALSE',
|
||||
|
||||
// JOIN 相关
|
||||
@@ -133,6 +136,9 @@ export const KEYWORDS: Record<string, TokenType> = {
|
||||
'FALSE': TokenType.FALSE,
|
||||
'REFERENCES': TokenType.REFERENCES,
|
||||
'CASCADE': TokenType.CASCADE,
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
|
||||
@@ -130,8 +130,12 @@ describe('Parser 边缘场景', () => {
|
||||
expect(ast.columns[3]).toMatchObject({ name: 'active', default: true });
|
||||
});
|
||||
|
||||
it('不支持 IF EXISTS 语法(表名被解析为 IF)', () => {
|
||||
// 解析器把 IF 当作表名,EXISTS 作为后续 token 导致错误
|
||||
it('支持 DROP TABLE IF EXISTS 语法', () => {
|
||||
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');
|
||||
expect(ast).toMatchObject({ type: 'DROP_TABLE', name: 'users' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user