Skip to content

feat(sql): register additional tables for Dataset.sql queries#7839

Open
sbrunk wants to merge 5 commits into
lance-format:mainfrom
sbrunk:register-table-sql
Open

feat(sql): register additional tables for Dataset.sql queries#7839
sbrunk wants to merge 5 commits into
lance-format:mainfrom
sbrunk:register-table-sql

Conversation

@sbrunk

@sbrunk sbrunk commented Jul 18, 2026

Copy link
Copy Markdown

Summary

Adds a way to register extra in-memory / provider-backed tables in the DataFusion SessionContext that backs Dataset.sql(...), so a query can join a caller-supplied relation instead of encoding it as a large literal IN (...) list.

New API:

  • Rust SqlQueryBuilder::register_table(name: &str, provider: Arc<dyn TableProvider>) -> Self and a convenience SqlQueryBuilder::register_arrow(name: &str, batches: Vec<RecordBatch>) -> Result<Self>.
  • Java SqlQuery.registerArrow(String name, ArrowArrayStream stream).
  • Python SqlQueryBuilder.register_arrow(name, data) (accepts a pyarrow Table or RecordBatchReader).

Example (Rust): semi-join a caller-supplied id set.

let results = ds
    .sql("SELECT * FROM t WHERE id IN (SELECT id FROM wanted)")
    .table_name("t")
    .register_arrow("wanted", vec![id_batch])?
    .build()
    .await?
    .into_batch_records()
    .await?;

Motivation

Dataset.sql today can only reference the dataset itself. To scope a query by a set of ids (or to join another table) the only options are:

  1. inline the values as a literal IN (...) list, which is expensive to parse/plan for large sets, or
  2. bypass SqlQueryBuilder entirely and hand-build a SessionContext, registering a LanceTableProvider plus the other table (this is what the "join two datasets" example in docs/src/integrations/datafusion.md currently requires).

Option 2 is Rust-only. The Java and Python bindings have no way to add a table to a Dataset.sql query at all. This change exposes the capability through the builder so all three bindings can do it, and so the common case (join an in-memory Arrow relation) is a one-liner.

Design decisions (and alternatives considered)

Table type is Arc<dyn TableProvider>, not Vec<RecordBatch> or a reader. SqlQueryBuilder is #[derive(Clone, Debug)] and the Python binding clones it on every builder call, so any stored field must be Clone + Debug. That rules out impl RecordBatchReader / SendableRecordBatchStream (neither is Clone). Arc<dyn TableProvider> is Clone + Debug, is the most general option (callers can register a MemTable, another dataset's LanceTableProvider, a view, etc.), and matches DataFusion's own SessionContext::register_table(_, Arc<dyn TableProvider>).

register_arrow is a thin, fallible convenience. register_table stays infallible like the other builder methods (table_name, with_row_id, ...). register_arrow wraps Vec<RecordBatch> in a MemTable, which is the fallible part, so it returns Result<Self> and keeps that fallibility at the boundary rather than in build().

Naming. register_table follows the existing in-tree precedent (FtsQueryUDTFBuilder::register_table) and DataFusion's register_table, rather than with_table. with_* in this builder is reserved for scalar toggles.

Empty relations. register_arrow requires at least one batch (it derives the schema from the first) and returns an error otherwise, since an empty Vec<RecordBatch> has no schema. If you need to register an empty relation, build a MemTable with a known schema and use register_table; the query then returns zero rows rather than failing with "table not found". (An earlier approach stored Vec<RecordBatch> and skipped empty inputs in build(), which silently produced a "table not found" error at query time; moving to TableProvider removes that footgun because a provider always carries a schema.)

name: &str matches the other builder methods. impl Into<TableReference> would allow schema-qualified names but would be the only method here not taking &str; happy to switch if preferred.

Duplicate names / multiple calls. register_table may be called more than once. A later duplicate name wins, deduplicated in build() (registering the same name into the DataFusion context twice would otherwise error, since DataFusion's register_table rejects a duplicate rather than replacing it).

In-memory vs streaming. register_arrow builds a MemTable, so its input is materialized. That is by design for an in-memory relation the caller already holds; for a large or streaming source, pass a custom TableProvider to register_table rather than collecting it. A MemTable is used here because a TableProvider must be re-scannable (self-joins, multi-partition planning), which a one-shot reader cannot satisfy.

Cross-language notes

  • Java: the input is an ArrowArrayStream imported through the JNI (ArrowArrayStreamReader::from_raw), the same pattern MergeInsert uses.
    Ownership: the native call consumes (takes ownership of) the C stream; the caller owns the Java ArrowArrayStream handle and closes it afterwards (e.g. try-with-resources), as MergeInsertTest does. registerArrow may be called multiple times to register multiple tables, matching Rust and Python.
    registerArrow rejects a null/blank name or a null stream. A query with registered relations is single-use: the streams are consumed on the first intoBatchRecords(), so a second intoBatchRecords(), or a registerArrow after it, throws.
  • Python: register_arrow accepts a pyarrow Table/RecordBatchReader via the existing convert_reader helper. A general register_table was not added because Python has no Rust TableProvider to pass; the existing FFILanceTableProvider + the external datafusion package already cover provider-level joins (see docs/src/integrations/datafusion.md). A Python register_table accepting an object implementing __datafusion_table_provider__ could be a follow-up.

Add a way to register extra tables in the DataFusion SessionContext that
backs Dataset.sql, so a query can join a caller-supplied relation (for
example an id set to semi-join, or another dataset via LanceTableProvider)
instead of scoping by a large literal IN list.

rust: SqlQueryBuilder gains register_table(name, Arc<dyn TableProvider>)
and a register_arrow(name, Vec<RecordBatch>) convenience that wraps the
batches in a MemTable. build() registers each extra table before running
the query. register_arrow errors on empty input (no schema to derive);
register_table accepts an already-built provider, including an empty one.

java: SqlQuery.registerArrow(name, ArrowArrayStream) imports the stream
through the JNI and registers it as an in-memory table.

python: SqlQueryBuilder.register_arrow(name, data) accepts a pyarrow Table
or RecordBatchReader.

tests: rust unit tests for register_arrow and register_table (semi-join,
direct select, empty relation, empty input error); a java SqlQueryTest
case; and a python test.
@github-actions github-actions Bot added A-python Python bindings A-java Java bindings + JNI enhancement New feature or request labels Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SQL query builders now accept in-memory Arrow relations. Rust registers them with DataFusion, Python exposes table and reader registration, and Java imports Arrow C streams through JNI. Tests cover joins, multiple relations, empty inputs, validation, and single-use behavior.

Changes

Arrow relation registration

Layer / File(s) Summary
Rust relation registration and execution
rust/lance/src/dataset/sql.rs
SqlQueryBuilder stores table providers, converts Arrow batches into MemTables, registers them during query construction, and tests joins, duplicate names, and empty relations.
Python Arrow registration binding
python/src/dataset.rs, python/python/lance/dataset.py, python/python/tests/test_dataset.py
Python accepts Arrow tables or readers, collects batches, forwards registration to Rust, and tests filtered results and empty-reader validation.
Java Arrow stream registration
java/src/main/java/org/lance/SqlQuery.java, java/lance-jni/src/sql.rs, java/src/test/java/org/lance/SqlQueryTest.java
Java stores multiple Arrow C stream addresses, passes them through JNI, reads batches with ArrowArrayStreamReader, registers the relations, and tests multiple relations, validation, and reuse semantics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SqlQuery
  participant JNI
  participant ArrowArrayStreamReader
  participant SqlQueryBuilder
  participant DataFusion
  SqlQuery->>JNI: pass extra relation names and stream addresses
  JNI->>ArrowArrayStreamReader: read Arrow C stream batches
  ArrowArrayStreamReader->>SqlQueryBuilder: register Arrow batches
  SqlQueryBuilder->>DataFusion: register relation and execute SQL
  DataFusion->>SqlQuery: return query batches
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: registering extra tables for Dataset.sql queries.
Description check ✅ Passed The description matches the changeset and explains the new registration APIs and motivation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
rust/lance/src/dataset/sql.rs-458-460 (1)

458-460: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the invalid-input error contract.

Line 460 accepts any failure. Assert both the invalid-input variant and message content so an unrelated regression cannot satisfy this test. As per coding guidelines, “Assert on both the error variant and the message content in tests; do not check only is_err().”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 458 - 460, Strengthen the test
around register_arrow in the SQL test by matching the returned error’s
invalid-input variant and verifying its message contains the expected
schema-related text, rather than asserting only err.is_err().

Source: Coding guidelines

rust/lance/src/dataset/sql.rs-425-456 (1)

425-456: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test duplicate registration semantics.

register_table documents that the later duplicate name wins, but no test locks down repeated registration. Add a chain with the same name twice and assert the later relation is queried. As per coding guidelines, “Every bugfix and feature must have corresponding tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 425 - 456, Add coverage in
test_sql_register_arrow for duplicate registration by chaining register_arrow
with the same relation name twice and different data, then query that relation
and assert the results come from the later registration. Keep the test focused
on verifying that repeated registration replaces the earlier relation.

Source: Coding guidelines

python/python/tests/test_dataset.py-5384-5401 (1)

5384-5401: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover the advertised reader input path.

This only exercises pa.Table; add pa.RecordBatchReader coverage and parameterize the shared success case. Also assert the empty-reader ValueError contract, since that error is translated by the PyO3 binding. As per coding guidelines, “Use @pytest.mark.parametrize for Python tests that differ only by inputs” and “Every bugfix and feature must have corresponding tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/python/tests/test_dataset.py` around lines 5384 - 5401, The
test_dataset_sql_register_arrow success case should cover both pa.Table and
pa.RecordBatchReader inputs. Parameterize the shared test using these two input
forms, and add a separate assertion that registering an empty
pa.RecordBatchReader raises the expected ValueError translated by the PyO3
binding.

Source: Coding guidelines

java/src/main/java/org/lance/SqlQuery.java-52-55 (1)

52-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate Arrow registration inputs and avoid reusing consumed streams.
registerArrow() forwards name and stream unchecked: a null stream NPEs on memoryAddress(), and blank names should be rejected at the boundary. intoBatchRecords() consumes the registered C stream, but the raw address stays cached on the object, so a second call can reuse a dead stream. Clear the registration state after use or make the query one-shot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/main/java/org/lance/SqlQuery.java` around lines 52 - 55, Update
SqlQuery.registerArrow to reject null streams and blank names before storing
registration state. In intoBatchRecords, clear extraTableName and
extraStreamAddress after consuming the registered Arrow stream, ensuring
subsequent calls cannot reuse the dead C stream; preserve normal behavior for
valid first use.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@java/src/main/java/org/lance/SqlQuery.java`:
- Around line 68-78: The intoBatchRecords() flow must reject reuse of the
consumed stream registered by registerArrow(). Track whether extraStreamAddress
has been consumed, mark it consumed after the first JNI invocation, and fail
fast on later calls until registerArrow() provides a new stream; preserve normal
behavior for the initial use.

In `@rust/lance/src/dataset/sql.rs`:
- Around line 53-64: Add concise, runnable Rust documentation examples for
DatasetSql::register_table and DatasetSql::register_arrow, using their actual
signatures and demonstrating SQL through a join or subquery. In
python/src/dataset.rs at lines 3957-3963, add a synchronized Python-facing
example showing relation registration and subsequent SQL use; update both public
API docs without changing implementation behavior.
- Around line 64-72: The SQL registration flow must avoid materializing reader
streams into memory. In rust/lance/src/dataset/sql.rs lines 64-72, retain
register_arrow as the batch convenience API and add a reader/provider-backed
registration path; in python/src/dataset.rs lines 3965-3973, pass the converted
reader to that path instead of collecting batches; and in
java/lance-jni/src/sql.rs lines 111-120, retain the imported C-stream reader in
the registered provider rather than accumulating batches.
- Around line 418-423: Update the collect_ids function to access each
RecordBatch’s test output field by the named “id” accessor (b["id"]) instead of
positional column(0), while preserving the existing Int32 extraction and
collection behavior.

---

Other comments:
In `@java/src/main/java/org/lance/SqlQuery.java`:
- Around line 52-55: Update SqlQuery.registerArrow to reject null streams and
blank names before storing registration state. In intoBatchRecords, clear
extraTableName and extraStreamAddress after consuming the registered Arrow
stream, ensuring subsequent calls cannot reuse the dead C stream; preserve
normal behavior for valid first use.

In `@python/python/tests/test_dataset.py`:
- Around line 5384-5401: The test_dataset_sql_register_arrow success case should
cover both pa.Table and pa.RecordBatchReader inputs. Parameterize the shared
test using these two input forms, and add a separate assertion that registering
an empty pa.RecordBatchReader raises the expected ValueError translated by the
PyO3 binding.

In `@rust/lance/src/dataset/sql.rs`:
- Around line 458-460: Strengthen the test around register_arrow in the SQL test
by matching the returned error’s invalid-input variant and verifying its message
contains the expected schema-related text, rather than asserting only
err.is_err().
- Around line 425-456: Add coverage in test_sql_register_arrow for duplicate
registration by chaining register_arrow with the same relation name twice and
different data, then query that relation and assert the results come from the
later registration. Keep the test focused on verifying that repeated
registration replaces the earlier relation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 8889e12e-6fef-45cc-9a1a-6ea5f292b178

📥 Commits

Reviewing files that changed from the base of the PR and between da5ef90 and cb5a850.

📒 Files selected for processing (7)
  • java/lance-jni/src/sql.rs
  • java/src/main/java/org/lance/SqlQuery.java
  • java/src/test/java/org/lance/SqlQueryTest.java
  • python/python/lance/dataset.py
  • python/python/tests/test_dataset.py
  • python/src/dataset.rs
  • rust/lance/src/dataset/sql.rs

Comment thread java/src/main/java/org/lance/SqlQuery.java Outdated
Comment on lines +53 to +64
/// Register an additional table named `name` in the query's DataFusion context, joinable alongside the
/// dataset. Accepts any `TableProvider`, such as a `MemTable` (an in-memory Arrow relation), another Lance
/// dataset's `LanceTableProvider`, or a view. May be called more than once; a later duplicate name wins.
pub fn register_table(mut self, name: &str, provider: Arc<dyn TableProvider>) -> Self {
self.extra_tables.push((name.to_string(), provider));
self
}

/// Convenience over [`Self::register_table`] that wraps in-memory Arrow `batches` in a `MemTable`. `batches`
/// must be non-empty (the schema is derived from the first batch); for an empty relation, build a `MemTable`
/// yourself and pass it to [`Self::register_table`].
pub fn register_arrow(self, name: &str, batches: Vec<RecordBatch>) -> lance_core::Result<Self> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add runnable examples for the new public registration APIs.

The Rust and PyO3-visible APIs describe the behavior but provide no synchronized usage example.

  • rust/lance/src/dataset/sql.rs#L53-L64: add concise Rust examples for register_table and register_arrow, including a join or subquery.
  • python/src/dataset.rs#L3957-L3963: add a Python-facing example showing relation registration and SQL use.

As per coding guidelines, “Document all public APIs with examples and links to relevant structs and methods; keep examples synchronized with actual signatures.”

📍 Affects 2 files
  • rust/lance/src/dataset/sql.rs#L53-L64 (this comment)
  • python/src/dataset.rs#L3957-L3963
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 53 - 64, Add concise, runnable
Rust documentation examples for DatasetSql::register_table and
DatasetSql::register_arrow, using their actual signatures and demonstrating SQL
through a join or subquery. In python/src/dataset.rs at lines 3957-3963, add a
synchronized Python-facing example showing relation registration and subsequent
SQL use; update both public API docs without changing implementation behavior.

Source: Coding guidelines

Comment on lines +64 to +72
pub fn register_arrow(self, name: &str, batches: Vec<RecordBatch>) -> lance_core::Result<Self> {
let schema = batches.first().map(|b| b.schema()).ok_or_else(|| {
lance_core::Error::invalid_input(
"register_arrow requires a non-empty relation to derive a schema; \
use register_table with a MemTable for an empty relation"
.to_string(),
)
})?;
Ok(self.register_table(name, Arc::new(MemTable::try_new(schema, vec![batches])?)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not materialize arbitrary Arrow readers before registration.

Python and Java accept reader/stream inputs, but the core API forces both bindings to collect every batch into a Vec before DataFusion can execute. Large or unbounded readers can exhaust memory.

  • rust/lance/src/dataset/sql.rs#L64-L72: retain the batch convenience API, but add a reader/provider-backed registration path.
  • python/src/dataset.rs#L3965-L3973: forward the converted reader to that path instead of collecting it.
  • java/lance-jni/src/sql.rs#L111-L120: retain the imported C-stream reader in the registered provider rather than accumulating batches.

As per coding guidelines, “Avoid collecting streams of RecordBatch into memory.”

📍 Affects 3 files
  • rust/lance/src/dataset/sql.rs#L64-L72 (this comment)
  • python/src/dataset.rs#L3965-L3973
  • java/lance-jni/src/sql.rs#L111-L120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 64 - 72, The SQL registration
flow must avoid materializing reader streams into memory. In
rust/lance/src/dataset/sql.rs lines 64-72, retain register_arrow as the batch
convenience API and add a reader/provider-backed registration path; in
python/src/dataset.rs lines 3965-3973, pass the converted reader to that path
instead of collecting batches; and in java/lance-jni/src/sql.rs lines 111-120,
retain the imported C-stream reader in the registered provider rather than
accumulating batches.

Source: Coding guidelines

Comment thread rust/lance/src/dataset/sql.rs
sbrunk added 2 commits July 18, 2026 13:54
SqlQuery.registerArrow now accumulates into a list rather than a single
field, and intoBatchRecords passes the (name, stream) pairs as two parallel
lists through the JNI, which loops and registers each. Rust and Python
already accept multiple tables (they accumulate into a Vec).

Add SqlQueryTest.testRegisterArrowMultiple covering two tables in one query.
- build(): a table name registered more than once now keeps the last
  provider (dedup), instead of erroring when the same name is registered
  twice; register_table/register_arrow docs already state last wins.
- java: SqlQuery.registerArrow rejects null/blank names and null streams; a
  query with registered relations is single-use (intoBatchRecords consumes
  the streams, so a second call throws instead of reusing dead pointers).
- docs: add runnable examples to register_table and register_arrow (Rust)
  and to the Python register_arrow.
- tests: use the b["id"] accessor; assert the empty-input error variant and
  message; cover duplicate-name registration; Java input validation and
  single-use; Python RecordBatchReader input and empty-input ValueError.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/sql.rs (1)

141-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve full input context in registration validation errors.

  • rust/lance/src/dataset/sql.rs#L141-L146: include name and batches.len() in the InvalidInput message.
  • java/src/main/java/org/lance/SqlQuery.java#L61-L66: include the parameter names and rejected values in both IllegalArgumentException messages.

As per coding guidelines, “Include full error context, including variable names, values, sizes, and types.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 141 - 146, The registration
validation errors lack full input context. In rust/lance/src/dataset/sql.rs
lines 141-146, update the register_arrow validation message to include the
relation name and batches.len(); in java/src/main/java/org/lance/SqlQuery.java
lines 61-66, update both IllegalArgumentException messages to include the
relevant parameter names and rejected values.

Source: Coding guidelines

🟡 Other comments (1)
java/src/main/java/org/lance/SqlQuery.java-53-54 (1)

53-54: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not attribute last-registration-wins behavior to DataFusion.

This behavior is implemented by SqlQueryBuilder::build deduplicating registrations in reverse order; the Rust comment states duplicate DataFusion registration would otherwise error.

-   * the later registration replaces the earlier one at query time (DataFusion register semantics).
+   * the later registration replaces the earlier one at query time.

As per coding guidelines, “Ensure doc comments match actual semantics.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/main/java/org/lance/SqlQuery.java` around lines 53 - 54, Update the
JavaDoc for SqlQuery registration to remove the attribution of
last-registration-wins behavior to DataFusion. Describe duplicate-name handling
as the behavior implemented by SqlQueryBuilder::build through reverse-order
deduplication, while preserving the guidance that multiple tables may be
registered.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/dataset/sql.rs`:
- Around line 141-146: The registration validation errors lack full input
context. In rust/lance/src/dataset/sql.rs lines 141-146, update the
register_arrow validation message to include the relation name and
batches.len(); in java/src/main/java/org/lance/SqlQuery.java lines 61-66, update
both IllegalArgumentException messages to include the relevant parameter names
and rejected values.

---

Other comments:
In `@java/src/main/java/org/lance/SqlQuery.java`:
- Around line 53-54: Update the JavaDoc for SqlQuery registration to remove the
attribution of last-registration-wins behavior to DataFusion. Describe
duplicate-name handling as the behavior implemented by SqlQueryBuilder::build
through reverse-order deduplication, while preserving the guidance that multiple
tables may be registered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: d78597b2-beec-48f5-af2c-6c91b4837917

📥 Commits

Reviewing files that changed from the base of the PR and between cb5a850 and e1cd1a0.

📒 Files selected for processing (6)
  • java/lance-jni/src/sql.rs
  • java/src/main/java/org/lance/SqlQuery.java
  • java/src/test/java/org/lance/SqlQueryTest.java
  • python/python/lance/dataset.py
  • python/python/tests/test_dataset.py
  • rust/lance/src/dataset/sql.rs

- register_arrow: the empty-input error now includes the table name and the
  batch count.
- java registerArrow: the null/blank-name and null-stream errors now include
  the parameter context and rejected value.
- java javadoc: describe last-registration-wins as build()-side dedup, not
  DataFusion register semantics (a MemorySchemaProvider errors on a duplicate
  name; the last-wins behavior is implemented by SqlQueryBuilder::build).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
rust/lance/src/dataset/sql.rs (2)

93-96: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid relation names at the API boundary.

register_table accepts empty or whitespace-only names and defers failure until build(), while register_arrow inherits that behavior. Validate names before storing them or during build(), returning a descriptive InvalidInput containing the offending name.

As per coding guidelines, “Validate inputs at API boundaries and reject invalid values with descriptive errors.”

Also applies to: 140-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 93 - 96, Update register_table to
reject empty or whitespace-only relation names at the API boundary before
pushing into extra_tables, returning a descriptive InvalidInput error that
includes the offending name. Ensure register_arrow receives the same validation
behavior, either by reusing the shared validation or validating during build
without storing invalid entries.

Source: Coding guidelines


562-568: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Borrow err in the variant assertion.

matches!(err, ...) moves the non-Copy error, so the format argument and subsequent err.to_string() use a moved value. Match by reference instead.

Proposed fix
-            matches!(err, lance_core::Error::InvalidInput { .. }),
+            matches!(&err, lance_core::Error::InvalidInput { .. }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/sql.rs` around lines 562 - 568, Update the variant
assertion in the affected test to match against a reference to err rather than
moving the non-Copy error. Preserve the existing InvalidInput validation and
subsequent err formatting and to_string checks.
java/src/main/java/org/lance/SqlQuery.java (2)

89-94: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Set consumed after output-stream allocation.

The flag is set before ArrowArrayStream.allocateNew(...); if allocation fails, JNI was never called and the input streams remain unconsumed, but retries are permanently blocked. Move the assignment inside the try block immediately before the native invocation.

Proposed fix
-    if (!extraTableNames.isEmpty()) {
-      consumed = true;
-    }
     try (ArrowArrayStream s = ArrowArrayStream.allocateNew(dataset.allocator())) {
+      if (!extraTableNames.isEmpty()) {
+        consumed = true;
+      }
       intoBatchRecords(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/main/java/org/lance/SqlQuery.java` around lines 89 - 94, Move the
consumed assignment for registered streams from before ArrowArrayStream
allocation into the try block immediately before the native invocation,
preserving the extraTableNames guard. This ensures failed output-stream
allocation does not block retries while still marking streams consumed before
JNI execution.

61-70: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject registration after the query is consumed.

registerArrow can append a new stream after consumed is already true. The method succeeds, but the next intoBatchRecords() call will always reject it, leaving an unexecutable registration and a raw stream address to clean up. Fail fast before mutating either list.

Proposed fix
  public SqlQuery registerArrow(String name, ArrowArrayStream stream) {
+   if (consumed) {
+     throw new IllegalStateException("registerArrow cannot be called after intoBatchRecords()");
+   }
    if (name == null || name.trim().isEmpty()) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/main/java/org/lance/SqlQuery.java` around lines 61 - 70, Update
registerArrow to reject calls when the query’s consumed state is already true,
before modifying extraTableNames or extraStreamAddresses. Throw the established
argument/state exception with a clear message, while preserving the existing
name and stream validation for unconsumed queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@java/src/main/java/org/lance/SqlQuery.java`:
- Around line 89-94: Move the consumed assignment for registered streams from
before ArrowArrayStream allocation into the try block immediately before the
native invocation, preserving the extraTableNames guard. This ensures failed
output-stream allocation does not block retries while still marking streams
consumed before JNI execution.
- Around line 61-70: Update registerArrow to reject calls when the query’s
consumed state is already true, before modifying extraTableNames or
extraStreamAddresses. Throw the established argument/state exception with a
clear message, while preserving the existing name and stream validation for
unconsumed queries.

In `@rust/lance/src/dataset/sql.rs`:
- Around line 93-96: Update register_table to reject empty or whitespace-only
relation names at the API boundary before pushing into extra_tables, returning a
descriptive InvalidInput error that includes the offending name. Ensure
register_arrow receives the same validation behavior, either by reusing the
shared validation or validating during build without storing invalid entries.
- Around line 562-568: Update the variant assertion in the affected test to
match against a reference to err rather than moving the non-Copy error. Preserve
the existing InvalidInput validation and subsequent err formatting and to_string
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 6295ee18-e1b0-47c6-8144-0a28ad62459b

📥 Commits

Reviewing files that changed from the base of the PR and between e1cd1a0 and 468a885.

📒 Files selected for processing (2)
  • java/src/main/java/org/lance/SqlQuery.java
  • rust/lance/src/dataset/sql.rs

- build(): reject an empty or whitespace-only registered table name with a
  descriptive InvalidInput, so register_table/register_arrow both validate
  names (previously only the Java binding did).
- java: reject registerArrow after the query was consumed, and set the
  consumed flag inside the try (just before the native call) so a failed
  output-stream allocation does not permanently block a retry.
- tests: blank-name rejection (rust); register-after-consumed (java).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-java Java bindings + JNI A-python Python bindings enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant