mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(dameng): fall back when DBMS_METADATA is unavailable
This commit is contained in:
@@ -49,8 +49,11 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class DamengAgent extends AbstractJdbcAgent {
|
||||
private static final Logger LOGGER = Logger.getLogger("com.dbx.agent.dameng");
|
||||
private static final String AGENT_VERSION = "9999.06.04.1-fix-default";
|
||||
private static final int DBMS_OUTPUT_ENABLE_TIMEOUT_SECS = 5;
|
||||
private static final int DBMS_OUTPUT_ENABLE_NETWORK_TIMEOUT_MILLIS = 5_000;
|
||||
@@ -505,6 +508,92 @@ public final class DamengAgent extends AbstractJdbcAgent {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断错误是否属于「元数据不可用」:DBMS_METADATA 系统包未安装/无权限、
|
||||
* 或系统字典视图缺失/无权限。典型 DM 错误码:[‑3325] 包/对象解析失败、
|
||||
* [‑2207] 无法解析的成员访问表达式(如 SF_DBMS_METADATA_RETURN_DDL)。
|
||||
* 此类错误应触发源码/DDL 降级,而不是让双击对象查看源码直接报错。
|
||||
*/
|
||||
private static boolean isDamengMetadataUnavailableError(Throwable error) {
|
||||
for (Throwable current = error; current != null; current = current.getCause()) {
|
||||
if (!(current instanceof SQLException sqlError)) {
|
||||
continue;
|
||||
}
|
||||
for (SQLException candidate = sqlError; candidate != null; candidate = candidate.getNextException()) {
|
||||
int errorCode = candidate.getErrorCode();
|
||||
if (errorCode == -3325 || errorCode == -2207) {
|
||||
return true;
|
||||
}
|
||||
String message = candidate.getMessage();
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = message.toLowerCase(Locale.ROOT);
|
||||
boolean metadataObject = normalized.contains("all_objects")
|
||||
|| normalized.contains("sysobjects")
|
||||
|| normalized.contains("all_dependencies")
|
||||
|| normalized.contains("all_tab_comments")
|
||||
|| normalized.contains("dbms_metadata")
|
||||
|| normalized.contains("get_ddl")
|
||||
|| normalized.contains("return_ddl")
|
||||
|| normalized.contains("all_source")
|
||||
|| normalized.contains("all_views")
|
||||
|| normalized.contains("all_triggers")
|
||||
|| normalized.contains("all_sequences")
|
||||
|| normalized.contains("systexts");
|
||||
boolean permissionDenied = normalized.contains("权限")
|
||||
|| normalized.contains("privilege")
|
||||
|| normalized.contains("permission")
|
||||
|| normalized.contains("access denied")
|
||||
|| normalized.contains("not authorized");
|
||||
boolean missingObject = normalized.contains("解析失败")
|
||||
|| normalized.contains("无法解析")
|
||||
|| normalized.contains("不存在")
|
||||
|| normalized.contains("not exist")
|
||||
|| normalized.contains("does not exist")
|
||||
|| normalized.contains("not found")
|
||||
|| normalized.contains("未发现");
|
||||
if (metadataObject && (permissionDenied || missingObject)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 连接类错误(断连/超时等)不属于元数据不可用,必须继续上抛。 */
|
||||
private static boolean isDamengConnectionError(Throwable error) {
|
||||
for (Throwable current = error; current != null; current = current.getCause()) {
|
||||
if (current instanceof SQLRecoverableException
|
||||
|| current instanceof SQLNonTransientConnectionException
|
||||
|| current instanceof SQLTransientConnectionException) {
|
||||
return true;
|
||||
}
|
||||
if (!(current instanceof SQLException sqlError)) {
|
||||
continue;
|
||||
}
|
||||
for (SQLException candidate = sqlError; candidate != null; candidate = candidate.getNextException()) {
|
||||
String message = candidate.getMessage();
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = message.toLowerCase(Locale.ROOT);
|
||||
if (normalized.contains("connection reset")
|
||||
|| normalized.contains("connection lost")
|
||||
|| normalized.contains("connection closed")
|
||||
|| normalized.contains("关闭的连接")
|
||||
|| normalized.contains("网络通信异常")
|
||||
|| normalized.contains("通信异常")
|
||||
|| normalized.contains("socket")
|
||||
|| normalized.contains("timed out")
|
||||
|| normalized.contains("超时")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<TableInfo> executeConstrainedTables(MetadataQuery query, MetadataListConstraints constraints) {
|
||||
return unchecked(() -> {
|
||||
List<TableInfo> result = new ArrayList<>();
|
||||
@@ -958,18 +1047,352 @@ public final class DamengAgent extends AbstractJdbcAgent {
|
||||
public ObjectSource getObjectSource(String schema, String name, String objectType) {
|
||||
return unchecked(() -> {
|
||||
String dbmsType = damengDdlObjectType(objectType);
|
||||
String source;
|
||||
String sql = "SELECT /*+ PARALLEL(1) */ DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL";
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, dbmsType);
|
||||
stmt.setString(2, name);
|
||||
stmt.setString(3, schema);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
source = rs.next() ? coalesce(readTextColumn(rs, 1)) : "";
|
||||
RuntimeException dbmsError;
|
||||
try {
|
||||
String source = readDbmsMetadataObjectSource(dbmsType, name, schema);
|
||||
return new ObjectSource(name, objectType, schema, source);
|
||||
} catch (Exception error) {
|
||||
// 检查型异常(SQLException 等)由 unchecked 透传后在此处统一处理。
|
||||
RuntimeException runtimeError = error instanceof RuntimeException runtime ? runtime : new RuntimeException(error);
|
||||
if (!isDamengMetadataUnavailableError(runtimeError)) {
|
||||
throw runtimeError;
|
||||
}
|
||||
dbmsError = runtimeError;
|
||||
}
|
||||
try {
|
||||
return buildCatalogObjectSource(schema, name, objectType, dbmsError);
|
||||
} catch (RuntimeException fallbackError) {
|
||||
fallbackError.addSuppressed(dbmsError);
|
||||
throw fallbackError;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String readDbmsMetadataObjectSource(String dbmsType, String name, String schema) throws Exception {
|
||||
String sql = "SELECT /*+ PARALLEL(1) */ DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL";
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, dbmsType);
|
||||
stmt.setString(2, name);
|
||||
stmt.setString(3, schema);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
return rs.next() ? coalesce(readTextColumn(rs, 1)) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 DBMS_METADATA 系统包缺失/无权限时(典型错误:[‑3325] 包/对象解析失败、
|
||||
* [‑2207] 无法解析的成员访问表达式,如 SF_DBMS_METADATA_RETURN_DDL),
|
||||
* 从系统字典视图重建对象源码:
|
||||
* <ul>
|
||||
* <li>VIEW/MATERIALIZED_VIEW → ALL_VIEWS.TEXT</li>
|
||||
* <li>TRIGGER → ALL_TRIGGERS.TRIGGER_BODY</li>
|
||||
* <li>SEQUENCE → 由 ALL_SEQUENCES 元数据重建 CREATE SEQUENCE 语句</li>
|
||||
* <li>PROCEDURE/FUNCTION/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → ALL_SOURCE 按 LINE 拼接,
|
||||
* 再以 SYS.SYSOBJECTS + SYS.SYSTEXTS 作为最后一层</li>
|
||||
* </ul>
|
||||
* 所有字典来源都不可用时返回带原因说明的占位源码(不可编辑),避免双击对象查看源码直接报错;
|
||||
* 连接类错误(断连/超时等)不属于元数据不可用,继续上抛由上层处理会话。
|
||||
*/
|
||||
private ObjectSource buildCatalogObjectSource(
|
||||
String schema,
|
||||
String name,
|
||||
String objectType,
|
||||
RuntimeException dbmsError
|
||||
) throws Exception {
|
||||
String type = normalizeObjectSourceType(objectType);
|
||||
switch (type) {
|
||||
case "VIEW", "MATERIALIZED_VIEW" -> {
|
||||
ObjectSource fromView = catalogTextSource(
|
||||
schema,
|
||||
name,
|
||||
objectType,
|
||||
"SELECT TEXT FROM ALL_VIEWS WHERE OWNER = ? AND VIEW_NAME = ?",
|
||||
false
|
||||
);
|
||||
if (fromView != null) {
|
||||
return fromView;
|
||||
}
|
||||
}
|
||||
return new ObjectSource(name, objectType, schema, source);
|
||||
});
|
||||
case "TRIGGER" -> {
|
||||
ObjectSource fromTrigger = catalogTextSource(
|
||||
schema,
|
||||
name,
|
||||
objectType,
|
||||
"SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = ? AND TRIGGER_NAME = ?",
|
||||
false
|
||||
);
|
||||
if (fromTrigger != null) {
|
||||
return fromTrigger;
|
||||
}
|
||||
}
|
||||
case "SEQUENCE" -> {
|
||||
ObjectSource fromSequence = catalogSequenceSource(schema, name, objectType);
|
||||
if (fromSequence != null) {
|
||||
return fromSequence;
|
||||
}
|
||||
}
|
||||
case "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY" -> {
|
||||
ObjectSource fromAllSource = catalogRoutineSource(schema, name, objectType);
|
||||
if (fromAllSource != null) {
|
||||
return fromAllSource;
|
||||
}
|
||||
ObjectSource fromSystemText = catalogRoutineSystemText(schema, name, objectType);
|
||||
if (fromSystemText != null) {
|
||||
return fromSystemText;
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unsupported object type: " + objectType);
|
||||
}
|
||||
return unavailableObjectSource(schema, name, objectType, dbmsError);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典视图来源(ALL_VIEWS.TEXT / ALL_TRIGGERS.TRIGGER_BODY)通常只包含对象正文、
|
||||
* 不含完整的 CREATE/ALTER 语句头,不能作为可执行 DDL 保存,故标记为不可在线编辑。
|
||||
*/
|
||||
private static final String CATALOG_BODY_HINT =
|
||||
"-- 以下内容来自系统字典视图,仅为对象正文,可能不包含完整语句头,不可在线编辑。\n";
|
||||
|
||||
/** 读取单行单列文本(视图/触发器源码),空结果返回空串。 */
|
||||
private String scalarText(String sql, String schema, String name) throws Exception {
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, schema);
|
||||
stmt.setString(2, name);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
return rs.next() ? coalesce(readTextColumn(rs, 1)) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectSource catalogTextSource(String schema, String name, String objectType, String sql, boolean editable) {
|
||||
try {
|
||||
String text = scalarText(sql, schema, name);
|
||||
if (!notBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
if (!editable) {
|
||||
text = CATALOG_BODY_HINT + text;
|
||||
}
|
||||
return new ObjectSource(name, objectType, schema, text, editable);
|
||||
} catch (RuntimeException error) {
|
||||
return metadataTierError(error);
|
||||
} catch (Exception error) {
|
||||
return metadataTierError(new RuntimeException(error));
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectSource catalogSequenceSource(String schema, String name, String objectType) {
|
||||
try {
|
||||
String sql = """
|
||||
SELECT MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, ORDER_FLAG, CACHE_SIZE
|
||||
FROM ALL_SEQUENCES
|
||||
WHERE SEQUENCE_OWNER = ? AND SEQUENCE_NAME = ?
|
||||
""".stripIndent().trim();
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, schema);
|
||||
stmt.setString(2, name);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder ddl = new StringBuilder("CREATE SEQUENCE ").append(qualifiedName(schema, name));
|
||||
String minValue = rs.getString("MIN_VALUE");
|
||||
String maxValue = rs.getString("MAX_VALUE");
|
||||
String increment = rs.getString("INCREMENT_BY");
|
||||
if (increment != null && !"1".equals(increment.trim())) {
|
||||
ddl.append(" INCREMENT BY ").append(increment.trim());
|
||||
}
|
||||
if (minValue != null && !"1".equals(minValue.trim())) {
|
||||
ddl.append(" MINVALUE ").append(minValue.trim());
|
||||
}
|
||||
if (maxValue != null) {
|
||||
ddl.append(" MAXVALUE ").append(maxValue.trim());
|
||||
}
|
||||
String cache = rs.getString("CACHE_SIZE");
|
||||
if (cache != null && !"0".equals(cache.trim())) {
|
||||
ddl.append(" CACHE ").append(cache.trim());
|
||||
} else {
|
||||
ddl.append(" NOCACHE");
|
||||
}
|
||||
ddl.append("Y".equalsIgnoreCase(rs.getString("CYCLE_FLAG")) ? " CYCLE" : " NOCYCLE");
|
||||
ddl.append("Y".equalsIgnoreCase(rs.getString("ORDER_FLAG")) ? " ORDER" : " NOORDER");
|
||||
ddl.append(";");
|
||||
// 序列 DDL 由元数据重建,不提供在线编辑。
|
||||
return new ObjectSource(name, objectType, schema, ddl.toString(), false);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
return metadataTierError(error);
|
||||
} catch (Exception error) {
|
||||
return metadataTierError(new RuntimeException(error));
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectSource catalogRoutineSource(String schema, String name, String objectType) {
|
||||
try {
|
||||
String type = normalizeObjectSourceType(objectType);
|
||||
List<String> typeCandidates = switch (type) {
|
||||
// 不同 DM 版本对函数/过程的 TYPE 取值不一致(有的统一为 PROCEDURE),逐一尝试。
|
||||
case "FUNCTION" -> List.of("FUNCTION", "PROCEDURE");
|
||||
case "PROCEDURE" -> List.of("PROCEDURE", "FUNCTION");
|
||||
case "PACKAGE" -> List.of("PACKAGE");
|
||||
case "PACKAGE_BODY" -> List.of("PACKAGE BODY", "PACKAGE_BODY");
|
||||
case "TYPE" -> List.of("TYPE");
|
||||
case "TYPE_BODY" -> List.of("TYPE BODY", "TYPE_BODY");
|
||||
default -> List.of();
|
||||
};
|
||||
for (String candidate : typeCandidates) {
|
||||
String source = readAllSourceLines(schema, name, candidate);
|
||||
if (notBlank(source)) {
|
||||
return catalogRoutineObjectSource(schema, name, objectType, source);
|
||||
}
|
||||
}
|
||||
// 兜底:忽略 TYPE 过滤按名称读取全部源码行(同一 schema 内对象名唯一)。
|
||||
String anyTypeSource = readAllSourceLines(schema, name, null);
|
||||
if (notBlank(anyTypeSource)) {
|
||||
return catalogRoutineObjectSource(schema, name, objectType, anyTypeSource);
|
||||
}
|
||||
return null;
|
||||
} catch (RuntimeException error) {
|
||||
return metadataTierError(error);
|
||||
} catch (Exception error) {
|
||||
return metadataTierError(new RuntimeException(error));
|
||||
}
|
||||
}
|
||||
|
||||
private String readAllSourceLines(String schema, String name, String sourceType) throws Exception {
|
||||
String sql = sourceType == null
|
||||
? "SELECT TEXT FROM ALL_SOURCE WHERE OWNER = ? AND NAME = ? ORDER BY LINE"
|
||||
: "SELECT TEXT FROM ALL_SOURCE WHERE OWNER = ? AND NAME = ? AND TYPE = ? ORDER BY LINE";
|
||||
StringBuilder source = new StringBuilder();
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, schema);
|
||||
stmt.setString(2, name);
|
||||
if (sourceType != null) {
|
||||
stmt.setString(3, sourceType);
|
||||
}
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String line = coalesce(readTextColumn(rs, 1));
|
||||
source.append(line);
|
||||
if (!line.endsWith("\n")) {
|
||||
source.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
|
||||
private static ObjectSource catalogRoutineObjectSource(
|
||||
String schema,
|
||||
String name,
|
||||
String objectType,
|
||||
String source
|
||||
) {
|
||||
String normalizedSource = source.stripLeading();
|
||||
String upperSource = normalizedSource.toUpperCase(Locale.ROOT);
|
||||
boolean executable = upperSource.startsWith("CREATE ") || upperSource.startsWith("ALTER ");
|
||||
if (!executable) {
|
||||
String declarationType = normalizeObjectSourceType(objectType).replace('_', ' ');
|
||||
if (startsWithRoutineDeclaration(normalizedSource, declarationType)) {
|
||||
normalizedSource = "CREATE OR REPLACE " + normalizedSource;
|
||||
executable = true;
|
||||
}
|
||||
}
|
||||
if (!executable) {
|
||||
normalizedSource = CATALOG_BODY_HINT + normalizedSource;
|
||||
}
|
||||
return new ObjectSource(name, objectType, schema, normalizedSource, executable);
|
||||
}
|
||||
|
||||
private static boolean startsWithRoutineDeclaration(String source, String declarationType) {
|
||||
if (!source.regionMatches(true, 0, declarationType, 0, declarationType.length())) {
|
||||
return false;
|
||||
}
|
||||
return source.length() == declarationType.length()
|
||||
|| Character.isWhitespace(source.charAt(declarationType.length()));
|
||||
}
|
||||
|
||||
private ObjectSource catalogRoutineSystemText(String schema, String name, String objectType) {
|
||||
try {
|
||||
// DM 将过程/函数/包/类型的定义文本按行存放在 SYS.SYSTEXTS,
|
||||
// SYS.SYSOBJECTS 的 SCHID 关联所属 schema;适用于 ALL_SOURCE 不可用的实例。
|
||||
String sql = """
|
||||
SELECT t.TEXT
|
||||
FROM SYS.SYSTEXTS t
|
||||
JOIN SYS.SYSOBJECTS o ON o.ID = t.ID
|
||||
JOIN SYS.SYSOBJECTS s ON s.ID = o.SCHID AND s.TYPE$ = 'SCH' AND s.NAME = ?
|
||||
WHERE o.NAME = ?
|
||||
ORDER BY t.LINE
|
||||
""".stripIndent().trim();
|
||||
StringBuilder source = new StringBuilder();
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
|
||||
stmt.setString(1, schema);
|
||||
stmt.setString(2, name);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String line = coalesce(readTextColumn(rs, 1));
|
||||
source.append(line);
|
||||
if (!line.endsWith("\n")) {
|
||||
source.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return notBlank(source.toString())
|
||||
? catalogRoutineObjectSource(schema, name, objectType, source.toString())
|
||||
: null;
|
||||
} catch (RuntimeException error) {
|
||||
return metadataTierError(error);
|
||||
} catch (Exception error) {
|
||||
return metadataTierError(new RuntimeException(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 元数据降级分层中的单层错误处理:连接类错误继续上抛(会话处理需要真实错误),
|
||||
* 元数据不可用/未知错误返回 {@code null} 让调用方尝试下一层。
|
||||
*/
|
||||
private static ObjectSource metadataTierError(RuntimeException error) {
|
||||
if (isDamengConnectionError(error)) {
|
||||
throw error;
|
||||
}
|
||||
LOGGER.log(
|
||||
Level.FINE,
|
||||
"Dameng metadata catalog tier unavailable for source fallback: " + firstLine(error.getMessage()),
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 所有降级来源都不可用时返回带原因说明的占位源码,避免双击对象查看源码直接报错。 */
|
||||
private ObjectSource unavailableObjectSource(
|
||||
String schema,
|
||||
String name,
|
||||
String objectType,
|
||||
RuntimeException dbmsError
|
||||
) {
|
||||
String reason = dbmsError == null ? "未知" : firstLine(dbmsError.getMessage());
|
||||
String source = "-- 无法获取 " + qualifiedName(schema, name) + "(" + objectType + ")的完整定义。\n"
|
||||
+ "-- 当前连接缺少 DBMS_METADATA 系统包(未安装或无执行权限),系统字典中也没有可用源码。\n"
|
||||
+ "-- 如需完整 DDL,请由 DBA 执行 SP_CREATE_SYSTEM_PACKAGES(1) 安装系统包后重试。\n"
|
||||
+ "-- 底层错误:" + reason;
|
||||
return new ObjectSource(name, objectType, schema, source, false);
|
||||
}
|
||||
|
||||
private static String firstLine(String message) {
|
||||
if (message == null || message.isEmpty()) {
|
||||
return "未知";
|
||||
}
|
||||
int newline = message.indexOf('\n');
|
||||
return newline < 0 ? message.trim() : message.substring(0, newline).trim();
|
||||
}
|
||||
|
||||
private static String normalizeObjectSourceType(String objectType) {
|
||||
String value = objectType == null ? "" : objectType.trim().toUpperCase(Locale.ROOT);
|
||||
return value.replace(' ', '_');
|
||||
}
|
||||
|
||||
static String damengDdlObjectType(String objectType) {
|
||||
@@ -983,6 +1406,9 @@ public final class DamengAgent extends AbstractJdbcAgent {
|
||||
case "PACKAGE_BODY", "PACKAGE BODY" -> "PKG_BODY";
|
||||
// DM DBMS_METADATA accepts TRIGGER directly and returns executable CREATE OR REPLACE DDL.
|
||||
case "TRIGGER" -> "TRIGGER";
|
||||
// 用户自定义类型:与 Oracle 兼容的 DBMS_METADATA 对象类型名。
|
||||
case "TYPE" -> "TYPE";
|
||||
case "TYPE_BODY", "TYPE BODY" -> "TYPE BODY";
|
||||
default -> throw new IllegalArgumentException("Unsupported object type: " + objectType);
|
||||
};
|
||||
}
|
||||
@@ -1010,7 +1436,7 @@ public final class DamengAgent extends AbstractJdbcAgent {
|
||||
throw new IllegalArgumentException("Table not found: " + schema + "." + table);
|
||||
});
|
||||
} catch (RuntimeException error) {
|
||||
if (!isDamengMetadataPermissionError(error)) {
|
||||
if (!isDamengMetadataUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
|
||||
+413
@@ -508,6 +508,341 @@ class DamengAgentMetadataTest {
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE VIEW"), source.getSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToAllSourceFunctionDdlWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(
|
||||
Arrays.asList("CREATE OR REPLACE FUNCTION \"APP\".\"CALC_SCORE\"(V_ID INT) RETURN INT AS"),
|
||||
Arrays.asList("BEGIN"),
|
||||
Arrays.asList(" RETURN V_ID * 2;"),
|
||||
Arrays.asList("END;")
|
||||
),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "CALC_SCORE", "FUNCTION");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE OR REPLACE FUNCTION"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("RETURN V_ID * 2;"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
Assertions.assertEquals(1, sqls.stream().filter(sql -> sql.contains("DBMS_METADATA.GET_DDL")).count());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("SYS.SYSTEXTS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToAllSourceTypeDdlWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(
|
||||
Arrays.asList("CREATE OR REPLACE TYPE \"APP\".\"T_ADDR\" AS OBJECT("),
|
||||
Arrays.asList(" CITY VARCHAR2(64)"),
|
||||
Arrays.asList(")")
|
||||
),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "T_ADDR", "TYPE");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE OR REPLACE TYPE"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToAllSourcePackageBodyDdlWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(
|
||||
Arrays.asList("CREATE OR REPLACE PACKAGE BODY \"APP\".\"PKG_DEMO\" AS"),
|
||||
Arrays.asList(" PROCEDURE P IS BEGIN NULL; END;"),
|
||||
Arrays.asList("END PKG_DEMO;")
|
||||
),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "PKG_DEMO", "PACKAGE_BODY");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE OR REPLACE PACKAGE BODY"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsBareAllSourcePackageBodyAsExecutableDdl() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(
|
||||
Arrays.asList("PACKAGE BODY \"APP\".\"PKG_DEMO\" AS"),
|
||||
Arrays.asList(" PROCEDURE P IS BEGIN NULL; END;"),
|
||||
Arrays.asList("END PKG_DEMO;")
|
||||
),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "PKG_DEMO", "PACKAGE_BODY");
|
||||
|
||||
Assertions.assertTrue(source.getSource().startsWith("CREATE OR REPLACE PACKAGE BODY"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToViewTextWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(missingDbmsMetadataPackageError(), sqls, List.of(), null, List.of(), null)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "V_ACTIVE_USERS", "VIEW");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("SELECT 1 AS ID FROM DUAL"), source.getSource());
|
||||
// 字典视图正文不包含完整 CREATE VIEW 头,标记为不可编辑。
|
||||
Assertions.assertFalse(source.isEditable());
|
||||
Assertions.assertTrue(source.getSource().startsWith("--"), source.getSource());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_VIEWS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToTriggerBodyWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(missingDbmsMetadataPackageError(), sqls, List.of(), null, List.of(), null)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "TRG_USERS", "TRIGGER");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE TRIGGER"), source.getSource());
|
||||
// 触发器正文可能不含完整创建语句头,标记为不可编辑。
|
||||
Assertions.assertFalse(source.isEditable());
|
||||
Assertions.assertTrue(source.getSource().startsWith("--"), source.getSource());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_TRIGGERS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconstructsSequenceDdlWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(missingDbmsMetadataPackageError(), sqls, List.of(), null, List.of(), null)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "SEQ_ORDERS", "SEQUENCE");
|
||||
|
||||
Assertions.assertTrue(
|
||||
source.getSource().contains("CREATE SEQUENCE \"APP\".\"SEQ_ORDERS\""),
|
||||
source.getSource()
|
||||
);
|
||||
Assertions.assertTrue(source.getSource().contains("INCREMENT BY 5"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("MAXVALUE 9999999999"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("CACHE 20"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("NOCYCLE"), source.getSource());
|
||||
Assertions.assertFalse(source.isEditable());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SEQUENCES")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToSysTextsWhenAllSourceIsUnavailable() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(),
|
||||
missingCatalogViewError("ALL_SOURCE"),
|
||||
List.of(
|
||||
Arrays.asList("CREATE OR REPLACE FUNCTION \"APP\".\"CALC_SCORE\"(V_ID INT) RETURN INT AS"),
|
||||
Arrays.asList("BEGIN"),
|
||||
Arrays.asList(" RETURN V_ID * 2;"),
|
||||
Arrays.asList("END;")
|
||||
),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "CALC_SCORE", "FUNCTION");
|
||||
|
||||
Assertions.assertTrue(source.getSource().contains("CREATE OR REPLACE FUNCTION"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("SYS.SYSTEXTS") && sql.contains("SYS.SYSOBJECTS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsBareSysTextsTypeAsExecutableDdl() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(),
|
||||
missingCatalogViewError("ALL_SOURCE"),
|
||||
List.of(
|
||||
Arrays.asList("TYPE \"APP\".\"T_ADDR\" AS OBJECT("),
|
||||
Arrays.asList(" CITY VARCHAR2(64)"),
|
||||
Arrays.asList(")")
|
||||
),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "T_ADDR", "TYPE");
|
||||
|
||||
Assertions.assertTrue(source.getSource().startsWith("CREATE OR REPLACE TYPE"), source.getSource());
|
||||
Assertions.assertTrue(source.isEditable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsUnrecognizedRoutineCatalogTextReadOnly() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(Arrays.asList("BEGIN NULL; END;")),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "PKG_DEMO", "PACKAGE");
|
||||
|
||||
Assertions.assertTrue(source.getSource().startsWith("--"), source.getSource());
|
||||
Assertions.assertFalse(source.isEditable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsUnavailablePlaceholderWhenAllCatalogSourcesFail() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
missingDbmsMetadataPackageError(),
|
||||
sqls,
|
||||
List.of(),
|
||||
missingCatalogViewError("ALL_SOURCE"),
|
||||
List.of(),
|
||||
missingCatalogViewError("SYS.SYSTEXTS")
|
||||
)
|
||||
);
|
||||
|
||||
ObjectSource source = agent.getObjectSource("APP", "CALC_SCORE", "FUNCTION");
|
||||
|
||||
Assertions.assertFalse(source.isEditable());
|
||||
Assertions.assertTrue(source.getSource().startsWith("--"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("SP_CREATE_SYSTEM_PACKAGES"), source.getSource());
|
||||
Assertions.assertTrue(source.getSource().contains("DBMS_METADATA"), source.getSource());
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("SYS.SYSTEXTS")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesConnectionResetWithoutCatalogFallback() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(
|
||||
new SQLException("DBMS_METADATA.GET_DDL connection reset"),
|
||||
sqls,
|
||||
List.of(),
|
||||
null,
|
||||
List.of(),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
RuntimeException error = Assertions.assertThrows(
|
||||
RuntimeException.class,
|
||||
() -> agent.getObjectSource("APP", "CALC_SCORE", "FUNCTION")
|
||||
);
|
||||
|
||||
Assertions.assertEquals("DBMS_METADATA.GET_DDL connection reset", error.getCause().getMessage());
|
||||
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotFallBackForUnrelatedDbmsMetadataErrors() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
SQLException unrelated = new SQLException("DBMS_METADATA.GET_DDL 内部执行错误", "99999", -9001);
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
catalogFallbackConnection(unrelated, sqls, List.of(), null, List.of(), null)
|
||||
);
|
||||
|
||||
Assertions.assertThrows(
|
||||
RuntimeException.class,
|
||||
() -> agent.getObjectSource("APP", "CALC_SCORE", "FUNCTION")
|
||||
);
|
||||
|
||||
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("FROM ALL_SOURCE")), String.join("\n", sqls));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tableDdlFallsBackWhenDbmsMetadataPackageIsMissing() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
List<String> sqls = new ArrayList<>();
|
||||
TestSupport.setPrivateConnection(
|
||||
agent,
|
||||
metadataConnectionWithDbmsMetadataError(
|
||||
sqls,
|
||||
"[-2207]:无法解析的成员访问表达式[SF_DBMS_METADATA_RETURN_DDL]\n[-3325]:包/对象[DBMS_METADATA]解析失败"
|
||||
)
|
||||
);
|
||||
|
||||
String ddl = agent.getTableDdl("APP", "USERS");
|
||||
|
||||
Assertions.assertTrue(ddl.contains("CREATE TABLE \"APP\".\"USERS\""), ddl);
|
||||
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("ALL_TAB_COLUMNS")), String.join("\n", sqls));
|
||||
Assertions.assertEquals(1, sqls.stream().filter(sql -> sql.contains("DBMS_METADATA.GET_DDL")).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggerMetadataDoesNotRequireOracleTriggerTypeColumn() {
|
||||
DamengAgent agent = new DamengAgent();
|
||||
@@ -1016,6 +1351,78 @@ class DamengAgentMetadataTest {
|
||||
});
|
||||
}
|
||||
|
||||
/** 模拟 DM 实例未安装 DBMS_METADATA 系统包时的典型错误([‑2207]/[‑3325])。 */
|
||||
private static SQLException missingDbmsMetadataPackageError() {
|
||||
return new SQLException(
|
||||
"[-2207]:无法解析的成员访问表达式[SF_DBMS_METADATA_RETURN_DDL]\n"
|
||||
+ "[-3325]:包/对象[DBMS_METADATA]解析失败",
|
||||
"22000",
|
||||
-3325
|
||||
);
|
||||
}
|
||||
|
||||
/** 模拟系统字典视图(ALL_SOURCE / SYS.SYSTEXTS 等)不存在的错误。 */
|
||||
private static SQLException missingCatalogViewError(String view) {
|
||||
return new SQLException("表或视图不存在: " + view, "42000", -2106);
|
||||
}
|
||||
|
||||
/**
|
||||
* DBMS_METADATA 失败时走系统字典降级的连接:ALL_VIEWS/ALL_TRIGGERS/ALL_SEQUENCES
|
||||
* 固定返回一行样例数据;ALL_SOURCE / SYS.SYSTEXTS 的行与错误可通过参数定制。
|
||||
*/
|
||||
private static Connection catalogFallbackConnection(
|
||||
SQLException dbmsMetadataError,
|
||||
List<String> sqls,
|
||||
List<List<Object>> allSourceRows,
|
||||
SQLException allSourceError,
|
||||
List<List<Object>> systemTextRows,
|
||||
SQLException systemTextError
|
||||
) {
|
||||
return proxy(Connection.class, (method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("prepareStatement".equals(name)) {
|
||||
String sql = (String) args[0];
|
||||
if (sqls != null) {
|
||||
sqls.add(sql);
|
||||
}
|
||||
if (sql.contains("DBMS_METADATA.GET_DDL")) {
|
||||
if (dbmsMetadataError != null) {
|
||||
return failingMetadataStatement(dbmsMetadataError);
|
||||
}
|
||||
return metadataStatement(List.of(List.of("CREATE OR REPLACE PROCEDURE placeholder")));
|
||||
}
|
||||
if (sql.contains("FROM ALL_SOURCE")) {
|
||||
if (allSourceError != null) {
|
||||
return failingMetadataStatement(allSourceError);
|
||||
}
|
||||
return metadataStatement(allSourceRows);
|
||||
}
|
||||
if (sql.contains("SYS.SYSTEXTS")) {
|
||||
if (systemTextError != null) {
|
||||
return failingMetadataStatement(systemTextError);
|
||||
}
|
||||
return metadataStatement(systemTextRows);
|
||||
}
|
||||
if (sql.contains("FROM ALL_VIEWS")) {
|
||||
return metadataStatement(List.of(List.of("SELECT 1 AS ID FROM DUAL")));
|
||||
}
|
||||
if (sql.contains("FROM ALL_TRIGGERS")) {
|
||||
return metadataStatement(List.of(List.of("CREATE TRIGGER \"APP\".\"TRG_USERS\" BEFORE INSERT ON \"APP\".\"USERS\" BEGIN NULL; END;")));
|
||||
}
|
||||
if (sql.contains("FROM ALL_SEQUENCES")) {
|
||||
return metadataStatement(List.of(List.of("1", "9999999999", "5", "N", "N", "20")));
|
||||
}
|
||||
}
|
||||
if ("close".equals(name)) {
|
||||
return null;
|
||||
}
|
||||
if ("isClosed".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static Connection restrictedCrossOwnerMetadataConnection(List<String> sqls) {
|
||||
return proxy(Connection.class, (method, args) -> {
|
||||
String name = method.getName();
|
||||
@@ -1274,6 +1681,12 @@ class DamengAgentMetadataTest {
|
||||
case "CHAR_USED" -> string(rows, index[0], 7);
|
||||
case "COMMENTS", "COMMENT$", "REMARKS" -> string(rows, index[0], rows.get(index[0]).size() - 1);
|
||||
case "COLNAME" -> string(rows, index[0], 0);
|
||||
case "MIN_VALUE" -> string(rows, index[0], 0);
|
||||
case "MAX_VALUE" -> string(rows, index[0], 1);
|
||||
case "INCREMENT_BY" -> string(rows, index[0], 2);
|
||||
case "CYCLE_FLAG" -> string(rows, index[0], 3);
|
||||
case "ORDER_FLAG" -> string(rows, index[0], 4);
|
||||
case "CACHE_SIZE" -> string(rows, index[0], 5);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user