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]