fix(kingbase): preserve table comments and schema expansion
This commit is contained in:
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-6
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user