Files
YelayMaths/yelaymaths_api/tools/legacy/parser.ts
T
2026-09-29 15:25:40 +08:00

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;
});
}