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 d2d32eddc feat(java/driver/jni): add JNI bindings to native driver 
manager (#2401)
d2d32eddc is described below

commit d2d32eddcaae522e536fc53e4c838ae4b634417d
Author: David Li <[email protected]>
AuthorDate: Tue Apr 8 11:00:32 2025 +0900

    feat(java/driver/jni): add JNI bindings to native driver manager (#2401)
    
    Fixes #2027.
---
 CONTRIBUTING.md                                    |  37 ++
 c/driver_manager/adbc_driver_manager.cc            |   2 +-
 ci/conda_env_python.txt                            |   3 +
 .../java_jni_build.sh}                             |  43 ++-
 compose.yaml                                       |   2 +-
 java/CMakeLists.txt                                |  61 ++++
 java/driver/flight-sql/pom.xml                     |   1 -
 java/driver/jni/CMakeLists.txt                     |  44 +++
 java/driver/{flight-sql => jni}/pom.xml            |  72 ++--
 java/driver/jni/src/main/cpp/jni_wrapper.cc        | 373 +++++++++++++++++++++
 .../arrow/adbc/driver/jni/JniConnection.java       |  52 +++
 .../apache/arrow/adbc/driver/jni/JniDatabase.java  |  45 +++
 .../apache/arrow/adbc/driver/jni/JniDriver.java    |  54 +++
 .../arrow/adbc/driver/jni/JniDriverFactory.java    |  30 ++
 .../apache/arrow/adbc/driver/jni/JniStatement.java |  68 ++++
 .../arrow/adbc/driver/jni/impl/JniLoader.java      | 100 ++++++
 .../arrow/adbc/driver/jni/impl/NativeAdbc.java     |  40 +++
 .../driver/jni/impl/NativeConnectionHandle.java    |  33 ++
 .../adbc/driver/jni/impl/NativeDatabaseHandle.java |  33 ++
 .../arrow/adbc/driver/jni/impl/NativeHandle.java   |  69 ++++
 .../adbc/driver/jni/impl/NativeQueryResult.java    |  36 ++
 .../driver/jni/impl/NativeStatementHandle.java     |  33 ++
 .../apache/arrow/adbc/driver/jni/package-info.java |  18 +
 ...ache.arrow.adbc.drivermanager.AdbcDriverFactory |  16 +-
 .../arrow/adbc/driver/jni/JniDriverTest.java       | 126 +++++++
 java/pom.xml                                       |  13 +
 26 files changed, 1329 insertions(+), 75 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c9cbd0afb..7608a051d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -316,6 +316,43 @@ mvn install -Perrorprone
 [checker-framework]: https://checkerframework.org/
 [errorprone]: https://errorprone.info/
 
+#### JNI
+
+To build the JNI bridge, the native components must be built.
+
+```
+# Build the driver manager
+export ADBC_BUILD_STATIC=ON
+export ADBC_BUILD_TESTS=OFF
+export ADBC_USE_ASAN=OFF
+export ADBC_USE_UBSAN=OFF
+export BUILD_ALL=OFF
+export BUILD_DRIVER_MANAGER=ON
+export BUILD_DRIVER_SQLITE=ON
+./ci/scripts/cpp_build.sh $(pwd) $(pwd)/build $(pwd)/local
+
+# Build the JNI libraries
+./ci/scripts/java_jni_build.sh $(pwd) $(pwd)/java/build $(pwd)/local
+```
+
+Now build the Java code with the `jni` Maven profile enabled.  To run tests,
+the SQLite driver must also be present in (DY)LD_LIBRARY_PATH.
+
+```
+export LD_LIBRARY_PATH=$(pwd)/local/lib
+pushd java
+mvn install -Pjni
+popd
+```
+
+This will build a JAR with native libraries for a single platform.  If the
+native libraries are built for multiple platforms, they can all be copied to
+appropriate paths in the resources directory to build a single JAR that works
+across multiple platforms.
+
+You can also build and test in IntelliJ; simply edit the run/test
+configuration to add `LD_LIBRARY_PATH` to the environment.
+
 ### Python
 
 Python libraries are managed with [setuptools][setuptools].  See
diff --git a/c/driver_manager/adbc_driver_manager.cc 
b/c/driver_manager/adbc_driver_manager.cc
index 0ce173a88..5e4a8b6df 100644
--- a/c/driver_manager/adbc_driver_manager.cc
+++ b/c/driver_manager/adbc_driver_manager.cc
@@ -173,7 +173,7 @@ struct ManagedLibrary {
 
     void* handle = dlopen(library, RTLD_NOW | RTLD_LOCAL);
     if (!handle) {
-      error_message = "[DriverManager] dlopen() failed: ";
+      error_message = "dlopen() failed: ";
       error_message += dlerror();
 
       // If applicable, append the shared library prefix/extension and
diff --git a/ci/conda_env_python.txt b/ci/conda_env_python.txt
index d0d4de475..c251dd8d1 100644
--- a/ci/conda_env_python.txt
+++ b/ci/conda_env_python.txt
@@ -17,6 +17,9 @@
 
 Cython
 importlib-resources
+# libxml2 broke ABI compatibility
+# https://github.com/conda-forge/arrow-cpp-feedstock/issues/1740
+libxml2 <2.14.0
 # nodejs is required by pyright
 nodejs >=13.0.0
 pandas
diff --git a/ci/conda_env_python.txt b/ci/scripts/java_jni_build.sh
old mode 100644
new mode 100755
similarity index 57%
copy from ci/conda_env_python.txt
copy to ci/scripts/java_jni_build.sh
index d0d4de475..6ccfb183a
--- a/ci/conda_env_python.txt
+++ b/ci/scripts/java_jni_build.sh
@@ -1,3 +1,4 @@
+#!/usr/bin/env bash
 # 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
@@ -15,18 +16,30 @@
 # specific language governing permissions and limitations
 # under the License.
 
-Cython
-importlib-resources
-# nodejs is required by pyright
-nodejs >=13.0.0
-pandas
-pip
-pyarrow-all
-pyright
-pytest
-setuptools
-
-# For integration testing
-polars
-protobuf
-python-duckdb
+set -ex
+
+: ${ADBC_CMAKE_ARGS:=""}
+: ${CMAKE_BUILD_TYPE:=Debug}
+
+main() {
+    local -r source_dir=${1}
+    local -r build_dir=${2}
+    local -r install_dir=${3}
+
+    mkdir -p "${build_dir}"
+    pushd "${build_dir}"
+
+    set -x
+    cmake "${source_dir}/java" \
+        ${ADBC_CMAKE_ARGS} \
+        -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
+        
-DCMAKE_INSTALL_PREFIX="${source_dir}/java/driver/jni/src/main/resources/" \
+        -DCMAKE_PREFIX_PATH="${install_dir}/lib/cmake/"
+    set +x
+
+    cmake --build . --target install -j
+
+    popd
+}
+
+main "$@"
diff --git a/compose.yaml b/compose.yaml
index a337bd5e6..9b7ca4c5d 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -67,7 +67,7 @@ services:
     command: |
       /bin/bash -c 'git config --global --add safe.directory /adbc && source 
/opt/conda/etc/profile.d/conda.sh && mamba create -y -n adbc -c conda-forge go 
--file /adbc/ci/conda_env_cpp.txt --file /adbc/ci/conda_env_docs.txt --file 
/adbc/ci/conda_env_java.txt --file /adbc/ci/conda_env_python.txt && conda 
activate adbc && /adbc/ci/scripts/cpp_build.sh /adbc /adbc/build && 
/adbc/ci/scripts/go_build.sh /adbc /adbc/build && 
/adbc/ci/scripts/python_build.sh /adbc /adbc/build && /adbc/ci/scrip [...]
 
-  ############################ Java JARs ######################################
+  ################################### Java ###################################
 
   java-dist:
     image: ${ARCH}/maven:${MAVEN}-jdk-${JDK}
diff --git a/java/CMakeLists.txt b/java/CMakeLists.txt
new file mode 100644
index 000000000..e4ae54ec3
--- /dev/null
+++ b/java/CMakeLists.txt
@@ -0,0 +1,61 @@
+# 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.
+
+cmake_minimum_required(VERSION 3.16)
+message(STATUS "Building using CMake version: ${CMAKE_VERSION}")
+
+# find_package() uses <PackageName>_ROOT variables.
+# https://cmake.org/cmake/help/latest/policy/CMP0074.html
+if(POLICY CMP0074)
+  cmake_policy(SET CMP0074 NEW)
+endif()
+
+project(adbc-java)
+
+if("${CMAKE_CXX_STANDARD}" STREQUAL "")
+  set(CMAKE_CXX_STANDARD 17)
+endif()
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+include(GNUInstallDirs)
+
+# Dependencies
+
+find_package(Java REQUIRED)
+find_package(JNI REQUIRED)
+
+include(UseJava)
+
+# ADBC_ARCH_DIR is derived from the architecture. The user can override this
+# variable if auto-detection fails.
+if("${ADBC_ARCH_DIR}" STREQUAL "")
+  if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64")
+    set(ADBC_ARCH_DIR "aarch_64")
+  elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i386")
+    set(ADBC_ARCH_DIR "x86_64")
+  elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm64")
+    set(ADBC_ARCH_DIR "aarch_64")
+  elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64")
+    set(ADBC_ARCH_DIR "x86_64")
+  elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64")
+    set(ADBC_ARCH_DIR "x86_64")
+  else()
+    message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
+  endif()
+endif()
+
+add_subdirectory(driver/jni)
diff --git a/java/driver/flight-sql/pom.xml b/java/driver/flight-sql/pom.xml
index c84897497..b7b909761 100644
--- a/java/driver/flight-sql/pom.xml
+++ b/java/driver/flight-sql/pom.xml
@@ -35,7 +35,6 @@
     <dependency>
       <groupId>com.github.ben-manes.caffeine</groupId>
       <artifactId>caffeine</artifactId>
-      <!-- Latest version still supporting Java 8 -->
       <version>3.2.0</version>
     </dependency>
     <dependency>
diff --git a/java/driver/jni/CMakeLists.txt b/java/driver/jni/CMakeLists.txt
new file mode 100644
index 000000000..b33d61879
--- /dev/null
+++ b/java/driver/jni/CMakeLists.txt
@@ -0,0 +1,44 @@
+# 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.
+
+find_package(AdbcDriverManager)
+
+add_custom_command(OUTPUT 
${CMAKE_CURRENT_SOURCE_DIR}/target/headers/org_apache_arrow_adbc_driver_jni_impl_NativeAdbc.h
+                   COMMENT "Generate JNI headers"
+                   # Force Maven to actually re-run the command
+                   COMMAND rm -rf ${CMAKE_CURRENT_SOURCE_DIR}/target/headers
+                           ${CMAKE_CURRENT_SOURCE_DIR}/target/maven-status
+                   COMMAND mvn --file ${CMAKE_CURRENT_SOURCE_DIR}/../.. 
-Pjni,javah
+                           compile --also-make --projects :adbc-driver-jni
+                   DEPENDS 
${CMAKE_CURRENT_SOURCE_DIR}/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeAdbc.java
+)
+
+add_library(adbc_driver_jni SHARED
+            src/main/cpp/jni_wrapper.cc
+            
${CMAKE_CURRENT_SOURCE_DIR}/target/headers/org_apache_arrow_adbc_driver_jni_impl_NativeAdbc.h
+)
+target_include_directories(adbc_driver_jni
+                           PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/target/headers)
+target_link_libraries(adbc_driver_jni JNI::JNI
+                      AdbcDriverManager::adbc_driver_manager_static)
+
+set(ADBC_DRIVER_JNI_C_LIBDIR "adbc_driver_jni/${ADBC_ARCH_DIR}")
+set(ADBC_DRIVER_JNI_C_BINDIR "adbc_driver_jni/${ADBC_ARCH_DIR}")
+
+install(TARGETS adbc_driver_jni
+        LIBRARY DESTINATION ${ADBC_DRIVER_JNI_C_LIBDIR}
+        RUNTIME DESTINATION ${ADBC_DRIVER_JNI_C_BINDIR})
diff --git a/java/driver/flight-sql/pom.xml b/java/driver/jni/pom.xml
similarity index 68%
copy from java/driver/flight-sql/pom.xml
copy to java/driver/jni/pom.xml
index c84897497..2afb4fd8b 100644
--- a/java/driver/flight-sql/pom.xml
+++ b/java/driver/jni/pom.xml
@@ -22,44 +22,33 @@
   <parent>
     <groupId>org.apache.arrow.adbc</groupId>
     <artifactId>arrow-adbc-java-root</artifactId>
-    <version>0.18.0-SNAPSHOT</version>
+    <version>0.17.0-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
-  <artifactId>adbc-driver-flight-sql</artifactId>
+  <artifactId>adbc-driver-jni</artifactId>
   <packaging>jar</packaging>
-  <name>Arrow ADBC Driver Flight SQL</name>
-  <description>An ADBC driver wrapping Flight SQL.</description>
+  <name>Arrow ADBC Driver Native</name>
+  <description>An ADBC driver wrapping the native driver manager.</description>
 
   <dependencies>
-    <dependency>
-      <groupId>com.github.ben-manes.caffeine</groupId>
-      <artifactId>caffeine</artifactId>
-      <!-- Latest version still supporting Java 8 -->
-      <version>3.2.0</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.protobuf</groupId>
-      <artifactId>protobuf-java</artifactId>
-      <version>4.30.2</version>
-    </dependency>
-
     <!-- Arrow -->
     <dependency>
       <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-memory-core</artifactId>
+      <artifactId>arrow-c-data</artifactId>
     </dependency>
     <dependency>
       <groupId>org.apache.arrow</groupId>
-      <artifactId>arrow-vector</artifactId>
+      <artifactId>arrow-memory-core</artifactId>
     </dependency>
     <dependency>
       <groupId>org.apache.arrow</groupId>
-      <artifactId>flight-core</artifactId>
+      <artifactId>arrow-memory-netty</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.arrow</groupId>
-      <artifactId>flight-sql</artifactId>
+      <artifactId>arrow-vector</artifactId>
     </dependency>
 
     <dependency>
@@ -70,16 +59,6 @@
       <groupId>org.apache.arrow.adbc</groupId>
       <artifactId>adbc-driver-manager</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.apache.arrow.adbc</groupId>
-      <artifactId>adbc-sql</artifactId>
-    </dependency>
-
-    <!-- Helpers for mapping Arrow types to ANSI SQL types and building test 
servers -->
-    <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>flight-sql-jdbc-core</artifactId>
-    </dependency>
 
     <!-- Static analysis and linting -->
     <dependency>
@@ -98,18 +77,6 @@
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.junit.vintage</groupId>
-      <artifactId>junit-vintage-engine</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.arrow</groupId>
-      <artifactId>flight-sql-jdbc-core</artifactId>
-      <version>${dep.arrow.version}</version>
-      <classifier>tests</classifier>
-      <scope>test</scope>
-    </dependency>
   </dependencies>
 
   <build>
@@ -125,4 +92,25 @@
       </plugin>
     </plugins>
   </build>
+
+  <profiles>
+    <profile>
+      <!-- Invoked by CMake to generate headers. -->
+      <!-- You must first build the project (without JNI enabled). -->
+      <id>javah</id>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <compilerArgs>
+                <arg>-h</arg>
+                <arg>${project.basedir}/target/headers</arg>
+              </compilerArgs>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git a/java/driver/jni/src/main/cpp/jni_wrapper.cc 
b/java/driver/jni/src/main/cpp/jni_wrapper.cc
new file mode 100644
index 000000000..61c812b30
--- /dev/null
+++ b/java/driver/jni/src/main/cpp/jni_wrapper.cc
@@ -0,0 +1,373 @@
+// 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.
+
+#include "org_apache_arrow_adbc_driver_jni_impl_NativeAdbc.h"
+
+#include <cassert>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include <arrow-adbc/adbc.h>
+#include <arrow-adbc/adbc_driver_manager.h>
+#include <jni.h>
+
+// We will use exceptions for error handling as that's easier with the JNI
+// model.
+
+namespace {
+
+/// Internal exception.  Meant to be used with RaiseAdbcException and
+///   CHECK_ADBC_ERROR.
+struct AdbcException {
+  AdbcStatusCode code;
+  std::string message;
+
+  void ThrowJavaException(JNIEnv* env) const {
+    jclass exception_klass = 
env->FindClass("org/apache/arrow/adbc/core/AdbcException");
+    assert(exception_klass != nullptr);
+    jmethodID exception_ctor =
+        env->GetMethodID(exception_klass, "<init>",
+                         "(Ljava/lang/String;Ljava/lang/Throwable;"
+                         "Lorg/apache/arrow/adbc/core/AdbcStatusCode;"
+                         "Ljava/lang/String;I)V");
+    assert(exception_ctor != nullptr);
+
+    jclass status_klass = 
env->FindClass("org/apache/arrow/adbc/core/AdbcStatusCode");
+    assert(status_klass != nullptr);
+
+    jfieldID status_field;
+
+    const char* sig = "Lorg/apache/arrow/adbc/core/AdbcStatusCode;";
+#define CASE(name)                                                  \
+  case ADBC_STATUS_##name:                                          \
+    status_field = env->GetStaticFieldID(status_klass, #name, sig); \
+    break;
+
+    switch (code) {
+      CASE(UNKNOWN);
+      CASE(NOT_IMPLEMENTED);
+      CASE(NOT_FOUND);
+      CASE(ALREADY_EXISTS);
+      CASE(INVALID_ARGUMENT);
+      CASE(INVALID_STATE);
+      CASE(INVALID_DATA);
+      CASE(INTEGRITY);
+      CASE(INTERNAL);
+      CASE(IO);
+      CASE(CANCELLED);
+      CASE(TIMEOUT);
+      CASE(UNAUTHENTICATED);
+      CASE(UNAUTHORIZED);
+      default:
+        // uh oh
+        status_field = env->GetStaticFieldID(status_klass, "INTERNAL", sig);
+        break;
+    }
+#undef CASE
+    jobject status_jni = env->GetStaticObjectField(status_klass, status_field);
+
+    jstring message_jni = env->NewStringUTF(message.c_str());
+    auto exc = static_cast<jthrowable>(env->NewObject(
+        exception_klass, exception_ctor, message_jni, /*cause=*/nullptr, 
status_jni,
+        /*sqlState=*/nullptr, /*vendorCode=*/0));
+    env->Throw(exc);
+  }
+};
+
+/// Signal an error to Java.
+void RaiseAdbcException(AdbcStatusCode code, const AdbcError& error) {
+  assert(code != ADBC_STATUS_OK);
+  throw AdbcException{
+      .code = code,
+      .message = std::string(error.message),
+  };
+}
+
+/// Check the result of an ADBC call and raise an exception to Java if it 
failed.
+#define CHECK_ADBC_ERROR(expr, error)      \
+  do {                                     \
+    AdbcStatusCode status = (expr);        \
+    if (status != ADBC_STATUS_OK) {        \
+      ::RaiseAdbcException(status, error); \
+    }                                      \
+  } while (0)
+
+/// Require that a Java class exists or error.
+jclass RequireImplClass(JNIEnv* env, std::string_view name) {
+  static std::string kPrefix = "org/apache/arrow/adbc/driver/jni/impl/";
+  std::string full_name = kPrefix + std::string(name);
+  jclass klass = env->FindClass(full_name.c_str());
+  if (klass == nullptr) {
+    throw AdbcException{
+        .code = ADBC_STATUS_INTERNAL,
+        .message = "[JNI] Could not find class " + full_name,
+    };
+  }
+  return klass;
+}
+
+/// Require that a Java method exists or error.
+jmethodID RequireMethod(JNIEnv* env, jclass klass, std::string_view name,
+                        std::string_view signature) {
+  jmethodID method = env->GetMethodID(klass, name.data(), signature.data());
+  if (method == nullptr) {
+    std::string message = "[JNI] Could not find method ";
+    message += name;
+    message += " with signature ";
+    message += signature;
+    throw AdbcException{
+        .code = ADBC_STATUS_INTERNAL,
+        .message = std::move(message),
+    };
+  }
+  return method;
+}
+
+struct JniStringView {
+  JNIEnv* env;
+  jstring jni_string;
+  const char* value;
+
+  explicit JniStringView(JNIEnv* env, jstring jni_string)
+      : env(env), jni_string(jni_string), value(nullptr) {
+    if (jni_string == nullptr) {
+      throw AdbcException{ADBC_STATUS_INTERNAL, "Java string was nullptr"};
+    }
+    value = env->GetStringUTFChars(jni_string, nullptr);
+    if (value == nullptr) {
+      throw AdbcException{ADBC_STATUS_INTERNAL,
+                          "Java string was nullptr (could not get string 
contents)"};
+    }
+  }
+
+  ~JniStringView() {
+    if (jni_string == nullptr) {
+      return;
+    }
+
+    env->ReleaseStringUTFChars(jni_string, value);
+    jni_string = nullptr;
+  }
+};
+
+std::string GetJniString(JNIEnv* env, jstring jni_string) {
+  JniStringView view(env, jni_string);
+  return std::string(view.value);
+}
+
+std::optional<std::string> MaybeGetJniString(JNIEnv* env, jstring jni_string) {
+  if (jni_string == nullptr) {
+    return std::nullopt;
+  }
+  JniStringView view(env, jni_string);
+  return std::string(view.value);
+}
+
+template <typename Callable>
+auto WithJniString(JNIEnv* env, jstring jni_string, Callable&& callable) {
+  JniStringView view(env, jni_string);
+  return callable(view.value);
+}
+
+}  // namespace
+
+extern "C" {
+
+JNIEXPORT jobject JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_openDatabase(
+    JNIEnv* env, [[maybe_unused]] jclass self, jint version, jobjectArray 
parameters) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto db = std::make_unique<struct AdbcDatabase>();
+    std::memset(db.get(), 0, sizeof(struct AdbcDatabase));
+
+    CHECK_ADBC_ERROR(AdbcDatabaseNew(db.get(), &error), error);
+
+    const jsize num_params = env->GetArrayLength(parameters);
+    if (num_params % 2 != 0) {
+      throw AdbcException{
+          .code = ADBC_STATUS_INVALID_ARGUMENT,
+          .message = "[JNI] Must provide even number of parameters",
+      };
+    }
+    for (jsize i = 0; i < num_params; i += 2) {
+      // N.B. assuming String because Java side is typed as String[]
+      auto key = 
reinterpret_cast<jstring>(env->GetObjectArrayElement(parameters, i));
+      auto value =
+          reinterpret_cast<jstring>(env->GetObjectArrayElement(parameters, i + 
1));
+
+      JniStringView key_str(env, key);
+      JniStringView value_str(env, value);
+      CHECK_ADBC_ERROR(
+          AdbcDatabaseSetOption(db.get(), key_str.value, value_str.value, 
&error), error);
+    }
+
+    CHECK_ADBC_ERROR(AdbcDatabaseInit(db.get(), &error), error);
+
+    jclass nativeHandleKlass = RequireImplClass(env, "NativeDatabaseHandle");
+    jmethodID nativeHandleCtor = RequireMethod(env, nativeHandleKlass, 
"<init>", "(J)V");
+    jobject object =
+        env->NewObject(nativeHandleKlass, nativeHandleCtor,
+                       
static_cast<jlong>(reinterpret_cast<uintptr_t>(db.get())));
+    // Don't release until after we've constructed the object
+    db.release();
+    return object;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+    return nullptr;
+  }
+}
+
+JNIEXPORT void JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_closeDatabase(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto* ptr = reinterpret_cast<struct 
AdbcDatabase*>(static_cast<uintptr_t>(handle));
+    CHECK_ADBC_ERROR(AdbcDatabaseRelease(ptr, &error), error);
+    delete ptr;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+  }
+}
+
+JNIEXPORT jobject JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_openConnection(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong database_handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto conn = std::make_unique<struct AdbcConnection>();
+    std::memset(conn.get(), 0, sizeof(struct AdbcConnection));
+
+    auto* db =
+        reinterpret_cast<struct 
AdbcDatabase*>(static_cast<uintptr_t>(database_handle));
+
+    CHECK_ADBC_ERROR(AdbcConnectionNew(conn.get(), &error), error);
+    CHECK_ADBC_ERROR(AdbcConnectionInit(conn.get(), db, &error), error);
+
+    jclass native_handle_class = RequireImplClass(env, 
"NativeConnectionHandle");
+    jmethodID native_handle_ctor =
+        RequireMethod(env, native_handle_class, "<init>", "(J)V");
+    jobject object =
+        env->NewObject(native_handle_class, native_handle_ctor,
+                       
static_cast<jlong>(reinterpret_cast<uintptr_t>(conn.get())));
+    // Don't release until after we've constructed the object
+    conn.release();
+    return object;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+    return nullptr;
+  }
+}
+
+JNIEXPORT void JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_closeConnection(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto* ptr = reinterpret_cast<struct 
AdbcConnection*>(static_cast<uintptr_t>(handle));
+    CHECK_ADBC_ERROR(AdbcConnectionRelease(ptr, &error), error);
+    delete ptr;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+  }
+}
+
+JNIEXPORT jobject JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_openStatement(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong connection_handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto stmt = std::make_unique<struct AdbcStatement>();
+    std::memset(stmt.get(), 0, sizeof(struct AdbcStatement));
+
+    auto* conn = reinterpret_cast<struct AdbcConnection*>(
+        static_cast<uintptr_t>(connection_handle));
+
+    CHECK_ADBC_ERROR(AdbcStatementNew(conn, stmt.get(), &error), error);
+
+    jclass native_handle_class = RequireImplClass(env, 
"NativeStatementHandle");
+    jmethodID native_handle_ctor =
+        RequireMethod(env, native_handle_class, "<init>", "(J)V");
+    jobject object =
+        env->NewObject(native_handle_class, native_handle_ctor,
+                       
static_cast<jlong>(reinterpret_cast<uintptr_t>(stmt.get())));
+    // Don't release until after we've constructed the object
+    stmt.release();
+    return object;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+    return nullptr;
+  }
+}
+
+JNIEXPORT void JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_closeStatement(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto* ptr = reinterpret_cast<struct 
AdbcStatement*>(static_cast<uintptr_t>(handle));
+    CHECK_ADBC_ERROR(AdbcStatementRelease(ptr, &error), error);
+    delete ptr;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+  }
+}
+
+JNIEXPORT jobject JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_statementExecuteQuery(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong handle) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto* ptr = reinterpret_cast<struct 
AdbcStatement*>(static_cast<uintptr_t>(handle));
+    auto out = std::make_unique<struct ArrowArrayStream>();
+    std::memset(out.get(), 0, sizeof(struct ArrowArrayStream));
+    int64_t rows_affected = 0;
+    CHECK_ADBC_ERROR(AdbcStatementExecuteQuery(ptr, out.get(), &rows_affected, 
&error),
+                     error);
+
+    jclass native_result_class = RequireImplClass(env, "NativeQueryResult");
+    jmethodID native_result_ctor =
+        RequireMethod(env, native_result_class, "<init>", "(JJ)V");
+    jobject object =
+        env->NewObject(native_result_class, native_result_ctor, rows_affected,
+                       
static_cast<jlong>(reinterpret_cast<uintptr_t>(out.get())));
+    // Don't release until after we've constructed the object
+    out.release();
+    return object;
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+  }
+  return nullptr;
+}
+
+JNIEXPORT void JNICALL
+Java_org_apache_arrow_adbc_driver_jni_impl_NativeAdbc_statementSetSqlQuery(
+    JNIEnv* env, [[maybe_unused]] jclass self, jlong handle, jstring query) {
+  try {
+    struct AdbcError error = ADBC_ERROR_INIT;
+    auto* ptr = reinterpret_cast<struct 
AdbcStatement*>(static_cast<uintptr_t>(handle));
+    JniStringView query_str(env, query);
+    CHECK_ADBC_ERROR(AdbcStatementSetSqlQuery(ptr, query_str.value, &error), 
error);
+  } catch (const AdbcException& e) {
+    e.ThrowJavaException(env);
+  }
+}
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniConnection.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniConnection.java
new file mode 100644
index 000000000..e16f8c861
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniConnection.java
@@ -0,0 +1,52 @@
+/*
+ * 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 org.apache.arrow.adbc.core.AdbcConnection;
+import org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcStatement;
+import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
+import org.apache.arrow.adbc.driver.jni.impl.NativeConnectionHandle;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class JniConnection implements AdbcConnection {
+  private final BufferAllocator allocator;
+  private final NativeConnectionHandle handle;
+
+  public JniConnection(BufferAllocator allocator, NativeConnectionHandle 
handle) {
+    this.allocator = allocator;
+    this.handle = handle;
+  }
+
+  @Override
+  public AdbcStatement createStatement() throws AdbcException {
+    return new JniStatement(allocator, 
JniLoader.INSTANCE.openStatement(handle));
+  }
+
+  @Override
+  public ArrowReader getInfo(int @Nullable [] infoCodes) throws AdbcException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void close() {
+    handle.close();
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDatabase.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDatabase.java
new file mode 100644
index 000000000..1eaea0c5a
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDatabase.java
@@ -0,0 +1,45 @@
+/*
+ * 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 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.driver.jni.impl.JniLoader;
+import org.apache.arrow.adbc.driver.jni.impl.NativeDatabaseHandle;
+import org.apache.arrow.memory.BufferAllocator;
+
+public class JniDatabase implements AdbcDatabase {
+  private final BufferAllocator allocator;
+  private final NativeDatabaseHandle handle;
+
+  public JniDatabase(BufferAllocator allocator, NativeDatabaseHandle handle) {
+    this.allocator = allocator;
+    this.handle = handle;
+  }
+
+  @Override
+  public AdbcConnection connect() throws AdbcException {
+    return new JniConnection(allocator, 
JniLoader.INSTANCE.openConnection(handle));
+  }
+
+  @Override
+  public void close() {
+    handle.close();
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriver.java 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriver.java
new file mode 100644
index 000000000..91586f3b1
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriver.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+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.TypedKey;
+import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
+import org.apache.arrow.adbc.driver.jni.impl.NativeDatabaseHandle;
+import org.apache.arrow.memory.BufferAllocator;
+
+/** An ADBC driver wrapping Arrow Flight SQL. */
+public class JniDriver implements AdbcDriver {
+  public static final TypedKey<String> PARAM_DRIVER = new 
TypedKey<>("jni.driver", String.class);
+
+  private final BufferAllocator allocator;
+
+  public JniDriver(BufferAllocator allocator) {
+    this.allocator = Objects.requireNonNull(allocator);
+  }
+
+  @Override
+  public AdbcDatabase open(Map<String, Object> parameters) throws 
AdbcException {
+    String driverName = PARAM_DRIVER.get(parameters);
+    if (driverName == null) {
+      throw AdbcException.invalidArgument(
+          "[JNI] Must provide String " + PARAM_DRIVER + " parameter");
+    }
+
+    Map<String, String> nativeParameters = new HashMap<>();
+    nativeParameters.put("driver", driverName);
+
+    NativeDatabaseHandle handle = 
JniLoader.INSTANCE.openDatabase(nativeParameters);
+    return new JniDatabase(allocator, handle);
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriverFactory.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriverFactory.java
new file mode 100644
index 000000000..b67a534fd
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniDriverFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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 org.apache.arrow.adbc.core.AdbcDriver;
+import org.apache.arrow.adbc.drivermanager.AdbcDriverFactory;
+import org.apache.arrow.memory.BufferAllocator;
+
+/** Constructs new JniDriver instances. */
+public class JniDriverFactory implements AdbcDriverFactory {
+  @Override
+  public AdbcDriver getDriver(BufferAllocator allocator) {
+    return new JniDriver(allocator);
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniStatement.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniStatement.java
new file mode 100644
index 000000000..62d973748
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/JniStatement.java
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.arrow.adbc.core.AdbcException;
+import org.apache.arrow.adbc.core.AdbcStatement;
+import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
+import org.apache.arrow.adbc.driver.jni.impl.NativeQueryResult;
+import org.apache.arrow.adbc.driver.jni.impl.NativeStatementHandle;
+import org.apache.arrow.c.ArrowArrayStream;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.ipc.ArrowReader;
+
+public class JniStatement implements AdbcStatement {
+  private final BufferAllocator allocator;
+  private final NativeStatementHandle handle;
+
+  public JniStatement(BufferAllocator allocator, NativeStatementHandle handle) 
{
+    this.allocator = allocator;
+    this.handle = handle;
+  }
+
+  @Override
+  public void setSqlQuery(String query) throws AdbcException {
+    JniLoader.INSTANCE.statementSetSqlQuery(handle, query);
+  }
+
+  @Override
+  public QueryResult executeQuery() throws AdbcException {
+    NativeQueryResult result = 
JniLoader.INSTANCE.statementExecuteQuery(handle);
+    // TODO: need to handle result in such a way that we free it even if we 
error here
+    ArrowReader reader;
+    try (final ArrowArrayStream cStream = 
ArrowArrayStream.wrap(result.cDataStream())) {
+      reader = org.apache.arrow.c.Data.importArrayStream(allocator, cStream);
+    }
+    return new QueryResult(result.rowsAffected(), reader);
+  }
+
+  @Override
+  public UpdateResult executeUpdate() throws AdbcException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void prepare() throws AdbcException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void close() {
+    handle.close();
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/JniLoader.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/JniLoader.java
new file mode 100644
index 000000000..d53c33616
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/JniLoader.java
@@ -0,0 +1,100 @@
+/*
+ * 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.impl;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.arrow.adbc.core.AdbcException;
+
+/** Singleton wrapper protecting access to JNI functions. */
+public enum JniLoader {
+  INSTANCE;
+
+  JniLoader() {
+    // The JAR may contain multiple binaries for different platforms, so load 
the appropriate one.
+    final String libraryName = "adbc_driver_jni";
+    String libraryToLoad =
+        libraryName + "/" + getNormalizedArch() + "/" + 
System.mapLibraryName(libraryName);
+
+    try {
+      InputStream is = 
JniLoader.class.getClassLoader().getResourceAsStream(libraryToLoad);
+      if (is == null) {
+        throw new FileNotFoundException(
+            "No JNI library for current platform, missing from JAR: " + 
libraryToLoad);
+      }
+      File temp =
+          File.createTempFile("adbc-jni-", ".tmp", new 
File(System.getProperty("java.io.tmpdir")));
+      temp.deleteOnExit();
+
+      try (is) {
+        Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
+      }
+      Runtime.getRuntime().load(temp.getAbsolutePath());
+    } catch (IOException e) {
+      throw new IllegalStateException("Error loading native library " + 
libraryToLoad, e);
+    }
+  }
+
+  private String getNormalizedArch() {
+    // Be consistent with our CMake config
+    String arch = System.getProperty("os.arch").toLowerCase(Locale.US);
+    switch (arch) {
+      case "amd64":
+        return "x86_64";
+      case "aarch64":
+        return "aarch_64";
+      default:
+        throw new RuntimeException("ADBC JNI driver not supported on 
architecture " + arch);
+    }
+  }
+
+  public NativeDatabaseHandle openDatabase(Map<String, String> parameters) 
throws AdbcException {
+    String[] nativeParameters = new String[parameters.size() * 2];
+    int index = 0;
+    for (Map.Entry<String, String> parameter : parameters.entrySet()) {
+      nativeParameters[index++] = parameter.getKey();
+      nativeParameters[index++] = parameter.getValue();
+    }
+    return NativeAdbc.openDatabase(1001000, nativeParameters);
+  }
+
+  public NativeConnectionHandle openConnection(NativeDatabaseHandle database) 
throws AdbcException {
+    return NativeAdbc.openConnection(database.getDatabaseHandle());
+  }
+
+  public NativeStatementHandle openStatement(NativeConnectionHandle connection)
+      throws AdbcException {
+    return NativeAdbc.openStatement(connection.getConnectionHandle());
+  }
+
+  public NativeQueryResult statementExecuteQuery(NativeStatementHandle 
statement)
+      throws AdbcException {
+    return NativeAdbc.statementExecuteQuery(statement.getStatementHandle());
+  }
+
+  public void statementSetSqlQuery(NativeStatementHandle statement, String 
query)
+      throws AdbcException {
+    NativeAdbc.statementSetSqlQuery(statement.getStatementHandle(), query);
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeAdbc.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeAdbc.java
new file mode 100644
index 000000000..f087c5fb2
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeAdbc.java
@@ -0,0 +1,40 @@
+/*
+ * 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.impl;
+
+import org.apache.arrow.adbc.core.AdbcException;
+
+/** All the JNI methods. Don't use this directly, prefer {@link JniLoader}. */
+class NativeAdbc {
+  static native NativeDatabaseHandle openDatabase(int version, String[] 
parameters)
+      throws AdbcException;
+
+  static native void closeDatabase(long handle) throws AdbcException;
+
+  static native NativeConnectionHandle openConnection(long databaseHandle) 
throws AdbcException;
+
+  static native void closeConnection(long handle) throws AdbcException;
+
+  static native NativeStatementHandle openStatement(long connectionHandle) 
throws AdbcException;
+
+  static native void closeStatement(long handle) throws AdbcException;
+
+  static native NativeQueryResult statementExecuteQuery(long handle) throws 
AdbcException;
+
+  static native void statementSetSqlQuery(long handle, String query) throws 
AdbcException;
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeConnectionHandle.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeConnectionHandle.java
new file mode 100644
index 000000000..7ad3efec8
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeConnectionHandle.java
@@ -0,0 +1,33 @@
+/*
+ * 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.impl;
+
+public class NativeConnectionHandle extends NativeHandle {
+  NativeConnectionHandle(long nativeHandle) {
+    super(nativeHandle);
+  }
+
+  long getConnectionHandle() {
+    return state.nativeHandle;
+  }
+
+  @Override
+  Closer getCloseFunction() {
+    return NativeAdbc::closeConnection;
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeDatabaseHandle.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeDatabaseHandle.java
new file mode 100644
index 000000000..45c7aa376
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeDatabaseHandle.java
@@ -0,0 +1,33 @@
+/*
+ * 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.impl;
+
+public class NativeDatabaseHandle extends NativeHandle {
+  NativeDatabaseHandle(long nativeHandle) {
+    super(nativeHandle);
+  }
+
+  long getDatabaseHandle() {
+    return state.nativeHandle;
+  }
+
+  @Override
+  Closer getCloseFunction() {
+    return NativeAdbc::closeDatabase;
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeHandle.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeHandle.java
new file mode 100644
index 000000000..a2975faad
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeHandle.java
@@ -0,0 +1,69 @@
+/*
+ * 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.impl;
+
+import java.lang.ref.Cleaner;
+import org.apache.arrow.adbc.core.AdbcException;
+
+/** A wrapper around a C-allocated ADBC resource. */
+abstract class NativeHandle implements AutoCloseable {
+  static final Cleaner cleaner = Cleaner.create();
+
+  protected final State state;
+  private final Cleaner.Cleanable cleanable;
+
+  NativeHandle(long nativeHandle) {
+    this.state = new State(nativeHandle, getCloseFunction());
+    this.cleanable = cleaner.register(this, state);
+  }
+
+  /** Get the native function used to free the resource. */
+  abstract Closer getCloseFunction();
+
+  @Override
+  public void close() {
+    cleanable.clean();
+  }
+
+  protected static class State implements Runnable {
+    long nativeHandle;
+    private final Closer closer;
+
+    State(long nativeHandle, Closer closer) {
+      this.nativeHandle = nativeHandle;
+      this.closer = closer;
+    }
+
+    @Override
+    public void run() {
+      if (nativeHandle == 0) return;
+      final long handle = nativeHandle;
+      nativeHandle = 0;
+      try {
+        closer.close(handle);
+      } catch (AdbcException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  @FunctionalInterface
+  interface Closer {
+    void close(long handle) throws AdbcException;
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeQueryResult.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeQueryResult.java
new file mode 100644
index 000000000..526d615c3
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeQueryResult.java
@@ -0,0 +1,36 @@
+/*
+ * 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.impl;
+
+public class NativeQueryResult {
+  private final long rowsAffected;
+  private final long cDataStream;
+
+  public NativeQueryResult(long rowsAffected, long cDataStream) {
+    this.rowsAffected = rowsAffected;
+    this.cDataStream = cDataStream;
+  }
+
+  public long rowsAffected() {
+    return rowsAffected;
+  }
+
+  public long cDataStream() {
+    return cDataStream;
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeStatementHandle.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeStatementHandle.java
new file mode 100644
index 000000000..cbc2a563f
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/NativeStatementHandle.java
@@ -0,0 +1,33 @@
+/*
+ * 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.impl;
+
+public class NativeStatementHandle extends NativeHandle {
+  NativeStatementHandle(long nativeHandle) {
+    super(nativeHandle);
+  }
+
+  long getStatementHandle() {
+    return state.nativeHandle;
+  }
+
+  @Override
+  Closer getCloseFunction() {
+    return NativeAdbc::closeStatement;
+  }
+}
diff --git 
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/package-info.java
 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/package-info.java
new file mode 100644
index 000000000..ed2ba096a
--- /dev/null
+++ 
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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;
diff --git a/ci/conda_env_python.txt 
b/java/driver/jni/src/main/resources/META-INF/services/org.apache.arrow.adbc.drivermanager.AdbcDriverFactory
similarity index 81%
copy from ci/conda_env_python.txt
copy to 
java/driver/jni/src/main/resources/META-INF/services/org.apache.arrow.adbc.drivermanager.AdbcDriverFactory
index d0d4de475..c592768f1 100644
--- a/ci/conda_env_python.txt
+++ 
b/java/driver/jni/src/main/resources/META-INF/services/org.apache.arrow.adbc.drivermanager.AdbcDriverFactory
@@ -15,18 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-Cython
-importlib-resources
-# nodejs is required by pyright
-nodejs >=13.0.0
-pandas
-pip
-pyarrow-all
-pyright
-pytest
-setuptools
-
-# For integration testing
-polars
-protobuf
-python-duckdb
+org.apache.arrow.adbc.driver.jni.NativeDriverFactory
diff --git 
a/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/JniDriverTest.java
 
b/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/JniDriverTest.java
new file mode 100644
index 000000000..d72436bd0
--- /dev/null
+++ 
b/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/JniDriverTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+import org.apache.arrow.adbc.core.*;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+class JniDriverTest {
+  @Test
+  void minimal() throws Exception {
+    try (final BufferAllocator allocator = new RootAllocator()) {
+      JniDriver driver = new JniDriver(allocator);
+      Map<String, Object> parameters = new HashMap<>();
+      JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_sqlite");
+
+      driver.open(parameters).close();
+    }
+  }
+
+  @Test
+  void querySimple() throws Exception {
+    try (final BufferAllocator allocator = new RootAllocator()) {
+      JniDriver driver = new JniDriver(allocator);
+      Map<String, Object> parameters = new HashMap<>();
+      JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_sqlite");
+
+      try (final AdbcDatabase db = driver.open(parameters);
+          final AdbcConnection conn = db.connect();
+          final AdbcStatement stmt = conn.createStatement()) {
+        stmt.setSqlQuery("SELECT 1");
+        try (final AdbcStatement.QueryResult result = stmt.executeQuery()) {
+          assertThat(result.getReader().loadNextBatch()).isTrue();
+          
assertThat(result.getReader().getVectorSchemaRoot().getVector(0).getObject(0))
+              .isEqualTo(1L);
+        }
+      }
+    }
+  }
+
+  @Test
+  void queryLarge() throws Exception {
+    try (final BufferAllocator allocator = new RootAllocator()) {
+      JniDriver driver = new JniDriver(allocator);
+      Map<String, Object> parameters = new HashMap<>();
+      JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_sqlite");
+
+      try (final AdbcDatabase db = driver.open(parameters);
+          final AdbcConnection conn = db.connect();
+          final AdbcStatement stmt = conn.createStatement()) {
+        stmt.setSqlQuery(
+            "WITH RECURSIVE seq(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM 
seq WHERE i < 65536)"
+                + " SELECT * FROM seq");
+        try (final AdbcStatement.QueryResult result = stmt.executeQuery()) {
+          List<Long> seen = new ArrayList<>();
+          List<Long> expected = LongStream.range(1, 
65537).boxed().collect(Collectors.toList());
+          while (result.getReader().loadNextBatch()) {
+            VectorSchemaRoot vsr = result.getReader().getVectorSchemaRoot();
+            //noinspection resource
+            BigIntVector i =
+                assertThat(vsr.getVector(0))
+                    
.asInstanceOf(InstanceOfAssertFactories.type(BigIntVector.class))
+                    .actual();
+            for (int index = 0; index < vsr.getRowCount(); index++) {
+              assertThat(i.isNull(index)).isFalse();
+              seen.add(i.get(index));
+            }
+          }
+          assertThat(seen).isEqualTo(expected);
+        }
+      }
+    }
+  }
+
+  @Test
+  void queryError() throws Exception {
+    try (final BufferAllocator allocator = new RootAllocator()) {
+      JniDriver driver = new JniDriver(allocator);
+      Map<String, Object> parameters = new HashMap<>();
+      JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_sqlite");
+
+      try (final AdbcDatabase db = driver.open(parameters);
+          final AdbcConnection conn = db.connect();
+          final AdbcStatement stmt = conn.createStatement()) {
+        stmt.setSqlQuery("SELECT ?");
+        AdbcException exc =
+            assertThrows(
+                AdbcException.class,
+                () -> {
+                  //noinspection EmptyTryBlock
+                  try (final AdbcStatement.QueryResult result = 
stmt.executeQuery()) {}
+                });
+        assertThat(exc.getStatus()).isEqualTo(AdbcStatusCode.INVALID_STATE);
+        assertThat(exc).hasMessageContaining("parameter count mismatch");
+      }
+    }
+  }
+}
diff --git a/java/pom.xml b/java/pom.xml
index 99311e689..8dd557163 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -179,6 +179,11 @@
           <artifactId>spotless-maven-plugin</artifactId>
           <version>2.44.3</version>
         </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo.natives</groupId>
+          <artifactId>maven-native-javah</artifactId>
+          <version>1.0-M1</version>
+        </plugin>
       </plugins>
     </pluginManagement>
 
@@ -201,6 +206,7 @@
             <exclude>**/*.log</exclude>
             <exclude>**/target/**</exclude>
             <exclude>.mvn/jvm.config</exclude>
+            <exclude>build/</exclude>
           </excludes>
         </configuration>
         <executions>
@@ -315,5 +321,12 @@
         </plugins>
       </build>
     </profile>
+
+    <profile>
+      <id>jni</id>
+      <modules>
+        <module>driver/jni</module>
+      </modules>
+    </profile>
   </profiles>
 </project>

Reply via email to