feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库
- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid - 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT - Query Builder链式API + TypeScript泛型支持 - 聚合函数:COUNT/SUM/AVG/MIN/MAX - 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出 - React/Vue框架集成 - 264个测试用例,93.46%覆盖率 - 零运行时依赖
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* metona-sqlark SQL Parser — SQL 字符串解析
|
||||
* @module sql
|
||||
*/
|
||||
|
||||
export { Lexer, tokenize } from './lexer';
|
||||
export { Parser, parse } from './parser';
|
||||
export { TokenType } from './tokens';
|
||||
export type { Token } from './tokens';
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* metona-sqlark SQL Lexer — 词法分析器
|
||||
* @module sql/lexer
|
||||
*
|
||||
* 将 SQL 字符串切分为 Token 流。
|
||||
*/
|
||||
|
||||
import { TokenType, type Token, KEYWORDS } from './tokens';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lexer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class Lexer {
|
||||
private input: string;
|
||||
private position = 0;
|
||||
private readPosition = 0;
|
||||
private ch: string = '';
|
||||
|
||||
constructor(input: string) {
|
||||
this.input = input;
|
||||
this.readChar();
|
||||
}
|
||||
|
||||
/** 读取下一个 Token */
|
||||
nextToken(): Token {
|
||||
this.skipWhitespace();
|
||||
|
||||
let tok: Token;
|
||||
|
||||
switch (this.ch) {
|
||||
case ',':
|
||||
tok = this.makeToken(TokenType.COMMA, ',');
|
||||
break;
|
||||
case '(':
|
||||
tok = this.makeToken(TokenType.LPAREN, '(');
|
||||
break;
|
||||
case ')':
|
||||
tok = this.makeToken(TokenType.RPAREN, ')');
|
||||
break;
|
||||
case ';':
|
||||
tok = this.makeToken(TokenType.SEMICOLON, ';');
|
||||
break;
|
||||
case '*':
|
||||
tok = this.makeToken(TokenType.STAR, '*');
|
||||
break;
|
||||
case '.':
|
||||
tok = this.makeToken(TokenType.DOT, '.');
|
||||
break;
|
||||
case '=':
|
||||
tok = this.makeToken(TokenType.EQ, '=');
|
||||
break;
|
||||
case '!':
|
||||
if (this.peekChar() === '=') {
|
||||
this.readChar();
|
||||
tok = this.makeToken(TokenType.NEQ, '!=');
|
||||
} else {
|
||||
tok = this.makeToken(TokenType.ILLEGAL, '!');
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if (this.peekChar() === '=') {
|
||||
this.readChar();
|
||||
tok = this.makeToken(TokenType.GTE, '>=');
|
||||
} else {
|
||||
tok = this.makeToken(TokenType.GT, '>');
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if (this.peekChar() === '=') {
|
||||
this.readChar();
|
||||
tok = this.makeToken(TokenType.LTE, '<=');
|
||||
} else if (this.peekChar() === '>') {
|
||||
this.readChar();
|
||||
tok = this.makeToken(TokenType.NEQ, '<>');
|
||||
} else {
|
||||
tok = this.makeToken(TokenType.LT, '<');
|
||||
}
|
||||
break;
|
||||
case "'":
|
||||
case '"':
|
||||
tok = this.readString(this.ch);
|
||||
break;
|
||||
case '':
|
||||
tok = { type: TokenType.EOF, value: '', position: this.position };
|
||||
break;
|
||||
default:
|
||||
if (this.isLetter(this.ch)) {
|
||||
const ident = this.readIdentifier();
|
||||
const keyword = KEYWORDS[ident.toUpperCase()];
|
||||
tok = {
|
||||
type: keyword ?? TokenType.IDENTIFIER,
|
||||
value: ident,
|
||||
position: this.position - ident.length,
|
||||
};
|
||||
return tok; // 已读取完毕,不需要再 readChar
|
||||
} else if (this.isDigit(this.ch) || (this.ch === '-' && this.isDigit(this.peekChar()))) {
|
||||
const num = this.readNumber();
|
||||
tok = {
|
||||
type: TokenType.NUMBER,
|
||||
value: num,
|
||||
position: this.position - num.length,
|
||||
};
|
||||
return tok;
|
||||
} else {
|
||||
tok = this.makeToken(TokenType.ILLEGAL, this.ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.readChar();
|
||||
return tok;
|
||||
}
|
||||
|
||||
// ---- 内部 ----
|
||||
|
||||
private readChar(): void {
|
||||
if (this.readPosition >= this.input.length) {
|
||||
this.ch = '';
|
||||
} else {
|
||||
this.ch = this.input[this.readPosition];
|
||||
}
|
||||
this.position = this.readPosition;
|
||||
this.readPosition++;
|
||||
}
|
||||
|
||||
private peekChar(): string {
|
||||
if (this.readPosition >= this.input.length) return '';
|
||||
return this.input[this.readPosition];
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n' || this.ch === '\r') {
|
||||
this.readChar();
|
||||
}
|
||||
}
|
||||
|
||||
private readIdentifier(): string {
|
||||
const start = this.position;
|
||||
while (this.isLetter(this.ch) || this.isDigit(this.ch) || this.ch === '_') {
|
||||
this.readChar();
|
||||
}
|
||||
return this.input.slice(start, this.position);
|
||||
}
|
||||
|
||||
private readNumber(): string {
|
||||
const start = this.position;
|
||||
// 负号
|
||||
if (this.ch === '-') this.readChar();
|
||||
while (this.isDigit(this.ch)) {
|
||||
this.readChar();
|
||||
}
|
||||
// 小数点
|
||||
if (this.ch === '.' && this.isDigit(this.peekChar())) {
|
||||
this.readChar();
|
||||
while (this.isDigit(this.ch)) {
|
||||
this.readChar();
|
||||
}
|
||||
}
|
||||
return this.input.slice(start, this.position);
|
||||
}
|
||||
|
||||
private readString(quote: string): Token {
|
||||
const start = this.position + 1; // 跳过一个引号
|
||||
this.readChar(); // 跳过开始引号
|
||||
let value = '';
|
||||
|
||||
while (this.ch !== quote && this.ch !== '') {
|
||||
// 处理转义
|
||||
if (this.ch === '\\' && this.peekChar() === quote) {
|
||||
this.readChar();
|
||||
value += quote;
|
||||
} else {
|
||||
value += this.ch;
|
||||
}
|
||||
this.readChar();
|
||||
}
|
||||
|
||||
// 跳过结束引号(在 readChar 之后才会调用,所以这里不需要处理)
|
||||
return {
|
||||
type: TokenType.STRING,
|
||||
value,
|
||||
position: start,
|
||||
};
|
||||
}
|
||||
|
||||
private isLetter(ch: string): boolean {
|
||||
return /[a-zA-Z_]/.test(ch);
|
||||
}
|
||||
|
||||
private isDigit(ch: string): boolean {
|
||||
return /[0-9]/.test(ch);
|
||||
}
|
||||
|
||||
private makeToken(type: TokenType, value: string): Token {
|
||||
return { type, value, position: this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 便捷方法:一次性词法分析
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 将 SQL 字符串解析为 Token 列表 */
|
||||
export function tokenize(sql: string): Token[] {
|
||||
const lexer = new Lexer(sql);
|
||||
const tokens: Token[] = [];
|
||||
let tok = lexer.nextToken();
|
||||
while (tok.type !== TokenType.EOF) {
|
||||
tokens.push(tok);
|
||||
tok = lexer.nextToken();
|
||||
}
|
||||
tokens.push(tok); // EOF
|
||||
return tokens;
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
/**
|
||||
* 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,
|
||||
InsertStatement,
|
||||
UpdateStatement,
|
||||
DeleteStatement,
|
||||
CreateTableStatement,
|
||||
DropTableStatement,
|
||||
ASTColumnDef,
|
||||
} from '../query/ast';
|
||||
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
|
||||
import { DatabaseError } from '../constants';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class Parser {
|
||||
private lexer: Lexer;
|
||||
private curToken!: Token;
|
||||
private peekToken!: Token;
|
||||
|
||||
constructor(sql: string) {
|
||||
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.parseCreateTable();
|
||||
case TokenType.DROP:
|
||||
return this.parseDropTable();
|
||||
default:
|
||||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// SELECT
|
||||
// ===================================================================
|
||||
|
||||
private parseSelect(): SelectStatement {
|
||||
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();
|
||||
} else {
|
||||
columns.push(...this.parseColumnList());
|
||||
}
|
||||
|
||||
// FROM
|
||||
this.expect(TokenType.FROM);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
|
||||
// 表别名(可选)
|
||||
let alias: string | undefined;
|
||||
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: {},
|
||||
};
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/** 解析 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);
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
return {
|
||||
type: 'INSERT',
|
||||
into: tableName,
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// UPDATE
|
||||
// ===================================================================
|
||||
|
||||
private parseUpdate(): UpdateStatement {
|
||||
this.expect(TokenType.UPDATE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
this.expect(TokenType.SET);
|
||||
|
||||
// SET col=val, ...
|
||||
const sets: Record<string, 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 = {};
|
||||
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 = {};
|
||||
if (this.curTokenIs(TokenType.WHERE)) {
|
||||
this.nextToken();
|
||||
where = this.parseCondition();
|
||||
}
|
||||
|
||||
return { type: 'DELETE', from: tableName, where };
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// CREATE TABLE
|
||||
// ===================================================================
|
||||
|
||||
private parseCreateTable(): CreateTableStatement {
|
||||
this.expect(TokenType.CREATE);
|
||||
this.expect(TokenType.TABLE);
|
||||
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 };
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
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();
|
||||
// 支持 NOT NULL → required
|
||||
this.expect(TokenType.NULL);
|
||||
col.required = true;
|
||||
} else if (this.curTokenIs(TokenType.DEFAULT)) {
|
||||
this.nextToken();
|
||||
col.default = this.parseValue();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// DROP TABLE
|
||||
// ===================================================================
|
||||
|
||||
private parseDropTable(): DropTableStatement {
|
||||
this.expect(TokenType.DROP);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
return { type: 'DROP_TABLE', name: tableName };
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 条件表达式
|
||||
// ===================================================================
|
||||
|
||||
/** condition → simple_cond ((AND|OR) simple_cond)* */
|
||||
private parseCondition(): WhereCondition {
|
||||
let left = this.parseSimpleCondition();
|
||||
|
||||
while (this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR)) {
|
||||
const isAnd = this.curTokenIs(TokenType.AND);
|
||||
this.nextToken();
|
||||
const right = this.parseSimpleCondition();
|
||||
|
||||
if (isAnd) {
|
||||
// 合并到 $and
|
||||
left = { $and: [left, right] } as unknown as WhereCondition;
|
||||
} else {
|
||||
left = { $or: [left, right] } as unknown as WhereCondition;
|
||||
}
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
|
||||
* | column [NOT] IN (values) | NOT condition | (condition) */
|
||||
private parseSimpleCondition(): WhereCondition {
|
||||
// NOT expr
|
||||
if (this.curTokenIs(TokenType.NOT)) {
|
||||
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 = {};
|
||||
result[column] = isNot ? { $ne: null } : { $eq: null };
|
||||
return result;
|
||||
}
|
||||
|
||||
// LIKE
|
||||
if (this.curTokenIs(TokenType.LIKE)) {
|
||||
this.nextToken();
|
||||
const pattern = this.parseValue();
|
||||
const result: WhereCondition = {};
|
||||
result[column] = { $like: pattern };
|
||||
return result;
|
||||
}
|
||||
|
||||
// IN
|
||||
if (this.curTokenIs(TokenType.IN)) {
|
||||
this.nextToken();
|
||||
this.expect(TokenType.LPAREN);
|
||||
const values = this.parseValueList();
|
||||
this.expect(TokenType.RPAREN);
|
||||
const result: WhereCondition = {};
|
||||
result[column] = { $in: values };
|
||||
return result;
|
||||
}
|
||||
|
||||
// 比较运算符
|
||||
const op = this.parseComparisonOp();
|
||||
|
||||
// 尝试解析列引用(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 = {};
|
||||
result[column] = { [op]: value };
|
||||
return result;
|
||||
}
|
||||
|
||||
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.parseColumnRef());
|
||||
while (this.curTokenIs(TokenType.COMMA)) {
|
||||
this.nextToken();
|
||||
cols.push(this.parseColumnRef());
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
/** 解析列引用:支持 'col'、'table.col' 和 'COUNT(*)'/'SUM(col)' 等 */
|
||||
private parseColumnRef(): string {
|
||||
// 聚合函数?
|
||||
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;
|
||||
}
|
||||
|
||||
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col) */
|
||||
private parseAggregateCall(): string {
|
||||
const func = this.curToken.value.toUpperCase();
|
||||
this.nextToken();
|
||||
this.expect(TokenType.LPAREN);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
return `${func}(${arg}) AS ${alias}`;
|
||||
}
|
||||
return `${func}(${arg})`;
|
||||
}
|
||||
|
||||
private _isAggregateAlias(): boolean {
|
||||
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
|
||||
}
|
||||
|
||||
private parseIdentifierList(): string[] {
|
||||
const ids: string[] = [];
|
||||
ids.push(this.expectIdentifier('identifier'));
|
||||
while (this.curTokenIs(TokenType.COMMA)) {
|
||||
this.nextToken();
|
||||
ids.push(this.expectIdentifier('identifier'));
|
||||
}
|
||||
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.expectIdentifier('column name');
|
||||
let direction: SortDirection = 'asc';
|
||||
if (this.curTokenIs(TokenType.ASC)) {
|
||||
this.nextToken();
|
||||
} else if (this.curTokenIs(TokenType.DESC)) {
|
||||
direction = 'desc';
|
||||
this.nextToken();
|
||||
}
|
||||
return { column, direction };
|
||||
}
|
||||
|
||||
/** 解析字面量值 */
|
||||
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 {
|
||||
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 &&
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* metona-sqlark SQL Token Types — 词法单元定义
|
||||
* @module sql/tokens
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Token 类型枚举
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export enum TokenType {
|
||||
// 关键字
|
||||
SELECT = 'SELECT',
|
||||
FROM = 'FROM',
|
||||
WHERE = 'WHERE',
|
||||
INSERT = 'INSERT',
|
||||
INTO = 'INTO',
|
||||
VALUES = 'VALUES',
|
||||
UPDATE = 'UPDATE',
|
||||
SET = 'SET',
|
||||
DELETE = 'DELETE',
|
||||
CREATE = 'CREATE',
|
||||
TABLE = 'TABLE',
|
||||
DROP = 'DROP',
|
||||
ORDER = 'ORDER',
|
||||
BY = 'BY',
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
LIMIT = 'LIMIT',
|
||||
OFFSET = 'OFFSET',
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
NOT = 'NOT',
|
||||
LIKE = 'LIKE',
|
||||
IN = 'IN',
|
||||
PRIMARY = 'PRIMARY',
|
||||
KEY = 'KEY',
|
||||
UNIQUE = 'UNIQUE',
|
||||
DEFAULT = 'DEFAULT',
|
||||
NULL = 'NULL',
|
||||
TRUE = 'TRUE',
|
||||
FALSE = 'FALSE',
|
||||
|
||||
// JOIN 相关
|
||||
INNER = 'INNER',
|
||||
LEFT = 'LEFT',
|
||||
RIGHT = 'RIGHT',
|
||||
CROSS = 'CROSS',
|
||||
JOIN = 'JOIN',
|
||||
ON = 'ON',
|
||||
AS = 'AS',
|
||||
OUTER = 'OUTER',
|
||||
|
||||
// 聚合
|
||||
GROUP = 'GROUP',
|
||||
HAVING = 'HAVING',
|
||||
COUNT = 'COUNT',
|
||||
SUM = 'SUM',
|
||||
AVG = 'AVG',
|
||||
MIN = 'MIN',
|
||||
MAX = 'MAX',
|
||||
DISTINCT = 'DISTINCT',
|
||||
|
||||
// 标识符 & 字面量
|
||||
IDENTIFIER = 'IDENTIFIER',
|
||||
STRING = 'STRING',
|
||||
NUMBER = 'NUMBER',
|
||||
|
||||
// 运算符 & 分隔符
|
||||
COMMA = 'COMMA',
|
||||
LPAREN = 'LPAREN',
|
||||
RPAREN = 'RPAREN',
|
||||
SEMICOLON = 'SEMICOLON',
|
||||
EQ = 'EQ',
|
||||
NEQ = 'NEQ',
|
||||
GT = 'GT',
|
||||
GTE = 'GTE',
|
||||
LT = 'LT',
|
||||
LTE = 'LTE',
|
||||
STAR = 'STAR',
|
||||
DOT = 'DOT',
|
||||
|
||||
// 特殊
|
||||
EOF = 'EOF',
|
||||
ILLEGAL = 'ILLEGAL',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Token
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
position: number; // 在源码中的位置
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 关键字映射
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const KEYWORDS: Record<string, TokenType> = {
|
||||
'SELECT': TokenType.SELECT,
|
||||
'FROM': TokenType.FROM,
|
||||
'WHERE': TokenType.WHERE,
|
||||
'INSERT': TokenType.INSERT,
|
||||
'INTO': TokenType.INTO,
|
||||
'VALUES': TokenType.VALUES,
|
||||
'UPDATE': TokenType.UPDATE,
|
||||
'SET': TokenType.SET,
|
||||
'DELETE': TokenType.DELETE,
|
||||
'CREATE': TokenType.CREATE,
|
||||
'TABLE': TokenType.TABLE,
|
||||
'DROP': TokenType.DROP,
|
||||
'ORDER': TokenType.ORDER,
|
||||
'BY': TokenType.BY,
|
||||
'ASC': TokenType.ASC,
|
||||
'DESC': TokenType.DESC,
|
||||
'LIMIT': TokenType.LIMIT,
|
||||
'OFFSET': TokenType.OFFSET,
|
||||
'AND': TokenType.AND,
|
||||
'OR': TokenType.OR,
|
||||
'NOT': TokenType.NOT,
|
||||
'LIKE': TokenType.LIKE,
|
||||
'IN': TokenType.IN,
|
||||
'PRIMARY': TokenType.PRIMARY,
|
||||
'KEY': TokenType.KEY,
|
||||
'UNIQUE': TokenType.UNIQUE,
|
||||
'DEFAULT': TokenType.DEFAULT,
|
||||
'NULL': TokenType.NULL,
|
||||
'TRUE': TokenType.TRUE,
|
||||
'FALSE': TokenType.FALSE,
|
||||
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
'RIGHT': TokenType.RIGHT,
|
||||
'CROSS': TokenType.CROSS,
|
||||
'JOIN': TokenType.JOIN,
|
||||
'ON': TokenType.ON,
|
||||
'AS': TokenType.AS,
|
||||
'OUTER': TokenType.OUTER,
|
||||
|
||||
// 聚合
|
||||
'GROUP': TokenType.GROUP,
|
||||
'HAVING': TokenType.HAVING,
|
||||
'COUNT': TokenType.COUNT,
|
||||
'SUM': TokenType.SUM,
|
||||
'AVG': TokenType.AVG,
|
||||
'MIN': TokenType.MIN,
|
||||
'MAX': TokenType.MAX,
|
||||
'DISTINCT': TokenType.DISTINCT,
|
||||
};
|
||||
Reference in New Issue
Block a user