feat(sqlserver): support linked server browsing
This commit is contained in:
1 parent
fc8ae4b69f
commit
9e75f62dbf
20 files changed
+617
-32
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user