diff --git a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java index 924d43fe2a..cddacb8c70 100644 --- a/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java +++ b/agents/drivers/dameng/src/main/java/com/dbx/agent/dameng/DamengAgent.java @@ -69,6 +69,10 @@ public final class DamengAgent extends AbstractJdbcAgent { private static final int DBMS_OUTPUT_ENABLE_TIMEOUT_SECS = 5; private static final int DBMS_OUTPUT_ENABLE_NETWORK_TIMEOUT_MILLIS = 5_000; private static final int VIEW_VALIDITY_BATCH_SIZE = 500; + // Dameng encodes the declared length unit of a character column in SCALE: see + // declaredWithCharacterLength. + private static final int CHARACTER_SCALE_VARIABLE = 7; + private static final int CHARACTER_SCALE_FIXED = 8; private static final Pattern DATABASE_VERSION_MAJOR_PATTERN = Pattern.compile("(\\d+)\\."); // Word-boundary match so a type or default merely containing the letters is not mistaken // for the IDENTITY keyword. @@ -1947,6 +1951,9 @@ public final class DamengAgent extends AbstractJdbcAgent { } Set identityColumns = identityColumns(schema, table); + // Lazily filled in only when a column's declared length is not visible in + // ALL_TAB_COLUMNS; the dictionary lookup costs an extra round trip. + Map declaredColumnLengths = null; List result = new ArrayList<>(); // DATA_DEFAULT is a LONG column — it must be selected first and read first // in JDBC, otherwise the data is truncated. @@ -1983,7 +1990,15 @@ public final class DamengAgent extends AbstractJdbcAgent { Integer dataLen = intObject(rs, "DATA_LENGTH"); Integer charLen = intObject(rs, "CHAR_LENGTH"); String charUsed = rs.getString("CHAR_USED"); - String dataType = formatDataType(baseType, numPrec, numScale, dataLen, charLen, charUsed); + Integer declaredCharLen = null; + if (needsDeclaredCharacterLength(baseType, numScale, charLen)) { + if (declaredColumnLengths == null) { + declaredColumnLengths = declaredColumnLengths(schema, table); + } + declaredCharLen = declaredColumnLengths.get(name); + } + String dataType = + formatDataType(baseType, numPrec, numScale, dataLen, charLen, charUsed, declaredCharLen); result.add(new ColumnInfo( name, @@ -2604,15 +2619,17 @@ public final class DamengAgent extends AbstractJdbcAgent { Integer numScale, Integer dataLen, Integer charLen, - String charUsed + String charUsed, + Integer declaredCharLen ) { return switch (base.toUpperCase(Locale.ROOT)) { case "VARCHAR2", "VARCHAR", "CHAR" -> { - Integer length = characterLength(dataLen, charLen, charUsed); - yield length != null ? base + "(" + length + characterLengthUnit(charUsed) + ")" : base; + String unit = characterLengthUnit(base, numScale, charUsed); + Integer length = characterLength(dataLen, charLen, unit, declaredCharLen); + yield length != null ? base + "(" + length + unit + ")" : base; } case "NVARCHAR2", "NCHAR" -> { - Integer length = charLen != null ? charLen : dataLen; + Integer length = firstPositive(charLen, declaredCharLen, dataLen); yield length != null ? base + "(" + length + ")" : base; } case "NUMBER", "NUMERIC", "DECIMAL" -> { @@ -2626,16 +2643,18 @@ public final class DamengAgent extends AbstractJdbcAgent { }; } - private static Integer characterLength(Integer dataLen, Integer charLen, String charUsed) { - String normalized = charUsed == null ? "" : charUsed.trim().toUpperCase(Locale.ROOT); - if ("B".equals(normalized) || "BYTE".equals(normalized)) { - return dataLen != null ? dataLen : charLen; + private static Integer characterLength(Integer dataLen, Integer charLen, String unit, Integer declaredCharLen) { + if (" BYTE".equals(unit)) { + return firstPositive(dataLen, declaredCharLen, charLen); } - return charLen != null ? charLen : dataLen; + return firstPositive(charLen, declaredCharLen, dataLen); } - private static String characterLengthUnit(String charUsed) { - if (charUsed == null) { + private static String characterLengthUnit(String base, Integer numScale, String charUsed) { + if (declaredWithCharacterLength(base, numScale)) { + return " CHAR"; + } + if (charUsed == null || charUsed.isBlank()) { return ""; } return switch (charUsed.trim().toUpperCase(Locale.ROOT)) { @@ -2645,6 +2664,83 @@ public final class DamengAgent extends AbstractJdbcAgent { }; } + private static Integer firstPositive(Integer... values) { + for (Integer value : values) { + if (value != null && value > 0) { + return value; + } + } + return null; + } + + /** + * Dameng keeps a character column's declared length unit in its SCALE field instead of in + * CHAR_USED, which reads 'B' for byte- and character-length columns alike: SCALE is 7 for + * VARCHAR/VARCHAR2/NVARCHAR2 and 8 for CHAR/NCHAR columns declared with character + * semantics, and 0 for byte semantics. Reading CHAR_USED alone rendered a column created as + * VARCHAR2(100 CHAR) as VARCHAR2(400 BYTE) on a UTF-8 server. + */ + private static boolean declaredWithCharacterLength(String base, Integer numScale) { + if (numScale == null) { + return false; + } + return switch (base.toUpperCase(Locale.ROOT)) { + case "VARCHAR2", "VARCHAR", "NVARCHAR2" -> numScale == CHARACTER_SCALE_VARIABLE; + case "CHAR", "NCHAR" -> numScale == CHARACTER_SCALE_FIXED; + default -> false; + }; + } + + /** + * NVARCHAR2/NCHAR columns leave CHAR_LENGTH at 0 in ALL_TAB_COLUMNS, and a character-length + * column always reports DATA_LENGTH in bytes, so the declared length has to come from + * elsewhere. Only ask when the cheaper columns cannot answer. + */ + private static boolean needsDeclaredCharacterLength(String base, Integer numScale, Integer charLen) { + if (charLen != null && charLen > 0) { + return false; + } + String type = base.toUpperCase(Locale.ROOT); + return switch (type) { + case "NVARCHAR2", "NCHAR" -> true; + case "VARCHAR2", "VARCHAR", "CHAR" -> declaredWithCharacterLength(type, numScale); + default -> false; + }; + } + + /** + * SYS.SYSCOLUMNS.LENGTH$ keeps the length exactly as declared, which ALL_TAB_COLUMNS cannot + * report for NVARCHAR2/NCHAR. The view is not granted to PUBLIC, so a permission error just + * means the caller keeps using the ALL_TAB_COLUMNS values instead of failing the request. + */ + private Map declaredColumnLengths(String schema, String table) { + Map result = new HashMap<>(); + String sql = """ + SELECT /*+ PARALLEL(1) */ c.NAME, c.LENGTH$ + FROM SYS.SYSCOLUMNS c + JOIN SYS.SYSOBJECTS t ON c.ID = t.ID + JOIN SYS.SYSOBJECTS s ON t.SCHID = s.ID + WHERE s.NAME = ? AND t.NAME = ? + """.stripIndent().trim(); + try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) { + stmt.setString(1, schema); + stmt.setString(2, table); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String name = rs.getString(1); + int length = rs.getInt(2); + if (notBlank(name) && length > 0) { + result.put(name, length); + } + } + } + } catch (Exception error) { + LOGGER.log(Level.FINE, "Unable to read declared column lengths for " + schema + "." + table, error); + return Map.of(); + } + return result; + } + private static Integer intObject(ResultSet rs, String column) throws Exception { Object value = rs.getObject(column); return value == null ? null : ((Number) value).intValue(); diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java index b0097a2559..0c2c90b4e4 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java @@ -1727,7 +1727,12 @@ class DamengAgentMetadataTest { Arrays.asList("DEFAULT_VALUE", "VARCHAR2", "Y", null, null, 80, 20, null, "default length"), Arrays.asList("WORD_VALUE", "VARCHAR2", "Y", null, null, 128, 32, "BYTE", "word byte length"), Arrays.asList("NATIONAL_VALUE", "NVARCHAR2", "Y", null, null, 80, 20, "C", "national length"), - Arrays.asList("NATIONAL_FIXED", "NCHAR", "Y", null, null, 40, 10, "B", "national fixed length") + Arrays.asList("NATIONAL_FIXED", "NCHAR", "Y", null, null, 40, 10, "B", "national fixed length"), + // DM8 reports CHAR_USED as 'B' for byte- and character-length columns alike and + // records the declared unit in SCALE instead. + Arrays.asList("SCALE_CHAR_VAR", "VARCHAR2", "Y", null, 7, 40, 10, "B", "dameng char unit"), + Arrays.asList("SCALE_CHAR_FIXED", "CHAR", "Y", null, 8, 10, 10, "B", "dameng fixed char unit"), + Arrays.asList("SCALE_BYTE_VAR", "VARCHAR2", "Y", null, 0, 10, 10, "B", "dameng byte unit") ))); List columns = agent.getColumns("APP", "USERS"); @@ -1739,6 +1744,28 @@ class DamengAgentMetadataTest { Assertions.assertEquals("VARCHAR2(128 BYTE)", columns.get(4).getData_type()); Assertions.assertEquals("NVARCHAR2(20)", columns.get(5).getData_type()); Assertions.assertEquals("NCHAR(10)", columns.get(6).getData_type()); + Assertions.assertEquals("VARCHAR2(10 CHAR)", columns.get(7).getData_type()); + Assertions.assertEquals("CHAR(10 CHAR)", columns.get(8).getData_type()); + Assertions.assertEquals("VARCHAR2(10 BYTE)", columns.get(9).getData_type()); + } + + @Test + void readsDeclaredLengthWhenAllTabColumnsHasNoCharacterLength() { + DamengAgent agent = new DamengAgent(); + TestSupport.setPrivateConnection(agent, metadataConnectionForColumns( + List.of( + // DM8 leaves CHAR_LENGTH at 0 for NVARCHAR2 and reports the byte length in + // DATA_LENGTH, so the declared character length has to come from SYSCOLUMNS. + Arrays.asList("NATIONAL_VALUE", "NVARCHAR2", "Y", null, 7, 400, 0, null, "national length"), + Arrays.asList("BIG_CHAR_VALUE", "VARCHAR2", "Y", null, 7, 800, 0, "B", "char unit from scale") + ), + List.of(Arrays.asList("NATIONAL_VALUE", 100), Arrays.asList("BIG_CHAR_VALUE", 200)) + )); + + List columns = agent.getColumns("APP", "USERS"); + + Assertions.assertEquals("NVARCHAR2(100)", columns.get(0).getData_type()); + Assertions.assertEquals("VARCHAR2(200 CHAR)", columns.get(1).getData_type()); } @Test @@ -2200,6 +2227,13 @@ class DamengAgentMetadataTest { } private static Connection metadataConnectionForColumns(List> columnRows) { + return metadataConnectionForColumns(columnRows, null); + } + + private static Connection metadataConnectionForColumns( + List> columnRows, + List> declaredColumnLengths + ) { return metadataConnection( "id comment", null, @@ -2207,7 +2241,10 @@ class DamengAgentMetadataTest { List.of(), null, "CREATE TABLE \"APP\".\"USERS\" (\n \"ID\" NUMBER\n);", - columnRows + columnRows, + null, + null, + declaredColumnLengths ); } @@ -2265,6 +2302,32 @@ class DamengAgentMetadataTest { List> columnRows, String dbmsMetadataError, SQLException systemIndexError + ) { + return metadataConnection( + allColumnComment, + fallbackColumnComment, + includeMaterializedView, + independentIndexes, + sqls, + dbmsMetadataDdl, + columnRows, + dbmsMetadataError, + systemIndexError, + null + ); + } + + private static Connection metadataConnection( + String allColumnComment, + String fallbackColumnComment, + boolean includeMaterializedView, + List> independentIndexes, + List sqls, + String dbmsMetadataDdl, + List> columnRows, + String dbmsMetadataError, + SQLException systemIndexError, + List> declaredColumnLengths ) { boolean[] dbmsMetadataResultOpen = {false}; return proxy(Connection.class, (method, args) -> { @@ -2295,6 +2358,9 @@ class DamengAgentMetadataTest { if (sql.contains("ALL_CONS_COLUMNS")) { return metadataStatement(List.of(List.of("ID"))); } + if (sql.contains("SYS.SYSCOLUMNS") && sql.contains("LENGTH$")) { + return metadataStatement(declaredColumnLengths == null ? List.of() : declaredColumnLengths); + } if (sql.contains("SYS.SYSCOLUMNS")) { return metadataStatement(List.of(List.of("ID"))); }