137 lines
3.4 KiB
TypeScript
137 lines
3.4 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[][] {
|
|
const re = new RegExp('INSERT INTO `' + table.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '` VALUES (.*?);\\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;
|
|
});
|
|
}
|
|
|