'use strict'; // KB 版本查看/回滚:备份表命名 kb_docs_v,当前表为 kb_docs。 function quoteIdent(name) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) throw new Error(`invalid identifier: ${name}`); return `"${name}"`; } async function listVersions(pool) { const backups = await pool.query( `SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'kb_docs_v%' ORDER BY tablename DESC` ); const runs = await pool.query('SELECT * FROM kb_sync_runs ORDER BY id DESC LIMIT 20').catch(() => ({ rows: [] })); return { backups: backups.rows.map((r) => r.tablename), runs: runs.rows }; } async function rollback(pool, target) { if (!/^kb_docs_v[a-zA-Z0-9_]+$/.test(String(target))) throw new Error(`invalid backup table: ${target}`); const exists = await pool.query('SELECT to_regclass($1) IS NOT NULL AS ok', [`public.${target}`]); if (!exists.rows[0].ok) throw new Error(`backup not found: ${target}`); const hasCurrent = await pool.query("SELECT to_regclass('public.kb_docs') IS NOT NULL AS ok"); const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14); const currentBackup = `kb_docs_before_rollback_${stamp}`; const client = await pool.connect(); try { await client.query('BEGIN'); if (hasCurrent.rows[0].ok) { const idx = await client.query("SELECT indexname FROM pg_indexes WHERE schemaname='public' AND tablename='kb_docs'"); await client.query(`ALTER TABLE kb_docs RENAME TO ${quoteIdent(currentBackup)}`); for (const row of idx.rows) { const newName = `${currentBackup}_${row.indexname.replace(/^kb_docs_/, '').replace(/_idx$/, '')}_idx`; await client.query(`ALTER INDEX ${quoteIdent(row.indexname)} RENAME TO ${quoteIdent(newName)}`).catch(() => {}); } } await client.query(`ALTER TABLE ${quoteIdent(target)} RENAME TO kb_docs`); const targetIdx = await client.query(`SELECT indexname FROM pg_indexes WHERE schemaname='public' AND tablename='kb_docs'`); for (const row of targetIdx.rows) { const suffix = row.indexname.includes('vec') ? 'vec_idx' : row.indexname.includes('tsv') ? 'tsv_idx' : row.indexname.includes('parent') ? 'parent_idx' : null; if (suffix) await client.query(`ALTER INDEX ${quoteIdent(row.indexname)} RENAME TO ${quoteIdent('kb_docs_' + suffix)}`).catch(() => {}); } await client.query('COMMIT'); return { rolled_back_to: target, previous: hasCurrent.rows[0].ok ? currentBackup : null }; } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } module.exports = { listVersions, rollback };