mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 17:09:56 +00:00
fix(dameng): show declared char/byte length unit in table structure
This commit is contained in:
@@ -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();
|
||||
|
||||
+68
-2
@@ -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")));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user