Files
MetonaSqlark/tests/sql/lexer.test.ts
T
thzxx 7526951804 fix(A19/A20/A21/A2): 词法器标准化 —— 双引号标识符、未闭合注释报错、去递归、移除方言转义
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 反斜杠转义)。
2026-09-14 21:08:20 +08:00

123 lines
4.6 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.
/**
* 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 开始
});
});