每项都先用可执行探针复现出**错误的实际输出**,再修根因、补永久回归套件
(tests/v080-query-layer.test.ts,8 项 × 4 引擎 + 跨引擎项,共 111 断言)。
A22 GROUP BY 引用 SELECT 别名
修复前:`SELECT g AS grp, COUNT(*) FROM t GROUP BY grp` 抛
`COLUMN_NOT_FOUND Unknown column "g" in SELECT list` —— 错误信息与真正原因
(GROUP BY 用了别名)无关,因为 grp 取到 undefined 使全表并成一组,
投影阶段又发现 g 不在输出行里。
修复:GROUP BY 项先解析回**基列**(别名 → 源表达式)再分组,输出键与
"按基列分组"完全一致。
A23 HAVING 引用未出现在 SELECT 里的聚合
修复前:`SELECT g FROM t GROUP BY g HAVING SUM(n) > 25` → `[]`
(SUM(n) 从未被求值 → HAVING 的键在分组行里不存在 → UNKNOWN)。
修复:需要计算的聚合 = SELECT 列 ∪ HAVING 中的聚合(并集),
并在 HAVING **之后**才把行收缩为 SELECT 输出键(否则又变回 [];
顺序错了会双向出错:先投影 → 空结果,不投影 → 泄漏内部聚合列)。
A25 带表前缀的聚合参数恒 0
修复前:`COUNT(t.n)` → 0、`SUM(t.n)` → null(行键是 n,直接取 row['t.n']
得 undefined 再被"过滤 NULL"剔除,**且不报错**)。
修复:新增唯一列引用解析 resolveColumnValue(前缀剥离 → 精确 → 唯一后缀),
聚合识别统一为 parseAggregateExpression —— 此前"是否聚合"与"如何求值"
用两条不同的正则。取不到列改为抛 COLUMN_NOT_FOUND,不再静默计 0。
A26 UNION 尾部 ORDER BY/LIMIT 归属错误
修复前:`A UNION B ORDER BY id DESC` 只排 B;`... LIMIT 3` 返回 4 行
(parser 把子句挂在右侧 SELECT 上,AST 没有复合查询级字段)。
修复:SelectUnionStatement 增加 orderBy/limit/offset,parser 把子句**上移**
(移动而非复制,否则 LIMIT 应用两次),executor 在合并+去重后统一排序/切片。
A27 DISTINCT 作用在投影前
修复前:`SELECT DISTINCT g AS d FROM t` 返回 4 行 a,a,b,b
(对 {id,g,n} 原始行去重),而 `SELECT DISTINCT g` 返回 2 行。
修复:DISTINCT 移到投影后(作用于输出列);ORDER BY 的应用时机随之拆成
"引用输出列 → 投影后" / "引用非输出列 → 投影前",两者互为因果必须一起改。
A29 maxRowsPerQuery 静默截断写入
修复前:maxRowsPerQuery=2 时 `INSERT INTO dst SELECT id FROM src`(4 行源)
只写 2 行并报成功 —— 不是"限制查询规模"而是**静默丢数据**。
修复:行源不截断(executeSelect 增加 purpose='source'),写路径显式报错。
A30 INSERT 值多于目标列静默丢弃
修复前:`INSERT INTO t (id,g) VALUES ('9','z','LOST')` 报成功、'LOST' 消失。
修复:显式列名时解析期拦截(PARSE_ERROR),未给列名时 executor 对照 schema
拦截(VALIDATION_ERROR)——两种情况都需要,因为前者无需 schema。
A36 派生表别名引用
修复前:`SELECT d.id FROM (SELECT id, g FROM t) AS d` 返回 `[]`,
而同义的 `SELECT id FROM (...) AS d` 正确。
修复:抽出 normalizeUnprefixedReferences(WHERE/ORDER BY/GROUP BY/SELECT
四类引用统一剥离别名前缀),非 JOIN 单表路径与派生表路径共用同一规则。
连带根治(修复过程中发现的两个更底层问题):
1. **同步抛错穿过 async 边界**:`executor.execute()` 里 `return this.executeXxx(stmt)`
的同步前导段若抛错(arity/校验),异常成为**同步抛出** ——
`await expect(db.query(...)).rejects...` 的断言不生效、`.catch()` 永不执行。
现统一包一层 try/catch,保证任何错误都是 rejected promise。
2. **缺列的行形状不一致**:validateRow 此前"值为 undefined 就不落键",
于是 `INSERT INTO t (id,g) VALUES ('9','z')` 的行里没有 n 键 →
`SELECT id,g,n FROM t` 抛 COLUMN_NOT_FOUND: n,而 `SELECT * FROM t` 正常。
现在缺列且无 default → 显式补 null(SQL 语义),行始终含全部 schema 列;
ALTER ADD 同步在已有行上物化 null,使"内存视图"与"重启后视图"一致。
验证:全量 83 套件 / 1589 测试通过(含 Aria 5 万行索引竞态、KVStore 持久化);
typecheck(src+tests) 与 lint 零错误。
1456 lines
50 KiB
TypeScript
1456 lines
50 KiB
TypeScript
/**
|
||
* metona-sqlark SQL Parser — 递归下降语法分析器
|
||
* @module sql/parser
|
||
*
|
||
* Token 流 → AST Statement。
|
||
* 支持的语法是标准 SQL 的子集。
|
||
*/
|
||
|
||
import { Lexer } from './lexer';
|
||
import { TokenType, type Token } from './tokens';
|
||
import type {
|
||
Statement,
|
||
SelectStatement,
|
||
SelectUnionStatement,
|
||
InsertStatement,
|
||
UpdateStatement,
|
||
DeleteStatement,
|
||
CreateTableStatement,
|
||
DropTableStatement,
|
||
AlterTableStatement,
|
||
TruncateTableStatement,
|
||
CreateIndexStatement,
|
||
DropIndexStatement,
|
||
BeginTransactionStatement,
|
||
CommitTransactionStatement,
|
||
ASTColumnDef,
|
||
} from '../query/ast';
|
||
import type { WhereCondition, FieldCondition, OrderBy, SortDirection } from '../constants';
|
||
import { DatabaseError } from '../constants';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Parser
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export class Parser {
|
||
private lexer: Lexer;
|
||
private curToken!: Token;
|
||
private peekToken!: Token;
|
||
private sql: string;
|
||
|
||
constructor(sql: string) {
|
||
this.sql = sql;
|
||
this.lexer = new Lexer(sql);
|
||
// 预读两个 token
|
||
this.nextToken();
|
||
this.nextToken();
|
||
}
|
||
|
||
/** 解析完整 SQL 语句 */
|
||
parseStatement(): Statement {
|
||
switch (this.curToken.type) {
|
||
case TokenType.SELECT:
|
||
return this.parseSelect();
|
||
case TokenType.INSERT:
|
||
return this.parseInsert();
|
||
case TokenType.UPDATE:
|
||
return this.parseUpdate();
|
||
case TokenType.DELETE:
|
||
return this.parseDelete();
|
||
case TokenType.CREATE:
|
||
return this.parseCreateStatement();
|
||
case TokenType.DROP:
|
||
return this.parseDropStatement();
|
||
case TokenType.ALTER:
|
||
return this.parseAlterTable();
|
||
case TokenType.TRUNCATE:
|
||
return this.parseTruncateTable();
|
||
case TokenType.BEGIN:
|
||
return this.parseBegin();
|
||
case TokenType.COMMIT:
|
||
return this.parseCommit();
|
||
case TokenType.ROLLBACK:
|
||
return this.parseRollback();
|
||
// v0.5.1: 维护语句入口
|
||
case TokenType.EXPLAIN:
|
||
return this.parseExplain();
|
||
case TokenType.ANALYZE:
|
||
return this.parseAnalyze();
|
||
case TokenType.REINDEX:
|
||
return this.parseReindex();
|
||
case TokenType.VACUUM:
|
||
return this.parseVacuum();
|
||
case TokenType.SAVEPOINT:
|
||
return this.parseSavepoint();
|
||
case TokenType.RELEASE:
|
||
return this.parseSavepoint();
|
||
default:
|
||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||
}
|
||
}
|
||
|
||
/** 解析所有语句(分号分隔的多语句支持) */
|
||
parseAllStatements(): Statement[] {
|
||
const statements: Statement[] = [];
|
||
while (!this.curTokenIs(TokenType.EOF)) {
|
||
// 跳过多余的分号
|
||
while (this.curTokenIs(TokenType.SEMICOLON)) this.nextToken();
|
||
if (this.curTokenIs(TokenType.EOF)) break;
|
||
statements.push(this.parseStatement());
|
||
// 语句后应紧跟分号或 EOF
|
||
if (this.curTokenIs(TokenType.SEMICOLON)) {
|
||
this.nextToken();
|
||
} else if (!this.curTokenIs(TokenType.EOF)) {
|
||
throw this.error(`Expected ';' after statement, got "${this.curToken.value}"`);
|
||
}
|
||
}
|
||
return statements;
|
||
}
|
||
|
||
// ---- 维护语句(v0.5.1) ----
|
||
|
||
/** EXPLAIN <statement> — 输出查询计划 */
|
||
private parseExplain(): import('../query/ast').ExplainStatement {
|
||
this.expect(TokenType.EXPLAIN);
|
||
if (this.curTokenIs(TokenType.EXPLAIN)) {
|
||
throw this.error('Nested EXPLAIN is not allowed');
|
||
}
|
||
const query = this.parseStatement();
|
||
return { type: 'EXPLAIN', query };
|
||
}
|
||
|
||
/** ANALYZE [TABLE] name — 收集表统计信息 */
|
||
private parseAnalyze(): import('../query/ast').AnalyzeStatement {
|
||
this.expect(TokenType.ANALYZE);
|
||
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TABLE') {
|
||
this.nextToken();
|
||
}
|
||
return { type: 'ANALYZE', table: this.expectIdentifier('table name') };
|
||
}
|
||
|
||
/** REINDEX [TABLE] name — 重建表二级索引 */
|
||
private parseReindex(): import('../query/ast').ReindexStatement {
|
||
this.expect(TokenType.REINDEX);
|
||
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TABLE') {
|
||
this.nextToken();
|
||
}
|
||
return { type: 'REINDEX', table: this.expectIdentifier('table name') };
|
||
}
|
||
|
||
/** VACUUM — 压缩 LSM + 清理碎片 */
|
||
private parseVacuum(): import('../query/ast').VacuumStatement {
|
||
this.expect(TokenType.VACUUM);
|
||
return { type: 'VACUUM' };
|
||
}
|
||
|
||
/** SAVEPOINT name | RELEASE [SAVEPOINT] name */
|
||
private parseSavepoint(): import('../query/ast').SavepointStatement {
|
||
let action: 'SAVE' | 'ROLLBACK' | 'RELEASE';
|
||
if (this.curTokenIs(TokenType.RELEASE)) {
|
||
action = 'RELEASE';
|
||
this.nextToken();
|
||
} else {
|
||
action = 'SAVE';
|
||
this.expect(TokenType.SAVEPOINT);
|
||
}
|
||
// 可选 SAVEPOINT 关键字(RELEASE SAVEPOINT name)
|
||
if (this.curTokenIs(TokenType.SAVEPOINT)) this.nextToken();
|
||
return { type: 'SAVEPOINT', name: this.expectIdentifier('savepoint name'), action };
|
||
}
|
||
|
||
// ---- 事务语句 ----
|
||
|
||
private parseBegin(): BeginTransactionStatement {
|
||
this.expect(TokenType.BEGIN);
|
||
// 可选 TRANSACTION 关键字
|
||
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
|
||
this.nextToken();
|
||
}
|
||
return { type: 'BEGIN' };
|
||
}
|
||
|
||
private parseCommit(): CommitTransactionStatement {
|
||
this.expect(TokenType.COMMIT);
|
||
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
|
||
this.nextToken();
|
||
}
|
||
return { type: 'COMMIT' };
|
||
}
|
||
|
||
/** ROLLBACK [TRANSACTION] | ROLLBACK TO [SAVEPOINT] name(v0.5.1) */
|
||
private parseRollback(): Statement {
|
||
this.expect(TokenType.ROLLBACK);
|
||
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
|
||
this.nextToken();
|
||
return { type: 'ROLLBACK' };
|
||
}
|
||
// ROLLBACK TO [SAVEPOINT] name
|
||
if (this.curTokenIs(TokenType.TO) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TO')) {
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.SAVEPOINT) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'SAVEPOINT')) {
|
||
this.nextToken();
|
||
}
|
||
return { type: 'SAVEPOINT', name: this.expectIdentifier('savepoint name'), action: 'ROLLBACK' };
|
||
}
|
||
return { type: 'ROLLBACK' };
|
||
}
|
||
|
||
// ---- CREATE TABLE / CREATE INDEX ----
|
||
|
||
private parseCreateStatement(): Statement {
|
||
this.expect(TokenType.CREATE);
|
||
if (this.curTokenIs(TokenType.TABLE)) {
|
||
return this.parseCreateTable();
|
||
}
|
||
if (this.curTokenIs(TokenType.INDEX) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
|
||
return this.parseCreateIndex();
|
||
}
|
||
if (this.curTokenIs(TokenType.UNIQUE)) {
|
||
// CREATE UNIQUE INDEX
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.INDEX) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
|
||
const stmt = this.parseCreateIndex();
|
||
stmt.unique = true;
|
||
return stmt;
|
||
}
|
||
}
|
||
throw this.error(`Expected TABLE or INDEX after CREATE, got "${this.curToken.value}"`);
|
||
}
|
||
|
||
private parseCreateIndex(): CreateIndexStatement {
|
||
this.expect(TokenType.INDEX);
|
||
const name = this.expectIdentifier('index name');
|
||
this.expect(TokenType.ON);
|
||
const table = this.expectIdentifier('table name');
|
||
this.expect(TokenType.LPAREN);
|
||
const column = this.expectIdentifier('column name');
|
||
this.expect(TokenType.RPAREN);
|
||
return { type: 'CREATE_INDEX', name, table, column };
|
||
}
|
||
|
||
// ---- DROP TABLE / DROP INDEX ----
|
||
|
||
private parseDropStatement(): Statement {
|
||
this.expect(TokenType.DROP);
|
||
if (this.curTokenIs(TokenType.TABLE)) {
|
||
return this.parseDropTable();
|
||
}
|
||
if (this.curTokenIs(TokenType.INDEX) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
|
||
return this.parseDropIndex();
|
||
}
|
||
throw this.error(`Expected TABLE or INDEX after DROP, got "${this.curToken.value}"`);
|
||
}
|
||
|
||
private parseDropIndex(): DropIndexStatement {
|
||
this.expect(TokenType.INDEX);
|
||
const name = this.expectIdentifier('index name');
|
||
// SQLite 风格:DROP INDEX idx_name [ON table]
|
||
let table = '';
|
||
let column = '';
|
||
if (this.curTokenIs(TokenType.ON)) {
|
||
this.nextToken();
|
||
table = this.expectIdentifier('table name');
|
||
if (this.curTokenIs(TokenType.LPAREN)) {
|
||
this.nextToken();
|
||
column = this.expectIdentifier('column name');
|
||
this.expect(TokenType.RPAREN);
|
||
}
|
||
}
|
||
return { type: 'DROP_INDEX', name, table, column };
|
||
}
|
||
|
||
// ===================================================================
|
||
// SELECT
|
||
// ===================================================================
|
||
|
||
private parseSelect(): SelectStatement | SelectUnionStatement {
|
||
this.expect(TokenType.SELECT);
|
||
|
||
// DISTINCT(可选)
|
||
let distinct = false;
|
||
if (this.curTokenIs(TokenType.DISTINCT)) {
|
||
distinct = true;
|
||
this.nextToken();
|
||
}
|
||
|
||
// 列
|
||
const columns: string[] = [];
|
||
if (this.curTokenIs(TokenType.STAR)) {
|
||
columns.push('*');
|
||
this.nextToken();
|
||
// v0.7.3: `SELECT *, col [AS alias], ...` —— '*' 后可继续列列表
|
||
// (此前 '*' 独占分支,逗号后直接 PARSE_ERROR;executor 侧投影已支持混合)
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
columns.push(this.parseColumnWithAlias());
|
||
}
|
||
} else {
|
||
columns.push(...this.parseColumnList());
|
||
}
|
||
|
||
// FROM(v0.4.0 可选:SELECT 1 / SELECT 'lit' 无表查询)
|
||
let fromSubquery: SelectStatement | SelectUnionStatement | undefined;
|
||
let tableName = '';
|
||
let alias: string | undefined;
|
||
if (this.curTokenIs(TokenType.FROM)) {
|
||
this.nextToken();
|
||
|
||
// v0.4.0: FROM (SELECT ...) AS alias 派生表
|
||
if (this.curTokenIs(TokenType.LPAREN)) {
|
||
this.nextToken();
|
||
fromSubquery = this.parseSelect();
|
||
this.expect(TokenType.RPAREN);
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
alias = this.expectIdentifier('alias');
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
|
||
alias = this.curToken.value;
|
||
this.nextToken();
|
||
}
|
||
} else {
|
||
tableName = this.expectIdentifier('table name');
|
||
|
||
// 表别名(可选)
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
alias = this.expectIdentifier('alias');
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
|
||
alias = this.curToken.value;
|
||
this.nextToken();
|
||
}
|
||
}
|
||
}
|
||
|
||
const stmt: SelectStatement = {
|
||
type: 'SELECT',
|
||
columns,
|
||
distinct: distinct || undefined,
|
||
from: tableName,
|
||
alias,
|
||
where: {},
|
||
};
|
||
if (fromSubquery) {
|
||
stmt.fromSubquery = fromSubquery;
|
||
}
|
||
|
||
// JOIN 子句(可选,支持多个)
|
||
const joins = this.parseJoinClauses();
|
||
if (joins.length > 0) {
|
||
stmt.joins = joins;
|
||
}
|
||
|
||
// WHERE(可选)
|
||
if (this.curTokenIs(TokenType.WHERE)) {
|
||
this.nextToken();
|
||
stmt.where = this.parseCondition();
|
||
}
|
||
|
||
// GROUP BY(可选)
|
||
if (this.curTokenIs(TokenType.GROUP)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.BY);
|
||
stmt.groupBy = this.parseIdentifierList();
|
||
}
|
||
|
||
// HAVING(可选)
|
||
if (this.curTokenIs(TokenType.HAVING)) {
|
||
this.nextToken();
|
||
stmt.having = this.parseCondition();
|
||
}
|
||
|
||
// ORDER BY(可选)
|
||
if (this.curTokenIs(TokenType.ORDER)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.BY);
|
||
stmt.orderBy = this.parseOrderByList();
|
||
}
|
||
|
||
// LIMIT(可选)
|
||
if (this.curTokenIs(TokenType.LIMIT)) {
|
||
this.nextToken();
|
||
stmt.limit = this.expectNumber('LIMIT value');
|
||
}
|
||
|
||
// OFFSET(可选)
|
||
if (this.curTokenIs(TokenType.OFFSET)) {
|
||
this.nextToken();
|
||
stmt.offset = this.expectNumber('OFFSET value');
|
||
}
|
||
|
||
// UNION / UNION ALL(可选,v0.3.0)
|
||
if (this.curTokenIs(TokenType.UNION)) {
|
||
return this.parseUnion(stmt);
|
||
}
|
||
|
||
return stmt;
|
||
}
|
||
|
||
/** 解析 UNION / UNION ALL 组合(支持链式) */
|
||
private parseUnion(left: SelectStatement | SelectUnionStatement): SelectUnionStatement {
|
||
this.expect(TokenType.UNION);
|
||
let all = false;
|
||
if (this.curTokenIs(TokenType.ALL)) {
|
||
all = true;
|
||
this.nextToken();
|
||
}
|
||
const right = this.parseSelect();
|
||
|
||
const unionStmt: SelectUnionStatement = { type: 'SELECT_UNION', left, right, all: all || undefined };
|
||
this.adoptTrailingClauses(unionStmt, right);
|
||
// 链式 UNION
|
||
if (this.curTokenIs(TokenType.UNION)) {
|
||
return this.parseUnionChain(unionStmt);
|
||
}
|
||
return unionStmt;
|
||
}
|
||
|
||
/** 链式 UNION:左侧是已组合的 UNION 语句 */
|
||
private parseUnionChain(left: SelectUnionStatement): SelectUnionStatement {
|
||
this.expect(TokenType.UNION);
|
||
let all = false;
|
||
if (this.curTokenIs(TokenType.ALL)) {
|
||
all = true;
|
||
this.nextToken();
|
||
}
|
||
const right = this.parseSelect();
|
||
const unionStmt: SelectUnionStatement = { type: 'SELECT_UNION', left, right, all: all || undefined };
|
||
this.adoptTrailingClauses(unionStmt, right);
|
||
if (this.curTokenIs(TokenType.UNION)) {
|
||
return this.parseUnionChain(unionStmt);
|
||
}
|
||
return unionStmt;
|
||
}
|
||
|
||
/**
|
||
* v0.8.0(A26):把"最后一个 SELECT 上的 ORDER BY / LIMIT / OFFSET"上移到
|
||
* 复合查询节点,并把这些子句从该 SELECT 上**移除**。
|
||
*
|
||
* 为什么必须"移动"而不是"复制":
|
||
* - 语法上它们写在最后一个 SELECT 之后,但 SQL 语义作用于整个 UNION
|
||
* (`A UNION B LIMIT 3` 是"合并去重后取前 3 行",不是"B 取前 3 行");
|
||
* - 若只复制不移除,LIMIT 会**应用两次** —— 正是 A5/A6 那类"两处都生效"
|
||
* 缺陷的同一个坑(B 先被截断,再对合并结果截断,结果可能少行)。
|
||
*
|
||
* 由于 `parseSelect` 无法预知后面有没有 UNION(它在返回后才知道),
|
||
* 只能先让它照常解析、发现 UNION 时再回收 —— 这比"预读 UNION"简单且无回溯。
|
||
*/
|
||
private adoptTrailingClauses(
|
||
unionStmt: SelectUnionStatement,
|
||
right: SelectStatement | SelectUnionStatement,
|
||
): void {
|
||
// 链式 UNION 时右侧可能已是 UNION 节点,其尾部子句在创建时已上移
|
||
if (right.type !== 'SELECT') return;
|
||
if (right.orderBy) {
|
||
unionStmt.orderBy = right.orderBy;
|
||
delete right.orderBy;
|
||
}
|
||
if (right.limit !== undefined) {
|
||
unionStmt.limit = right.limit;
|
||
delete right.limit;
|
||
}
|
||
if (right.offset !== undefined) {
|
||
unionStmt.offset = right.offset;
|
||
delete right.offset;
|
||
}
|
||
}
|
||
|
||
/** 解析 JOIN 子句列表 */
|
||
private parseJoinClauses(): import('../query/ast').JoinClause[] {
|
||
const joins: import('../query/ast').JoinClause[] = [];
|
||
while (this._isJoinKeyword()) {
|
||
joins.push(this.parseJoinClause());
|
||
}
|
||
return joins;
|
||
}
|
||
|
||
private _isJoinKeyword(): boolean {
|
||
return (
|
||
this.curTokenIs(TokenType.INNER) ||
|
||
this.curTokenIs(TokenType.LEFT) ||
|
||
this.curTokenIs(TokenType.RIGHT) ||
|
||
this.curTokenIs(TokenType.CROSS) ||
|
||
this.curTokenIs(TokenType.JOIN)
|
||
);
|
||
}
|
||
|
||
/** 解析单个 JOIN 子句 */
|
||
private parseJoinClause(): import('../query/ast').JoinClause {
|
||
let type: import('../query/ast').JoinType = 'INNER';
|
||
|
||
if (this.curTokenIs(TokenType.INNER)) {
|
||
type = 'INNER';
|
||
this.nextToken();
|
||
} else if (this.curTokenIs(TokenType.LEFT)) {
|
||
type = 'LEFT';
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.OUTER)) this.nextToken(); // 可选 OUTER
|
||
} else if (this.curTokenIs(TokenType.RIGHT)) {
|
||
type = 'RIGHT';
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.OUTER)) this.nextToken();
|
||
} else if (this.curTokenIs(TokenType.CROSS)) {
|
||
type = 'CROSS';
|
||
this.nextToken();
|
||
}
|
||
|
||
this.expect(TokenType.JOIN);
|
||
const tableName = this.expectIdentifier('table name');
|
||
|
||
// JOIN 表别名(可选)
|
||
let alias: string | undefined;
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
alias = this.expectIdentifier('alias');
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isJoinReserved()) {
|
||
alias = this.curToken.value;
|
||
this.nextToken();
|
||
}
|
||
|
||
// ON 条件(CROSS JOIN 不需要 ON)
|
||
let on = {};
|
||
if (type !== 'CROSS' && this.curTokenIs(TokenType.ON)) {
|
||
this.nextToken();
|
||
on = this.parseCondition();
|
||
}
|
||
|
||
return { type, table: tableName, alias, on };
|
||
}
|
||
|
||
/** 判断当前 token 是否为 FROM 之后的保留字 */
|
||
private _isReservedAfterFrom(): boolean {
|
||
return (
|
||
this.curTokenIs(TokenType.WHERE) ||
|
||
this.curTokenIs(TokenType.ORDER) ||
|
||
this.curTokenIs(TokenType.LIMIT) ||
|
||
this.curTokenIs(TokenType.OFFSET) ||
|
||
this.curTokenIs(TokenType.GROUP) ||
|
||
this._isJoinKeyword()
|
||
);
|
||
}
|
||
|
||
private _isJoinReserved(): boolean {
|
||
return (
|
||
this.curTokenIs(TokenType.ON) ||
|
||
this.curTokenIs(TokenType.WHERE) ||
|
||
this.curTokenIs(TokenType.ORDER) ||
|
||
this.curTokenIs(TokenType.LIMIT) ||
|
||
this._isJoinKeyword()
|
||
);
|
||
}
|
||
|
||
// ===================================================================
|
||
// INSERT
|
||
// ===================================================================
|
||
|
||
private parseInsert(): InsertStatement {
|
||
this.expect(TokenType.INSERT);
|
||
this.expect(TokenType.INTO);
|
||
|
||
const tableName = this.expectIdentifier('table name');
|
||
|
||
// 列名(可选)
|
||
let columns: string[] | undefined;
|
||
if (this.curTokenIs(TokenType.LPAREN)) {
|
||
this.nextToken();
|
||
columns = this.parseIdentifierList();
|
||
this.expect(TokenType.RPAREN);
|
||
}
|
||
|
||
// INSERT INTO ... SELECT ...(v0.3.0)
|
||
if (this.curTokenIs(TokenType.SELECT)) {
|
||
return {
|
||
type: 'INSERT',
|
||
into: tableName,
|
||
columns,
|
||
select: this.parseSelect(),
|
||
};
|
||
}
|
||
|
||
// VALUES
|
||
this.expect(TokenType.VALUES);
|
||
|
||
// 值列表
|
||
const values: unknown[][] = [];
|
||
do {
|
||
if (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
}
|
||
this.expect(TokenType.LPAREN);
|
||
const rowValues = this.parseValueList();
|
||
this.expect(TokenType.RPAREN);
|
||
values.push(rowValues);
|
||
} while (this.curTokenIs(TokenType.COMMA));
|
||
|
||
// v0.8.0 根治:显式列名时校验每行值的个数与列数一致。
|
||
//
|
||
// 此前完全不校验 arity,实测:
|
||
// INSERT INTO t (id, name) VALUES ('4','z',9) → 多余的 9 被**静默丢弃**
|
||
// INSERT INTO t VALUES ('3') → 静默写入半行(其余列缺失)
|
||
// SQLite / MySQL 都会报错。静默丢弃/截断属于"静默数据丢失",
|
||
// 必须在解析期拦下(此时无需 schema,只要有显式列名即可判断)。
|
||
//
|
||
// 未显式给列名时(INSERT INTO t VALUES (...))需要 schema 才能判断个数,
|
||
// 由 executor 在拿到 schema 后校验(见 validateInsertArity)。
|
||
if (columns) {
|
||
for (let i = 0; i < values.length; i++) {
|
||
if (values[i].length !== columns.length) {
|
||
throw this.error(
|
||
`INSERT column/value count mismatch: ${columns.length} column(s) but row ${i + 1} has ${values[i].length} value(s)`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
type: 'INSERT',
|
||
into: tableName,
|
||
columns,
|
||
values,
|
||
};
|
||
}
|
||
|
||
// ===================================================================
|
||
// UPDATE
|
||
// ===================================================================
|
||
|
||
/**
|
||
* v0.8.0: 创建**无原型**对象,用于以用户提供的列名为键的映射。
|
||
*
|
||
* 背景:`obj['__proto__'] = v` 在普通对象上会触发原型 setter 而不是新增属性,
|
||
* 于是 `UPDATE t SET __proto__ = 'x'` 的 sets 变成 `{}` —— 既没写进去、也不会被
|
||
* v0.7.4 新增的"未知列显式报错"预检看到,表现为"返回成功但什么都没发生"。
|
||
* 建表路径在 v0.7.1 已用 Object.create(null) 防护,此处补齐其余路径。
|
||
*/
|
||
private newColumnMap<T>(): Record<string, T> {
|
||
return Object.create(null) as Record<string, T>;
|
||
}
|
||
|
||
private parseUpdate(): UpdateStatement {
|
||
this.expect(TokenType.UPDATE);
|
||
const tableName = this.expectIdentifier('table name');
|
||
this.expect(TokenType.SET);
|
||
|
||
// SET col=val, ...(v0.8.0: 无原型对象,防 __proto__ 列名静默吞掉赋值)
|
||
const sets: Record<string, unknown> = this.newColumnMap<unknown>();
|
||
do {
|
||
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
|
||
const col = this.expectIdentifier('column name');
|
||
this.expect(TokenType.EQ);
|
||
sets[col] = this.parseValue();
|
||
} while (this.curTokenIs(TokenType.COMMA));
|
||
|
||
let where: WhereCondition = this.newColumnMap<unknown>() as WhereCondition;
|
||
if (this.curTokenIs(TokenType.WHERE)) {
|
||
this.nextToken();
|
||
where = this.parseCondition();
|
||
}
|
||
|
||
return { type: 'UPDATE', table: tableName, sets, where };
|
||
}
|
||
|
||
// ===================================================================
|
||
// DELETE
|
||
// ===================================================================
|
||
|
||
private parseDelete(): DeleteStatement {
|
||
this.expect(TokenType.DELETE);
|
||
this.expect(TokenType.FROM);
|
||
const tableName = this.expectIdentifier('table name');
|
||
|
||
let where: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
if (this.curTokenIs(TokenType.WHERE)) {
|
||
this.nextToken();
|
||
where = this.parseCondition();
|
||
}
|
||
|
||
return { type: 'DELETE', from: tableName, where };
|
||
}
|
||
|
||
// ===================================================================
|
||
// CREATE TABLE
|
||
// ===================================================================
|
||
|
||
private parseCreateTable(): CreateTableStatement {
|
||
this.expect(TokenType.TABLE);
|
||
|
||
// IF NOT EXISTS(可选)
|
||
let ifNotExists = false;
|
||
if (this.curTokenIs(TokenType.IF)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.NOT);
|
||
this.expect(TokenType.EXISTS);
|
||
ifNotExists = true;
|
||
}
|
||
|
||
const tableName = this.expectIdentifier('table name');
|
||
this.expect(TokenType.LPAREN);
|
||
|
||
const columns: ASTColumnDef[] = [];
|
||
do {
|
||
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
|
||
columns.push(this.parseColumnDef());
|
||
} while (this.curTokenIs(TokenType.COMMA));
|
||
|
||
this.expect(TokenType.RPAREN);
|
||
|
||
return { type: 'CREATE_TABLE', name: tableName, columns, ifNotExists: ifNotExists || undefined };
|
||
}
|
||
|
||
private parseColumnDef(): ASTColumnDef {
|
||
const name = this.expectIdentifier('column name');
|
||
const type = this.expectIdentifier('column type').toLowerCase();
|
||
|
||
const col: ASTColumnDef = { name, type };
|
||
|
||
// 修饰符
|
||
while (
|
||
this.curTokenIs(TokenType.PRIMARY) ||
|
||
this.curTokenIs(TokenType.UNIQUE) ||
|
||
this.curTokenIs(TokenType.NOT) ||
|
||
this.curTokenIs(TokenType.DEFAULT) ||
|
||
this.curTokenIs(TokenType.REFERENCES)
|
||
) {
|
||
if (this.curTokenIs(TokenType.PRIMARY)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.KEY);
|
||
col.primaryKey = true;
|
||
} else if (this.curTokenIs(TokenType.UNIQUE)) {
|
||
this.nextToken();
|
||
col.unique = true;
|
||
} else if (this.curTokenIs(TokenType.NOT)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.NULL);
|
||
col.required = true;
|
||
} else if (this.curTokenIs(TokenType.DEFAULT)) {
|
||
this.nextToken();
|
||
col.default = this.parseValue();
|
||
} else if (this.curTokenIs(TokenType.REFERENCES)) {
|
||
this.nextToken();
|
||
const refTable = this.expectIdentifier('referenced table');
|
||
this.expect(TokenType.LPAREN);
|
||
const refCol = this.expectIdentifier('referenced column');
|
||
this.expect(TokenType.RPAREN);
|
||
col.references = `${refTable}.${refCol}`;
|
||
// ON DELETE / ON UPDATE
|
||
while (this.curTokenIs(TokenType.ON)) {
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.DELETE)) {
|
||
this.nextToken();
|
||
col.onDelete = this.parseCascadeAction();
|
||
} else if (this.curTokenIs(TokenType.UPDATE)) {
|
||
this.nextToken();
|
||
col.onUpdate = this.parseCascadeAction();
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return col;
|
||
}
|
||
|
||
/** 解析 CASCADE | SET NULL | RESTRICT */
|
||
private parseCascadeAction(): 'CASCADE' | 'SET NULL' | 'RESTRICT' {
|
||
if (this.curTokenIs(TokenType.CASCADE)) {
|
||
this.nextToken();
|
||
return 'CASCADE';
|
||
}
|
||
if (this.curTokenIs(TokenType.SET)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.NULL);
|
||
return 'SET NULL';
|
||
}
|
||
// RESTRICT 或默认
|
||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
|
||
this.nextToken();
|
||
return 'RESTRICT';
|
||
}
|
||
return 'RESTRICT';
|
||
}
|
||
|
||
// ===================================================================
|
||
// ALTER TABLE
|
||
// ===================================================================
|
||
|
||
private parseAlterTable(): AlterTableStatement {
|
||
this.expect(TokenType.ALTER);
|
||
this.expect(TokenType.TABLE);
|
||
const tableName = this.expectIdentifier('table name');
|
||
|
||
// ADD COLUMN / DROP COLUMN
|
||
let action: 'ADD' | 'DROP';
|
||
if (this.curTokenIs(TokenType.ADD)) {
|
||
action = 'ADD';
|
||
this.nextToken();
|
||
// Optional COLUMN keyword
|
||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||
this.nextToken();
|
||
}
|
||
const col = this.parseColumnDef();
|
||
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
|
||
} else if (this.curTokenIs(TokenType.DROP) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
|
||
action = 'DROP';
|
||
this.nextToken();
|
||
// Optional COLUMN keyword
|
||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||
this.nextToken();
|
||
}
|
||
const colName = this.expectIdentifier('column name');
|
||
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
|
||
} else {
|
||
throw this.error('Expected ADD or DROP in ALTER TABLE');
|
||
}
|
||
}
|
||
|
||
// ===================================================================
|
||
// TRUNCATE TABLE
|
||
// ===================================================================
|
||
|
||
private parseTruncateTable(): TruncateTableStatement {
|
||
this.expect(TokenType.TRUNCATE);
|
||
this.expect(TokenType.TABLE);
|
||
const tableName = this.expectIdentifier('table name');
|
||
return { type: 'TRUNCATE_TABLE', name: tableName };
|
||
}
|
||
|
||
// ===================================================================
|
||
// DROP TABLE
|
||
// ===================================================================
|
||
|
||
private parseDropTable(): DropTableStatement {
|
||
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, ifExists: ifExists || undefined };
|
||
}
|
||
|
||
// ===================================================================
|
||
// 条件表达式
|
||
// ===================================================================
|
||
|
||
/**
|
||
* condition → or_expr
|
||
*
|
||
* v0.8.0 根治:AND 的优先级必须高于 OR(SQL 标准)。
|
||
*
|
||
* 此前实现是**纯左折叠**的单层循环:
|
||
* `a = 1 OR a = 2 AND b = 3` → `(a = 1 OR a = 2) AND b = 3` ← 错
|
||
* 标准语义应为:
|
||
* `a = 1 OR a = 2 AND b = 3` → `a = 1 OR (a = 2 AND b = 3)` ← 对
|
||
*
|
||
* 影响面:任何"权限条件 OR 业务条件 AND 软删标记"的写法都会静默返回错误行集
|
||
* (审计实测:4 行表上返回 1 行而非 3 行)。这是本层影响面最大、改动最小的缺陷。
|
||
*
|
||
* 现在按标准文法分层:or_expr → and_expr (OR and_expr)*
|
||
* and_expr → unary (AND unary)*
|
||
* unary → [NOT] primary
|
||
* 并且只在**确实有多个操作数**时才包 $and/$or,避免生成 {$and:[x]} 这种冗余节点
|
||
* (否则 `WHERE a = 1` 的结构会从 `{a:{$eq:1}}` 变成 `{$and:[{a:{$eq:1}}]}`,
|
||
* 破坏既有 AST 契约与下游引擎的索引下推识别)。
|
||
*/
|
||
private parseCondition(): WhereCondition {
|
||
return this.parseOrExpression();
|
||
}
|
||
|
||
/** or_expr → and_expr (OR and_expr)* */
|
||
private parseOrExpression(): WhereCondition {
|
||
const operands: WhereCondition[] = [this.parseAndExpression()];
|
||
while (this.curTokenIs(TokenType.OR)) {
|
||
this.nextToken();
|
||
operands.push(this.parseAndExpression());
|
||
}
|
||
return operands.length === 1 ? operands[0] : ({ $or: operands } as unknown as WhereCondition);
|
||
}
|
||
|
||
/** and_expr → simple_cond (AND simple_cond)* */
|
||
private parseAndExpression(): WhereCondition {
|
||
const operands: WhereCondition[] = [this.parseSimpleCondition()];
|
||
while (this.curTokenIs(TokenType.AND)) {
|
||
this.nextToken();
|
||
operands.push(this.parseSimpleCondition());
|
||
}
|
||
return operands.length === 1 ? operands[0] : ({ $and: operands } as unknown as WhereCondition);
|
||
}
|
||
|
||
/** 公共 WHERE 条件入口(供 CASE WHEN 求值等外部场景,v0.3.1) */
|
||
parseWhere(): WhereCondition {
|
||
return this.parseCondition();
|
||
}
|
||
|
||
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
|
||
* | column [NOT] IN (values) | NOT condition | (condition)
|
||
* | [NOT] EXISTS (SELECT ...) ← v0.3.0 */
|
||
private parseSimpleCondition(): WhereCondition {
|
||
// [NOT] EXISTS (SELECT ...)
|
||
if (this.curTokenIs(TokenType.EXISTS) ||
|
||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'EXISTS')) {
|
||
this.nextToken();
|
||
return this.parseExistsCondition(false);
|
||
}
|
||
if (this.curTokenIs(TokenType.NOT) && this._peekIsExists()) {
|
||
this.nextToken(); // 跳过 NOT
|
||
this.nextToken(); // 跳过 EXISTS
|
||
return this.parseExistsCondition(true);
|
||
}
|
||
|
||
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT)
|
||
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
|
||
this.nextToken();
|
||
const inner = this.parseSimpleCondition();
|
||
return { $not: inner } as unknown as WhereCondition;
|
||
}
|
||
|
||
// (condition)
|
||
if (this.curTokenIs(TokenType.LPAREN)) {
|
||
this.nextToken();
|
||
const inner = this.parseCondition();
|
||
this.expect(TokenType.RPAREN);
|
||
return inner;
|
||
}
|
||
|
||
// column
|
||
const column = this.parseColumnRef();
|
||
|
||
// IS NULL / IS NOT NULL
|
||
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'IS') {
|
||
this.nextToken();
|
||
const isNot = this.curTokenIs(TokenType.NOT);
|
||
if (isNot) this.nextToken();
|
||
this.expect(TokenType.NULL);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
// v0.8.0 三值语义:IS NULL / IS NOT NULL 是**谓词**,不是等值比较。
|
||
// 此前生成为 { $eq: null } / { $ne: null } —— 在 SQL 标准里
|
||
// `x = NULL` 恒为 UNKNOWN(不保留任何行),而 IS NULL 要保留 NULL 行。
|
||
// 两者语义不同,必须用不同标记(见 query/sql-compare.ts 的谓词说明)。
|
||
result[column] = isNot ? { $isNotNull: true } : { $isNull: true };
|
||
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();
|
||
// v0.8.0 根治:BETWEEN 必须是**范围**条件。
|
||
// 此前把同一个对象同时当成"操作符对象"和"操作数"传给 `$eq`
|
||
// (`{ $eq: { $gte, $lte } }`),matchOperator 的 `$eq` 收到一个对象再去比较,
|
||
// 结果只对"值恰好等于该对象"的行成立 —— 实测 `WHERE n BETWEEN 1 AND 2`
|
||
// 在 (1,2,NULL,3) 上只返回 1 行(应 2 行),静默错值。
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as 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 = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
// v0.8.0:NOT BETWEEN ≡ (x < low) OR (x > high),直接展开为字段级 `$or`。
|
||
//
|
||
// 此前生成 `{ $not: { $gte, $lte } }`。这**曾经**恒为空集:字段级 `$not`
|
||
// 把内层对象当"单个条件对象",key `$gte`/`$lte` 被当成列名去取
|
||
// `row['$gte']` → undefined → 整条恒 UNKNOWN → 取反仍 UNKNOWN → 全部排除。
|
||
//
|
||
// 求值器已修正(内层按操作符对象解释),但这里仍选择展开为 `$or`:
|
||
// - `$or` 的三值行为(含 NULL 时 UNKNOWN)是显式可读的;
|
||
// - 避免依赖"$not 作用于比较"与"$not 作用于谓词"(如 `$isNull`)的差别。
|
||
// 两条路径都有测试锁定(tests/v080-sql-three-valued.test.ts 与
|
||
// tests/sql/where-matcher.test.ts 的 `$not` 用例)。
|
||
result[column] = { $or: [{ $lt: low } as never, { $gt: high } as never] as never };
|
||
return result;
|
||
}
|
||
|
||
// NOT LIKE / NOT IN(NOT 后紧跟 LIKE 或 IN)
|
||
if (this.curTokenIs(TokenType.NOT)) {
|
||
if (this.peekTokenIs(TokenType.IN)) {
|
||
// NOT IN
|
||
this.nextToken(); // skip NOT
|
||
this.nextToken(); // skip IN
|
||
this.expect(TokenType.LPAREN);
|
||
if (this.curTokenIs(TokenType.SELECT)) {
|
||
const subquery = this.parseSelect();
|
||
this.expect(TokenType.RPAREN);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $nin: { $subquery: subquery } };
|
||
return result;
|
||
}
|
||
const values = this.parseValueList();
|
||
this.expect(TokenType.RPAREN);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $nin: values };
|
||
return result;
|
||
} else if (this.peekTokenIs(TokenType.LIKE)) {
|
||
// NOT LIKE
|
||
this.nextToken(); // skip NOT
|
||
this.nextToken(); // skip LIKE
|
||
const pattern = this.parseValue();
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $not: { $like: pattern } };
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// LIKE
|
||
if (this.curTokenIs(TokenType.LIKE)) {
|
||
this.nextToken();
|
||
const pattern = this.parseValue();
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $like: pattern };
|
||
return result;
|
||
}
|
||
|
||
// IN
|
||
if (this.curTokenIs(TokenType.IN)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.LPAREN);
|
||
// 子查询: IN (SELECT ...)
|
||
if (this.curTokenIs(TokenType.SELECT)) {
|
||
const subquery = this.parseSelect();
|
||
this.expect(TokenType.RPAREN);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $in: { $subquery: subquery } };
|
||
return result;
|
||
}
|
||
const values = this.parseValueList();
|
||
this.expect(TokenType.RPAREN);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $in: values };
|
||
return result;
|
||
}
|
||
|
||
// v0.4.1: 裸布尔列条件(WHERE done / CASE WHEN done THEN)— 列后直接是终止符时视为真值判断
|
||
if (
|
||
this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR) ||
|
||
this.curTokenIs(TokenType.RPAREN) || this.curTokenIs(TokenType.EOF) ||
|
||
(this.curToken.type === TokenType.IDENTIFIER && ['THEN', 'END', 'ELSE', 'NULLS', 'LIMIT', 'OFFSET', 'ORDER', 'GROUP', 'HAVING', 'UNION', 'WHERE'].includes(this.curToken.value.toUpperCase()))
|
||
) {
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { $eq: true };
|
||
return result;
|
||
}
|
||
|
||
// 比较运算符
|
||
const op = this.parseComparisonOp();
|
||
|
||
// 子查询: op (SELECT ...)
|
||
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
|
||
this.nextToken(); // skip (
|
||
const subquery = this.parseSelect();
|
||
this.expect(TokenType.RPAREN);
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { [op]: { $subquery: subquery } };
|
||
return result;
|
||
}
|
||
|
||
// 尝试解析列引用(identifier DOT identifier 格式)
|
||
let value: unknown;
|
||
if (
|
||
(this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
||
this.peekTokenIs(TokenType.DOT)
|
||
) {
|
||
const colRef = this.parseColumnRef();
|
||
value = { $col: colRef };
|
||
} else {
|
||
value = this.parseValue();
|
||
}
|
||
|
||
const result: WhereCondition = this.newColumnMap<FieldCondition>() as WhereCondition;
|
||
result[column] = { [op]: value };
|
||
return result;
|
||
}
|
||
|
||
/** 解析 EXISTS (SELECT ...) / NOT EXISTS (SELECT ...) */
|
||
private parseExistsCondition(negate: boolean): WhereCondition {
|
||
this.expect(TokenType.LPAREN);
|
||
const subquery = this.parseSelect();
|
||
this.expect(TokenType.RPAREN);
|
||
// $exists 键由 Executor.resolveSubqueries 解析为 boolean,where-matcher 消费
|
||
return { $exists: { $subquery: subquery, $negate: negate || undefined } } as unknown as WhereCondition;
|
||
}
|
||
|
||
/** 判断当前 NOT 后是否紧跟 EXISTS */
|
||
private _peekIsExists(): boolean {
|
||
return this.peekToken.type === TokenType.EXISTS ||
|
||
(this.peekToken.type === TokenType.IDENTIFIER && this.peekToken.value.toUpperCase() === 'EXISTS');
|
||
}
|
||
|
||
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
|
||
private _isNotInOrLike(): boolean {
|
||
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
|
||
}
|
||
|
||
private peekTokenIs(type: TokenType): boolean {
|
||
return this.peekToken.type === type;
|
||
}
|
||
|
||
private parseComparisonOp(): string {
|
||
switch (this.curToken.type) {
|
||
case TokenType.EQ: this.nextToken(); return '$eq';
|
||
case TokenType.NEQ: this.nextToken(); return '$ne';
|
||
case TokenType.GT: this.nextToken(); return '$gt';
|
||
case TokenType.GTE: this.nextToken(); return '$gte';
|
||
case TokenType.LT: this.nextToken(); return '$lt';
|
||
case TokenType.LTE: this.nextToken(); return '$lte';
|
||
default:
|
||
throw this.error(`Expected comparison operator, got "${this.curToken.value}"`);
|
||
}
|
||
}
|
||
|
||
// ===================================================================
|
||
// 辅助解析
|
||
// ===================================================================
|
||
|
||
private parseColumnList(): string[] {
|
||
const cols: string[] = [];
|
||
cols.push(this.parseColumnWithAlias());
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
cols.push(this.parseColumnWithAlias());
|
||
}
|
||
return cols;
|
||
}
|
||
|
||
/** v0.3.3: 解析列(支持 `col AS alias` 显式别名与 `col alias` 隐式别名) */
|
||
private parseColumnWithAlias(): string {
|
||
let col = this.parseColumnRef();
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
const alias = this.expectIdentifier('alias');
|
||
col = `${col} AS ${alias}`;
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom() && !this._isJoinKeyword()) {
|
||
const alias = this.curToken.value;
|
||
this.nextToken();
|
||
col = `${col} AS ${alias}`;
|
||
}
|
||
return col;
|
||
}
|
||
|
||
/** 解析列引用:支持 'col'、'table.col'、'COUNT(*)'/'SUM(col)'、数字常量列(SELECT 1)、字符串常量列(SELECT 'x',v0.4.0)和 CASE WHEN 表达式(v0.3.1) */
|
||
private parseColumnRef(): string {
|
||
// CASE WHEN 表达式(v0.3.1)
|
||
if (this.curTokenIs(TokenType.CASE)) {
|
||
return this.parseCaseExpressionText();
|
||
}
|
||
|
||
// 数字常量列:SELECT 1 FROM t(常见于 EXISTS 子查询)
|
||
// v0.8.0: 只返回常量文本,别名交给调用方 parseColumnWithAlias 处理
|
||
//(此前这里直接 return,导致 `SELECT 1 AS one` 的别名被丢弃,
|
||
// 投影时 row['1'] → undefined → 整行变成 {})。
|
||
if (this.curTokenIs(TokenType.NUMBER)) {
|
||
const value = this.curToken.value;
|
||
this.nextToken();
|
||
return value;
|
||
}
|
||
|
||
// v0.4.0: 字符串常量列:SELECT 'value' FROM t
|
||
// v0.8.0: 同样只返回字面量文本,别名由 parseColumnWithAlias 叠加
|
||
if (this.curTokenIs(TokenType.STRING)) {
|
||
const value = this.curToken.value;
|
||
this.nextToken();
|
||
return `'${value.replace(/'/g, "''")}'`;
|
||
}
|
||
|
||
// 聚合函数?
|
||
if (
|
||
this.curTokenIs(TokenType.COUNT) ||
|
||
this.curTokenIs(TokenType.SUM) ||
|
||
this.curTokenIs(TokenType.AVG) ||
|
||
this.curTokenIs(TokenType.MIN) ||
|
||
this.curTokenIs(TokenType.MAX)
|
||
) {
|
||
return this.parseAggregateCall();
|
||
}
|
||
|
||
const first = this.expectIdentifier('column name');
|
||
if (this.curTokenIs(TokenType.DOT)) {
|
||
this.nextToken();
|
||
const second = this.expectIdentifier('column name');
|
||
return `${first}.${second}`;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
/**
|
||
* 解析 CASE WHEN 表达式,返回原文(含可选 AS 别名)。
|
||
* 例:CASE WHEN age > 30 THEN 'senior' ELSE 'junior' END AS status
|
||
*/
|
||
private parseCaseExpressionText(): string {
|
||
const start = this.curToken.position;
|
||
this.nextToken(); // 跳过 CASE
|
||
let depth = 1;
|
||
let end = start + 'CASE'.length;
|
||
|
||
while (!this.curTokenIs(TokenType.EOF) && depth > 0) {
|
||
if (this.curTokenIs(TokenType.CASE)) depth++;
|
||
if (this.curTokenIs(TokenType.END)) {
|
||
depth--;
|
||
end = this.curToken.position + 'END'.length;
|
||
this.nextToken();
|
||
if (depth === 0) break;
|
||
}
|
||
end = this.curToken.position + this.curToken.value.length;
|
||
this.nextToken();
|
||
}
|
||
|
||
let text = this.sql.slice(start, end);
|
||
// 可选 AS 别名
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
text += ` AS ${this.expectIdentifier('alias')}`;
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && !this.curTokenIs(TokenType.COMMA) && !this._isReservedAfterFrom()) {
|
||
text += ` AS ${this.curToken.value}`;
|
||
this.nextToken();
|
||
}
|
||
return text;
|
||
}
|
||
|
||
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col),v0.4.0 支持 COUNT(DISTINCT col) */
|
||
private parseAggregateCall(): string {
|
||
const func = this.curToken.value.toUpperCase();
|
||
this.nextToken();
|
||
this.expect(TokenType.LPAREN);
|
||
|
||
// v0.4.0: COUNT(DISTINCT col) 等去重聚合
|
||
let distinct = false;
|
||
if (this.curTokenIs(TokenType.DISTINCT)) {
|
||
distinct = true;
|
||
this.nextToken();
|
||
}
|
||
|
||
let arg: string;
|
||
if (this.curTokenIs(TokenType.STAR)) {
|
||
arg = '*';
|
||
this.nextToken();
|
||
} else {
|
||
arg = this.parseColumnRef();
|
||
}
|
||
|
||
this.expect(TokenType.RPAREN);
|
||
|
||
// 可选别名: AS alias
|
||
let alias = '';
|
||
if (this.curTokenIs(TokenType.AS)) {
|
||
this.nextToken();
|
||
alias = this.expectIdentifier('alias');
|
||
} else if (this.curToken.type === TokenType.IDENTIFIER && this._isAggregateAlias()) {
|
||
alias = this.curToken.value;
|
||
this.nextToken();
|
||
}
|
||
|
||
const inner = distinct ? `DISTINCT ${arg}` : arg;
|
||
if (alias) {
|
||
return `${func}(${inner}) AS ${alias}`;
|
||
}
|
||
return `${func}(${inner})`;
|
||
}
|
||
|
||
private _isAggregateAlias(): boolean {
|
||
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
|
||
}
|
||
|
||
private parseIdentifierList(): string[] {
|
||
const ids: string[] = [];
|
||
ids.push(this.parseIdentifierWithDot());
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
ids.push(this.parseIdentifierWithDot());
|
||
}
|
||
return ids;
|
||
}
|
||
|
||
private parseValueList(): unknown[] {
|
||
const vals: unknown[] = [];
|
||
vals.push(this.parseValue());
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
vals.push(this.parseValue());
|
||
}
|
||
return vals;
|
||
}
|
||
|
||
private parseOrderByList(): OrderBy[] {
|
||
const list: OrderBy[] = [];
|
||
list.push(this.parseOrderBy());
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
list.push(this.parseOrderBy());
|
||
}
|
||
return list;
|
||
}
|
||
|
||
private parseOrderBy(): OrderBy {
|
||
const column = this.parseIdentifierWithDot();
|
||
let direction: SortDirection = 'asc';
|
||
if (this.curTokenIs(TokenType.ASC)) {
|
||
this.nextToken();
|
||
} else if (this.curTokenIs(TokenType.DESC)) {
|
||
direction = 'desc';
|
||
this.nextToken();
|
||
}
|
||
// v0.4.0: NULLS FIRST / NULLS LAST
|
||
let nulls: 'first' | 'last' | undefined;
|
||
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'NULLS') {
|
||
this.nextToken();
|
||
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'FIRST') {
|
||
nulls = 'first';
|
||
this.nextToken();
|
||
} else if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'LAST') {
|
||
nulls = 'last';
|
||
this.nextToken();
|
||
}
|
||
}
|
||
return { column, direction, ...(nulls ? { nulls } : {}) };
|
||
}
|
||
|
||
/** v0.4.0: 标识符(支持 'table.column' 带表前缀引用,用于 ORDER BY / GROUP BY) */
|
||
private parseIdentifierWithDot(): string {
|
||
const first = this.expectIdentifier('identifier');
|
||
if (this.curTokenIs(TokenType.DOT)) {
|
||
this.nextToken();
|
||
return `${first}.${this.expectIdentifier('identifier')}`;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
/** 解析字面量值 */
|
||
private parseValue(): unknown {
|
||
switch (this.curToken.type) {
|
||
case TokenType.STRING: {
|
||
const val = this.curToken.value;
|
||
this.nextToken();
|
||
return val;
|
||
}
|
||
case TokenType.NUMBER: {
|
||
const val = Number(this.curToken.value);
|
||
this.nextToken();
|
||
return val;
|
||
}
|
||
case TokenType.TRUE: this.nextToken(); return true;
|
||
case TokenType.FALSE: this.nextToken(); return false;
|
||
case TokenType.NULL: this.nextToken(); return null;
|
||
default:
|
||
throw this.error(`Expected value, got "${this.curToken.value}"`);
|
||
}
|
||
}
|
||
|
||
// ===================================================================
|
||
// Token 操作
|
||
// ===================================================================
|
||
|
||
private nextToken(): void {
|
||
this.curToken = this.peekToken;
|
||
this.peekToken = this.lexer.nextToken();
|
||
}
|
||
|
||
private curTokenIs(type: TokenType): boolean {
|
||
return this.curToken.type === type;
|
||
}
|
||
|
||
private expect(type: TokenType): void {
|
||
if (this.curTokenIs(type)) {
|
||
this.nextToken();
|
||
return;
|
||
}
|
||
throw this.error(`Expected ${type}, got "${this.curToken.value}"`);
|
||
}
|
||
|
||
private expectIdentifier(context: string): string {
|
||
// v0.8.0: 分隔标识符 "col" 与普通标识符等价(但不参与关键字识别,
|
||
// 因此可以用它引用保留字列名,如 "order" / "select")
|
||
if (this.curToken.type === TokenType.QUOTED_IDENTIFIER) {
|
||
const val = this.curToken.value;
|
||
this.nextToken();
|
||
return val;
|
||
}
|
||
if (this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) {
|
||
const val = this.curToken.value;
|
||
this.nextToken();
|
||
return val;
|
||
}
|
||
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
|
||
}
|
||
|
||
/** 关键字可以作为标识符(如列名等于关键字) */
|
||
private _isKeywordAsIdent(): boolean {
|
||
return (
|
||
this.curToken.type !== TokenType.EOF &&
|
||
this.curToken.type !== TokenType.ILLEGAL &&
|
||
this.curToken.type !== TokenType.STRING &&
|
||
// v0.8.0: 分隔标识符由 expectIdentifier 的显式分支处理,不走"关键字当标识符"兜底
|
||
this.curToken.type !== TokenType.QUOTED_IDENTIFIER &&
|
||
this.curToken.type !== TokenType.NUMBER &&
|
||
this.curToken.type !== TokenType.COMMA &&
|
||
this.curToken.type !== TokenType.LPAREN &&
|
||
this.curToken.type !== TokenType.RPAREN &&
|
||
this.curToken.type !== TokenType.SEMICOLON &&
|
||
this.curToken.type !== TokenType.EQ &&
|
||
this.curToken.type !== TokenType.NEQ &&
|
||
this.curToken.type !== TokenType.GT &&
|
||
this.curToken.type !== TokenType.GTE &&
|
||
this.curToken.type !== TokenType.LT &&
|
||
this.curToken.type !== TokenType.LTE &&
|
||
this.curToken.type !== TokenType.DOT &&
|
||
this.curToken.type !== TokenType.STAR
|
||
);
|
||
}
|
||
|
||
private expectNumber(context: string): number {
|
||
if (this.curToken.type === TokenType.NUMBER) {
|
||
const val = Number(this.curToken.value);
|
||
this.nextToken();
|
||
return val;
|
||
}
|
||
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
|
||
}
|
||
|
||
private error(msg: string): DatabaseError {
|
||
return new DatabaseError(
|
||
`Parse error at position ${this.curToken.position}: ${msg}`,
|
||
'PARSE_ERROR',
|
||
);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 便捷方法
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 解析 SQL 字符串为 AST Statement */
|
||
export function parse(sql: string): Statement {
|
||
const parser = new Parser(sql);
|
||
const stmt = parser.parseStatement();
|
||
return stmt;
|
||
}
|
||
|
||
/** 解析 SQL 字符串为 AST Statement 数组(分号分隔的多语句支持,v0.3.0) */
|
||
export function parseAll(sql: string): Statement[] {
|
||
const parser = new Parser(sql);
|
||
return parser.parseAllStatements();
|
||
}
|
||
|
||
/** 解析独立 WHERE 条件表达式(CASE WHEN 求值等场景,v0.3.1) */
|
||
export function parseWhereCondition(sql: string): WhereCondition {
|
||
const parser = new Parser(sql);
|
||
return parser.parseWhere();
|
||
}
|