Skip to content

fix: Add Substrait roundtrip support for EXISTS and correlated OuterReferenceColumn - #18987

Closed
Nithurshen wants to merge 5 commits into
apache:mainfrom
Nithurshen:fix/substrait-exists-support
Closed

fix: Add Substrait roundtrip support for EXISTS and correlated OuterReferenceColumn#18987
Nithurshen wants to merge 5 commits into
apache:mainfrom
Nithurshen:fix/substrait-exists-support

Conversation

@Nithurshen

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Substrait roundtrip tests were failing for queries involving EXISTS and correlated subqueries (specifically those using outer references). The DataFusion Substrait producer did not support serializing Expr::Exists, and it threw a "feature not implemented" error for OuterReferenceColumn.

Supporting these features is essential for improving the reliability of DataFusion's Substrait integration and enabling complex join scenarios in distributed environments.

What changes are included in this PR?

  1. Producer Support for EXISTS:

    • Implemented from_exists in the producer to map Expr::Exists to the Substrait SetPredicate (using PredicateOp::Existence).
    • Added support for NOT EXISTS by wrapping the predicate in a "not" scalar function.
  2. Producer Support for OuterReferenceColumn:

    • Updated the producer to serialize OuterReferenceColumn as a custom scalar function named "outer_reference". This avoids schema validation errors during serialization since outer columns often cannot be resolved against the local subquery schema.
  3. Consumer Support for OuterReferenceColumn:

    • Updated the consumer to intercept the "outer_reference" scalar function and deserialize it back into a DataFusion OuterReferenceColumn, ensuring a successful roundtrip.

Are these changes tested?

Yes.

  • Verified using the existing SQLLogicTest case that was previously failing: joins.slt.
  • Command run: cargo test --test sqllogictests -- --substrait-round-trip joins.slt:1233

Are there any user-facing changes?

No breaking API changes. This PR purely expands the coverage of supported Logical Plans that can be converted to/from Substrait.

@github-actions github-actions Bot added the substrait Changes to the substrait crate label Nov 29, 2025
@gabotechs

Copy link
Copy Markdown
Contributor

I'm requesting some backup for reviewing this one, thanks for the contribution!

@vbarua vbarua left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this is the best approach to fix this, primarily because you've added a non-standard mechanism to handle outer references. The plans you produce will only work on DataFusion, and not other systems.

If you want to see what this would look like in more standard Substrait, you can use the isthmus tool with the following input:

isthmus \
  --create "CREATE TABLE lsaj_t1(t1_id INT, t1_name VARCHAR, t1_int INT)" \
  --create "CREATE TABLE lsaj_t2(t2_id INT, t2_name VARCHAR, t2_int INT)" \
  "SELECT t1_id, t1_name
FROM lsaj_t1
WHERE NOT EXISTS (SELECT 1 FROM lsaj_t2 WHERE t1_id = t2_id)
ORDER BY t1_id"

to generate a Substrait plan that captures the outer reference using standard Substrait.

};

let fn_name = substrait_fun_name(fn_signature);
if fn_name == "outer_reference" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Substrait already has a mechanism for handling outer references via the OuterReference root_type inside of a FieldReference.

Defining a custom function like you have here means that other systems won't be able to understand these plans, and DataFusion won't understands plans from other systems that use the standard mechanism for outer references.

@Nithurshen Nithurshen closed this Jan 13, 2026
Smallfu666 pushed a commit to Smallfu666/datafusion that referenced this pull request Aug 18, 2026
…field references (apache#23488)

## Which issue does this PR close?

- Closes apache#16280.

## Rationale for this change

The Substrait producer errors on `Expr::OuterReferenceColumn`, so any
plan containing a correlated subquery cannot be serialized currently.
DataFusion's round-trip tests don't hit this because the optimizer
decorrelates subqueries into joins before serialization, but any
workflow that serializes *unoptimized* plans (e.g. sending raw plans
between systems for later optimization in my case) fails on queries like
TPC-H q2/q4/q17/q20/q21/q22, and ~26 cases in `joins.slt` fail in
`--substrait-round-trip` mode.

A previous attempt (apache#18987) was closed because it introduced a
non-standard mechanism for outer references, producing plans only
DataFusion would be able consume but Substrait already represents
correlated references natively via a `FieldReference` with an
`OuterReference` root type and a `steps_out` depth. The consumer side of
was implemented in apache#20439, which resolves `OuterReference` field
references against a stack of outer schemas. This PR implements the
producing half, symmetric with that design, so correlated plans
round-trip using only standard Substrait.

## What changes are included in this PR?

- `SubstraitProducer` gains outer-schema-stack methods mirroring
`SubstraitConsumer`: `push_outer_schema` / `pop_outer_schema` (default
no-ops) and `get_outer_schema(steps_out)` (default `None`), plus a
`handle_outer_reference_column` method so custom producers can override
the behaviour like every other expression kind. Defaults are backward
compatible: existing custom producers are unaffected unless a plan
actually contains an outer reference, in which case they now get an
actionable error instead of `not_impl_err`.
- `DefaultSubstraitProducer` maintains the stack in a
`Vec<DFSchemaRef>`.
- The four subquery producers (`from_in_subquery`,
`from_scalar_subquery`, `from_exists`, `from_set_comparison`) push the
enclosing query's schema around the subquery plan conversion (via a
shared `produce_subquery_rel`, analogous to the consumer's
`consume_subquery_rel`).
- `from_outer_reference_column` (previously unused and emitting an
incorrect plain `RootReference`) now resolves the column against the
outer-schema stack, innermost first, and emits a `FieldReference` with
an `OuterReference` root and the corresponding `steps_out`.
- `to_substrait_rex` dispatches `Expr::OuterReferenceColumn` to the new
handler instead of erroring.

## Are these changes tested?

Yes:

- New round-trip tests in `roundtrip_logical_plan.rs` covering
correlated `EXISTS`, correlated `IN` subquery, correlated scalar
subquery, and a nested correlated subquery that crosses two subquery
boundaries (`steps_out = 2`). Each test asserts the produced plan
contains an `OuterReference` at the expected depth and that the plan
round-trips through the existing consumer with its schema intact.
- Consumer-side resolution was already covered by the tests added in
apache#20439; these tests now exercise both halves together.
- `joins.slt` in `--substrait-round-trip` mode goes from 38 failures to
12; the remaining failures are pre-existing gaps unrelated to outer
references (`USING` join constraint, plan-level lateral
`LogicalPlan::Subquery`, duplicate unqualified field names).

## Are there any user-facing changes?

- Plans containing correlated subqueries now serialize instead of
returning "not implemented", emitting spec-standard `OuterReference`
field references.
- `SubstraitProducer` has three new provided methods and
`handle_outer_reference_column`; all have defaults, so existing
implementations continue to compile.
- The signature of the public helper `from_outer_reference_column`
changed (it now takes the producer and the outer field) — its previous
form resolved against the wrong schema and emitted a plain
`RootReference`, and it was not called from anywhere in the crate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

substrait Changes to the substrait crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[substrait] [sqllogictest] Cannot convert Exists { subquery: <subquery>, negated: true } to Substrait

3 participants