fix(dameng): fall back when DBMS_METADATA is unavailable

This commit is contained in:
GIWTO
2026-08-18 11:39:55 +08:00
committed by GitHub
parent cf1476a6dc
commit 99066319cd
2 changed files with 850 additions and 11 deletions
@@ -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 {
@@ -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;
};
}