From 32aa7733f82468688587995926722673d20b9e3a Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 16 May 2026 12:51:38 +0800 Subject: [PATCH] fix: refresh metadata after schema changes --- src/components/diff/SchemaDiffDialog.vue | 7 +++++ src/components/objects/ObjectBrowser.vue | 7 +++++ .../transfer/DataTransferDialog.vue | 3 +++ src/composables/useNavigationTargets.ts | 7 ++++- src/composables/useSqlExecution.ts | 9 +++++++ src/lib/sqlMetadataRefresh.ts | 19 ++++++++++++++ src/stores/connectionStore.ts | 12 +++++++++ tests/sqlMetadataRefresh.test.ts | 26 +++++++++++++++++++ 8 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/lib/sqlMetadataRefresh.ts create mode 100644 tests/sqlMetadataRefresh.test.ts diff --git a/src/components/diff/SchemaDiffDialog.vue b/src/components/diff/SchemaDiffDialog.vue index 5ace97cbc0..0e93cfee23 100644 --- a/src/components/diff/SchemaDiffDialog.vue +++ b/src/components/diff/SchemaDiffDialog.vue @@ -19,6 +19,7 @@ import { generateSyncSql, type TableDiff, } from "@/lib/schemaDiff"; +import { sqlMetadataRefreshScope } from "@/lib/sqlMetadataRefresh"; import { useToast } from "@/composables/useToast"; import { Loader2, Copy, Play, GitCompareArrows, ArrowLeftRight } from "lucide-vue-next"; @@ -285,6 +286,12 @@ async function executeSql() { } const failed = syncErrors.value.length; if (failed === 0) { + const scope = sqlMetadataRefreshScope(sql); + if (scope === "connection") { + await store.loadDatabases(targetConnectionId.value, { force: true }); + } else if (scope === "database") { + await store.refreshObjectListTreeNode(targetConnectionId.value, targetDatabase.value, targetSchema.value); + } toast(t("diff.syncSuccess"), 2000); open.value = false; } else { diff --git a/src/components/objects/ObjectBrowser.vue b/src/components/objects/ObjectBrowser.vue index 10385f11c0..271b522cf0 100644 --- a/src/components/objects/ObjectBrowser.vue +++ b/src/components/objects/ObjectBrowser.vue @@ -34,6 +34,7 @@ import { isSchemaAware } from "@/lib/databaseCapabilities"; import { buildTableSelectSql, qualifiedTableName } from "@/lib/tableSelectSql"; import { useToast } from "@/composables/useToast"; import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor"; +import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import QueryEditor from "@/components/editor/QueryEditor.vue"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; @@ -62,6 +63,7 @@ const emit = defineEmits<{ const { t } = useI18n(); const { toast } = useToast(); +const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const schemas = ref([]); @@ -263,6 +265,11 @@ async function confirmDrop() { : "contextMenu.dropTableSuccess"; toast(t(successKey, { name: row.name })); await reload(); + await connectionStore.refreshObjectListTreeNode( + props.connection.id, + props.database, + row.schema || selectedSchema.value, + ); } catch (e: any) { toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000); } diff --git a/src/components/transfer/DataTransferDialog.vue b/src/components/transfer/DataTransferDialog.vue index f66cbec21e..c207ac0791 100644 --- a/src/components/transfer/DataTransferDialog.vue +++ b/src/components/transfer/DataTransferDialog.vue @@ -249,6 +249,9 @@ async function startTransfer() { overallCancelled.value = nextState.cancelled; overallError.value = nextState.error; }); + if (createTable.value && !overallError.value && !overallCancelled.value) { + await store.refreshObjectListTreeNode(targetConnectionId.value, targetDatabase.value, targetSchema.value); + } } catch (e: any) { overallError.value = true; } diff --git a/src/composables/useNavigationTargets.ts b/src/composables/useNavigationTargets.ts index 2e878a8fa2..7c421cef7d 100644 --- a/src/composables/useNavigationTargets.ts +++ b/src/composables/useNavigationTargets.ts @@ -127,7 +127,7 @@ export function useNavigationTargets(dialogs: { const schema = dialogs.structurePrefillSchema?.value; if (connId && db) { try { - await connectionStore.loadTables(connId, db, schema || undefined); + await connectionStore.refreshObjectListTreeNode(connId, db, schema || undefined); } catch {} } return; @@ -146,6 +146,11 @@ export function useNavigationTargets(dialogs: { columns, primaryKeys: editablePrimaryKeys(connectionStore.getConfig(activeTab.connectionId)?.db_type, columns), }); + await connectionStore.refreshObjectListTreeNode( + activeTab.connectionId, + activeTab.database, + activeTab.tableMeta.schema, + ); await reloadData(); } catch (e: any) { toast(e?.message || String(e), 5000); diff --git a/src/composables/useSqlExecution.ts b/src/composables/useSqlExecution.ts index 1114b91e4c..78e9e6251a 100644 --- a/src/composables/useSqlExecution.ts +++ b/src/composables/useSqlExecution.ts @@ -5,6 +5,7 @@ import { useHistoryStore } from "@/stores/historyStore"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; import { classifySqlActivityKind } from "@/lib/historyActivityKind"; +import { sqlMetadataRefreshScope } from "@/lib/sqlMetadataRefresh"; import type { ConnectionConfig, QueryTab } from "@/types/database"; const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; @@ -80,6 +81,14 @@ export function useSqlExecution(deps: { operation: primarySqlOperation(sql), affected_rows: success ? tab.result?.affected_rows : undefined, }); + if (success) { + const scope = sqlMetadataRefreshScope(sql); + if (scope === "connection") { + await connectionStore.loadDatabases(tab.connectionId, { force: true }); + } else if (scope === "database") { + await connectionStore.refreshObjectListTreeNode(tab.connectionId, tab.database, tab.schema); + } + } } function cancelActiveExecution() { diff --git a/src/lib/sqlMetadataRefresh.ts b/src/lib/sqlMetadataRefresh.ts new file mode 100644 index 0000000000..eb1c305030 --- /dev/null +++ b/src/lib/sqlMetadataRefresh.ts @@ -0,0 +1,19 @@ +export type SqlMetadataRefreshScope = "none" | "connection" | "database"; + +const DATABASE_DDL_RE = /\b(CREATE|DROP)\s+(DATABASE|SCHEMA)\b/i; +const OBJECT_DDL_RE = + /\b(CREATE|ALTER|DROP|RENAME)\s+(OR\s+REPLACE\s+)?(((GLOBAL|LOCAL)\s+)?TEMP(ORARY)?\s+)?(MATERIALIZED\s+)?(TABLE|VIEW|INDEX|SEQUENCE|PROCEDURE|FUNCTION|TRIGGER|TYPE)\b/i; + +function stripSqlMetadataComments(sql: string): string { + return sql + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/--.*$/gm, " ") + .replace(/#.*$/gm, " "); +} + +export function sqlMetadataRefreshScope(sql: string): SqlMetadataRefreshScope { + const cleaned = stripSqlMetadataComments(sql); + if (DATABASE_DDL_RE.test(cleaned)) return "connection"; + if (OBJECT_DDL_RE.test(cleaned)) return "database"; + return "none"; +} diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index fa42fa58cd..30d75ee177 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -1157,6 +1157,17 @@ export const useConnectionStore = defineStore("connection", () => { await loadDatabases(connectionId, { force: true }); } + async function refreshObjectListTreeNode(connectionId: string, database: string, schema?: string) { + const config = getConfig(connectionId); + const shouldRefreshSchemaNode = schema && !(config?.db_type === "sqlserver" && schema.toLowerCase() === "dbo"); + const node = shouldRefreshSchemaNode ? findNode(treeNodes.value, `${connectionId}:${database}:${schema}`) : null; + if (node) { + await refreshTreeNode(node); + return; + } + await refreshDatabaseTreeNode(connectionId, database); + } + function isSchemaAwareDatabase(connectionId: string): boolean { return isSchemaAware(getConfig(connectionId)?.db_type); } @@ -1587,6 +1598,7 @@ export const useConnectionStore = defineStore("connection", () => { refreshSavedSqlTree, refreshTreeNode, refreshDatabaseTreeNode, + refreshObjectListTreeNode, connectedIds, connectionErrors, setConnectionError, diff --git a/tests/sqlMetadataRefresh.test.ts b/tests/sqlMetadataRefresh.test.ts new file mode 100644 index 0000000000..80f620f296 --- /dev/null +++ b/tests/sqlMetadataRefresh.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { sqlMetadataRefreshScope } from "../src/lib/sqlMetadataRefresh.ts"; + +test("database DDL refreshes the connection database list", () => { + assert.equal(sqlMetadataRefreshScope("CREATE DATABASE app;"), "connection"); + assert.equal(sqlMetadataRefreshScope("drop schema public cascade;"), "connection"); +}); + +test("object DDL refreshes the selected database tree", () => { + assert.equal(sqlMetadataRefreshScope("CREATE TABLE users (id int);"), "database"); + assert.equal(sqlMetadataRefreshScope("CREATE TEMP TABLE scratch (id int);"), "database"); + assert.equal(sqlMetadataRefreshScope("CREATE MATERIALIZED VIEW daily_users AS SELECT 1;"), "database"); + assert.equal(sqlMetadataRefreshScope("ALTER TABLE users ADD COLUMN name text;"), "database"); + assert.equal(sqlMetadataRefreshScope("DROP VIEW active_users;"), "database"); + assert.equal( + sqlMetadataRefreshScope("CREATE OR REPLACE FUNCTION f() RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;"), + "database", + ); +}); + +test("data-only SQL does not refresh metadata trees", () => { + assert.equal(sqlMetadataRefreshScope("SELECT * FROM users;"), "none"); + assert.equal(sqlMetadataRefreshScope("INSERT INTO users VALUES (1);"), "none"); + assert.equal(sqlMetadataRefreshScope("TRUNCATE TABLE users;"), "none"); +});