Visitar URL original
feat: Alternative table provider as spark data source by timsaucer · Pull Request #110 · apache/datafusion-java · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3cf0e81
build: consolidate Rust crates into a Cargo workspace + extract nativ…
timsaucer Jun 12, 2026
fd4dd74
build: inherit crate version/edition from workspace; publish datafusi…
timsaucer Jun 12, 2026
2e50d6e
build: drop forward-referencing RAT excludes from foundation split
timsaucer Jun 12, 2026
9d2096c
build: drop overbroad **/*.md RAT exclude
timsaucer Jun 12, 2026
2514f56
docs: add README for datafusion-jni-common
timsaucer Jun 12, 2026
3ff4fda
fix: scope foundation docs/excludes to what 1/6 ships
timsaucer Jun 12, 2026
bb023eb
build: point release scripts at workspace rust-target/ dir
timsaucer Jun 12, 2026
9e9de7d
build: mark datafusion-jni-common publish = false
timsaucer Jun 12, 2026
e87fd64
feat: add plain-C scan ABI crate over Arrow C Data/Stream
timsaucer Jun 15, 2026
2598322
feat: add scan_config / scan_request protobuf for the C scan ABI
timsaucer Jun 15, 2026
cfaac8d
build: compile scan protobufs into datafusion-scan-ffi
timsaucer Jun 15, 2026
4cded33
feat: add datafusion.listing provider decoding ScanConfig
timsaucer Jun 15, 2026
f95191d
feat: add JNI shim over the plain-C scan ABI
timsaucer Jun 15, 2026
bc91ff9
test: end-to-end JVM scan through the JNI shim
timsaucer Jun 15, 2026
436e2b3
test: projection and filter pushdown from Java
timsaucer Jun 15, 2026
d5eefee
feat: wire row limit through the scan ABI
timsaucer Jun 15, 2026
0b24d7c
feat: add Spark DataSourceV2 connector
timsaucer Jun 15, 2026
1a1d0ec
feat: columnar reader on Spark 4.0 with provided Arrow
timsaucer Jun 15, 2026
573b438
feat: push Spark limit into the scan (SupportsPushDownLimit)
timsaucer Jun 15, 2026
a378bb4
docs: add DataFusion-Spark DataSource design doc
timsaucer Jun 15, 2026
301fd13
docs: add ADBC reuse analysis to Spark design doc
timsaucer Jun 16, 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: wire row limit through the scan ABI
Apply ScanRequest.limit end to end so all consumers agree:

- scan core: ScanRequest gains `limit: Option<usize>`, applied as
  df.limit(0, Some(n)) after filters.
- C ABI: df_scan_create gains an `int64 limit` parameter (negative means
  none); header updated.
- JNI shim: build_request maps the proto's optional limit through.

Tests: Rust limit_caps_row_count (demo provider, cap 2 across two
partitions) and Java limitCapsRows (limit 2 over a 3-row CSV). Existing
df_scan_create call sites pass -1. Rust suite 11, Java scan suite 5;
clippy/fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Loading branch information
timsaucer and claude committed Jun 15, 2026
commit d5eefee5dd327e7a815bf1c4c2eeaedcd75dae9d
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,25 @@ void filterPushdownSelectsRows() throws Exception {
}
}

@Test
void limitCapsRows() throws Exception {
byte[] config = csvConfig(writeCsv());
// Pushed limit of 2 over the 3-row CSV.
byte[] request = ScanRequest.newBuilder().setLimit(2).build().toByteArray();

try (BufferAllocator allocator = new RootAllocator();
DatafusionScan scan = DatafusionScan.create(PROVIDER, config, request)) {
int rows = 0;
try (ArrowReader reader = scan.execute(allocator)) {
VectorSchemaRoot root = reader.getVectorSchemaRoot();
while (reader.loadNextBatch()) {
rows += root.getRowCount();
}
}
assertEquals(2, rows, "limit should cap the scan at 2 rows");
}
}

/** Serialize the LogicalExprNode for {@code id >= value}, as the engine's filter pushdown would. */
private static byte[] idAtLeast(long value) {
LogicalExprNode column =
Expand Down
5 changes: 3 additions & 2 deletions native-ffi/include/datafusion_scan.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ int32_t df_scan_schema(DfStr provider, DfBytes options, DfBytes partition,
// Plan a scan. On success writes an owned handle to *out_handle (release with
// df_scan_close). projection is an array of column-name DfStr (empty = all);
// filters is an array of serialized datafusion.LogicalExprNode DfBytes;
// target_partitions / batch_size <= 0 keep DataFusion defaults.
// target_partitions / batch_size <= 0 keep DataFusion defaults; limit < 0 means
// no row limit.
int32_t df_scan_create(DfStr provider, DfBytes options, DfBytes partition,
int32_t target_partitions, int32_t batch_size,
int32_t target_partitions, int32_t batch_size, int64_t limit,
const DfKeyValue* config_overrides, size_t config_overrides_len,
const DfStr* projection, size_t projection_len,
const DfBytes* filters, size_t filters_len,
Expand Down
10 changes: 6 additions & 4 deletions native-ffi/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ pub unsafe extern "C" fn df_scan_schema(
/// Plan a scan. On success writes an owned [`DfScanHandle`] pointer to
/// `*out_handle`; the caller must release it with [`df_scan_close`].
///
/// `config_keys`/`config_values` ... here folded into a single
/// `config_overrides` array of [`DfKeyValue`]. `projection` is an array of
/// column-name [`DfStr`]s (empty selects all). `filters` is an array of
/// serialized `datafusion.LogicalExprNode` [`DfBytes`].
/// Session config overrides are a single `config_overrides` array of
/// [`DfKeyValue`]. `projection` is an array of column-name [`DfStr`]s (empty
/// selects all). `filters` is an array of serialized `datafusion.LogicalExprNode`
/// [`DfBytes`]. `limit` is the pushed row limit; a negative value means none.
///
/// # Safety
/// Array args follow the `(ptr, len)` borrow contract; `out_handle` must be a
Expand All @@ -129,6 +129,7 @@ pub unsafe extern "C" fn df_scan_create(
partition: DfBytes,
target_partitions: c_int,
batch_size: c_int,
limit: i64,
config_overrides: *const DfKeyValue,
config_overrides_len: usize,
projection: *const DfStr,
Expand Down Expand Up @@ -163,6 +164,7 @@ pub unsafe extern "C" fn df_scan_create(
partition: partition.as_slice(),
target_partitions,
batch_size,
limit: if limit < 0 { None } else { Some(limit as usize) },
config_overrides: overrides,
projection: cols,
filters: filter_bytes,
Expand Down
5 changes: 5 additions & 0 deletions native-ffi/src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub struct ScanRequest<'a> {
pub projection: Vec<String>,
/// Each entry is a serialized `datafusion.LogicalExprNode`.
pub filters: Vec<Vec<u8>>,
/// Optional row limit pushed into the scan. `None` means no limit.
pub limit: Option<usize>,
}

/// A planned scan. Holds the context alive for the plan's lifetime.
Expand Down Expand Up @@ -117,6 +119,9 @@ pub fn create(req: ScanRequest<'_>) -> ScanResult<ScanHandle> {
.map_err(|e| ScanError::new(DfStatus::Planning, e.to_string()))?;
df = df.filter(expr)?;
}
if let Some(fetch) = req.limit {
df = df.limit(0, Some(fetch))?;
}

// task_ctx() borrows df; capture before create_physical_plan consumes it.
let task_ctx = Arc::new(df.task_ctx());
Expand Down
1 change: 1 addition & 0 deletions native-ffi/tests/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ fn listing_csv_scans_rows() {
EMPTY,
0,
0,
-1,
ptr::null(),
0,
ptr::null(),
Expand Down
47 changes: 45 additions & 2 deletions native-ffi/tests/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use datafusion::arrow::ffi::FFI_ArrowSchema;
use datafusion::arrow::ffi_stream::{ArrowArrayStreamReader, FFI_ArrowArrayStream};

use datafusion_scan_ffi::abi::{
df_error_free, df_scan_abi_version, df_scan_close, df_scan_create, df_scan_execute_partition,
df_scan_partition_count, df_scan_schema, DfScanHandle,
df_error_free, df_scan_abi_version, df_scan_close, df_scan_create, df_scan_execute,
df_scan_execute_partition, df_scan_partition_count, df_scan_schema, DfScanHandle,
};
use datafusion_scan_ffi::ffi_types::{DfBytes, DfStr};
use datafusion_scan_ffi::{demo, ABI_VERSION};
Expand Down Expand Up @@ -138,6 +138,48 @@ fn execute_partition_roundtrips_arrow_c_stream() {
unsafe { df_scan_close(handle) };
}

#[test]
fn limit_caps_row_count() {
demo::register();
// demo provider has 5 rows across two partitions; cap at 2.
let mut handle: *mut DfScanHandle = ptr::null_mut();
let mut err: *mut c_char = ptr::null_mut();
let status = unsafe {
df_scan_create(
provider(),
EMPTY_BYTES,
EMPTY_BYTES,
0,
0,
2, // limit
ptr::null(),
0,
ptr::null(),
0,
ptr::null(),
0,
&mut handle,
&mut err,
)
};
assert_eq!(status, 0, "err: {:?}", unsafe { take_err(err) });

// Read the whole plan; the limit must hold across partitions.
let mut stream = FFI_ArrowArrayStream::empty();
let mut err2: *mut c_char = ptr::null_mut();
assert_eq!(
unsafe { df_scan_execute(handle, &mut stream, &mut err2) },
0,
"err: {:?}",
unsafe { take_err(err2) }
);
let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) }.expect("import");
let rows: usize = reader.map(|b| b.expect("batch").num_rows()).sum();
assert_eq!(rows, 2, "limit should cap the scan at 2 rows");

unsafe { df_scan_close(handle) };
}

#[test]
fn close_is_null_safe() {
unsafe { df_scan_close(ptr::null_mut()) };
Expand All @@ -154,6 +196,7 @@ fn create_full_scan() -> *mut DfScanHandle {
EMPTY_BYTES,
0,
0,
-1,
ptr::null(),
0,
ptr::null(),
Expand Down
3 changes: 1 addition & 2 deletions native-jni/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,13 @@ fn build_request<'a>(
ProtoScanRequest::decode(scan_request)
.map_err(|e| format!("failed to decode ScanRequest: {e}"))?
};
// NOTE: `req.limit` is carried in the proto but not yet applied by the scan
// core or the C ABI; wire it through in a follow-up so both consumers agree.
Ok(ScanRequest {
provider,
options: config,
partition: &[],
target_partitions: req.target_partitions,
batch_size: req.batch_size,
limit: req.limit.map(|l| l as usize),
config_overrides: req.config_overrides.into_iter().collect(),
projection: req.projection,
filters: req.filters,
Expand Down