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 d3a45225b feat(java/driver/jni): close child resources automatically
(#4441)
d3a45225b is described below
commit d3a45225b567520405eae1cd6821e04a6e99a4b5
Author: David Li <[email protected]>
AuthorDate: Thu Jun 25 16:55:54 2026 -0700
feat(java/driver/jni): close child resources automatically (#4441)
This mimics what the Python driver manager does. Unlike Python, it
simply closes the children.
---
.../arrow/adbc/driver/jni/JniConnection.java | 52 +++++++---
.../apache/arrow/adbc/driver/jni/JniDatabase.java | 22 +++-
.../apache/arrow/adbc/driver/jni/JniStatement.java | 29 +++++-
.../adbc/driver/jni/impl/ChildReferences.java | 62 ++++++++++++
.../adbc/driver/jni/impl/HasChildReferences.java | 21 ++++
.../adbc/driver/jni/impl/NativeQueryResult.java | 6 +-
.../adbc/driver/jni/impl/TiedArrowReader.java | 112 +++++++++++++++++++++
.../arrow/adbc/driver/jni/JniDriverTest.java | 50 ++++++++-
.../arrow/adbc/driver/jni/impl/ImplTest.java | 40 ++++++++
9 files changed, 370 insertions(+), 24 deletions(-)
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
index a67b210f5..23b96ee85 100644
---
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
@@ -25,26 +25,42 @@ import org.apache.arrow.adbc.core.BulkIngestMode;
import org.apache.arrow.adbc.core.IngestOption;
import org.apache.arrow.adbc.core.IsolationLevel;
import org.apache.arrow.adbc.core.TypedKey;
+import org.apache.arrow.adbc.driver.jni.impl.ChildReferences;
+import org.apache.arrow.adbc.driver.jni.impl.HasChildReferences;
import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
import org.apache.arrow.adbc.driver.jni.impl.NativeConnectionHandle;
import org.apache.arrow.adbc.driver.jni.impl.NativeStatementHandle;
import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.util.AutoCloseables;
import org.apache.arrow.vector.ipc.ArrowReader;
import org.apache.arrow.vector.types.pojo.Schema;
import org.checkerframework.checker.nullness.qual.Nullable;
-public class JniConnection implements AdbcConnection {
+public class JniConnection implements AdbcConnection, HasChildReferences {
private final BufferAllocator allocator;
private final NativeConnectionHandle handle;
+ private final ChildReferences childReferences;
+ // Hold the owning database alive, and try to ensure this connection gets
cleaned up before the
+ // database does
+ private @Nullable HasChildReferences parent;
- public JniConnection(BufferAllocator allocator, NativeConnectionHandle
handle) {
+ public JniConnection(
+ BufferAllocator allocator, HasChildReferences parent,
NativeConnectionHandle handle) {
this.allocator = allocator;
this.handle = handle;
+ this.childReferences = new ChildReferences();
+ this.parent = parent;
+ parent.getChildReferences().addReference(this);
+ }
+
+ @Override
+ public ChildReferences getChildReferences() {
+ return childReferences;
}
@Override
public AdbcStatement createStatement() throws AdbcException {
- return new JniStatement(allocator,
JniLoader.INSTANCE.openStatement(handle));
+ return new JniStatement(allocator, this,
JniLoader.INSTANCE.openStatement(handle));
}
@Override
@@ -108,7 +124,7 @@ public class JniConnection implements AdbcConnection {
}
}
- return new JniStatement(allocator, stmtHandle);
+ return new JniStatement(allocator, this, stmtHandle);
} catch (Exception e) {
stmtHandle.close();
throw e;
@@ -117,7 +133,7 @@ public class JniConnection implements AdbcConnection {
@Override
public ArrowReader getInfo(int @Nullable [] infoCodes) throws AdbcException {
- return JniLoader.INSTANCE.connectionGetInfo(handle,
infoCodes).importStream(allocator);
+ return JniLoader.INSTANCE.connectionGetInfo(handle,
infoCodes).importStream(allocator, this);
}
@Override
@@ -138,7 +154,7 @@ public class JniConnection implements AdbcConnection {
tableNamePattern,
tableTypes,
columnNamePattern)
- .importStream(allocator);
+ .importStream(allocator, this);
}
@Override
@@ -151,7 +167,7 @@ public class JniConnection implements AdbcConnection {
@Override
public ArrowReader getTableTypes() throws AdbcException {
- return
JniLoader.INSTANCE.connectionGetTableTypes(handle).importStream(allocator);
+ return
JniLoader.INSTANCE.connectionGetTableTypes(handle).importStream(allocator,
this);
}
@Override
@@ -257,7 +273,9 @@ public class JniConnection implements AdbcConnection {
@Override
public ArrowReader readPartition(ByteBuffer descriptor) throws AdbcException
{
- return JniLoader.INSTANCE.connectionReadPartition(handle,
descriptor).importStream(allocator);
+ return JniLoader.INSTANCE
+ .connectionReadPartition(handle, descriptor)
+ .importStream(allocator, this);
}
@Override
@@ -267,17 +285,27 @@ public class JniConnection implements AdbcConnection {
return JniLoader.INSTANCE
.connectionGetStatistics(
handle, catalogPattern, dbSchemaPattern, tableNamePattern,
approximate)
- .importStream(allocator);
+ .importStream(allocator, this);
}
@Override
public ArrowReader getStatisticNames() throws AdbcException {
- return
JniLoader.INSTANCE.connectionGetStatisticNames(handle).importStream(allocator);
+ return
JniLoader.INSTANCE.connectionGetStatisticNames(handle).importStream(allocator,
this);
}
@Override
- public void close() {
- handle.close();
+ public void close() throws AdbcException {
+ try {
+ AutoCloseables.close(childReferences, handle);
+ } catch (Exception e) {
+ throw AdbcException.internal("[jni] failed to close
connection").withCause(e);
+ } finally {
+ final var parent = this.parent;
+ if (parent != null) {
+ parent.getChildReferences().releaseReference(this);
+ this.parent = null;
+ }
+ }
}
@Override
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
index 946810705..1f5e10eb8 100644
---
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
@@ -21,27 +21,41 @@ import org.apache.arrow.adbc.core.AdbcConnection;
import org.apache.arrow.adbc.core.AdbcDatabase;
import org.apache.arrow.adbc.core.AdbcException;
import org.apache.arrow.adbc.core.TypedKey;
+import org.apache.arrow.adbc.driver.jni.impl.ChildReferences;
+import org.apache.arrow.adbc.driver.jni.impl.HasChildReferences;
import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
import org.apache.arrow.adbc.driver.jni.impl.NativeDatabaseHandle;
import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.util.AutoCloseables;
-public class JniDatabase implements AdbcDatabase {
+public class JniDatabase implements AdbcDatabase, HasChildReferences {
private final BufferAllocator allocator;
private final NativeDatabaseHandle handle;
+ private final ChildReferences childReferences;
public JniDatabase(BufferAllocator allocator, NativeDatabaseHandle handle) {
this.allocator = allocator;
this.handle = handle;
+ this.childReferences = new ChildReferences();
+ }
+
+ @Override
+ public ChildReferences getChildReferences() {
+ return childReferences;
}
@Override
public AdbcConnection connect() throws AdbcException {
- return new JniConnection(allocator,
JniLoader.INSTANCE.openConnection(handle));
+ return new JniConnection(allocator, this,
JniLoader.INSTANCE.openConnection(handle));
}
@Override
- public void close() {
- handle.close();
+ public void close() throws AdbcException {
+ try {
+ AutoCloseables.close(childReferences, handle);
+ } catch (Exception e) {
+ throw AdbcException.internal("[jni] failed to close
database").withCause(e);
+ }
}
@Override
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
index 0e5da15b2..427d6b283 100644
---
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
@@ -24,6 +24,8 @@ import java.util.NoSuchElementException;
import org.apache.arrow.adbc.core.AdbcException;
import org.apache.arrow.adbc.core.AdbcStatement;
import org.apache.arrow.adbc.core.TypedKey;
+import org.apache.arrow.adbc.driver.jni.impl.ChildReferences;
+import org.apache.arrow.adbc.driver.jni.impl.HasChildReferences;
import org.apache.arrow.adbc.driver.jni.impl.JniLoader;
import org.apache.arrow.adbc.driver.jni.impl.NativePartitionResult;
import org.apache.arrow.adbc.driver.jni.impl.NativeQueryResult;
@@ -39,15 +41,28 @@ import org.apache.arrow.vector.ipc.ArrowReader;
import org.apache.arrow.vector.types.pojo.Schema;
import org.checkerframework.checker.nullness.qual.Nullable;
-public class JniStatement implements AdbcStatement {
+public class JniStatement implements AdbcStatement, HasChildReferences {
private final BufferAllocator allocator;
private final NativeStatementHandle handle;
+ private final ChildReferences childReferences;
+ // Hold the owning connection alive, and try to ensure this statement gets
cleaned up before the
+ // connection does
+ private @Nullable HasChildReferences parent;
private @Nullable VectorSchemaRoot bindRoot;
private @Nullable ArrowReader bindStream;
- public JniStatement(BufferAllocator allocator, NativeStatementHandle handle)
{
+ public JniStatement(
+ BufferAllocator allocator, HasChildReferences parent,
NativeStatementHandle handle) {
this.allocator = allocator;
this.handle = handle;
+ this.childReferences = new ChildReferences();
+ this.parent = parent;
+ parent.getChildReferences().addReference(this);
+ }
+
+ @Override
+ public ChildReferences getChildReferences() {
+ return childReferences;
}
@Override
@@ -126,7 +141,7 @@ public class JniStatement implements AdbcStatement {
public QueryResult executeQuery() throws AdbcException {
exportBind();
NativeQueryResult result =
JniLoader.INSTANCE.statementExecuteQuery(handle);
- return new QueryResult(result.rowsAffected(),
result.importStream(allocator));
+ return new QueryResult(result.rowsAffected(),
result.importStream(allocator, this));
}
@Override
@@ -171,9 +186,15 @@ public class JniStatement implements AdbcStatement {
@Override
public void close() throws AdbcException {
try {
- AutoCloseables.close(handle, bindStream);
+ AutoCloseables.close(childReferences, handle, bindStream);
} catch (Exception e) {
throw AdbcException.internal("[jni] failed to close
statement").withCause(e);
+ } finally {
+ final var parent = this.parent;
+ if (parent != null) {
+ parent.getChildReferences().releaseReference(this);
+ this.parent = null;
+ }
}
}
diff --git
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/ChildReferences.java
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/ChildReferences.java
new file mode 100644
index 000000000..0db99e58b
--- /dev/null
+++
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/ChildReferences.java
@@ -0,0 +1,62 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+import org.apache.arrow.util.AutoCloseables;
+
+/**
+ * Track child resources for the ADBC FFI.
+ *
+ * <p>You are supposed to close statements before closing the connection
(etc.). This class helps
+ * track those references to prevent misuse at runtime.
+ *
+ * <p>This class is thread-safe.
+ */
+public final class ChildReferences implements AutoCloseable {
+ private final Set<AutoCloseable> openReferences;
+
+ public ChildReferences() {
+ // TODO(lidavidm): we could use caffeine LoadingCache with weakKeys instead
+ this.openReferences =
+ Collections.synchronizedSet(Collections.newSetFromMap(new
WeakHashMap<>()));
+ }
+
+ public void close() throws Exception {
+ // synchronizedSet requires explicit synchronization for iteration
+ synchronized (openReferences) {
+ try {
+ var closeables = new ArrayList<>(openReferences);
+ AutoCloseables.close(closeables);
+ } finally {
+ openReferences.clear();
+ }
+ }
+ }
+
+ public void addReference(AutoCloseable any) {
+ openReferences.add(any);
+ }
+
+ public void releaseReference(AutoCloseable any) {
+ openReferences.remove(any);
+ }
+}
diff --git
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/HasChildReferences.java
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/HasChildReferences.java
new file mode 100644
index 000000000..cbdca1529
--- /dev/null
+++
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/HasChildReferences.java
@@ -0,0 +1,21 @@
+/*
+ * 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 interface HasChildReferences {
+ ChildReferences getChildReferences();
+}
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
index 6399dfbb3..c7409e36a 100644
---
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
@@ -21,6 +21,7 @@ import org.apache.arrow.c.ArrowArrayStream;
import org.apache.arrow.c.Data;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.vector.ipc.ArrowReader;
+import org.checkerframework.checker.nullness.qual.Nullable;
public class NativeQueryResult {
private final long rowsAffected;
@@ -39,10 +40,11 @@ public class NativeQueryResult {
}
/** Import the C Data stream into a Java ArrowReader. */
- public ArrowReader importStream(BufferAllocator allocator) {
+ public ArrowReader importStream(BufferAllocator allocator, @Nullable
HasChildReferences parent) {
try (final ArrowArrayStream cStream =
ArrowArrayStream.allocateNew(allocator)) {
cStream.save(streamSnapshot);
- return Data.importArrayStream(allocator, cStream);
+ final var reader = Data.importArrayStream(allocator, cStream);
+ return new TiedArrowReader(allocator, reader, parent);
}
}
}
diff --git
a/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/TiedArrowReader.java
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/TiedArrowReader.java
new file mode 100644
index 000000000..c3c1abea8
--- /dev/null
+++
b/java/driver/jni/src/main/java/org/apache/arrow/adbc/driver/jni/impl/TiedArrowReader.java
@@ -0,0 +1,112 @@
+/*
+ * 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.IOException;
+import java.util.Map;
+import java.util.Set;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A proxy {@link ArrowReader} that keeps an associated ADBC resource alive.
*/
+class TiedArrowReader extends ArrowReader {
+ private final ArrowReader delegate;
+ private @Nullable HasChildReferences parent;
+
+ TiedArrowReader(
+ BufferAllocator allocator, ArrowReader delegate, @Nullable
HasChildReferences parent) {
+ // XXX: ArrowReader being an abstract class and not an interface is a
massive wart in arrow-java
+ // design
+ super(allocator);
+ this.delegate = delegate;
+ this.parent = parent;
+ if (parent != null) {
+ parent.getChildReferences().addReference(this);
+ }
+ }
+
+ @Override
+ public void close(boolean closeReadSource) throws IOException {
+ try {
+ delegate.close(closeReadSource);
+ } finally {
+ // release even if we couldn't close properly
+ if (parent != null) {
+ parent.getChildReferences().releaseReference(this);
+ }
+ parent = null;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ delegate.close();
+ } finally {
+ if (parent != null) {
+ parent.getChildReferences().releaseReference(this);
+ }
+ parent = null;
+ }
+ }
+
+ @Override
+ public long bytesRead() {
+ return delegate.bytesRead();
+ }
+
+ @Override
+ public boolean loadNextBatch() throws IOException {
+ return delegate.loadNextBatch();
+ }
+
+ @Override
+ public Set<Long> getDictionaryIds() {
+ return delegate.getDictionaryIds();
+ }
+
+ @Override
+ public Dictionary lookup(long id) {
+ return delegate.lookup(id);
+ }
+
+ @Override
+ public Map<Long, Dictionary> getDictionaryVectors() throws IOException {
+ return delegate.getDictionaryVectors();
+ }
+
+ @Override
+ public VectorSchemaRoot getVectorSchemaRoot() throws IOException {
+ return delegate.getVectorSchemaRoot();
+ }
+
+ @Override
+ protected void closeReadSource() {
+ // Not actually called because we delegate all public methods
+ throw new AssertionError();
+ }
+
+ @Override
+ protected Schema readSchema() throws IOException {
+ // Not actually called because we delegate all public methods
+ throw new AssertionError();
+ }
+}
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
index 94712c86f..0605875a1 100644
---
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
@@ -144,7 +144,7 @@ class JniDriverTest {
}
}
- // Ensure strings with characters that differ between UTF-8 and Java's
"modifiefd UTF-8" are
+ // Ensure strings with characters that differ between UTF-8 and Java's
"modified UTF-8" are
// properly serialized
@Test
void queryNonBmpUtf8() throws Exception {
@@ -152,7 +152,10 @@ class JniDriverTest {
JniDriver driver = new JniDriver(allocator);
Map<String, Object> parameters = new HashMap<>();
JniDriver.PARAM_DRIVER.set(parameters, "adbc_driver_sqlite");
- String expected = "\uD83D\uDE00";
+ String expected = "\uD83D\uDE00"; // U+1f600 GRINNING FACE (big-endian
UTF-16)
+ // Sanity check that this encodes to what we expect
+ assertThat(expected.getBytes(StandardCharsets.UTF_8))
+ .isEqualTo(new byte[] {(byte) 0xf0, (byte) 0x9f, (byte) 0x98, (byte)
0x80});
try (final AdbcDatabase db = driver.open(parameters);
final AdbcConnection conn = db.connect();
@@ -186,6 +189,49 @@ class JniDriverTest {
}
}
+ @Test
+ void connectionClosesStatements() 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();
+
+ conn.close();
+
+ assertThatThrownBy(() -> stmt.setSqlQuery("SELECT 1"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Native statement handle is closed");
+ }
+ }
+ }
+
+ @Test
+ void databaseClosesConnections() 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");
+
+ final AdbcDatabase db = driver.open(parameters);
+ final AdbcConnection conn = db.connect();
+ final AdbcStatement stmt = conn.createStatement();
+
+ db.close();
+
+ assertThatThrownBy(() -> stmt.setSqlQuery("SELECT 1"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Native statement handle is closed");
+
+ assertThatThrownBy(conn::commit)
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Native connection handle is closed");
+ }
+ }
+
@Test
void queryLarge() throws Exception {
try (final BufferAllocator allocator = new RootAllocator()) {
diff --git
a/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/impl/ImplTest.java
b/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/impl/ImplTest.java
index e102ebf22..0d8664657 100644
---
a/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/impl/ImplTest.java
+++
b/java/driver/jni/src/test/java/org/apache/arrow/adbc/driver/jni/impl/ImplTest.java
@@ -18,6 +18,7 @@ package org.apache.arrow.adbc.driver.jni.impl;
import static org.assertj.core.api.Assertions.assertThat;
+import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import org.junit.jupiter.api.Test;
@@ -119,4 +120,43 @@ public class ImplTest {
assertThat(buf.remaining()).isEqualTo(3);
assertThat(JniLoader.INSTANCE.internalGetByteBuffer(buf)).isEqualTo(new
byte[] {1, 2, 3});
}
+
+ @Test
+ void childReferencesCloses() throws Exception {
+ ChildReferences refs = new ChildReferences();
+ var flag = new Closeable();
+ refs.addReference(flag);
+ refs.close();
+ assertThat(flag.closed).isTrue();
+ }
+
+ @Test
+ void childReferencesIsWeak() throws Exception {
+ ChildReferences refs = new ChildReferences();
+ var flag = new Closeable();
+ refs.addReference(flag);
+ var ref = new WeakReference<>(flag);
+ //noinspection UnusedAssignment
+ flag = null;
+
+ for (int i = 0; i < 50; i++) {
+ System.gc();
+ if (ref.get() == null) {
+ break;
+ }
+ Thread.sleep(100);
+ }
+
+ assertThat(ref.get()).isNull();
+ refs.close();
+ }
+
+ static final class Closeable implements AutoCloseable {
+ boolean closed = false;
+
+ @Override
+ public void close() throws Exception {
+ closed = true;
+ }
+ }
}