merge: catalogless schema tree fix

This commit is contained in:
t8y2 2026-05-19 15:11:05 +08:00
commit 9cbca5fc3a
9 changed files with 223 additions and 41 deletions

View File

@ -105,6 +105,7 @@ import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType }
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { hexToRgba } from "@/lib/color";
import { focusSidebarRenameInput, shouldPreventRenameCloseAutoFocus } from "@/lib/sidebarRenameFocus";
import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
@ -250,6 +251,7 @@ function isGroupLabel(node: TreeNode): boolean {
function displayLabel(node: TreeNode): string {
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
if (node.label === "tree.defaultDatabase") return t(node.label);
return isGroupLabel(node) ? t(node.label) : node.label;
}
@ -308,7 +310,7 @@ async function toggle() {
const tabTitle = `${node.database}.${node.label}`;
const tab = queryStore.createTab(node.connectionId, node.database, tabTitle, "mongo");
queryStore.updateSql(tab, node.label);
} else if (node.type === "database" && node.connectionId && node.database) {
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "sqlserver") {
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
@ -317,17 +319,36 @@ async function toggle() {
} else {
await connectionStore.loadTables(node.connectionId, node.database);
}
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
} else if (
(node.type === "table" || node.type === "view") &&
node.connectionId &&
hasTreeNodeDatabaseContext(node)
) {
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-columns" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-indexes" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-indexes" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-fkeys" && node.connectionId && node.database && node.tableName) {
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-triggers" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id);
}
emit("node-toggled", node, wasExpanded);

View File

@ -744,6 +744,7 @@ export default {
},
tree: {
savedSql: "SQL Library",
defaultDatabase: "Default DB",
columns: "Columns",
indexes: "Indexes",
foreignKeys: "Foreign Keys",

View File

@ -641,6 +641,7 @@ export default {
},
tree: {
savedSql: "Biblioteca SQL",
defaultDatabase: "Base predeterminada",
columns: "Columnas",
indexes: "Índices",
foreignKeys: "Claves foráneas",

View File

@ -725,6 +725,7 @@ export default {
},
tree: {
savedSql: "SQL 库",
defaultDatabase: "默认库",
columns: "字段",
indexes: "索引",
foreignKeys: "外键",

View File

@ -1,13 +1,38 @@
import type { DatabaseInfo, TreeNode } from "@/types/database";
import { DEFAULT_DATABASE_TREE_LABEL } from "./treeNodeContext";
export function buildDatabaseTreeNodes(connectionId: string, databases: DatabaseInfo[]): TreeNode[] {
return databases.map((db) => ({
id: `${connectionId}:${db.name}`,
label: db.name,
type: "database" as const,
connectionId,
database: db.name,
isExpanded: false,
children: [],
}));
export function buildDatabaseTreeNodes(
connectionId: string,
databases: DatabaseInfo[],
options: { includeDefaultWhenEmpty?: boolean } = {},
): TreeNode[] {
const nodes = databases.flatMap((db) => {
const name = db.name.trim();
if (!name) return [];
return [
{
id: `${connectionId}:${name}`,
label: name,
type: "database" as const,
connectionId,
database: name,
isExpanded: false,
children: [],
},
];
});
if (nodes.length > 0 || !options.includeDefaultWhenEmpty) return nodes;
return [
{
id: `${connectionId}:`,
label: DEFAULT_DATABASE_TREE_LABEL,
type: "database" as const,
connectionId,
database: "",
isExpanded: false,
children: [],
},
];
}

View File

@ -0,0 +1,42 @@
import type { TreeNode } from "@/types/database";
export const DEFAULT_DATABASE_TREE_LABEL = "tree.defaultDatabase";
export function hasTreeNodeDatabaseContext(node: Pick<TreeNode, "database">): node is Pick<TreeNode, "database"> & {
database: string;
} {
return node.database != null;
}
function schemaCacheKey(...parts: string[]): string {
return parts.map((part) => encodeURIComponent(part)).join(":");
}
export function treeNodeSchemaCachePrefix(node: TreeNode): string | null {
if (node.type === "connection" && node.connectionId) {
return `${schemaCacheKey(node.connectionId)}:`;
}
if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
return `${schemaCacheKey(node.connectionId, node.database)}:`;
}
if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
return `${schemaCacheKey(node.connectionId, node.database, node.schema)}:`;
}
return null;
}
export function normalizeCataloglessDatabaseNodes(nodes: TreeNode[]): TreeNode[] {
return nodes.map((node) => {
const normalized =
node.type === "database" && node.database === "" && !node.label.trim()
? { ...node, label: DEFAULT_DATABASE_TREE_LABEL }
: node;
return normalized.children
? {
...normalized,
children: normalizeCataloglessDatabaseNodes(normalized.children),
}
: normalized;
});
}

View File

@ -32,6 +32,11 @@ import {
expandCachedObjectBrowserNodes,
objectGroupRefreshParentId,
} from "@/lib/tableTree";
import {
hasTreeNodeDatabaseContext,
normalizeCataloglessDatabaseNodes,
treeNodeSchemaCachePrefix,
} from "@/lib/treeNodeContext";
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/schemaTreeCache";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
@ -355,7 +360,7 @@ export const useConnectionStore = defineStore("connection", () => {
const payload = await api.loadSchemaCache<unknown>(cacheKey).catch(() => null);
const decoded = decodeSchemaTreeCache<TreeNode[]>(payload);
if (!decoded) return { hit: false, isStale: false };
const normalizedChildren = expandCachedObjectBrowserNodes(decoded.children);
const normalizedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
setChildren(
node,
node.type === "connection" && node.connectionId
@ -394,16 +399,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
function schemaCachePrefixForNode(node: TreeNode): string | null {
if (node.type === "connection" && node.connectionId) {
return `${schemaCacheKey(node.connectionId)}:`;
}
if (node.type === "database" && node.connectionId && node.database) {
return `${schemaCacheKey(node.connectionId, node.database)}:`;
}
if (node.type === "schema" && node.connectionId && node.database && node.schema) {
return `${schemaCacheKey(node.connectionId, node.database, node.schema)}:`;
}
return null;
return treeNodeSchemaCachePrefix(node);
}
async function clearPersistedTreeCacheForNode(node: TreeNode) {
@ -692,7 +688,13 @@ export const useConnectionStore = defineStore("connection", () => {
);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
const children = withSavedSqlRoot(connectionId, buildDatabaseTreeNodes(connectionId, visibleDatabases), node);
const children = withSavedSqlRoot(
connectionId,
buildDatabaseTreeNodes(connectionId, visibleDatabases, {
includeDefaultWhenEmpty: usesTreeSchemaMode(config?.db_type),
}),
node,
);
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
}
@ -1161,7 +1163,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
await loadMongoCollections(node.connectionId, node.database);
} else if (node.type === "database" && node.connectionId && node.database) {
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = getConfig(node.connectionId);
if (config?.db_type === "sqlserver") {
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
@ -1170,17 +1172,36 @@ export const useConnectionStore = defineStore("connection", () => {
} else {
await loadTables(node.connectionId, node.database, undefined, options);
}
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await loadTables(node.connectionId, node.database, node.schema, options);
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
} else if (
(node.type === "table" || node.type === "view") &&
node.connectionId &&
hasTreeNodeDatabaseContext(node)
) {
await loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-columns" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-indexes" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-indexes" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-fkeys" && node.connectionId && node.database && node.tableName) {
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
} else if (
node.type === "group-triggers" &&
node.connectionId &&
hasTreeNodeDatabaseContext(node) &&
node.tableName
) {
await loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id);
} else if (
node.type === "group-tables" ||

View File

@ -3,11 +3,7 @@ import test from "node:test";
import { buildDatabaseTreeNodes } from "../../apps/desktop/src/lib/databaseTree.ts";
test("设置默认库后侧边栏数据库树仍保留全部数据库", () => {
const nodes = buildDatabaseTreeNodes("conn-1", [
{ name: "campaign_data" },
{ name: "cms" },
{ name: "mk_campaign" },
]);
const nodes = buildDatabaseTreeNodes("conn-1", [{ name: "campaign_data" }, { name: "cms" }, { name: "mk_campaign" }]);
assert.deepEqual(
nodes.map((node) => node.database),
@ -15,3 +11,27 @@ test("设置默认库后侧边栏数据库树仍保留全部数据库", () => {
);
assert.equal(nodes.find((node) => node.database === "mk_campaign")?.id, "conn-1:mk_campaign");
});
test("catalogless database metadata gets a visible default node", () => {
const nodes = buildDatabaseTreeNodes("conn-1", [{ name: " " }], { includeDefaultWhenEmpty: true });
assert.deepEqual(nodes, [
{
id: "conn-1:",
label: "tree.defaultDatabase",
type: "database",
connectionId: "conn-1",
database: "",
isExpanded: false,
children: [],
},
]);
});
test("tree schema mode can show a default node when no catalog is returned", () => {
const nodes = buildDatabaseTreeNodes("conn-1", [], { includeDefaultWhenEmpty: true });
assert.equal(nodes.length, 1);
assert.equal(nodes[0].database, "");
assert.equal(nodes[0].label, "tree.defaultDatabase");
});

View File

@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
hasTreeNodeDatabaseContext,
normalizeCataloglessDatabaseNodes,
treeNodeSchemaCachePrefix,
} from "../../apps/desktop/src/lib/treeNodeContext.ts";
import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
test("treats empty database string as a valid catalogless context", () => {
assert.equal(hasTreeNodeDatabaseContext({ database: "" }), true);
assert.equal(hasTreeNodeDatabaseContext({ database: "app" }), true);
assert.equal(hasTreeNodeDatabaseContext({}), false);
});
test("builds cache prefixes for catalogless database and schema nodes", () => {
const databaseNode: TreeNode = {
id: "conn:",
label: "tree.defaultDatabase",
type: "database",
connectionId: "conn",
database: "",
};
const schemaNode: TreeNode = {
id: "conn::APP",
label: "APP",
type: "schema",
connectionId: "conn",
database: "",
schema: "APP",
};
assert.equal(treeNodeSchemaCachePrefix(databaseNode), "conn::");
assert.equal(treeNodeSchemaCachePrefix(schemaNode), "conn::APP:");
});
test("normalizes legacy blank cached catalogless database labels", () => {
const nodes = normalizeCataloglessDatabaseNodes([
{
id: "conn:",
label: "",
type: "database",
connectionId: "conn",
database: "",
children: [],
},
]);
assert.equal(nodes[0].label, "tree.defaultDatabase");
});