Skip to content

fix(planner): Add filter predicate pushdown to AddExchangesForSingleNodeExecution#27456

Merged
swapsmagic merged 1 commit intoprestodb:masterfrom
swapsmagic:filter_predicate_pushdown_for_single_node_execution
Mar 30, 2026
Merged

fix(planner): Add filter predicate pushdown to AddExchangesForSingleNodeExecution#27456
swapsmagic merged 1 commit intoprestodb:masterfrom
swapsmagic:filter_predicate_pushdown_for_single_node_execution

Conversation

@swapsmagic
Copy link
Copy Markdown
Contributor

@swapsmagic swapsmagic commented Mar 29, 2026

Description

Motivation and Context

CONTEXT:
DESCRIBE / SHOW COLUMNS queries are rewritten into SELECTs against information_schema.columns with a FilterNode (table_schema=X AND table_name=Y) above a TableScanNode. In distributed mode (AddExchanges), visitFilter detects this pattern and pushes the filter predicate into the InformationSchemaTableHandle via pushPredicateIntoTableScan. The connector then knows which table's columns to return.

In single-node execution (AddExchangesForSingleNodeExecution), there was no visitFilter override. The FilterNode passed through unchanged, and visitTableScan only pushed TRUE as the predicate — so the information_schema connector never received the table name filter. This caused DESCRIBE to return wrong/empty results in single-node execution.

Impact

WHAT:
Added a visitFilter override to AddExchangesForSingleNodeExecution.Rewriter that mirrors the logic from AddExchanges.visitFilter:

  1. Detects FilterNode -> TableScanNode where legacy layout is supported
  2. Pushes the actual filter predicate into the table scan
  3. Adds GATHER exchange only for system table scans (same as existing behavior)
  4. Falls through to default rewriting for non-table-scan filters

Plan before fix (single-node DESCRIBE):

OutputNode
SortNode
ProjectNode
FilterNode (table_schema='s' AND table_name='t') <-- predicate stuck here
ExchangeNode (GATHER) <-- from visitTableScan
TableScanNode (info_schema.columns) <-- gets TRUE, no table info

Plan after fix (single-node DESCRIBE):

OutputNode
SortNode
ProjectNode
ExchangeNode (GATHER) <-- same exchange, moved up
TableScanNode (info_schema.columns) <-- gets real predicate

No new exchanges are introduced. The GATHER exchange for system tables was already present (added by visitTableScan). This fix ensures the predicate is pushed down so the connector scans only the target table's columns.

Test Plan

Unit tests and verifier run

Contributor checklist

  • Please make sure your submission complies with our contributing guide, in particular code style and commit standards.
  • PR description addresses the issue accurately and concisely. If the change is non-trivial, a GitHub Issue is referenced.
  • Documented new properties (with its default value), SQL syntax, functions, or other functionality.
  • If release notes are required, they follow the release notes guidelines.
  • Adequate tests were added if applicable.
  • CI passed.
  • If adding new dependencies, verified they have an OpenSSF Scorecard score of 5.0 or higher (or obtained explicit TSC approval for lower scores).

Release Notes

Please follow release notes guidelines and fill in the release notes below.

== RELEASE NOTES ==

General Changes
* Fix DESCRIBE and SHOW COLUMNS queries hanging in PLANNING state on clusters with single-node execution enabled.

@swapsmagic swapsmagic requested review from a team, feilong-liu and jaystarshot as code owners March 29, 2026 16:24
@prestodb-ci prestodb-ci added the from:Meta PR from Meta label Mar 29, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 29, 2026

Reviewer's Guide

Adds filter predicate pushdown support in AddExchangesForSingleNodeExecution for single-node execution, mirroring distributed-mode behavior and verifying it with new plan tests for SHOW COLUMNS/DESCRIBE and regular table scans.

Sequence diagram for filter predicate pushdown in single-node AddExchangesForSingleNodeExecution

sequenceDiagram
    participant Planner
    participant Rewriter as AddExchangesForSingleNodeExecution_Rewriter
    participant Metadata
    participant PlanUtils as PlanNodeUtilities

    Planner->>Rewriter: rewrite(FilterNode)
    Rewriter->>Rewriter: visitFilter(filterNode)

    alt source_is_TableScanNode_and_legacy_layout_supported
        Rewriter->>Metadata: isLegacyGetLayoutSupported(session, tableHandle)
        Metadata-->>Rewriter: legacySupported

        Rewriter->>PlanUtils: pushPredicateIntoTableScan(tableScanNode, predicate, true, session, idAllocator, metadata)
        PlanUtils-->>Rewriter: plan

        Rewriter->>PlanUtils: containsSystemTableScan(plan)
        PlanUtils-->>Rewriter: hasSystemTableScan

        alt hasSystemTableScan
            Rewriter->>PlanUtils: gatheringExchange(idAllocator.getNextId(), REMOTE_STREAMING, plan)
            PlanUtils-->>Rewriter: planWithGather
            Rewriter-->>Planner: planWithGather
        else no_system_table_scan
            Rewriter-->>Planner: plan
        end
    else non_table_scan_source_or_no_legacy_layout
        Rewriter->>Rewriter: context.defaultRewrite(filterNode)
        Rewriter-->>Planner: rewrittenSubtree
    end
Loading

Updated class diagram for AddExchangesForSingleNodeExecution Rewriter with visitFilter

classDiagram
    class AddExchangesForSingleNodeExecution_Rewriter {
        - Session session
        - PlanNodeIdAllocator idAllocator
        - Metadata metadata
        - boolean planChanged
        + PlanNode visitTableScan(TableScanNode node, RewriteContext context)
        + PlanNode visitFilter(FilterNode node, RewriteContext context)
        + PlanNode visitExplainAnalyze(ExplainAnalyzeNode node, RewriteContext context)
    }

    class TableScanNode {
        + TableHandle getTable()
        + Map~Symbol,ColumnHandle~ getAssignments()
        + TupleDomain~ColumnHandle~ getEnforcedConstraint()
    }

    class FilterNode {
        + RowExpression getPredicate()
        + PlanNode getSource()
    }

    class RewriteContext {
        + PlanNode defaultRewrite(PlanNode node)
    }

    class Metadata {
        + boolean isLegacyGetLayoutSupported(Session session, TableHandle table)
    }

    class PlanNodeUtilities {
        + PlanNode pushPredicateIntoTableScan(TableScanNode tableScanNode, RowExpression predicate, boolean forceNewTableScanNode, Session session, PlanNodeIdAllocator idAllocator, Metadata metadata)
        + boolean containsSystemTableScan(PlanNode plan)
        + PlanNode gatheringExchange(PlanNodeId id, ExchangeNodeScope scope, PlanNode source)
    }

    class PlanNode {
    }

    class Session {
    }

    class PlanNodeIdAllocator {
        + PlanNodeId getNextId()
    }

    class PlanNodeId {
    }

    class ExchangeNodeScope {
        <<enum>>
        REMOTE_STREAMING
    }

    AddExchangesForSingleNodeExecution_Rewriter --> Session
    AddExchangesForSingleNodeExecution_Rewriter --> PlanNodeIdAllocator
    AddExchangesForSingleNodeExecution_Rewriter --> Metadata

    AddExchangesForSingleNodeExecution_Rewriter ..> TableScanNode : uses
    AddExchangesForSingleNodeExecution_Rewriter ..> FilterNode : uses
    AddExchangesForSingleNodeExecution_Rewriter ..> RewriteContext : uses

    AddExchangesForSingleNodeExecution_Rewriter ..> PlanNodeUtilities : calls
    PlanNodeUtilities ..> PlanNode : returns

    TableScanNode --|> PlanNode
    FilterNode --|> PlanNode
    ExplainAnalyzeNode --|> PlanNode

    class ExplainAnalyzeNode {
    }
Loading

Flow diagram for logical plan shape before and after single-node DESCRIBE rewrite

flowchart TD
    A["Original logical plan for single-node DESCRIBE / SHOW COLUMNS"] --> B["AddExchangesForSingleNodeExecution_Rewriter visitFilter"]

    subgraph Before_fix
        direction TB
        C["OutputNode"] --> D["SortNode"]
        D --> E["ProjectNode"]
        E --> F["FilterNode(table_schema='s' AND table_name='t')"]
        F --> G["ExchangeNode(GATHER)"]
        G --> H["TableScanNode(information_schema.columns, predicate=TRUE)"]
    end

    subgraph After_fix
        direction TB
        I["OutputNode"] --> J["SortNode"]
        J --> K["ProjectNode"]
        K --> L["ExchangeNode(GATHER)"]
        L --> M["TableScanNode(information_schema.columns, predicate=table_schema='s' AND table_name='t')"]
    end

    B --> Before_fix
    B --> After_fix
Loading

File-Level Changes

Change Details Files
Push filter predicates into table scans during single-node exchange rewriting when applicable, and add exchanges only for system table scans.
  • Override visitFilter in the single-node rewriter to detect FilterNode over TableScanNode when legacy layouts are supported.
  • Use pushPredicateIntoTableScan with the original predicate to rewrite the TableScanNode instead of leaving the predicate above the scan.
  • Wrap the resulting plan in a REMOTE_STREAMING GATHER exchange when the rewritten plan contains a system table scan, reusing existing exchange-creation logic.
  • Mark the plan as changed when this specialized filter handling path is taken, otherwise fall back to the default rewrite behavior.
presto-main-base/src/main/java/com/facebook/presto/sql/planner/optimizations/AddExchangesForSingleNodeExecution.java
Introduce plan tests to validate filter pushdown and exchange placement for single-node execution, especially for SHOW COLUMNS/DESCRIBE and regular tables.
  • Add a BasePlanTest-derived test class that plans queries with SINGLE_NODE_EXECUTION_ENABLED set to true.
  • Verify that SHOW COLUMNS and DESCRIBE plans contain a REMOTE_STREAMING GATHER exchange on system table scans.
  • Assert that filter nodes do not remain above the GATHER exchange for SHOW COLUMNS, ensuring predicates are pushed into the scan.
  • Confirm that regular (non-system) table scans do not gain remote exchanges and that standard filters/table scans remain present and unaffected.
  • Provide a helper method to detect GATHER exchanges within a plan using PlanNodeSearcher.
presto-main-base/src/test/java/com/facebook/presto/sql/planner/optimizations/TestAddExchangesForSingleNodeExecution.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The visitFilter implementation largely mirrors AddExchanges.visitFilter; consider extracting the shared predicate-pushdown logic into a common helper to avoid future divergence between distributed and single-node planning.
  • In visitFilter, you repeatedly access node.getSource() and cast it; assigning the cast TableScanNode to a local before the metadata check would simplify the code and avoid the duplicate instanceof/cast sequence.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The visitFilter implementation largely mirrors AddExchanges.visitFilter; consider extracting the shared predicate-pushdown logic into a common helper to avoid future divergence between distributed and single-node planning.
- In visitFilter, you repeatedly access node.getSource() and cast it; assigning the cast TableScanNode to a local before the metadata check would simplify the code and avoid the duplicate instanceof/cast sequence.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@swapsmagic swapsmagic force-pushed the filter_predicate_pushdown_for_single_node_execution branch 2 times, most recently from e2d898c to ad43d93 Compare March 29, 2026 17:14
@swapsmagic swapsmagic changed the title Add filter predicate pushdown to AddExchangesForSingleNodeExecution Fix: Add filter predicate pushdown to AddExchangesForSingleNodeExecution Mar 29, 2026
@swapsmagic swapsmagic force-pushed the filter_predicate_pushdown_for_single_node_execution branch from ad43d93 to bdf6c04 Compare March 29, 2026 17:15
@swapsmagic swapsmagic changed the title Fix: Add filter predicate pushdown to AddExchangesForSingleNodeExecution fix : Add filter predicate pushdown to AddExchangesForSingleNodeExecution Mar 29, 2026
@swapsmagic swapsmagic changed the title fix : Add filter predicate pushdown to AddExchangesForSingleNodeExecution fix Add filter predicate pushdown to AddExchangesForSingleNodeExecution Mar 30, 2026
CONTEXT:
DESCRIBE / SHOW COLUMNS queries are rewritten into SELECTs against
information_schema.columns with a FilterNode (table_schema=X AND table_name=Y)
above a TableScanNode. In distributed mode (AddExchanges), visitFilter detects
this pattern and pushes the filter predicate into the InformationSchemaTableHandle
via pushPredicateIntoTableScan. The connector then knows which table's columns to
return.

In single-node execution (AddExchangesForSingleNodeExecution), there was no
visitFilter override. The FilterNode passed through unchanged, and visitTableScan
only pushed TRUE as the predicate — so the information_schema connector never
received the table name filter. This caused DESCRIBE to return wrong/empty results
in single-node execution.

WHAT:
Added a visitFilter override to AddExchangesForSingleNodeExecution.Rewriter that
mirrors the logic from AddExchanges.visitFilter:
1. Detects FilterNode -> TableScanNode where legacy layout is supported
2. Pushes the actual filter predicate into the table scan
3. Adds GATHER exchange only for system table scans (same as existing behavior)
4. Falls through to default rewriting for non-table-scan filters

Plan before fix (single-node DESCRIBE):

  OutputNode
    SortNode
      ProjectNode
        FilterNode (table_schema='s' AND table_name='t')  <-- predicate stuck here
          ExchangeNode (GATHER)                           <-- from visitTableScan
            TableScanNode (info_schema.columns)           <-- gets TRUE, no table info

Plan after fix (single-node DESCRIBE):

  OutputNode
    SortNode
      ProjectNode
        ExchangeNode (GATHER)                             <-- same exchange, moved up
          TableScanNode (info_schema.columns)             <-- gets real predicate

No new exchanges are introduced. The GATHER exchange for system tables was already
present (added by visitTableScan). This fix ensures the predicate is pushed down so
the connector scans only the target table's columns.
@swapsmagic swapsmagic force-pushed the filter_predicate_pushdown_for_single_node_execution branch from bdf6c04 to 3b8f46d Compare March 30, 2026 04:03
@swapsmagic swapsmagic changed the title fix Add filter predicate pushdown to AddExchangesForSingleNodeExecution fix: Add filter predicate pushdown to AddExchangesForSingleNodeExecution Mar 30, 2026
@swapsmagic swapsmagic changed the title fix: Add filter predicate pushdown to AddExchangesForSingleNodeExecution fix(planner): Add filter predicate pushdown to AddExchangesForSingleNodeExecution Mar 30, 2026
@swapsmagic swapsmagic merged commit 1967fd1 into prestodb:master Mar 30, 2026
86 of 87 checks passed
bibith4 pushed a commit to bibith4/presto that referenced this pull request Apr 1, 2026
…odeExecution (prestodb#27456)

## Description
<!---Describe your changes in detail-->

## Motivation and Context
CONTEXT:
DESCRIBE / SHOW COLUMNS queries are rewritten into SELECTs against
information_schema.columns with a FilterNode (table_schema=X AND
table_name=Y) above a TableScanNode. In distributed mode (AddExchanges),
visitFilter detects this pattern and pushes the filter predicate into
the InformationSchemaTableHandle via pushPredicateIntoTableScan. The
connector then knows which table's columns to return.

In single-node execution (AddExchangesForSingleNodeExecution), there was
no visitFilter override. The FilterNode passed through unchanged, and
visitTableScan only pushed TRUE as the predicate — so the
information_schema connector never received the table name filter. This
caused DESCRIBE to return wrong/empty results in single-node execution.

## Impact
WHAT:
Added a visitFilter override to
AddExchangesForSingleNodeExecution.Rewriter that mirrors the logic from
AddExchanges.visitFilter:
1. Detects FilterNode -> TableScanNode where legacy layout is supported
2. Pushes the actual filter predicate into the table scan
3. Adds GATHER exchange only for system table scans (same as existing
behavior)
4. Falls through to default rewriting for non-table-scan filters

Plan before fix (single-node DESCRIBE):

  OutputNode
    SortNode
      ProjectNode
FilterNode (table_schema='s' AND table_name='t') <-- predicate stuck
here
ExchangeNode (GATHER) <-- from visitTableScan
TableScanNode (info_schema.columns) <-- gets TRUE, no table info

Plan after fix (single-node DESCRIBE):

  OutputNode
    SortNode
      ProjectNode
ExchangeNode (GATHER) <-- same exchange, moved up
TableScanNode (info_schema.columns) <-- gets real predicate

No new exchanges are introduced. The GATHER exchange for system tables
was already present (added by visitTableScan). This fix ensures the
predicate is pushed down so the connector scans only the target table's
columns.


## Test Plan
Unit tests and verifier run

## Contributor checklist

- [x] Please make sure your submission complies with our [contributing
guide](https://github.com/prestodb/presto/blob/master/CONTRIBUTING.md),
in particular [code
style](https://github.com/prestodb/presto/blob/master/CONTRIBUTING.md#code-style)
and [commit
standards](https://github.com/prestodb/presto/blob/master/CONTRIBUTING.md#commit-standards).
- [x] PR description addresses the issue accurately and concisely. If
the change is non-trivial, a GitHub Issue is referenced.
- [x] Documented new properties (with its default value), SQL syntax,
functions, or other functionality.
- [ ] If release notes are required, they follow the [release notes
guidelines](https://github.com/prestodb/presto/wiki/Release-Notes-Guidelines).
- [ ] Adequate tests were added if applicable.
- [ ] CI passed.
- [ ] If adding new dependencies, verified they have an [OpenSSF
Scorecard](https://securityscorecards.dev/#the-checks) score of 5.0 or
higher (or obtained explicit TSC approval for lower scores).

## Release Notes
Please follow [release notes
guidelines](https://github.com/prestodb/presto/wiki/Release-Notes-Guidelines)
and fill in the release notes below.

```
== RELEASE NOTES ==

General Changes
* Fix DESCRIBE and SHOW COLUMNS queries hanging in PLANNING state on clusters with single-node execution enabled.

```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

from:Meta PR from Meta

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants