diff --git a/agents/common/src/main/java/com/dbx/agent/ColumnInfo.java b/agents/common/src/main/java/com/dbx/agent/ColumnInfo.java index d65df3b5e0..ebbe589ca3 100644 --- a/agents/common/src/main/java/com/dbx/agent/ColumnInfo.java +++ b/agents/common/src/main/java/com/dbx/agent/ColumnInfo.java @@ -13,6 +13,8 @@ public final class ColumnInfo { private Integer numeric_precision; private Integer numeric_scale; private Integer character_maximum_length; + private String character_set; + private String collation; public ColumnInfo() { this("", "", true, null, false); @@ -33,6 +35,36 @@ public final class ColumnInfo { Integer numeric_precision, Integer numeric_scale, Integer character_maximum_length + ) { + this( + name, + data_type, + is_nullable, + column_default, + is_primary_key, + extra, + comment, + numeric_precision, + numeric_scale, + character_maximum_length, + null, + null + ); + } + + public ColumnInfo( + String name, + String data_type, + boolean is_nullable, + String column_default, + boolean is_primary_key, + String extra, + String comment, + Integer numeric_precision, + Integer numeric_scale, + Integer character_maximum_length, + String character_set, + String collation ) { this.name = name; this.data_type = data_type; @@ -44,6 +76,8 @@ public final class ColumnInfo { this.numeric_precision = numeric_precision; this.numeric_scale = numeric_scale; this.character_maximum_length = character_maximum_length; + this.character_set = character_set; + this.collation = collation; } public String getName() { @@ -86,6 +120,14 @@ public final class ColumnInfo { return character_maximum_length; } + public String getCharacter_set() { + return character_set; + } + + public String getCollation() { + return collation; + } + public void setName(String name) { this.name = name; } @@ -126,6 +168,14 @@ public final class ColumnInfo { this.character_maximum_length = character_maximum_length; } + public void setCharacter_set(String character_set) { + this.character_set = character_set; + } + + public void setCollation(String collation) { + this.collation = collation; + } + @Override public boolean equals(Object other) { if (this == other) return true; @@ -140,7 +190,9 @@ public final class ColumnInfo { && Objects.equals(comment, that.comment) && Objects.equals(numeric_precision, that.numeric_precision) && Objects.equals(numeric_scale, that.numeric_scale) - && Objects.equals(character_maximum_length, that.character_maximum_length); + && Objects.equals(character_maximum_length, that.character_maximum_length) + && Objects.equals(character_set, that.character_set) + && Objects.equals(collation, that.collation); } @Override @@ -155,7 +207,9 @@ public final class ColumnInfo { comment, numeric_precision, numeric_scale, - character_maximum_length + character_maximum_length, + character_set, + collation ); } @@ -171,6 +225,8 @@ public final class ColumnInfo { + ", numeric_precision=" + numeric_precision + ", numeric_scale=" + numeric_scale + ", character_maximum_length=" + character_maximum_length + + ", character_set=" + character_set + + ", collation=" + collation + ")"; } } diff --git a/agents/common/src/test/java/com/dbx/agent/ColumnInfoTest.java b/agents/common/src/test/java/com/dbx/agent/ColumnInfoTest.java new file mode 100644 index 0000000000..8e43b96d7e --- /dev/null +++ b/agents/common/src/test/java/com/dbx/agent/ColumnInfoTest.java @@ -0,0 +1,30 @@ +package com.dbx.agent; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ColumnInfoTest { + @Test + void serializesOptionalCharacterSetAndCollationFields() { + ColumnInfo column = new ColumnInfo( + "name", + "varchar(64)", + true, + "'guest'", + false, + null, + null, + null, + null, + 64, + "utf8mb4", + "utf8mb4_bin" + ); + + String json = new Gson().toJson(column); + + Assertions.assertTrue(json.contains("\"character_set\":\"utf8mb4\""), json); + Assertions.assertTrue(json.contains("\"collation\":\"utf8mb4_bin\""), json); + } +} diff --git a/agents/drivers/goldendb/src/main/java/com/dbx/agent/goldendb/GoldendbAgent.java b/agents/drivers/goldendb/src/main/java/com/dbx/agent/goldendb/GoldendbAgent.java index 80360c4bcc..899b52c5c8 100644 --- a/agents/drivers/goldendb/src/main/java/com/dbx/agent/goldendb/GoldendbAgent.java +++ b/agents/drivers/goldendb/src/main/java/com/dbx/agent/goldendb/GoldendbAgent.java @@ -248,7 +248,8 @@ public final class GoldendbAgent extends AbstractJdbcAgent { try (java.sql.PreparedStatement stmt = requireConnected().prepareStatement( """ SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT, - NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_MAXIMUM_LENGTH + NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION @@ -269,7 +270,9 @@ public final class GoldendbAgent extends AbstractJdbcAgent { emptyToNull(rs.getString("COLUMN_COMMENT")), integerOrNull(rs, "NUMERIC_PRECISION"), integerOrNull(rs, "NUMERIC_SCALE"), - numberToIntOrNull(rs, "CHARACTER_MAXIMUM_LENGTH") + numberToIntOrNull(rs, "CHARACTER_MAXIMUM_LENGTH"), + emptyToNull(rs.getString("CHARACTER_SET_NAME")), + emptyToNull(rs.getString("COLLATION_NAME")) )); } } diff --git a/agents/drivers/goldendb/src/test/java/com/dbx/agent/goldendb/GoldendbAgentMetadataTest.java b/agents/drivers/goldendb/src/test/java/com/dbx/agent/goldendb/GoldendbAgentMetadataTest.java index 6ec7a08cc7..28bfc538bf 100644 --- a/agents/drivers/goldendb/src/test/java/com/dbx/agent/goldendb/GoldendbAgentMetadataTest.java +++ b/agents/drivers/goldendb/src/test/java/com/dbx/agent/goldendb/GoldendbAgentMetadataTest.java @@ -4,12 +4,31 @@ import com.dbx.agent.MetadataListConstraints; import com.dbx.agent.test.TestSupport; import com.dbx.agent.test.JdbcAgentFake; import com.dbx.agent.test.JdbcMetadataSqlFake; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class GoldendbAgentMetadataTest { + @Test + void returnsCharacterSetAndCollationForEditableColumns() { + GoldendbAgent agent = new GoldendbAgent(); + TestSupport.setPrivateConnection(agent, columnsConnection()); + + var columns = agent.getColumns("app", "orders"); + + Assertions.assertEquals(1, columns.size()); + Assertions.assertEquals("utf8mb4", columns.get(0).getCharacter_set()); + Assertions.assertEquals("utf8mb4_bin", columns.get(0).getCollation()); + } + @Test void quotesSchemaAndTableIdentifiersInIndexMetadataSql() { GoldendbAgent agent = new GoldendbAgent(); @@ -51,4 +70,71 @@ class GoldendbAgentMetadataTest { Assertions.assertTrue(sql.contains("ORDER BY CASE OBJECT_TYPE"), sql); Assertions.assertTrue(sql.endsWith("LIMIT 10"), sql); } + + private static Connection columnsConnection() { + InvocationHandler connectionHandler = (proxy, method, args) -> { + if ("prepareStatement".equals(method.getName())) { + String sql = (String) args[0]; + boolean columnsQuery = sql.contains("CHARACTER_SET_NAME"); + return preparedStatement(columnsQuery ? columnResultSet() : emptyResultSet()); + } + if ("getAutoCommit".equals(method.getName())) return true; + if ("close".equals(method.getName())) return null; + return defaultValue(method.getReturnType()); + }; + return proxy(Connection.class, connectionHandler); + } + + private static PreparedStatement preparedStatement(ResultSet resultSet) { + InvocationHandler handler = (proxy, method, args) -> { + if ("executeQuery".equals(method.getName())) return resultSet; + if ("setString".equals(method.getName()) || "close".equals(method.getName())) return null; + return defaultValue(method.getReturnType()); + }; + return proxy(PreparedStatement.class, handler); + } + + private static ResultSet emptyResultSet() { + return resultSet(List.of()); + } + + private static ResultSet columnResultSet() { + Map row = new HashMap<>(); + row.put("COLUMN_NAME", "name"); + row.put("COLUMN_TYPE", "varchar(64)"); + row.put("IS_NULLABLE", "YES"); + row.put("COLUMN_DEFAULT", "'guest'"); + row.put("EXTRA", ""); + row.put("COLUMN_COMMENT", ""); + row.put("NUMERIC_PRECISION", null); + row.put("NUMERIC_SCALE", null); + row.put("CHARACTER_MAXIMUM_LENGTH", 64); + row.put("CHARACTER_SET_NAME", "utf8mb4"); + row.put("COLLATION_NAME", "utf8mb4_bin"); + return resultSet(List.of(row)); + } + + private static ResultSet resultSet(List> rows) { + int[] index = {-1}; + InvocationHandler handler = (proxy, method, args) -> { + if ("next".equals(method.getName())) return ++index[0] < rows.size(); + if (("getString".equals(method.getName()) || "getObject".equals(method.getName())) && args != null && args.length == 1) { + return rows.get(index[0]).get(args[0]); + } + if ("close".equals(method.getName())) return null; + return defaultValue(method.getReturnType()); + }; + return proxy(ResultSet.class, handler); + } + + private static T proxy(Class type, InvocationHandler handler) { + return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler)); + } + + private static Object defaultValue(Class type) { + if (Boolean.TYPE.equals(type)) return false; + if (Integer.TYPE.equals(type)) return 0; + if (Long.TYPE.equals(type)) return 0L; + return null; + } } diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index f53a0ed0f2..11415398d2 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -6693,6 +6693,37 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() { vec!["ALTER TABLE `users` MODIFY COLUMN `name` varchar(255) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci` DEFAULT 'guest';"] ); } + +#[test] +fn goldendb_character_column_keeps_charset_collation_when_default_changes() { + let mut name = column("name"); + name.data_type = "varchar(64)".to_string(); + name.default_value = "guest".to_string(); + name.character_set = "utf8mb4".to_string(); + name.collation = "utf8mb4_bin".to_string(); + name.original = Some(ColumnInfo { + name: "name".to_string(), + data_type: "varchar(64)".to_string(), + is_nullable: true, + column_default: Some("member".to_string()), + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_bin".to_string()), + ..Default::default() + }); + + let result = build_table_structure_change_sql(structure_change_options( + DatabaseType::Goldendb, + Some("app"), + "users", + vec![name], + )); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["ALTER TABLE `users` MODIFY COLUMN `name` varchar(64) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_bin` DEFAULT 'guest';"] + ); +} #[test] fn mysql_inherited_column_charset_is_omitted_from_generated_ddl() { // MySQL reports the effective collation of every character column, so a column