fix: refresh metadata after schema changes

This commit is contained in:
t8y2
2026-05-16 12:51:38 +08:00
parent 70e324989c
commit 32aa7733f8
8 changed files with 89 additions and 1 deletions
+7
View File
@@ -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 {
+7
View File
@@ -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<string[]>([]);
@@ -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);
}
@@ -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;
}
+6 -1
View File
@@ -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);
+9
View File
@@ -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() {
+19
View File
@@ -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";
}
+12
View File
@@ -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,
+26
View File
@@ -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");
});