feat(sqlserver): support linked server browsing

This commit is contained in:
t8y2 committed 2026-06-18 13:17:44 +08:00
1 parent fc8ae4b69f
commit 9e75f62dbf
20 files changed
+617 -32

No files matched your search

+190 -9
View File
@@ -48,6 +48,7 @@ import { prunePinnedTreeNodeIdsForConnection } from "@/lib/pinnedTreeNodeIds";
import { supportsDatabaseUserAdmin } from "@/lib/databaseUserAdmin";
import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
import { useSettingsStore } from "@/stores/settingsStore";
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/sqlServerLinkedServers";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
@@ -58,6 +59,46 @@ function sidebarObjectGroupPageSize(): number {
}
type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip";
function nodeIdPart(value: string): string {
return encodeURIComponent(value);
}
function sqlServerLinkedRootId(connectionId: string): string {
return `${connectionId}:__linked_servers`;
}
function sqlServerLinkedServerId(connectionId: string, server: string): string {
return `${sqlServerLinkedRootId(connectionId)}:${nodeIdPart(server)}`;
}
function sqlServerLinkedCatalogId(connectionId: string, server: string, catalog: string): string {
return `${sqlServerLinkedServerId(connectionId, server)}:${nodeIdPart(catalog)}`;
}
function sqlServerLinkedRuntimeDatabase(config?: ConnectionConfig): string {
return config?.database?.trim() || "master";
}
function sqlServerLinkedRootNode(connectionId: string, database: string): TreeNode {
return {
id: sqlServerLinkedRootId(connectionId),
label: "tree.linkedServers",
type: "linked-server-root",
connectionId,
database,
isExpanded: false,
children: [],
};
}
function ensureSqlServerLinkedRootNode(connectionId: string, children: TreeNode[], config?: ConnectionConfig): TreeNode[] {
if (config?.db_type !== "sqlserver") return children;
if (children.some((child) => child.type === "linked-server-root" || child.id === sqlServerLinkedRootId(connectionId))) {
return children;
}
return [...children, sqlServerLinkedRootNode(connectionId, sqlServerLinkedRuntimeDatabase(config))];
}
// Temporary storage for DataGrip import payload (used to read Keychain passwords after import)
let pendingDataGripPayload: { format: "datagrip-import"; dataSources: string; dataSourcesLocal?: string } | null = null;
@@ -499,7 +540,10 @@ 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 = sortSidebarTreeChildrenForParent(node, normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children)), node.connectionId ? getConfig(node.connectionId)?.db_type : undefined);
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
const cachedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
const childrenWithLinkedServers = node.type === "connection" && node.connectionId ? ensureSqlServerLinkedRootNode(node.connectionId, cachedChildren, config) : cachedChildren;
const normalizedChildren = sortSidebarTreeChildrenForParent(node, childrenWithLinkedServers, config?.db_type);
setChildren(node, node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, normalizedChildren, node) : normalizedChildren);
node.isExpanded = true;
return { hit: true, isStale: decoded.isStale };
@@ -930,13 +974,29 @@ export const useConnectionStore = defineStore("connection", () => {
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const children = withSavedSqlRoot(
connectionId,
buildDatabaseTreeNodes(connectionId, visibleDatabases, {
includeDefaultWhenEmpty: usesTreeSchemaMode(effectiveDbType) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
}),
node,
);
const databaseNodes = buildDatabaseTreeNodes(connectionId, visibleDatabases, {
includeDefaultWhenEmpty: usesTreeSchemaMode(effectiveDbType) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
});
if (config?.db_type === "sqlserver") {
const linkedServers = await api.listSqlServerLinkedServers(connectionId).catch(() => []);
const linkedDatabase = sqlServerLinkedRuntimeDatabase(config);
databaseNodes.push({
...sqlServerLinkedRootNode(connectionId, linkedDatabase),
children: linkedServers.map((server) => ({
id: sqlServerLinkedServerId(connectionId, server.name),
label: server.name,
type: "linked-server",
connectionId,
database: linkedDatabase,
linkedServer: server.name,
comment: [server.product, server.provider, server.data_source].filter(Boolean).join(" / ") || null,
isExpanded: false,
children: [],
})),
});
if (linkedServers.length > 0) loadedTreeNodeChildrenIds.value.add(sqlServerLinkedRootId(connectionId));
}
const children = withSavedSqlRoot(connectionId, databaseNodes, node);
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
}
@@ -1255,6 +1315,116 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadSqlServerLinkedServers(connectionId: string, options?: LoadTreeOptions) {
const node = findNode(treeNodes.value, sqlServerLinkedRootId(connectionId));
if (!node) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const config = getConfig(connectionId);
const database = sqlServerLinkedRuntimeDatabase(config);
const linkedServers = await api.listSqlServerLinkedServers(connectionId);
setChildren(
node,
linkedServers.map((server) => ({
id: sqlServerLinkedServerId(connectionId, server.name),
label: server.name,
type: "linked-server" as const,
connectionId,
database,
linkedServer: server.name,
comment: [server.product, server.provider, server.data_source].filter(Boolean).join(" / ") || null,
isExpanded: false,
children: [],
})),
);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
async function loadSqlServerLinkedServerCatalogs(node: TreeNode, options?: LoadTreeOptions) {
if (!node.connectionId || !node.linkedServer) return;
const connectionId = node.connectionId;
const server = node.linkedServer;
node.isLoading = true;
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const catalogs = await api.listSqlServerLinkedServerCatalogs(connectionId, server);
const database = node.database || sqlServerLinkedRuntimeDatabase(getConfig(connectionId));
setChildren(
node,
catalogs
.filter((catalog) => catalog.name.trim())
.map((catalog) => ({
id: sqlServerLinkedCatalogId(connectionId, server, catalog.name),
label: catalog.name,
type: "linked-server-catalog" as const,
connectionId,
database,
linkedServer: server,
linkedCatalog: catalog.name,
isExpanded: false,
children: [],
})),
);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
async function loadSqlServerLinkedServerSchemas(node: TreeNode, options?: LoadTreeOptions) {
if (!node.connectionId || !node.linkedServer || !node.linkedCatalog) return;
node.isLoading = true;
try {
await ensureConnected(node.connectionId);
if (useCachedChildren(node, options)) return;
const schemas = await api.listSqlServerLinkedServerSchemas(node.connectionId, node.linkedServer, node.linkedCatalog);
const database = node.database || sqlServerLinkedRuntimeDatabase(getConfig(node.connectionId));
setChildren(
node,
sortSidebarNames(schemas)
.filter((schema) => schema.trim())
.map((schema) => {
const encodedSchema = encodeSqlServerLinkedSchema({
server: node.linkedServer!,
catalog: node.linkedCatalog!,
schema,
});
return {
id: `${node.connectionId}:${database}:${encodedSchema}`,
label: schema,
type: "linked-server-schema" as const,
connectionId: node.connectionId,
database,
schema: encodedSchema,
linkedServer: node.linkedServer,
linkedCatalog: node.linkedCatalog,
linkedSchema: schema,
isExpanded: false,
children: [],
};
}),
);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(node.connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
async function loadTables(connectionId: string, database: string, schema?: string, options?: LoadTreeOptions) {
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
@@ -1435,7 +1605,7 @@ export const useConnectionStore = defineStore("connection", () => {
const config = getConfig(connectionId);
const metadataCapabilities = getTableMetadataCapabilities(effectiveDatabaseTypeForConnection(config));
if (node.type === "table") {
if (node.type === "table" && !parseSqlServerLinkedSchema(schema)) {
if (metadataCapabilities.indexes) {
children.push({
id: `${parentId}:__indexes`,
@@ -1665,6 +1835,14 @@ export const useConnectionStore = defineStore("connection", () => {
}
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await loadTables(node.connectionId, node.database, node.schema, options);
} else if (node.type === "linked-server-root" && node.connectionId) {
await loadSqlServerLinkedServers(node.connectionId, options);
} else if (node.type === "linked-server" && node.connectionId) {
await loadSqlServerLinkedServerCatalogs(node, options);
} else if (node.type === "linked-server-catalog" && node.connectionId) {
await loadSqlServerLinkedServerSchemas(node, options);
} else if (node.type === "linked-server-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.type === "materialized_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 && hasTreeNodeDatabaseContext(node) && node.tableName) {
@@ -2786,6 +2964,9 @@ export const useConnectionStore = defineStore("connection", () => {
loadMongoCollections,
loadSchemas,
loadSqlServerDatabaseObjects,
loadSqlServerLinkedServers,
loadSqlServerLinkedServerCatalogs,
loadSqlServerLinkedServerSchemas,
loadTables,
loadObjectGroupChildren,
loadMoreObjectGroupChildren,