Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b64accb
build(native): add async-trait and futures deps for Java data sources
andygrove May 18, 2026
484cd12
refactor(native): lift jthrowable_to_string into shared jni_util module
andygrove May 18, 2026
b699291
feat(datasource): add DataSource interface and SessionContext.registe…
andygrove May 18, 2026
e16a99e
feat(native): add JavaDataSource TableProvider and JNI registration
andygrove May 18, 2026
79213dc
docs(native): clarify JavaScanExec safety + schema check + JVM attach
andygrove May 18, 2026
9c60f3c
feat(datasource)!: pass framework allocator to DataSource.scan
andygrove May 18, 2026
cd03d90
test(datasource): cover repeated scans within a single query
andygrove May 18, 2026
1004f6c
test(datasource): cover empty-stream scan
andygrove May 18, 2026
bf9c435
test(datasource): cover column projection through DataFusion
andygrove May 18, 2026
0ff2d8c
test(datasource): reject scan whose schema differs from registered sc…
andygrove May 18, 2026
248dc70
test(datasource): surface Java exception class and message from scan()
andygrove May 18, 2026
82d13fb
test(datasource): reject null ArrowReader from scan()
andygrove May 18, 2026
953fcf2
test(datasource): cover joining two registered Java data sources
andygrove May 18, 2026
af57098
docs(datasource): document SessionContext.registerDataSource
andygrove May 18, 2026
a4eb41e
docs(datasource): clarify scan() is per-physical-scan, not per-query
andygrove May 18, 2026
82c740a
feat(examples): add JDBC-backed DataSource example using H2 + arrow-jdbc
andygrove May 18, 2026
1df2bd2
refactor(datasource)!: rename DataSource API to TableProvider
andygrove May 19, 2026
9e8279d
Merge remote-tracking branch 'apache/main' into feat/columnar-value-udf
andygrove May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(datasource): add DataSource interface and SessionContext.registe…
…rDataSource Java API
  • Loading branch information
andygrove committed May 18, 2026
commit b69929183011f1d48b547c49e795b7f0568f1047
47 changes: 47 additions & 0 deletions core/src/main/java/org/apache/datafusion/DataSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.datafusion;

import org.apache.arrow.vector.ipc.ArrowReader;
import org.apache.arrow.vector.types.pojo.Schema;

/**
* A Java-implemented table that can be registered with a {@link SessionContext}.
*
* <p>Each call to {@link #scan()} must return a fresh, independent {@link ArrowReader} so that
* queries which touch the table more than once (self-joins, {@code UNION ALL}, repeated reads) work
* correctly. The returned reader is closed by the framework when the stream ends.
*
* <p>The schema returned by {@link #schema()} is captured once at registration time. Every batch
* produced by every {@code ArrowReader} returned from {@link #scan()} must conform to it; a
* mismatch fails the query.
*/
public interface DataSource {
/** The fixed schema of this table. Called once, at registration time. */
Schema schema();

/**
* Open a fresh batch stream for this table. Called once per query that scans the table.
*
* <p>Each invocation MUST return an independent {@link ArrowReader}. The reader's schema MUST
* equal {@link #schema()}.
*/
ArrowReader scan();
}
35 changes: 35 additions & 0 deletions core/src/main/java/org/apache/datafusion/SessionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,38 @@ public void registerUdf(ScalarUdf udf) {
registerScalarUdf(nativeHandle, name, signatureBytes, volatility.code(), impl);
}

/**
* Register a Java-implemented data source as a table. SQL queries that reference {@code name}
* call back into {@code source} to fetch batches.
*
* <p>{@link DataSource#schema()} is called once here, on the calling thread, and cached on the
* native side. {@link DataSource#scan()} is called once per query that touches the table, on a
* Tokio worker thread; it must return a fresh, independent {@link
* org.apache.arrow.vector.ipc.ArrowReader} on every call.
*
* @throws IllegalArgumentException if {@code name} or {@code source} is {@code null}.
* @throws IllegalStateException if {@code source.schema()} returns {@code null}, or this context
* is closed.
* @throws RuntimeException if native registration fails.
*/
public void registerDataSource(String name, DataSource source) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is basically a simplified API on top of the SessionContext::register_table rust function, what if we called the java function that instead (registerTable), and made the interface it accepts TableProvider?

I get that this PR is basically barebones support for custom table registration in java, and that data_source.rs is handling a lot so the java user gets a simple scan() callback. I think only providing that for now makes sense as a first step (and will always be useful for simple cases), but I'd like to make sure this can evolve towards all the flexibility of the TableProvider trait that interacts with ExecutionPlan and ultimately an ArrowReader. The LiteralGuaranteeTest from my bindings demonstrates what this could look like and what it enables (filter pushdown).

To keep things minimal for PR, maybe we could just

  • rename registerDataSource to registerTable
  • rename the DataSource interface to TableProvider
  • provide a simple implementation of TableProvider that just holds what the current DataSource does - not sure about a name for that, but maybe like SimpleTableProvider or FullScanTableProvider or something

Then we can make TableProvider more featured over time. Totally open to other ideas too.

Part of my motivation in renaming is that in the back of my head I'm thinking about eventual support for the separate DataSource, so don't want to clash on naming.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pgwhalen. I have addressed your feedback.

if (nativeHandle == 0) {
throw new IllegalStateException("SessionContext is closed");
}
if (name == null) {
throw new IllegalArgumentException("registerDataSource name must be non-null");
}
if (source == null) {
throw new IllegalArgumentException("registerDataSource source must be non-null");
}
Schema schema = source.schema();
if (schema == null) {
throw new IllegalStateException("DataSource.schema returned null");
}
byte[] schemaIpc = serializeSchemaIpc(schema);
registerDataSourceNative(nativeHandle, name, schemaIpc, source);
}

private static byte[] serializeSchemaIpc(Schema schema) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (BufferAllocator allocator = new RootAllocator();
Expand Down Expand Up @@ -453,4 +485,7 @@ private static native long readJsonWithOptions(

private static native void registerScalarUdf(
long handle, String name, byte[] signatureSchemaBytes, byte volatility, ScalarFunction impl);

private static native void registerDataSourceNative(
long handle, String name, byte[] schemaIpcBytes, DataSource source);
}
29 changes: 29 additions & 0 deletions core/src/main/java/org/apache/datafusion/internal/JniBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
import java.util.List;

import org.apache.arrow.c.ArrowArray;
import org.apache.arrow.c.ArrowArrayStream;
import org.apache.arrow.c.ArrowSchema;
import org.apache.arrow.c.Data;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.FieldVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.ipc.ArrowReader;
import org.apache.datafusion.ColumnarValue;
import org.apache.datafusion.DataSource;
import org.apache.datafusion.ScalarFunction;
import org.apache.datafusion.ScalarFunctionArgs;

Expand Down Expand Up @@ -139,4 +142,30 @@ public static byte invokeScalarUdf(
return resultKind;
}
}

/**
* Open a fresh batch stream from a Java {@link DataSource} and export it through the supplied
* Arrow C Data Interface address. Called from native code; not for application use.
*
* <p>On success, ownership of the returned reader transfers to the FFI stream's release callback,
* so the native side closing the stream also closes the reader. On any failure during export, the
* reader is closed here before the exception propagates.
*/
public static void invokeDataSourceScan(DataSource source, long ffiStreamAddr) {
ArrowReader reader = source.scan();
if (reader == null) {
throw new IllegalStateException("DataSource.scan returned null");
}
ArrowArrayStream stream = ArrowArrayStream.wrap(ffiStreamAddr);
try {
Data.exportArrayStream(ALLOCATOR, reader, stream);
} catch (Throwable t) {
try {
reader.close();
} catch (Exception ignored) {
// best-effort cleanup; original error wins
}
throw t;
}
}
}