Skip to content

feat: protobuf serialization for the materialized CTE plan nodes - #26273

Draft
adriangb wants to merge 2 commits into
apache:mainfrom
adriangb:mcte-proto
Draft

adriangb wants to merge 2 commits into
apache:mainfrom
adriangb:mcte-proto

Conversation

@adriangb

@adriangb adriangb commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Stacked on #26269. Only the last commit is new.

Rationale for this change

A plan that has a materialized CTE cannot be sent to another process.

let df = ctx
    .sql("WITH c AS MATERIALIZED (SELECT * FROM (VALUES (1), (2)) AS t(v)) \
          SELECT * FROM c AS a JOIN c AS b ON a.v = b.v")
    .await?;

logical_plan_to_bytes(df.logical_plan())?;
physical_plan_to_bytes(df.create_physical_plan().await?)?;

Before this PR, the two calls return an error. After this PR, the two calls succeed. The decoded physical plan runs the body of the CTE one time, and all the scans read its buffer.

What changes are included in this PR?

  • New protobuf messages MaterializedCteNode, MaterializedCteScanNode, MaterializedCteExecNode and MaterializedCteScanExecNode.
  • Logical plans: the encoder and the decoder keep the id of the CTE. A logical id is used only inside one plan tree, and the physical planner gives each CTE a new id.
  • Physical plans: the encoding has only the id and the name of the CTE, not the buffer.
  • PhysicalPlanDecodeContext holds a DecodedMaterializedCteBuffers registry. Each decoded node asks the registry for the buffer of its id. Thus a scan and its MaterializedCteExec share one buffer in any decode order, also when the scan is in a scalar subquery and is not below the MaterializedCteExec.
  • Decoded buffers get new ids from MaterializedCteId::next(), because an id from another process can be equal to a local id.
  • A plan fragment that has a scan but not its MaterializedCteExec can be decoded. It fails when the scan is executed, with an error that says that the body and the scans must be in one plan and run in one process.

Known limits:

Are these changes tested?

Yes. Physical round trip of a CTE with two scans and of a CTE with a scan in a scalar subquery; each test runs the decoded plan and checks the result and that the body ran one time. A test for a scan without its MaterializedCteExec. Logical round trip of the same two queries, before and after the optimizer. The error for two decoded MaterializedCteExec nodes with one id has no test.

Are there any user-facing changes?

Yes. Plans with WITH ... AS MATERIALIZED can be serialized with datafusion-proto. API additions: MaterializedCteId::from_u64, MaterializedCteBuffer::name, DecodedMaterializedCteBuffers, try_from_proto on the two physical nodes, and ExecutionPlanDecodeCtx::materialized_cte_buffer. The #[doc(hidden)] trait ExecutionPlanDecode has a new required method.

@github-actions github-actions Bot added documentation Improvements or additions to documentation sql SQL Planner logical-expr Logical plan and expressions optimizer Optimizer rules core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) substrait Changes to the substrait crate proto Related to proto crate physical-plan Changes to the physical-plan crate labels Oct 13, 2026
@codecov-commenter

codecov-commenter commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.17647% with 466 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.82%. Comparing base (484183d) to head (1e7d18c).

Files with missing lines Patch % Lines
datafusion/proto-models/src/generated/pbjson.rs 0.00% 371 Missing ⚠️
...tafusion/expr/src/logical_plan/materialized_cte.rs 87.50% 10 Missing and 9 partials ⚠️
datafusion/physical-plan/src/materialized_cte.rs 84.26% 5 Missing and 9 partials ⚠️
datafusion/physical-plan/src/proto_test_util.rs 0.00% 13 Missing ⚠️
datafusion/expr/src/logical_plan/plan.rs 59.25% 11 Missing ⚠️
datafusion/expr/src/logical_plan/display.rs 0.00% 8 Missing ⚠️
datafusion/optimizer/src/analyzer/type_coercion.rs 83.72% 4 Missing and 3 partials ⚠️
datafusion/proto/src/logical_plan/mod.rs 76.66% 2 Missing and 5 partials ⚠️
datafusion/core/src/physical_planner.rs 85.71% 2 Missing and 3 partials ⚠️
...ion/substrait/src/logical_plan/producer/rel/mod.rs 0.00% 4 Missing ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #26273      +/-   ##
==========================================
- Coverage   82.89%   82.82%   -0.08%     
==========================================
  Files        1154     1154              
  Lines      457630   458230     +600     
  Branches   457630   458230     +600     
==========================================
+ Hits       379370   379543     +173     
- Misses      55256    55647     +391     
- Partials    23004    23040      +36     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion v55.1.0 (current)
       Built [  56.483s] (current)
     Parsing datafusion v55.1.0 (current)
      Parsed [   0.033s] (current)
    Building datafusion v55.1.0 (baseline)
       Built [  53.202s] (baseline)
     Parsing datafusion v55.1.0 (baseline)
      Parsed [   0.033s] (baseline)
    Checking datafusion v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.615s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [ 112.034s] datafusion
    Building datafusion-expr v55.1.0 (current)
       Built [  28.871s] (current)
     Parsing datafusion-expr v55.1.0 (current)
      Parsed [   0.088s] (current)
    Building datafusion-expr v55.1.0 (baseline)
       Built [  28.873s] (baseline)
     Parsing datafusion-expr v55.1.0 (baseline)
      Parsed [   0.089s] (baseline)
    Checking datafusion-expr v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   1.426s] 229 checks: 228 pass, 1 fail, 0 warn, 31 skip

--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/enum_variant_added.ron

Failed in:
  variant LogicalPlan:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/expr/src/logical_plan/plan.rs:309
  variant LogicalPlan:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/expr/src/logical_plan/plan.rs:309

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  60.457s] datafusion-expr
    Building datafusion-optimizer v55.1.0 (current)
       Built [  23.933s] (current)
     Parsing datafusion-optimizer v55.1.0 (current)
      Parsed [   0.032s] (current)
    Building datafusion-optimizer v55.1.0 (baseline)
       Built [  23.942s] (baseline)
     Parsing datafusion-optimizer v55.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-optimizer v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.180s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  48.867s] datafusion-optimizer
    Building datafusion-physical-plan v55.1.0 (current)
       Built [  34.426s] (current)
     Parsing datafusion-physical-plan v55.1.0 (current)
      Parsed [   0.190s] (current)
    Building datafusion-physical-plan v55.1.0 (baseline)
       Built [  34.158s] (baseline)
     Parsing datafusion-physical-plan v55.1.0 (baseline)
      Parsed [   0.191s] (baseline)
    Checking datafusion-physical-plan v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.755s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  70.758s] datafusion-physical-plan
    Building datafusion-proto v55.1.0 (current)
       Built [  49.011s] (current)
     Parsing datafusion-proto v55.1.0 (current)
      Parsed [   0.013s] (current)
    Building datafusion-proto v55.1.0 (baseline)
       Built [  48.851s] (baseline)
     Parsing datafusion-proto v55.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-proto v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.115s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  99.434s] datafusion-proto
    Building datafusion-proto-models v55.1.0 (current)
       Built [  22.489s] (current)
     Parsing datafusion-proto-models v55.1.0 (current)
      Parsed [   0.142s] (current)
    Building datafusion-proto-models v55.1.0 (baseline)
       Built [  22.434s] (baseline)
     Parsing datafusion-proto-models v55.1.0 (baseline)
      Parsed [   0.144s] (baseline)
    Checking datafusion-proto-models v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   2.025s] 229 checks: 228 pass, 1 fail, 0 warn, 31 skip

--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/enum_variant_added.ron

Failed in:
  variant PhysicalPlanType:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1496
  variant PhysicalPlanType:MaterializedCteScan in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1498
  variant PhysicalPlanType:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1496
  variant PhysicalPlanType:MaterializedCteScan in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1498
  variant LogicalPlanType:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:85
  variant LogicalPlanType:MaterializedCteScan in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:87
  variant LogicalPlanType:MaterializedCte in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:85
  variant LogicalPlanType:MaterializedCteScan in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:87

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  48.458s] datafusion-proto-models
    Building datafusion-sql v55.1.0 (current)
       Built [  39.721s] (current)
     Parsing datafusion-sql v55.1.0 (current)
      Parsed [   0.032s] (current)
    Building datafusion-sql v55.1.0 (baseline)
       Built [  39.411s] (baseline)
     Parsing datafusion-sql v55.1.0 (baseline)
      Parsed [   0.032s] (baseline)
    Checking datafusion-sql v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.243s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  80.375s] datafusion-sql
    Building datafusion-substrait v55.1.0 (current)
       Built [ 303.956s] (current)
     Parsing datafusion-substrait v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-substrait v55.1.0 (baseline)
       Built [ 306.291s] (baseline)
     Parsing datafusion-substrait v55.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-substrait v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.232s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [ 612.111s] datafusion-substrait

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Oct 13, 2026
@adriangb
adriangb force-pushed the mcte-proto branch 2 times, most recently from e711398 to 12d61e5 Compare October 16, 2026 03:46
@github-actions github-actions Bot removed the sqllogictest SQL Logic Tests (.slt) label Oct 16, 2026
@adriangb
adriangb force-pushed the mcte-proto branch 2 times, most recently from 00e49a8 to 0a0fa38 Compare October 16, 2026 16:57
adriangb and others added 2 commits October 17, 2026 16:41
Replace the `MaterializedCte` and `MaterializedCteScan` extension nodes with the variants `LogicalPlan::MaterializedCte` and `LogicalPlan::MaterializedCteScan`.
Each rule now states what it does with the nodes in its `match`, and the downcasts are removed from `PushDownFilter`, `PullUpCorrelatedExpr`, `head_output_expr` and the physical planner.
`MaterializedCte::try_new` checks that the scans have the schema of the body, and `MaterializedCteScan::find_in` finds the scans of one CTE, also in subquery expressions.
The unparser, the Substrait producer and the protobuf serializer return a "not implemented" error for the two variants.
Plans and results do not change.

Closes apache#26238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add protobuf encodings for LogicalPlan::MaterializedCte, LogicalPlan::MaterializedCteScan, MaterializedCteExec and MaterializedCteScanExec.

The physical encoding carries only the id of the CTE. The decode context holds one buffer for each id, so a decoded scan and its MaterializedCteExec share the buffer wherever they are in the plan, also when the scan is in a scalar subquery. Decoded buffers get new ids, because the ids of another process can be equal to local ids.

A decoded plan fragment that has a scan without its MaterializedCteExec fails with a clear error when the scan is executed.

Closes apache#26231.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change core Core DataFusion crate documentation Improvements or additions to documentation logical-expr Logical plan and expressions optimizer Optimizer rules physical-plan Changes to the physical-plan crate proto Related to proto crate sql SQL Planner substrait Changes to the substrait crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Protobuf serialization for materialized CTE plan nodes

2 participants