fix(kingbase): preserve table comments and schema expansion

This commit is contained in:
zipg authored and GitHub committed 2026-07-16 16:05:51 +08:00
1 parent eb4abba713
commit bfcd7593ee
5 files changed
+124 -32

No files matched your search

@@ -165,7 +165,7 @@ public final class KingbaseAgent extends PostgresLikeAgent {
public List<TableInfo> listTables(String schema) { public List<TableInfo> listTables(String schema) {
if (postgresCatalogMode) return super.listTables(schema); if (postgresCatalogMode) return super.listTables(schema);
if (isMysqlCompatMode()) { if (isMysqlCompatMode()) {
return listTables(schema, "table_type IN ('BASE TABLE', 'VIEW')"); return queryMysqlCompatTables(schema, MetadataListConstraints.NONE);
} }
return queryRegularTables(schema, MetadataListConstraints.NONE); return queryRegularTables(schema, MetadataListConstraints.NONE);
} }
@@ -222,17 +222,26 @@ public final class KingbaseAgent extends PostgresLikeAgent {
return unchecked(() -> { return unchecked(() -> {
List<TableInfo> result = new ArrayList<>(); List<TableInfo> result = new ArrayList<>();
List<Object> args = new ArrayList<>(); List<Object> args = new ArrayList<>();
StringBuilder sql = new StringBuilder("SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = ?"); StringBuilder sql = new StringBuilder("SELECT t.table_name, t.table_type, ")
args.add(effectiveSchema(schema)); .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); appendMysqlCompatTableTypePredicate(sql, args, constraints);
MetadataSqlSupport.appendNameFilter(sql, args, "table_name", constraints); MetadataSqlSupport.appendNameFilter(sql, args, "t.table_name", constraints);
sql.append(" ORDER BY table_name"); sql.append(" ORDER BY t.table_name");
MetadataSqlSupport.appendLiteralLimitOffset(sql, constraints); MetadataSqlSupport.appendLiteralLimitOffset(sql, constraints);
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) { try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
MetadataSqlSupport.bind(stmt, args); MetadataSqlSupport.bind(stmt, args);
try (ResultSet rs = stmt.executeQuery()) { try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) { 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<TableInfo> listTables(String schema, String tableTypePredicate) {
return unchecked(() -> {
List<TableInfo> 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) { private static boolean isUnconstrained(MetadataListConstraints constraints) {
return !constraints.hasFilter() && !constraints.hasLimit() && !constraints.hasOffset() && !constraints.hasObjectTypes(); return !constraints.hasFilter() && !constraints.hasLimit() && !constraints.hasOffset() && !constraints.hasObjectTypes();
} }
@@ -200,20 +200,45 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
} }
@Test @Test
void mysqlCompatListTablesUsesInformationSchema() { void mysqlCompatListTablesUsesInformationSchemaAndPreservesComments() {
List<String> sql = new ArrayList<>(); List<String> sql = new ArrayList<>();
KingbaseAgent agent = new KingbaseAgent(); KingbaseAgent agent = new KingbaseAgent();
agent.setMysqlCompatMode(true); agent.setMysqlCompatMode(true);
TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet(
new String[]{"table_name", "table_type"}, new String[]{"table_name", "table_type", "table_comment"},
new Object[][]{{"test_timestamps", "BASE TABLE"}} 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("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")); Assertions.assertFalse(sql.get(0).contains("SHOW"));
} }
@Test
void constrainedMysqlCompatTableMetadataPreservesCommentsAndPaging() {
List<String> 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<TableInfo> 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 @Test
void regularListTablesUsesKingbaseCatalogAndIncludesViews() { void regularListTablesUsesKingbaseCatalogAndIncludesViews() {
List<String> sql = new ArrayList<>(); List<String> sql = new ArrayList<>();
@@ -382,7 +407,7 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
KingbaseAgent agent = new KingbaseAgent(); KingbaseAgent agent = new KingbaseAgent();
agent.setMysqlCompatMode(true); agent.setMysqlCompatMode(true);
TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet(
new String[]{"table_name", "table_type"}, new String[]{"table_name", "table_type", "table_comment"},
new Object[][]{} 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("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("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)); Assertions.assertTrue(sql.get(0).endsWith("LIMIT 20 OFFSET 40"), sql.get(0));
} }
@@ -662,6 +662,10 @@ function isTooltipDisabled(): boolean {
async function toggle() { async function toggle() {
const node = props.node; const node = props.node;
if (node.isLoading) { if (node.isLoading) {
if (!node.isExpanded) {
node.isExpanded = true;
emit("node-toggled", node, false);
}
return; return;
} }
emit("search-toggle", node); emit("search-toggle", node);
@@ -123,6 +123,66 @@ describe("connectionStore metadata loading", () => {
expect(schemaNode.children?.map((node) => node.label)).toEqual(["users"]); 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 () => { 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 firstPage = Array.from({ length: 201 }, (_, index) => procedure(`p_${String(index + 1).padStart(4, "0")}`));
const listObjects = vi const listObjects = vi
+14 -3
View File
@@ -18,7 +18,7 @@ import type {
TunnelProfile, TunnelProfile,
VectorCollectionMeta, VectorCollectionMeta,
} from "@/types/database"; } from "@/types/database";
import { applyPinnedTreeNodeState, inheritNaturalTreeNodeOrder, migrateLegacyPinnedTreeNodeIds, syncPinnedTreeNodeStateInPlace, treeNodePinKey } from "@/lib/app/pinnedItems"; import { inheritNaturalTreeNodeOrder, migrateLegacyPinnedTreeNodeIds, syncPinnedTreeNodeStateInPlace, treeNodePinKey } from "@/lib/app/pinnedItems";
import { import {
reconcileLayout, reconcileLayout,
buildTreeNodesFromLayout, buildTreeNodesFromLayout,
@@ -989,7 +989,17 @@ export const useConnectionStore = defineStore("connection", () => {
const oldMap = new Map(parent.children.map((c) => [c.id, c] as const)); const oldMap = new Map(parent.children.map((c) => [c.id, c] as const));
children = children.map((child) => { children = children.map((child) => {
const old = oldMap.get(child.id); 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, isExpanded: true, children: old.children };
} }
return child; return child;
@@ -1000,7 +1010,8 @@ export const useConnectionStore = defineStore("connection", () => {
pinnedTreeNodeIds.value = migratedPins.ids; pinnedTreeNodeIds.value = migratedPins.ids;
persistPinnedTreeNodeIds(); persistPinnedTreeNodeIds();
} }
parent.children = markRawLeafTreeNodes(applyPinnedTreeNodeState(children, migratedPins.ids)); syncPinnedTreeNodeStateInPlace(children, migratedPins.ids);
parent.children = markRawLeafTreeNodes(children);
loadedTreeNodeChildrenIds.value.add(parent.id); loadedTreeNodeChildrenIds.value.add(parent.id);
} }