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");
+  }
+}