github-actions[bot] commented on code in PR #65325:
URL: https://github.com/apache/doris/pull/65325#discussion_r3550201584


##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;
+            case "SMALLINT":
+            case "INT2":
+            case "YEAR":
+                return unsigned ? DorisType.INT : DorisType.SMALLINT;
+            case "MEDIUMINT":
+            case "INT":
+            case "INTEGER":
+            case "INT3":
+            case "INT4":
+                return unsigned ? DorisType.BIGINT : DorisType.INT;
+            case "BIGINT":
+            case "INT8":
+                return unsigned ? DorisType.LARGEINT : DorisType.BIGINT;
+            case "FLOAT":
+            case "FLOAT4":
+                return DorisType.FLOAT;
+            case "DOUBLE":
+            case "FLOAT8":
+            case "REAL":
+                return DorisType.DOUBLE;
+            case "DECIMAL":
+            case "DEC":
+            case "FIXED":
+            case "NUMERIC":
+                {
+                    int precision = length > 0 ? length : 10;
+                    if (unsigned) {
+                        precision++;
+                    }
+                    if (precision > MAX_DECIMAL128_PRECISION) {
+                        return DorisType.STRING;
+                    }
+                    int decimalScale = scale >= 0 ? scale : 0;
+                    return String.format("%s(%d, %d)", DorisType.DECIMAL, 
precision, decimalScale);
+                }
+            case "DATE":
+                return DorisType.DATE;
+            case "DATETIME":
+            case "TIMESTAMP":
+                {
+                    int timeScale = mysqlTimeScale(length, scale);
+                    return String.format("%s(%d)", DorisType.DATETIME, 
timeScale);
+                }
+            case "CHAR":
+            case "NCHAR":
+                return charToDorisType(length);
+            case "VARCHAR":
+            case "NVARCHAR":
+                return varcharToDorisType(length);
+            case "BIT":
+                return length == 1 ? DorisType.BOOLEAN : DorisType.STRING;
+            case "JSON":
+                return DorisType.JSON;

Review Comment:
   This MySQL ADD COLUMN mapper still diverges from initial table creation for 
JSON. When the column exists at job creation, FE goes through 
`JdbcMySQLClient.jdbcTypeToDoris()` and maps MySQL `JSON` to `STRING`, but a 
later `ALTER TABLE ... ADD COLUMN payload JSON` reaches this new branch and 
emits Doris `JSON`. That means the same upstream schema gets a different Doris 
type depending on whether the column existed before the job started or was 
added later. Please mirror the initial MySQL JDBC mapping here, or change both 
paths together with coverage for the intended JSON behavior.



##########
fe/fe-common/src/main/java/org/apache/doris/job/cdc/DataSourceConfigKeys.java:
##########
@@ -39,6 +39,7 @@ public class DataSourceConfigKeys {
     public static final String SNAPSHOT_PARALLELISM = "snapshot_parallelism";
     public static final String SNAPSHOT_PARALLELISM_DEFAULT = "1";
     public static final String SKIP_SNAPSHOT_BACKFILL = 
"skip_snapshot_backfill";
+    public static final String SCHEMA_CHANGE_ENABLED = "schema_change_enabled";

Review Comment:
   Please also register this new source property in FE validation. The readers 
now consume `schema_change_enabled` and tests cover setting it to `false`, but 
`CreateJobCommand`/`AlterJobCommand` still pass source properties through 
`DataSourceConfigValidator.validateSource()`, whose `ALLOW_SOURCE_KEYS` does 
not include this key. A user trying `CREATE JOB ... FROM MYSQL/POSTGRES 
("schema_change_enabled"="false", ...)` will get `Unexpected key: 
'schema_change_enabled'` before the request reaches cdc_client, so the new 
disable path is only usable from internally injected TVF properties, not from 
the streaming job source config.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/deserialize/MySqlDebeziumJsonDeserializer.java:
##########
@@ -53,14 +69,185 @@ public void init(Map<String, String> props) {
     public DeserializeResult deserialize(Map<String, String> context, 
SourceRecord record)
             throws IOException {
         if (RecordUtils.isSchemaChangeEvent(record)) {
+            if (!isSchemaChangeEnabled(context)) {
+                LOG.info("[SCHEMA-CHANGE-SKIPPED] MySQL schema change is 
disabled.");
+                return DeserializeResult.empty();
+            }
             return handleSchemaChangeEvent(record, context);
         }
         return super.deserialize(context, record);
     }
 
     private DeserializeResult handleSchemaChangeEvent(
-            SourceRecord record, Map<String, String> context) {
-        // todo: record has mysql ddl, need to convert doris ddl
-        return DeserializeResult.empty();
+            SourceRecord record, Map<String, String> context) throws 
IOException {
+        HistoryRecord history = RecordUtils.getHistoryRecord(record);
+        String database = 
history.document().getString(HistoryRecord.Fields.DATABASE_NAME);
+        String ddl = 
history.document().getString(HistoryRecord.Fields.DDL_STATEMENTS);
+        Map<TableId, TableChanges.TableChange> freshSchemas = 
deserializeTableChanges(history);
+        LOG.info(
+                "[SCHEMA-CHANGE] MySQL deserializer received schema change, 
baselineSchemas={}, freshSchemas={}, ddl={}",
+                tableSchemas == null ? 0 : tableSchemas.size(),
+                freshSchemas.size(),
+                ddl);
+        if (freshSchemas.isEmpty()) {
+            LOG.warn(
+                    "[SCHEMA-CHANGE-SKIPPED] MySQL schema change event has no 
table schema "
+                            + "change. DDL: {}",
+                    ddl);
+            return DeserializeResult.empty();
+        }
+
+        if (tableSchemas == null || tableSchemas.isEmpty()) {
+            LOG.info(
+                    "[SCHEMA-CHANGE] MySQL schema baseline is empty, adopting 
fresh schemas as "
+                            + "baseline without emitting Doris DDL. DDL: {}",
+                    ddl);
+            return DeserializeResult.schemaChange(
+                    Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+        }
+        for (TableId tableId : freshSchemas.keySet()) {
+            if (!tableSchemas.containsKey(tableId)) {
+                LOG.info(
+                        "[SCHEMA-CHANGE] MySQL table {} has no baseline, 
adopting fresh schemas as "
+                                + "baseline without emitting Doris DDL. DDL: 
{}",
+                        tableId.identifier(),
+                        ddl);
+                return DeserializeResult.schemaChange(
+                        Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+            }
+        }
+
+        Tables tables = toTables(tableSchemas);
+        List<MySqlSchemaChange> changes = Collections.emptyList();
+        try {
+            CustomMySqlAntlrDdlParser parser = new CustomMySqlAntlrDdlParser();
+            parser.setCurrentDatabase(database);
+            parser.parse(ddl, tables);
+            changes = parser.getAndClearParsedChanges();
+        } catch (Exception e) {
+            LOG.warn(
+                    "[SCHEMA-CHANGE-SKIPPED] MySQL schema change DDL parser 
failed. No Doris DDL "
+                            + "emitted, FE baseline will advance to the schema 
carried by the "
+                            + "history record. DDL: {}",
+                    ddl,
+                    e);
+            return DeserializeResult.schemaChange(
+                    Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+        }
+        if (changes.isEmpty()) {
+            LOG.warn(
+                    "[SCHEMA-CHANGE-SKIPPED] MySQL schema change event 
produced no supported "
+                            + "column change. No Doris DDL emitted, FE 
baseline will advance to "
+                            + "the schema carried by the history record. DDL: 
{}",
+                    ddl);
+            return DeserializeResult.schemaChange(
+                    Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+        }
+
+        List<MySqlSchemaChange> supportedChanges = new ArrayList<>();
+        for (MySqlSchemaChange change : changes) {
+            if (change.getType() == MySqlSchemaChange.Type.UNSUPPORTED) {
+                LOG.warn(
+                        "[SCHEMA-CHANGE-SKIPPED] MySQL unsupported schema 
change {} for table {}: {}."
+                                + " Doris DDL will not be emitted for this 
change, FE baseline will"
+                                + " advance to the schema carried by the 
history record. DDL: {}",
+                        change.getSourceOperation(),
+                        change.getTableId() == null
+                                ? "<unknown>"
+                                : change.getTableId().identifier(),
+                        change.getReason(),
+                        ddl);
+                continue;
+            }
+            supportedChanges.add(change);
+        }
+        if (supportedChanges.isEmpty()) {
+            return DeserializeResult.schemaChange(
+                    Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+        }
+        for (MySqlSchemaChange change : supportedChanges) {
+            TableId tableId = change.getTableId();
+            if (tableId == null || !tableSchemas.containsKey(tableId)) {
+                LOG.warn(
+                        "[SCHEMA-CHANGE-SKIPPED] MySQL schema change table {} 
has no baseline."
+                                + " No Doris DDL emitted, FE baseline will 
advance to the schema"
+                                + " carried by the history record. DDL: {}",
+                        tableId == null ? "<unknown>" : tableId.identifier(),
+                        ddl);
+                return DeserializeResult.schemaChange(
+                        Collections.emptyList(), freshSchemas, 
Collections.emptyList());
+            }
+        }
+
+        List<SchemaChangeOperation> schemaChanges = new ArrayList<>();
+        for (MySqlSchemaChange change : supportedChanges) {
+            TableId tableId = change.getTableId();
+            Set<String> excludedCols =
+                    excludeColumnsCache.getOrDefault(tableId.table(), 
Collections.emptySet());
+            String targetTable = resolveTargetTable(tableId.table());
+            String db = context.get(Constants.DORIS_TARGET_DB);
+            if (change.getType() == MySqlSchemaChange.Type.ADD) {
+                String columnName = change.getColumn().name();
+                if (excludedCols.contains(columnName)) {
+                    LOG.info(
+                            "[SCHEMA-CHANGE] MySQL table {}: added column '{}' 
is excluded, skipping ADD",
+                            tableId.identifier(),
+                            columnName);
+                    continue;
+                }
+                String colType = 
SchemaChangeHelper.mysqlColumnToDorisType(change.getColumn());
+                schemaChanges.add(
+                        SchemaChangeOperation.addColumn(
+                                targetTable,
+                                columnName,
+                                SchemaChangeHelper.buildAddColumnSql(

Review Comment:
   This generated ADD COLUMN DDL drops MySQL default/backfill and `NOT NULL` 
semantics. For example, `ALTER TABLE ... ADD COLUMN c INT NOT NULL DEFAULT 1` 
backfills existing rows in MySQL and records the column as non-null, but this 
path emits only `ALTER TABLE ... ADD COLUMN c INT` plus an optional comment, so 
existing Doris rows remain `NULL` and the target column is nullable. Initial 
table creation does preserve JDBC nullability through 
`getColumnsFromJdbc()`/`ColumnDefinition`, so the same source column behaves 
differently depending on when it is introduced. If the intended schema-change 
contract is to keep added columns nullable to avoid backfill, please 
detect/default such MySQL ADDs and avoid treating them as fully applied; 
otherwise carry the default/backfill/nullability into the Doris DDL.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;
+            case "SMALLINT":
+            case "INT2":
+            case "YEAR":
+                return unsigned ? DorisType.INT : DorisType.SMALLINT;
+            case "MEDIUMINT":
+            case "INT":
+            case "INTEGER":
+            case "INT3":
+            case "INT4":
+                return unsigned ? DorisType.BIGINT : DorisType.INT;
+            case "BIGINT":
+            case "INT8":
+                return unsigned ? DorisType.LARGEINT : DorisType.BIGINT;
+            case "FLOAT":
+            case "FLOAT4":
+                return DorisType.FLOAT;
+            case "DOUBLE":
+            case "FLOAT8":
+            case "REAL":
+                return DorisType.DOUBLE;
+            case "DECIMAL":
+            case "DEC":
+            case "FIXED":
+            case "NUMERIC":
+                {
+                    int precision = length > 0 ? length : 10;
+                    if (unsigned) {
+                        precision++;
+                    }
+                    if (precision > MAX_DECIMAL128_PRECISION) {
+                        return DorisType.STRING;
+                    }
+                    int decimalScale = scale >= 0 ? scale : 0;
+                    return String.format("%s(%d, %d)", DorisType.DECIMAL, 
precision, decimalScale);
+                }
+            case "DATE":
+                return DorisType.DATE;
+            case "DATETIME":
+            case "TIMESTAMP":
+                {
+                    int timeScale = mysqlTimeScale(length, scale);
+                    return String.format("%s(%d)", DorisType.DATETIME, 
timeScale);
+                }
+            case "CHAR":
+            case "NCHAR":
+                return charToDorisType(length);
+            case "VARCHAR":
+            case "NVARCHAR":
+                return varcharToDorisType(length);
+            case "BIT":
+                return length == 1 ? DorisType.BOOLEAN : DorisType.STRING;
+            case "JSON":
+                return DorisType.JSON;
+            case "TINYBLOB":
+            case "BLOB":
+            case "MEDIUMBLOB":
+            case "LONGBLOB":
+            case "BINARY":
+            case "VARBINARY":
+            case "TIME":
+            case "TINYTEXT":
+            case "TEXT":
+            case "MEDIUMTEXT":
+            case "LONGTEXT":
+            case "STRING":
+            case "SET":
+            case "ENUM":
+                return DorisType.STRING;
+            default:
+                LOG.warn("Unrecognized MySQL type '{}', defaulting to STRING", 
mysqlTypeName);
+                return DorisType.STRING;
+        }
+    }
+
+    private static int mysqlTimeScale(int length, int scale) {
+        if (scale >= 0 && scale <= 6) {
+            return scale;
+        }
+        if (length >= 0 && length <= 6) {
+            return length;
+        }
+        return 0;
+    }
+
+    private static String charToDorisType(int length) {
+        if (length <= 0) {
+            return DorisType.STRING;
+        }
+        int len = length * 3;
+        if (len > MAX_VARCHAR_LENGTH) {
+            return DorisType.STRING;
+        }
+        if (len > MAX_CHAR_LENGTH) {
+            return String.format("%s(%d)", DorisType.VARCHAR, len);
+        }
+        return String.format("%s(%d)", DorisType.CHAR, len);
+    }
+
+    private static String varcharToDorisType(int length) {
+        if (length <= 0) {
+            return DorisType.STRING;
+        }
+        int len = length * 3;
+        if (len > MAX_VARCHAR_LENGTH) {
+            return DorisType.STRING;
+        }
+        return String.format("%s(%d)", DorisType.VARCHAR, len);
+    }
+
     /** Map a PostgreSQL native type name to a Doris type string. */
     static String pgTypeNameToDorisType(String pgTypeName, int length, int 
scale) {

Review Comment:
   Please do not default unrecognized PostgreSQL ADD COLUMN types to Doris 
`STRING`. Initial table creation goes through 
`JdbcPostgreSQLClient.jdbcTypeToDoris()`, which returns `UNSUPPORTED` for 
unknown non-array source types, and `StreamingJobUtils.getColumns()` rejects 
those columns before creating the target table. This schema-change path instead 
maps the same unknown type to `STRING`, so a source column can be rejected when 
present at job creation but silently accepted after startup, with the FE 
baseline advanced as if the source DDL were faithfully applied. Please align 
this path with the JDBC mapper, or add an intentional shared mapping with 
coverage.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;
+            case "SMALLINT":
+            case "INT2":
+            case "YEAR":
+                return unsigned ? DorisType.INT : DorisType.SMALLINT;
+            case "MEDIUMINT":

Review Comment:
   This branch over-widens `MEDIUMINT UNSIGNED` compared with initial table 
creation. `JdbcMySQLClient.jdbcTypeToDoris()` maps both `SMALLINT UNSIGNED` and 
`MEDIUMINT UNSIGNED` to Doris `INT`, and the existing integer-boundary expected 
output records `mediumint_u` as `int`. Here `MEDIUMINT` shares the `INT` 
branch, so once `UNSIGNED` is detected it returns Doris `BIGINT`. An upstream 
`MEDIUMINT UNSIGNED` column therefore has a different Doris type depending on 
whether it existed before the job started or was added later. Please keep this 
mapper aligned with the JDBC path, while still mapping `INT UNSIGNED` to 
`BIGINT`.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));

Review Comment:
   This PostgreSQL ADD COLUMN mapper diverges from initial table creation for 
`json`/`jsonb`. The JDBC creation path maps those source types to Doris 
string/text, and existing generated baselines record PostgreSQL `json` and 
`jsonb` columns as `text`; this schema-change path returns Doris `JSON` 
instead. That means the same upstream column has a different Doris type 
depending on whether it existed before the job started or was added later. 
Please align this mapper with `JdbcPostgreSQLClient`, or change both paths 
together with regression coverage for the intended PostgreSQL JSON behavior.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;
+            case "SMALLINT":
+            case "INT2":
+            case "YEAR":
+                return unsigned ? DorisType.INT : DorisType.SMALLINT;
+            case "MEDIUMINT":
+            case "INT":
+            case "INTEGER":
+            case "INT3":
+            case "INT4":
+                return unsigned ? DorisType.BIGINT : DorisType.INT;
+            case "BIGINT":
+            case "INT8":
+                return unsigned ? DorisType.LARGEINT : DorisType.BIGINT;
+            case "FLOAT":
+            case "FLOAT4":
+                return DorisType.FLOAT;
+            case "DOUBLE":
+            case "FLOAT8":
+            case "REAL":
+                return DorisType.DOUBLE;
+            case "DECIMAL":
+            case "DEC":
+            case "FIXED":
+            case "NUMERIC":
+                {
+                    int precision = length > 0 ? length : 10;
+                    if (unsigned) {
+                        precision++;
+                    }
+                    if (precision > MAX_DECIMAL128_PRECISION) {
+                        return DorisType.STRING;
+                    }
+                    int decimalScale = scale >= 0 ? scale : 0;
+                    return String.format("%s(%d, %d)", DorisType.DECIMAL, 
precision, decimalScale);
+                }
+            case "DATE":
+                return DorisType.DATE;
+            case "DATETIME":
+            case "TIMESTAMP":
+                {
+                    int timeScale = mysqlTimeScale(length, scale);
+                    return String.format("%s(%d)", DorisType.DATETIME, 
timeScale);
+                }
+            case "CHAR":
+            case "NCHAR":
+                return charToDorisType(length);
+            case "VARCHAR":
+            case "NVARCHAR":
+                return varcharToDorisType(length);
+            case "BIT":
+                return length == 1 ? DorisType.BOOLEAN : DorisType.STRING;
+            case "JSON":
+                return DorisType.JSON;
+            case "TINYBLOB":
+            case "BLOB":
+            case "MEDIUMBLOB":
+            case "LONGBLOB":
+            case "BINARY":
+            case "VARBINARY":
+            case "TIME":
+            case "TINYTEXT":
+            case "TEXT":
+            case "MEDIUMTEXT":
+            case "LONGTEXT":
+            case "STRING":
+            case "SET":
+            case "ENUM":
+                return DorisType.STRING;
+            default:
+                LOG.warn("Unrecognized MySQL type '{}', defaulting to STRING", 
mysqlTypeName);
+                return DorisType.STRING;
+        }
+    }
+
+    private static int mysqlTimeScale(int length, int scale) {
+        if (scale >= 0 && scale <= 6) {
+            return scale;
+        }
+        if (length >= 0 && length <= 6) {
+            return length;
+        }
+        return 0;
+    }
+
+    private static String charToDorisType(int length) {
+        if (length <= 0) {
+            return DorisType.STRING;
+        }
+        int len = length * 3;
+        if (len > MAX_VARCHAR_LENGTH) {
+            return DorisType.STRING;
+        }
+        if (len > MAX_CHAR_LENGTH) {
+            return String.format("%s(%d)", DorisType.VARCHAR, len);
+        }
+        return String.format("%s(%d)", DorisType.CHAR, len);
+    }
+
+    private static String varcharToDorisType(int length) {
+        if (length <= 0) {
+            return DorisType.STRING;
+        }
+        int len = length * 3;
+        if (len > MAX_VARCHAR_LENGTH) {
+            return DorisType.STRING;
+        }
+        return String.format("%s(%d)", DorisType.VARCHAR, len);
+    }
+
     /** Map a PostgreSQL native type name to a Doris type string. */
     static String pgTypeNameToDorisType(String pgTypeName, int length, int 
scale) {
         Preconditions.checkNotNull(pgTypeName);

Review Comment:
   This array shortcut accepts PostgreSQL array types that initial table 
creation rejects. The JDBC path only allows array inner types from 
`supportedInnerType`; if the inner type is not in that list, 
`convertArrayType()` returns `UNSUPPORTED` and the job creation path rejects 
the column. Here any `_type` is recursively mapped, so arrays such as `_bytea`, 
`_time`, or `_hstore` become Doris `ARRAY<STRING>` when added after startup. 
Please apply the same PostgreSQL array allowlist as `JdbcPostgreSQLClient`, or 
add an intentional shared mapping with coverage for the extra array types.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;

Review Comment:
   This branch also needs to preserve the existing MySQL `TINYINT(1)` boolean 
mapping. The initial streaming-job path already expects a source `bool_col 
tinyint(1)` to become Doris `boolean` (see the committed 
`test_streaming_mysql_job_integer_boundary` expected `DESC` output), but an 
`ALTER TABLE ... ADD COLUMN flag TINYINT(1)` reaches this mapper and returns 
Doris `TINYINT` because the branch ignores `column.length()`. That makes the 
same source schema produce `BOOLEAN` or `TINYINT` depending on when the column 
appears. Please mirror the initial mapping here, or change both paths together 
with coverage for the intended behavior.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -136,6 +109,130 @@ public static String columnToDorisType(Column column) {
         return pgTypeNameToDorisType(column.typeName(), column.length(), 
column.scale().orElse(-1));
     }
 
+    /** Convert a Debezium MySQL Column to a Doris column type string. */
+    public static String mysqlColumnToDorisType(Column column) {
+        Preconditions.checkNotNull(column.typeName());
+        String mysqlTypeName = column.typeName().toUpperCase();
+        String[] typeFields = mysqlTypeName.split(" ");
+        String mysqlType = typeFields[0];
+        boolean unsigned = typeFields.length > 1 && 
"UNSIGNED".equals(typeFields[1]);
+        int length = column.length();
+        int scale = column.scale().orElse(-1);
+        switch (mysqlType) {
+            case "BOOLEAN":
+            case "BOOL":
+                return DorisType.BOOLEAN;
+            case "TINYINT":
+                return unsigned ? DorisType.SMALLINT : DorisType.TINYINT;
+            case "SMALLINT":
+            case "INT2":
+            case "YEAR":
+                return unsigned ? DorisType.INT : DorisType.SMALLINT;
+            case "MEDIUMINT":
+            case "INT":
+            case "INTEGER":
+            case "INT3":
+            case "INT4":
+                return unsigned ? DorisType.BIGINT : DorisType.INT;
+            case "BIGINT":
+            case "INT8":
+                return unsigned ? DorisType.LARGEINT : DorisType.BIGINT;
+            case "FLOAT":
+            case "FLOAT4":
+                return DorisType.FLOAT;
+            case "DOUBLE":
+            case "FLOAT8":
+            case "REAL":
+                return DorisType.DOUBLE;
+            case "DECIMAL":
+            case "DEC":
+            case "FIXED":
+            case "NUMERIC":
+                {
+                    int precision = length > 0 ? length : 10;
+                    if (unsigned) {
+                        precision++;
+                    }
+                    if (precision > MAX_DECIMAL128_PRECISION) {
+                        return DorisType.STRING;
+                    }
+                    int decimalScale = scale >= 0 ? scale : 0;
+                    return String.format("%s(%d, %d)", DorisType.DECIMAL, 
precision, decimalScale);
+                }
+            case "DATE":
+                return DorisType.DATE;
+            case "DATETIME":
+            case "TIMESTAMP":
+                {
+                    int timeScale = mysqlTimeScale(length, scale);
+                    return String.format("%s(%d)", DorisType.DATETIME, 
timeScale);
+                }
+            case "CHAR":
+            case "NCHAR":
+                return charToDorisType(length);
+            case "VARCHAR":
+            case "NVARCHAR":
+                return varcharToDorisType(length);
+            case "BIT":
+                return length == 1 ? DorisType.BOOLEAN : DorisType.STRING;
+            case "JSON":
+                return DorisType.JSON;
+            case "TINYBLOB":
+            case "BLOB":
+            case "MEDIUMBLOB":
+            case "LONGBLOB":
+            case "BINARY":
+            case "VARBINARY":
+            case "TIME":
+            case "TINYTEXT":
+            case "TEXT":
+            case "MEDIUMTEXT":
+            case "LONGTEXT":
+            case "STRING":
+            case "SET":
+            case "ENUM":
+                return DorisType.STRING;
+            default:
+                LOG.warn("Unrecognized MySQL type '{}', defaulting to STRING", 
mysqlTypeName);

Review Comment:
   Please do not let unsupported MySQL ADD COLUMN types fall through to 
`STRING` here. The custom parser registers spatial types such as 
`POINT`/`GEOMETRY`, but this mapper has no matching cases, so `ALTER TABLE ... 
ADD COLUMN g POINT` reaches the default branch and becomes a Doris `STRING` 
column. Initial job creation goes through `JdbcMySQLClient.jdbcTypeToDoris()` 
and `StreamingJobUtils.getColumns()`, which reject unsupported source columns 
instead of silently remapping them. This makes the same source schema accepted 
or rejected depending on when the column appears, and advances the schema 
baseline as if the ADD were faithfully applied. Please reject/skip these types 
without treating the DDL as applied, or add an explicit shared mapping with 
coverage.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/deserialize/PostgresDebeziumJsonDeserializer.java:
##########
@@ -96,6 +100,11 @@ private DeserializeResult handleSchemaChangeEvent(
         // Debezium PG TableId is (catalog=null, schema, table) — matches the 
tableSchemas key.
         TableId tableId = freshTable.id();
         TableChanges.TableChange stored = tableSchemas != null ? 
tableSchemas.get(tableId) : null;
+        LOG.info(
+                "[SCHEMA-CHANGE] Postgres deserializer received schema change, 
table={}, baselineSchemas={}, hasStoredSchema={}",
+                tableId.identifier(),
+                tableSchemas == null ? 0 : tableSchemas.size(),
+                stored != null && stored.getTable() != null);
 
         // changeType is not consumed inside cdc_client — downstream only 
reads getTable() and
         // serializeTableSchemas does not persist it — so ALTER is used 
uniformly, including for the

Review Comment:
   This advances the PostgreSQL schema baseline even though no Doris DDL is 
emitted. A non-ADD/DROP Relation change reaches this branch, and the rename 
guard below has the same pattern for simultaneous ADD+DROP: `updatedSchemas` 
gets the fresh source schema, the DDL list is empty, and `PipelineCoordinator` 
still calls `applySchemaChange()`. After that, FE believes the target has the 
new source schema while the Doris table was left unchanged. Please keep 
skipped/manual PostgreSQL changes from being persisted as applied, or require 
the corresponding Doris schema change before committing offsets against the 
fresh baseline.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/deserialize/PostgresDebeziumJsonDeserializer.java:
##########
@@ -200,7 +209,7 @@ private DeserializeResult handleSchemaChangeEvent(
                             targetTable,
                             col.name(),
                             SchemaChangeHelper.buildAddColumnSql(

Review Comment:
   This path acknowledges that existing Doris rows are not backfilled, but it 
still treats PostgreSQL `ADD COLUMN ... DEFAULT` / `NOT NULL` as an applied 
schema change and advances the baseline. PostgreSQL returns the default for 
existing source rows after such an ADD, while Doris keeps those rows `NULL` and 
omits the source default/constraint. If backfilling is not supported here, 
these DEFAULT/NOT NULL ADDs should be treated as unsupported/manual instead of 
being persisted as fully applied; otherwise the Doris DDL needs to carry 
equivalent default/backfill/nullability behavior.



##########
fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/utils/SchemaChangeHelper.java:
##########
@@ -164,22 +261,15 @@ static String pgTypeNameToDorisType(String pgTypeName, 
int length, int scale) {
                 return DorisType.DOUBLE;
             case "numeric":
                 {
-                    int p = length > 0 ? Math.min(length, 38) : 38;
+                    if (length > MAX_DECIMAL128_PRECISION) {
+                        return DorisType.STRING;
+                    }
+                    int p = length > 0 ? length : 38;

Review Comment:
   This default for PostgreSQL `numeric` does not match initial table creation. 
For an unbounded `numeric`, the JDBC path passes precision 0 into 
`createDecimalOrStringType()`, which falls back to Doris `STRING`; this 
schema-change mapper instead turns missing precision/scale into `DECIMAL(38, 
9)`. The same upstream column can therefore be unconstrained when present at 
job creation but fixed to decimal(38,9) when added later. Please align this 
with `JdbcPostgreSQLClient` and return `STRING` when the source precision is 
not positive, including array coverage if `_numeric` remains supported.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to