Skip to content

Commit 0de9a67

Browse files
authored
Merge pull request #39457 from apache/debezium-io-yaml
Debezium IO yaml
2 parents 8a7c972 + 5178343 commit 0de9a67

7 files changed

Lines changed: 367 additions & 34 deletions

File tree

sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumIO.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.io.Serializable;
2424
import java.util.HashMap;
2525
import java.util.Map;
26-
import java.util.stream.Collectors;
2726
import org.apache.beam.sdk.coders.Coder;
2827
import org.apache.beam.sdk.coders.MapCoder;
2928
import org.apache.beam.sdk.coders.StringUtf8Coder;
@@ -319,23 +318,14 @@ protected Schema getRecordSchema() {
319318
SourceRecord sampledRecord =
320319
fn.getOneRecord(getConnectorConfiguration().getConfigurationMap());
321320
fn.reset();
322-
Schema keySchema =
323-
sampledRecord.keySchema() != null
324-
? KafkaConnectUtils.beamSchemaFromKafkaConnectSchema(sampledRecord.keySchema())
325-
: Schema.builder().build();
326321
Schema valueSchema =
327322
KafkaConnectUtils.beamSchemaFromKafkaConnectSchema(sampledRecord.valueSchema());
328323

329324
return Schema.builder()
330325
.addFields(valueSchema.getFields())
331-
.setOptions(
332-
Schema.Options.builder()
333-
.setOption(
334-
"primaryKeyColumns",
335-
Schema.FieldType.array(Schema.FieldType.STRING),
336-
keySchema.getFields().stream()
337-
.map(Schema.Field::getName)
338-
.collect(Collectors.toList())))
326+
// TODO(https://github.com/apache/beam/issues/39557):
327+
// Restore 'primaryKeyColumns' once Python can decode ARRAY<STRING>
328+
// schema options across the Java/Python cross-language boundary.
339329
.build();
340330
}
341331

sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProvider.java

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
import com.google.auto.service.AutoService;
2121
import com.google.auto.value.AutoValue;
2222
import java.util.Arrays;
23-
import java.util.Collection;
2423
import java.util.Collections;
2524
import java.util.List;
26-
import java.util.stream.Collectors;
2725
import org.apache.beam.sdk.coders.RowCoder;
26+
import org.apache.beam.sdk.schemas.AutoValueSchema;
2827
import org.apache.beam.sdk.schemas.Schema;
28+
import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
29+
import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
2930
import org.apache.beam.sdk.schemas.transforms.SchemaTransform;
3031
import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider;
3132
import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider;
@@ -73,6 +74,19 @@ protected DebeziumReadSchemaTransformProvider(
7374
this.testLimitMilliseconds = timeLimitMs;
7475
}
7576

77+
private static Connectors parseConnector(String value) {
78+
try {
79+
return Connectors.valueOf(value);
80+
} catch (IllegalArgumentException e) {
81+
throw new IllegalArgumentException(
82+
"Unsupported connector '"
83+
+ value
84+
+ "'. Supported connectors are: "
85+
+ Arrays.toString(Connectors.values()),
86+
e);
87+
}
88+
}
89+
7690
@Override
7791
protected @NonNull @Initialized Class<DebeziumReadSchemaTransformConfiguration>
7892
configurationClass() {
@@ -82,21 +96,15 @@ protected DebeziumReadSchemaTransformProvider(
8296
@Override
8397
protected @NonNull @Initialized SchemaTransform from(
8498
DebeziumReadSchemaTransformConfiguration configuration) {
85-
// TODO(pabloem): Validate configuration parameters to ensure formatting is correct.
99+
100+
configuration.validate();
101+
Connectors connector = parseConnector(configuration.getDatabase());
102+
86103
return new SchemaTransform() {
87104
@Override
88105
public PCollectionRowTuple expand(PCollectionRowTuple input) {
89106
// TODO(pabloem): Test this behavior
90-
Collection<String> connectors =
91-
Arrays.stream(Connectors.values()).map(Object::toString).collect(Collectors.toSet());
92-
if (!connectors.contains(configuration.getDatabase())) {
93-
throw new IllegalArgumentException(
94-
"Unsupported database "
95-
+ configuration.getDatabase()
96-
+ ". Unable to select a JDBC driver for it. Supported Databases are: "
97-
+ String.join(", ", connectors));
98-
}
99-
Class<?> connectorClass = Connectors.valueOf(configuration.getDatabase()).getConnector();
107+
Class<?> connectorClass = connector.getConnector();
100108
DebeziumIO.ConnectorConfiguration connectorConfiguration =
101109
DebeziumIO.ConnectorConfiguration.create()
102110
.withUsername(configuration.getUsername())
@@ -123,9 +131,9 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) {
123131
configuration.getDebeziumConnectionProperties();
124132
if (debeziumConnectionProperties != null) {
125133
for (String connectionProperty : debeziumConnectionProperties) {
126-
String[] parts = connectionProperty.split("=", -1);
127-
String key = parts[0];
128-
String value = parts[1];
134+
int separator = connectionProperty.indexOf('=');
135+
String key = connectionProperty.substring(0, separator);
136+
String value = connectionProperty.substring(separator + 1);
129137
connectorConfiguration = connectorConfiguration.withConnectionProperty(key, value);
130138
}
131139
}
@@ -138,6 +146,15 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) {
138146
readTransform
139147
.withMaxNumberOfRecords(testLimitRecords)
140148
.withMaxTimeToRun(testLimitMilliseconds);
149+
} else {
150+
Integer maxNumberOfRecords = configuration.getMaxNumberOfRecords();
151+
if (maxNumberOfRecords != null) {
152+
readTransform = readTransform.withMaxNumberOfRecords(maxNumberOfRecords);
153+
}
154+
Long maxTimeToRun = configuration.getMaxTimeToRun();
155+
if (maxTimeToRun != null) {
156+
readTransform = readTransform.withMaxTimeToRun(maxTimeToRun);
157+
}
141158
}
142159

143160
// TODO(pabloem): Database connection issues can be debugged here.
@@ -172,29 +189,76 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) {
172189
return Collections.singletonList("output");
173190
}
174191

192+
@DefaultSchema(AutoValueSchema.class)
175193
@AutoValue
176194
public abstract static class DebeziumReadSchemaTransformConfiguration {
195+
196+
@SchemaFieldDescription("Maximum number of records to read before stopping.")
197+
public abstract @Nullable Integer getMaxNumberOfRecords();
198+
199+
@SchemaFieldDescription("Maximum time in milliseconds to run before stopping.")
200+
public abstract @Nullable Long getMaxTimeToRun();
201+
202+
@SchemaFieldDescription("Username used to connect to the source database.")
177203
public abstract String getUsername();
178204

205+
@SchemaFieldDescription("Password used to connect to the source database.")
179206
public abstract String getPassword();
180207

208+
@SchemaFieldDescription("Hostname of the source database.")
181209
public abstract String getHost();
182210

211+
@SchemaFieldDescription("Port of the source database.")
183212
public abstract int getPort();
184213

214+
@SchemaFieldDescription("Fully qualified table name included in the Debezium change stream.")
185215
public abstract String getTable();
186216

217+
@SchemaFieldDescription(
218+
"Debezium connector type. Supported values: MYSQL, POSTGRES, SQLSERVER, ORACLE, and DB2.")
187219
public abstract @NonNull String getDatabase();
188220

221+
@SchemaFieldDescription("Additional Debezium connection properties in key=value format.")
189222
public abstract @Nullable List<String> getDebeziumConnectionProperties();
190223

224+
public void validate() {
225+
if (getHost().isEmpty()) {
226+
throw new IllegalArgumentException("host must not be empty.");
227+
}
228+
229+
if (getPort() <= 0 || getPort() > 65535) {
230+
throw new IllegalArgumentException(
231+
"port must be between 1 and 65535, but was " + getPort() + ".");
232+
}
233+
234+
if (getTable().isEmpty()) {
235+
throw new IllegalArgumentException("table must not be empty.");
236+
}
237+
238+
List<String> connectionProperties = getDebeziumConnectionProperties();
239+
if (connectionProperties != null) {
240+
for (String property : connectionProperties) {
241+
if (property == null || property.indexOf('=') <= 0) {
242+
throw new IllegalArgumentException(
243+
"Invalid Debezium connection property '"
244+
+ property
245+
+ "'. Expected key=value format.");
246+
}
247+
}
248+
}
249+
}
250+
191251
public static Builder builder() {
192252
return new AutoValue_DebeziumReadSchemaTransformProvider_DebeziumReadSchemaTransformConfiguration
193253
.Builder();
194254
}
195255

196256
@AutoValue.Builder
197257
public abstract static class Builder {
258+
public abstract Builder setMaxNumberOfRecords(@Nullable Integer maxNumberOfRecords);
259+
260+
public abstract Builder setMaxTimeToRun(@Nullable Long maxTimeToRun);
261+
198262
public abstract Builder setUsername(String username);
199263

200264
public abstract Builder setPassword(String password);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.io.debezium;
19+
20+
import static org.hamcrest.MatcherAssert.assertThat;
21+
import static org.junit.Assert.assertThrows;
22+
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import org.hamcrest.Matchers;
26+
import org.junit.Test;
27+
28+
public class DebeziumReadSchemaTransformProviderTest {
29+
30+
private static DebeziumReadSchemaTransformProvider.DebeziumReadSchemaTransformConfiguration
31+
.Builder
32+
validConfiguration() {
33+
return DebeziumReadSchemaTransformProvider.DebeziumReadSchemaTransformConfiguration.builder()
34+
.setUsername("user")
35+
.setPassword("password")
36+
.setHost("localhost")
37+
.setPort(5432)
38+
.setDatabase("POSTGRES")
39+
.setTable("inventory.customers")
40+
.setDebeziumConnectionProperties(Collections.emptyList());
41+
}
42+
43+
@Test
44+
public void testValidConfiguration() {
45+
validConfiguration().build().validate();
46+
}
47+
48+
@Test
49+
public void testEmptyHost() {
50+
IllegalArgumentException exception =
51+
assertThrows(
52+
IllegalArgumentException.class,
53+
() -> validConfiguration().setHost("").build().validate());
54+
55+
assertThat(exception.getMessage(), Matchers.containsString("host must not be empty"));
56+
}
57+
58+
@Test
59+
public void testInvalidPort() {
60+
IllegalArgumentException exception =
61+
assertThrows(
62+
IllegalArgumentException.class,
63+
() -> validConfiguration().setPort(0).build().validate());
64+
65+
assertThat(exception.getMessage(), Matchers.containsString("port must be between"));
66+
}
67+
68+
@Test
69+
public void testEmptyTable() {
70+
IllegalArgumentException exception =
71+
assertThrows(
72+
IllegalArgumentException.class,
73+
() -> validConfiguration().setTable("").build().validate());
74+
75+
assertThat(exception.getMessage(), Matchers.containsString("table must not be empty"));
76+
}
77+
78+
@Test
79+
public void testInvalidConnectionProperty() {
80+
IllegalArgumentException exception =
81+
assertThrows(
82+
IllegalArgumentException.class,
83+
() ->
84+
validConfiguration()
85+
.setDebeziumConnectionProperties(Collections.singletonList("invalid-property"))
86+
.build()
87+
.validate());
88+
89+
assertThat(exception.getMessage(), Matchers.containsString("Expected key=value format"));
90+
}
91+
92+
@Test
93+
public void testNullConnectionProperty() {
94+
IllegalArgumentException exception =
95+
assertThrows(
96+
IllegalArgumentException.class,
97+
() ->
98+
validConfiguration()
99+
.setDebeziumConnectionProperties(Arrays.asList("valid=value", null))
100+
.build()
101+
.validate());
102+
103+
assertThat(exception.getMessage(), Matchers.containsString("Expected key=value format"));
104+
}
105+
106+
@Test
107+
public void testConnectionPropertyValueContainingEquals() {
108+
validConfiguration()
109+
.setDebeziumConnectionProperties(
110+
Collections.singletonList("some.property=value=containing=equals"))
111+
.build()
112+
.validate();
113+
}
114+
115+
@Test
116+
public void testUnsupportedConnector() {
117+
IllegalArgumentException exception =
118+
assertThrows(
119+
IllegalArgumentException.class,
120+
() ->
121+
new DebeziumReadSchemaTransformProvider()
122+
.from(validConfiguration().setDatabase("UNKNOWN").build()));
123+
124+
assertThat(exception.getMessage(), Matchers.containsString("Unsupported connector 'UNKNOWN'"));
125+
assertThat(exception.getMessage(), Matchers.containsString("MYSQL"));
126+
assertThat(exception.getMessage(), Matchers.containsString("POSTGRES"));
127+
}
128+
129+
@Test
130+
public void testProviderContract() {
131+
DebeziumReadSchemaTransformProvider provider = new DebeziumReadSchemaTransformProvider();
132+
133+
assertThat(provider.inputCollectionNames(), Matchers.empty());
134+
assertThat(provider.outputCollectionNames(), Matchers.contains("output"));
135+
assertThat(
136+
provider.identifier(),
137+
Matchers.equalTo("beam:schematransform:org.apache.beam:debezium_read:v1"));
138+
}
139+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
fixtures:
19+
- name: DEBEZIUM_DB
20+
type: "apache_beam.yaml.integration_tests.temp_debezium_postgres_database"
21+
22+
pipelines:
23+
- pipeline:
24+
type: chain
25+
transforms:
26+
- type: ReadFromDebezium
27+
config:
28+
connector: POSTGRES
29+
username: "{DEBEZIUM_DB[USERNAME]}"
30+
password: "{DEBEZIUM_DB[PASSWORD]}"
31+
host: "{DEBEZIUM_DB[HOST]}"
32+
port: "{DEBEZIUM_DB[PORT]}"
33+
table: "{DEBEZIUM_DB[TABLE]}"
34+
max_number_of_records: 2
35+
max_time_to_run: 600000
36+
connection_properties:
37+
- "database.dbname={DEBEZIUM_DB[DATABASE]}"
38+
- "plugin.name=pgoutput"
39+
- "snapshot.mode=initial_only"
40+
- type: MapToFields
41+
config:
42+
language: python
43+
fields:
44+
id: "after.id"
45+
name: "after.name"
46+
- type: AssertEqual
47+
config:
48+
elements:
49+
- {id: 1, name: Alice}
50+
- {id: 2, name: Bob}

0 commit comments

Comments
 (0)