SQL 转 JSON:为什么正则难以可靠解析 INSERT
用正则从简单 INSERT 中抓取值看起来很快,但 SQL 字符串、注释、标识符和方言语法都可能包含逗号、括号或分号。本文只讨论 SQL dump 转 JSON 时正则提取 INSERT 的边界,展示一个不冒充完整 SQL parser 的分句扫描器,并说明何时必须使用支持目标方言的解析器。
一、问题概述:看起来像 CSV 的 VALUES 不是 CSV
INSERT INTO t VALUES ('a,b', 2) 中的逗号属于字符串;批量 VALUES 又会出现多组括号。正则如果不理解引号和转义,就无法判断哪个逗号分隔列、哪个逗号是数据。解析失败时不能靠删除字符来修复原始 SQL。
二、最小复现:非贪婪捕获也会在字符串中提前结束
下面的模式只适合非常窄的演示语法,遇到带括号或转义引号的值就没有可靠保证。
const sql = "INSERT INTO logs(message) VALUES ('a,b (quoted)')";
const naive = /INSERT\s+INTO\s+.+?VALUES\s*\((.*?)\)/is;
console.log(naive.exec(sql)?.[1]);
// The first closing parenthesis inside a value can be mistaken for the end.三、根因:SQL 有状态的词法上下文和方言差异
单引号字符串可能使用成对单引号或反斜杠转义;MySQL 还有反引号标识符,PostgreSQL 可能使用 dollar-quoted 字符串。行注释、块注释、触发器、函数体和分号也改变了分句边界。正则可以匹配受限语法,却不能凭一个通用表达式覆盖所有这些状态。
四、推荐方案:先声明方言,再使用真正的 parser
如果输入来自受控的单行 INSERT 子集,可以写一个明确限制的扫描器并在遇到不支持的语法时失败。面对生产 dump、批量 values、函数、注释或多种方言,应选择能声明目标 dialect 的 SQL parser,并根据其文档处理 AST;不要在文章或代码中虚构 parser API。
五、完整代码:只做安全的顶层分句
下面的 Node.js/浏览器兼容 TypeScript 代码只在引号和注释之外按分号切分,并主动拒绝 PostgreSQL dollar-quoted 字符串。它不是 INSERT 值解析器,返回的语句仍需交给目标方言 parser。
function splitSqlStatements(sql: string): string[] {
const result: string[] = [];
let start = 0;
let mode: "normal" | "single" | "double" | "backtick" | "line" | "block" = "normal";
for (let i = 0; i < sql.length; i += 1) {
const ch = sql[i];
const next = sql[i + 1];
if (mode === "line") { if (ch === "\n") mode = "normal"; continue; }
if (mode === "block") { if (ch === "*" && next === "/") { mode = "normal"; i += 1; } continue; }
if (mode === "single") {
if (ch === "\\") { i += 1; continue; }
if (ch === "'" && next === "'") { i += 1; continue; }
if (ch === "'") mode = "normal";
continue;
}
if (mode === "double" || mode === "backtick") {
const quote = mode === "double" ? '"' : String.fromCharCode(96);
if (ch === "\\") { i += 1; continue; }
if (ch === quote && next === quote) { i += 1; continue; }
if (ch === quote) mode = "normal";
continue;
}
const dollar = sql.slice(i).match(/^\$\$|^\$[A-Za-z_][A-Za-z0-9_]*\$/);
if (dollar) throw new Error("Dollar-quoted strings require a PostgreSQL parser");
if (ch === "-" && next === "-") { mode = "line"; i += 1; continue; }
if (ch === "#") { mode = "line"; continue; }
if (ch === "/" && next === "*") { mode = "block"; i += 1; continue; }
if (ch === "'") { mode = "single"; continue; }
if (ch === '"') { mode = "double"; continue; }
if (ch === String.fromCharCode(96)) { mode = "backtick"; continue; }
if (ch === ";") {
const statement = sql.slice(start, i).trim();
if (statement) result.push(statement);
start = i + 1;
}
}
if (mode !== "normal") throw new Error("Unterminated quote or comment");
const tail = sql.slice(start).trim();
if (tail) result.push(tail);
return result;
}
console.log(splitSqlStatements("INSERT INTO t VALUES ('a; b'); -- next\nINSERT INTO t VALUES (2);"));六、常见错误方案
用 split(',') 取列会破坏字符串中的逗号;用 split(');') 取行会破坏字符串和注释;给所有长数字加引号会修改字符串内容和方言语义。一个能处理样例的正则不等于能处理 dump,尤其不能把失败结果静默转换成 JSON。
七、边界条件:转义、注释、批量与方言
测试至少覆盖单引号、成对引号、反斜杠、逗号、括号、分号、行/块注释、批量 VALUES、反引号和 PostgreSQL dollar quote。不同方言对反斜杠和引号规则不同;扫描器应在不确定时拒绝,而不是猜测。
八、如何验证解析结果
先验证分句数量和每句原文是否保持,再用目标方言 parser 生成 AST,检查 INSERT 表名、列数、值数及 NULL 语义。把含复杂字符串的语句与数据库客户端或官方工具的输出进行对照;不能只比较最终 JSON 看起来像不像。
九、FAQ
问:正则完全不能解析 SQL 吗?答:可以覆盖明确、受限的语法子集,但必须把限制写出来并在遇到边界时失败。
问:为什么先分句还不够?答:分句只解决顶层分号,INSERT 的列和值仍需要词法和方言解析。
问:能否自动删除注释后再解析?答:不能盲删,字符串和方言构造中可能包含看似注释的字符。
十、总结
INSERT 的逗号、括号和分号是否属于语法,取决于当前词法上下文。正则只适合明确的小语法;复杂 dump 应声明方言并使用真正 parser。自定义扫描器最多负责可验证的分句,并在引号、注释或 dollar quote 等未知边界上主动停止。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
- PostgreSQL Documentation — SQL Syntax
PostgreSQL Global Development Group
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
RFC Editor
相关文章
SQL Dump 转 JSON Mock:解析、脱敏、批量与类型边界
从 SQL dump 生成测试 JSON 时,分别处理 INSERT parser、敏感字段脱敏、批量行和 SQL 类型限制,不把 Mock 转换当成数据库迁移。
踩坑避坑SQL 转 JSON:NULL、布尔值与 0/1 的语义差异
分开说明 PostgreSQL boolean、MySQL TINYINT(1)、SQL NULL、字符串 null 与数值 0/1 在 JSON 转换中的差异,并给出显式映射和验证示例。
错误排查SQL Formatter 方言兼容:关键字、函数与语法保真
说明 SQL formatter 为什么必须声明方言,比较 PostgreSQL、MySQL 与 SQLite 的关键字、函数、引号和语法保真边界。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具