feat(sqlserver): support linked server browsing
This commit is contained in:
parent
fc8ae4b69f
commit
9e75f62dbf
|
|
@ -32,6 +32,7 @@ import {
|
|||
Upload,
|
||||
FileCode,
|
||||
Network,
|
||||
Server,
|
||||
PencilRuler,
|
||||
Search,
|
||||
FolderInput,
|
||||
|
|
@ -111,6 +112,7 @@ import { copyToClipboard } from "@/lib/clipboard";
|
|||
import { hasEnabledTransportLayers } from "@/lib/connectionTransport";
|
||||
import { formatShortcut } from "@/lib/shortcutRegistry";
|
||||
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSqlHistory";
|
||||
import { isSqlServerLinkedNode } from "@/lib/sqlServerLinkedServers";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
|
||||
import VisibleDatabasesDialog from "@/components/sidebar/VisibleDatabasesDialog.vue";
|
||||
|
|
@ -195,6 +197,14 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
|||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-amber-500" };
|
||||
case "database":
|
||||
return { icon: Database, colorClass: "text-yellow-500" };
|
||||
case "linked-server-root":
|
||||
return { icon: Network, colorClass: "text-blue-500" };
|
||||
case "linked-server":
|
||||
return { icon: Server, colorClass: "text-blue-400" };
|
||||
case "linked-server-catalog":
|
||||
return { icon: Database, colorClass: "text-yellow-500" };
|
||||
case "linked-server-schema":
|
||||
return { icon: FolderOpen, colorClass: "text-sky-400" };
|
||||
case "schema":
|
||||
return { icon: FolderOpen, colorClass: "text-sky-400" };
|
||||
case "table":
|
||||
|
|
@ -273,7 +283,7 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
|||
}
|
||||
|
||||
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]);
|
||||
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
|
||||
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "linked-server", "linked-server-catalog", "linked-server-schema", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
|
||||
|
||||
function isGroupLabel(node: TreeNode): boolean {
|
||||
return groupTypes.has(node.type);
|
||||
|
|
@ -283,6 +293,7 @@ function displayLabel(node: TreeNode): string {
|
|||
if (node.type === "load-more") return t(node.label);
|
||||
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
|
||||
if (node.type === "user-admin") return t(node.label);
|
||||
if (node.type === "linked-server-root") return t(node.label);
|
||||
if (node.label === "tree.defaultDatabase") return t(node.label);
|
||||
return isGroupLabel(node) ? t(node.label) : node.label;
|
||||
}
|
||||
|
|
@ -392,6 +403,14 @@ async function toggle() {
|
|||
}
|
||||
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
} else if (node.type === "linked-server-root" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServers(node.connectionId);
|
||||
} else if (node.type === "linked-server" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServerCatalogs(node);
|
||||
} else if (node.type === "linked-server-catalog" && node.connectionId) {
|
||||
await connectionStore.loadSqlServerLinkedServerSchemas(node);
|
||||
} else if (node.type === "linked-server-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.type === "materialized_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 && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
||||
|
|
@ -1447,6 +1466,7 @@ function requestDropTableChildObject() {
|
|||
}
|
||||
|
||||
function canDropTreeNode(node: TreeNode): boolean {
|
||||
if (isSqlServerLinkedNode(node)) return false;
|
||||
if (node.type === "table") return !!node.connectionId && !!node.database;
|
||||
if (node.type === "view" || node.type === "materialized_view" || node.type === "procedure" || node.type === "function") {
|
||||
return !!node.connectionId && !!node.database && !!dropObjectSqlOptionsForNode(node);
|
||||
|
|
@ -1684,7 +1704,7 @@ async function confirmBatchDrop() {
|
|||
}
|
||||
}
|
||||
|
||||
const isTableNotView = computed(() => props.node.type === "table");
|
||||
const isTableNotView = computed(() => props.node.type === "table" && !isSqlServerLinkedNode(props.node));
|
||||
|
||||
const supportsTruncate = computed(() => {
|
||||
return supportsTableTruncate(currentDatabaseType());
|
||||
|
|
@ -1692,7 +1712,7 @@ const supportsTruncate = computed(() => {
|
|||
|
||||
const canCreateTable = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (props.node.type === "database" || props.node.type === "schema" || props.node.type === "group-tables") && !!props.node.database && supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config));
|
||||
return (props.node.type === "database" || props.node.type === "schema" || props.node.type === "group-tables") && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config));
|
||||
});
|
||||
|
||||
const canCreateDatabase = computed(() => {
|
||||
|
|
@ -1712,7 +1732,7 @@ const canSetCreateDatabaseCharset = computed(() => {
|
|||
|
||||
const canDropDatabase = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return props.node.type === "database" && supportsDatabaseCreation(config?.db_type);
|
||||
return props.node.type === "database" && !isSqlServerLinkedNode(props.node) && supportsDatabaseCreation(config?.db_type);
|
||||
});
|
||||
|
||||
const canCreateSchema = computed(() => {
|
||||
|
|
@ -1722,7 +1742,7 @@ const canCreateSchema = computed(() => {
|
|||
|
||||
const canDropSchema = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return props.node.type === "schema" && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
return props.node.type === "schema" && !isSqlServerLinkedNode(props.node) && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
});
|
||||
|
||||
function tableAdminSqlOptions(): TableAdminSqlOptions {
|
||||
|
|
@ -2598,10 +2618,10 @@ const canOpenObjectBrowser = computed(() => {
|
|||
return supportsObjectBrowserTreeNode(rawDatabaseType(), props.node.type);
|
||||
});
|
||||
const canOpenTableImport = computed(() => {
|
||||
return props.node.type === "table" && !!props.node.database && supportsTableImport(currentDatabaseType());
|
||||
return props.node.type === "table" && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableImport(currentDatabaseType());
|
||||
});
|
||||
const canOpenStructureEditor = computed(() => {
|
||||
return props.node.type === "table" && !!props.node.database && supportsTableStructureEditing(currentTableStructureDatabaseType());
|
||||
return props.node.type === "table" && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableStructureEditing(currentTableStructureDatabaseType());
|
||||
});
|
||||
const canOpenFieldLineage = computed(() => {
|
||||
return props.node.type === "column" && !!props.node.database && !!props.node.tableName && supportsFieldLineage(currentDatabaseType());
|
||||
|
|
|
|||
|
|
@ -1217,6 +1217,7 @@ export default {
|
|||
tree: {
|
||||
savedSql: "SQL Library",
|
||||
userAdmin: "Users & Privileges",
|
||||
linkedServers: "Linked Servers",
|
||||
defaultDatabase: "Default DB",
|
||||
columns: "Columns",
|
||||
indexes: "Indexes",
|
||||
|
|
|
|||
|
|
@ -1216,6 +1216,7 @@ export default {
|
|||
tree: {
|
||||
savedSql: "SQL 库",
|
||||
userAdmin: "用户与权限",
|
||||
linkedServers: "链接服务器",
|
||||
defaultDatabase: "默认库",
|
||||
columns: "字段",
|
||||
indexes: "索引",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ export const syncSavedSqlDirectory = forward("syncSavedSqlDirectory");
|
|||
|
||||
// Schema
|
||||
export const listDatabases = forward("listDatabases");
|
||||
export const listSqlServerLinkedServers = forward("listSqlServerLinkedServers");
|
||||
export const listSqlServerLinkedServerCatalogs = forward("listSqlServerLinkedServerCatalogs");
|
||||
export const listSqlServerLinkedServerSchemas = forward("listSqlServerLinkedServerSchemas");
|
||||
export const listSqlServerLinkedServerTables = forward("listSqlServerLinkedServerTables");
|
||||
export const saveSchemaCache = forward("saveSchemaCache");
|
||||
export const loadSchemaCache = forward("loadSchemaCache");
|
||||
export const deleteSchemaCachePrefix = forward("deleteSchemaCachePrefix");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
ConnectionConfig,
|
||||
DatabaseInfo,
|
||||
LinkedServerInfo,
|
||||
TableInfo,
|
||||
ObjectInfo,
|
||||
ObjectSource,
|
||||
|
|
@ -392,6 +393,22 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
|
|||
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServers(connectionId: string): Promise<LinkedServerInfo[]> {
|
||||
return get(`/api/schema/sqlserver/linked-servers?${qs({ connection_id: connectionId })}`);
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerCatalogs(connectionId: string, server: string): Promise<DatabaseInfo[]> {
|
||||
return get(`/api/schema/sqlserver/linked-server-catalogs?${qs({ connection_id: connectionId, server })}`);
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerSchemas(connectionId: string, server: string, catalog: string): Promise<string[]> {
|
||||
return get(`/api/schema/sqlserver/linked-server-schemas?${qs({ connection_id: connectionId, server, catalog })}`);
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerTables(connectionId: string, server: string, catalog: string, schema: string, filter?: string, limit?: number, offset?: number): Promise<TableInfo[]> {
|
||||
return get(`/api/schema/sqlserver/linked-server-tables?${qs({ connection_id: connectionId, server, catalog, schema, filter, limit, offset })}`);
|
||||
}
|
||||
|
||||
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
|
||||
return post("/api/schema/cache", { cacheKey, payload });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
const SQLSERVER_LINKED_SCHEMA_PREFIX = "__dbx_sqlserver_linked__:";
|
||||
|
||||
export interface SqlServerLinkedSchemaRef {
|
||||
server: string;
|
||||
catalog: string;
|
||||
schema: string;
|
||||
}
|
||||
|
||||
export function encodeSqlServerLinkedSchema(ref: SqlServerLinkedSchemaRef): string {
|
||||
return `${SQLSERVER_LINKED_SCHEMA_PREFIX}${[ref.server, ref.catalog, ref.schema].map(encodeURIComponent).join("|")}`;
|
||||
}
|
||||
|
||||
export function parseSqlServerLinkedSchema(schema: string | undefined): SqlServerLinkedSchemaRef | null {
|
||||
if (!schema?.startsWith(SQLSERVER_LINKED_SCHEMA_PREFIX)) return null;
|
||||
const parts = schema.slice(SQLSERVER_LINKED_SCHEMA_PREFIX.length).split("|").map(decodeURIComponent);
|
||||
if (parts.length !== 3 || parts.some((part) => !part.trim())) return null;
|
||||
return { server: parts[0], catalog: parts[1], schema: parts[2] };
|
||||
}
|
||||
|
||||
export function isSqlServerLinkedNode(node: Pick<TreeNode, "linkedServer" | "linkedCatalog" | "linkedSchema" | "schema">): boolean {
|
||||
return !!node.linkedServer || !!node.linkedCatalog || !!node.linkedSchema || !!parseSqlServerLinkedSchema(node.schema);
|
||||
}
|
||||
|
||||
export function sqlServerLinkedTableSchema(node: Pick<TreeNode, "linkedServer" | "linkedCatalog" | "linkedSchema">): string | undefined {
|
||||
if (!node.linkedServer || !node.linkedCatalog || !node.linkedSchema) return undefined;
|
||||
return encodeSqlServerLinkedSchema({
|
||||
server: node.linkedServer,
|
||||
catalog: node.linkedCatalog,
|
||||
schema: node.linkedSchema,
|
||||
});
|
||||
}
|
||||
|
||||
export function quoteSqlServerIdentifier(name: string): string {
|
||||
return `[${name.replace(/\]/g, "]]")}]`;
|
||||
}
|
||||
|
||||
export function sqlServerLinkedTableName(ref: SqlServerLinkedSchemaRef, tableName: string): string {
|
||||
return [ref.server, ref.catalog, ref.schema, tableName].map(quoteSqlServerIdentifier).join(".");
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { DatabaseType } from "../types/database.ts";
|
||||
import { isSchemaAware, usesDatabaseObjectTreeMode } from "./databaseCapabilities.ts";
|
||||
import * as api from "./api.ts";
|
||||
import { parseSqlServerLinkedSchema, sqlServerLinkedTableName } from "./sqlServerLinkedServers.ts";
|
||||
|
||||
export interface BuildTableSelectSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
|
|
@ -42,6 +43,10 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
|
|||
return quoteTableIdentifier(databaseType, tableName);
|
||||
}
|
||||
if (isSchemaAware(databaseType) && !usesDatabaseObjectTreeMode(databaseType) && schema) {
|
||||
if (databaseType === "sqlserver") {
|
||||
const linked = parseSqlServerLinkedSchema(schema);
|
||||
if (linked) return sqlServerLinkedTableName(linked, tableName);
|
||||
}
|
||||
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
|
||||
}
|
||||
return quoteTableIdentifier(databaseType, tableName);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|||
import type {
|
||||
ConnectionConfig,
|
||||
DatabaseInfo,
|
||||
LinkedServerInfo,
|
||||
TableInfo,
|
||||
ObjectInfo,
|
||||
ObjectSource,
|
||||
|
|
@ -474,6 +475,22 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
|
|||
return invoke("list_databases", { connectionId });
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServers(connectionId: string): Promise<LinkedServerInfo[]> {
|
||||
return invoke("list_sqlserver_linked_servers", { connectionId });
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerCatalogs(connectionId: string, server: string): Promise<DatabaseInfo[]> {
|
||||
return invoke("list_sqlserver_linked_server_catalogs", { connectionId, server });
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerSchemas(connectionId: string, server: string, catalog: string): Promise<string[]> {
|
||||
return invoke("list_sqlserver_linked_server_schemas", { connectionId, server, catalog });
|
||||
}
|
||||
|
||||
export async function listSqlServerLinkedServerTables(connectionId: string, server: string, catalog: string, schema: string, filter?: string, limit?: number, offset?: number): Promise<TableInfo[]> {
|
||||
return invoke("list_sqlserver_linked_server_tables", { connectionId, server, catalog, schema, filter, limit, offset });
|
||||
}
|
||||
|
||||
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
|
||||
return invoke("save_schema_cache", { cacheKey, payload });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -207,6 +207,13 @@ export interface DatabaseInfo {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export interface LinkedServerInfo {
|
||||
name: string;
|
||||
product?: string | null;
|
||||
provider?: string | null;
|
||||
data_source?: string | null;
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
name: string;
|
||||
table_type: string;
|
||||
|
|
@ -391,6 +398,10 @@ export type TreeNodeType =
|
|||
| "connection"
|
||||
| "connection-group"
|
||||
| "database"
|
||||
| "linked-server-root"
|
||||
| "linked-server"
|
||||
| "linked-server-catalog"
|
||||
| "linked-server-schema"
|
||||
| "schema"
|
||||
| "table"
|
||||
| "view"
|
||||
|
|
@ -452,6 +463,9 @@ export interface TreeNode {
|
|||
pinned?: boolean;
|
||||
connectionId?: string;
|
||||
database?: string;
|
||||
linkedServer?: string;
|
||||
linkedCatalog?: string;
|
||||
linkedSchema?: string;
|
||||
mqTenant?: string;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
|
|
|
|||
|
|
@ -723,21 +723,20 @@ async fn linked_server_table_rows(
|
|||
schema: Option<&str>,
|
||||
table_name: Option<&str>,
|
||||
) -> Result<Vec<LinkedServerTableRow>, String> {
|
||||
let schema_param = schema.unwrap_or("");
|
||||
let table_name_param = table_name.unwrap_or("");
|
||||
let stream = client
|
||||
.query(
|
||||
"EXEC sp_tables_ex \
|
||||
@table_server = @P1, \
|
||||
@table_name = @P2, \
|
||||
@table_schema = @P3, \
|
||||
@table_catalog = @P4, \
|
||||
@table_type = '''TABLE'',''VIEW''', \
|
||||
@fUsePattern = 0",
|
||||
&[&server, &table_name_param, &schema_param, &catalog],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let sql = format!(
|
||||
"EXEC sp_tables_ex \
|
||||
@table_server = {}, \
|
||||
@table_name = {}, \
|
||||
@table_schema = {}, \
|
||||
@table_catalog = {}, \
|
||||
@table_type = '''TABLE'',''VIEW''', \
|
||||
@fUsePattern = 0",
|
||||
sqlserver_nstring_literal(server),
|
||||
sqlserver_optional_nstring_literal(table_name),
|
||||
sqlserver_optional_nstring_literal(schema),
|
||||
sqlserver_nstring_literal(catalog),
|
||||
);
|
||||
let stream = client.query(sql.as_str(), &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
|
|
@ -756,6 +755,14 @@ async fn linked_server_table_rows(
|
|||
.collect())
|
||||
}
|
||||
|
||||
fn sqlserver_optional_nstring_literal(value: Option<&str>) -> String {
|
||||
value.filter(|value| !value.trim().is_empty()).map(sqlserver_nstring_literal).unwrap_or_else(|| "NULL".to_string())
|
||||
}
|
||||
|
||||
fn sqlserver_nstring_literal(value: &str) -> String {
|
||||
format!("N'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn normalize_linked_server_table_type(value: Option<&str>) -> String {
|
||||
let upper = value.unwrap_or("TABLE").to_ascii_uppercase();
|
||||
if upper.contains("VIEW") {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,68 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
|
|||
retry_metadata_connection(state, connection_id, None, || list_databases_once(state, connection_id)).await
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_servers_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
) -> Result<Vec<db::LinkedServerInfo>, String> {
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_pool!(&connections, connection_id, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_linked_servers(&mut client).await;
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_catalogs_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
server: &str,
|
||||
) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_pool!(&connections, connection_id, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_linked_server_catalogs(&mut client, server).await;
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_schemas_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
server: &str,
|
||||
catalog: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_pool!(&connections, connection_id, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_linked_server_schemas(&mut client, server, catalog).await;
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_tables_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
server: &str,
|
||||
catalog: &str,
|
||||
schema: &str,
|
||||
filter: Option<&str>,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_pool!(&connections, connection_id, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_linked_server_tables(&mut client, server, catalog, schema, filter, limit, offset)
|
||||
.await;
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
log::info!("[list_databases] connection_id={connection_id}");
|
||||
{
|
||||
|
|
@ -491,6 +553,23 @@ async fn list_tables_once(
|
|||
.await
|
||||
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types));
|
||||
}
|
||||
if let Some(linked) = crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema) {
|
||||
if let Some(client) = extract_pool!(&connections, &pool_key, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_linked_server_tables(
|
||||
&mut client,
|
||||
&linked.server,
|
||||
&linked.catalog,
|
||||
&linked.schema,
|
||||
filter,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types));
|
||||
}
|
||||
}
|
||||
if object_types.is_some() {
|
||||
if let Some(client) = extract_pool!(&connections, &pool_key, SqlServer) {
|
||||
drop(connections);
|
||||
|
|
@ -1277,6 +1356,21 @@ pub async fn get_columns_core(
|
|||
drop(connections);
|
||||
return db::influxdb_driver::get_columns(&client, database, table).await.map(deduplicate_column_infos);
|
||||
}
|
||||
if let Some(linked) = crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema) {
|
||||
if let Some(client) = extract_pool!(&connections, &pool_key, SqlServer) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::get_linked_server_columns(
|
||||
&mut client,
|
||||
&linked.server,
|
||||
&linked.catalog,
|
||||
&linked.schema,
|
||||
table,
|
||||
)
|
||||
.await
|
||||
.map(deduplicate_column_infos);
|
||||
}
|
||||
}
|
||||
try_sqlserver!(connections, &pool_key, get_columns, schema, table);
|
||||
if let Some(client) = extract_pool!(&connections, &pool_key, Agent) {
|
||||
let fallback_config = db_config.clone();
|
||||
|
|
@ -1406,6 +1500,9 @@ pub async fn list_indexes_core(
|
|||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::IndexInfo>, String> {
|
||||
if crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema).is_some() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
||||
|
|
@ -1442,6 +1539,9 @@ pub async fn list_foreign_keys_core(
|
|||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ForeignKeyInfo>, String> {
|
||||
if crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema).is_some() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
|
||||
{
|
||||
|
|
@ -1471,6 +1571,9 @@ pub async fn list_triggers_core(
|
|||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::TriggerInfo>, String> {
|
||||
if crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema).is_some() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
|
||||
{
|
||||
|
|
@ -1566,6 +1669,9 @@ pub async fn get_table_ddl_core(
|
|||
table: &str,
|
||||
object_type: Option<db::ObjectSourceKind>,
|
||||
) -> Result<String, String> {
|
||||
if crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema).is_some() {
|
||||
return Err("DDL is not supported for SQL Server linked server tables".to_string());
|
||||
}
|
||||
if matches!(object_type, Some(db::ObjectSourceKind::View)) {
|
||||
let source =
|
||||
get_object_source_core(state, connection_id, database, schema, table, db::ObjectSourceKind::View).await?;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ mod tests;
|
|||
|
||||
pub use capabilities::{is_schema_aware, uses_fetch_first};
|
||||
pub use identifiers::{normalize_where_input, qualified_table_name, quote_table_identifier};
|
||||
pub(crate) use identifiers::{qualified_transfer_table, quote_transfer_identifier};
|
||||
pub(crate) use identifiers::{parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_transfer_identifier};
|
||||
pub use table_select::{build_count_table_sql, build_table_data_select_sql, build_table_select_sql};
|
||||
pub use types::*;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,41 @@
|
|||
use crate::models::connection::DatabaseType;
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
||||
use super::capabilities::{is_schema_aware, is_simple_informix_identifier};
|
||||
|
||||
pub const SQLSERVER_LINKED_SCHEMA_PREFIX: &str = "__dbx_sqlserver_linked__:";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SqlServerLinkedSchemaRef {
|
||||
pub server: String,
|
||||
pub catalog: String,
|
||||
pub schema: String,
|
||||
}
|
||||
|
||||
pub fn parse_sqlserver_linked_schema_ref(schema: &str) -> Option<SqlServerLinkedSchemaRef> {
|
||||
let payload = schema.strip_prefix(SQLSERVER_LINKED_SCHEMA_PREFIX)?;
|
||||
let mut parts = payload.split('|');
|
||||
let server = decode_linked_schema_part(parts.next()?)?;
|
||||
let catalog = decode_linked_schema_part(parts.next()?)?;
|
||||
let schema = decode_linked_schema_part(parts.next()?)?;
|
||||
if parts.next().is_some() || server.trim().is_empty() || catalog.trim().is_empty() || schema.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(SqlServerLinkedSchemaRef { server, catalog, schema })
|
||||
}
|
||||
|
||||
pub fn sqlserver_linked_table_name(linked: &SqlServerLinkedSchemaRef, table_name: &str) -> String {
|
||||
[linked.server.as_str(), linked.catalog.as_str(), linked.schema.as_str(), table_name]
|
||||
.into_iter()
|
||||
.map(|part| quote_table_identifier(Some(DatabaseType::SqlServer), part))
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
}
|
||||
|
||||
fn decode_linked_schema_part(value: &str) -> Option<String> {
|
||||
percent_decode_str(value).decode_utf8().ok().map(|value| value.into_owned())
|
||||
}
|
||||
|
||||
pub fn qualified_table_name(database_type: Option<DatabaseType>, schema: Option<&str>, table_name: &str) -> String {
|
||||
if database_type == Some(DatabaseType::Iotdb) {
|
||||
let table_name = quote_table_identifier(database_type, table_name);
|
||||
|
|
@ -18,6 +52,11 @@ pub fn qualified_table_name(database_type: Option<DatabaseType>, schema: Option<
|
|||
&& database_type != Some(DatabaseType::Jdbc)
|
||||
&& schema.is_some_and(|schema| !schema.trim().is_empty())
|
||||
{
|
||||
if database_type == Some(DatabaseType::SqlServer) {
|
||||
if let Some(linked) = schema.and_then(parse_sqlserver_linked_schema_ref) {
|
||||
return sqlserver_linked_table_name(&linked, table_name);
|
||||
}
|
||||
}
|
||||
return format!(
|
||||
"{}.{}",
|
||||
quote_table_identifier(database_type, schema.unwrap()),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ fn qualifies_schema_only_for_schema_aware_databases() {
|
|||
qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "root.test.device2"),
|
||||
"root.test.device2"
|
||||
);
|
||||
assert_eq!(
|
||||
qualified_table_name(
|
||||
Some(DatabaseType::SqlServer),
|
||||
Some("__dbx_sqlserver_linked__:ERP%5D01|Finance%20DB|dbo"),
|
||||
"Orders]2026"
|
||||
),
|
||||
"[ERP]]01].[Finance DB].[dbo].[Orders]]2026]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ pub struct DatabaseInfo {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkedServerInfo {
|
||||
pub name: String,
|
||||
pub product: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub data_source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TableInfo {
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ async fn main() {
|
|||
.route("/agents/progress/{operationId}", get(routes::agents::agent_progress))
|
||||
// Schema
|
||||
.route("/schema/databases", get(routes::schema::list_databases))
|
||||
.route("/schema/sqlserver/linked-servers", get(routes::schema::list_sqlserver_linked_servers))
|
||||
.route("/schema/sqlserver/linked-server-catalogs", get(routes::schema::list_sqlserver_linked_server_catalogs))
|
||||
.route("/schema/sqlserver/linked-server-schemas", get(routes::schema::list_sqlserver_linked_server_schemas))
|
||||
.route("/schema/sqlserver/linked-server-tables", get(routes::schema::list_sqlserver_linked_server_tables))
|
||||
.route("/schema/schemas", get(routes::schema::list_schemas))
|
||||
.route("/schema/tables", get(routes::schema::list_tables))
|
||||
.route("/schema/objects", get(routes::schema::list_objects))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ pub struct SchemaQuery {
|
|||
pub database: Option<String>,
|
||||
pub schema: Option<String>,
|
||||
pub table: Option<String>,
|
||||
pub server: Option<String>,
|
||||
pub catalog: Option<String>,
|
||||
pub filter: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub offset: Option<usize>,
|
||||
|
|
@ -28,6 +30,61 @@ pub async fn list_databases(
|
|||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_servers(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let result =
|
||||
dbx_core::schema::list_sqlserver_linked_servers_core(&state.app, &q.connection_id).await.map_err(AppError)?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_catalogs(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let server = q.server.as_deref().unwrap_or("");
|
||||
let result = dbx_core::schema::list_sqlserver_linked_server_catalogs_core(&state.app, &q.connection_id, server)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_schemas(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<Vec<String>>, AppError> {
|
||||
let server = q.server.as_deref().unwrap_or("");
|
||||
let catalog = q.catalog.as_deref().unwrap_or("");
|
||||
let result =
|
||||
dbx_core::schema::list_sqlserver_linked_server_schemas_core(&state.app, &q.connection_id, server, catalog)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn list_sqlserver_linked_server_tables(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let server = q.server.as_deref().unwrap_or("");
|
||||
let catalog = q.catalog.as_deref().unwrap_or("");
|
||||
let schema = q.schema.as_deref().unwrap_or("");
|
||||
let result = dbx_core::schema::list_sqlserver_linked_server_tables_core(
|
||||
&state.app,
|
||||
&q.connection_id,
|
||||
server,
|
||||
catalog,
|
||||
schema,
|
||||
q.filter.as_deref(),
|
||||
q.limit,
|
||||
q.offset,
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn list_schemas(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,57 @@ pub async fn list_databases(
|
|||
dbx_core::schema::list_databases_core(&state, &connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_sqlserver_linked_servers(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<db::LinkedServerInfo>, String> {
|
||||
dbx_core::schema::list_sqlserver_linked_servers_core(&state, &connection_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_sqlserver_linked_server_catalogs(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
server: String,
|
||||
) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
dbx_core::schema::list_sqlserver_linked_server_catalogs_core(&state, &connection_id, &server).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_sqlserver_linked_server_schemas(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
server: String,
|
||||
catalog: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
dbx_core::schema::list_sqlserver_linked_server_schemas_core(&state, &connection_id, &server, &catalog).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_sqlserver_linked_server_tables(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
server: String,
|
||||
catalog: String,
|
||||
schema: String,
|
||||
filter: Option<String>,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
dbx_core::schema::list_sqlserver_linked_server_tables_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
&server,
|
||||
&catalog,
|
||||
&schema,
|
||||
filter.as_deref(),
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_schemas(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -374,6 +374,10 @@ pub fn run() {
|
|||
commands::plugins::install_jdbc_plugin_local,
|
||||
commands::plugins::uninstall_jdbc_plugin,
|
||||
commands::schema::list_databases,
|
||||
commands::schema::list_sqlserver_linked_servers,
|
||||
commands::schema::list_sqlserver_linked_server_catalogs,
|
||||
commands::schema::list_sqlserver_linked_server_schemas,
|
||||
commands::schema::list_sqlserver_linked_server_tables,
|
||||
commands::schema::list_tables,
|
||||
commands::schema::list_objects,
|
||||
commands::schema::list_completion_objects,
|
||||
|
|
|
|||
Loading…
Reference in New Issue