This is an automated email from the ASF dual-hosted git repository.
luwei16 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 f075c8aabba [fix](binlog) Parse table stream properties before base
validation (#66683)
f075c8aabba is described below
commit f075c8aabba9af0a66f8d9f6bede48a482be4dcc
Author: Luwei <[email protected]>
AuthorDate: Mon Aug 17 11:16:00 2026 +0800
[fix](binlog) Parse table stream properties before base validation (#66683)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: CREATE STREAM validated the base table with the stream
object default MIN_DELTA type before parsing the requested stream
properties. This incorrectly rejected valid APPEND_ONLY and DETAIL
streams on UNIQUE or PRIMARY ROW-binlog tables that did not meet
MIN_DELTA historical-value requirements. Parse and apply the stream
properties before performing type-dependent base-table validation, while
preserving the validation for real MIN_DELTA streams.
### Release note
Fix CREATE STREAM so APPEND_ONLY and DETAIL streams are validated using
their requested type instead of the default MIN_DELTA type.
### Check List (For Author)
- Test:
- Unit Test: ./run-fe-ut.sh --run
org.apache.doris.catalog.CreateTableStreamTest,org.apache.doris.cloud.datasource.CloudInternalCatalogTableStreamTest
(passed: 9 tests)
- Regression test: ./run-regression-test.sh --run -d table_stream_p0 -s
test_create_append_only_stream_without_historical_value (passed: 1
suite)
- Behavior changed: Yes. Valid APPEND_ONLY and DETAIL streams are no
longer rejected by MIN_DELTA-only prerequisites.
- Does this need documentation: No
---
.../apache/doris/datasource/InternalCatalog.java | 4 +-
.../doris/catalog/CreateTableStreamTest.java | 46 ++++++++
.../CloudInternalCatalogTableStreamTest.java | 123 +++++++++++++++++++++
...append_only_stream_without_historical_value.out | 5 +
...end_only_stream_without_historical_value.groovy | 100 +++++++++++++++++
5 files changed, 276 insertions(+), 2 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
index 59cdd7d9b9d..65a426da75f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
@@ -4096,13 +4096,13 @@ public class InternalCatalog implements
CatalogIf<Database> {
.withBaseTable(baseTable)
.build();
newStream.setComment(createStreamInfo.getComment());
- // check base table type is supported for stream
-
baseTable.checkAsTableStreamBaseTable(newStream.getStreamScanType());
try {
setTableStreamProperties(newStream, properties);
} catch (AnalysisException e) {
throw new DdlException(e.getMessage(), e);
}
+ // check base table type is supported for stream
+
baseTable.checkAsTableStreamBaseTable(newStream.getStreamScanType());
if (properties != null && !properties.isEmpty()) {
// before here, all properties should be checked
throw new DdlException("Unknown properties: " +
properties);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableStreamTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableStreamTest.java
index e775e3bc2a1..f0d665338b9 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableStreamTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableStreamTest.java
@@ -17,12 +17,14 @@
package org.apache.doris.catalog;
+import org.apache.doris.catalog.stream.BaseTableStream;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ExceptionChecker;
import org.apache.doris.common.FeConstants;
import org.apache.doris.utframe.TestWithFeService;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -73,6 +75,50 @@ public class CreateTableStreamTest extends TestWithFeService
{
dropDatabase("test_stream");
}
+ @Test
+ public void testCreateStreamUsesRequestedTypeForBaseValidation() throws
Exception {
+ createDatabase("test_stream_type_validation");
+ String sql = "create table test_stream_type_validation.base_table\n" +
"(k1 int, k2 int)\n"
+ + "unique key(k1)\n"
+ + "distributed by hash(k1) buckets 1\n"
+ + "properties('replication_num' = '1',
'enable_unique_key_merge_on_write' = 'true', "
+ + "'binlog.enable' = 'true', 'binlog.format' = 'ROW', "
+ + "'binlog.need_historical_value' = 'false'); ";
+ createTable(sql);
+
+ ExceptionChecker.expectThrowsNoException(() ->
+ createTable("create stream
test_stream_type_validation.append_only_stream "
+ + "on table test_stream_type_validation.base_table\n"
+ + "properties('type' = 'append_only',
'show_initial_rows' = 'false'); "));
+ ExceptionChecker.expectThrowsNoException(() ->
+ createTable("create stream
test_stream_type_validation.detail_stream "
+ + "on table test_stream_type_validation.base_table\n"
+ + "properties('type' = 'detail', 'show_initial_rows' =
'false'); "));
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("test_stream_type_validation");
+ BaseTableStream appendOnlyStream = (BaseTableStream)
db.getTableOrDdlException("append_only_stream");
+ BaseTableStream detailStream = (BaseTableStream)
db.getTableOrDdlException("detail_stream");
+ Assertions.assertEquals(BaseTableStream.StreamScanType.APPEND_ONLY,
+ appendOnlyStream.getStreamScanType());
+ Assertions.assertEquals(BaseTableStream.StreamScanType.DETAIL,
detailStream.getStreamScanType());
+
+ String minDeltaError = "MIN_DELTA table stream requires base mow table
to enable "
+ + "binlog.need_historical_value=true";
+ ExceptionChecker.expectThrowsWithMsg(DdlException.class, minDeltaError,
+ () -> createTable("create stream
test_stream_type_validation.default_stream "
+ + "on table test_stream_type_validation.base_table\n"
+ + "properties('show_initial_rows' = 'false'); "));
+ ExceptionChecker.expectThrowsWithMsg(DdlException.class, minDeltaError,
+ () -> createTable("create stream
test_stream_type_validation.min_delta_stream "
+ + "on table test_stream_type_validation.base_table\n"
+ + "properties('type' = 'min_delta',
'show_initial_rows' = 'false'); "));
+ ExceptionChecker.expectThrowsWithMsg(DdlException.class, "not
supported type: invalid_type",
+ () -> createTable("create stream
test_stream_type_validation.invalid_type_stream "
+ + "on table test_stream_type_validation.base_table\n"
+ + "properties('type' = 'invalid_type',
'show_initial_rows' = 'false'); "));
+ dropDatabase("test_stream_type_validation");
+ }
+
@Test
public void testCreateStreamAbnormalOLAP() throws Exception {
createDatabase("test_stream");
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/datasource/CloudInternalCatalogTableStreamTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/datasource/CloudInternalCatalogTableStreamTest.java
index acff2c9687f..fbf440ce271 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/datasource/CloudInternalCatalogTableStreamTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/datasource/CloudInternalCatalogTableStreamTest.java
@@ -17,8 +17,14 @@
package org.apache.doris.cloud.datasource;
+import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Table;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.catalog.stream.BaseTableStream;
import org.apache.doris.catalog.stream.OlapTableStream;
import org.apache.doris.catalog.stream.TableStreamBaseTableInfo;
import org.apache.doris.cloud.proto.Cloud;
@@ -26,6 +32,11 @@ import org.apache.doris.cloud.rpc.MetaServiceProxy;
import org.apache.doris.cloud.rpc.VersionHelper;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
+import org.apache.doris.common.Pair;
+import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.nereids.trees.plans.commands.CreateStreamCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateStreamInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -35,12 +46,114 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
public class CloudInternalCatalogTableStreamTest {
+ @Test
+ public void testCreateUsesParsedTypeBeforeCloudPreparation() throws
Exception {
+ boolean previousEnableTableStream = Config.enable_table_stream;
+ String previousCloudUniqueId = Config.cloud_unique_id;
+ String previousMetaServiceEndpoint = Config.meta_service_endpoint;
+ Config.enable_table_stream = true;
+ Config.cloud_unique_id = "cloud_table_stream_type_validation_ut";
+ Config.meta_service_endpoint = "127.0.0.1:20121";
+ try {
+ CloudInternalCatalog catalog = Mockito.spy(new
CloudInternalCatalog());
+ Database streamDb = Mockito.mock(Database.class);
+ OlapTable baseTable = Mockito.mock(OlapTable.class);
+
Mockito.doReturn(streamDb).when(catalog).getDbNullable("test_stream");
+ Mockito.doReturn(streamDb).when(catalog).getDbNullable(10L);
+ Mockito.when(streamDb.getId()).thenReturn(10L);
+ Mockito.when(streamDb.getFullName()).thenReturn("test_stream");
+ Mockito.when(streamDb.getCatalog()).thenReturn(catalog);
+
Mockito.when(streamDb.getTable("append_only_stream")).thenReturn(Optional.empty());
+
Mockito.when(streamDb.getTable("min_delta_stream")).thenReturn(Optional.empty());
+
Mockito.when(streamDb.getTableOrDdlException("base_table")).thenReturn(baseTable);
+
Mockito.when(streamDb.getTable(20L)).thenReturn(Optional.of(baseTable));
+
Mockito.when(streamDb.createTableWithLock(Mockito.any(Table.class),
Mockito.eq(false),
+ Mockito.eq(false))).thenReturn(Pair.of(true, false));
+
+ Mockito.when(baseTable.getId()).thenReturn(20L);
+ Mockito.when(baseTable.getName()).thenReturn("base_table");
+ Mockito.when(baseTable.getDatabase()).thenReturn(streamDb);
+
Mockito.when(baseTable.getType()).thenReturn(TableIf.TableType.OLAP);
+
Mockito.when(baseTable.getBaseSchema()).thenReturn(List.<Column>of());
+ Mockito.when(baseTable.getPartitionIds()).thenReturn(List.of());
+ Mockito.when(baseTable.getBaseSchemaVersion()).thenReturn(7);
+
+ List<BaseTableStream.StreamScanType> checkedTypes = new
ArrayList<>();
+ Mockito.doAnswer(invocation -> {
+ BaseTableStream.StreamScanType type =
invocation.getArgument(0);
+ checkedTypes.add(type);
+ if (type == BaseTableStream.StreamScanType.MIN_DELTA) {
+ throw new DdlException("MIN_DELTA rejected before Cloud
preparation");
+ }
+ return null;
+ }).when(baseTable).checkAsTableStreamBaseTable(Mockito.any());
+
Mockito.doReturn(List.of()).when(catalog).captureTableStreamInitialOffsets(
+ Mockito.any(OlapTableStream.class),
Mockito.same(baseTable), Mockito.anyList());
+
+ Env env = Mockito.mock(Env.class);
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ Mockito.when(env.getNextId()).thenReturn(40L, 41L);
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+
Mockito.doReturn(catalog).when(catalogMgr).getCatalog(InternalCatalog.INTERNAL_CATALOG_ID);
+
+ MetaServiceProxy proxy = Mockito.mock(MetaServiceProxy.class);
+ Cloud.IndexResponse indexResponse =
Cloud.IndexResponse.newBuilder()
+
.setStatus(Cloud.MetaServiceResponseStatus.newBuilder().setCode(Cloud.MetaServiceCode.OK))
+ .build();
+
Mockito.when(proxy.prepareIndex(Mockito.any())).thenReturn(indexResponse);
+
Mockito.when(proxy.commitIndex(Mockito.any())).thenReturn(indexResponse);
+
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class);
+ MockedStatic<MetaServiceProxy> mockedProxy =
Mockito.mockStatic(MetaServiceProxy.class)) {
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+
mockedProxy.when(MetaServiceProxy::getInstance).thenReturn(proxy);
+
+
catalog.createTableStream(createStreamCommand("append_only_stream",
"append_only"));
+ DdlException minDeltaException =
Assertions.assertThrows(DdlException.class,
+ () ->
catalog.createTableStream(createStreamCommand("min_delta_stream",
"min_delta")));
+ Assertions.assertTrue(minDeltaException.getMessage()
+ .contains("MIN_DELTA rejected before Cloud
preparation"));
+
+ Assertions.assertEquals(List.of(
+ BaseTableStream.StreamScanType.APPEND_ONLY,
+ BaseTableStream.StreamScanType.APPEND_ONLY,
+ BaseTableStream.StreamScanType.MIN_DELTA),
+ checkedTypes);
+ InOrder order = Mockito.inOrder(baseTable, proxy);
+ order.verify(baseTable).checkAsTableStreamBaseTable(
+ BaseTableStream.StreamScanType.APPEND_ONLY);
+ order.verify(proxy).prepareIndex(Mockito.any());
+ order.verify(baseTable).checkAsTableStreamBaseTable(
+ BaseTableStream.StreamScanType.APPEND_ONLY);
+ order.verify(proxy).commitIndex(Mockito.any());
+ order.verify(baseTable).checkAsTableStreamBaseTable(
+ BaseTableStream.StreamScanType.MIN_DELTA);
+ Mockito.verify(catalog).captureTableStreamInitialOffsets(
+ Mockito.any(OlapTableStream.class),
Mockito.same(baseTable), Mockito.anyList());
+ Mockito.verify(proxy).prepareIndex(Mockito.any());
+
+ ArgumentCaptor<Table> streamCaptor =
ArgumentCaptor.forClass(Table.class);
+
Mockito.verify(streamDb).createTableWithLock(streamCaptor.capture(),
Mockito.eq(false),
+ Mockito.eq(false));
+
Assertions.assertEquals(BaseTableStream.StreamScanType.APPEND_ONLY,
+ ((OlapTableStream)
streamCaptor.getValue()).getStreamScanType());
+ }
+ } finally {
+ Config.enable_table_stream = previousEnableTableStream;
+ Config.cloud_unique_id = previousCloudUniqueId;
+ Config.meta_service_endpoint = previousMetaServiceEndpoint;
+ }
+ }
+
@Test
public void testCreateBatchesOffsetsAndCommitsIndexLast() throws Exception
{
int previousBatchSize =
Config.cloud_table_stream_create_partition_batch_size;
@@ -268,6 +381,16 @@ public class CloudInternalCatalogTableStreamTest {
.build();
}
+ private static CreateStreamCommand createStreamCommand(String streamName,
String streamType) {
+ Map<String, String> properties = new HashMap<>();
+ properties.put("type", streamType);
+ properties.put("show_initial_rows", "false");
+ CreateStreamInfo createStreamInfo = new CreateStreamInfo(false, false,
+ new TableNameInfo(null, "test_stream", streamName),
+ new TableNameInfo(null, "test_stream", "base_table"),
properties, "");
+ return new CreateStreamCommand(createStreamInfo);
+ }
+
private static class CaptureCloudInternalCatalog extends
CloudInternalCatalog {
private List<Cloud.TableStreamOffsetPB> capture(OlapTableStream
stream, OlapTable baseTable,
List<Long> basePartitionIds) throws DdlException {
diff --git
a/regression-test/data/table_stream_p0/test_create_append_only_stream_without_historical_value.out
b/regression-test/data/table_stream_p0/test_create_append_only_stream_without_historical_value.out
new file mode 100644
index 00000000000..beb543d248d
--- /dev/null
+++
b/regression-test/data/table_stream_p0/test_create_append_only_stream_without_historical_value.out
@@ -0,0 +1,5 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !stream_types --
+test_append_only_without_historical_value_stream APPEND_ONLY
+test_detail_without_historical_value_stream DETAIL
+
diff --git
a/regression-test/suites/table_stream_p0/test_create_append_only_stream_without_historical_value.groovy
b/regression-test/suites/table_stream_p0/test_create_append_only_stream_without_historical_value.groovy
new file mode 100644
index 00000000000..d25d9d62ebd
--- /dev/null
+++
b/regression-test/suites/table_stream_p0/test_create_append_only_stream_without_historical_value.groovy
@@ -0,0 +1,100 @@
+// 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.
+
+suite("test_create_append_only_stream_without_historical_value",
"nonConcurrent") {
+ sql "DROP STREAM IF EXISTS
test_append_only_without_historical_value_stream"
+ sql "DROP STREAM IF EXISTS test_detail_without_historical_value_stream"
+ sql "DROP STREAM IF EXISTS test_default_without_historical_value_stream"
+ sql "DROP STREAM IF EXISTS test_min_delta_without_historical_value_stream"
+ sql "DROP STREAM IF EXISTS
test_invalid_type_without_historical_value_stream"
+ sql "DROP TABLE IF EXISTS test_stream_without_historical_value_base"
+
+ sql """
+ CREATE TABLE test_stream_without_historical_value_base (
+ id INT NOT NULL,
+ value INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "false"
+ )
+ """
+
+ sql """
+ CREATE STREAM test_append_only_without_historical_value_stream
+ ON TABLE test_stream_without_historical_value_base
+ PROPERTIES (
+ "type" = "append_only",
+ "show_initial_rows" = "false"
+ )
+ """
+ sql """
+ CREATE STREAM test_detail_without_historical_value_stream
+ ON TABLE test_stream_without_historical_value_base
+ PROPERTIES (
+ "type" = "detail",
+ "show_initial_rows" = "false"
+ )
+ """
+
+ order_qt_stream_types """
+ SELECT STREAM_NAME, CONSUME_TYPE
+ FROM information_schema.table_streams
+ WHERE DB_NAME = DATABASE()
+ AND STREAM_NAME IN (
+ 'test_append_only_without_historical_value_stream',
+ 'test_detail_without_historical_value_stream'
+ )
+ ORDER BY STREAM_NAME
+ """
+
+ test {
+ sql """
+ CREATE STREAM test_default_without_historical_value_stream
+ ON TABLE test_stream_without_historical_value_base
+ PROPERTIES ("show_initial_rows" = "false")
+ """
+ exception "MIN_DELTA table stream requires base mow table to enable
binlog.need_historical_value=true"
+ }
+ test {
+ sql """
+ CREATE STREAM test_min_delta_without_historical_value_stream
+ ON TABLE test_stream_without_historical_value_base
+ PROPERTIES (
+ "type" = "min_delta",
+ "show_initial_rows" = "false"
+ )
+ """
+ exception "MIN_DELTA table stream requires base mow table to enable
binlog.need_historical_value=true"
+ }
+ test {
+ sql """
+ CREATE STREAM test_invalid_type_without_historical_value_stream
+ ON TABLE test_stream_without_historical_value_base
+ PROPERTIES (
+ "type" = "invalid_type",
+ "show_initial_rows" = "false"
+ )
+ """
+ exception "not supported type: invalid_type"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]