Files
YelayMaths/yelaymaths_api/tools/legacy/parser.ts
T
2026-09-30 03:38:10 +08:00

142 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 极简 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;
});
}