fix(hana): show tables under schemas

This commit is contained in:
gggaiitx 2026-07-24 10:02:27 +08:00 committed by GitHub
parent 562edef4dc
commit b121240b2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 140 additions and 9 deletions

View File

@ -123,12 +123,12 @@ public final class StandardJdbcMetadata {
listObjects(listTables(conn, profile, configuredDatabase, schema, tableConstraints), schema)
);
DatabaseMetaData meta = conn.getMetaData();
appendRoutines(result, meta, null, blankToNull(schema), schema);
appendRoutines(result, meta, null, schema, schema);
if (!containsRoutine(result)
&& profile.getCatalogFallbackEnabled()
&& configuredDatabase != null
&& !configuredDatabase.trim().isEmpty()) {
appendRoutines(result, meta, configuredDatabase, blankToNull(schema), schema);
appendRoutines(result, meta, configuredDatabase, schema, schema);
}
result.sort(Comparator.comparing(ObjectInfo::getName));
return normalized.filterObjects(result);
@ -332,7 +332,9 @@ public final class StandardJdbcMetadata {
return;
}
// JDBC has no portable metadata limit/offset. Use table types for safe pushdown and filter/page locally.
try (ResultSet rs = meta.getTables(catalog, blankToNull(schema), "%", tableTypes)) {
// schema DatabaseMetaData.getTables() schemaPattern_ % 具有通配符语义
// HANA 存在 _SYS_RT 等含下划线的 schema需按 getSearchStringEscape() 转义避免混入其他 schema 的对象
try (ResultSet rs = meta.getTables(catalog, escapeSchemaPattern(meta, schema), "%", tableTypes)) {
while (rs.next()) {
result.add(new TableInfo(
rs.getString("TABLE_NAME"),
@ -618,16 +620,18 @@ public final class StandardJdbcMetadata {
List<ObjectInfo> result,
DatabaseMetaData meta,
String catalog,
String schemaPattern,
String schema
String schema,
String schemaLabel
) {
// schema 作为 getProcedures/getFunctions schemaPattern同样需要转义 _ % 通配符
String schemaPattern = escapeSchemaPattern(meta, schema);
Set<String> procedureNames = new LinkedHashSet<>();
try (ResultSet rs = meta.getProcedures(catalog, schemaPattern, "%")) {
while (rs.next()) {
String name = rs.getString("PROCEDURE_NAME");
if (name != null && !name.trim().isEmpty()) {
procedureNames.add(name);
result.add(new ObjectInfo(name, "PROCEDURE", schema, rs.getString("REMARKS")));
result.add(new ObjectInfo(name, "PROCEDURE", schemaLabel, rs.getString("REMARKS")));
}
}
} catch (Exception | AbstractMethodError ignored) {
@ -637,7 +641,7 @@ public final class StandardJdbcMetadata {
while (rs.next()) {
String name = rs.getString("FUNCTION_NAME");
if (name != null && !name.trim().isEmpty() && !procedureNames.contains(name)) {
result.add(new ObjectInfo(name, "FUNCTION", schema, rs.getString("REMARKS")));
result.add(new ObjectInfo(name, "FUNCTION", schemaLabel, rs.getString("REMARKS")));
}
}
} catch (Exception | AbstractMethodError ignored) {
@ -676,6 +680,38 @@ public final class StandardJdbcMetadata {
return value == null || value.trim().isEmpty() ? null : value;
}
/**
* schema 名转义为可安全用作 {@link DatabaseMetaData#getTables(String, String, String, String[])}
* schemaPattern 参数的形式
*
* <p>JDBC schemaPattern {@code _}匹配任意单字符 {@code %}匹配任意字符序列当作通配符
* schema 名本身含有这些字符时例如 HANA {@code _SYS_RT}{@code _SYS_BIC}或被引号引用的含
* {@code %} schema直接作为 schemaPattern 传入会误匹配到其他 schema 的对象参考 DBeaver
* {@code JDBCUtils.escapeWildCards()} {@link DatabaseMetaData#getSearchStringEscape()} 对通配符做转义
*
* <p> schema 为空白将映射为 {@code null}表示不做 schema 过滤或驱动未提供转义字符时原样返回
*/
private static String escapeSchemaPattern(DatabaseMetaData meta, String schema) {
String normalized = blankToNull(schema);
if (normalized == null) {
return null;
}
String escape;
try {
escape = meta.getSearchStringEscape();
} catch (Exception ignored) {
escape = null;
}
if (escape == null || escape.isEmpty()) {
return normalized;
}
// 先转义 escape 自身再转义 % _顺序与 PreparedStatement.setEscapeProcessing / DBeaver 一致
String escapedEscape = normalized.replace(escape, escape + escape);
String escapedPercent = escapedEscape.replace("%", escape + "%");
String escapedUnderscore = escapedPercent.replace("_", escape + "_");
return escapedUnderscore;
}
private static void addNonBlank(Set<String> values, String value) {
if (value != null && !value.trim().isEmpty()) {
values.add(value);

View File

@ -218,6 +218,101 @@ class StandardJdbcMetadataTest {
assertEquals("TABLE", tables.get(2).getTable_type());
}
@Test
void listTablesEscapesUnderscoreInHanaSysSchema() {
// HANA _SYS_RT schema 名含下划线JDBC schemaPattern _ 当作通配符
// 若不转义会误匹配 _xSYSxRT 等其他 schemaHANA 驱动 getSearchStringEscape() 返回 "\\"
AtomicReference<Object[]> capturedArgs = new AtomicReference<>();
Connection conn = schemaEscapeConnection("\\", rows(
row("TABLE_NAME", "RT_OBJECTS", "TABLE_TYPE", "TABLE", "REMARKS", null)
), capturedArgs);
List<TableInfo> tables = StandardJdbcMetadata.INSTANCE.listTables(conn, profile, "", "_SYS_RT");
assertEquals(1, tables.size());
assertEquals("RT_OBJECTS", tables.get(0).getName());
// schema 第二个参数应转义为 _\_S\_Y\_S\_R\_THANA 转义符为反斜杠
assertEquals("\\_SYS\\_RT", capturedArgs.get()[1]);
}
@Test
void listTablesEscapesPercentInQuotedSchema() {
// 被引号引用 % schema "SALES%2024"% 是通配符必须转义
AtomicReference<Object[]> capturedArgs = new AtomicReference<>();
Connection conn = schemaEscapeConnection("\\", rows(
row("TABLE_NAME", "ORDERS_2024", "TABLE_TYPE", "TABLE", "REMARKS", null)
), capturedArgs);
List<TableInfo> tables = StandardJdbcMetadata.INSTANCE.listTables(conn, profile, "", "SALES%2024");
assertEquals(1, tables.size());
assertEquals("ORDERS_2024", tables.get(0).getName());
assertEquals("SALES\\%2024", capturedArgs.get()[1]);
}
@Test
void listTablesLeavesPlainSchemaUntouched() {
// 普通 schema _ %不应被转义且仍作为 schemaPattern 传入
AtomicReference<Object[]> capturedArgs = new AtomicReference<>();
Connection conn = schemaEscapeConnection("\\", rows(
row("TABLE_NAME", "ORDERS", "TABLE_TYPE", "TABLE", "REMARKS", null)
), capturedArgs);
List<TableInfo> tables = StandardJdbcMetadata.INSTANCE.listTables(conn, profile, "", "SALES");
assertEquals(1, tables.size());
assertEquals("ORDERS", tables.get(0).getName());
assertEquals("SALES", capturedArgs.get()[1]);
}
@Test
void listTablesFallsBackWhenSearchEscapeUnavailable() {
// 驱动不支持 getSearchStringEscape() schema 原样返回不做转义不抛异常
AtomicReference<Object[]> capturedArgs = new AtomicReference<>();
Connection conn = schemaEscapeConnection(null, rows(
row("TABLE_NAME", "ORDERS", "TABLE_TYPE", "TABLE", "REMARKS", null)
), capturedArgs);
List<TableInfo> tables = StandardJdbcMetadata.INSTANCE.listTables(conn, profile, "", "SALES");
assertEquals(1, tables.size());
assertEquals("SALES", capturedArgs.get()[1]);
}
private static Connection schemaEscapeConnection(String searchEscape, ResultSet tables, AtomicReference<Object[]> capturedArgs) {
DatabaseMetaData meta = proxy(DatabaseMetaData.class, new MethodHandler() {
@Override
public Object handle(Method method, Object[] args) {
String name = method.getName();
if ("getTables".equals(name)) {
if (capturedArgs != null) {
capturedArgs.set(args);
}
return tables;
}
if ("getTableTypes".equals(name)) {
return rows(row("TABLE_TYPE", "TABLE"));
}
if ("getSearchStringEscape".equals(name)) {
if (searchEscape == null) {
throw new UnsupportedOperationException("escape unavailable");
}
return searchEscape;
}
return defaultValue(method.getReturnType());
}
});
return proxy(Connection.class, new MethodHandler() {
@Override
public Object handle(Method method, Object[] args) {
if ("getMetaData".equals(method.getName())) {
return meta;
}
return defaultValue(method.getReturnType());
}
});
}
@Test
void mapsColumnsWithPrimaryKeysAndLengths() {
Connection conn = connection(

View File

@ -1297,7 +1297,7 @@ export const useConnectionStore = defineStore("connection", () => {
searchFilter?: string;
force?: boolean;
}): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number; loadMoreParent?: TableTreeLoadMoreParent }> {
if (!options.node.connectionId || !options.node.database) {
if (!options.node.connectionId || options.node.database == null) {
return { children: [], objectCount: 0, hasMore: false, nextOffset: options.offset };
}
const searchFilter = (options.searchFilter ?? sidebarSearchQuery.value) || undefined;
@ -1351,7 +1351,7 @@ export const useConnectionStore = defineStore("connection", () => {
searchFilter?: string;
force?: boolean;
}): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number }> {
if (!options.node.connectionId || !options.node.database) {
if (!options.node.connectionId || options.node.database == null) {
return { children: [], objectCount: 0, hasMore: false, nextOffset: options.offset };
}
const searchFilter = options.searchFilter || undefined;