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 6c80fa306 test(java/driver/jni): add tests with drivers besides SQLite 
(#4235)
6c80fa306 is described below

commit 6c80fa306fbe21448903aaaf9a48f6db15457dfd
Author: David Li <[email protected]>
AuthorDate: Thu Apr 23 07:47:17 2026 +0900

    test(java/driver/jni): add tests with drivers besides SQLite (#4235)
    
    Closes #4230.
---
 .env                                               |   1 +
 .github/workflows/java.yml                         |  30 +-
 CONTRIBUTING.md                                    |   1 +
 .../pom.xml                                        |   2 +-
 .../jni/JniSqliteConnectionMetadataTest.java       |   0
 .../adbc/driver/jni/JniSqliteConnectionTest.java   |  12 +
 .../arrow/adbc/driver/jni/JniSqliteQuirks.java     |   0
 .../adbc/driver/jni/JniSqliteStatementTest.java    |   0
 .../adbc/driver/jni/JniSqliteTransactionTest.java  |   0
 .../adbc/driver/jni/PostgresIntegrationTest.java   | 534 +++++++++++++++++++++
 .../adbc/driver/jni/SqlServerIntegrationTest.java  | 527 ++++++++++++++++++++
 .../driver/testsuite/AbstractConnectionTest.java   |   8 +-
 .../arrow/adbc/driver/testsuite/ArrowToJava.java   | 287 +++++++++++
 java/pom.xml                                       |   2 +-
 14 files changed, 1395 insertions(+), 9 deletions(-)

diff --git a/.env b/.env
index 607f2f52c..5a328374d 100644
--- a/.env
+++ b/.env
@@ -55,6 +55,7 @@ ADBC_JDBC_POSTGRESQL_URL=localhost:5432/postgres
 ADBC_JDBC_POSTGRESQL_USER=postgres
 ADBC_JDBC_POSTGRESQL_PASSWORD=password
 ADBC_JDBC_POSTGRESQL_DATABASE=postgres
+ADBC_MSSQL_TEST_URI="sqlserver://SA:Password1!@localhost:1433"
 
ADBC_POSTGRESQL_TEST_URI="postgresql://localhost:5432/postgres?user=postgres&password=password"
 ADBC_SQLITE_FLIGHTSQL_URI=grpc+tcp://localhost:8080
 ADBC_TEST_FLIGHTSQL_URI=grpc+tls://localhost:41414
diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml
index df5173bc7..541a6eb82 100644
--- a/.github/workflows/java.yml
+++ b/.github/workflows/java.yml
@@ -219,7 +219,7 @@ jobs:
           cache: "maven"
           distribution: "temurin"
           java-version: 11
-      - name: Build/Test
+      - name: Build
         run: |
           set -x
           pushd artifacts
@@ -228,10 +228,34 @@ jobs:
           done
           popd
           cp -r artifacts/*/jni/adbc_driver_jni 
java/driver/jni/src/main/resources
+          env BUILD_JNI=ON ./ci/scripts/java_build.sh $(pwd)
+
+      - name: Start Dependencies
+        if: matrix.os == 'Linux' && matrix.arch == 'amd64'
+        run: |
+          docker compose up --detach --wait mssql-test postgres-test
+          cat .env | grep -v -e '^#' | grep -e '^ADBC_' | awk NF | sed 
's/"//g' | tee -a $GITHUB_ENV
+
+      - name: Download thirdparty driver
+        if: matrix.os == 'Linux' && matrix.arch == 'amd64'
+        run: |
+          wget 
https://dbc-cdn.columnar.tech/mssql/v1.3.1/mssql_linux_amd64_v1.3.1.tar.gz
+          echo 
"e6723cf417403f313fb75c1ac03aea9b9ff857d4a947608c8ae44eacc1aa22b3  
mssql_linux_amd64_v1.3.1.tar.gz" > mssql_linux_amd64_v1.3.1.tar.gz.sha256
+          sha256sum -c mssql_linux_amd64_v1.3.1.tar.gz.sha256
+
+          tar xvf mssql_linux_amd64_v1.3.1.tar.gz
+          mkdir -p ~/.config/adbc/drivers/
+          mv libadbc_driver_mssql.so ~/.config/adbc/drivers/
+          echo "manifest_version = 1" > ~/.config/adbc/drivers/mssql.toml
+          echo "[Driver]" >> ~/.config/adbc/drivers/mssql.toml
+          echo "shared = '$HOME/.config/adbc/drivers/libadbc_driver_mssql.so'" 
>> ~/.config/adbc/drivers/mssql.toml
+          cat ~/.config/adbc/drivers/mssql.toml
+
+      - name: Test
+        run: |
           for driver in artifacts/*/driver; do
             export LD_LIBRARY_PATH=$(pwd)/$driver:${LD_LIBRARY_PATH:-}
             export DYLD_LIBRARY_PATH=$(pwd)/$driver:${DYLD_LIBRARY_PATH:-}
           done
-          env BUILD_JNI=ON ./ci/scripts/java_build.sh $(pwd)
           cd java
-          mvn -B -Pjni test -pl :adbc-driver-jni
+          mvn -B -Pjni test -pl :adbc-driver-jni -pl 
:adbc-driver-jni-validation
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f177e04fc..bc4ce23f1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -361,6 +361,7 @@ export ADBC_USE_ASAN=OFF
 export ADBC_USE_UBSAN=OFF
 export BUILD_ALL=OFF
 export BUILD_DRIVER_MANAGER=ON
+export BUILD_DRIVER_POSTGRESQL=ON
 export BUILD_DRIVER_SQLITE=ON
 ./ci/scripts/cpp_build.sh $(pwd) $(pwd)/build $(pwd)/local
 
diff --git a/java/driver/jni-validation-sqlite/pom.xml 
b/java/driver/jni-validation/pom.xml
similarity index 97%
rename from java/driver/jni-validation-sqlite/pom.xml
rename to java/driver/jni-validation/pom.xml
index 20d121150..148f84785 100644
--- a/java/driver/jni-validation-sqlite/pom.xml
+++ b/java/driver/jni-validation/pom.xml
@@ -26,7 +26,7 @@
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
-  <artifactId>adbc-driver-jni-validation-sqlite</artifactId>
+  <artifactId>adbc-driver-jni-validation</artifactId>
   <packaging>jar</packaging>
   <name>Arrow ADBC Driver JNI Validation with SQLite</name>
   <description>Tests validating the JNI driver against SQLite.</description>
diff --git 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionMetadataTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionMetadataTest.java
similarity index 100%
rename from 
java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionMetadataTest.java
rename to 
java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionMetadataTest.java
diff --git 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
similarity index 84%
rename from 
java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
rename to 
java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
index e15182bca..6b1150716 100644
--- 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
+++ 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteConnectionTest.java
@@ -19,10 +19,22 @@ package org.apache.arrow.adbc.driver.jni;
 
 import org.apache.arrow.adbc.driver.testsuite.AbstractConnectionTest;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 
 public class JniSqliteConnectionTest extends AbstractConnectionTest {
   @BeforeAll
   static void beforeAll() {
     quirks = new JniSqliteQuirks();
   }
+
+  @Test
+  @Disabled
+  @Override
+  protected void readOnly() {}
+
+  @Test
+  @Disabled
+  @Override
+  protected void isolationLevel() {}
 }
diff --git 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteQuirks.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteQuirks.java
similarity index 100%
rename from 
java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteQuirks.java
rename to 
java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteQuirks.java
diff --git 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteStatementTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteStatementTest.java
similarity index 100%
rename from 
java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteStatementTest.java
rename to 
java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteStatementTest.java
diff --git 
a/java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteTransactionTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteTransactionTest.java
similarity index 100%
rename from 
java/driver/jni-validation-sqlite/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteTransactionTest.java
rename to 
java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/JniSqliteTransactionTest.java
diff --git 
a/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/PostgresIntegrationTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/PostgresIntegrationTest.java
new file mode 100644
index 000000000..b805a420d
--- /dev/null
+++ 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/PostgresIntegrationTest.java
@@ -0,0 +1,534 @@
+/*
+ * 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.jni;
+
+import static 
org.apache.arrow.adbc.driver.testsuite.ArrowAssertions.assertSchema;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+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.AdbcDriver;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcInfoCode;
+import org.apache.arrow.adbc.core.AdbcOptions;
+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.adbc.core.TypedKey;
+import org.apache.arrow.adbc.driver.testsuite.ArrowToJava;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.Types;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PostgresIntegrationTest {
+  public static final String URI_ENV = "ADBC_POSTGRESQL_TEST_URI";
+  static String URI = System.getenv(URI_ENV);
+
+  BufferAllocator allocator;
+  JniDriver driver;
+  AdbcDatabase db;
+  AdbcConnection conn;
+
+  @BeforeAll
+  static void beforeAll() {
+    Assumptions.assumeFalse(
+        URI == null || URI.isEmpty(),
+        String.format("Must set %s to run Postgres integration tests", 
URI_ENV));
+  }
+
+  @BeforeEach
+  void beforeEach() throws Exception {
+    System.err.println("Connecting to PostgreSQL with URI: " + URI);
+    allocator = new RootAllocator();
+    driver = new JniDriver(allocator);
+    Map<String, Object> parameters = new HashMap<>();
+    JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_postgresql");
+    AdbcDriver.PARAM_URI.set(parameters, URI);
+    db = driver.open(parameters);
+    conn = db.connect();
+  }
+
+  @AfterEach
+  void afterEach() throws Exception {
+    conn.close();
+    db.close();
+    allocator.close();
+  }
+
+  @Test
+  void simple() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT 1 + 1 AS sum");
+      try (var reader = stmt.executeQuery()) {
+        assertThat(reader.getReader().loadNextBatch()).isTrue();
+        
assertThat(reader.getReader().getVectorSchemaRoot().getVector("sum").getObject(0))
+            .isEqualTo(2);
+      }
+    }
+  }
+
+  @Test
+  void options() throws Exception {
+    testOptions(db);
+    testOptions(conn);
+    try (AdbcStatement stmt = conn.createStatement()) {
+      testOptions(stmt);
+    }
+  }
+
+  @Test
+  void connectionGetInfo() throws Exception {
+    try (ArrowReader reader = conn.getInfo()) {
+      assertThat(reader.loadNextBatch()).isTrue();
+
+      var codes = 
ArrowToJava.toIntegers(reader.getVectorSchemaRoot().getVector("info_name"));
+      var values = 
ArrowToJava.toObjects(reader.getVectorSchemaRoot().getVector("info_value"));
+      var infos =
+          IntStream.range(0, codes.size())
+              .mapToObj(i -> Map.entry(codes.get(i), values.get(i)))
+              .collect(Collectors.toMap(Map.Entry::getKey, 
Map.Entry::getValue));
+
+      assertThat(codes).contains(AdbcInfoCode.VENDOR_NAME.getValue());
+      assertThat(codes).contains(AdbcInfoCode.VENDOR_VERSION.getValue());
+      assertThat(codes).contains(AdbcInfoCode.DRIVER_NAME.getValue());
+      assertThat(codes).contains(AdbcInfoCode.DRIVER_VERSION.getValue());
+      
assertThat(infos.get(AdbcInfoCode.VENDOR_NAME.getValue())).isEqualTo("PostgreSQL");
+      assertThat(infos.get(AdbcInfoCode.DRIVER_NAME.getValue()))
+          .isEqualTo("ADBC PostgreSQL Driver");
+
+      assertThat(reader.loadNextBatch()).isFalse();
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void connectionGetObjects() throws Exception {
+    runSetup(
+        "DROP SCHEMA IF EXISTS test_schema CASCADE",
+        "DROP TABLE IF EXISTS public.foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE public.foobar (b TEXT)");
+
+    try (var reader =
+        conn.getObjects(
+            AdbcConnection.GetObjectsDepth.ALL, null, "test_schema", null, 
null, null)) {
+      var values = ArrowToJava.toObjects(reader, "catalog_db_schemas");
+      var schemas = (List<?>) values.get(0);
+      var schema = (Map<String, ?>) schemas.get(0);
+      
assertThat(schema).extractingByKey("db_schema_name").isEqualTo("test_schema");
+      var tables = (List<?>) schema.get("db_schema_tables");
+      assertThat(tables).size().isEqualTo(1);
+      var table = (Map<String, ?>) tables.get(0);
+      assertThat(table).extractingByKey("table_name").isEqualTo("foobar");
+      assertThat(table).extractingByKey("table_type").isEqualTo("table");
+      var columns = (List<Map<String, ?>>) table.get("table_columns");
+      
assertThat(columns).singleElement().extracting("column_name").isEqualTo("a");
+    }
+  }
+
+  @Test
+  void connectionGetTableSchema() throws Exception {
+    runSetup(
+        "DROP SCHEMA IF EXISTS test_schema CASCADE",
+        "DROP TABLE IF EXISTS public.foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE public.foobar (b TEXT)");
+
+    assertSchema(conn.getTableSchema(null, "test_schema", "foobar"))
+        .isEqualTo(new Schema(List.of(Field.nullable("a", 
Types.MinorType.INT.getType()))));
+    assertSchema(conn.getTableSchema(null, null, "foobar"))
+        .isEqualTo(new Schema(List.of(Field.nullable("b", 
Types.MinorType.VARCHAR.getType()))));
+  }
+
+  @Test
+  void connectionTableTypes() throws Exception {
+    try (ArrowReader reader = conn.getTableTypes()) {
+      List<String> tableTypes = ArrowToJava.toStrings(reader, "table_type");
+      assertThat(tableTypes)
+          .containsExactlyInAnyOrder(
+              "partitioned_table",
+              "foreign_table",
+              "toast_table",
+              "materialized_view",
+              "view",
+              "table");
+    }
+  }
+
+  @Test
+  void connectionReadOnly() {
+    AdbcException e = assertThrows(AdbcException.class, () -> 
conn.getReadOnly());
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+
+    e = assertThrows(AdbcException.class, () -> conn.setReadOnly(true));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+  }
+
+  @Test
+  void bulkIngest() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar");
+
+    final Schema schema =
+        new Schema(
+            List.of(
+                Field.nullable("index", Types.MinorType.INT.getType()),
+                Field.nullable("value", Types.MinorType.VARCHAR.getType())));
+    try (VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      IntVector iv = (IntVector) vsr.getVector(0);
+      VarCharVector vv = (VarCharVector) vsr.getVector(1);
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.CREATE)) {
+        iv.setSafe(0, 1);
+        iv.setSafe(1, 2);
+        vv.setNull(0);
+        vv.setSafe(1, "foobar".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(2);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(2);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.REPLACE)) {
+        iv.setSafe(0, 1);
+        iv.setSafe(1, 2);
+        vv.setSafe(0, "".getBytes(StandardCharsets.UTF_8));
+        vv.setSafe(1, "testtest".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(2);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(2);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.APPEND)) {
+        iv.setSafe(0, 3);
+        iv.setSafe(1, 4);
+        iv.setSafe(2, 5);
+        vv.setSafe(0, "spam".getBytes(StandardCharsets.UTF_8));
+        vv.setNull(1);
+        vv.setSafe(2, "eggs".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(3);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(3);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.CREATE_APPEND)) {
+        iv.setSafe(0, 6);
+        iv.setSafe(1, 7);
+        iv.setSafe(2, 8);
+        vv.setSafe(0, "spam".getBytes(StandardCharsets.UTF_8));
+        vv.setNull(1);
+        vv.setNull(2);
+        vsr.setRowCount(3);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(3);
+      }
+    }
+
+    try (AdbcStatement stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT value FROM foobar ORDER BY index");
+      try (var result = stmt.executeQuery()) {
+        var values = ArrowToJava.toStrings(result.getReader(), "value");
+        assertThat(values)
+            .containsExactly("", "testtest", "spam", null, "eggs", "spam", 
null, null);
+      }
+    }
+  }
+
+  @Test
+  void currentCatalogSchema() throws Exception {
+    runSetup(
+        "DROP SCHEMA IF EXISTS test_schema CASCADE",
+        "DROP TABLE IF EXISTS public.foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE public.foobar (b TEXT)");
+    assertThat(conn.getCurrentCatalog()).isEqualTo("postgres");
+    assertThat(conn.getCurrentDbSchema()).isEqualTo("public");
+
+    AdbcException e = assertThrows(AdbcException.class, () -> 
conn.setCurrentCatalog("foobar"));
+    assertThat(e).hasMessageContaining("Unknown option");
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+
+    conn.setCurrentDbSchema("test_schema");
+    try (AdbcStatement stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT * FROM foobar");
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        assertSchema(result.getReader().getVectorSchemaRoot().getSchema())
+            .isEqualTo(new Schema(List.of(Field.nullable("a", 
Types.MinorType.INT.getType()))));
+      }
+    }
+  }
+
+  @Test
+  void transactions() throws Exception {}
+
+  @Test
+  void selectQuery() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT 42 AS THEANSWER, 'meaning of life' AS 
THEQUESTION");
+
+      assertSchema(stmt.executeSchema())
+          .isEqualTo(
+              new Schema(
+                  List.of(
+                      Field.nullable("theanswer", 
Types.MinorType.INT.getType()),
+                      Field.nullable("thequestion", 
Types.MinorType.VARCHAR.getType()))));
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        assertThat(result.getReader().loadNextBatch()).isTrue();
+        assertThat(result.getReader().loadNextBatch()).isFalse();
+      }
+    }
+    // TODO(https://github.com/apache/arrow-adbc/issues/4239): test get 
parameter schema
+  }
+
+  @Test
+  void updateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES (1), (2)");
+      AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  void prepare() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      assertThat(assertThrows(AdbcException.class, stmt::prepare))
+          .hasMessageContaining("Must SetSqlQuery() before Prepare()");
+    }
+  }
+
+  @Test
+  void prepareSelectQuery() throws Exception {
+    runSetup(
+        "DROP TABLE IF EXISTS foobar",
+        "CREATE TABLE foobar (i INT)",
+        "INSERT INTO foobar VALUES (1), (2)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT i + 2 AS baz FROM foobar ORDER BY baz ASC");
+      stmt.prepare();
+
+      assertSchema(stmt.executeSchema())
+          .isEqualTo(new Schema(List.of(Field.nullable("baz", 
Types.MinorType.INT.getType()))));
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(3, 4);
+      }
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(3, 4);
+      }
+    }
+  }
+
+  @Test
+  void prepareUpdateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES (1), (2)");
+      stmt.prepare();
+
+      AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+
+      result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  void bindSelectQuery() throws Exception {
+    Schema schema = new Schema(List.of(Field.nullable("$1", 
Types.MinorType.INT.getType())));
+    try (var stmt = conn.createStatement();
+        VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      stmt.setSqlQuery("SELECT $1 + 1 AS baz");
+      assertSchema(stmt.executeSchema())
+          .isEqualTo(new Schema(List.of(Field.nullable("baz", 
Types.MinorType.INT.getType()))));
+
+      var iv = (IntVector) vsr.getVector(0);
+      iv.setSafe(0, 1);
+      iv.setSafe(1, 42);
+      iv.setNull(2);
+      vsr.setRowCount(3);
+      stmt.bind(vsr);
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(2, 43, null);
+      }
+
+      iv.setNull(0);
+      iv.setSafe(1, 401);
+      iv.setSafe(2, 200);
+      iv.setSafe(3, 503);
+      vsr.setRowCount(4);
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(null, 402, 201, 504);
+      }
+    }
+  }
+
+  @Test
+  void bindUpdateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT, j 
BIGINT)");
+
+    Schema schema =
+        new Schema(
+            List.of(
+                Field.nullable("$1", Types.MinorType.INT.getType()),
+                Field.nullable("$2", Types.MinorType.BIGINT.getType())));
+    try (var stmt = conn.createStatement();
+        VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES ($1, $2 * 2)");
+
+      var iv = (IntVector) vsr.getVector(0);
+      iv.setSafe(0, 1);
+      iv.setSafe(1, 42);
+      iv.setNull(2);
+
+      var biv = (BigIntVector) vsr.getVector(1);
+      biv.setSafe(0, 0);
+      biv.setSafe(1, 1);
+      biv.setSafe(2, 2);
+
+      vsr.setRowCount(3);
+      stmt.bind(vsr);
+
+      AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(3);
+
+      iv.setSafe(0, 100);
+      biv.setSafe(0, 3);
+      vsr.setRowCount(1);
+
+      result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(1);
+
+      stmt.setSqlQuery("SELECT i FROM foobar ORDER BY j ASC");
+      try (AdbcStatement.QueryResult queryResult = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(queryResult.getReader(), "i");
+        assertThat(values).containsExactly(1, 42, null, 100);
+      }
+    }
+  }
+
+  void runSetup(String... sql) throws Exception {
+    try (var stmt = conn.createStatement()) {
+      for (String s : sql) {
+        stmt.setSqlQuery(s);
+        stmt.executeUpdate();
+      }
+    }
+  }
+
+  void testOptions(AdbcOptions handle) {
+    AdbcException e;
+
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
String.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Integer.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Long.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Boolean.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Float.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Double.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
byte[].class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+
+    e =
+        assertThrows(
+            AdbcException.class,
+            () -> handle.setOption(new TypedKey<>("foo", String.class), 
"bar"));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Integer.class), 42));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Long.class), 42L));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class,
+            () -> handle.setOption(new TypedKey<>("foo", Boolean.class), 
true));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Float.class), 3.14f));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Double.class), 3.14));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+    e =
+        assertThrows(
+            AdbcException.class,
+            () -> handle.setOption(new TypedKey<>("foo", byte[].class), new 
byte[] {1, 2, 3}));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+  }
+}
diff --git 
a/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/SqlServerIntegrationTest.java
 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/SqlServerIntegrationTest.java
new file mode 100644
index 000000000..8a6737816
--- /dev/null
+++ 
b/java/driver/jni-validation/src/test/java/org/apache/arrow/adbc/driver/jni/SqlServerIntegrationTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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.jni;
+
+import static 
org.apache.arrow.adbc.driver.testsuite.ArrowAssertions.assertSchema;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+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.AdbcDriver;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcInfoCode;
+import org.apache.arrow.adbc.core.AdbcOptions;
+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.adbc.core.TypedKey;
+import org.apache.arrow.adbc.driver.testsuite.ArrowToJava;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.Types;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class SqlServerIntegrationTest {
+  public static final String URI_ENV = "ADBC_MSSQL_TEST_URI";
+  static String URI = System.getenv(URI_ENV);
+
+  BufferAllocator allocator;
+  JniDriver driver;
+  AdbcDatabase db;
+  AdbcConnection conn;
+
+  @BeforeAll
+  static void beforeAll() {
+    Assumptions.assumeFalse(
+        URI == null || URI.isEmpty(),
+        String.format("Must set %s to run MSSQL integration tests", URI_ENV));
+  }
+
+  @BeforeEach
+  void beforeEach() throws Exception {
+    System.err.println("Connecting to MSSQL with URI: " + URI);
+    allocator = new RootAllocator();
+    driver = new JniDriver(allocator);
+    Map<String, Object> parameters = new HashMap<>();
+    JniDriver.PARAM_DRIVER.set(parameters, "mssql");
+    AdbcDriver.PARAM_URI.set(parameters, URI);
+    db = driver.open(parameters);
+    conn = db.connect();
+  }
+
+  @AfterEach
+  void afterEach() throws Exception {
+    conn.close();
+    db.close();
+    allocator.close();
+  }
+
+  @Test
+  void simple() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT 1 + 1 AS sum");
+      try (var reader = stmt.executeQuery()) {
+        assertThat(reader.getReader().loadNextBatch()).isTrue();
+        
assertThat(reader.getReader().getVectorSchemaRoot().getVector("sum").getObject(0))
+            .isEqualTo(2);
+      }
+    }
+  }
+
+  @Test
+  void options() throws Exception {
+    testOptions(db);
+    testOptions(conn);
+    try (AdbcStatement stmt = conn.createStatement()) {
+      testOptions(stmt);
+    }
+  }
+
+  @Test
+  void connectionGetInfo() throws Exception {
+    try (ArrowReader reader = conn.getInfo()) {
+      assertThat(reader.loadNextBatch()).isTrue();
+
+      var codes = 
ArrowToJava.toIntegers(reader.getVectorSchemaRoot().getVector("info_name"));
+      var values = 
ArrowToJava.toObjects(reader.getVectorSchemaRoot().getVector("info_value"));
+      var infos =
+          IntStream.range(0, codes.size())
+              .mapToObj(i -> Map.entry(codes.get(i), values.get(i)))
+              .collect(Collectors.toMap(Map.Entry::getKey, 
Map.Entry::getValue));
+
+      assertThat(codes).contains(AdbcInfoCode.VENDOR_NAME.getValue());
+      assertThat(codes).contains(AdbcInfoCode.VENDOR_VERSION.getValue());
+      assertThat(codes).contains(AdbcInfoCode.DRIVER_NAME.getValue());
+      assertThat(codes).contains(AdbcInfoCode.DRIVER_VERSION.getValue());
+      
assertThat(infos.get(AdbcInfoCode.VENDOR_NAME.getValue())).isEqualTo("Microsoft 
SQL Server");
+      assertThat(infos.get(AdbcInfoCode.DRIVER_NAME.getValue()))
+          .isEqualTo("Columnar ADBC Driver for Microsoft SQL Server");
+
+      assertThat(reader.loadNextBatch()).isFalse();
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void connectionGetObjects() throws Exception {
+    runSetup(
+        "DROP TABLE IF EXISTS test_schema.foobar",
+        "DROP SCHEMA IF EXISTS test_schema",
+        "DROP TABLE IF EXISTS foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE foobar (b VARCHAR)");
+
+    try (var reader =
+        conn.getObjects(
+            AdbcConnection.GetObjectsDepth.ALL, null, "TEST_SCHEMA", null, 
null, null)) {
+      var values =
+          ArrowToJava.toObjects(reader, "catalog_db_schemas").stream()
+              .filter(Objects::nonNull)
+              .filter(x -> !((List<?>) x).isEmpty())
+              .collect(Collectors.toList());
+      var schemas = (List<?>) values.get(0);
+      assertThat(schemas).isNotNull();
+      var schema = (Map<String, ?>) schemas.get(0);
+      
assertThat(schema).extractingByKey("db_schema_name").isEqualTo("test_schema");
+      var tables = (List<?>) schema.get("db_schema_tables");
+      assertThat(tables).size().isEqualTo(1);
+      var table = (Map<String, ?>) tables.get(0);
+      assertThat(table).extractingByKey("table_name").isEqualTo("foobar");
+      assertThat(table).extractingByKey("table_type").isEqualTo("BASE TABLE");
+      var columns = (List<Map<String, ?>>) table.get("table_columns");
+      
assertThat(columns).singleElement().extracting("column_name").isEqualTo("a");
+    }
+  }
+
+  @Test
+  void connectionGetTableSchema() throws Exception {
+    runSetup(
+        "DROP TABLE IF EXISTS test_schema.foobar",
+        "DROP SCHEMA IF EXISTS test_schema",
+        "DROP TABLE IF EXISTS foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE foobar (b VARCHAR)");
+
+    // XXX: this appears to get the wrong table
+    // assertSchema(conn.getTableSchema(null, "test_schema", "foobar"))
+    //     .isEqualTo(new Schema(List.of(Field.nullable("a", 
Types.MinorType.INT.getType()))));
+    assertSchema(conn.getTableSchema(null, null, "foobar"))
+        .isEqualTo(new Schema(List.of(Field.nullable("b", 
Types.MinorType.VARCHAR.getType()))));
+  }
+
+  @Test
+  void connectionTableTypes() throws Exception {
+    try (ArrowReader reader = conn.getTableTypes()) {
+      List<String> tableTypes = ArrowToJava.toStrings(reader, "table_type");
+      assertThat(tableTypes).containsExactlyInAnyOrder("BASE TABLE", "VIEW");
+    }
+  }
+
+  @Test
+  void connectionReadOnly() {
+    AdbcException e = assertThrows(AdbcException.class, () -> 
conn.getReadOnly());
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+
+    e = assertThrows(AdbcException.class, () -> conn.setReadOnly(true));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.INVALID_ARGUMENT);
+  }
+
+  @Test
+  void bulkIngest() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar");
+
+    final Schema schema =
+        new Schema(
+            List.of(
+                Field.nullable("ndx", Types.MinorType.INT.getType()),
+                Field.nullable("value", Types.MinorType.VARCHAR.getType())));
+    try (VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      IntVector iv = (IntVector) vsr.getVector(0);
+      VarCharVector vv = (VarCharVector) vsr.getVector(1);
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.CREATE)) {
+        iv.setSafe(0, 1);
+        iv.setSafe(1, 2);
+        vv.setNull(0);
+        vv.setSafe(1, "foobar".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(2);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(2);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.REPLACE)) {
+        iv.setSafe(0, 1);
+        iv.setSafe(1, 2);
+        vv.setSafe(0, "".getBytes(StandardCharsets.UTF_8));
+        vv.setSafe(1, "testtest".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(2);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(2);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.APPEND)) {
+        iv.setSafe(0, 3);
+        iv.setSafe(1, 4);
+        iv.setSafe(2, 5);
+        vv.setSafe(0, "spam".getBytes(StandardCharsets.UTF_8));
+        vv.setNull(1);
+        vv.setSafe(2, "eggs".getBytes(StandardCharsets.UTF_8));
+        vsr.setRowCount(3);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(3);
+      }
+
+      try (AdbcStatement stmt = conn.bulkIngest("foobar", 
BulkIngestMode.CREATE_APPEND)) {
+        iv.setSafe(0, 6);
+        iv.setSafe(1, 7);
+        iv.setSafe(2, 8);
+        vv.setSafe(0, "spam".getBytes(StandardCharsets.UTF_8));
+        vv.setNull(1);
+        vv.setNull(2);
+        vsr.setRowCount(3);
+
+        stmt.bind(vsr);
+        assertThat(stmt.executeUpdate().getAffectedRows()).isEqualTo(3);
+      }
+    }
+
+    try (AdbcStatement stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT value FROM foobar ORDER BY ndx");
+      try (var result = stmt.executeQuery()) {
+        var values = ArrowToJava.toStrings(result.getReader(), "value");
+        assertThat(values)
+            .containsExactly("", "testtest", "spam", null, "eggs", "spam", 
null, null);
+      }
+    }
+  }
+
+  @Test
+  void currentCatalogSchema() throws Exception {
+    runSetup(
+        "DROP TABLE IF EXISTS test_schema.foobar",
+        "DROP SCHEMA IF EXISTS test_schema",
+        "DROP TABLE IF EXISTS foobar",
+        "CREATE SCHEMA test_schema",
+        "CREATE TABLE test_schema.foobar (a INT)",
+        "CREATE TABLE foobar (b VARCHAR)");
+    assertThat(conn.getCurrentCatalog()).isEqualTo("master");
+    assertThat(conn.getCurrentDbSchema()).isEqualTo("dbo");
+
+    AdbcException e = assertThrows(AdbcException.class, () -> 
conn.setCurrentCatalog("foobar"));
+    assertThat(e).hasMessageContaining("Database 'foobar' does not exist");
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.INTERNAL);
+
+    // MSSQL does not let you change the search path
+    e = assertThrows(AdbcException.class, () -> 
conn.setCurrentDbSchema("test_schema"));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_IMPLEMENTED);
+
+    try (AdbcStatement stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT * FROM foobar");
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        assertSchema(result.getReader().getVectorSchemaRoot().getSchema())
+            .isEqualTo(new Schema(List.of(Field.nullable("b", 
Types.MinorType.VARCHAR.getType()))));
+      }
+    }
+  }
+
+  @Test
+  void transactions() throws Exception {}
+
+  @Test
+  void selectQuery() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT 42 AS THEANSWER, 'meaning of life' AS 
THEQUESTION");
+
+      assertSchema(stmt.executeSchema())
+          .isEqualTo(
+              new Schema(
+                  List.of(
+                      Field.notNullable("THEANSWER", 
Types.MinorType.INT.getType()),
+                      Field.notNullable("THEQUESTION", 
Types.MinorType.VARCHAR.getType()))));
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        assertThat(result.getReader().loadNextBatch()).isTrue();
+        assertThat(result.getReader().loadNextBatch()).isFalse();
+      }
+    }
+    // TODO(https://github.com/apache/arrow-adbc/issues/4239): test get 
parameter schema
+  }
+
+  @Test
+  void updateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES (1), (2)");
+      AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  void prepare() throws Exception {
+    try (var stmt = conn.createStatement()) {
+      assertThat(assertThrows(AdbcException.class, stmt::prepare))
+          .hasMessageContaining("Cannot Prepare without a query");
+    }
+  }
+
+  @Test
+  void prepareSelectQuery() throws Exception {
+    runSetup(
+        "DROP TABLE IF EXISTS foobar",
+        "CREATE TABLE foobar (i INT)",
+        "INSERT INTO foobar VALUES (1), (2)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("SELECT i + 2 AS baz FROM foobar ORDER BY baz ASC");
+      stmt.prepare();
+
+      assertSchema(stmt.executeSchema())
+          .isEqualTo(new Schema(List.of(Field.nullable("baz", 
Types.MinorType.INT.getType()))));
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(3, 4);
+      }
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(3, 4);
+      }
+    }
+  }
+
+  @Test
+  void prepareUpdateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT)");
+
+    try (var stmt = conn.createStatement()) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES (1), (2)");
+      stmt.prepare();
+
+      AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+
+      result = stmt.executeUpdate();
+      assertThat(result.getAffectedRows()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  void bindSelectQuery() throws Exception {
+    Schema schema = new Schema(List.of(Field.nullable("$1", 
Types.MinorType.INT.getType())));
+    try (var stmt = conn.createStatement();
+        VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      stmt.setSqlQuery("SELECT @p1 + 1 AS baz");
+      // XXX: not supported due to parameter
+      // assertSchema(stmt.executeSchema())
+      //     .isEqualTo(new Schema(List.of(Field.nullable("baz", 
Types.MinorType.INT.getType()))));
+
+      var iv = (IntVector) vsr.getVector(0);
+      iv.setSafe(0, 1);
+      iv.setSafe(1, 42);
+      iv.setNull(2);
+      vsr.setRowCount(3);
+      stmt.bind(vsr);
+
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(2, 43, null);
+      }
+
+      iv.setNull(0);
+      iv.setSafe(1, 401);
+      iv.setSafe(2, 200);
+      iv.setSafe(3, 503);
+      vsr.setRowCount(4);
+      try (AdbcStatement.QueryResult result = stmt.executeQuery()) {
+        var values = ArrowToJava.toIntegers(result.getReader(), "baz");
+        assertThat(values).containsExactly(null, 402, 201, 504);
+      }
+    }
+  }
+
+  @Test
+  void bindUpdateQuery() throws Exception {
+    runSetup("DROP TABLE IF EXISTS foobar", "CREATE TABLE foobar (i INT, j 
BIGINT)");
+
+    Schema schema =
+        new Schema(
+            List.of(
+                Field.nullable("$1", Types.MinorType.INT.getType()),
+                Field.nullable("$2", Types.MinorType.BIGINT.getType())));
+    try (var stmt = conn.createStatement();
+        VectorSchemaRoot vsr = VectorSchemaRoot.create(schema, allocator)) {
+      stmt.setSqlQuery("INSERT INTO foobar VALUES (@p1, @p2 * 2)");
+
+      var iv = (IntVector) vsr.getVector(0);
+      iv.setSafe(0, 1);
+      iv.setSafe(1, 42);
+      iv.setNull(2);
+
+      var biv = (BigIntVector) vsr.getVector(1);
+      biv.setSafe(0, 0);
+      biv.setSafe(1, 1);
+      biv.setSafe(2, 2);
+
+      vsr.setRowCount(3);
+      stmt.bind(vsr);
+
+      // XXX: appears to be a leak in the driver
+      // AdbcStatement.UpdateResult result = stmt.executeUpdate();
+      // assertThat(result.getAffectedRows()).isEqualTo(3);
+
+      // iv.setSafe(0, 100);
+      // biv.setSafe(0, 3);
+      // vsr.setRowCount(1);
+
+      // result = stmt.executeUpdate();
+      // assertThat(result.getAffectedRows()).isEqualTo(1);
+
+      // stmt.setSqlQuery("SELECT i FROM foobar ORDER BY j ASC");
+      // try (AdbcStatement.QueryResult queryResult = stmt.executeQuery()) {
+      //   var values = ArrowToJava.toIntegers(queryResult.getReader(), "i");
+      //   assertThat(values).containsExactly(1, 42, null, 100);
+      // }
+    }
+  }
+
+  void runSetup(String... sql) throws Exception {
+    try (var stmt = conn.createStatement()) {
+      for (String s : sql) {
+        stmt.setSqlQuery(s);
+        stmt.executeUpdate();
+      }
+    }
+  }
+
+  void testOptions(AdbcOptions handle) {
+    AdbcException e;
+
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
String.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Integer.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Long.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Boolean.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Float.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
Double.class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+    e =
+        assertThrows(
+            AdbcException.class, () -> handle.getOption(new TypedKey<>("foo", 
byte[].class)));
+    assertThat(e.getStatus()).isEqualTo(AdbcStatusCode.NOT_FOUND);
+
+    // XXX: the returned status code is inconsistent between different handle 
types
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
String.class), "bar"));
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Integer.class), 42));
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Long.class), 42L));
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Boolean.class), true));
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Float.class), 3.14f));
+    assertThrows(
+        AdbcException.class, () -> handle.setOption(new TypedKey<>("foo", 
Double.class), 3.14));
+    assertThrows(
+        AdbcException.class,
+        () -> handle.setOption(new TypedKey<>("foo", byte[].class), new byte[] 
{1, 2, 3}));
+  }
+}
diff --git 
a/java/driver/validation/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
index 54e605904..aa7a247bd 100644
--- 
a/java/driver/validation/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
@@ -50,7 +50,7 @@ public abstract class AbstractConnectionTest {
   }
 
   @Test
-  void currentCatalog() throws Exception {
+  protected void currentCatalog() throws Exception {
     assumeThat(quirks.supportsCurrentCatalog()).isTrue();
 
     
assertThat(connection.getCurrentCatalog()).isEqualTo(quirks.defaultCatalog());
@@ -63,17 +63,17 @@ public abstract class AbstractConnectionTest {
   }
 
   @Test
-  void multipleConnections() throws Exception {
+  protected void multipleConnections() throws Exception {
     try (final AdbcConnection ignored = database.connect()) {}
   }
 
   @Test
-  void readOnly() throws Exception {
+  protected void readOnly() throws Exception {
     assertThat(connection.getReadOnly()).isFalse();
   }
 
   @Test
-  void isolationLevel() throws Exception {
+  protected void isolationLevel() throws Exception {
     connection.getIsolationLevel();
   }
 }
diff --git 
a/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowToJava.java
 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowToJava.java
new file mode 100644
index 000000000..243a5199c
--- /dev/null
+++ 
b/java/driver/validation/src/main/java/org/apache/arrow/adbc/driver/testsuite/ArrowToJava.java
@@ -0,0 +1,287 @@
+/*
+ * 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.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.LargeVarCharVector;
+import org.apache.arrow.vector.SmallIntVector;
+import org.apache.arrow.vector.UInt4Vector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.complex.DenseUnionVector;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public final class ArrowToJava {
+  private ArrowToJava() {
+    throw new AssertionError();
+  }
+
+  public static List<@Nullable Long> toLongs(List<@Nullable Long> result, 
ValueVector vector) {
+    if (vector instanceof IntVector) {
+      IntVector v = (IntVector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add((long) v.get(i));
+        }
+      }
+      return result;
+    } else if (vector instanceof BigIntVector) {
+      BigIntVector v = (BigIntVector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(v.get(i));
+        }
+      }
+      return result;
+    } else if (vector instanceof UInt4Vector) {
+      UInt4Vector v = (UInt4Vector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(Integer.toUnsignedLong(v.get(i)));
+        }
+      }
+      return result;
+    } else {
+      throw new IllegalArgumentException("Unsupported vector type: " + 
vector.getClass());
+    }
+  }
+
+  public static List<@Nullable Long> toLongs(ValueVector vector) {
+    return toLongs(new ArrayList<>(), vector);
+  }
+
+  public static List<@Nullable Long> toLongs(ArrowReader reader, String 
fieldName)
+      throws IOException {
+    return collect(reader, fieldName, ArrowToJava::toLongs);
+  }
+
+  public static List<@Nullable Integer> toIntegers(
+      List<@Nullable Integer> result, ValueVector vector) {
+    if (vector instanceof IntVector) {
+      IntVector v = (IntVector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(v.get(i));
+        }
+      }
+      return result;
+    } else if (vector instanceof UInt4Vector) {
+      UInt4Vector v = (UInt4Vector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(v.get(i));
+        }
+      }
+      return result;
+    } else {
+      throw new IllegalArgumentException("Unsupported vector type: " + 
vector.getClass());
+    }
+  }
+
+  public static List<@Nullable Integer> toIntegers(ValueVector vector) {
+    return toIntegers(new ArrayList<>(), vector);
+  }
+
+  public static List<@Nullable Integer> toIntegers(ArrowReader reader, String 
fieldName)
+      throws IOException {
+    return collect(reader, fieldName, ArrowToJava::toIntegers);
+  }
+
+  public static List<@Nullable String> toStrings(
+      List<@Nullable String> result, ValueVector vector) {
+    if (vector instanceof VarCharVector) {
+      VarCharVector v = (VarCharVector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(v.getObject(i).toString());
+        }
+      }
+    } else if (vector instanceof LargeVarCharVector) {
+      LargeVarCharVector v = (LargeVarCharVector) vector;
+      for (int i = 0; i < v.getValueCount(); i++) {
+        if (v.isNull(i)) {
+          result.add(null);
+        } else {
+          result.add(v.getObject(i).toString());
+        }
+      }
+    } else {
+      throw new IllegalArgumentException("Unsupported vector type: " + 
vector.getClass());
+    }
+    return result;
+  }
+
+  public static List<@Nullable String> toStrings(ValueVector vector) {
+    return toStrings(new ArrayList<>(), vector);
+  }
+
+  public static List<@Nullable String> toStrings(ArrowReader reader, String 
fieldName)
+      throws IOException {
+    return collect(reader, fieldName, ArrowToJava::toStrings);
+  }
+
+  public static List<@Nullable Object> toObjects(
+      List<@Nullable Object> result, ValueVector vector) {
+    for (int i = 0; i < vector.getValueCount(); i++) {
+      result.add(getObject(vector, i));
+    }
+    return result;
+  }
+
+  public static List<@Nullable Object> toObjects(ValueVector vector) {
+    return toObjects(new ArrayList<>(), vector);
+  }
+
+  public static List<@Nullable Object> toObjects(ArrowReader reader, String 
fieldName)
+      throws IOException {
+    return collect(reader, fieldName, ArrowToJava::toObjects);
+  }
+
+  static <T> List<@Nullable T> collect(
+      ArrowReader reader, String fieldName, ValueVectorCollector<T> collector) 
throws IOException {
+    List<@Nullable T> result = new ArrayList<>();
+    while (reader.loadNextBatch()) {
+      collector.collect(result, 
reader.getVectorSchemaRoot().getVector(fieldName));
+    }
+    return result;
+  }
+
+  @FunctionalInterface
+  interface ValueVectorCollector<T> {
+    void collect(List<@Nullable T> result, ValueVector vector);
+  }
+
+  static @Nullable Object getObject(BigIntVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      return vector.getObject(index);
+    }
+  }
+
+  static @Nullable Object getObject(BitVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      return vector.getObject(index);
+    }
+  }
+
+  static @Nullable Object getObject(DenseUnionVector vector, int index) {
+    byte typeId = vector.getTypeId(index);
+    int offset = vector.getOffset(index);
+    return getObject(vector.getVectorByType(typeId), offset);
+  }
+
+  static @Nullable Object getObject(IntVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      return vector.getObject(index);
+    }
+  }
+
+  static @Nullable Object getObject(ListVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      List<@Nullable Object> vals = new ArrayList<>();
+      ValueVector children = vector.getDataVector();
+      int start = vector.getOffsetBuffer().getInt(index * 4L);
+      int end = vector.getOffsetBuffer().getInt((index + 1) * 4L);
+      for (int i = start; i < end; ++i) {
+        vals.add(getObject(children, i));
+      }
+      return vals;
+    }
+  }
+
+  static @Nullable Object getObject(SmallIntVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      return vector.getObject(index);
+    }
+  }
+
+  static @Nullable Object getObject(StructVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      Map<String, @Nullable Object> m = new HashMap<>();
+      for (String field : vector.getChildFieldNames()) {
+        var child = vector.getChild(field);
+        Object childValue = getObject(child, index);
+        m.put(field, childValue);
+      }
+      return m;
+    }
+  }
+
+  static @Nullable Object getObject(VarCharVector vector, int index) {
+    if (vector.isNull(index)) {
+      return null;
+    } else {
+      return vector.getObject(index).toString();
+    }
+  }
+
+  static @Nullable Object getObject(ValueVector vector, int index) {
+    if (vector instanceof BigIntVector) {
+      return getObject((BigIntVector) vector, index);
+    } else if (vector instanceof BitVector) {
+      return getObject((BitVector) vector, index);
+    } else if (vector instanceof DenseUnionVector) {
+      return getObject((DenseUnionVector) vector, index);
+    } else if (vector instanceof IntVector) {
+      return getObject((IntVector) vector, index);
+    } else if (vector instanceof ListVector) {
+      return getObject((ListVector) vector, index);
+    } else if (vector instanceof SmallIntVector) {
+      return getObject((SmallIntVector) vector, index);
+    } else if (vector instanceof StructVector) {
+      return getObject((StructVector) vector, index);
+    } else if (vector instanceof VarCharVector) {
+      return getObject((VarCharVector) vector, index);
+    } else {
+      throw new UnsupportedOperationException("Unsupported vector type: " + 
vector.getClass());
+    }
+  }
+}
diff --git a/java/pom.xml b/java/pom.xml
index d83273080..fdbb08b56 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -333,7 +333,7 @@
       <id>jni</id>
       <modules>
         <module>driver/jni</module>
-        <module>driver/jni-validation-sqlite</module>
+        <module>driver/jni-validation</module>
       </modules>
     </profile>
   </profiles>

Reply via email to