-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[fix](streaming-job) Support delete sign in CDC stream TVF #65146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JNSimba
wants to merge
5
commits into
apache:master
Choose a base branch
from
JNSimba:fix/cdc-stream-include-delete-sign
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f98234a
[doc](streaming-job) Add CDC stream delete sign design
JNSimba e49d87b
[fix](streaming-job) Support delete sign in CDC stream TVF
JNSimba 173721a
Remove docs files that should not be in the PR
JNSimba 1b4595d
Revert existing TVF cases, add separate delete sign tests
JNSimba 6dd0e27
[test](regression) Add CDC delete sign expected results
JNSimba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
...e-core/src/test/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunctionTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // 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.tablefunction; | ||
|
|
||
| import org.apache.doris.catalog.Column; | ||
| import org.apache.doris.catalog.PrimitiveType; | ||
| import org.apache.doris.common.AnalysisException; | ||
| import org.apache.doris.datasource.jdbc.client.JdbcClient; | ||
| import org.apache.doris.job.cdc.DataSourceConfigKeys; | ||
| import org.apache.doris.job.common.DataSourceType; | ||
| import org.apache.doris.job.util.StreamingJobUtils; | ||
|
|
||
| import org.junit.Assert; | ||
| import org.junit.Test; | ||
| import org.mockito.MockedStatic; | ||
| import org.mockito.Mockito; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public class CdcStreamTableValuedFunctionTest { | ||
|
|
||
| @Test | ||
| public void testDeleteSignIsExcludedByDefault() throws Exception { | ||
| List<Column> columns = getTableColumns(baseProperties()); | ||
|
|
||
| Assert.assertEquals(1, columns.size()); | ||
| Assert.assertEquals("id", columns.get(0).getName()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDeleteSignIsIncludedWhenEnabled() throws Exception { | ||
| Map<String, String> properties = baseProperties(); | ||
| properties.put(CdcStreamTableValuedFunction.INCLUDE_DELETE_SIGN, "true"); | ||
|
|
||
| List<Column> columns = getTableColumns(properties); | ||
|
|
||
| Assert.assertEquals(2, columns.size()); | ||
| Column deleteSign = columns.get(1); | ||
| Assert.assertEquals(Column.DELETE_SIGN, deleteSign.getName()); | ||
| Assert.assertEquals(PrimitiveType.TINYINT, deleteSign.getType().getPrimitiveType()); | ||
| Assert.assertFalse(deleteSign.isAllowNull()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testInvalidIncludeDeleteSignIsRejected() { | ||
| Map<String, String> properties = baseProperties(); | ||
| properties.put(CdcStreamTableValuedFunction.INCLUDE_DELETE_SIGN, "invalid"); | ||
|
|
||
| AnalysisException exception = Assert.assertThrows(AnalysisException.class, | ||
| () -> new CdcStreamTableValuedFunction(properties)); | ||
|
|
||
| Assert.assertTrue(exception.getMessage().contains("include_delete_sign")); | ||
| } | ||
|
|
||
| private List<Column> getTableColumns(Map<String, String> properties) throws Exception { | ||
| JdbcClient jdbcClient = Mockito.mock(JdbcClient.class); | ||
| List<Column> sourceColumns = new ArrayList<>(); | ||
| sourceColumns.add(new Column("id", PrimitiveType.INT)); | ||
| Mockito.when(jdbcClient.isTableExist("test_db", "test_table")).thenReturn(true); | ||
| Mockito.when(jdbcClient.getColumnsFromJdbc("test_db", "test_table")).thenReturn(sourceColumns); | ||
|
|
||
| try (MockedStatic<StreamingJobUtils> utils = Mockito.mockStatic(StreamingJobUtils.class)) { | ||
| utils.when(() -> StreamingJobUtils.getJdbcClient( | ||
| Mockito.eq(DataSourceType.MYSQL), Mockito.anyMap())) | ||
| .thenReturn(jdbcClient); | ||
| utils.when(() -> StreamingJobUtils.getRemoteDbName( | ||
| Mockito.eq(DataSourceType.MYSQL), Mockito.anyMap())) | ||
| .thenReturn("test_db"); | ||
| CdcStreamTableValuedFunction function = new CdcStreamTableValuedFunction(properties); | ||
| return function.getTableColumns(); | ||
| } | ||
| } | ||
|
|
||
| private Map<String, String> baseProperties() { | ||
| Map<String, String> properties = new HashMap<>(); | ||
| properties.put(DataSourceConfigKeys.TYPE, "mysql"); | ||
| properties.put(DataSourceConfigKeys.JDBC_URL, "jdbc:mysql://localhost:3306/test_db"); | ||
| properties.put(DataSourceConfigKeys.DATABASE, "test_db"); | ||
| properties.put(DataSourceConfigKeys.TABLE, "test_table"); | ||
| properties.put(DataSourceConfigKeys.OFFSET, "initial"); | ||
| return properties; | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
...est/data/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_delete_sign.out
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| -- This file is automatically generated. You should know what you did if you want to edit this | ||
| -- !snapshot_data -- | ||
| A1 1 | ||
| B1 2 | ||
|
|
||
| -- !incremental_data -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 3 0 | ||
| D1 4 0 | ||
|
|
||
| -- !after_update -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 30 0 | ||
| D1 4 0 | ||
|
|
||
| -- !after_delete -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 30 0 |
21 changes: 21 additions & 0 deletions
21
.../data/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_postgres_delete_sign.out
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| -- This file is automatically generated. You should know what you did if you want to edit this | ||
| -- !snapshot_data -- | ||
| A1 1 | ||
| B1 2 | ||
|
|
||
| -- !incremental_data -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 3 0 | ||
| D1 4 0 | ||
|
|
||
| -- !after_update -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 30 0 | ||
| D1 4 0 | ||
|
|
||
| -- !after_delete -- | ||
| A1 1 0 | ||
| B1 2 0 | ||
| C1 30 0 |
157 changes: 157 additions & 0 deletions
157
...uites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql_delete_sign.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // 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. | ||
|
|
||
|
|
||
| import org.awaitility.Awaitility | ||
|
|
||
| import static java.util.concurrent.TimeUnit.SECONDS | ||
|
|
||
| /** | ||
| * Test CDC stream TVF with delete sign enabled for MySQL primary-key table. | ||
| * | ||
| * Scenario: | ||
| * 1. Snapshot phase (offset=initial): pre-existing rows (A1, B1) are synced. | ||
| * 2. Incremental INSERT (C1, D1). | ||
| * 3. Incremental UPDATE (C1 age -> 30). | ||
| * 4. Incremental DELETE (D1 removed). | ||
| */ | ||
| suite("test_streaming_job_cdc_stream_mysql_delete_sign", "p0,external,mysql,external_docker,external_docker_mysql,nondatalake") { | ||
| def jobName = "test_streaming_job_cdc_stream_mysql_ds_name" | ||
| def currentDb = (sql "select database()")[0][0] | ||
| def dorisTable = "test_streaming_job_cdc_stream_mysql_ds_tbl" | ||
| def mysqlDb = "test_cdc_db" | ||
| def mysqlTable = "test_streaming_job_cdc_stream_mysql_ds_src" | ||
|
|
||
| sql """DROP JOB IF EXISTS where jobname = '${jobName}'""" | ||
| sql """drop table if exists ${currentDb}.${dorisTable} force""" | ||
|
|
||
| sql """ | ||
| CREATE TABLE IF NOT EXISTS ${currentDb}.${dorisTable} ( | ||
| `name` varchar(200) NULL, | ||
| `age` int NULL | ||
| ) ENGINE=OLAP | ||
| UNIQUE KEY(`name`) | ||
| DISTRIBUTED BY HASH(`name`) BUCKETS AUTO | ||
| PROPERTIES ( | ||
| "replication_allocation" = "tag.location.default: 1", | ||
| "enable_unique_key_merge_on_write" = "true" | ||
| ) | ||
| """ | ||
|
|
||
| String enabled = context.config.otherConfigs.get("enableJdbcTest") | ||
| if (enabled != null && enabled.equalsIgnoreCase("true")) { | ||
| String mysql_port = context.config.otherConfigs.get("mysql_57_port") | ||
| String externalEnvIp = context.config.otherConfigs.get("externalEnvIp") | ||
| String s3_endpoint = getS3Endpoint() | ||
| String bucket = getS3BucketName() | ||
| String driver_url = "https://${bucket}.${s3_endpoint}/regression/jdbc_driver/mysql-connector-j-8.4.0.jar" | ||
|
|
||
| // prepare source table with primary key and pre-existing snapshot data | ||
| connect("root", "123456", "jdbc:mysql://${externalEnvIp}:${mysql_port}") { | ||
| sql """CREATE DATABASE IF NOT EXISTS ${mysqlDb}""" | ||
| sql """DROP TABLE IF EXISTS ${mysqlDb}.${mysqlTable}""" | ||
| sql """CREATE TABLE ${mysqlDb}.${mysqlTable} ( | ||
| `name` varchar(200) NOT NULL, | ||
| `age` int DEFAULT NULL, | ||
| PRIMARY KEY (`name`) | ||
| ) ENGINE=InnoDB""" | ||
| sql """INSERT INTO ${mysqlDb}.${mysqlTable} (name, age) VALUES ('A1', 1)""" | ||
| sql """INSERT INTO ${mysqlDb}.${mysqlTable} (name, age) VALUES ('B1', 2)""" | ||
| } | ||
|
|
||
| // create streaming job via cdc_stream TVF with delete sign enabled | ||
| sql """ | ||
| CREATE JOB ${jobName} | ||
| ON STREAMING DO INSERT INTO ${currentDb}.${dorisTable} (name, age, __DORIS_DELETE_SIGN__) | ||
| SELECT name, age, __DORIS_DELETE_SIGN__ FROM cdc_stream( | ||
| "type" = "mysql", | ||
| "jdbc_url" = "jdbc:mysql://${externalEnvIp}:${mysql_port}", | ||
| "driver_url" = "${driver_url}", | ||
| "driver_class" = "com.mysql.cj.jdbc.Driver", | ||
| "user" = "root", | ||
| "password" = "123456", | ||
| "database" = "${mysqlDb}", | ||
| "table" = "${mysqlTable}", | ||
| "offset" = "initial", | ||
| "snapshot_split_key" = "name", | ||
| "include_delete_sign" = "true" | ||
| ) | ||
| """ | ||
|
|
||
| // wait for at least one snapshot task to succeed | ||
| try { | ||
| Awaitility.await().atMost(300, SECONDS).pollInterval(2, SECONDS).until({ | ||
| def cnt = sql """select SucceedTaskCount from jobs("type"="insert") where Name='${jobName}' and ExecuteType='STREAMING'""" | ||
| log.info("SucceedTaskCount: " + cnt) | ||
| cnt.size() == 1 && (cnt.get(0).get(0) as int) >= 1 | ||
| }) | ||
| } catch (Exception ex) { | ||
| log.info("job: " + (sql """select * from jobs("type"="insert") where Name='${jobName}'""")) | ||
| log.info("tasks: " + (sql """select * from tasks("type"="insert") where JobName='${jobName}'""")) | ||
| throw ex | ||
| } | ||
|
|
||
| // verify snapshot data | ||
| qt_snapshot_data """ SELECT name, age FROM ${currentDb}.${dorisTable} ORDER BY name """ | ||
|
|
||
| // insert incremental rows | ||
| connect("root", "123456", "jdbc:mysql://${externalEnvIp}:${mysql_port}") { | ||
| sql """INSERT INTO ${mysqlDb}.${mysqlTable} (name, age) VALUES ('C1', 3)""" | ||
| sql """INSERT INTO ${mysqlDb}.${mysqlTable} (name, age) VALUES ('D1', 4)""" | ||
| } | ||
|
|
||
| // wait for binlog tasks to pick up the new rows | ||
| try { | ||
| Awaitility.await().atMost(120, SECONDS).pollInterval(2, SECONDS).until({ | ||
| def rows = sql """SELECT count(1) FROM ${currentDb}.${dorisTable} WHERE name IN ('C1', 'D1')""" | ||
| log.info("incremental rows: " + rows) | ||
| (rows.get(0).get(0) as int) == 2 | ||
| }) | ||
| } catch (Exception ex) { | ||
| log.info("job: " + (sql """select * from jobs("type"="insert") where Name='${jobName}'""")) | ||
| log.info("tasks: " + (sql """select * from tasks("type"="insert") where JobName='${jobName}'""")) | ||
| throw ex | ||
| } | ||
|
|
||
| qt_incremental_data """ SELECT name, age, __DORIS_DELETE_SIGN__ FROM ${currentDb}.${dorisTable} ORDER BY name """ | ||
|
|
||
| // verify incremental UPDATE | ||
| connect("root", "123456", "jdbc:mysql://${externalEnvIp}:${mysql_port}") { | ||
| sql """UPDATE ${mysqlDb}.${mysqlTable} SET age = 30 WHERE name = 'C1'""" | ||
| } | ||
| Awaitility.await().atMost(120, SECONDS).pollInterval(2, SECONDS).until({ | ||
| def rows = sql """SELECT age FROM ${currentDb}.${dorisTable} WHERE name = 'C1'""" | ||
| rows.size() == 1 && (rows.get(0).get(0) as int) == 30 | ||
| }) | ||
|
|
||
| qt_after_update """ SELECT name, age, __DORIS_DELETE_SIGN__ FROM ${currentDb}.${dorisTable} ORDER BY name """ | ||
|
|
||
| // verify incremental DELETE | ||
| connect("root", "123456", "jdbc:mysql://${externalEnvIp}:${mysql_port}") { | ||
| sql """DELETE FROM ${mysqlDb}.${mysqlTable} WHERE name = 'D1'""" | ||
| } | ||
| Awaitility.await().atMost(120, SECONDS).pollInterval(2, SECONDS).until({ | ||
| def rows = sql """SELECT count(1) FROM ${currentDb}.${dorisTable} WHERE name = 'D1'""" | ||
| (rows.get(0).get(0) as int) == 0 | ||
| }) | ||
|
|
||
| qt_after_delete """ SELECT name, age, __DORIS_DELETE_SIGN__ FROM ${currentDb}.${dorisTable} ORDER BY name """ | ||
|
|
||
| sql """DROP JOB IF EXISTS where jobname = '${jobName}'""" | ||
| sql """drop table if exists ${currentDb}.${dorisTable} force""" | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.