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 反斜杠转义)。
This commit is contained in:
thzxx
2026-09-14 21:08:20 +08:00
parent 83c5aa0b9d
commit 7526951804
5 changed files with 171 additions and 38 deletions
+28 -2
View File
@@ -44,10 +44,36 @@ describe('Lexer 边缘场景', () => {
expect(tokens[1].value).toBe('hello world');
});
it('双引号字符串', () => {
// 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.STRING);
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('所有关键字', () => {