diff --git a/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java b/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java index 0623abd484..3b9f31741a 100644 --- a/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java +++ b/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java @@ -165,7 +165,7 @@ public final class KingbaseAgent extends PostgresLikeAgent { public List listTables(String schema) { if (postgresCatalogMode) return super.listTables(schema); if (isMysqlCompatMode()) { - return listTables(schema, "table_type IN ('BASE TABLE', 'VIEW')"); + return queryMysqlCompatTables(schema, MetadataListConstraints.NONE); } return queryRegularTables(schema, MetadataListConstraints.NONE); } @@ -222,17 +222,26 @@ public final class KingbaseAgent extends PostgresLikeAgent { return unchecked(() -> { List result = new ArrayList<>(); List args = new ArrayList<>(); - StringBuilder sql = new StringBuilder("SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = ?"); - args.add(effectiveSchema(schema)); + StringBuilder sql = new StringBuilder("SELECT t.table_name, t.table_type, ") + .append(KINGBASE_DESCRIPTION).append(" AS table_comment ") + .append("FROM information_schema.tables t ") + .append("LEFT JOIN sys_catalog.sys_namespace n ON ").append(KINGBASE_SCHEMA_NAME) + .append(" = CAST(t.table_schema AS varchar(256)) ") + .append("LEFT JOIN sys_catalog.sys_class c ON ").append(KINGBASE_REL_NAMESPACE) + .append(" = ").append(KINGBASE_NAMESPACE_OID) + .append(" AND ").append(KINGBASE_REL_NAME).append(" = CAST(t.table_name AS varchar(256)) ") + .append("LEFT JOIN sys_catalog.sys_description d ON CAST(d.objoid AS varchar(64)) = ") + .append(KINGBASE_REL_OID).append(" AND d.objsubid = 0 ") + .append("WHERE t.table_schema = ").append(sqlString(effectiveSchema(schema))); appendMysqlCompatTableTypePredicate(sql, args, constraints); - MetadataSqlSupport.appendNameFilter(sql, args, "table_name", constraints); - sql.append(" ORDER BY table_name"); + MetadataSqlSupport.appendNameFilter(sql, args, "t.table_name", constraints); + sql.append(" ORDER BY t.table_name"); MetadataSqlSupport.appendLiteralLimitOffset(sql, constraints); try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) { MetadataSqlSupport.bind(stmt, args); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { - result.add(new TableInfo(rs.getString(1), normalizeTableType(rs.getString(2)))); + result.add(new TableInfo(rs.getString(1), normalizeTableType(rs.getString(2)), rs.getString(3))); } } } @@ -699,23 +708,6 @@ public final class KingbaseAgent extends PostgresLikeAgent { }); } - private List listTables(String schema, String tableTypePredicate) { - return unchecked(() -> { - List result = new ArrayList<>(); - String sql = "SELECT table_name, table_type " + - "FROM information_schema.tables " + - "WHERE table_schema = " + sqlString(effectiveSchema(schema)) + " AND " + tableTypePredicate + " " + - "ORDER BY table_name"; - try (Statement stmt = requireConnected().createStatement(); - ResultSet rs = stmt.executeQuery(sql)) { - while (rs.next()) { - result.add(new TableInfo(rs.getString(1), normalizeTableType(rs.getString(2)))); - } - } - return result; - }); - } - private static boolean isUnconstrained(MetadataListConstraints constraints) { return !constraints.hasFilter() && !constraints.hasLimit() && !constraints.hasOffset() && !constraints.hasObjectTypes(); } diff --git a/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java b/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java index cbd7a65d89..58ade7266f 100644 --- a/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java +++ b/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java @@ -200,20 +200,45 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { } @Test - void mysqlCompatListTablesUsesInformationSchema() { + void mysqlCompatListTablesUsesInformationSchemaAndPreservesComments() { List sql = new ArrayList<>(); KingbaseAgent agent = new KingbaseAgent(); agent.setMysqlCompatMode(true); TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( - new String[]{"table_name", "table_type"}, - new Object[][]{{"test_timestamps", "BASE TABLE"}} + new String[]{"table_name", "table_type", "table_comment"}, + new Object[][]{{"test_timestamps", "BASE TABLE", "timestamp samples"}} ))); - Assertions.assertEquals("test_timestamps", agent.listTables("PUBLIC").get(0).getName()); + TableInfo table = agent.listTables("PUBLIC").get(0); + + Assertions.assertEquals("test_timestamps", table.getName()); + Assertions.assertEquals("timestamp samples", table.getComment()); Assertions.assertTrue(sql.get(0).contains("FROM information_schema.tables")); + Assertions.assertTrue(sql.get(0).contains("LEFT JOIN sys_catalog.sys_description"), sql.get(0)); Assertions.assertFalse(sql.get(0).contains("SHOW")); } + @Test + void constrainedMysqlCompatTableMetadataPreservesCommentsAndPaging() { + List sql = new ArrayList<>(); + KingbaseAgent agent = new KingbaseAgent(); + agent.setMysqlCompatMode(true); + TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( + new String[]{"table_name", "table_type", "table_comment"}, + new Object[][]{{"orders", "BASE TABLE", "customer orders"}} + ))); + + List tables = agent.listTables( + "sales", + new MetadataListConstraints("ord", 20, 40, List.of("TABLE")) + ); + + Assertions.assertEquals("customer orders", tables.get(0).getComment()); + Assertions.assertTrue(sql.get(0).contains("UPPER(t.table_name) LIKE ? ESCAPE '\\\\'"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("LEFT JOIN sys_catalog.sys_description"), sql.get(0)); + Assertions.assertTrue(sql.get(0).endsWith("LIMIT 20 OFFSET 40"), sql.get(0)); + } + @Test void regularListTablesUsesKingbaseCatalogAndIncludesViews() { List sql = new ArrayList<>(); @@ -382,7 +407,7 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { KingbaseAgent agent = new KingbaseAgent(); agent.setMysqlCompatMode(true); TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( - new String[]{"table_name", "table_type"}, + new String[]{"table_name", "table_type", "table_comment"}, new Object[][]{} ))); @@ -390,7 +415,7 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertTrue(sql.get(0).contains("FROM information_schema.tables"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("table_type IN (?)"), sql.get(0)); - Assertions.assertTrue(sql.get(0).contains("UPPER(table_name) LIKE ? ESCAPE '\\\\'"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("UPPER(t.table_name) LIKE ? ESCAPE '\\\\'"), sql.get(0)); Assertions.assertTrue(sql.get(0).endsWith("LIMIT 20 OFFSET 40"), sql.get(0)); } diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index a220eb444a..b46d60ea53 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -662,6 +662,10 @@ function isTooltipDisabled(): boolean { async function toggle() { const node = props.node; if (node.isLoading) { + if (!node.isExpanded) { + node.isExpanded = true; + emit("node-toggled", node, false); + } return; } emit("search-toggle", node); diff --git a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts index edb777a168..abff9d224c 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts @@ -123,6 +123,66 @@ describe("connectionStore metadata loading", () => { expect(schemaNode.children?.map((node) => node.label)).toEqual(["users"]); }); + it("keeps an expanding schema attached while its parent refreshes", async () => { + const listSchemaInfos = vi.fn().mockResolvedValue([{ name: "core", comment: null }]); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + listSchemaInfos, + loadSchemaCache: vi.fn().mockResolvedValue(null), + saveSchemaCache: vi.fn().mockResolvedValue(undefined), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const connection = postgresConnection(); + const schemaNode: TreeNode = { + id: "pg-1:app:core", + label: "core", + type: "schema", + connectionId: connection.id, + database: "app", + schema: "core", + isExpanded: true, + isLoading: true, + children: [], + }; + store.connections = [connection]; + store.connectedIds.add(connection.id); + store.treeNodes = [ + { + id: connection.id, + label: connection.name, + type: "connection", + connectionId: connection.id, + isExpanded: true, + children: [ + { + id: "pg-1:app", + label: "app", + type: "database", + connectionId: connection.id, + database: "app", + isExpanded: true, + children: [schemaNode], + }, + ], + }, + ]; + + const storedSchema = store.treeNodes[0].children?.[0].children?.[0]; + await store.loadSchemas(connection.id, "app", { force: true }); + + const refreshedSchema = store.treeNodes[0].children?.[0].children?.[0]; + expect(refreshedSchema).toBe(storedSchema); + expect(refreshedSchema?.isExpanded).toBe(true); + expect(refreshedSchema?.isLoading).toBe(true); + }); + it("paginates procedure groups and appends the next page", async () => { const firstPage = Array.from({ length: 201 }, (_, index) => procedure(`p_${String(index + 1).padStart(4, "0")}`)); const listObjects = vi diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 25cfe02e59..89c87ab88c 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -18,7 +18,7 @@ import type { TunnelProfile, VectorCollectionMeta, } from "@/types/database"; -import { applyPinnedTreeNodeState, inheritNaturalTreeNodeOrder, migrateLegacyPinnedTreeNodeIds, syncPinnedTreeNodeStateInPlace, treeNodePinKey } from "@/lib/app/pinnedItems"; +import { inheritNaturalTreeNodeOrder, migrateLegacyPinnedTreeNodeIds, syncPinnedTreeNodeStateInPlace, treeNodePinKey } from "@/lib/app/pinnedItems"; import { reconcileLayout, buildTreeNodesFromLayout, @@ -989,7 +989,17 @@ export const useConnectionStore = defineStore("connection", () => { const oldMap = new Map(parent.children.map((c) => [c.id, c] as const)); children = children.map((child) => { const old = oldMap.get(child.id); - if (old && old.isExpanded && old.children && old.children.length > 0) { + if (old?.isLoading) { + const isExpanded = old.isExpanded; + const isLoading = old.isLoading; + const oldChildren = old.children; + Object.assign(old, child); + old.isExpanded = isExpanded; + old.isLoading = isLoading; + old.children = oldChildren; + return old; + } + if (old?.isExpanded) { return { ...child, isExpanded: true, children: old.children }; } return child; @@ -1000,7 +1010,8 @@ export const useConnectionStore = defineStore("connection", () => { pinnedTreeNodeIds.value = migratedPins.ids; persistPinnedTreeNodeIds(); } - parent.children = markRawLeafTreeNodes(applyPinnedTreeNodeState(children, migratedPins.ids)); + syncPinnedTreeNodeStateInPlace(children, migratedPins.ids); + parent.children = markRawLeafTreeNodes(children); loadedTreeNodeChildrenIds.value.add(parent.id); }