You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by li...@apache.org on 2023/04/26 23:09:57 UTC
[arrow-adbc] branch main updated: feat(java/driver/jdbc): create AdbcDatabase from javax.sql.DataSource (#607)
This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new 2923891 feat(java/driver/jdbc): create AdbcDatabase from javax.sql.DataSource (#607)
2923891 is described below
commit 2923891974d8fc5a5ce0b97eb88470ebaa8c647f
Author: David Li <li...@gmail.com>
AuthorDate: Thu Apr 27 08:09:51 2023 +0900
feat(java/driver/jdbc): create AdbcDatabase from javax.sql.DataSource (#607)
Fixes #475.
---
.../driver/jdbc/derby/DerbyConnectionTest.java | 38 ++++++
java/driver/jdbc/pom.xml | 12 ++
...bcDatabase.java => JdbcDataSourceDatabase.java} | 39 +++++--
.../arrow/adbc/driver/jdbc/JdbcDatabase.java | 14 ++-
.../apache/arrow/adbc/driver/jdbc/JdbcDriver.java | 79 ++++++++++---
.../arrow/adbc/driver/jdbc/UrlDataSource.java | 87 ++++++++++++++
.../arrow/adbc/driver/jdbc/JdbcDatabaseTest.java | 128 +++++++++++++++++++++
7 files changed, 368 insertions(+), 29 deletions(-)
diff --git a/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
index f7065a2..0641b0e 100644
--- a/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
+++ b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
@@ -16,9 +16,20 @@
*/
package org.apache.arrow.adbc.driver.jdbc.derby;
+import static org.assertj.core.api.Assertions.assertThat;
+
import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcDatabase;
+import org.apache.arrow.adbc.core.AdbcDriver;
+import org.apache.arrow.adbc.driver.jdbc.JdbcDriver;
import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionTest;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.derby.jdbc.EmbeddedDataSource;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
public class DerbyConnectionTest extends AbstractConnectionTest {
@@ -28,4 +39,31 @@ public class DerbyConnectionTest extends AbstractConnectionTest {
static void beforeAll() {
quirks = new DerbyQuirks(tempDir);
}
+
+ @Test
+ void newUrlOption() throws Exception {
+ try (final BufferAllocator allocator = new RootAllocator()) {
+ AdbcDriver driver = new JdbcDriver(allocator);
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "jdbc:derby:memory:newUrlOption;create=true");
+ try (final AdbcDatabase database = driver.open(parameters)) {
+ assertThat(database).isNotNull();
+ }
+ }
+ }
+
+ @Test
+ void dataSourceOption() throws Exception {
+ try (final BufferAllocator allocator = new RootAllocator()) {
+ EmbeddedDataSource dataSource = new EmbeddedDataSource();
+ dataSource.setDatabaseName("memory:dataSourceOption");
+ dataSource.setCreateDatabase("create");
+ AdbcDriver driver = new JdbcDriver(allocator);
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_DATASOURCE, dataSource);
+ try (final AdbcDatabase database = driver.open(parameters)) {
+ assertThat(database).isNotNull();
+ }
+ }
+ }
}
diff --git a/java/driver/jdbc/pom.xml b/java/driver/jdbc/pom.xml
index ff4ae0f..0f1d4b2 100644
--- a/java/driver/jdbc/pom.xml
+++ b/java/driver/jdbc/pom.xml
@@ -50,5 +50,17 @@
<groupId>org.apache.arrow.adbc</groupId>
<artifactId>adbc-sql</artifactId>
</dependency>
+
+ <!-- Testing -->
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDataSourceDatabase.java
similarity index 64%
copy from java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java
copy to java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDataSourceDatabase.java
index 7c8a2a2..53adc36 100644
--- a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDataSourceDatabase.java
@@ -18,30 +18,40 @@
package org.apache.arrow.adbc.driver.jdbc;
import java.sql.Connection;
-import java.sql.DriverManager;
import java.sql.SQLException;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
+import javax.sql.DataSource;
import org.apache.arrow.adbc.core.AdbcConnection;
import org.apache.arrow.adbc.core.AdbcDatabase;
import org.apache.arrow.adbc.core.AdbcException;
import org.apache.arrow.adbc.sql.SqlQuirks;
import org.apache.arrow.memory.BufferAllocator;
-/** An instance of a database (e.g. a handle to an in-memory database). */
-public final class JdbcDatabase implements AdbcDatabase {
+/** An instance of a database based on a {@link DataSource}. */
+public final class JdbcDataSourceDatabase implements AdbcDatabase {
private final BufferAllocator allocator;
- private final String target;
+ private final DataSource dataSource;
+ private final String username;
+ private final String password;
private final SqlQuirks quirks;
private final Connection connection;
private final AtomicInteger counter;
- JdbcDatabase(BufferAllocator allocator, final String target, SqlQuirks quirks)
+ JdbcDataSourceDatabase(
+ BufferAllocator allocator,
+ DataSource dataSource,
+ String username,
+ String password,
+ SqlQuirks quirks)
throws AdbcException {
- this.allocator = allocator;
- this.target = target;
- this.quirks = quirks;
+ this.allocator = Objects.requireNonNull(allocator);
+ this.dataSource = Objects.requireNonNull(dataSource);
+ this.username = username;
+ this.password = password;
+ this.quirks = Objects.requireNonNull(quirks);
try {
- this.connection = DriverManager.getConnection(target);
+ this.connection = dataSource.getConnection();
} catch (SQLException e) {
throw JdbcDriverUtil.fromSqlException(e);
}
@@ -52,13 +62,18 @@ public final class JdbcDatabase implements AdbcDatabase {
public AdbcConnection connect() throws AdbcException {
final Connection connection;
try {
- connection = DriverManager.getConnection(target);
+ if (username != null && password != null) {
+ connection = dataSource.getConnection(username, password);
+ } else {
+ connection = dataSource.getConnection();
+ }
} catch (SQLException e) {
throw JdbcDriverUtil.fromSqlException(e);
}
final int count = counter.getAndIncrement();
return new JdbcConnection(
- allocator.newChildAllocator("adbc-jdbc-connection-" + count, 0, allocator.getLimit()),
+ allocator.newChildAllocator(
+ "adbc-jdbc-datasource-connection-" + count, 0, allocator.getLimit()),
connection,
quirks);
}
@@ -70,6 +85,6 @@ public final class JdbcDatabase implements AdbcDatabase {
@Override
public String toString() {
- return "JdbcDatabase{" + "target='" + target + '\'' + '}';
+ return "JdbcDatabase{" + "dataSource='" + dataSource + '\'' + '}';
}
}
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java
index 7c8a2a2..38494bc 100644
--- a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabase.java
@@ -20,6 +20,7 @@ package org.apache.arrow.adbc.driver.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.arrow.adbc.core.AdbcConnection;
import org.apache.arrow.adbc.core.AdbcDatabase;
@@ -27,7 +28,12 @@ import org.apache.arrow.adbc.core.AdbcException;
import org.apache.arrow.adbc.sql.SqlQuirks;
import org.apache.arrow.memory.BufferAllocator;
-/** An instance of a database (e.g. a handle to an in-memory database). */
+/**
+ * An instance of a database (e.g. a handle to an in-memory database).
+ *
+ * @deprecated Use {@link JdbcDataSourceDatabase}.
+ */
+@Deprecated
public final class JdbcDatabase implements AdbcDatabase {
private final BufferAllocator allocator;
private final String target;
@@ -37,9 +43,9 @@ public final class JdbcDatabase implements AdbcDatabase {
JdbcDatabase(BufferAllocator allocator, final String target, SqlQuirks quirks)
throws AdbcException {
- this.allocator = allocator;
- this.target = target;
- this.quirks = quirks;
+ this.allocator = Objects.requireNonNull(allocator);
+ this.target = Objects.requireNonNull(target);
+ this.quirks = Objects.requireNonNull(quirks);
try {
this.connection = DriverManager.getConnection(target);
} catch (SQLException e) {
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriver.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriver.java
index 7925acb..2e30cb9 100644
--- a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriver.java
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriver.java
@@ -16,8 +16,10 @@
*/
package org.apache.arrow.adbc.driver.jdbc;
+import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
+import javax.sql.DataSource;
import org.apache.arrow.adbc.core.AdbcDatabase;
import org.apache.arrow.adbc.core.AdbcDriver;
import org.apache.arrow.adbc.core.AdbcException;
@@ -25,11 +27,18 @@ import org.apache.arrow.adbc.drivermanager.AdbcDriverManager;
import org.apache.arrow.adbc.sql.SqlQuirks;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
-import org.apache.arrow.util.Preconditions;
/** An ADBC driver wrapping the JDBC API. */
public class JdbcDriver implements AdbcDriver {
public static final JdbcDriver INSTANCE = new JdbcDriver();
+ /** A parameter for creating an {@link AdbcDatabase} from a {@link DataSource}. */
+ public static final String PARAM_DATASOURCE = "adbc.jdbc.datasource";
+ /**
+ * A parameter for specifying a URI to connect to.
+ *
+ * <p>Matches the parameter used by C and Go.
+ */
+ public static final String PARAM_URI = "uri";
static {
AdbcDriverManager.getInstance().registerDriver("org.apache.arrow.adbc.driver.jdbc", INSTANCE);
@@ -47,20 +56,64 @@ public class JdbcDriver implements AdbcDriver {
@Override
public AdbcDatabase open(Map<String, Object> parameters) throws AdbcException {
- Object target = parameters.get(PARAM_URL);
- if (!(target instanceof String)) {
- throw AdbcException.invalidArgument("[JDBC] Must provide String " + PARAM_URL + " parameter");
+ DataSource dataSource = getParam(DataSource.class, parameters, PARAM_DATASOURCE);
+ // XXX(apache/arrow-adbc#316): allow "uri" to align with C/Go
+ String target = getParam(String.class, parameters, PARAM_URI, PARAM_URL);
+ if (dataSource != null && target != null) {
+ throw AdbcException.invalidArgument(
+ "[JDBC] Provide at most one of " + PARAM_URI + " and " + PARAM_DATASOURCE);
}
- Object quirks = parameters.get(PARAM_SQL_QUIRKS);
- if (quirks != null) {
- Preconditions.checkArgument(
- quirks instanceof SqlQuirks,
- String.format(
- "[JDBC] %s must be a SqlQuirks instance, not %s",
- PARAM_SQL_QUIRKS, quirks.getClass().getName()));
- } else {
+
+ SqlQuirks quirks = getParam(SqlQuirks.class, parameters, PARAM_SQL_QUIRKS);
+ if (quirks == null) {
quirks = new SqlQuirks();
}
- return new JdbcDatabase(allocator, (String) target, (SqlQuirks) quirks);
+
+ String username = getParam(String.class, parameters, "username");
+ String password = getParam(String.class, parameters, "password");
+ if ((username != null && password == null) || (username == null && password != null)) {
+ throw AdbcException.invalidArgument(
+ "[JDBC] Must provide both or neither of username and password");
+ }
+
+ if (target != null) {
+ dataSource = new UrlDataSource(target);
+ }
+
+ if (dataSource != null) {
+ return new JdbcDataSourceDatabase(allocator, dataSource, username, password, quirks);
+ }
+ throw AdbcException.invalidArgument(
+ "[JDBC] Must provide one of " + PARAM_URI + " and " + PARAM_DATASOURCE + " options");
+ }
+
+ private static <T> T getParam(Class<T> klass, Map<String, Object> parameters, String... choices)
+ throws AdbcException {
+ Object result = null;
+ for (String choice : choices) {
+ Object value = parameters.get(choice);
+ if (value != null) {
+ if (result != null) {
+ throw AdbcException.invalidArgument(
+ "[JDBC] Provide at most one of " + Arrays.toString(choices));
+ }
+ result = value;
+ }
+ }
+ if (result == null) {
+ return null;
+ }
+
+ try {
+ return klass.cast(result);
+ } catch (ClassCastException e) {
+ throw AdbcException.invalidArgument(
+ "[JDBC] "
+ + Arrays.toString(choices)
+ + " must be a "
+ + klass
+ + ", not a "
+ + result.getClass());
+ }
}
}
diff --git a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/UrlDataSource.java b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/UrlDataSource.java
new file mode 100644
index 0000000..f74b97f
--- /dev/null
+++ b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/UrlDataSource.java
@@ -0,0 +1,87 @@
+/*
+ * 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.arrow.adbc.driver.jdbc;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Objects;
+import java.util.logging.Logger;
+import javax.sql.DataSource;
+
+/** Adapt a JDBC URL to the DataSource interface. */
+class UrlDataSource implements DataSource {
+ final String target;
+ PrintWriter logWriter;
+ int loginTimeout;
+
+ UrlDataSource(String target) {
+ this.target = Objects.requireNonNull(target);
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ return DriverManager.getConnection(target);
+ }
+
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ return DriverManager.getConnection(target, username, password);
+ }
+
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ return logWriter;
+ }
+
+ @Override
+ public void setLogWriter(PrintWriter out) throws SQLException {
+ logWriter = out;
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ return loginTimeout;
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ loginTimeout = seconds;
+ }
+
+ @Override
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException("UrlDataSource does not support getParentLogger");
+ }
+
+ @Override
+ public <T> T unwrap(Class<T> iface) throws SQLException {
+ try {
+ return iface.cast(this);
+ } catch (ClassCastException e) {
+ throw new SQLException(e);
+ }
+ }
+
+ @Override
+ public boolean isWrapperFor(Class<?> iface) throws SQLException {
+ return iface.isInstance(this);
+ }
+}
diff --git a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabaseTest.java b/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabaseTest.java
new file mode 100644
index 0000000..831abac
--- /dev/null
+++ b/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcDatabaseTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.arrow.adbc.driver.jdbc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcDriver;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.util.AutoCloseables;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings("resource") // due to driver.open calls
+class JdbcDatabaseTest {
+ BufferAllocator allocator;
+ AdbcDriver driver;
+
+ @BeforeEach
+ void beforeEach() {
+ allocator = new RootAllocator();
+ driver = new JdbcDriver(allocator);
+ }
+
+ @AfterEach
+ void afterEach() throws Exception {
+ AutoCloseables.close(allocator);
+ }
+
+ @Test
+ void conflictingUriOption() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "jdbc:derby:memory:newUriOption;create=true");
+ parameters.put(AdbcDriver.PARAM_URL, "jdbc:derby:memory:newUriOption;create=true");
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining("Provide at most one of [uri, adbc.url]");
+ }
+
+ @Test
+ void conflictingUriDataSourceOption() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "jdbc:derby:memory:newUriOption;create=true");
+ parameters.put(
+ JdbcDriver.PARAM_DATASOURCE,
+ new UrlDataSource("jdbc:derby:memory:newUriOption;create=true"));
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining("Provide at most one of uri and adbc.jdbc.datasource");
+ }
+
+ @Test
+ void noUri() {
+ Map<String, Object> parameters = new HashMap<>();
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining("Must provide one of uri and adbc.jdbc.datasource options");
+ }
+
+ @Test
+ void badUriOptionType() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, 5);
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining(
+ "[uri, adbc.url] must be a class java.lang.String, not a class java.lang.Integer");
+ }
+
+ @Test
+ void badSqlQuirksOptionType() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "");
+ parameters.put(JdbcDriver.PARAM_SQL_QUIRKS, "");
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining(
+ "[adbc.sql.quirks] must be a class org.apache.arrow.adbc.sql.SqlQuirks, not a class"
+ + " java.lang.String");
+ }
+
+ @Test
+ void badUsernameOptionType() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "");
+ parameters.put("username", 2);
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining(
+ "[username] must be a class java.lang.String, not a class java.lang.Integer");
+
+ parameters.clear();
+ parameters.put(JdbcDriver.PARAM_URI, "");
+ parameters.put("password", 2);
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining(
+ "[password] must be a class java.lang.String, not a class java.lang.Integer");
+ }
+
+ @Test
+ void usernameWithoutPassword() {
+ Map<String, Object> parameters = new HashMap<>();
+ parameters.put(JdbcDriver.PARAM_URI, "");
+ parameters.put("username", "");
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining("Must provide both or neither of username and password");
+
+ parameters.clear();
+ parameters.put(JdbcDriver.PARAM_URI, "");
+ parameters.put("password", "");
+ assertThat(assertThrows(AdbcException.class, () -> driver.open(parameters)))
+ .hasMessageContaining("Must provide both or neither of username and password");
+ }
+}