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 2485d7c  [Java] Implement prepared statements, refactor for 
consistency (#40)
2485d7c is described below

commit 2485d7c3da217a7190f86128d769a7d0445755ab
Author: David Li <[email protected]>
AuthorDate: Fri Jul 15 14:01:19 2022 -0400

    [Java] Implement prepared statements, refactor for consistency (#40)
    
    * Implement prepared statements for Java
    
    * Split out database-specific validation tests
---
 .../org/apache/arrow/adbc/core/AdbcConnection.java |  43 +++----
 .../org/apache/arrow/adbc/core/AdbcDriver.java     |   2 +-
 .../org/apache/arrow/adbc/core/AdbcException.java  |  24 ++--
 .../org/apache/arrow/adbc/core/AdbcStatement.java  |  24 ++--
 .../org/apache/arrow/adbc/core/AdbcStatusCode.java |  18 ++-
 .../adbc/drivermanager/AdbcDriverManager.java      |   5 +-
 java/driver/jdbc-util/pom.xml                      |   2 +-
 .../driver/{jdbc => jdbc-validation-derby}/pom.xml |  30 ++---
 .../jdbc/derby/DerbyConnectionMetadataTest.java}   |  19 ++-
 .../driver/jdbc/derby/DerbyConnectionTest.java}    |  19 ++-
 .../arrow/adbc/driver/jdbc/derby/DerbyQuirks.java  |  65 ++++++++++
 .../driver/jdbc/derby/DerbyStatementTest.java}     |  19 ++-
 .../driver/jdbc/derby/DerbyTransactionTest.java}   |  19 ++-
 .../pom.xml                                        |  26 ++--
 .../JdbcPostgresqlConnectionMetadataTest.java}     |  18 ++-
 .../postgresql/JdbcPostgresqlConnectionTest.java}  |  18 ++-
 .../postgresql/JdbcPostgresqlStatementTest.java}   |  18 ++-
 .../postgresql/JdbcPostgresqlTransactionTest.java} |  18 ++-
 .../driver/jdbc/postgresql/PostgresqlQuirks.java   |  90 ++++++++++++++
 java/driver/jdbc/pom.xml                           |   2 +-
 .../arrow/adbc/driver/jdbc/FixedJdbcStatement.java |   4 +-
 .../arrow/adbc/driver/jdbc/JdbcBindReader.java     | 108 +++++++++++++++++
 .../arrow/adbc/driver/jdbc/JdbcConnection.java     |  12 +-
 .../arrow/adbc/driver/jdbc/JdbcDatabase.java       |   8 +-
 .../apache/arrow/adbc/driver/jdbc/JdbcDriver.java  |  17 ++-
 .../arrow/adbc/driver/jdbc/JdbcDriverQuirks.java   |  91 ++++++++++++++
 .../arrow/adbc/driver/jdbc/JdbcDriverUtil.java     |  28 ++++-
 .../arrow/adbc/driver/jdbc/JdbcStatement.java      | 132 ++++++++++-----------
 java/driver/{testsuite => validation}/pom.xml      |   4 +-
 .../testsuite/AbstractConnectionMetadataTest.java  |  76 +++++-------
 .../driver/testsuite/AbstractConnectionTest.java   |   8 +-
 .../driver/testsuite/AbstractStatementTest.java    | 128 ++++++++++++++------
 .../driver/testsuite/AbstractTransactionTest.java  |  37 ++++--
 .../adbc/driver/testsuite/ArrowAssertions.java     |  27 +++++
 .../arrow/adbc/driver/testsuite/SqlTestUtil.java   |  72 +++++++++++
 .../driver/testsuite/SqlValidationQuirks.java}     |  27 +++--
 java/pom.xml                                       |   6 +-
 37 files changed, 896 insertions(+), 368 deletions(-)

diff --git 
a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcConnection.java 
b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcConnection.java
index 5f11a88..00002a8 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcConnection.java
+++ b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcConnection.java
@@ -16,20 +16,15 @@
  */
 package org.apache.arrow.adbc.core;
 
+import java.nio.ByteBuffer;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.types.pojo.Schema;
 
 /** A connection to a {@link AdbcDatabase}. */
 public interface AdbcConnection extends AutoCloseable {
-  /**
-   * Commit the pending transaction.
-   *
-   * @throws AdbcException if a database error occurs
-   * @throws IllegalStateException if autocommit is enabled
-   * @throws UnsupportedOperationException if the database does not support 
transactions
-   */
+  /** Commit the pending transaction. */
   default void commit() throws AdbcException {
-    throw new UnsupportedOperationException("Connection does not support 
transactions");
+    throw AdbcException.notImplemented("Connection does not support 
transactions");
   }
 
   /** Create a new statement that can be executed. */
@@ -43,10 +38,22 @@ public interface AdbcConnection extends AutoCloseable {
    */
   default AdbcStatement bulkIngest(String targetTableName, BulkIngestMode mode)
       throws AdbcException {
-    throw new UnsupportedOperationException(
+    throw AdbcException.notImplemented(
         "Connection does not support bulkIngest(String, BulkIngestMode)");
   }
 
+  /**
+   * Create a statement from a serialized PartitionDescriptor.
+   *
+   * @param descriptor The descriptor to load ({@link 
PartitionDescriptor#getDescriptor()}.
+   * @return A statement that can be immediately executed.
+   * @see AdbcStatement#getPartitionDescriptors()
+   */
+  default AdbcStatement deserializePartitionDescriptor(ByteBuffer descriptor) 
throws AdbcException {
+    throw AdbcException.notImplemented(
+        "Connection does not support 
deserializePartitionDescriptor(ByteBuffer)");
+  }
+
   /**
    * Get a hierarchical view of all catalogs, database schemas, tables, and 
columns.
    *
@@ -160,7 +167,7 @@ public interface AdbcConnection extends AutoCloseable {
       String[] tableTypes,
       String columnNamePattern)
       throws AdbcException {
-    throw new UnsupportedOperationException(
+    throw AdbcException.notImplemented(
         "Connection does not support getTableSchema(String, String, String)");
   }
 
@@ -189,7 +196,7 @@ public interface AdbcConnection extends AutoCloseable {
    */
   default Schema getTableSchema(String catalog, String dbSchema, String 
tableName)
       throws AdbcException {
-    throw new UnsupportedOperationException(
+    throw AdbcException.notImplemented(
         "Connection does not support getTableSchema(String, String, String)");
   }
 
@@ -210,18 +217,16 @@ public interface AdbcConnection extends AutoCloseable {
    * </table>
    */
   default AdbcStatement getTableTypes() throws AdbcException {
-    throw new UnsupportedOperationException("Connection does not support 
getTableTypes()");
+    throw AdbcException.notImplemented("Connection does not support 
getTableTypes()");
   }
 
   /**
    * Rollback the pending transaction.
    *
    * @throws AdbcException if a database error occurs
-   * @throws IllegalStateException if autocommit is enabled
-   * @throws UnsupportedOperationException if the database does not support 
transactions
    */
   default void rollback() throws AdbcException {
-    throw new UnsupportedOperationException("Connection does not support 
transactions");
+    throw AdbcException.notImplemented("Connection does not support 
transactions");
   }
 
   /**
@@ -233,12 +238,8 @@ public interface AdbcConnection extends AutoCloseable {
     return true;
   }
 
-  /**
-   * Toggle whether autocommit is enabled.
-   *
-   * @throws UnsupportedOperationException if the database does not support 
toggling autocommit
-   */
+  /** Toggle whether autocommit is enabled. */
   default void setAutoCommit(boolean enableAutoCommit) throws AdbcException {
-    throw new UnsupportedOperationException("Connection does not support 
transactions");
+    throw AdbcException.notImplemented("Connection does not support 
transactions");
   }
 }
diff --git a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java 
b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
index fc3f5e4..8b71f95 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
+++ b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
@@ -26,5 +26,5 @@ public interface AdbcDriver {
    *
    * @param parameters Driver-specific parameters.
    */
-  AdbcDatabase open(Map<String, String> parameters) throws AdbcException;
+  AdbcDatabase open(Map<String, Object> parameters) throws AdbcException;
 }
diff --git 
a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcException.java 
b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcException.java
index af412af..66de077 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcException.java
+++ b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcException.java
@@ -28,15 +28,6 @@ package org.apache.arrow.adbc.core;
  *   <li>A SQLSTATE string
  *   <li>A vendor-specific status code
  * </ul>
- *
- * Driver implementations should also use the following standard exception 
classes to indicate
- * invalid API usage:
- *
- * <ul>
- *   <li>{@link IllegalArgumentException} for invalid argument values
- *   <li>{@link UnsupportedOperationException} for unimplemented operations
- *   <li>{@link IllegalStateException} for other invalid use of the API (e.g. 
preconditions not met)
- * </ul>
  */
 public class AdbcException extends Exception {
   private final AdbcStatusCode status;
@@ -52,6 +43,21 @@ public class AdbcException extends Exception {
     this.vendorCode = vendorCode;
   }
 
+  /** Create a new exception with code {@link 
AdbcStatusCode#INVALID_ARGUMENT}. */
+  public static AdbcException invalidArgument(String message) {
+    return new AdbcException(message, /*cause*/ null, 
AdbcStatusCode.INVALID_ARGUMENT, null, 0);
+  }
+
+  /** Create a new exception with code {@link AdbcStatusCode#INVALID_STATE}. */
+  public static AdbcException invalidState(String message) {
+    return new AdbcException(message, /*cause*/ null, 
AdbcStatusCode.INVALID_STATE, null, 0);
+  }
+
+  /** Create a new exception with code {@link AdbcStatusCode#NOT_IMPLEMENTED}. 
*/
+  public static AdbcException notImplemented(String message) {
+    return new AdbcException(message, /*cause*/ null, 
AdbcStatusCode.NOT_IMPLEMENTED, null, 0);
+  }
+
   public AdbcStatusCode getStatus() {
     return status;
   }
diff --git 
a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatement.java 
b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatement.java
index 7b62ef0..b621587 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatement.java
+++ b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatement.java
@@ -25,8 +25,8 @@ import org.apache.arrow.vector.ipc.ArrowReader;
 
 public interface AdbcStatement extends AutoCloseable {
   /** Set a generic query option. */
-  default void setOption(String key, Object value) {
-    throw new UnsupportedOperationException("Unsupported option " + key);
+  default void setOption(String key, Object value) throws AdbcException {
+    throw AdbcException.notImplemented("Unsupported option " + key);
   }
 
   /**
@@ -34,8 +34,8 @@ public interface AdbcStatement extends AutoCloseable {
    *
    * @param query The SQL query.
    */
-  default void setSqlQuery(String query) {
-    throw new UnsupportedOperationException("Statement does not support SQL 
queries");
+  default void setSqlQuery(String query) throws AdbcException {
+    throw AdbcException.notImplemented("Statement does not support SQL 
queries");
   }
 
   /**
@@ -43,13 +43,13 @@ public interface AdbcStatement extends AutoCloseable {
    *
    * @param plan The serialized Substrait plan.
    */
-  default void setSubstraitPlan(ByteBuffer plan) {
-    throw new UnsupportedOperationException("Statement does not support 
Substrait plans");
+  default void setSubstraitPlan(ByteBuffer plan) throws AdbcException {
+    throw AdbcException.notImplemented("Statement does not support Substrait 
plans");
   }
 
   /** Bind this statement to a VectorSchemaRoot to provide parameter 
values/bulk data ingestion. */
-  default void bind(VectorSchemaRoot root) {
-    throw new UnsupportedOperationException("Statement does not support bind");
+  default void bind(VectorSchemaRoot root) throws AdbcException {
+    throw AdbcException.notImplemented("Statement does not support bind");
   }
 
   /**
@@ -70,7 +70,8 @@ public interface AdbcStatement extends AutoCloseable {
    *
    * <p>Must be called after {@link #execute()}.
    *
-   * @throws IllegalStateException if the statement has not been executed.
+   * @throws AdbcException with {@link AdbcStatusCode#INVALID_STATE} if the 
statement has not been
+   *     executed.
    */
   ArrowReader getArrowReader() throws AdbcException;
 
@@ -81,10 +82,11 @@ public interface AdbcStatement extends AutoCloseable {
    *
    * <p>Must be called after {@link #execute()}.
    *
-   * @throws IllegalStateException if the statement has not been executed.
+   * @throws AdbcException with {@link AdbcStatusCode#INVALID_STATE} if the 
statement has not been
+   *     executed.
    * @return The list of descriptors, or an empty list if unsupported.
    */
-  default List<PartitionDescriptor> getPartitionDescriptors() {
+  default List<PartitionDescriptor> getPartitionDescriptors() throws 
AdbcException {
     return Collections.emptyList();
   }
 
diff --git 
a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatusCode.java 
b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatusCode.java
index c97a384..7b8c634 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatusCode.java
+++ b/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcStatusCode.java
@@ -19,13 +19,26 @@ package org.apache.arrow.adbc.core;
 /**
  * A status code indicating the general category of error that occurred.
  *
- * <p>Also see the ADBC C API definition, which has similar status codes 
(except here we use
- * standard Java exceptions to indicate API misuse).
+ * <p>Also see the ADBC C API definition, which has similar status codes.
  */
 public enum AdbcStatusCode {
+  /**
+   * An unknown error occurred.
+   *
+   * <p>May indicate client-side or database-side error.
+   */
   UNKNOWN,
+  /**
+   * The operation is not supported.
+   *
+   * <p>May indicate client-side or database-side error.
+   */
+  NOT_IMPLEMENTED,
+  /** */
   NOT_FOUND,
   ALREADY_EXISTS,
+  INVALID_ARGUMENT,
+  INVALID_STATE,
   INVALID_DATA,
   INTEGRITY,
   INTERNAL,
@@ -34,4 +47,5 @@ public enum AdbcStatusCode {
   TIMEOUT,
   UNAUTHENTICATED,
   UNAUTHORIZED,
+  ;
 }
diff --git 
a/java/driver-manager/src/main/java/org/apache/arrow/adbc/drivermanager/AdbcDriverManager.java
 
b/java/driver-manager/src/main/java/org/apache/arrow/adbc/drivermanager/AdbcDriverManager.java
index b197bf5..06cf5f9 100644
--- 
a/java/driver-manager/src/main/java/org/apache/arrow/adbc/drivermanager/AdbcDriverManager.java
+++ 
b/java/driver-manager/src/main/java/org/apache/arrow/adbc/drivermanager/AdbcDriverManager.java
@@ -42,7 +42,7 @@ public final class AdbcDriverManager {
    * @return The AdbcDatabase instance.
    * @throws AdbcException if the driver was not found or if connection fails.
    */
-  public AdbcDatabase connect(String driverName, Map<String, String> 
parameters)
+  public AdbcDatabase connect(String driverName, Map<String, Object> 
parameters)
       throws AdbcException {
     final AdbcDriver driver = lookupDriver(driverName);
     if (driver == null) {
@@ -64,7 +64,8 @@ public final class AdbcDriverManager {
 
   public void registerDriver(String driverName, AdbcDriver driver) {
     if (drivers.putIfAbsent(driverName, driver) != null) {
-      throw new IllegalStateException("Driver already registered for '" + 
driverName + "'");
+      throw new IllegalStateException(
+          "[DriverManager] Driver already registered for '" + driverName + 
"'");
     }
   }
 
diff --git a/java/driver/jdbc-util/pom.xml b/java/driver/jdbc-util/pom.xml
index 44b5405..ea38d87 100644
--- a/java/driver/jdbc-util/pom.xml
+++ b/java/driver/jdbc-util/pom.xml
@@ -51,7 +51,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-testsuite</artifactId>
+      <artifactId>adbc-driver-validation</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/java/driver/jdbc/pom.xml 
b/java/driver/jdbc-validation-derby/pom.xml
similarity index 75%
copy from java/driver/jdbc/pom.xml
copy to java/driver/jdbc-validation-derby/pom.xml
index 105ad4f..453c5d6 100644
--- a/java/driver/jdbc/pom.xml
+++ b/java/driver/jdbc-validation-derby/pom.xml
@@ -18,37 +18,21 @@
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
-  <artifactId>adbc-driver-jdbc</artifactId>
+  <artifactId>adbc-driver-jdbc-validation-derby</artifactId>
   <packaging>jar</packaging>
-  <name>Arrow ADBC Driver JDBC</name>
-  <description>An ADBC driver wrapping the JDBC API.</description>
+  <name>Arrow ADBC Driver JDBC Validation with Derby</name>
+  <description>Tests validating the JDBC driver against Apache 
Derby.</description>
 
   <dependencies>
-    <!-- Arrow -->
-    <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-jdbc</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-memory-core</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-vector</artifactId>
-    </dependency>
-
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
       <artifactId>adbc-core</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-jdbc-util</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-manager</artifactId>
+      <artifactId>adbc-driver-jdbc</artifactId>
+      <scope>test</scope>
     </dependency>
 
     <!-- Derby -->
@@ -79,7 +63,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-testsuite</artifactId>
+      <artifactId>adbc-driver-validation</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionMetadataTest.java
 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionMetadataTest.java
similarity index 63%
rename from 
java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionMetadataTest.java
rename to 
java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionMetadataTest.java
index 9d8dca4..09eafcb 100644
--- 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionMetadataTest.java
+++ 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionMetadataTest.java
@@ -14,23 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.arrow.adbc.driver.jdbc;
+package org.apache.arrow.adbc.driver.jdbc.derby;
 
 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.AdbcException;
 import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionMetadataTest;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.io.TempDir;
 
-public class JdbcConnectionMetadataTest extends AbstractConnectionMetadataTest 
{
-  @TempDir Path tempDir;
+public class DerbyConnectionMetadataTest extends 
AbstractConnectionMetadataTest {
+  @TempDir static Path tempDir;
 
-  @Override
-  protected AdbcDatabase init() throws AdbcException {
-    final Map<String, String> parameters = new HashMap<>();
-    parameters.put("path", tempDir.toString() + "/db;create=true");
-    return JdbcDriver.INSTANCE.open(parameters);
+  @BeforeAll
+  static void beforeAll() {
+    quirks = new DerbyQuirks(tempDir);
   }
 }
diff --git 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionTest.java
 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
similarity index 64%
rename from 
java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionTest.java
rename to 
java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
index 4fcf69e..f7065a2 100644
--- 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnectionTest.java
+++ 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyConnectionTest.java
@@ -14,23 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.arrow.adbc.driver.jdbc;
+package org.apache.arrow.adbc.driver.jdbc.derby;
 
 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.AdbcException;
 import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionTest;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.io.TempDir;
 
-public class JdbcConnectionTest extends AbstractConnectionTest {
-  @TempDir Path tempDir;
+public class DerbyConnectionTest extends AbstractConnectionTest {
+  @TempDir static Path tempDir;
 
-  @Override
-  protected AdbcDatabase init() throws AdbcException {
-    final Map<String, String> parameters = new HashMap<>();
-    parameters.put("path", tempDir.toString() + "/db;create=true");
-    return JdbcDriver.INSTANCE.open(parameters);
+  @BeforeAll
+  static void beforeAll() {
+    quirks = new DerbyQuirks(tempDir);
   }
 }
diff --git 
a/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyQuirks.java
 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyQuirks.java
new file mode 100644
index 0000000..877f512
--- /dev/null
+++ 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyQuirks.java
@@ -0,0 +1,65 @@
+/*
+ * 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.derby;
+
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcDatabase;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.driver.jdbc.JdbcDriver;
+import org.apache.arrow.adbc.driver.testsuite.SqlValidationQuirks;
+
+public class DerbyQuirks extends SqlValidationQuirks {
+  private final String jdbcUrl;
+
+  public DerbyQuirks(Path databaseRoot) {
+    this.jdbcUrl = "jdbc:derby:" + databaseRoot.toString() + "/db;create=true";
+  }
+
+  @Override
+  public AdbcDatabase initDatabase() throws AdbcException {
+    final Map<String, Object> parameters = new HashMap<>();
+    parameters.put("adbc.jdbc.url", jdbcUrl);
+    return JdbcDriver.INSTANCE.open(parameters);
+  }
+
+  @Override
+  public void cleanupTable(String name) throws Exception {
+    try (final Connection connection1 = DriverManager.getConnection(jdbcUrl)) {
+      try (Statement statement = connection1.createStatement()) {
+        statement.execute("DROP TABLE " + name);
+      } catch (SQLException ignored) {
+      }
+    }
+  }
+
+  @Override
+  public String caseFoldTableName(String name) {
+    return name.toUpperCase();
+  }
+
+  @Override
+  public String caseFoldColumnName(String name) {
+    return name.toUpperCase();
+  }
+}
diff --git 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyStatementTest.java
similarity index 64%
copy from 
java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
copy to 
java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyStatementTest.java
index 72a28f3..0f71387 100644
--- 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
+++ 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyStatementTest.java
@@ -15,23 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.driver.jdbc;
+package org.apache.arrow.adbc.driver.jdbc.derby;
 
 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.AdbcException;
 import org.apache.arrow.adbc.driver.testsuite.AbstractStatementTest;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.io.TempDir;
 
-class JdbcStatementTest extends AbstractStatementTest {
-  @TempDir Path tempDir;
+class DerbyStatementTest extends AbstractStatementTest {
+  @TempDir static Path tempDir;
 
-  @Override
-  protected AdbcDatabase init() throws AdbcException {
-    final Map<String, String> parameters = new HashMap<>();
-    parameters.put("path", tempDir.toString() + "/db;create=true");
-    return JdbcDriver.INSTANCE.open(parameters);
+  @BeforeAll
+  static void beforeAll() {
+    quirks = new DerbyQuirks(tempDir);
   }
 }
diff --git 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcTransactionTest.java
 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyTransactionTest.java
similarity index 64%
rename from 
java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcTransactionTest.java
rename to 
java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyTransactionTest.java
index e5bcaeb..39446a9 100644
--- 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcTransactionTest.java
+++ 
b/java/driver/jdbc-validation-derby/src/test/java/org/apache/arrow/adbc/driver/jdbc/derby/DerbyTransactionTest.java
@@ -14,23 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.arrow.adbc.driver.jdbc;
+package org.apache.arrow.adbc.driver.jdbc.derby;
 
 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.AdbcException;
 import org.apache.arrow.adbc.driver.testsuite.AbstractTransactionTest;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.io.TempDir;
 
-public class JdbcTransactionTest extends AbstractTransactionTest {
-  @TempDir Path tempDir;
+public class DerbyTransactionTest extends AbstractTransactionTest {
+  @TempDir static Path tempDir;
 
-  @Override
-  protected AdbcDatabase init() throws AdbcException {
-    final Map<String, String> parameters = new HashMap<>();
-    parameters.put("path", tempDir.toString() + "/db;create=true");
-    return JdbcDriver.INSTANCE.open(parameters);
+  @BeforeAll
+  static void beforeAll() {
+    quirks = new DerbyQuirks(tempDir);
   }
 }
diff --git a/java/driver/jdbc-util/pom.xml 
b/java/driver/jdbc-validation-postgresql/pom.xml
similarity index 74%
copy from java/driver/jdbc-util/pom.xml
copy to java/driver/jdbc-validation-postgresql/pom.xml
index 44b5405..9c5c209 100644
--- a/java/driver/jdbc-util/pom.xml
+++ b/java/driver/jdbc-validation-postgresql/pom.xml
@@ -18,24 +18,28 @@
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
-  <artifactId>adbc-driver-jdbc-util</artifactId>
+  <artifactId>adbc-driver-jdbc-validation-postgresql</artifactId>
   <packaging>jar</packaging>
-  <name>Arrow ADBC Driver JDBC Util</name>
-  <description>Utilities for working with Arrow and JDBC.</description>
+  <name>Arrow ADBC Driver JDBC Validation with PostgreSQL</name>
+  <description>Tests validating the JDBC driver against 
Postgresql.</description>
 
   <dependencies>
-    <!-- Arrow -->
     <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-jdbc</artifactId>
+      <groupId>org.apache.arrow.adbc</groupId>
+      <artifactId>adbc-core</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-memory-core</artifactId>
+      <groupId>org.apache.arrow.adbc</groupId>
+      <artifactId>adbc-driver-jdbc</artifactId>
+      <scope>test</scope>
     </dependency>
+
     <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-vector</artifactId>
+      <groupId>org.postgresql</groupId>
+      <artifactId>postgresql</artifactId>
+      <version>42.4.0</version>
+      <scope>test</scope>
     </dependency>
 
     <!-- Testing -->
@@ -51,7 +55,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-testsuite</artifactId>
+      <artifactId>adbc-driver-validation</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionMetadataTest.java
similarity index 69%
copy from java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
copy to 
java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionMetadataTest.java
index fc3f5e4..a34ced5 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
+++ 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionMetadataTest.java
@@ -15,16 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.core;
+package org.apache.arrow.adbc.driver.jdbc.postgresql;
 
-import java.util.Map;
+import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionMetadataTest;
+import org.junit.jupiter.api.BeforeAll;
 
-/** A handle to an ADBC database driver. */
-public interface AdbcDriver {
-  /**
-   * Open a database via this driver.
-   *
-   * @param parameters Driver-specific parameters.
-   */
-  AdbcDatabase open(Map<String, String> parameters) throws AdbcException;
+public class JdbcPostgresqlConnectionMetadataTest extends 
AbstractConnectionMetadataTest {
+  @BeforeAll
+  public static void beforeAll() {
+    quirks = new PostgresqlQuirks();
+  }
 }
diff --git a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionTest.java
similarity index 71%
copy from java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
copy to 
java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionTest.java
index fc3f5e4..4ca7fe3 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
+++ 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlConnectionTest.java
@@ -15,16 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.core;
+package org.apache.arrow.adbc.driver.jdbc.postgresql;
 
-import java.util.Map;
+import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionTest;
+import org.junit.jupiter.api.BeforeAll;
 
-/** A handle to an ADBC database driver. */
-public interface AdbcDriver {
-  /**
-   * Open a database via this driver.
-   *
-   * @param parameters Driver-specific parameters.
-   */
-  AdbcDatabase open(Map<String, String> parameters) throws AdbcException;
+public class JdbcPostgresqlConnectionTest extends AbstractConnectionTest {
+  @BeforeAll
+  public static void beforeAll() {
+    quirks = new PostgresqlQuirks();
+  }
 }
diff --git a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlStatementTest.java
similarity index 71%
copy from java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
copy to 
java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlStatementTest.java
index fc3f5e4..598475d 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
+++ 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlStatementTest.java
@@ -15,16 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.core;
+package org.apache.arrow.adbc.driver.jdbc.postgresql;
 
-import java.util.Map;
+import org.apache.arrow.adbc.driver.testsuite.AbstractStatementTest;
+import org.junit.jupiter.api.BeforeAll;
 
-/** A handle to an ADBC database driver. */
-public interface AdbcDriver {
-  /**
-   * Open a database via this driver.
-   *
-   * @param parameters Driver-specific parameters.
-   */
-  AdbcDatabase open(Map<String, String> parameters) throws AdbcException;
+class JdbcPostgresqlStatementTest extends AbstractStatementTest {
+  @BeforeAll
+  public static void beforeAll() {
+    quirks = new PostgresqlQuirks();
+  }
 }
diff --git a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlTransactionTest.java
similarity index 70%
copy from java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
copy to 
java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlTransactionTest.java
index fc3f5e4..7c509fe 100644
--- a/java/core/src/main/java/org/apache/arrow/adbc/core/AdbcDriver.java
+++ 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/JdbcPostgresqlTransactionTest.java
@@ -15,16 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.core;
+package org.apache.arrow.adbc.driver.jdbc.postgresql;
 
-import java.util.Map;
+import org.apache.arrow.adbc.driver.testsuite.AbstractTransactionTest;
+import org.junit.jupiter.api.BeforeAll;
 
-/** A handle to an ADBC database driver. */
-public interface AdbcDriver {
-  /**
-   * Open a database via this driver.
-   *
-   * @param parameters Driver-specific parameters.
-   */
-  AdbcDatabase open(Map<String, String> parameters) throws AdbcException;
+public class JdbcPostgresqlTransactionTest extends AbstractTransactionTest {
+  @BeforeAll
+  public static void beforeAll() {
+    quirks = new PostgresqlQuirks();
+  }
 }
diff --git 
a/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/PostgresqlQuirks.java
 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/PostgresqlQuirks.java
new file mode 100644
index 0000000..a0a1885
--- /dev/null
+++ 
b/java/driver/jdbc-validation-postgresql/src/test/java/org/apache/arrow/adbc/driver/jdbc/postgresql/PostgresqlQuirks.java
@@ -0,0 +1,90 @@
+/*
+ * 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.postgresql;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcDatabase;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.driver.jdbc.JdbcDriver;
+import org.apache.arrow.adbc.driver.jdbc.JdbcDriverQuirks;
+import org.apache.arrow.adbc.driver.testsuite.SqlValidationQuirks;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.junit.jupiter.api.Assumptions;
+
+public class PostgresqlQuirks extends SqlValidationQuirks {
+  static final String POSTGRESQL_URL_ENV_VAR = "ADBC_JDBC_POSTGRESQL_URL";
+  static final String POSTGRESQL_USER_ENV_VAR = "ADBC_JDBC_POSTGRESQL_USER";
+  static final String POSTGRESQL_PASSWORD_ENV_VAR = 
"ADBC_JDBC_POSTGRESQL_PASSWORD";
+
+  static String makeJdbcUrl() {
+    final String postgresUrl = System.getenv(POSTGRESQL_URL_ENV_VAR);
+    final String user = System.getenv(POSTGRESQL_USER_ENV_VAR);
+    final String password = System.getenv(POSTGRESQL_PASSWORD_ENV_VAR);
+    Assumptions.assumeFalse(
+        postgresUrl == null, "Postgres not found, set " + 
POSTGRESQL_URL_ENV_VAR);
+    Assumptions.assumeFalse(
+        postgresUrl.isEmpty(), "Postgres not found, set " + 
POSTGRESQL_URL_ENV_VAR);
+    return String.format("jdbc:postgresql://%s?user=%s&password=%s", 
postgresUrl, user, password);
+  }
+
+  @Override
+  public AdbcDatabase initDatabase() throws AdbcException {
+    String url = makeJdbcUrl();
+
+    final Map<String, Object> parameters = new HashMap<>();
+    parameters.put("adbc.jdbc.url", url);
+    parameters.put(
+        "adbc.jdbc.quirks",
+        JdbcDriverQuirks.builder()
+            .arrowToSqlTypeNameMapping(
+                (arrowType -> {
+                  if (arrowType.getTypeID() == ArrowType.ArrowTypeID.Utf8) {
+                    return "TEXT";
+                  }
+                  return 
JdbcDriverQuirks.DEFAULT_ARROW_TYPE_TO_SQL_TYPE_NAME_MAPPING.apply(
+                      arrowType);
+                }))
+            .build());
+    return JdbcDriver.INSTANCE.open(parameters);
+  }
+
+  @Override
+  public void cleanupTable(String name) throws Exception {
+    try (final Connection connection1 = 
DriverManager.getConnection(makeJdbcUrl())) {
+      try (Statement statement = connection1.createStatement()) {
+        statement.execute("DROP TABLE " + name);
+      } catch (SQLException ignored) {
+      }
+    }
+  }
+
+  @Override
+  public String caseFoldTableName(String name) {
+    return name.toLowerCase();
+  }
+
+  @Override
+  public String caseFoldColumnName(String name) {
+    return name.toLowerCase();
+  }
+}
diff --git a/java/driver/jdbc/pom.xml b/java/driver/jdbc/pom.xml
index 105ad4f..0ea8d43 100644
--- a/java/driver/jdbc/pom.xml
+++ b/java/driver/jdbc/pom.xml
@@ -79,7 +79,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-driver-testsuite</artifactId>
+      <artifactId>adbc-driver-validation</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/FixedJdbcStatement.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/FixedJdbcStatement.java
index feae265..4f34d22 100644
--- 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/FixedJdbcStatement.java
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/FixedJdbcStatement.java
@@ -51,7 +51,7 @@ class FixedJdbcStatement implements AdbcStatement {
 
   @Override
   public void execute() throws AdbcException {
-    throw new UnsupportedOperationException("Cannot execute() this 
statement.");
+    throw AdbcException.invalidState("[JDBC] Cannot execute() this statement");
   }
 
   @Override
@@ -70,7 +70,7 @@ class FixedJdbcStatement implements AdbcStatement {
 
   @Override
   public void prepare() throws AdbcException {
-    throw new UnsupportedOperationException("Cannot prepare() this 
statement.");
+    throw AdbcException.invalidState("[JDBC] Cannot execute() this statement");
   }
 
   @Override
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcBindReader.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcBindReader.java
new file mode 100644
index 0000000..da32f5d
--- /dev/null
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcBindReader.java
@@ -0,0 +1,108 @@
+/*
+ * 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.IOException;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.apache.arrow.adapter.jdbc.ArrowVectorIterator;
+import org.apache.arrow.adapter.jdbc.JdbcToArrow;
+import org.apache.arrow.adapter.jdbc.JdbcToArrowUtils;
+import org.apache.arrow.adbc.driver.jdbc.util.JdbcParameterBinder;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.VectorUnloader;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.ipc.message.ArrowRecordBatch;
+import org.apache.arrow.vector.types.pojo.Schema;
+
+/** An Arrow reader that binds parameters. */
+final class JdbcBindReader extends ArrowReader {
+  private final PreparedStatement statement;
+  private final JdbcParameterBinder binder;
+  private ResultSet currentResultSet;
+  private ArrowVectorIterator currentSource;
+
+  JdbcBindReader(
+      BufferAllocator allocator, PreparedStatement statement, VectorSchemaRoot 
bindParameters) {
+    super(allocator);
+    this.statement = statement;
+    this.binder = JdbcParameterBinder.builder(statement, 
bindParameters).bindAll().build();
+  }
+
+  @Override
+  public boolean loadNextBatch() throws IOException {
+    if (currentSource == null || !currentSource.hasNext()) {
+      if (!advance()) {
+        return false;
+      }
+    }
+
+    final VectorSchemaRoot root = currentSource.next();
+    try (final ArrowRecordBatch batch = new 
VectorUnloader(root).getRecordBatch()) {
+      loadRecordBatch(batch);
+    }
+    return true;
+  }
+
+  @Override
+  public long bytesRead() {
+    return 0;
+  }
+
+  @Override
+  protected void closeReadSource() throws IOException {
+    try {
+      // Do not close PreparedStatement so we can reuse it
+      currentResultSet.close();
+    } catch (SQLException e) {
+      throw new IOException(e);
+    }
+  }
+
+  @Override
+  protected Schema readSchema() throws IOException {
+    try {
+      if (!advance()) {
+        throw new IOException("Parameter set is empty!");
+      }
+      return JdbcToArrowUtils.jdbcToArrowSchema(
+          currentResultSet.getMetaData(), JdbcToArrowUtils.getUtcCalendar());
+    } catch (SQLException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private boolean advance() throws IOException {
+    try {
+      if (binder.next()) {
+        if (currentResultSet != null) {
+          currentSource.close();
+          currentResultSet.close();
+        }
+        currentResultSet = statement.executeQuery();
+        currentSource = JdbcToArrow.sqlToArrowVectorIterator(currentResultSet, 
allocator);
+        return true;
+      }
+    } catch (SQLException e) {
+      throw new IOException(e);
+    }
+    return false;
+  }
+}
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnection.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnection.java
index 0bc8fa3..8824b12 100644
--- 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnection.java
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcConnection.java
@@ -39,10 +39,12 @@ import org.apache.arrow.vector.types.pojo.Schema;
 public class JdbcConnection implements AdbcConnection {
   private final BufferAllocator allocator;
   private final Connection connection;
+  private final JdbcDriverQuirks quirks;
 
-  JdbcConnection(BufferAllocator allocator, Connection connection) {
+  JdbcConnection(BufferAllocator allocator, Connection connection, 
JdbcDriverQuirks quirks) {
     this.allocator = allocator;
     this.connection = connection;
+    this.quirks = quirks;
   }
 
   @Override
@@ -57,13 +59,13 @@ public class JdbcConnection implements AdbcConnection {
 
   @Override
   public AdbcStatement createStatement() throws AdbcException {
-    return new JdbcStatement(allocator, connection);
+    return new JdbcStatement(allocator, connection, quirks);
   }
 
   @Override
   public AdbcStatement bulkIngest(String targetTableName, BulkIngestMode mode)
       throws AdbcException {
-    return JdbcStatement.ingestRoot(allocator, connection, targetTableName, 
mode);
+    return JdbcStatement.ingestRoot(allocator, connection, quirks, 
targetTableName, mode);
   }
 
   @Override
@@ -171,9 +173,9 @@ public class JdbcConnection implements AdbcConnection {
     connection.close();
   }
 
-  private void checkAutoCommit() throws SQLException {
+  private void checkAutoCommit() throws AdbcException, SQLException {
     if (connection.getAutoCommit()) {
-      throw new IllegalStateException("Cannot perform operation in autocommit 
mode");
+      throw AdbcException.invalidState("[JDBC] Cannot perform operation in 
autocommit mode");
     }
   }
 
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 eded48e..4cef0e4 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
@@ -30,12 +30,15 @@ import org.apache.arrow.memory.BufferAllocator;
 public final class JdbcDatabase implements AdbcDatabase {
   private final BufferAllocator allocator;
   private final String target;
+  private final JdbcDriverQuirks quirks;
   private final Connection connection;
   private final AtomicInteger counter;
 
-  JdbcDatabase(BufferAllocator allocator, final String target) throws 
AdbcException {
+  JdbcDatabase(BufferAllocator allocator, final String target, 
JdbcDriverQuirks quirks)
+      throws AdbcException {
     this.allocator = allocator;
     this.target = target;
+    this.quirks = quirks;
     try {
       this.connection = DriverManager.getConnection(target);
     } catch (SQLException e) {
@@ -55,7 +58,8 @@ public final class JdbcDatabase implements AdbcDatabase {
     final int count = counter.getAndIncrement();
     return new JdbcConnection(
         allocator.newChildAllocator("adbc-jdbc-connection-" + count, 0, 
allocator.getLimit()),
-        connection);
+        connection,
+        quirks);
   }
 
   @Override
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 b3621ac..341c849 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
@@ -23,6 +23,7 @@ import org.apache.arrow.adbc.core.AdbcException;
 import org.apache.arrow.adbc.drivermanager.AdbcDriverManager;
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.util.Preconditions;
 
 public enum JdbcDriver implements AdbcDriver {
   INSTANCE;
@@ -35,7 +36,19 @@ public enum JdbcDriver implements AdbcDriver {
   }
 
   @Override
-  public AdbcDatabase open(Map<String, String> parameters) throws 
AdbcException {
-    return new JdbcDatabase(allocator, "jdbc:derby:" + parameters.get("path"));
+  public AdbcDatabase open(Map<String, Object> parameters) throws 
AdbcException {
+    Object target = parameters.get("adbc.jdbc.url");
+    if (!(target instanceof String)) {
+      throw AdbcException.invalidArgument("[JDBC] Must provide String 
adbc.jdbc.url parameter");
+    }
+    Object quirks = parameters.get("adbc.jdbc.quirks");
+    if (quirks != null) {
+      Preconditions.checkArgument(
+          quirks instanceof JdbcDriverQuirks,
+          "[JDBC] adbc.jdbc.quirks must be a JdbcDriverQuirks instance");
+    } else {
+      quirks = new JdbcDriverQuirks();
+    }
+    return new JdbcDatabase(allocator, (String) target, (JdbcDriverQuirks) 
quirks);
   }
 }
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverQuirks.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverQuirks.java
new file mode 100644
index 0000000..99c87e6
--- /dev/null
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverQuirks.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util.function.Function;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+/** Parameters to pass to the ADBC JDBC driver to account for 
driver/vendor-specific quirks. */
+public final class JdbcDriverQuirks {
+  public static final Function<ArrowType, String> 
DEFAULT_ARROW_TYPE_TO_SQL_TYPE_NAME_MAPPING =
+      (arrowType) -> {
+        switch (arrowType.getTypeID()) {
+          case Null:
+          case Struct:
+          case List:
+          case LargeList:
+          case FixedSizeList:
+          case Union:
+          case Map:
+            return null;
+          case Int:
+            // TODO:
+            return "INT";
+          case FloatingPoint:
+            return null;
+          case Utf8:
+            return "CLOB";
+          case LargeUtf8:
+          case Binary:
+          case LargeBinary:
+          case FixedSizeBinary:
+          case Bool:
+          case Decimal:
+          case Date:
+          case Time:
+          case Timestamp:
+          case Interval:
+          case Duration:
+          case NONE:
+          default:
+            return null;
+        }
+      };
+  Function<ArrowType, String> arrowToSqlTypeNameMapping;
+
+  public JdbcDriverQuirks() {
+    this.arrowToSqlTypeNameMapping = 
DEFAULT_ARROW_TYPE_TO_SQL_TYPE_NAME_MAPPING;
+  }
+
+  /** The mapping from Arrow type to SQL type name, used to build queries. */
+  public Function<ArrowType, String> getArrowToSqlTypeNameMapping() {
+    return arrowToSqlTypeNameMapping;
+  }
+
+  /** Create a new builder. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static final class Builder {
+    Function<ArrowType, String> arrowToSqlTypeNameMapping;
+
+    public Builder arrowToSqlTypeNameMapping(Function<ArrowType, String> 
mapper) {
+      this.arrowToSqlTypeNameMapping = mapper;
+      return this;
+    }
+
+    public JdbcDriverQuirks build() {
+      final JdbcDriverQuirks quirks = new JdbcDriverQuirks();
+      if (arrowToSqlTypeNameMapping != null) {
+        quirks.arrowToSqlTypeNameMapping = arrowToSqlTypeNameMapping;
+      }
+      return quirks;
+    }
+  }
+}
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverUtil.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverUtil.java
index 4926877..7ba30f0 100644
--- 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverUtil.java
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcDriverUtil.java
@@ -17,10 +17,25 @@
 package org.apache.arrow.adbc.driver.jdbc;
 
 import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 import org.apache.arrow.adbc.core.AdbcException;
 import org.apache.arrow.adbc.core.AdbcStatusCode;
 
 final class JdbcDriverUtil {
+  // Do our best to properly map database-specific errors to NOT_FOUND status.
+  private static final Set<String> SQLSTATE_TABLE_NOT_FOUND =
+      new HashSet<>(
+          Arrays.asList(
+              // Apache Derby 
https://db.apache.org/derby/docs/10.4/ref/rrefexcept71493.html
+              "42X05",
+              // MySQL
+              // 
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-error-sqlstates.html
+              "42S02",
+              // Postgres 
https://www.postgresql.org/docs/current/errcodes-appendix.html
+              "42P01"));
+
   private JdbcDriverUtil() {
     throw new AssertionError("Do not instantiate this class");
   }
@@ -29,17 +44,26 @@ final class JdbcDriverUtil {
     return "[JDBC] " + s;
   }
 
+  static AdbcStatusCode guessStatusCode(String sqlState) {
+    if (sqlState == null) {
+      return AdbcStatusCode.UNKNOWN;
+    } else if (SQLSTATE_TABLE_NOT_FOUND.contains(sqlState)) {
+      return AdbcStatusCode.NOT_FOUND;
+    }
+    return AdbcStatusCode.UNKNOWN;
+  }
+
   static AdbcException fromSqlException(SQLException e) {
     return new AdbcException(
         prefixExceptionMessage(e.getMessage()),
         e.getCause(),
-        AdbcStatusCode.UNKNOWN,
+        guessStatusCode(e.getSQLState()),
         e.getSQLState(),
         e.getErrorCode());
   }
 
   static AdbcException fromSqlException(String format, SQLException e, 
Object... values) {
-    return fromSqlException(AdbcStatusCode.UNKNOWN, format, e, values);
+    return fromSqlException(guessStatusCode(e.getSQLState()), format, e, 
values);
   }
 
   static AdbcException fromSqlException(
diff --git 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatement.java
 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatement.java
index 99b46ac..df523d4 100644
--- 
a/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatement.java
+++ 
b/java/driver/jdbc/src/main/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatement.java
@@ -17,13 +17,12 @@
 
 package org.apache.arrow.adbc.driver.jdbc;
 
+import java.io.IOException;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Objects;
 import org.apache.arrow.adbc.core.AdbcException;
 import org.apache.arrow.adbc.core.AdbcStatement;
@@ -37,40 +36,34 @@ import org.apache.arrow.vector.ipc.ArrowReader;
 import org.apache.arrow.vector.types.pojo.Field;
 
 public class JdbcStatement implements AdbcStatement {
-  // Do our best to properly map database-specific errors to NOT_FOUND status.
-  private static final List<String> SQLSTATE_TABLE_NOT_FOUND =
-      Arrays.asList(
-          // Apache Derby 
https://db.apache.org/derby/docs/10.4/ref/rrefexcept71493.html
-          "42X05",
-          // MySQL
-          // 
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-error-sqlstates.html
-          "42S02",
-          // Postgres 
https://www.postgresql.org/docs/current/errcodes-appendix.html
-          "42P01");
   private final BufferAllocator allocator;
   private final Connection connection;
+  private final JdbcDriverQuirks quirks;
 
   // State for SQL queries
   private Statement statement;
   private String sqlQuery;
+  private ArrowReader reader;
   private ResultSet resultSet;
   // State for bulk ingest
   private BulkState bulkOperation;
   private VectorSchemaRoot bindRoot;
 
-  JdbcStatement(BufferAllocator allocator, Connection connection) {
+  JdbcStatement(BufferAllocator allocator, Connection connection, 
JdbcDriverQuirks quirks) {
     this.allocator = allocator;
     this.connection = connection;
+    this.quirks = quirks;
     this.sqlQuery = null;
   }
 
   static JdbcStatement ingestRoot(
       BufferAllocator allocator,
       Connection connection,
+      JdbcDriverQuirks quirks,
       String targetTableName,
       BulkIngestMode mode) {
     Objects.requireNonNull(targetTableName);
-    final JdbcStatement statement = new JdbcStatement(allocator, connection);
+    final JdbcStatement statement = new JdbcStatement(allocator, connection, 
quirks);
     statement.bulkOperation = new BulkState();
     statement.bulkOperation.mode = mode;
     statement.bulkOperation.targetTable = targetTableName;
@@ -78,9 +71,10 @@ public class JdbcStatement implements AdbcStatement {
   }
 
   @Override
-  public void setSqlQuery(String query) {
+  public void setSqlQuery(String query) throws AdbcException {
     if (bulkOperation != null) {
-      throw new IllegalStateException("Statement is configured for a bulk 
ingest/append operation");
+      throw AdbcException.invalidState(
+          "[JDBC] Statement is configured for a bulk ingest/append operation");
     }
     sqlQuery = query;
   }
@@ -97,7 +91,7 @@ public class JdbcStatement implements AdbcStatement {
     } else if (sqlQuery != null) {
       executeSqlQuery();
     } else {
-      throw new IllegalStateException("Must setSqlQuery first");
+      throw AdbcException.invalidState("[JDBC] Must setSqlQuery() first");
     }
   }
 
@@ -111,38 +105,13 @@ public class JdbcStatement implements AdbcStatement {
       }
       final Field field = bindRoot.getVector(col).getField();
       create.append(field.getName());
-      switch (field.getType().getTypeID()) {
-        case Null:
-        case Struct:
-        case List:
-        case LargeList:
-        case FixedSizeList:
-        case Union:
-        case Map:
-          throw new UnsupportedOperationException("Type " + field);
-        case Int:
-          // TODO:
-          create.append(" INT");
-          break;
-        case FloatingPoint:
-          throw new UnsupportedOperationException("Type " + field);
-        case Utf8:
-          create.append(" CLOB");
-          break;
-        case LargeUtf8:
-        case Binary:
-        case LargeBinary:
-        case FixedSizeBinary:
-        case Bool:
-        case Decimal:
-        case Date:
-        case Time:
-        case Timestamp:
-        case Interval:
-        case Duration:
-        case NONE:
-          throw new UnsupportedOperationException("Type " + field);
+      create.append(' ');
+      String typeName = 
quirks.getArrowToSqlTypeNameMapping().apply(field.getType());
+      if (typeName == null) {
+        throw AdbcException.notImplemented(
+            "[JDBC] Cannot generate CREATE TABLE statement for field " + 
field);
       }
+      create.append(typeName);
     }
     create.append(")");
 
@@ -150,13 +119,16 @@ public class JdbcStatement implements AdbcStatement {
       statement.execute(create.toString());
     } catch (SQLException e) {
       throw JdbcDriverUtil.fromSqlException(
-          AdbcStatusCode.ALREADY_EXISTS, "Could not create table %s", e, 
bulkOperation.targetTable);
+          AdbcStatusCode.ALREADY_EXISTS,
+          "Could not create table %s: ",
+          e,
+          bulkOperation.targetTable);
     }
   }
 
   private void executeBulk() throws AdbcException {
     if (bindRoot == null) {
-      throw new IllegalStateException("Must bind() before bulk insert");
+      throw AdbcException.invalidState("[JDBC] Must call bind() before bulk 
insert");
     }
 
     if (bulkOperation.mode == BulkIngestMode.CREATE) {
@@ -180,17 +152,8 @@ public class JdbcStatement implements AdbcStatement {
     try {
       statement = connection.prepareStatement(insert.toString());
     } catch (SQLException e) {
-      // It's hard to differentiate between 'table not found' and parameter 
type/count mismatch here
-      // because SQLState is inconsistent (see SQLSTATE_TABLE_NOT_FOUND 
above). We could query for
-      // table existence but that's another roundtrip and leads to a TOC/TOU
-      // error. Instead, we hard-code some common codes here.
-
-      final AdbcStatusCode code =
-          SQLSTATE_TABLE_NOT_FOUND.contains(e.getSQLState())
-              ? AdbcStatusCode.NOT_FOUND
-              : AdbcStatusCode.ALREADY_EXISTS;
       throw JdbcDriverUtil.fromSqlException(
-          code, "Could not bulk insert into table %s: ", e, 
bulkOperation.targetTable);
+          "Could not bulk insert into table %s: ", e, 
bulkOperation.targetTable);
     }
     try {
       try {
@@ -211,12 +174,33 @@ public class JdbcStatement implements AdbcStatement {
 
   private void executeSqlQuery() throws AdbcException {
     try {
+      if (reader != null) {
+        try {
+          reader.close();
+        } catch (IOException e) {
+          throw new AdbcException(
+              "Failed to close unread result set", e, AdbcStatusCode.IO, null, 
/*vendorCode*/ 0);
+        }
+      }
       if (resultSet != null) {
         resultSet.close();
       }
-      statement =
-          connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, 
ResultSet.CONCUR_READ_ONLY);
-      resultSet = statement.executeQuery(sqlQuery);
+      if (statement instanceof PreparedStatement) {
+        PreparedStatement preparedStatement = (PreparedStatement) statement;
+        if (bindRoot != null) {
+          reader = new JdbcBindReader(allocator, preparedStatement, bindRoot);
+        } else {
+          resultSet = preparedStatement.executeQuery();
+        }
+      } else {
+        if (statement != null) {
+          statement.close();
+        }
+        statement =
+            connection.createStatement(
+                ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+        resultSet = statement.executeQuery(sqlQuery);
+      }
     } catch (SQLException e) {
       throw JdbcDriverUtil.fromSqlException(e);
     }
@@ -224,8 +208,13 @@ public class JdbcStatement implements AdbcStatement {
 
   @Override
   public ArrowReader getArrowReader() throws AdbcException {
+    if (reader != null) {
+      ArrowReader result = reader;
+      reader = null;
+      return result;
+    }
     if (resultSet == null) {
-      throw new IllegalStateException("Must call execute() before 
getArrowIterator()");
+      throw AdbcException.invalidState("[JDBC] Must call execute() before 
getArrowReader()");
     }
     final JdbcArrowReader reader =
         new JdbcArrowReader(allocator, resultSet, /*overrideSchema*/ null);
@@ -234,13 +223,22 @@ public class JdbcStatement implements AdbcStatement {
   }
 
   @Override
-  public void prepare() {
-    throw new UnsupportedOperationException("prepare");
+  public void prepare() throws AdbcException {
+    try {
+      if (resultSet != null) {
+        resultSet.close();
+      }
+      statement =
+          connection.prepareStatement(
+              sqlQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, 
ResultSet.CONCUR_READ_ONLY);
+    } catch (SQLException e) {
+      throw JdbcDriverUtil.fromSqlException(e);
+    }
   }
 
   @Override
   public void close() throws Exception {
-    AutoCloseables.close(resultSet, statement);
+    AutoCloseables.close(reader, resultSet, statement);
   }
 
   private static final class BulkState {
diff --git a/java/driver/testsuite/pom.xml b/java/driver/validation/pom.xml
similarity index 95%
rename from java/driver/testsuite/pom.xml
rename to java/driver/validation/pom.xml
index cf489bc..6128fa7 100644
--- a/java/driver/testsuite/pom.xml
+++ b/java/driver/validation/pom.xml
@@ -17,9 +17,9 @@
     <version>9.0.0-SNAPSHOT</version>
   </parent>
 
-  <artifactId>adbc-driver-testsuite</artifactId>
+  <artifactId>adbc-driver-validation</artifactId>
   <packaging>jar</packaging>
-  <name>Arrow ADBC Driver Test Suite</name>
+  <name>Arrow ADBC Driver Validation Suite</name>
   <description>A reusable ADBC driver compliance/test suite.</description>
 
   <dependencies>
diff --git 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
similarity index 79%
rename from 
java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
index 0ded7d9..6d9b3fc 100644
--- 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionMetadataTest.java
@@ -19,23 +19,22 @@ package org.apache.arrow.adbc.driver.testsuite;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 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.core.AdbcStatement;
 import org.apache.arrow.adbc.core.BulkIngestMode;
 import org.apache.arrow.adbc.core.StandardSchemas;
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.util.Preconditions;
 import org.apache.arrow.vector.FieldVector;
-import org.apache.arrow.vector.IntVector;
 import org.apache.arrow.vector.VarCharVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.complex.ListVector;
@@ -49,28 +48,36 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+/** Common tests of metadata methods of AdbcConnection. */
 public abstract class AbstractConnectionMetadataTest {
+  /** Must be initialized by the subclass. */
+  protected static SqlValidationQuirks quirks;
+
   protected AdbcDatabase database;
   protected AdbcConnection connection;
   protected BufferAllocator allocator;
-
-  protected abstract AdbcDatabase init() throws AdbcException;
+  protected SqlTestUtil util;
+  protected String tableName;
 
   @BeforeEach
   public void beforeEach() throws Exception {
-    database = init();
+    Preconditions.checkNotNull(quirks, "Must initialize quirks in subclass 
with @BeforeAll");
+    database = quirks.initDatabase();
     connection = database.connect();
     allocator = new RootAllocator();
+    util = new SqlTestUtil(quirks);
+    tableName = quirks.caseFoldTableName("foo");
   }
 
   @AfterEach
   public void afterEach() throws Exception {
+    quirks.cleanupTable(tableName);
     AutoCloseables.close(connection, database, allocator);
   }
 
   @Test
   void getObjectsColumns() throws Exception {
-    loadTable();
+    final Schema schema = util.ingestTableIntsStrs(allocator, connection, 
tableName);
     boolean tableFound = false;
     try (final AdbcStatement stmt =
         connection.getObjects(AdbcConnection.GetObjectsDepth.ALL, null, null, 
null, null, null)) {
@@ -91,13 +98,16 @@ public abstract class AbstractConnectionMetadataTest {
             continue;
           }
           final Text tableName = tableNames.getObject(i);
-          if (tableName != null && tableName.toString().equals("FOO")) {
+          if (tableName != null && 
tableName.toString().equalsIgnoreCase(this.tableName)) {
             tableFound = true;
             @SuppressWarnings("unchecked")
             final List<Map<String, ?>> columns = (List<Map<String, ?>>) 
tableColumns.getObject(i);
             assertThat(columns)
                 .extracting("column_name")
-                .containsExactlyInAnyOrder(new Text("INTS"), new Text("STRS"));
+                .containsExactlyInAnyOrderElementsOf(
+                    schema.getFields().stream()
+                        .map(field -> new Text(field.getName()))
+                        .collect(Collectors.toList()));
             
assertThat(columns).extracting("ordinal_position").containsExactlyInAnyOrder(1, 
2);
           }
         }
@@ -108,7 +118,7 @@ public abstract class AbstractConnectionMetadataTest {
 
   @Test
   void getObjectsCatalogs() throws Exception {
-    loadTable();
+    util.ingestTableIntsStrs(allocator, connection, tableName);
     try (final AdbcStatement stmt =
         connection.getObjects(
             AdbcConnection.GetObjectsDepth.CATALOGS, null, null, null, null, 
null)) {
@@ -126,7 +136,7 @@ public abstract class AbstractConnectionMetadataTest {
 
   @Test
   void getObjectsDbSchemas() throws Exception {
-    loadTable();
+    util.ingestTableIntsStrs(allocator, connection, tableName);
     try (final AdbcStatement stmt =
         connection.getObjects(
             AdbcConnection.GetObjectsDepth.DB_SCHEMAS, null, null, null, null, 
null)) {
@@ -141,7 +151,7 @@ public abstract class AbstractConnectionMetadataTest {
 
   @Test
   void getObjectsTables() throws Exception {
-    loadTable();
+    util.ingestTableIntsStrs(allocator, connection, tableName);
     try (final AdbcStatement stmt =
         connection.getObjects(
             AdbcConnection.GetObjectsDepth.TABLES, null, null, null, null, 
null)) {
@@ -156,7 +166,7 @@ public abstract class AbstractConnectionMetadataTest {
         final StructVector tables = (StructVector) 
dbSchemaTables.getDataVector();
         final VarCharVector tableNames = (VarCharVector) 
tables.getVectorById(0);
         assertThat(IntStream.range(0, 
tableNames.getValueCount()).mapToObj(tableNames::getObject))
-            .contains(new Text("FOO"));
+            .containsAnyOf(new Text(quirks.caseFoldTableName(tableName)));
       }
     }
   }
@@ -166,15 +176,16 @@ public abstract class AbstractConnectionMetadataTest {
     final Schema schema =
         new Schema(
             Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
+                Field.nullable(
+                    quirks.caseFoldColumnName("INTS"), new ArrowType.Int(32, 
/*signed=*/ true)),
+                Field.nullable(quirks.caseFoldColumnName("STRS"), new 
ArrowType.Utf8())));
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
     }
-    assertThat(connection.getTableSchema(/*catalog*/ null, /*dbSchema*/ null, 
"FOO"))
+    assertThat(connection.getTableSchema(/*catalog*/ null, /*dbSchema*/ null, 
tableName))
         .isEqualTo(schema);
   }
 
@@ -196,35 +207,4 @@ public abstract class AbstractConnectionMetadataTest {
       }
     }
   }
-
-  void loadTable() throws Exception {
-    final Schema schema =
-        new Schema(
-            Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
-    try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      final IntVector ints = (IntVector) root.getVector(0);
-      final VarCharVector strs = (VarCharVector) root.getVector(1);
-
-      ints.allocateNew(4);
-      ints.setSafe(0, 0);
-      ints.setSafe(1, 1);
-      ints.setSafe(2, 2);
-      ints.setNull(3);
-      strs.allocateNew(4);
-      strs.setNull(0);
-      strs.setSafe(1, "foo".getBytes(StandardCharsets.UTF_8));
-      strs.setSafe(2, "".getBytes(StandardCharsets.UTF_8));
-      strs.setSafe(3, "asdf".getBytes(StandardCharsets.UTF_8));
-      root.setRowCount(4);
-
-      // TODO: XXX: need a "quirks" system to handle idiosyncracies. For 
example: Derby forces table
-      // names to uppercase, but does not do case folding in all places.
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
-        stmt.bind(root);
-        stmt.execute();
-      }
-    }
-  }
 }
diff --git 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
similarity index 92%
rename from 
java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
index fb22e8a..4b6944e 100644
--- 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractConnectionTest.java
@@ -19,21 +19,21 @@ package org.apache.arrow.adbc.driver.testsuite;
 
 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.util.AutoCloseables;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 public abstract class AbstractConnectionTest {
+  /** Must be initialized by the subclass. */
+  protected static SqlValidationQuirks quirks;
+
   protected AdbcDatabase database;
   protected AdbcConnection connection;
 
-  protected abstract AdbcDatabase init() throws AdbcException;
-
   @BeforeEach
   public void beforeEach() throws Exception {
-    database = init();
+    database = quirks.initDatabase();
     connection = database.connect();
   }
 
diff --git 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
similarity index 55%
rename from 
java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
index a47a339..947cd3d 100644
--- 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractStatementTest.java
@@ -33,6 +33,7 @@ import org.apache.arrow.adbc.core.BulkIngestMode;
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.util.Preconditions;
 import org.apache.arrow.vector.IntVector;
 import org.apache.arrow.vector.VarCharVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
@@ -40,36 +41,46 @@ import org.apache.arrow.vector.ipc.ArrowReader;
 import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.arrow.vector.util.Text;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 public abstract class AbstractStatementTest {
+  /** Must be initialized by the subclass. */
+  protected static SqlValidationQuirks quirks;
+
   protected AdbcDatabase database;
   protected AdbcConnection connection;
   protected BufferAllocator allocator;
-
-  protected abstract AdbcDatabase init() throws AdbcException;
+  protected SqlTestUtil util;
+  protected String tableName;
+  protected Schema schema;
 
   @BeforeEach
   public void beforeEach() throws Exception {
-    database = init();
+    Preconditions.checkNotNull(quirks, "Must initialize quirks in subclass 
with @BeforeAll");
+    database = quirks.initDatabase();
     connection = database.connect();
     allocator = new RootAllocator();
+    util = new SqlTestUtil(quirks);
+    tableName = quirks.caseFoldTableName("bulktable");
+    schema =
+        new Schema(
+            Arrays.asList(
+                Field.nullable(
+                    quirks.caseFoldColumnName("ints"), new ArrowType.Int(32, 
/*signed=*/ true)),
+                Field.nullable(quirks.caseFoldColumnName("strs"), new 
ArrowType.Utf8())));
   }
 
   @AfterEach
   public void afterEach() throws Exception {
+    quirks.cleanupTable(tableName);
     AutoCloseables.close(connection, database, allocator);
   }
 
   @Test
   public void bulkInsertAppend() throws Exception {
-    final Schema schema =
-        new Schema(
-            Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
       final IntVector ints = (IntVector) root.getVector(0);
       final VarCharVector strs = (VarCharVector) root.getVector(1);
@@ -86,14 +97,12 @@ public abstract class AbstractStatementTest {
       strs.setSafe(3, "asdf".getBytes(StandardCharsets.UTF_8));
       root.setRowCount(4);
 
-      // TODO: XXX: need a "quirks" system to handle idiosyncracies. For 
example: Derby forces table
-      // names to uppercase, but does not do case folding in all places.
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM foo");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         try (ArrowReader arrowReader = stmt.executeQuery()) {
           assertThat(arrowReader.loadNextBatch()).isTrue();
           assertRoot(arrowReader.getVectorSchemaRoot()).isEqualTo(root);
@@ -101,12 +110,12 @@ public abstract class AbstractStatementTest {
       }
 
       // Append
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.APPEND)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.APPEND)) {
         stmt.bind(root);
         stmt.execute();
       }
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM FOO");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         try (ArrowReader arrowReader = stmt.executeQuery()) {
           assertThat(arrowReader.loadNextBatch()).isTrue();
           root.setRowCount(8);
@@ -126,65 +135,108 @@ public abstract class AbstractStatementTest {
 
   @Test
   public void bulkIngestAppendConflict() throws Exception {
-    final Schema schema =
-        new Schema(
-            Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
     final Schema schema2 =
         new Schema(
             Collections.singletonList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true))));
+                Field.nullable(quirks.caseFoldColumnName("ints"), new 
ArrowType.Utf8())));
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
     }
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema2, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.APPEND)) {
+      root.setRowCount(1);
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.APPEND)) {
         stmt.bind(root);
-        final AdbcException e = assertThrows(AdbcException.class, 
stmt::execute);
-        assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.ALREADY_EXISTS);
+        assertThrows(AdbcException.class, stmt::execute);
       }
     }
   }
 
   @Test
   public void bulkIngestAppendNotFound() throws Exception {
-    final Schema schema =
-        new Schema(
-            Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.APPEND)) {
+      root.setRowCount(1);
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.APPEND)) {
         stmt.bind(root);
         final AdbcException e = assertThrows(AdbcException.class, 
stmt::execute);
-        assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+        assertThat(e.getStatus()).describedAs("%s", 
e).isEqualTo(AdbcStatusCode.NOT_FOUND);
       }
     }
   }
 
   @Test
   public void bulkIngestCreateConflict() throws Exception {
-    final Schema schema =
-        new Schema(
-            Arrays.asList(
-                Field.nullable("INTS", new ArrowType.Int(32, /*signed=*/ 
true)),
-                Field.nullable("STRS", new ArrowType.Utf8())));
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
     }
     try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
-      try (final AdbcStatement stmt = connection.bulkIngest("FOO", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         final AdbcException e = assertThrows(AdbcException.class, 
stmt::execute);
         assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.ALREADY_EXISTS);
       }
     }
   }
+
+  @Test
+  public void prepareQuery() throws Exception {
+    final Schema expectedSchema = util.ingestTableIntsStrs(allocator, 
connection, tableName);
+    try (final AdbcStatement stmt = connection.createStatement()) {
+      stmt.setSqlQuery("SELECT * FROM " + tableName);
+      stmt.prepare();
+      try (final ArrowReader reader = stmt.executeQuery()) {
+        
assertThat(reader.getVectorSchemaRoot().getSchema()).isEqualTo(expectedSchema);
+        assertThat(reader.loadNextBatch()).isTrue();
+        assertThat(reader.getVectorSchemaRoot().getRowCount()).isEqualTo(4);
+        assertThat(reader.loadNextBatch()).isFalse();
+      }
+    }
+  }
+
+  @Test
+  public void prepareQueryWithParameters() throws Exception {
+    final Schema expectedSchema = util.ingestTableIntsStrs(allocator, 
connection, tableName);
+    final Schema paramsSchema =
+        new 
Schema(Collections.singletonList(expectedSchema.getFields().get(0)));
+    try (final AdbcStatement stmt = connection.createStatement();
+        final VectorSchemaRoot params = VectorSchemaRoot.create(paramsSchema, 
allocator)) {
+      stmt.setSqlQuery(String.format("SELECT * FROM %s WHERE INTS = ?", 
tableName));
+      stmt.prepare();
+      stmt.bind(params);
+      IntVector param0 = (IntVector) params.getVector(0);
+      param0.setSafe(0, 1);
+      param0.setSafe(1, 2);
+      params.setRowCount(2);
+      try (final ArrowReader reader = stmt.executeQuery()) {
+        VectorSchemaRoot root = reader.getVectorSchemaRoot();
+        assertThat(root.getSchema()).isEqualTo(expectedSchema);
+        assertThat(reader.loadNextBatch()).isTrue();
+        assertThat(root.getRowCount()).isEqualTo(1);
+        assertThat(root.getVector(1).getObject(0)).isEqualTo(new Text("foo"));
+
+        assertThat(reader.loadNextBatch()).isTrue();
+        assertThat(root.getRowCount()).isEqualTo(1);
+        assertThat(root.getVector(1).getObject(0)).isEqualTo(new Text(""));
+
+        assertThat(reader.loadNextBatch()).isFalse();
+      }
+
+      param0.setSafe(0, 0);
+      params.setRowCount(1);
+      try (final ArrowReader reader = stmt.executeQuery()) {
+        VectorSchemaRoot root = reader.getVectorSchemaRoot();
+        assertThat(root.getSchema()).isEqualTo(expectedSchema);
+        assertThat(reader.loadNextBatch()).isTrue();
+        assertThat(root.getRowCount()).isEqualTo(1);
+        assertThat(root.getVector(1).getObject(0)).isNull();
+
+        assertThat(reader.loadNextBatch()).isFalse();
+      }
+    }
+  }
 }
diff --git 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
similarity index 79%
rename from 
java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
index cc2908e..208d656 100644
--- 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/AbstractTransactionTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.arrow.adbc.driver.testsuite;
 
+import static 
org.apache.arrow.adbc.driver.testsuite.ArrowAssertions.assertAdbcException;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -25,10 +26,12 @@ 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.core.AdbcStatement;
+import org.apache.arrow.adbc.core.AdbcStatusCode;
 import org.apache.arrow.adbc.core.BulkIngestMode;
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.util.Preconditions;
 import org.apache.arrow.vector.IntVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.types.pojo.ArrowType;
@@ -39,15 +42,17 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 public abstract class AbstractTransactionTest {
+  /** Must be initialized by the subclass. */
+  protected static SqlValidationQuirks quirks;
+
   protected BufferAllocator allocator;
   protected AdbcDatabase database;
   protected AdbcConnection connection;
 
-  protected abstract AdbcDatabase init() throws AdbcException;
-
   @BeforeEach
   public void beforeEach() throws Exception {
-    database = init();
+    Preconditions.checkNotNull(quirks, "Must initialize quirks in subclass 
with @BeforeAll");
+    database = quirks.initDatabase();
     connection = database.connect();
     allocator = new RootAllocator();
   }
@@ -59,8 +64,10 @@ public abstract class AbstractTransactionTest {
 
   @Test
   void autoCommitByDefault() throws Exception {
-    assertThrows(IllegalStateException.class, () -> connection.commit());
-    assertThrows(IllegalStateException.class, () -> connection.rollback());
+    assertAdbcException(assertThrows(AdbcException.class, () -> 
connection.commit()))
+        .isStatus(AdbcStatusCode.INVALID_STATE);
+    assertAdbcException(assertThrows(AdbcException.class, () -> 
connection.rollback()))
+        .isStatus(AdbcStatusCode.INVALID_STATE);
     assertThat(connection.getAutoCommit()).isTrue();
   }
 
@@ -102,6 +109,8 @@ public abstract class AbstractTransactionTest {
         assertThrows(AdbcException.class, stmt::execute);
       }
     }
+
+    quirks.cleanupTable("foo");
   }
 
   @Test
@@ -110,6 +119,7 @@ public abstract class AbstractTransactionTest {
         new Schema(
             Collections.singletonList(
                 Field.nullable("ints", new ArrowType.Int(32, /*signed=*/ 
true))));
+    final String tableName = quirks.caseFoldTableName("temptable");
 
     connection.setAutoCommit(false);
     try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
@@ -117,21 +127,23 @@ public abstract class AbstractTransactionTest {
       ints.setSafe(0, 1);
       ints.setSafe(1, 2);
       root.setRowCount(2);
-      try (final AdbcStatement stmt = connection.bulkIngest("foo", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM foo");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         stmt.execute();
       }
       connection.commit();
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM foo");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         stmt.execute();
       }
       connection.commit();
     }
+
+    quirks.cleanupTable(tableName);
   }
 
   @Test
@@ -140,6 +152,7 @@ public abstract class AbstractTransactionTest {
         new Schema(
             Collections.singletonList(
                 Field.nullable("ints", new ArrowType.Int(32, /*signed=*/ 
true))));
+    final String tableName = quirks.caseFoldTableName("temptable");
 
     connection.setAutoCommit(false);
     try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
@@ -147,19 +160,21 @@ public abstract class AbstractTransactionTest {
       ints.setSafe(0, 1);
       ints.setSafe(1, 2);
       root.setRowCount(2);
-      try (final AdbcStatement stmt = connection.bulkIngest("foo", 
BulkIngestMode.CREATE)) {
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
         stmt.bind(root);
         stmt.execute();
       }
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM foo");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         stmt.execute();
       }
       connection.setAutoCommit(true);
       try (final AdbcStatement stmt = connection.createStatement()) {
-        stmt.setSqlQuery("SELECT * FROM foo");
+        stmt.setSqlQuery("SELECT * FROM " + tableName);
         stmt.execute();
       }
     }
+
+    quirks.cleanupTable(tableName);
   }
 }
diff --git 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
similarity index 76%
rename from 
java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
index 2b3f280..e357824 100644
--- 
a/java/driver/testsuite/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowAssertions.java
@@ -17,6 +17,8 @@
 
 package org.apache.arrow.adbc.driver.testsuite;
 
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcStatusCode;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.compare.VectorEqualsVisitor;
@@ -24,11 +26,36 @@ import org.assertj.core.api.AbstractAssert;
 
 /** AssertJ assertions for Arrow. */
 public final class ArrowAssertions {
+  /** Assert on a {@link AdbcException}. */
+  public static AdbcExceptionAssert assertAdbcException(AdbcException actual) {
+    return new AdbcExceptionAssert(actual);
+  }
+
   /** Assert on a {@link VectorSchemaRoot}. */
   public static VectorSchemaRootAssert assertRoot(VectorSchemaRoot actual) {
     return new VectorSchemaRootAssert(actual);
   }
 
+  public static final class AdbcExceptionAssert
+      extends AbstractAssert<AdbcExceptionAssert, AdbcException> {
+    AdbcExceptionAssert(AdbcException e) {
+      super(e, AdbcExceptionAssert.class);
+    }
+
+    public AdbcExceptionAssert isStatus(AdbcStatusCode status) {
+      if (actual.getStatus() != status) {
+        throw failureWithActualExpected(
+            actual.getStatus(),
+            status,
+            "Expected status %s but got %s:\n%s",
+            status,
+            actual.getStatus(),
+            actual);
+      }
+      return this;
+    }
+  }
+
   public static final class VectorSchemaRootAssert
       extends AbstractAssert<VectorSchemaRootAssert, VectorSchemaRoot> {
     VectorSchemaRootAssert(VectorSchemaRoot vectorSchemaRoot) {
diff --git 
a/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlTestUtil.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlTestUtil.java
new file mode 100644
index 0000000..9248720
--- /dev/null
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlTestUtil.java
@@ -0,0 +1,72 @@
+/*
+ * 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.testsuite;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.apache.arrow.adbc.core.AdbcConnection;
+import org.apache.arrow.adbc.core.AdbcStatement;
+import org.apache.arrow.adbc.core.BulkIngestMode;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+
+public final class SqlTestUtil {
+  private final SqlValidationQuirks quirks;
+
+  public SqlTestUtil(SqlValidationQuirks quirks) {
+    this.quirks = quirks;
+  }
+
+  /** Load a simple table with two columns. */
+  public Schema ingestTableIntsStrs(
+      BufferAllocator allocator, AdbcConnection connection, String tableName) 
throws Exception {
+    tableName = quirks.caseFoldTableName(tableName);
+    final Schema schema =
+        new Schema(
+            Arrays.asList(
+                Field.nullable(
+                    quirks.caseFoldColumnName("INTS"), new ArrowType.Int(32, 
/*signed=*/ true)),
+                Field.nullable(quirks.caseFoldColumnName("STRS"), new 
ArrowType.Utf8())));
+    try (final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
+      final IntVector ints = (IntVector) root.getVector(0);
+      final VarCharVector strs = (VarCharVector) root.getVector(1);
+
+      ints.allocateNew(4);
+      ints.setSafe(0, 0);
+      ints.setSafe(1, 1);
+      ints.setSafe(2, 2);
+      ints.setNull(3);
+      strs.allocateNew(4);
+      strs.setNull(0);
+      strs.setSafe(1, "foo".getBytes(StandardCharsets.UTF_8));
+      strs.setSafe(2, "".getBytes(StandardCharsets.UTF_8));
+      strs.setSafe(3, "asdf".getBytes(StandardCharsets.UTF_8));
+      root.setRowCount(4);
+      try (final AdbcStatement stmt = connection.bulkIngest(tableName, 
BulkIngestMode.CREATE)) {
+        stmt.bind(root);
+        stmt.execute();
+      }
+    }
+    return schema;
+  }
+}
diff --git 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
similarity index 62%
rename from 
java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
rename to 
java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
index 72a28f3..9ad6883 100644
--- 
a/java/driver/jdbc/src/test/java/org/apache/arrow/adbc/driver/jdbc/JdbcStatementTest.java
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/SqlValidationQuirks.java
@@ -15,23 +15,24 @@
  * limitations under the License.
  */
 
-package org.apache.arrow.adbc.driver.jdbc;
+package org.apache.arrow.adbc.driver.testsuite;
 
-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.AdbcException;
-import org.apache.arrow.adbc.driver.testsuite.AbstractStatementTest;
-import org.junit.jupiter.api.io.TempDir;
 
-class JdbcStatementTest extends AbstractStatementTest {
-  @TempDir Path tempDir;
+/** Account for driver/vendor-specific quirks in implementing validation 
tests. */
+public abstract class SqlValidationQuirks {
+  public abstract AdbcDatabase initDatabase() throws AdbcException;
 
-  @Override
-  protected AdbcDatabase init() throws AdbcException {
-    final Map<String, String> parameters = new HashMap<>();
-    parameters.put("path", tempDir.toString() + "/db;create=true");
-    return JdbcDriver.INSTANCE.open(parameters);
+  public void cleanupTable(String name) throws Exception {}
+
+  /** Normalize a table name. */
+  public String caseFoldTableName(String name) {
+    return name;
+  }
+
+  /** Normalize a column name. */
+  public String caseFoldColumnName(String name) {
+    return name;
   }
 }
diff --git a/java/pom.xml b/java/pom.xml
index b74158a..11987e1 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -79,7 +79,9 @@
     <module>core</module>
     <module>driver/jdbc</module>
     <module>driver/jdbc-util</module>
-    <module>driver/testsuite</module>
+    <module>driver/jdbc-validation-derby</module>
+    <module>driver/jdbc-validation-postgresql</module>
+    <module>driver/validation</module>
     <module>driver-manager</module>
   </modules>
 
@@ -119,7 +121,7 @@
       </dependency>
       <dependency>
         <groupId>org.apache.arrow.adbc</groupId>
-        <artifactId>adbc-driver-testsuite</artifactId>
+        <artifactId>adbc-driver-validation</artifactId>
         <version>${adbc.version}</version>
       </dependency>
       <dependency>

Reply via email to