import { useEffect, useState } from "react"; import { execSql, listColumns } from "./api"; import { DdlModal } from "./DdlModal"; import { ensureSession } from "./ObjectTree"; import { useWorkspaceStore } from "./store"; type MetaRow = Record; type TableMeta = { indexes: MetaRow[]; fks: MetaRow[]; triggers: MetaRow[]; }; const EMPTY_META: TableMeta = { indexes: [], fks: [], triggers: [] }; /** MySQL 协议族才走 information_schema 元数据(PG/Oracle 的系统目录另行适配) */ function isMysqlFamily(driver?: string) { return ["mysql", "mariadb", "tidb", "oceanbase", "polardb", "tdsql"].includes((driver || "").toLowerCase()); } /** 标识符转义进 information_schema 字符串比较 */ const esc = (s: string) => s.replace(/'/g, "''"); export function SchemaPane() { const schema = useWorkspaceStore((s) => s.schemaView); const set = useWorkspaceStore((s) => s.set); const conn = useWorkspaceStore((s) => s.connections.find((c) => c.id === s.activeConnectionId)); const [meta, setMeta] = useState(EMPTY_META); /** R2-F3:DDL 查看弹窗 */ const [showDdl, setShowDdl] = useState(false); const mysql = isMysqlFamily(conn?.driver); /** Redis 键的结构页不展示 DDL(服务端仅支持表/视图) */ const ddlEnabled = !!conn && (conn.driver || "").toLowerCase() !== "redis"; useEffect(() => { let alive = true; setMeta(EMPTY_META); if (!conn || !mysql || !schema || schema.loading || schema.error || !schema.table) return; (async () => { try { const sid = await ensureSession(conn); const db = esc(schema.database); const t = esc(schema.table); const run = (sqlText: string) => execSql(conn, sqlText, { database: schema.database, sessionId: sid, limit: 200 }); const [ix, fk, tg] = await Promise.all([ run( `SELECT INDEX_NAME AS name, IF(NON_UNIQUE=0,'UNIQUE','') AS uniq, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ', ') AS cols FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='${db}' AND TABLE_NAME='${t}' GROUP BY INDEX_NAME, NON_UNIQUE ORDER BY INDEX_NAME`, ), run( `SELECT CONSTRAINT_NAME AS name, COLUMN_NAME AS col, REFERENCED_TABLE_NAME AS refTable, REFERENCED_COLUMN_NAME AS refCol FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='${db}' AND TABLE_NAME='${t}' AND REFERENCED_TABLE_NAME IS NOT NULL`, ), run( `SELECT TRIGGER_NAME AS name, ACTION_TIMING AS timing, EVENT_MANIPULATION AS event, EVENT_OBJECT_TABLE AS onTable FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA='${db}' AND EVENT_OBJECT_TABLE='${t}'`, ), ]); if (alive) setMeta({ indexes: ix.rows, fks: fk.rows, triggers: tg.rows }); } catch { /* 元数据不可用时静默降级:结构页至少保底显示列 */ } })(); return () => { alive = false; }; }, [conn, mysql, schema?.database, schema?.table, schema?.loading, schema?.error]); if (!schema) { return (

表结构

在对象树点表,或右键「查看结构」。

); } /** E1:结构加载失败重试 —— 重新拉列元数据 */ const retryLoad = async () => { if (!conn || !schema) return; set("schemaView", { ...schema, loading: true, error: undefined }); try { const sid = await ensureSession(conn); const { columns } = await listColumns(conn, schema.database, schema.table, sid); set("schemaView", { database: schema.database, table: schema.table, columns, loading: false }); const pkCols = columns.filter((c) => c.key === "PRI").map((c) => c.name); set("resultMeta", { database: schema.database, table: schema.table, pkCols }); set("connState", "ok"); } catch (e) { const msg = e instanceof Error ? e.message : String(e); set("schemaView", { database: schema.database, table: schema.table, columns: [], loading: false, error: msg }); if (msg.includes("会话") || msg.toLowerCase().includes("session")) set("connState", "down"); } }; if (schema.loading) { return (
加载 {schema.table} 结构…
); } if (schema.error) { return (
{schema.error}
); } const str = (v: unknown) => (v == null ? "" : String(v)); return (
结构 · {schema.database ? `${schema.database}.` : ""} {schema.table}
{ddlEnabled && ( )}
{schema.columns.length === 0 ? (
无列信息
) : ( {schema.columns.map((c, i) => ( ))}
# 列名 类型 键 可空 默认值
{i + 1} {c.key === "PRI" ? 🔑 : null} {c.name} {c.type} {c.key === "PRI" ? ( PRI ) : ( "" )} {c.nullable ? "YES" : "NO"} {c.default == null ? NULL : String(c.default)}
)} {mysql && (
索引({meta.indexes.length}) {meta.indexes.length === 0 ? (
无索引
) : ( {meta.indexes.map((r, i) => ( ))}
索引名 唯一 列
{str(r.name)} {r.uniq ? "✓" : ""} {str(r.cols)}
)}
外键({meta.fks.length}) {meta.fks.length === 0 ? (
无外键
) : ( {meta.fks.map((r, i) => ( ))}
约束名 列 引用
{str(r.name)} {str(r.col)} {str(r.refTable)}.{str(r.refCol)}
)}
触发器({meta.triggers.length}) {meta.triggers.length === 0 ? (
无触发器
) : ( {meta.triggers.map((r, i) => ( ))}
触发器 时机 事件
{str(r.name)} {str(r.timing)} {str(r.event)}
)}
)}
{showDdl && conn && ( setShowDdl(false)} /> )}
); }