Javascript is required
错误排查发布于 2026-07-28更新于 2026-08-08审校于 2026-08-086 分钟阅读

SQL 转 JSON:为什么正则难以可靠解析 INSERT

用正则从简单 INSERT 中抓取值看起来很快,但 SQL 字符串、注释、标识符和方言语法都可能包含逗号、括号或分号。本文只讨论 SQL dump 转 JSON 时正则提取 INSERT 的边界,展示一个不冒充完整 SQL parser 的分句扫描器,并说明何时必须使用支持目标方言的解析器。

SQL ParserINSERTRegexSQL to JSON

一、问题概述:看起来像 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 等未知边界上主动停止。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具