fix(hana): show tables under schemas
This commit is contained in:
parent
562edef4dc
commit
b121240b2f
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -218,6 +218,101 @@ class StandardJdbcMetadataTest {
|
|||
assertEquals("TABLE", tables.get(2).getTable_type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listTablesEscapesUnderscoreInHanaSysSchema() {
|
||||
// HANA 的 _SYS_RT 等 schema 名含下划线,JDBC schemaPattern 把 _ 当作通配符,
|
||||
// 若不转义会误匹配 _xSYSxRT 等其他 schema。HANA 驱动 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\_T(HANA 转义符为反斜杠)
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue