A19 双引号是分隔标识符(SQL 标准)
此前 '"' 与 "'" 一起交给 readString,于是 SELECT "name" FROM t 静默产出
一个名为 'name' 的**常量列**(行数正确、值全错、无报错),且被 lexer.test.ts
钉死为期望。新增 TokenType.QUOTED_IDENTIFIER;双引号内 "" 表示一个双引号;
可用于引用保留字列名(SELECT "order" FROM t)。parser 的 expectIdentifier
显式接受 QUOTED_IDENTIFIER。
A20 未闭合块注释必须报错
此前 skipBlockComment 循环到 EOF 就返回、不抛错,实测
db.query("DELETE FROM t WHERE id = '4' /*") **真的删掉了 1 行**;
SQLite 会报 unterminated comment。任何被截断/拼接的 SQL 都会静默改变语义。
A21 注释跳过改为循环(消除递归爆栈)
此前 default 分支用 return this.nextToken() 递归,栈深 = 连续注释数,
两万个连续块注释直接 RangeError(原生错误,调用方无法按 code 分类)。
A2 移除 MySQL 方言的反斜杠转义
此前只识别反斜杠+单引号:双反斜杠不解转义,以反斜杠结尾的 Windows 路径会
吞掉闭引号并报出与输入无关的解析错误。更严重的是参数绑定器只翻倍单引号、
不处理反斜杠 —— 两份词法规则不一致,"参数不改变 SQL 结构"在文本层不成立。
SQL 标准中反斜杠是普通字符,移除后词法器与绑定器的字符串边界判定完全一致。
更新 2 处把旧行为钉死的测试(lexer.test 双引号=字符串、v033 反斜杠转义)。
123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
/**
|
||
* Lexer 边缘场景测试
|
||
*/
|
||
|
||
import { Lexer, tokenize } from '../../src/sql/lexer';
|
||
import { TokenType } from '../../src/sql/tokens';
|
||
|
||
describe('Lexer 边缘场景', () => {
|
||
it('空字符串返回 EOF', () => {
|
||
const tokens = tokenize('');
|
||
expect(tokens).toHaveLength(1);
|
||
expect(tokens[0].type).toBe(TokenType.EOF);
|
||
});
|
||
|
||
it('只包含空格', () => {
|
||
const tokens = tokenize(' \t\n ');
|
||
expect(tokens).toHaveLength(1);
|
||
expect(tokens[0].type).toBe(TokenType.EOF);
|
||
});
|
||
|
||
it('非法字符返回 ILLEGAL', () => {
|
||
const tokens = tokenize('@');
|
||
expect(tokens[0].type).toBe(TokenType.ILLEGAL);
|
||
});
|
||
|
||
it('完整的 CREATE TABLE 语句', () => {
|
||
const tokens = tokenize('CREATE TABLE users (id STRING PRIMARY KEY, name STRING NOT NULL, age NUMBER DEFAULT 0)');
|
||
const types = tokens.map(t => t.type);
|
||
expect(types).toEqual([
|
||
TokenType.CREATE, TokenType.TABLE, TokenType.IDENTIFIER, TokenType.LPAREN,
|
||
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.PRIMARY, TokenType.KEY,
|
||
TokenType.COMMA,
|
||
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.NOT, TokenType.NULL,
|
||
TokenType.COMMA,
|
||
TokenType.IDENTIFIER, TokenType.IDENTIFIER, TokenType.DEFAULT, TokenType.NUMBER,
|
||
TokenType.RPAREN, TokenType.EOF,
|
||
]);
|
||
});
|
||
|
||
it('带转义单引号的字符串', () => {
|
||
// SQL 中两个单引号 '' 表示转义的单引号
|
||
const tokens = tokenize("SELECT 'hello world'");
|
||
expect(tokens[1].type).toBe(TokenType.STRING);
|
||
expect(tokens[1].value).toBe('hello world');
|
||
});
|
||
|
||
// v0.8.0 更正:双引号在 SQL 标准(及 SQLite/PostgreSQL)中是**分隔标识符**,
|
||
// 不是字符串字面量。此前本用例把 `"hello world"` 钉死为 STRING,正是
|
||
// `SELECT "name" FROM t` 静默产出常量列(行数正确、值全错)的原因。
|
||
it('双引号是分隔标识符(非字符串字面量)', () => {
|
||
const tokens = tokenize('SELECT "hello world"');
|
||
expect(tokens[1].type).toBe(TokenType.QUOTED_IDENTIFIER);
|
||
expect(tokens[1].value).toBe('hello world');
|
||
// 单引号才是字符串
|
||
const strTokens = tokenize("SELECT 'hello world'");
|
||
expect(strTokens[1].type).toBe(TokenType.STRING);
|
||
});
|
||
|
||
it('分隔标识符内的 "" 表示一个双引号', () => {
|
||
const tokens = tokenize('SELECT "a""b"');
|
||
expect(tokens[1].type).toBe(TokenType.QUOTED_IDENTIFIER);
|
||
expect(tokens[1].value).toBe('a"b');
|
||
});
|
||
|
||
it('未闭合的分隔标识符报 PARSE_ERROR', () => {
|
||
expect(() => tokenize('SELECT "abc')).toThrow(/Unterminated quoted identifier/);
|
||
});
|
||
|
||
it('未闭合的块注释报 PARSE_ERROR(此前静默吞掉剩余 SQL)', () => {
|
||
expect(() => tokenize('SELECT 1 /* oops')).toThrow(/Unterminated block comment/);
|
||
});
|
||
|
||
it('反斜杠是普通字符(与参数绑定器保持一致)', () => {
|
||
const tokens = tokenize("SELECT 'C:\\'");
|
||
expect(tokens[1].type).toBe(TokenType.STRING);
|
||
expect(tokens[1].value).toBe('C:\\');
|
||
});
|
||
|
||
it('所有关键字', () => {
|
||
const sql = 'SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE DROP ORDER BY ASC DESC LIMIT OFFSET AND OR NOT LIKE IN PRIMARY KEY UNIQUE DEFAULT NULL TRUE FALSE';
|
||
const tokens = tokenize(sql);
|
||
const nonEof = tokens.filter(t => t.type !== TokenType.EOF);
|
||
expect(nonEof).toHaveLength(30);
|
||
// 验证都是关键字,不是 IDENTIFIER
|
||
for (const t of nonEof) {
|
||
expect(t.type).not.toBe(TokenType.IDENTIFIER);
|
||
}
|
||
});
|
||
|
||
it('下划线标识符', () => {
|
||
const tokens = tokenize('SELECT my_col_1 FROM user_table');
|
||
expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
|
||
expect(tokens[1].value).toBe('my_col_1');
|
||
expect(tokens[3].type).toBe(TokenType.IDENTIFIER);
|
||
expect(tokens[3].value).toBe('user_table');
|
||
});
|
||
|
||
it('运算符分隔符', () => {
|
||
const tokens = tokenize('= != > >= < <= <> ( ) , ; *');
|
||
const types = tokens.map(t => t.type);
|
||
expect(types).toEqual([
|
||
TokenType.EQ, TokenType.NEQ, TokenType.GT, TokenType.GTE,
|
||
TokenType.LT, TokenType.LTE, TokenType.NEQ,
|
||
TokenType.LPAREN, TokenType.RPAREN, TokenType.COMMA,
|
||
TokenType.SEMICOLON, TokenType.STAR, TokenType.EOF,
|
||
]);
|
||
});
|
||
|
||
it('数字: 整数和浮点数和负数', () => {
|
||
const tokens = tokenize('42 3.14 -7 -2.5');
|
||
expect(tokens[0].value).toBe('42');
|
||
expect(tokens[1].value).toBe('3.14');
|
||
expect(tokens[2].value).toBe('-7');
|
||
expect(tokens[3].value).toBe('-2.5');
|
||
});
|
||
|
||
it('Token position 正确', () => {
|
||
const lexer = new Lexer('SELECT * FROM users');
|
||
const tok = lexer.nextToken();
|
||
expect(tok.position).toBe(0); // SELECT 从位置 0 开始
|
||
});
|
||
});
|