Files
MetonaSqlark/src/sql/parser.ts
T
thzxx 841db2e049 fix(A22/A23/A25/A26/A27/A29/A30/A36): 查询层 8 项缺陷根治 + 单一语义收敛
每项都先用可执行探针复现出**错误的实际输出**,再修根因、补永久回归套件
(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 零错误。
2026-09-15 00:00:08 +08:00

1456 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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] namev0.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_ERRORexecutor 侧投影已支持混合)
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
columns.push(this.parseColumnWithAlias());
}
} else {
columns.push(...this.parseColumnList());
}
// FROMv0.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.0A26):把"最后一个 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.0NOT 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 INNOT 后紧跟 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 解析为 booleanwhere-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();
}