mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
@@ -159,6 +159,133 @@ describe("connectionStore database info", () => {
|
||||
await vi.waitFor(() => expect(store.getConfig(config.id)?.database_info?.productVersion).toBe("8.0.34"));
|
||||
});
|
||||
|
||||
it("refreshes physical table metadata after delayed ShardingSphere detection", async () => {
|
||||
const config = mysqlConnection();
|
||||
let resolveDatabaseInfo!: (value: { productName: string; productVersion: string }) => void;
|
||||
const databaseInfo = new Promise<{ productName: string; productVersion: string }>((resolve) => {
|
||||
resolveDatabaseInfo = resolve;
|
||||
});
|
||||
let useLogicalTables = false;
|
||||
const listTables = vi.fn().mockImplementation(async () => [
|
||||
{
|
||||
name: useLogicalTables ? "async_task" : "async_task_0",
|
||||
table_type: "BASE TABLE",
|
||||
comment: null,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
connectDb: vi.fn().mockResolvedValue(config.id),
|
||||
connectionDatabaseInfo: vi.fn(() => databaseInfo),
|
||||
connectionIdentifierQuote: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases: vi.fn().mockResolvedValue([{ name: "app" }]),
|
||||
listObjects: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnectionDatabaseInfo: vi.fn().mockImplementation(async () => {
|
||||
useLogicalTables = true;
|
||||
}),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "simple";
|
||||
await store.addConnection(config);
|
||||
await store.connect(config);
|
||||
await store.loadDatabases(config.id, { force: true });
|
||||
const databaseNode = store.treeNodes[0]?.children?.find((node) => node.type === "database" && node.database === "app");
|
||||
expect(databaseNode).toBeDefined();
|
||||
await store.loadTreeNodeChildren(databaseNode!, { force: true });
|
||||
expect(databaseNode?.children?.filter((node) => node.type === "table").map((node) => node.label)).toEqual(["async_task_0"]);
|
||||
|
||||
resolveDatabaseInfo({ productName: "MySQL", productVersion: "8.0.27-ShardingSphere-Proxy 5.5.2" });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const refreshedDatabaseNode = store.treeNodes[0]?.children?.find((node) => node.type === "database" && node.database === "app");
|
||||
expect(refreshedDatabaseNode?.children?.filter((node) => node.type === "table").map((node) => node.label)).toEqual(["async_task"]);
|
||||
});
|
||||
expect(listTables).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not let a pre-detection table request overwrite the refreshed logical tables", async () => {
|
||||
const config = mysqlConnection();
|
||||
let resolveDatabaseInfo!: (value: { productName: string; productVersion: string }) => void;
|
||||
const databaseInfo = new Promise<{ productName: string; productVersion: string }>((resolve) => {
|
||||
resolveDatabaseInfo = resolve;
|
||||
});
|
||||
let resolvePhysicalTables!: (value: Array<{ name: string; table_type: string; comment: null }>) => void;
|
||||
const physicalTables = new Promise<Array<{ name: string; table_type: string; comment: null }>>((resolve) => {
|
||||
resolvePhysicalTables = resolve;
|
||||
});
|
||||
let useLogicalTables = false;
|
||||
const listTables = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => physicalTables)
|
||||
.mockResolvedValue([
|
||||
{
|
||||
name: "async_task",
|
||||
table_type: "BASE TABLE",
|
||||
comment: null,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
connectDb: vi.fn().mockResolvedValue(config.id),
|
||||
connectionDatabaseInfo: vi.fn(() => databaseInfo),
|
||||
connectionIdentifierQuote: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases: vi.fn().mockResolvedValue([{ name: "app" }]),
|
||||
listObjects: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnectionDatabaseInfo: vi.fn().mockImplementation(async () => {
|
||||
useLogicalTables = true;
|
||||
}),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "simple";
|
||||
await store.addConnection(config);
|
||||
await store.connect(config);
|
||||
await store.loadDatabases(config.id, { force: true });
|
||||
const databaseNode = store.treeNodes[0]?.children?.find((node) => node.type === "database" && node.database === "app");
|
||||
expect(databaseNode).toBeDefined();
|
||||
databaseNode!.isExpanded = true;
|
||||
const physicalLoad = store.loadTreeNodeChildren(databaseNode!, { force: true });
|
||||
await vi.waitFor(() => expect(listTables).toHaveBeenCalledTimes(1));
|
||||
|
||||
resolveDatabaseInfo({ productName: "MySQL", productVersion: "8.0.27-ShardingSphere-Proxy 5.5.2" });
|
||||
await vi.waitFor(() => expect(listTables).toHaveBeenCalledTimes(2));
|
||||
resolvePhysicalTables([
|
||||
{
|
||||
name: "async_task_0",
|
||||
table_type: "BASE TABLE",
|
||||
comment: null,
|
||||
},
|
||||
]);
|
||||
await physicalLoad;
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const refreshedDatabaseNode = store.treeNodes[0]?.children?.find((node) => node.type === "database" && node.database === "app");
|
||||
expect(refreshedDatabaseNode?.children?.filter((node) => node.type === "table").map((node) => node.label)).toEqual(["async_task"]);
|
||||
});
|
||||
expect(useLogicalTables).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a successful connection when optional metadata refresh fails", async () => {
|
||||
const config = mysqlConnection();
|
||||
|
||||
|
||||
@@ -165,6 +165,15 @@ const MONGO_LEGACY_DRIVER_PROFILE = "mongodb-legacy";
|
||||
const MONGO_LEGACY_DRIVER_LABEL = "MongoDB (Legacy)";
|
||||
const XUGU_TABLE_CHILD_METADATA_AGENT_VERSION = "0.1.23";
|
||||
const SUPERSEDED_CONNECTION_ATTEMPT_MESSAGE = "Connection attempt was superseded by a newer attempt";
|
||||
const SHARDINGSPHERE_PROXY_VERSION_MARKER = "shardingsphere-proxy";
|
||||
|
||||
function usesShardingSphereLogicalTables(databaseInfo: DatabaseConnectionInfo | undefined): boolean {
|
||||
return databaseInfo?.productVersion?.toLowerCase().includes(SHARDINGSPHERE_PROXY_VERSION_MARKER) === true;
|
||||
}
|
||||
|
||||
function mysqlTableListSourceChanged(config: ConnectionConfig, databaseInfo: DatabaseConnectionInfo): boolean {
|
||||
return config.db_type === "mysql" && usesShardingSphereLogicalTables(config.database_info) !== usesShardingSphereLogicalTables(databaseInfo);
|
||||
}
|
||||
|
||||
function normalizeTableNameFilter(filter: Partial<TableNameFilter> | undefined | null): TableNameFilter {
|
||||
const normalizePatterns = (patterns: unknown): string[] => (Array.isArray(patterns) ? patterns.map((pattern) => (typeof pattern === "string" ? pattern.trim() : "")).filter(Boolean) : []);
|
||||
@@ -514,6 +523,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const successfulLocalConnectionAttempts = new Map<string, number>();
|
||||
const connectionStateRevisions = new Map<string, number>();
|
||||
const connectionErrorRevisions = new Map<string, number>();
|
||||
const tableListSourceRevisions = new Map<string, number>();
|
||||
const treeNodeLoads = new TreeNodeLoadRegistry();
|
||||
const filteredObjectGroupChildrenIds = new Set<string>();
|
||||
const primaryVisibleObjectRefreshInFlight = new Set<string>();
|
||||
@@ -1646,13 +1656,24 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function tableNameFilterMetadataExtra(filter: TableNameFilter | undefined): MetadataScopeInput["extra"] {
|
||||
return filter
|
||||
? {
|
||||
tableNameFilterInclude: filter.includePatterns,
|
||||
tableNameFilterExclude: filter.excludePatterns,
|
||||
}
|
||||
: undefined;
|
||||
function tableListSourceRevision(connectionId: string): number {
|
||||
return tableListSourceRevisions.get(connectionId) ?? 0;
|
||||
}
|
||||
|
||||
function bumpTableListSourceRevision(connectionId: string): void {
|
||||
tableListSourceRevisions.set(connectionId, tableListSourceRevision(connectionId) + 1);
|
||||
}
|
||||
|
||||
function tableNameFilterMetadataExtra(filter: TableNameFilter | undefined, sourceRevision: number): MetadataScopeInput["extra"] {
|
||||
return {
|
||||
tableListSourceRevision: sourceRevision,
|
||||
...(filter
|
||||
? {
|
||||
tableNameFilterInclude: filter.includePatterns,
|
||||
tableNameFilterExclude: filter.excludePatterns,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function setSidebarTableNameFilter(scopeKey: string, filter: TableNameFilter) {
|
||||
@@ -1888,6 +1909,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
catalog: options.node.catalog,
|
||||
});
|
||||
const tableNameFilter = effectiveTableNameFilterForNode(options.node, userTableNameFilter);
|
||||
const sourceRevision = tableListSourceRevision(options.node.connectionId);
|
||||
// A search must never truncate the fuzzy result set to the first page: the
|
||||
// target table can sort beyond it (e.g. "T_Erp_Nc_SuPlan_List" for
|
||||
// "erpncs" in a large ERP schema), which silently drops it from the first
|
||||
@@ -1909,14 +1931,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
limit: fetchLimit,
|
||||
offset: fetchOffset,
|
||||
sidebarDisplayMode: "grouped",
|
||||
extra: tableNameFilterMetadataExtra(tableNameFilter),
|
||||
extra: tableNameFilterMetadataExtra(tableNameFilter, sourceRevision),
|
||||
}),
|
||||
() => listTablesWithOptionalTableNameFilter(options.node.connectionId!, options.node.database!, options.querySchema, searchFilter, fetchLimit, fetchOffset, options.objectTypes, options.node.catalog, tableNameFilter),
|
||||
{ force: options.force },
|
||||
);
|
||||
const hasMore = searchFilter ? false : tables.length > options.pageSize;
|
||||
const pageTables = hasMore ? tables.slice(0, options.pageSize) : tables;
|
||||
indexCompletionTables(options.node.connectionId, options.node.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
if (tableListSourceRevision(options.node.connectionId) === sourceRevision) {
|
||||
indexCompletionTables(options.node.connectionId, options.node.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
}
|
||||
const objects = mergeTableInfosIntoObjects([], pageTables, options.effectiveSchema);
|
||||
const children = objectGroupChildrenFromObjects({
|
||||
node: options.node,
|
||||
@@ -2004,6 +2028,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
schema: options.effectiveSchema ?? options.querySchema,
|
||||
nodeKind: "simple-tables",
|
||||
});
|
||||
const sourceRevision = tableListSourceRevision(options.connectionId);
|
||||
// A search must never truncate the fuzzy result set to the first page (see
|
||||
// loadPagedTableGroupChildren); results are bounded by
|
||||
// SIDEBAR_TABLE_SEARCH_RESULT_BUDGET, and unfiltered loads keep the
|
||||
@@ -2021,14 +2046,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
limit: fetchLimit,
|
||||
offset: fetchOffset,
|
||||
sidebarDisplayMode: "simple",
|
||||
extra: tableNameFilterMetadataExtra(tableNameFilter),
|
||||
extra: tableNameFilterMetadataExtra(tableNameFilter, sourceRevision),
|
||||
}),
|
||||
() => listTablesWithOptionalTableNameFilter(options.connectionId, options.database, options.querySchema, searchFilter, fetchLimit, fetchOffset, undefined, undefined, tableNameFilter),
|
||||
{ force: options.force },
|
||||
);
|
||||
const hasMore = searchFilter ? false : tables.length > options.pageSize;
|
||||
const pageTables = hasMore ? tables.slice(0, options.pageSize) : tables;
|
||||
indexCompletionTables(options.connectionId, options.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
if (tableListSourceRevision(options.connectionId) === sourceRevision) {
|
||||
indexCompletionTables(options.connectionId, options.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
}
|
||||
|
||||
const children = buildTableTreeNodes({
|
||||
nodeId: options.nodeId,
|
||||
@@ -2878,6 +2905,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!current) return;
|
||||
if (expectedConfigFingerprint && connectionConfigFingerprint(current) !== expectedConfigFingerprint) return;
|
||||
if (JSON.stringify(current.database_info) === JSON.stringify(normalized)) return;
|
||||
const tableListSourceChanged = mysqlTableListSourceChanged(current, normalized);
|
||||
|
||||
await api.saveConnectionDatabaseInfo(connectionId, normalized);
|
||||
const index = connections.value.findIndex((connection) => connection.id === connectionId);
|
||||
@@ -2886,9 +2914,25 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const nextConnections = [...connections.value];
|
||||
nextConnections[index] = { ...nextConnections[index], database_info: normalized };
|
||||
connections.value = nextConnections;
|
||||
// Database info is reactive connection metadata, not tree structure. Keep
|
||||
// navigator node identities stable so an in-flight first expansion can
|
||||
// still apply its loaded children after this background refresh completes.
|
||||
if (tableListSourceChanged) {
|
||||
// ShardingSphere switches table enumeration from information_schema to SHOW FULL TABLES.
|
||||
// Discard physical-table metadata that may have loaded before background version detection completed.
|
||||
bumpTableListSourceRevision(connectionId);
|
||||
treeNodeLoads.cancelPrefix(connectionId);
|
||||
invalidateCompletionCache(connectionId);
|
||||
void invalidateSidebarTableSearchIndexesForConnection(connectionId);
|
||||
const connectionNode = findConnectionNode(connectionId);
|
||||
if (connectionNode?.isExpanded && connectedIds.value.has(connectionId)) {
|
||||
void refreshTreeNode(connectionNode).catch((error) => {
|
||||
console.debug("[DBX][connection-info:table-metadata-refresh-failed]", { connectionId, error });
|
||||
});
|
||||
} else {
|
||||
clearLoadedChildrenCache(connectionId);
|
||||
}
|
||||
}
|
||||
// Other database info is reactive connection metadata, not tree structure. Keep
|
||||
// navigator node identities stable so an in-flight first expansion can apply
|
||||
// its loaded children after this background refresh completes.
|
||||
}
|
||||
|
||||
async function refreshConnectedDatabaseInfo(connectionId: string, config: ConnectionConfig): Promise<void> {
|
||||
|
||||
@@ -3864,6 +3864,7 @@ for line in sys.stdin:
|
||||
#[test]
|
||||
fn shardingsphere_proxy_marker_is_ascii_case_insensitive_and_exact() {
|
||||
assert!(super::is_shardingsphere_proxy_version("5.7.22-ShardingSphere-Proxy 5.5.2"));
|
||||
assert!(super::is_shardingsphere_proxy_version("8.0.27-ShardingSphere-Proxy 5.5.2"));
|
||||
assert!(super::is_shardingsphere_proxy_version("8.0.36-SHARDINGSPHERE-PROXY 5.5.2"));
|
||||
assert!(!super::is_shardingsphere_proxy_version("8.0.36-ShardingSphere Proxy 5.5.2"));
|
||||
assert!(!super::is_shardingsphere_proxy_version("8.0.36-MySQL Community Server"));
|
||||
|
||||
Reference in New Issue
Block a user