fix(dameng): show declared char/byte length unit in table structure

This commit is contained in:
zipg
2026-09-26 21:45:33 +08:00
committed by GitHub
parent 33cac93254
commit 3829513cfb
2 changed files with 176 additions and 14 deletions
@@ -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<String> 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<String, Integer> declaredColumnLengths = null;
List<ColumnInfo> 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
* <code>VARCHAR2(100 CHAR)</code> as <code>VARCHAR2(400 BYTE)</code> 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<String, Integer> declaredColumnLengths(String schema, String table) {
Map<String, Integer> 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();
@@ -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<ColumnInfo> 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<ColumnInfo> 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<List<Object>> columnRows) {
return metadataConnectionForColumns(columnRows, null);
}
private static Connection metadataConnectionForColumns(
List<List<Object>> columnRows,
List<List<Object>> 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<List<Object>> 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<List<Object>> independentIndexes,
List<String> sqls,
String dbmsMetadataDdl,
List<List<Object>> columnRows,
String dbmsMetadataError,
SQLException systemIndexError,
List<List<Object>> 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")));
}