Skip to content

Commit f005b2f

Browse files
rodireichryankfu
authored andcommitted
Allow sessionvariables jdbc url param in source-mysql (airbytehq#25859)
* initial commit * cleanup * cleanup * Throw a configuration exception in case of bad jdbc url param * End-to-End test session variable jdbc param * reafctoring sanity * sanity * bump version and update note * cherry pick fix to unrelated build error * fix failing test --------- Co-authored-by: Ryan Fu <[email protected]>
1 parent 5d3aba6 commit f005b2f

File tree

8 files changed

+214
-113
lines changed

8 files changed

+214
-113
lines changed

airbyte-db/db-lib/src/main/java/io/airbyte/db/jdbc/JdbcUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import com.fasterxml.jackson.databind.JsonNode;
2626
import com.google.common.collect.Maps;
27+
import io.airbyte.commons.exceptions.ConfigErrorException;
2728
import java.sql.JDBCType;
2829
import java.util.HashMap;
2930
import java.util.List;
@@ -108,7 +109,7 @@ public static Map<String, String> parseJdbcParameters(final String jdbcPropertie
108109
if (split.length == 2) {
109110
parameters.put(split[0], split[1]);
110111
} else {
111-
throw new IllegalArgumentException(
112+
throw new ConfigErrorException(
112113
"jdbc_url_params must be formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). Got "
113114
+ jdbcPropertiesString);
114115
}

airbyte-integrations/bases/bases-destination-jdbc/src/test/java/io/airbyte/integrations/destination/jdbc/AbstractJdbcDestinationTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.fasterxml.jackson.databind.JsonNode;
1111
import com.google.common.collect.ImmutableMap;
12+
import io.airbyte.commons.exceptions.ConfigErrorException;
1213
import io.airbyte.commons.json.Jsons;
1314
import io.airbyte.db.jdbc.JdbcUtils;
1415
import io.airbyte.integrations.destination.StandardNameTransformer;
@@ -104,7 +105,7 @@ void testExtraParameterDiffersFromDefault() {
104105
@Test
105106
void testInvalidExtraParam() {
106107
final String extraParam = "key1=value1&sdf&";
107-
assertThrows(IllegalArgumentException.class,
108+
assertThrows(ConfigErrorException.class,
108109
() -> new TestJdbcDestination().getConnectionProperties(buildConfigWithExtraJdbcParameters(extraParam)));
109110
}
110111

airbyte-integrations/connectors/source-jdbc/src/main/java/io/airbyte/integrations/source/jdbc/JdbcDataSourceUtils.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static Map<String, String> getConnectionProperties(final JsonNode config)
5252
* @param config A configuration used to check Jdbc connection
5353
* @return A mapping of the default connection properties
5454
*/
55-
private static Map<String, String> getDefaultConnectionProperties(final JsonNode config) {
55+
public static Map<String, String> getDefaultConnectionProperties(final JsonNode config) {
5656
// NOTE that Postgres returns an empty map for some reason?
5757
return JdbcUtils.parseJdbcParameters(config, "connection_properties", DEFAULT_JDBC_PARAMETERS_DELIMITER);
5858
};

airbyte-integrations/connectors/source-mysql-strict-encrypt/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ ENV APPLICATION source-mysql-strict-encrypt
1616

1717
COPY --from=build /airbyte /airbyte
1818

19-
LABEL io.airbyte.version=2.0.21
19+
LABEL io.airbyte.version=2.0.22
2020

2121
LABEL io.airbyte.name=airbyte/source-mysql-strict-encrypt

airbyte-integrations/connectors/source-mysql/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ ENV APPLICATION source-mysql
1616

1717
COPY --from=build /airbyte /airbyte
1818

19-
LABEL io.airbyte.version=2.0.21
19+
LABEL io.airbyte.version=2.0.22
2020

2121
LABEL io.airbyte.name=airbyte/source-mysql

airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java

+63
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import static io.airbyte.integrations.debezium.AirbyteDebeziumHandler.shouldUseCDC;
99
import static io.airbyte.integrations.debezium.internals.DebeziumEventUtils.CDC_DELETED_AT;
1010
import static io.airbyte.integrations.debezium.internals.DebeziumEventUtils.CDC_UPDATED_AT;
11+
import static io.airbyte.integrations.source.jdbc.JdbcDataSourceUtils.DEFAULT_JDBC_PARAMETERS_DELIMITER;
12+
import static io.airbyte.integrations.source.jdbc.JdbcDataSourceUtils.assertCustomParametersDontOverwriteDefaultParameters;
1113
import static io.airbyte.integrations.source.jdbc.JdbcSSLConnectionUtils.SSL_MODE;
1214
import static java.util.stream.Collectors.toList;
1315

@@ -17,15 +19,19 @@
1719
import com.google.common.collect.ImmutableMap;
1820
import com.google.common.collect.Lists;
1921
import com.mysql.cj.MysqlType;
22+
import io.airbyte.commons.exceptions.ConfigErrorException;
2023
import io.airbyte.commons.features.EnvVariableFeatureFlags;
2124
import io.airbyte.commons.features.FeatureFlags;
2225
import io.airbyte.commons.functional.CheckedConsumer;
2326
import io.airbyte.commons.json.Jsons;
27+
import io.airbyte.commons.map.MoreMaps;
2428
import io.airbyte.commons.util.AutoCloseableIterator;
2529
import io.airbyte.commons.util.AutoCloseableIterators;
30+
import io.airbyte.db.factory.DataSourceFactory;
2631
import io.airbyte.db.factory.DatabaseDriver;
2732
import io.airbyte.db.jdbc.JdbcDatabase;
2833
import io.airbyte.db.jdbc.JdbcUtils;
34+
import io.airbyte.db.jdbc.StreamingJdbcDatabase;
2935
import io.airbyte.integrations.base.AirbyteTraceMessageUtility;
3036
import io.airbyte.integrations.base.IntegrationRunner;
3137
import io.airbyte.integrations.base.Source;
@@ -35,6 +41,7 @@
3541
import io.airbyte.integrations.debezium.internals.mysql.MySqlCdcPosition;
3642
import io.airbyte.integrations.debezium.internals.mysql.MySqlCdcTargetPosition;
3743
import io.airbyte.integrations.source.jdbc.AbstractJdbcSource;
44+
import io.airbyte.integrations.source.jdbc.JdbcDataSourceUtils;
3845
import io.airbyte.integrations.source.jdbc.JdbcSSLConnectionUtils;
3946
import io.airbyte.integrations.source.jdbc.JdbcSSLConnectionUtils.SslMode;
4047
import io.airbyte.integrations.source.mysql.helpers.CdcConfigurationHelper;
@@ -55,13 +62,15 @@
5562
import java.time.Instant;
5663
import java.util.ArrayList;
5764
import java.util.Collections;
65+
import java.util.HashMap;
5866
import java.util.List;
5967
import java.util.Map;
6068
import java.util.Objects;
6169
import java.util.Optional;
6270
import java.util.Set;
6371
import java.util.function.Supplier;
6472
import java.util.stream.Collectors;
73+
import javax.sql.DataSource;
6574
import org.slf4j.Logger;
6675
import org.slf4j.LoggerFactory;
6776

@@ -382,6 +391,60 @@ protected static String toSslJdbcParamInternal(final SslMode sslMode) {
382391
return result;
383392
}
384393

394+
@Override
395+
public JdbcDatabase createDatabase(final JsonNode sourceConfig) throws SQLException {
396+
// return super.createDatabase(sourceConfig, this::getConnectionProperties);
397+
final JsonNode jdbcConfig = toDatabaseConfig(sourceConfig);
398+
// Create the data source
399+
final DataSource dataSource = DataSourceFactory.create(
400+
jdbcConfig.has(JdbcUtils.USERNAME_KEY) ? jdbcConfig.get(JdbcUtils.USERNAME_KEY).asText() : null,
401+
jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null,
402+
driverClass,
403+
jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(),
404+
this.getConnectionProperties(sourceConfig));
405+
// Record the data source so that it can be closed.
406+
dataSources.add(dataSource);
407+
408+
final JdbcDatabase database = new StreamingJdbcDatabase(
409+
dataSource,
410+
sourceOperations,
411+
streamingQueryConfigProvider);
412+
413+
quoteString = (quoteString == null ? database.getMetaData().getIdentifierQuoteString() : quoteString);
414+
database.setSourceConfig(sourceConfig);
415+
database.setDatabaseConfig(jdbcConfig);
416+
return database;
417+
}
418+
419+
public Map<String, String> getConnectionProperties(final JsonNode config) {
420+
final Map<String, String> customProperties =
421+
config.has(JdbcUtils.JDBC_URL_PARAMS_KEY)
422+
? parseJdbcParameters(config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText(), DEFAULT_JDBC_PARAMETERS_DELIMITER) : new HashMap<>();
423+
final Map<String, String> defaultProperties = JdbcDataSourceUtils.getDefaultConnectionProperties(config);
424+
assertCustomParametersDontOverwriteDefaultParameters(customProperties, defaultProperties);
425+
return MoreMaps.merge(customProperties, defaultProperties);
426+
}
427+
428+
public static Map<String, String> parseJdbcParameters(final String jdbcPropertiesString, final String delimiter) {
429+
final Map<String, String> parameters = new HashMap<>();
430+
if (!jdbcPropertiesString.isBlank()) {
431+
final String[] keyValuePairs = jdbcPropertiesString.split(delimiter);
432+
for (final String kv : keyValuePairs) {
433+
final String[] split = kv.split("=");
434+
if (split.length == 2) {
435+
parameters.put(split[0], split[1]);
436+
} else if (split.length == 3 && kv.contains("sessionVariables")) {
437+
parameters.put(split[0], split[1] + "=" + split[2]);
438+
} else {
439+
throw new ConfigErrorException(
440+
"jdbc_url_params must be formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). Got "
441+
+ jdbcPropertiesString);
442+
}
443+
}
444+
}
445+
return parameters;
446+
}
447+
385448
public static void main(final String[] args) throws Exception {
386449
final Source source = MySqlSource.sshWrappedSource();
387450
LOGGER.info("starting source: {}", MySqlSource.class);

airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceTests.java

+35
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.sql.SQLException;
3232
import java.util.Collections;
3333
import java.util.List;
34+
import java.util.Map;
3435
import java.util.Properties;
3536
import org.junit.jupiter.api.Disabled;
3637
import org.junit.jupiter.api.Test;
@@ -194,4 +195,38 @@ CREATE VIEW test_view_null_cursor(id) as
194195

195196
}
196197

198+
199+
@Test
200+
void testParseJdbcParameters() {
201+
Map<String, String> parameters = MySqlSource.parseJdbcParameters("theAnswerToLiveAndEverything=42&sessionVariables=max_execution_time=10000&foo=bar", "&");
202+
assertEquals("max_execution_time=10000", parameters.get("sessionVariables"));
203+
assertEquals("42", parameters.get("theAnswerToLiveAndEverything"));
204+
assertEquals("bar", parameters.get("foo"));
205+
}
206+
207+
@Test
208+
public void testJDBCSessionVariable() throws Exception {
209+
// start DB
210+
try (final MySQLContainer<?> container = new MySQLContainer<>("mysql:8.0")
211+
.withUsername(TEST_USER)
212+
.withPassword(TEST_PASSWORD)
213+
.withEnv("MYSQL_ROOT_HOST", "%")
214+
.withEnv("MYSQL_ROOT_PASSWORD", TEST_PASSWORD)
215+
.withLogConsumer(new Slf4jLogConsumer(LOGGER))) {
216+
217+
container.start();
218+
final Properties properties = new Properties();
219+
properties.putAll(ImmutableMap.of("user", "root", JdbcUtils.PASSWORD_KEY, TEST_PASSWORD));
220+
DriverManager.getConnection(container.getJdbcUrl(), properties);
221+
final String dbName = Strings.addRandomSuffix("db", "_", 10);
222+
final JsonNode config = getConfig(container, dbName, "sessionVariables=MAX_EXECUTION_TIME=28800000");
223+
224+
try (final Connection connection = DriverManager.getConnection(container.getJdbcUrl(), properties)) {
225+
connection.createStatement().execute("GRANT ALL PRIVILEGES ON *.* TO '" + TEST_USER + "'@'%';\n");
226+
connection.createStatement().execute("CREATE DATABASE " + config.get(JdbcUtils.DATABASE_KEY).asText());
227+
}
228+
final AirbyteConnectionStatus check = new MySqlSource().check(config);
229+
assertEquals(AirbyteConnectionStatus.Status.SUCCEEDED, check.getStatus());
230+
}
231+
}
197232
}

0 commit comments

Comments
 (0)