Skip to content

Commit 4b848ce

Browse files
Add user and password config to MySQL event listener
Allow configuring MySQL credentials for the event listener using separate mysql-event-listener.db.user and mysql-event-listener.db.password properties. This makes it easier and safer to provide credentials, especially when passwords contain special characters, and aligns with the configuration style of other Trino connectors. The connection logic now uses these properties if provided, falling back to credentials in the JDBC URL if not set.
1 parent cec39cd commit 4b848ce

File tree

5 files changed

+105
-13
lines changed

5 files changed

+105
-13
lines changed

docs/src/main/sphinx/admin/event-listeners-mysql.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,12 @@ string, user, catalog, and others with information about the query processing.
6666
* - Property name
6767
- Description
6868
* - `mysql-event-listener.db.url`
69-
- JDBC connection URL to the database including credentials
69+
- Fully qualified JDBC connection URL used to connect to the MySQL database.
70+
Credentials can be embedded in the URL if `user` and `password` are not specified separately
71+
* - `mysql-event-listener.db.user`
72+
- Username used for authenticating the JDBC connection to the MySQL database.
73+
An exception will be raised if credentials are included in the `URL`.
74+
* - `mysql-event-listener.db.password`
75+
- Password used for authenticating the JDBC connection to the MySQL database.
76+
An exception will be raised if credentials are included in the `URL`.
7077
:::

plugin/trino-mysql-event-listener/src/main/java/io/trino/plugin/eventlistener/mysql/MysqlEventListenerConfig.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@
1313
*/
1414
package io.trino.plugin.eventlistener.mysql;
1515

16+
import com.mysql.cj.conf.ConnectionUrlParser;
1617
import com.mysql.cj.jdbc.Driver;
1718
import io.airlift.configuration.Config;
19+
import io.airlift.configuration.ConfigDescription;
1820
import io.airlift.configuration.ConfigSecuritySensitive;
1921
import jakarta.validation.constraints.AssertTrue;
2022
import jakarta.validation.constraints.NotNull;
2123

2224
import java.sql.SQLException;
25+
import java.util.Optional;
2326

2427
public class MysqlEventListenerConfig
2528
{
2629
private String url;
30+
private Optional<String> user = Optional.empty();
31+
private Optional<String> password = Optional.empty();
2732

2833
@NotNull
2934
public String getUrl()
@@ -39,11 +44,50 @@ public MysqlEventListenerConfig setUrl(String url)
3944
return this;
4045
}
4146

47+
public Optional<String> getUser()
48+
{
49+
return user;
50+
}
51+
52+
@ConfigDescription("MySQL connection user")
53+
@Config("mysql-event-listener.db.user")
54+
public MysqlEventListenerConfig setUser(String user)
55+
{
56+
this.user = Optional.ofNullable(user);
57+
return this;
58+
}
59+
60+
public Optional<String> getPassword()
61+
{
62+
return password;
63+
}
64+
65+
@ConfigSecuritySensitive
66+
@ConfigDescription("MySQL connection password")
67+
@Config("mysql-event-listener.db.password")
68+
public MysqlEventListenerConfig setPassword(String password)
69+
{
70+
this.password = Optional.ofNullable(password);
71+
return this;
72+
}
73+
4274
@AssertTrue(message = "Invalid JDBC URL for MySQL event listener")
4375
public boolean isValidUrl()
4476
{
4577
try {
46-
return new Driver().acceptsURL(getUrl());
78+
String url = getUrl();
79+
Boolean isValid = new Driver().acceptsURL(url);
80+
81+
if (isValid) {
82+
ConnectionUrlParser connUrl = ConnectionUrlParser.parseConnectionString(url);
83+
if (getUser().isPresent() && connUrl.getProperties().containsKey("user")) {
84+
throw new RuntimeException("'user' property is set, but JDBC URL also contains 'user'. Please specify the user only once.");
85+
}
86+
if (getPassword().isPresent() && connUrl.getProperties().containsKey("password")) {
87+
throw new RuntimeException("'password' property is set, but JDBC URL also contains 'password'. Please specify the password only once.");
88+
}
89+
}
90+
return isValid;
4791
}
4892
catch (SQLException e) {
4993
throw new RuntimeException(e);

plugin/trino-mysql-event-listener/src/main/java/io/trino/plugin/eventlistener/mysql/MysqlEventListenerFactory.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ public void configure(Binder binder)
9090
@Provides
9191
public ConnectionFactory createConnectionFactory(MysqlEventListenerConfig config)
9292
{
93-
return () -> new Driver().connect(config.getUrl(), new Properties());
93+
return () -> {
94+
Properties properties = new Properties();
95+
config.getUser().ifPresent(user -> properties.setProperty("user", user));
96+
config.getPassword().ifPresent(password -> properties.setProperty("password", password));
97+
return new Driver().connect(config.getUrl(), properties);
98+
};
9499
}
95100

96101
@Singleton

plugin/trino-mysql-event-listener/src/test/java/io/trino/plugin/eventlistener/mysql/TestMysqlEventListener.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ final class TestMysqlEventListener
354354

355355
private MySQLContainer<?> mysqlContainer;
356356
private String mysqlContainerUrl;
357+
private String mysqlContainerUser;
358+
private String mysqlContainerPassword;
357359
private EventListener eventListener;
358360
private JsonCodecFactory jsonCodecFactory;
359361

@@ -363,8 +365,12 @@ void setup()
363365
mysqlContainer = new MySQLContainer<>("mysql:8.0.36");
364366
mysqlContainer.start();
365367
mysqlContainerUrl = getJdbcUrl(mysqlContainer);
368+
mysqlContainerUser = mysqlContainer.getUsername();
369+
mysqlContainerPassword = mysqlContainer.getPassword();
366370
eventListener = new MysqlEventListenerFactory()
367-
.create(Map.of("mysql-event-listener.db.url", mysqlContainerUrl), new TestingEventListenerContext());
371+
.create(Map.of("mysql-event-listener.db.url", mysqlContainerUrl,
372+
"mysql-event-listener.db.user", mysqlContainerUser,
373+
"mysql-event-listener.db.password", mysqlContainerPassword), new TestingEventListenerContext());
368374
jsonCodecFactory = new JsonCodecFactory();
369375
}
370376

@@ -375,17 +381,17 @@ void teardown()
375381
mysqlContainer.close();
376382
mysqlContainer = null;
377383
mysqlContainerUrl = null;
384+
mysqlContainerUser = null;
385+
mysqlContainerPassword = null;
378386
}
379387
eventListener = null;
380388
jsonCodecFactory = null;
381389
}
382390

383391
private static String getJdbcUrl(MySQLContainer<?> container)
384392
{
385-
return format("%s?user=%s&password=%s&useSSL=false&allowPublicKeyRetrieval=true",
386-
container.getJdbcUrl(),
387-
container.getUsername(),
388-
container.getPassword());
393+
return format("%s?useSSL=false&allowPublicKeyRetrieval=true",
394+
container.getJdbcUrl());
389395
}
390396

391397
@Test
@@ -394,7 +400,7 @@ void testFull()
394400
{
395401
eventListener.queryCompleted(FULL_QUERY_COMPLETED_EVENT);
396402

397-
try (Connection connection = DriverManager.getConnection(mysqlContainerUrl)) {
403+
try (Connection connection = DriverManager.getConnection(mysqlContainerUrl, mysqlContainerUser, mysqlContainerPassword)) {
398404
try (Statement statement = connection.createStatement()) {
399405
statement.execute("SELECT * FROM trino_queries WHERE query_id = 'full_query'");
400406
try (ResultSet resultSet = statement.getResultSet()) {
@@ -479,7 +485,7 @@ void testMinimal()
479485
{
480486
eventListener.queryCompleted(MINIMAL_QUERY_COMPLETED_EVENT);
481487

482-
try (Connection connection = DriverManager.getConnection(mysqlContainerUrl)) {
488+
try (Connection connection = DriverManager.getConnection(mysqlContainerUrl, mysqlContainerUser, mysqlContainerPassword)) {
483489
try (Statement statement = connection.createStatement()) {
484490
statement.execute("SELECT * FROM trino_queries WHERE query_id = 'minimal_query'");
485491
try (ResultSet resultSet = statement.getResultSet()) {

plugin/trino-mysql-event-listener/src/test/java/io/trino/plugin/eventlistener/mysql/TestMysqlEventListenerConfig.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,37 @@
1616
import org.junit.jupiter.api.Test;
1717

1818
import java.util.Map;
19+
import java.util.Optional;
1920

2021
import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping;
2122
import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults;
2223
import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults;
2324
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2426

2527
final class TestMysqlEventListenerConfig
2628
{
2729
@Test
2830
void testDefaults()
2931
{
3032
assertRecordedDefaults(recordDefaults(MysqlEventListenerConfig.class)
31-
.setUrl(null));
33+
.setUrl(null)
34+
.setUser(null)
35+
.setPassword(null));
3236
}
3337

3438
@Test
3539
void testExplicitPropertyMappings()
3640
{
3741
Map<String, String> properties = Map.of(
38-
"mysql-event-listener.db.url", "jdbc:mysql://example.net:3306");
42+
"mysql-event-listener.db.url", "jdbc:mysql://example.net:3306",
43+
"mysql-event-listener.db.user", "user",
44+
"mysql-event-listener.db.password", "password");
3945

4046
MysqlEventListenerConfig expected = new MysqlEventListenerConfig()
41-
.setUrl("jdbc:mysql://example.net:3306");
47+
.setUrl("jdbc:mysql://example.net:3306")
48+
.setUser("user")
49+
.setPassword("password");
4250

4351
assertFullMapping(properties, expected);
4452
}
@@ -48,9 +56,31 @@ void testIsValidUrl()
4856
{
4957
assertThat(isValidUrl("jdbc:mysql://example.net:3306")).isTrue();
5058
assertThat(isValidUrl("jdbc:mysql://example.net:3306/")).isTrue();
59+
assertThat(isValidUrl("jdbc:mysql://example.net:3306/?user=trino&password=123456")).isTrue();
5160
assertThat(isValidUrl("jdbc:postgresql://example.net:3306/somedatabase")).isFalse();
5261
}
5362

63+
@Test
64+
void testIsAuthenticationRedundant()
65+
{
66+
assertThatThrownBy(() -> isAuthenticationRedundant("jdbc:mysql://example.net:3306?user=trino", Optional.of("trino"), Optional.empty()))
67+
.isInstanceOf(RuntimeException.class);
68+
assertThatThrownBy(() -> isAuthenticationRedundant("jdbc:mysql://example.net:3306?password=123456", Optional.empty(), Optional.of("123456")))
69+
.isInstanceOf(RuntimeException.class);
70+
}
71+
72+
private static boolean isAuthenticationRedundant(String url, Optional<String> user, Optional<String> password)
73+
{
74+
MysqlEventListenerConfig config = new MysqlEventListenerConfig().setUrl(url);
75+
if (user.isPresent()) {
76+
config.setUser(user.get());
77+
}
78+
if (password.isPresent()) {
79+
config.setPassword(password.get());
80+
}
81+
return config.isValidUrl();
82+
}
83+
5484
private static boolean isValidUrl(String url)
5585
{
5686
return new MysqlEventListenerConfig()

0 commit comments

Comments
 (0)