This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 5ffdc28db08 [improvement](binlog) Make binlog.format property 
case-insensitive (#68982)
5ffdc28db08 is described below

commit 5ffdc28db08cb7c2d54bd0a3993b06e2f3179869
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 13 16:01:18 2026 +0800

    [improvement](binlog) Make binlog.format property case-insensitive (#68982)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    #### In short
    
    The table and database property `binlog.format` only accepted the exact
    upper case names `ROW` and `STATEMENT_AND_SNAPSHOT`. `'binlog.format' =
    'row'` was rejected by every DDL, and CREATE TABLE even answered with
    Java's internal `No enum constant ...` message. This PR matches the
    value case-insensitively at every place it is parsed and always stores
    the canonical upper case name, including for database properties, which
    Doris persists as written and parses again on replay. Unknown values are
    still rejected, now with the same message everywhere.
    
    #### Background
    
    - `binlog.format` is a table and database property. `ROW` turns on row
    binlog (table streams, IVM, the `binlog()` TVF);
    `STATEMENT_AND_SNAPSHOT` is the CCR binlog. A table's format cannot be
    changed after it is created.
    - The value is parsed in three places, all with `BinlogFormat.valueOf`,
    which is case-sensitive:
    - `PropertyAnalyzer#analyzeBinlogConfig`: validation used by ALTER
    TABLE, CREATE DATABASE and ALTER DATABASE (and by CREATE TABLE, but only
    later, see below).
    - `BinlogConfig#mergeFromProperties`: builds an effective config. CREATE
    TABLE calls it on the raw properties first, in
    `Database#getBinlogConfigsForCreateTable`, before anything validates
    them. Database properties also go through it on every replay and image
    load (`Database#gsonPostProcess`, `replayUpdateDbProperties`).
    - `TableProperty#buildBinlogConfig`: rebuilds a table's config from its
    persisted properties.
    - Table metadata stores the format from the enum
    (`BinlogConfig#toProperties`), so it is always canonical. Database
    properties are different: `DatabaseProperty` keeps the map exactly as
    the user wrote it, and that map is what goes to the edit log and the
    image.
    - Neighbouring enum-like properties such as `storage_medium` and
    `compression` are already matched case-insensitively.
    
    #### The problem, and what it cost
    
    - `'binlog.format' = 'row'`, or any spelling other than all upper case,
    is rejected by all four DDLs that take the property. CREATE TABLE fails
    with `No enum constant
    org.apache.doris.catalog.BinlogConfig.BinlogFormat.row`, which does not
    even name the property; ALTER TABLE, CREATE DATABASE and ALTER DATABASE
    fail with `Invalid binlog format value: row`.
    - Users have to know that this one value is case-sensitive while the
    properties next to it are not. The full before / after comparison is in
    Results below.
    
    #### How this PR fixes it
    
    1. `BinlogConfig.BinlogFormat#fromString` looks the value up by enum
    name, ignoring case, and throws `IllegalArgumentException("Invalid
    binlog format value: <value>")` for anything else. The three parse sites
    above all use it.
    2. `PropertyAnalyzer#analyzeBinlogConfig` returns the canonical name, so
    the table property, `SHOW CREATE TABLE` and
    `information_schema.table_properties` show `ROW`.
    3. `PropertyAnalyzer#normalizeBinlogFormat`, called by
    `CreateDatabaseCommand` and `AlterDatabasePropertiesCommand` right after
    validation, rewrites the database property to its canonical name before
    it is stored. This matters for compatibility: the stored map goes to the
    edit log and the image, and an FE without this change (an old follower
    during a rolling upgrade, or after a downgrade) parses it with
    `valueOf`. Had it been stored as `row`, that FE would fail to replay it
    with the same `No enum constant` exception. With the rewrite, the stored
    value is always one that an FE without this change could have written
    itself.
    
    What it buys:
    
    - Any case is accepted by all four DDLs, and the stored and displayed
    value is always upper case.
    - Persisted metadata keeps exactly the old format, so mixed-version
    clusters and downgrades are unaffected.
    - An unknown value such as `rows` gets `Invalid binlog format value:
    rows` from CREATE TABLE too, instead of the `No enum constant` message.
    
    #### Results
    
    "Before" is current master: the messages were reproduced by switching
    `fromString` back to `valueOf` in the FE unit tests. "After" is this
    branch on a local 1 FE + 1 BE cluster, checked by the new regression
    suite.
    
    | Statement | Before | After |
    |---|---|---|
    | `CREATE TABLE ... ('binlog.enable' = 'true', 'binlog.format' = 'row')`
    | `No enum constant
    org.apache.doris.catalog.BinlogConfig.BinlogFormat.row` | ROW binlog
    table; `information_schema.table_properties` shows `ROW`; the `binlog()`
    TVF returns the inserted rows |
    | `CREATE TABLE ... ('binlog.enable' = 'true', 'binlog.format' =
    'Statement_And_Snapshot')` | `No enum constant
    ...BinlogFormat.Statement_And_Snapshot` | CCR binlog table, stored as
    `STATEMENT_AND_SNAPSHOT` |
    | `ALTER TABLE <ROW table> SET ('binlog.format' = 'Row')` | `Invalid
    binlog format value: Row` | accepted, no-op |
    | `ALTER TABLE <ROW table> SET ('binlog.format' =
    'statement_and_snapshot')` | `Invalid binlog format value:
    statement_and_snapshot` | `not support change binlog format from ROW to
    STATEMENT_AND_SNAPSHOT` (the real reason) |
    | `CREATE DATABASE d PROPERTIES ('binlog.enable' = 'true',
    'binlog.format' = 'row')` | `Invalid binlog format value: row` |
    created; `information_schema.database_properties` shows `ROW`; a new
    table in `d` inherits ROW binlog |
    | `ALTER DATABASE d SET PROPERTIES ('binlog.enable' = 'false',
    'binlog.format' = 'statement_and_snapshot')` | `Invalid binlog format
    value: statement_and_snapshot` | accepted, stored as
    `STATEMENT_AND_SNAPSHOT` |
    | `CREATE TABLE ... ('binlog.format' = 'rows')` | `No enum constant
    ...BinlogFormat.rows` | `Invalid binlog format value: rows` |
    | ALTER TABLE / CREATE DATABASE / ALTER DATABASE with `'rows'` |
    `Invalid binlog format value: rows` | unchanged |
    
    #### Classes, and how they call each other
    
    - `BinlogConfig.BinlogFormat#fromString` (new): the only parser of the
    value.
    - `PropertyAnalyzer#analyzeBinlogConfig`: validates, now returns the
    canonical name.
    - `PropertyAnalyzer#normalizeBinlogFormat` (new): rewrites a map's value
    in place; used only for database properties.
    - `BinlogConfig#mergeFromProperties`, `TableProperty#buildBinlogConfig`:
    switched from `valueOf` to `fromString`.
    - `CreateDatabaseCommand#validate`,
    `AlterDatabasePropertiesCommand#validate`: call `normalizeBinlogFormat`
    after `analyzeBinlogConfig`.
    
    ```
    CREATE TABLE     InternalCatalog#createOlapTable
                       Database#getBinlogConfigsForCreateTable -> 
BinlogConfig#mergeFromProperties -> fromString
                       PropertyAnalyzer#analyzeBinlogConfig                     
                   -> fromString
                       (the table stores BinlogConfig#toProperties, i.e. the 
enum name)
    ALTER TABLE      ModifyTablePropertiesOp#validate        -> 
PropertyAnalyzer#analyzeBinlogConfig -> fromString
                     SchemaChangeHandler#updateBinlogConfig  -> 
analyzeBinlogConfig + mergeFromProperties
    CREATE DATABASE  CreateDatabaseCommand#validate          -> 
analyzeBinlogConfig, then normalizeBinlogFormat -> fromString
    ALTER DATABASE   AlterDatabasePropertiesCommand#validate -> 
analyzeBinlogConfig, then normalizeBinlogFormat -> fromString
                       (the map is then stored in DatabaseProperty and 
journaled as is)
    replay / image   Database#gsonPostProcess, #replayUpdateDbProperties
                       -> DatabaseProperty#getBinlogConfig -> 
BinlogConfig#mergeFromProperties    -> fromString
                     TableProperty#buildBinlogConfig                            
                  -> fromString
    ```
    
    Not changed: BE, thrift and protobuf (they receive the enum, not the
    string), the rule that a table's format is immutable, and every other
    `binlog.*` property.
---
 .../org/apache/doris/catalog/BinlogConfig.java     |  12 +-
 .../org/apache/doris/catalog/TableProperty.java    |   2 +-
 .../apache/doris/common/util/PropertyAnalyzer.java |  13 +-
 .../commands/AlterDatabasePropertiesCommand.java   |   1 +
 .../plans/commands/CreateDatabaseCommand.java      |   1 +
 .../catalog/BinlogFormatCaseInsensitiveTest.java   | 147 +++++++++++++++++
 .../apache/doris/common/PropertyAnalyzerTest.java  |  27 ++++
 .../test_binlog_format_case_insensitive.out        |  21 +++
 .../test_binlog_format_case_insensitive.groovy     | 180 +++++++++++++++++++++
 9 files changed, 400 insertions(+), 4 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BinlogConfig.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BinlogConfig.java
index fc6a65f824d..aeaa64da7ce 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/BinlogConfig.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/BinlogConfig.java
@@ -69,6 +69,16 @@ public class BinlogConfig {
         public int value() {
             return this.value;
         }
+
+        // binlog.format is a user-facing property value, so it is matched 
case-insensitively.
+        public static BinlogFormat fromString(String format) {
+            for (BinlogFormat binlogFormat : values()) {
+                if (binlogFormat.name().equalsIgnoreCase(format)) {
+                    return binlogFormat;
+                }
+            }
+            throw new IllegalArgumentException("Invalid binlog format value: " 
+ format);
+        }
     }
 
     @SerializedName("needHistoricalValue")
@@ -147,7 +157,7 @@ public class BinlogConfig {
         }
         if (properties.containsKey(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT)) 
{
             BinlogFormat tmpBinlogFormat =
-                    
BinlogFormat.valueOf(properties.get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+                    
BinlogFormat.fromString(properties.get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
             if (!force && tmpBinlogFormat != binlogFormat) {
                 LOG.warn("not support change {} from {} to {}",
                         PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/TableProperty.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/TableProperty.java
index 1378b39233d..9d0c3d96f6f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/TableProperty.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/TableProperty.java
@@ -616,7 +616,7 @@ public class TableProperty implements GsonPostProcessable {
                     
Long.parseLong(properties.get(PropertyAnalyzer.PROPERTIES_BINLOG_MAX_HISTORY_NUMS)));
         }
         if (properties.containsKey(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT)) 
{
-            binlogConfig.setBinlogFormat(BinlogConfig.BinlogFormat.valueOf(
+            binlogConfig.setBinlogFormat(BinlogConfig.BinlogFormat.fromString(
                     
properties.get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT)));
         }
         if 
(properties.containsKey(PropertyAnalyzer.PROPERTIES_BINLOG_NEED_HISTORICAL_VALUE))
 {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
index c8bccb9449f..21a0fe74ff1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
@@ -1574,8 +1574,7 @@ public class PropertyAnalyzer {
         if (properties.containsKey(PROPERTIES_BINLOG_FORMAT)) {
             String format = properties.get(PROPERTIES_BINLOG_FORMAT);
             try {
-                BinlogConfig.BinlogFormat.valueOf(format);
-                binlogConfigMap.put(PROPERTIES_BINLOG_FORMAT, format);
+                binlogConfigMap.put(PROPERTIES_BINLOG_FORMAT, 
BinlogConfig.BinlogFormat.fromString(format).name());
                 properties.remove(PROPERTIES_BINLOG_FORMAT);
             } catch (Exception e) {
                 throw new AnalysisException("Invalid binlog format value: " + 
format);
@@ -1597,6 +1596,16 @@ public class PropertyAnalyzer {
         return binlogConfigMap;
     }
 
+    /**
+     * Rewrite binlog.format to its canonical name, for properties persisted 
as written (database
+     * properties): they are parsed again on replay, and an FE that predates 
case-insensitive parsing
+     * only accepts the canonical name. The value must already be validated by 
analyzeBinlogConfig.
+     */
+    public static void normalizeBinlogFormat(Map<String, String> properties) {
+        properties.computeIfPresent(PROPERTIES_BINLOG_FORMAT,
+                (key, format) -> 
BinlogConfig.BinlogFormat.fromString(format).name());
+    }
+
     public static boolean analyzeIsBeingSynced(Map<String, String> properties, 
boolean defaultValue) {
         if (properties != null && 
properties.containsKey(PROPERTIES_IS_BEING_SYNCED)) {
             String value = properties.remove(PROPERTIES_IS_BEING_SYNCED);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterDatabasePropertiesCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterDatabasePropertiesCommand.java
index a6350ae0d0e..d507d7bebc9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterDatabasePropertiesCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterDatabasePropertiesCommand.java
@@ -66,6 +66,7 @@ public class AlterDatabasePropertiesCommand extends 
AlterCommand {
         // clone properties for analyse
         Map<String, String> analysisProperties = new HashMap<String, 
String>(properties);
         PropertyAnalyzer.analyzeBinlogConfig(analysisProperties);
+        PropertyAnalyzer.normalizeBinlogFormat(properties);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDatabaseCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDatabaseCommand.java
index 75c81808d85..b754fb96a66 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDatabaseCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDatabaseCommand.java
@@ -81,6 +81,7 @@ public class CreateDatabaseCommand extends Command implements 
ForwardWithSync, N
         FeNameFormat.checkDbName(dbName);
         InternalDatabaseUtil.checkDatabase(dbName, ConnectContext.get());
         PropertyAnalyzer.analyzeBinlogConfig(new HashMap<>(properties));
+        PropertyAnalyzer.normalizeBinlogFormat(properties);
         BinlogConfig config = BinlogConfig.fromProperties(properties);
         if (config.isEnableForStreaming() && 
properties.containsKey(PropertyAnalyzer.PROPERTIES_BINLOG_TTL_SECONDS)) {
             config.applyExplicitRowTtl(config.getTtlSeconds());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/BinlogFormatCaseInsensitiveTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/BinlogFormatCaseInsensitiveTest.java
new file mode 100644
index 00000000000..5d86e35354d
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/BinlogFormatCaseInsensitiveTest.java
@@ -0,0 +1,147 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.catalog;
+
+import org.apache.doris.catalog.BinlogConfig.BinlogFormat;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.util.PropertyAnalyzer;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class BinlogFormatCaseInsensitiveTest extends TestWithFeService {
+    private Database db;
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        Config.enable_feature_binlog = true;
+        createDatabase("binlog_format_case");
+        db = 
Env.getCurrentInternalCatalog().getDbOrDdlException("binlog_format_case");
+    }
+
+    @Test
+    public void testFromStringIgnoresCase() {
+        Assertions.assertEquals(BinlogFormat.ROW, 
BinlogFormat.fromString("row"));
+        Assertions.assertEquals(BinlogFormat.ROW, 
BinlogFormat.fromString("Row"));
+        Assertions.assertEquals(BinlogFormat.STATEMENT_AND_SNAPSHOT,
+                BinlogFormat.fromString("statement_and_snapshot"));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
BinlogFormat.fromString("rows"));
+    }
+
+    @Test
+    public void testCreateRowTableWithLowerCaseFormat() throws Exception {
+        createTable("CREATE TABLE binlog_format_case.lower_row (k INT) 
DUPLICATE KEY(k) "
+                + "DISTRIBUTED BY HASH(k) BUCKETS 1 "
+                + "PROPERTIES('replication_num'='1', 'binlog.enable'='true', 
'binlog.format'='row')");
+        OlapTable table = (OlapTable) db.getTableOrDdlException("lower_row");
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForStreaming());
+        Assertions.assertTrue(table.enableTso());
+        Assertions.assertEquals("ROW",
+                
table.getTableProperty().getProperties().get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+
+        // The format is still immutable, whatever the case of the new value.
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> alterTableSync(
+                "ALTER TABLE binlog_format_case.lower_row SET 
('binlog.format'='statement_and_snapshot')"));
+        Assertions.assertTrue(exception.getMessage().contains(
+                "not support change binlog format from ROW to 
STATEMENT_AND_SNAPSHOT"), exception.getMessage());
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForStreaming());
+    }
+
+    @Test
+    public void testAlterStatementTableWithLowerCaseFormat() throws Exception {
+        createTable("CREATE TABLE binlog_format_case.lower_statement (k INT) 
DUPLICATE KEY(k) "
+                + "DISTRIBUTED BY HASH(k) BUCKETS 1 "
+                + "PROPERTIES('replication_num'='1', 'binlog.enable'='true', "
+                + "'binlog.format'='Statement_And_Snapshot')");
+        OlapTable table = (OlapTable) 
db.getTableOrDdlException("lower_statement");
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForCCR());
+        Assertions.assertEquals("STATEMENT_AND_SNAPSHOT",
+                
table.getTableProperty().getProperties().get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+
+        // Repeating the current format in another case is a no-op.
+        alterTableSync("ALTER TABLE binlog_format_case.lower_statement "
+                + "SET ('binlog.format'='statement_and_snapshot')");
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForCCR());
+
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> alterTableSync(
+                "ALTER TABLE binlog_format_case.lower_statement SET 
('binlog.format'='row')"));
+        Assertions.assertTrue(exception.getMessage().contains(
+                "not support change binlog format from STATEMENT_AND_SNAPSHOT 
to ROW"), exception.getMessage());
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForCCR());
+    }
+
+    @Test
+    public void testRejectUnknownTableFormat() {
+        IllegalStateException exception = 
Assertions.assertThrows(IllegalStateException.class,
+                () -> executeSql("CREATE TABLE 
binlog_format_case.unknown_format (k INT) DUPLICATE KEY(k) "
+                        + "DISTRIBUTED BY HASH(k) BUCKETS 1 "
+                        + "PROPERTIES('replication_num'='1', 
'binlog.enable'='true', 'binlog.format'='rows')"));
+        Assertions.assertTrue(exception.getMessage().contains("Invalid binlog 
format value: rows"),
+                exception.getMessage());
+        Assertions.assertNull(db.getTableNullable("unknown_format"));
+    }
+
+    @Test
+    public void testDatabaseFormatIsStoredByCanonicalName() throws Exception {
+        createDatabaseWithSql("CREATE DATABASE binlog_format_case_db "
+                + "PROPERTIES ('binlog.enable'='true', 
'binlog.format'='row')");
+        Database rowDb = 
Env.getCurrentInternalCatalog().getDbOrDdlException("binlog_format_case_db");
+        // Database properties are persisted as given and parsed again on 
replay.
+        Assertions.assertEquals("ROW", 
rowDb.getDbProperties().get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+        Assertions.assertTrue(rowDb.getBinlogConfig().isEnableForStreaming());
+        
Assertions.assertTrue(rowDb.getDbProperties().getBinlogConfig().isEnableForStreaming());
+
+        // A table created in the database inherits the ROW format.
+        createTable("CREATE TABLE binlog_format_case_db.inherited_row (k INT) 
DUPLICATE KEY(k) "
+                + "DISTRIBUTED BY HASH(k) BUCKETS 1 
PROPERTIES('replication_num'='1')");
+        OlapTable table = (OlapTable) 
rowDb.getTableOrDdlException("inherited_row");
+        Assertions.assertTrue(table.getBinlogConfig().isEnableForStreaming());
+
+        executeSql("ALTER DATABASE binlog_format_case_db SET PROPERTIES "
+                + "('binlog.enable'='false', 
'binlog.format'='statement_and_snapshot')");
+        Assertions.assertEquals("STATEMENT_AND_SNAPSHOT",
+                
rowDb.getDbProperties().get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+        Assertions.assertEquals(BinlogFormat.STATEMENT_AND_SNAPSHOT, 
rowDb.getBinlogConfig().getBinlogFormat());
+    }
+
+    @Test
+    public void testRejectUnknownDatabaseFormat() {
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> createDatabaseWithSql("CREATE DATABASE 
binlog_format_case_unknown "
+                        + "PROPERTIES ('binlog.enable'='true', 
'binlog.format'='rows')"));
+        Assertions.assertTrue(exception.getMessage().contains("Invalid binlog 
format value: rows"),
+                exception.getMessage());
+        
Assertions.assertNull(Env.getCurrentInternalCatalog().getDbNullable("binlog_format_case_unknown"));
+    }
+
+    @Test
+    public void testReplayParsesNonCanonicalFormat() {
+        Map<String, String> properties = new HashMap<>();
+        properties.put(PropertyAnalyzer.PROPERTIES_BINLOG_ENABLE, "true");
+        properties.put(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT, "row");
+        Assertions.assertTrue(new 
TableProperty(properties).buildBinlogConfig().getBinlogConfig()
+                .isEnableForStreaming());
+        Assertions.assertTrue(new 
DatabaseProperty(properties).getBinlogConfig().isEnableForStreaming());
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/PropertyAnalyzerTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/common/PropertyAnalyzerTest.java
index 8e8fda29030..4afa2d44f7c 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/PropertyAnalyzerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/PropertyAnalyzerTest.java
@@ -39,6 +39,8 @@ import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 
 import java.time.Instant;
 import java.time.ZoneId;
@@ -65,6 +67,31 @@ public class PropertyAnalyzerTest {
                 () -> PropertyAnalyzer.analyzeBinlogConfig(properties));
     }
 
+    @ParameterizedTest
+    @CsvSource({"ROW, ROW", "row, ROW", "Row, ROW", "STATEMENT_AND_SNAPSHOT, 
STATEMENT_AND_SNAPSHOT",
+            "statement_and_snapshot, STATEMENT_AND_SNAPSHOT"})
+    public void testBinlogFormatIgnoresCase(String format, String 
canonicalName) throws AnalysisException {
+        Map<String, String> properties = new HashMap<>();
+        properties.put(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT, format);
+        Assertions.assertEquals(canonicalName, 
PropertyAnalyzer.analyzeBinlogConfig(properties)
+                .get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+        Assertions.assertTrue(properties.isEmpty());
+
+        properties.put(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT, format);
+        PropertyAnalyzer.normalizeBinlogFormat(properties);
+        Assertions.assertEquals(canonicalName, 
properties.get(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT));
+    }
+
+    @Test
+    public void testBinlogFormatRejectsUnknownValue() {
+        Map<String, String> properties = new HashMap<>();
+        properties.put(PropertyAnalyzer.PROPERTIES_BINLOG_FORMAT, "rows");
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PropertyAnalyzer.analyzeBinlogConfig(properties));
+        Assertions.assertTrue(exception.getMessage().contains("Invalid binlog 
format value: rows"),
+                exception.getMessage());
+    }
+
     @Test
     public void testBfColumns() throws AnalysisException {
         List<Column> columns = Lists.newArrayList();
diff --git 
a/regression-test/data/row_binlog_p0/test_binlog_format_case_insensitive.out 
b/regression-test/data/row_binlog_p0/test_binlog_format_case_insensitive.out
new file mode 100644
index 00000000000..419567edea5
--- /dev/null
+++ b/regression-test/data/row_binlog_p0/test_binlog_format_case_insensitive.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !table_format --
+test_binlog_format_case_row_tbl        ROW
+test_binlog_format_case_statement_tbl  STATEMENT_AND_SNAPSHOT
+
+-- !row_binlog --
+0      1       10
+0      2       20
+
+-- !db_format --
+binlog.enable  true
+binlog.format  ROW
+
+-- !inherited_table_format --
+binlog.enable  true
+binlog.format  ROW
+
+-- !altered_db_format --
+binlog.enable  false
+binlog.format  STATEMENT_AND_SNAPSHOT
+
diff --git 
a/regression-test/suites/row_binlog_p0/test_binlog_format_case_insensitive.groovy
 
b/regression-test/suites/row_binlog_p0/test_binlog_format_case_insensitive.groovy
new file mode 100644
index 00000000000..9748c7e186d
--- /dev/null
+++ 
b/regression-test/suites/row_binlog_p0/test_binlog_format_case_insensitive.groovy
@@ -0,0 +1,180 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// "binlog.format" is matched case-insensitively and always stored by its 
canonical (upper case) name,
+// at both the table and the database level.
+suite("test_binlog_format_case_insensitive") {
+    if (isCloudMode()) {
+        logger.info("skip test_binlog_format_case_insensitive in cloud mode")
+        return
+    }
+
+    // enable_feature_binlog is an EXPERIMENTAL config, so SHOW FRONTEND 
CONFIG reports it as
+    // experimental_enable_feature_binlog. checkEnableFeatureBinlog() accounts 
for that prefix.
+    if (!getSyncer().checkEnableFeatureBinlog()) {
+        logger.info("fe enable_feature_binlog is false, skip case 
test_binlog_format_case_insensitive")
+        return
+    }
+
+    sql "DROP TABLE IF EXISTS test_binlog_format_case_row_tbl FORCE"
+    sql "DROP TABLE IF EXISTS test_binlog_format_case_statement_tbl FORCE"
+    sql "DROP TABLE IF EXISTS test_binlog_format_case_invalid_tbl FORCE"
+    sql "DROP DATABASE IF EXISTS test_binlog_format_case_db FORCE"
+    sql "DROP DATABASE IF EXISTS test_binlog_format_case_invalid_db FORCE"
+
+    sql """
+        CREATE TABLE test_binlog_format_case_row_tbl (
+            k1 INT NOT NULL,
+            v1 INT
+        )
+        DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "binlog.enable" = "true",
+            "binlog.format" = "row"
+        )
+    """
+    sql """
+        CREATE TABLE test_binlog_format_case_statement_tbl (
+            k1 INT NOT NULL,
+            v1 INT
+        )
+        DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "binlog.enable" = "true",
+            "binlog.format" = "Statement_And_Snapshot"
+        )
+    """
+    order_qt_table_format """
+        SELECT TABLE_NAME, PROPERTY_VALUE
+        FROM information_schema.table_properties
+        WHERE TABLE_CATALOG = 'internal'
+            AND TABLE_SCHEMA = '${context.dbName}'
+            AND TABLE_NAME IN ('test_binlog_format_case_row_tbl', 
'test_binlog_format_case_statement_tbl')
+            AND PROPERTY_NAME = 'binlog.format'
+    """
+
+    // The lower case value created a real ROW binlog: the write below is 
readable from it.
+    sql "INSERT INTO test_binlog_format_case_row_tbl VALUES (1, 10), (2, 20)"
+    order_qt_row_binlog """
+        SELECT __DORIS_BINLOG_OP__ AS op, k1, v1
+        FROM binlog("table" = "test_binlog_format_case_row_tbl")
+    """
+
+    // Repeating the current format in another case is a no-op.
+    sql """ALTER TABLE test_binlog_format_case_row_tbl SET ("binlog.format" = 
"Row")"""
+    sql """ALTER TABLE test_binlog_format_case_statement_tbl SET 
("binlog.format" = "statement_and_snapshot")"""
+
+    // The format stays immutable, whatever the case of the new value.
+    test {
+        sql """ALTER TABLE test_binlog_format_case_row_tbl SET 
("binlog.format" = "statement_and_snapshot")"""
+        exception "not support change binlog format from ROW to 
STATEMENT_AND_SNAPSHOT"
+    }
+    test {
+        sql """ALTER TABLE test_binlog_format_case_statement_tbl SET 
("binlog.format" = "row")"""
+        exception "not support change binlog format from 
STATEMENT_AND_SNAPSHOT to ROW"
+    }
+
+    // Values that are not a format in any case are still rejected.
+    test {
+        sql """
+            CREATE TABLE test_binlog_format_case_invalid_tbl (
+                k1 INT NOT NULL,
+                v1 INT
+            )
+            DUPLICATE KEY(k1)
+            DISTRIBUTED BY HASH(k1) BUCKETS 1
+            PROPERTIES (
+                "replication_num" = "1",
+                "binlog.enable" = "true",
+                "binlog.format" = "rows"
+            )
+        """
+        exception "Invalid binlog format value: rows"
+    }
+    test {
+        sql """ALTER TABLE test_binlog_format_case_row_tbl SET 
("binlog.format" = "rows")"""
+        exception "Invalid binlog format value: rows"
+    }
+
+    // Database properties are stored as given, so the value is rewritten to 
its canonical name first.
+    sql """
+        CREATE DATABASE test_binlog_format_case_db PROPERTIES (
+            "binlog.enable" = "true",
+            "binlog.format" = "row"
+        )
+    """
+    order_qt_db_format """
+        SELECT PROPERTY_NAME, PROPERTY_VALUE
+        FROM information_schema.database_properties
+        WHERE CATALOG_NAME = 'internal'
+            AND SCHEMA_NAME = 'test_binlog_format_case_db'
+            AND PROPERTY_NAME IN ('binlog.enable', 'binlog.format')
+    """
+
+    // A table created in the database inherits the ROW format.
+    sql """
+        CREATE TABLE 
test_binlog_format_case_db.test_binlog_format_case_inherited_tbl (
+            k1 INT NOT NULL,
+            v1 INT
+        )
+        DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1"
+        )
+    """
+    order_qt_inherited_table_format """
+        SELECT PROPERTY_NAME, PROPERTY_VALUE
+        FROM information_schema.table_properties
+        WHERE TABLE_CATALOG = 'internal'
+            AND TABLE_SCHEMA = 'test_binlog_format_case_db'
+            AND TABLE_NAME = 'test_binlog_format_case_inherited_tbl'
+            AND PROPERTY_NAME IN ('binlog.enable', 'binlog.format')
+    """
+
+    sql """
+        ALTER DATABASE test_binlog_format_case_db SET PROPERTIES (
+            "binlog.enable" = "false",
+            "binlog.format" = "statement_and_snapshot"
+        )
+    """
+    order_qt_altered_db_format """
+        SELECT PROPERTY_NAME, PROPERTY_VALUE
+        FROM information_schema.database_properties
+        WHERE CATALOG_NAME = 'internal'
+            AND SCHEMA_NAME = 'test_binlog_format_case_db'
+            AND PROPERTY_NAME IN ('binlog.enable', 'binlog.format')
+    """
+
+    test {
+        sql """
+            CREATE DATABASE test_binlog_format_case_invalid_db PROPERTIES (
+                "binlog.enable" = "true",
+                "binlog.format" = "rows"
+            )
+        """
+        exception "Invalid binlog format value: rows"
+    }
+    test {
+        sql """ALTER DATABASE test_binlog_format_case_db SET PROPERTIES 
("binlog.format" = "rows")"""
+        exception "Invalid binlog format value: rows"
+    }
+}


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

Reply via email to