SQL Formatter Lexer 与高亮引擎:Token、缩进状态和能力边界
SQL 高亮和基础格式化通常先把文本切成 token,再根据关键字、字符串、注释和括号状态决定颜色与缩进。lexer 只识别局部词法,不一定建立完整 AST,也不能据此验证 SQL 语义。本文聚焦 token、状态和缩进边界,不把高亮引擎描述成完整 parser。
一、问题概述:按空格切分无法支持 SQL 高亮
SELECT 'a,b' -- note 中的逗号属于字符串,-- 之后是注释;如果直接 split,高亮器会把字符串内容当成关键字或把注释中的词染色。lexer 需要记录当前状态,至少区分普通文本、引号和注释。
二、最小复现:字符串和注释中的关键字不应高亮为 SQL
下面的输入只用于说明 token 边界。关键字集合是演示用的有限集合,不代表完整方言词表。
const sql = "SELECT 'FROM users' AS note -- SELECT inside comment\nFROM users";
// Expected conceptual tokens:
// KEYWORD SELECT, STRING 'FROM users', WORD AS, WORD note, COMMENT -- ..., KEYWORD FROM, WORD users三、根因:lexer 依赖状态机而不是正则替换
字符串可能包含转义或成对引号,注释可能跨行,标识符还可能使用方言特有的引号。lexer 在读取每个字符时要决定何时进入或离开状态;状态错误会让后续整行的高亮和缩进一起偏移。
四、推荐方案:先产出 token,再让 formatter/highlighter 消费
将 token 类型、原始文本和起止位置保留下来,高亮只映射 token 到 CSS 类,缩进逻辑只根据括号和关键字状态调整。遇到未知扩展时保留原 token。这样可以明确区分词法识别、视觉高亮和格式化规则。
五、完整代码:一个受限 lexer 与高亮映射
下面的 lexer 支持普通单词、数字、字符串、行/块注释和标点,并记录 token 类型;它不构造 AST、不验证表名和列是否存在,也不覆盖所有 SQL 方言。
type TokenKind = "keyword" | "word" | "number" | "string" | "comment" | "punctuation";
type Token = { kind: TokenKind; text: string; start: number; end: number };
const keywords = new Set(["SELECT", "FROM", "WHERE", "JOIN", "AND", "OR", "AS"]);
function lexSql(source: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < source.length) {
const start = i;
const ch = source[i];
if (/\s/.test(ch)) { i += 1; continue; }
if (ch === "-" && source[i + 1] === "-") {
i += 2;
while (i < source.length && source[i] !== "\n") i += 1;
tokens.push({ kind: "comment", text: source.slice(start, i), start, end: i });
continue;
}
if (ch === "/" && source[i + 1] === "*") {
i += 2;
while (i + 1 < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
i = Math.min(source.length, i + 2);
tokens.push({ kind: "comment", text: source.slice(start, i), start, end: i });
continue;
}
if (ch === "'" || ch === "\"" || ch === String.fromCharCode(96)) {
const quote = ch;
i += 1;
while (i < source.length) {
if (source[i] === quote && source[i + 1] === quote) { i += 2; continue; }
if (source[i] === quote) { i += 1; break; }
i += 1;
}
tokens.push({ kind: "string", text: source.slice(start, i), start, end: i });
continue;
}
if (/\d/.test(ch)) {
i += 1;
while (i < source.length && /[\d.]/.test(source[i])) i += 1;
tokens.push({ kind: "number", text: source.slice(start, i), start, end: i });
continue;
}
if (/[A-Za-z_]/.test(ch)) {
i += 1;
while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) i += 1;
const text = source.slice(start, i);
tokens.push({ kind: keywords.has(text.toUpperCase()) ? "keyword" : "word", text, start, end: i });
continue;
}
i += 1;
tokens.push({ kind: "punctuation", text: ch, start, end: i });
}
return tokens;
}
function highlightClass(token: Token): string {
return "sql-" + token.kind;
}
console.log(lexSql("SELECT 'FROM users' -- note\nFROM users").map(highlightClass));六、常见错误方案
用正则把所有大写单词包成关键字会误伤字符串和注释;只按括号数量缩进会忽略注释与方言构造;高亮 lexer 成功就宣称 SQL 有效,也无法发现列名、类型或语义错误。
七、边界条件:转义、反引号与缩进状态
不同方言对反斜杠、成对引号、反引号标识符和 dollar-quoted 字符串的规则不同。缩进器还要决定括号、CASE、JOIN 和子查询何时增加或减少层级;未知 token 应保持原文并避免破坏后续位置。
八、如何验证 lexer 与高亮
用字符串中的关键字、单/双/反引号、行/块注释、数字、小数、嵌套括号和跨行查询测试。断言 token 原文拼接后等于输入,字符串/注释不会被标成 keyword,token 起止位置单调递增;再单独验证缩进规则。
九、FAQ
问:lexer 能替代 SQL parser 吗?答:不能,它只提供词法 token,通常没有完整 AST 和语义验证。
问:为什么高亮看起来正确但 formatter 仍会错位?答:高亮和缩进可能使用不同状态规则,任一状态机遗漏方言语法都会影响格式。
问:未知函数应当高亮成关键字吗?答:不应猜测,保留为普通 token 或扩展 token,并由目标方言配置决定。
十、总结
SQL 高亮和基础格式化依赖 lexer 的状态、token 原文和位置;字符串、注释、引号和括号会影响缩进状态。lexer/highlighter 不是完整 SQL parser,验证时应检查 token 保真,再把语义校验交给真正的 parser 或数据库。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
- PostgreSQL Documentation — SQL Syntax
PostgreSQL Global Development Group
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
RFC Editor
相关文章
SQL Formatter 接入 CI/CD:检查、自动修改与失败策略
围绕 SQL 格式化的 CI/CD 接入说明 check、write 两种模式、差异输出和失败策略,避免把格式检查写成泛化工程模板。
错误排查SQL Formatter 方言兼容:关键字、函数与语法保真
说明 SQL formatter 为什么必须声明方言,比较 PostgreSQL、MySQL 与 SQLite 的关键字、函数、引号和语法保真边界。
最佳实践SQL Dump 转 JSON Mock:解析、脱敏、批量与类型边界
从 SQL dump 生成测试 JSON 时,分别处理 INSERT parser、敏感字段脱敏、批量行和 SQL 类型限制,不把 Mock 转换当成数据库迁移。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具