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 60854dbfb52 [feat](stream) Support ALTER STREAM ... SET/MODIFY COMMENT 
(#67471)
60854dbfb52 is described below

commit 60854dbfb52cf1b165bd9111d93b91fd3644f909
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Sep 4 11:41:24 2026 +0800

    [feat](stream) Support ALTER STREAM ... SET/MODIFY COMMENT (#67471)
    
    ### What problem does this PR solve?
    
    Issue Number: close #65388, related #65418
    
    Related PR: #65810
    
    Problem Summary:
    
    **1. `ALTER STREAM ... SET COMMENT` was not supported (#65388)**
    
    A table stream can be created with a comment and the comment is fully
    wired up everywhere except for changing it:
    
    | step | before this PR |
    | --- | --- |
    | `CREATE STREAM s ON TABLE t COMMENT 'x'` | supported
    (`InternalCatalog#createTableStream`) |
    | persisted in the image | supported (`Table#comment`) |
    | `SHOW CREATE STREAM s` | prints the comment |
    | `information_schema.table_streams.STREAM_COMMENT` | exposes the
    comment |
    | changing the comment | **not possible** |
    
    There was no `ALTER STREAM` rule in `DorisParser.g4` at all — `STREAM`
    only appeared in `CREATE STREAM`, `DROP STREAM`, `SHOW STREAMS` and
    `SHOW CREATE STREAM` — so the statement failed at parser stage:
    
    ```
    errCode = 2, detailMessage = no viable alternative at input 'ALTER 
STREAM'(line 1, pos 6)
    ```
    
    `ALTER TABLE` is not an alternative either: `Alter#processAlterTable`
    rejects the `STREAM` table type with `Do not support alter STREAM
    table[...]`.
    
    This PR adds:
    
    ```sql
    ALTER STREAM <name> SET COMMENT 'new comment';
    ALTER STREAM <name> MODIFY COMMENT 'new comment';   -- same thing
    ```
    
    `MODIFY` is accepted alongside `SET` so the syntax stays consistent with
    `ALTER TABLE ... MODIFY COMMENT`, which is the existing Doris spelling
    for the same operation on a table.
    
    Implementation notes:
    
    - The comment of a stream lives in the `Table` metadata only, so
    `Alter#processAlterStreamComment` reuses
    `ModifyCommentOperationLog.forTable(...)` and the existing replay path
    `Alter#replayModifyComment`, which already resolves a generic `Table`.
    **No new edit log operation and no meta version bump.**
    - Cloud Meta Service only stores stream offsets and ids
    (`CloudInternalCatalog#beforeCreateTableStream` /
    `#afterCreateTableStream`), so no extra RPC is needed and the behaviour
    is the same in cloud mode.
    - `AlterStreamCommand` extends `AlterCommand`, which already provides
    `ForwardWithSync` and `StmtType.ALTER`. It carries an `AlterType` enum
    so that other `ALTER STREAM` clauses can be added later without
    reshaping the command.
    - Privilege required is `ALTER` on the stream, matching `ALTER TABLE`.
    Altering a non-stream table through `ALTER STREAM` reports
    `ERR_WRONG_OBJECT`, the same way `SHOW CREATE STREAM` does.
    - `Config.enable_table_stream` gates the operation, consistent with
    `CREATE STREAM` and `DROP STREAM`.
    - The comment literal is decoded with
    `SqlLiteralUtils.parseStringLiteral`, so a doubled quote
    collapses to one quote and backslash escapes follow the session sql
    mode, matching the lexer
    (`NereidsParser` drives the lexer with
    `SqlModeHelper.hasNoBackSlashEscapes()`).
    `CREATE STREAM ... COMMENT` was decoding the same literal differently --
    it unescaped
    backslashes but never collapsed doubled quotes and ignored
    `NO_BACKSLASH_ESCAPES` -- so it was
    moved onto the same decoder, otherwise the comment stored by CREATE and
    by ALTER would differ
    for the same text. Not fixed here: `Env#addTableComment` quotes the
    value with single quotes
    while escaping only double quotes, so a comment holding a `'` makes
    `SHOW CREATE` emit
    non-parsable DDL. That is pre-existing, shared by all 19 call sites of
    every table type, and
      will be filed separately.
    
    **2. Regression coverage for immutable binlog properties (#65383)**
    
    `ALTER TABLE ... SET ("binlog.format" = ...)` on a ROW binlog table used
    to fail with a misleading light-schema-change error, because
    `AlterOperations#checkBinlogConfigChange` did not list `binlog.format` /
    `binlog.need_historical_value` and the statement was dispatched to the
    generic schema change path. That was fixed as a side effect of #65810
    (`f745ddf9e22`), but no test locked the behaviour in. This PR adds
    `test_binlog_property_alter_exception.groovy` covering:
    
    | statement (on a `binlog.format = ROW` MOW table) | expected |
    | --- | --- |
    | `SET ("binlog.format" = "STATEMENT_AND_SNAPSHOT")` | `not support
    change binlog format from ROW to STATEMENT_AND_SNAPSHOT` |
    | `SET ("binlog.need_historical_value" = "false")` | `not support change
    binlog.need_historical_value from true to false` |
    | `SET ("binlog.enable" = "false")` | `can't disable binlog when format
    is [Row]` |
    | `SET ("binlog.format" = "ROW")` (same value) | accepted, no-op |
    | `SET ("binlog.ttl_seconds" = "7200")` | accepted |
    | `SET ("binlog.format" = "ROW")` on a table without binlog | `not
    support change binlog format from STATEMENT_AND_SNAPSHOT to ROW` |
---
 .../main/java/org/apache/doris/alter/Alter.java    |  21 ++++
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  12 ++-
 .../apache/doris/nereids/trees/plans/PlanType.java |   1 +
 .../trees/plans/commands/AlterStreamCommand.java   | 112 +++++++++++++++++++
 .../trees/plans/visitor/CommandVisitor.java        |   5 +
 .../doris/catalog/AlterTableStreamCommentTest.java | 120 +++++++++++++++++++++
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |   2 +
 .../test_table_stream_alter_comment.out            |  19 ++++
 .../test_binlog_property_alter_exception.groovy    |  96 +++++++++++++++++
 .../test_table_stream_alter_comment.groovy         | 105 ++++++++++++++++++
 10 files changed, 491 insertions(+), 2 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java 
b/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
index dbe86016887..2a246e64deb 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
@@ -476,6 +476,27 @@ public class Alter {
         }
     }
 
+    /**
+     * Modify the comment of a table stream, e.g. ALTER STREAM s1 SET COMMENT 
'new comment'.
+     * The comment of a stream is kept in the Table metadata only, so it 
shares the same
+     * edit log entry and the same replay path with ALTER TABLE ... MODIFY 
COMMENT.
+     */
+    public void processAlterStreamComment(long dbId, BaseTableStream stream, 
String comment) throws DdlException {
+        if (!Config.enable_table_stream) {
+            throw new DdlException("Table Stream is experimental."
+                    + " Please set enable_table_stream=true to enable it.");
+        }
+        stream.writeLockOrDdlException();
+        try {
+            stream.setComment(comment);
+            // log
+            ModifyCommentOperationLog op = 
ModifyCommentOperationLog.forTable(dbId, stream.getId(), comment);
+            Env.getCurrentEnv().getEditLog().logModifyComment(op);
+        } finally {
+            stream.writeUnlock();
+        }
+    }
+
     private void processModifyColumnComment(Database db, OlapTable tbl, 
List<AlterOp> alterOps)
             throws DdlException {
         tbl.writeLockOrDdlException();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 2760827b5ae..a268d7a4bd0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -669,6 +669,7 @@ import 
org.apache.doris.nereids.trees.plans.commands.AlterRoutineLoadCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterSqlBlockRuleCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterStoragePolicyCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterStorageVaultCommand;
+import org.apache.doris.nereids.trees.plans.commands.AlterStreamCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterSystemCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.AlterSystemRenameComputeGroupCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterTableCommand;
@@ -4071,8 +4072,8 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         if (ctx.REPLACE() != null && ctx.EXISTS() != null) {
             throw new AnalysisException("[OR REPLACE] and [IF NOT EXISTS] 
cannot used at the same time");
         }
-        String comment = ctx.STRING_LITERAL() == null ? "" : 
LogicalPlanBuilderAssistant.escapeBackSlash(
-                ctx.STRING_LITERAL().getText().substring(1, 
ctx.STRING_LITERAL().getText().length() - 1));
+        String comment = ctx.STRING_LITERAL() == null ? ""
+                : 
SqlLiteralUtils.parseStringLiteral(ctx.STRING_LITERAL().getText());
         Map<String, String> properties = ctx.properties != null
                 // NOTICE: we should not generate immutable map here, because 
it will be modified when analyzing.
                 ? Maps.newHashMap(visitPropertyClause(ctx.properties))
@@ -7339,6 +7340,13 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         return new AlterCatalogCommentCommand(catalogName, comment);
     }
 
+    @Override
+    public LogicalPlan 
visitAlterStreamComment(DorisParser.AlterStreamCommentContext ctx) {
+        TableNameInfo streamName = new 
TableNameInfo(visitMultipartIdentifier(ctx.name));
+        String comment = 
SqlLiteralUtils.parseStringLiteral(ctx.comment.getText());
+        return new AlterStreamCommand(streamName, 
AlterStreamCommand.AlterType.SET_COMMENT, comment);
+    }
+
     @Override
     public LogicalPlan visitAlterDatabaseRename(AlterDatabaseRenameContext 
ctx) {
         String dbName = Optional.ofNullable(ctx.name)
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index 05c19fd82e2..5c0fc387261 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -487,6 +487,7 @@ public enum PlanType {
     DROP_INDEX_NORMALIZER_COMMAND,
     SHOW_INDEX_NORMALIZER_COMMAND,
     CREATE_STREAM_COMMAND,
+    ALTER_STREAM_COMMAND,
     DROP_STREAM_COMMAND,
     SHOW_CREATE_STREAM_COMMAND,
     SHOW_STREAMS,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterStreamCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterStreamCommand.java
new file mode 100644
index 00000000000..1930b211bbf
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterStreamCommand.java
@@ -0,0 +1,112 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.catalog.stream.BaseTableStream;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.InternalDatabaseUtil;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.base.Strings;
+
+/**
+ * AlterStreamCommand, e.g. ALTER STREAM s1 SET COMMENT 'new comment'
+ */
+public class AlterStreamCommand extends AlterCommand {
+    /**
+     * The kind of alteration applied to the stream.
+     */
+    public enum AlterType {
+        SET_COMMENT
+    }
+
+    private final TableNameInfo streamName;
+    private final AlterType alterType;
+    private final String comment;
+
+    public AlterStreamCommand(TableNameInfo streamName, AlterType alterType, 
String comment) {
+        super(PlanType.ALTER_STREAM_COMMAND);
+        this.streamName = streamName;
+        this.alterType = alterType;
+        this.comment = comment;
+    }
+
+    @Override
+    public void doRun(ConnectContext ctx, StmtExecutor executor) throws 
Exception {
+        validate(ctx);
+
+        DatabaseIf db = Env.getCurrentEnv().getCatalogMgr()
+                .getCatalogOrDdlException(streamName.getCtl())
+                .getDbOrDdlException(streamName.getDb());
+        TableIf table = db.getTableOrDdlException(streamName.getTbl());
+        if (!(table instanceof BaseTableStream)) {
+            ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_OBJECT, 
streamName.getDb(), streamName.getTbl(),
+                    "STREAM", "Use 'ALTER TABLE " + streamName.getTbl() + "'");
+        }
+
+        switch (alterType) {
+            case SET_COMMENT:
+                Env.getCurrentEnv().getAlterInstance()
+                        .processAlterStreamComment(db.getId(), 
(BaseTableStream) table, comment);
+                break;
+            default:
+                throw new UserException("Unsupported alter stream operation: " 
+ alterType);
+        }
+    }
+
+    private void validate(ConnectContext ctx) throws UserException {
+        if (Strings.isNullOrEmpty(streamName.getDb())) {
+            streamName.setDb(ctx.getDatabase());
+        }
+        streamName.analyze(ctx.getNameSpaceContext());
+        InternalDatabaseUtil.checkDatabase(streamName.getDb(), ctx);
+        if (!Env.getCurrentEnv().getAccessManager()
+                .checkTblPriv(ctx, streamName.getCtl(), streamName.getDb(), 
streamName.getTbl(),
+                        PrivPredicate.ALTER)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, 
"ALTER STREAM",
+                    ctx.getQualifiedUser(), ctx.getRemoteIP(), 
streamName.getDb() + ": " + streamName.getTbl());
+        }
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitAlterStreamCommand(this, context);
+    }
+
+    public TableNameInfo getStreamName() {
+        return streamName;
+    }
+
+    public AlterType getAlterType() {
+        return alterType;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
index 10f637bf2eb..0e5ac5ffd4f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
@@ -53,6 +53,7 @@ import 
org.apache.doris.nereids.trees.plans.commands.AlterRoleCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterRoutineLoadCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterSqlBlockRuleCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterStoragePolicyCommand;
+import org.apache.doris.nereids.trees.plans.commands.AlterStreamCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterTableCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterTableStatsCommand;
 import org.apache.doris.nereids.trees.plans.commands.AlterUserCommand;
@@ -989,6 +990,10 @@ public interface CommandVisitor<R, C> {
         return visitCommand(command, context);
     }
 
+    default R visitAlterStreamCommand(AlterStreamCommand command, C context) {
+        return visitCommand(command, context);
+    }
+
     default R visitDropRoleCommand(DropRoleCommand dropRoleCommand, C context) 
{
         return visitCommand(dropRoleCommand, context);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/AlterTableStreamCommentTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/AlterTableStreamCommentTest.java
new file mode 100644
index 00000000000..7ff3c828948
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/AlterTableStreamCommentTest.java
@@ -0,0 +1,120 @@
+// 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.stream.BaseTableStream;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.ExceptionChecker;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.qe.SqlModeHelper;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class AlterTableStreamCommentTest extends TestWithFeService {
+
+    @Override
+    protected int backendNum() {
+        return 3;
+    }
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        FeConstants.runningUnitTest = true;
+        Config.allow_replica_on_same_host = true;
+        Config.enable_table_stream = true;
+    }
+
+    @Test
+    public void testAlterStreamComment() throws Exception {
+        createDatabase("test_alter_stream_comment");
+        createTable("create table test_alter_stream_comment.base_tbl (k1 int, 
v1 int)\n"
+                + "duplicate key(k1)\n"
+                + "distributed by hash(k1) buckets 1\n"
+                + "properties('replication_num' = '1', 'binlog.enable' = 
'true', 'binlog.format' = 'ROW');");
+        createTable("create stream test_alter_stream_comment.s1 on table 
test_alter_stream_comment.base_tbl\n"
+                + "comment 'initial comment'\n"
+                + "properties('type' = 'append_only');");
+
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrDdlException("test_alter_stream_comment");
+        BaseTableStream stream = (BaseTableStream) 
db.getTableOrDdlException("s1");
+        Assertions.assertEquals("initial comment", stream.getComment());
+
+        executeSql("alter stream test_alter_stream_comment.s1 set comment 
'updated comment'");
+        Assertions.assertEquals("updated comment", stream.getComment());
+
+        // MODIFY COMMENT is accepted as well, to stay consistent with ALTER 
TABLE
+        executeSql("alter stream test_alter_stream_comment.s1 modify comment 
'modified comment'");
+        Assertions.assertEquals("modified comment", stream.getComment());
+
+        // an empty comment clears the comment
+        executeSql("alter stream test_alter_stream_comment.s1 set comment ''");
+        Assertions.assertEquals("", stream.getComment());
+
+        // altering a normal table through ALTER STREAM is rejected
+        ExceptionChecker.expectThrowsWithMsg(IllegalStateException.class, "is 
not STREAM",
+                () -> executeSql("alter stream 
test_alter_stream_comment.base_tbl set comment 'not a stream'"));
+
+        // altering an unknown stream is rejected
+        ExceptionChecker.expectThrowsWithMsg(IllegalStateException.class, 
"Unknown table",
+                () -> executeSql("alter stream 
test_alter_stream_comment.not_exist set comment 'no such stream'"));
+
+        dropDatabase("test_alter_stream_comment");
+    }
+
+    @Test
+    public void testAlterStreamCommentStringLiteral() throws Exception {
+        createDatabase("test_alter_stream_comment_literal");
+        createTable("create table test_alter_stream_comment_literal.base_tbl 
(k1 int, v1 int)\n"
+                + "duplicate key(k1)\n"
+                + "distributed by hash(k1) buckets 1\n"
+                + "properties('replication_num' = '1', 'binlog.enable' = 
'true', 'binlog.format' = 'ROW');");
+        // CREATE STREAM and ALTER STREAM must decode the string literal in 
the same way:
+        // a doubled quote is one quote and, unless NO_BACKSLASH_ESCAPES is 
set, backslash escapes are decoded
+        createTable("create stream test_alter_stream_comment_literal.s1"
+                + " on table test_alter_stream_comment_literal.base_tbl\n"
+                + "comment 'a''b\\nc'\n"
+                + "properties('type' = 'append_only');");
+
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrDdlException("test_alter_stream_comment_literal");
+        BaseTableStream stream = (BaseTableStream) 
db.getTableOrDdlException("s1");
+        Assertions.assertEquals("a'b\nc", stream.getComment());
+
+        executeSql("alter stream test_alter_stream_comment_literal.s1 set 
comment 'x''y\\tz'");
+        Assertions.assertEquals("x'y\tz", stream.getComment());
+
+        // a double quoted literal doubles the double quote instead
+        executeSql("alter stream test_alter_stream_comment_literal.s1 set 
comment \"p\"\"q\"");
+        Assertions.assertEquals("p\"q", stream.getComment());
+
+        // under NO_BACKSLASH_ESCAPES a backslash is an ordinary character. 
Both the lexer and
+        // SqlLiteralUtils read the sql mode from the thread local 
ConnectContext, so bind it first.
+        connectContext.setThreadLocalInfo();
+        long originalSqlMode = 
connectContext.getSessionVariable().getSqlMode();
+        try {
+            
connectContext.getSessionVariable().setSqlMode(SqlModeHelper.MODE_NO_BACKSLASH_ESCAPES);
+            executeSql("alter stream test_alter_stream_comment_literal.s1 set 
comment 'a\\nb'");
+            Assertions.assertEquals("a\\nb", stream.getComment());
+        } finally {
+            connectContext.getSessionVariable().setSqlMode(originalSqlMode);
+        }
+
+        dropDatabase("test_alter_stream_comment_literal");
+    }
+}
diff --git 
a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 
b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index bbe5064a2fd..fdc62599f3f 100644
--- a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -498,6 +498,8 @@ alterStatement
         properties=propertyClause?                                             
             #alterWorkloadPolicy
     | ALTER SQL_BLOCK_RULE name=identifier properties=propertyClause?          
             #alterSqlBlockRule
     | ALTER CATALOG name=identifier MODIFY COMMENT comment=STRING_LITERAL      
             #alterCatalogComment
+    | ALTER STREAM name=multipartIdentifier
+        (SET | MODIFY) COMMENT comment=STRING_LITERAL                          
             #alterStreamComment
     | ALTER DATABASE name=identifier RENAME newName=identifier                 
             #alterDatabaseRename
     | ALTER STORAGE POLICY name=identifierOrText
         properties=propertyClause                                              
             #alterStoragePolicy
diff --git 
a/regression-test/data/table_stream_p0/test_table_stream_alter_comment.out 
b/regression-test/data/table_stream_p0/test_table_stream_alter_comment.out
new file mode 100644
index 00000000000..317e1ad8dcf
--- /dev/null
+++ b/regression-test/data/table_stream_p0/test_table_stream_alter_comment.out
@@ -0,0 +1,19 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !comment_after_create --
+initial comment
+
+-- !comment_after_set --
+updated comment
+
+-- !comment_after_modify --
+modified comment
+
+-- !comment_doubled_quote --
+a'b    3
+
+-- !comment_after_clear --
+
+
+-- !comment_final --
+final comment
+
diff --git 
a/regression-test/suites/table_stream_p0/test_binlog_property_alter_exception.groovy
 
b/regression-test/suites/table_stream_p0/test_binlog_property_alter_exception.groovy
new file mode 100644
index 00000000000..3f7934a4e53
--- /dev/null
+++ 
b/regression-test/suites/table_stream_p0/test_binlog_property_alter_exception.groovy
@@ -0,0 +1,96 @@
+// 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.
+
+// Immutable binlog properties must be rejected by the binlog config path
+// (AlterOperations.checkBinlogConfigChange -> 
SchemaChangeHandler.updateBinlogConfig),
+// not by the light-schema-change guard of the generic schema change path.
+suite("test_binlog_property_alter_exception") {
+    if (isCloudMode()) {
+        logger.info("skip test_binlog_property_alter_exception 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_property_alter_exception")
+        return
+    }
+
+    sql "DROP TABLE IF EXISTS test_binlog_property_alter_row_tbl FORCE"
+    sql "DROP TABLE IF EXISTS test_binlog_property_alter_plain_tbl FORCE"
+
+    sql """
+        CREATE TABLE test_binlog_property_alter_row_tbl (
+            k1 INT NOT NULL,
+            v1 INT
+        )
+        UNIQUE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "enable_unique_key_merge_on_write" = "true",
+            "binlog.enable" = "true",
+            "binlog.format" = "ROW",
+            "binlog.need_historical_value" = "true"
+        )
+    """
+
+    // binlog.format can not be changed once the table is created
+    test {
+        sql """ALTER TABLE test_binlog_property_alter_row_tbl SET 
("binlog.format" = "STATEMENT_AND_SNAPSHOT")"""
+        exception "not support change binlog format from ROW to 
STATEMENT_AND_SNAPSHOT"
+    }
+
+    // binlog.need_historical_value can not be changed either
+    test {
+        sql """ALTER TABLE test_binlog_property_alter_row_tbl SET 
("binlog.need_historical_value" = "false")"""
+        exception "not support change binlog.need_historical_value from true 
to false"
+    }
+
+    // binlog can not be disabled while the format is ROW
+    test {
+        sql """ALTER TABLE test_binlog_property_alter_row_tbl SET 
("binlog.enable" = "false")"""
+        exception "can't disable binlog when format is [Row]"
+    }
+
+    // setting the same value is a no-op and must not be rejected
+    sql """ALTER TABLE test_binlog_property_alter_row_tbl SET ("binlog.format" 
= "ROW")"""
+
+    // mutable binlog properties are still accepted on a ROW binlog table
+    sql """ALTER TABLE test_binlog_property_alter_row_tbl SET 
("binlog.ttl_seconds" = "7200")"""
+    def rowTableDdl = sql("SHOW CREATE TABLE 
test_binlog_property_alter_row_tbl")[0][1].toString()
+    assertTrue(rowTableDdl.contains('"binlog.format" = "ROW"'), rowTableDdl)
+    assertTrue(rowTableDdl.contains('"binlog.ttl_seconds" = "7200"'), 
rowTableDdl)
+
+    // the same check applies to a table without binlog: turning on ROW format 
afterwards is rejected
+    sql """
+        CREATE TABLE test_binlog_property_alter_plain_tbl (
+            k1 INT NOT NULL,
+            v1 INT
+        )
+        DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1"
+        )
+    """
+    test {
+        sql """ALTER TABLE test_binlog_property_alter_plain_tbl SET 
("binlog.format" = "ROW")"""
+        exception "not support change binlog format from 
STATEMENT_AND_SNAPSHOT to ROW"
+    }
+}
diff --git 
a/regression-test/suites/table_stream_p0/test_table_stream_alter_comment.groovy 
b/regression-test/suites/table_stream_p0/test_table_stream_alter_comment.groovy
new file mode 100644
index 00000000000..ebb98418b15
--- /dev/null
+++ 
b/regression-test/suites/table_stream_p0/test_table_stream_alter_comment.groovy
@@ -0,0 +1,105 @@
+// 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.
+
+// ALTER STREAM ... SET/MODIFY COMMENT updates the comment of a table stream.
+suite("test_table_stream_alter_comment") {
+    if (isCloudMode()) {
+        logger.info("skip test_table_stream_alter_comment in cloud mode")
+        return
+    }
+
+    sql "DROP STREAM IF EXISTS test_stream_alter_comment_stream"
+    sql "DROP TABLE IF EXISTS test_stream_alter_comment_base FORCE"
+
+    sql """
+        CREATE TABLE test_stream_alter_comment_base (
+            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 STREAM test_stream_alter_comment_stream ON TABLE 
test_stream_alter_comment_base
+        COMMENT 'initial comment'
+        PROPERTIES ("type" = "append_only")
+    """
+
+    order_qt_comment_after_create """
+        SELECT STREAM_COMMENT FROM information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+
+    // SET COMMENT
+    sql """ALTER STREAM test_stream_alter_comment_stream SET COMMENT 'updated 
comment'"""
+    order_qt_comment_after_set """
+        SELECT STREAM_COMMENT FROM information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+
+    // the new comment is part of SHOW CREATE STREAM as well. The whole DDL is 
not used as the
+    // expected output on purpose: its PROPERTIES section changes as the 
stream feature evolves.
+    def createStmt = sql("SHOW CREATE STREAM 
test_stream_alter_comment_stream")[0][1].toString()
+    assertTrue(createStmt.contains("COMMENT 'updated comment'"), createStmt)
+
+    // MODIFY COMMENT is accepted as well, to stay consistent with ALTER TABLE
+    sql """ALTER STREAM test_stream_alter_comment_stream MODIFY COMMENT 
'modified comment'"""
+    order_qt_comment_after_modify """
+        SELECT STREAM_COMMENT FROM information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+
+    // the literal is decoded the same way CREATE STREAM decodes it: a doubled 
quote is one quote
+    sql """ALTER STREAM test_stream_alter_comment_stream SET COMMENT 'a''b'"""
+    order_qt_comment_doubled_quote """
+        SELECT STREAM_COMMENT, LENGTH(STREAM_COMMENT) FROM 
information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+
+    // an empty comment clears the comment
+    sql """ALTER STREAM test_stream_alter_comment_stream SET COMMENT ''"""
+    order_qt_comment_after_clear """
+        SELECT STREAM_COMMENT FROM information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+    createStmt = sql("SHOW CREATE STREAM 
test_stream_alter_comment_stream")[0][1].toString()
+    assertFalse(createStmt.contains("COMMENT '"), createStmt)
+
+    sql """ALTER STREAM test_stream_alter_comment_stream SET COMMENT 'final 
comment'"""
+    order_qt_comment_final """
+        SELECT STREAM_COMMENT FROM information_schema.table_streams
+        WHERE DB_NAME = DATABASE() AND STREAM_NAME = 
'test_stream_alter_comment_stream'
+    """
+
+    // altering a normal table through ALTER STREAM is rejected
+    test {
+        sql """ALTER STREAM test_stream_alter_comment_base SET COMMENT 'not a 
stream'"""
+        exception "is not STREAM"
+    }
+
+    // altering an unknown stream is rejected
+    test {
+        sql """ALTER STREAM test_stream_alter_comment_not_exist SET COMMENT 
'no such stream'"""
+        exception "Unknown table"
+    }
+}


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

Reply via email to