142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
/**
|
||
* 极简 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<string, SqlValue>[] {
|
||
return rows.map((row) => {
|
||
const obj: Record<string, SqlValue> = {};
|
||
columns.forEach((col, idx) => {
|
||
obj[col] = row[idx] ?? null;
|
||
});
|
||
return obj;
|
||
});
|
||
}
|
||
|