/** * 极简 mysqldump 解析器。 * * 只做一件事:从 mysqldump 导出的 .sql 文本里取出 `INSERT INTO `表名` VALUES (...),(...)` 的行数据。 * 不解析 DDL,不处理多语句逻辑。 */ export type SqlValue = string | number | null; /** 解码 MySQL 字符串中的反斜杠转义。未识别的转义按 MySQL 语义保留反斜杠。 */ function decodeEscape(e: string): string { switch (e) { case '0': return '\0'; case 'b': return '\b'; case 'n': return '\n'; case 'r': return '\r'; case 't': return '\t'; case 'Z': return '\x1a'; case '\\': return '\\'; case "'": return "'"; case '"': return '"'; default: return '\\' + e; } } /** * 把 `VALUES` 之后的载荷解析成二维数组。 * 每个字段的三种形态:带引号的字符串 / NULL / 数字。 */ export function parseTupleList(payload: string): SqlValue[][] { const rows: SqlValue[][] = []; const n = payload.length; let i = 0; const skipSeparators = (): void => { while (i < n && (payload[i] === ',' || payload[i]! <= ' ')) i++; }; skipSeparators(); while (i < n) { if (payload[i] !== '(') { throw new Error(`位置 ${i} 处期望 '(',实际是 ${JSON.stringify(payload[i])}`); } i++; const row: SqlValue[] = []; let closed = false; while (!closed) { while (i < n && payload[i]! <= ' ') i++; if (payload[i] === "'") { i++; let s = ''; for (;;) { const c = payload[i]; if (c === undefined) throw new Error('字符串未闭合'); if (c === '\\') { s += decodeEscape(payload[i + 1] ?? ''); i += 2; continue; } if (c === "'") { if (payload[i + 1] === "'") { s += "'"; i += 2; continue; } i++; break; } s += c; i++; } row.push(s); } else { let token = ''; while (i < n && payload[i] !== ',' && payload[i] !== ')') { token += payload[i]; i++; } token = token.trim(); row.push(token === '' || token.toUpperCase() === 'NULL' ? null : Number(token)); } while (i < n && payload[i]! <= ' ' && payload[i] !== ',' && payload[i] !== ')') i++; const c = payload[i]; if (c === ',') { i++; continue; } if (c === ')') { i++; closed = true; continue; } throw new Error(`位置 ${i} 处期望 ',' 或 ')',实际是 ${JSON.stringify(c)}`); } rows.push(row); skipSeparators(); } return rows; } /** 取出某张表全部 INSERT 行。表不存在时返回空数组。 */ export function readTable(sql: string, table: string): SqlValue[][] { // 结尾同时兼容 LF 与 CRLF:Windows 上导出的 dump 是 CRLF, // 只写 \n 会全部匹配失败,表现为「所有表都是空数组」这种静默错误。 const re = new RegExp( 'INSERT INTO `' + table.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '` VALUES (.*?);(?:\\r?\\n|$)', 'gs', ); const rows: SqlValue[][] = []; for (const m of sql.matchAll(re)) { rows.push(...parseTupleList(m[1]!)); } return rows; } /** 转成对象数组,便于按字段名取值。 */ export function toObjects(rows: SqlValue[][], columns: string[]): Record[] { return rows.map((row) => { const obj: Record = {}; columns.forEach((col, idx) => { obj[col] = row[idx] ?? null; }); return obj; }); }