Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 42 additions & 2 deletions datafusion-examples/examples/relation_planner/table_sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,13 @@ impl RelationPlanner for TableSamplePlanner {
})
.transpose()?;

// Plan the underlying table without the sample clause
// Plan the underlying table without the sample clause or alias. The
// alias belongs to the complete TABLESAMPLE relation, so we return it
// with `PlannedRelation` below and let DataFusion apply it once, after
// the sampling node has been added.
let base_relation = TableFactor::Table {
sample: None,
alias: alias.clone(),
alias: None,
name,
args,
with_hints,
Expand Down Expand Up @@ -467,6 +470,43 @@ impl RelationPlanner for TableSamplePlanner {
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn table_sample_applies_relation_alias_once() -> Result<()> {
let ctx = SessionContext::new();
ctx.register_relation_planner(Arc::new(TableSamplePlanner))?;
register_sample_data(&ctx)?;

let df = ctx
.sql(
"SELECT * FROM sample_data AS sampled(first, second) \
TABLESAMPLE (3 ROWS)",
)
.await?;
let plan = df.logical_plan().display_indent().to_string();

assert_snapshot!(plan, @r"
Projection: sampled.first, sampled.second
SubqueryAlias: sampled
Projection: sample_data.column1 AS first, sample_data.column2 AS second
Limit: skip=0, fetch=3
TableScan: sample_data
");

// Keep the intent of this regression test obvious even if the plan
// display gains unrelated detail: the relation alias and its column
// rename projection each belong around the sampled relation once.
assert_eq!(plan.matches("SubqueryAlias: sampled").count(), 1);
assert_eq!(plan.matches(" AS first").count(), 1);
assert_eq!(plan.matches(" AS second").count(), 1);

Ok(())
}
}

/// Custom logical plan node representing a TABLESAMPLE operation.
///
/// Stores sampling parameters (bounds, seed) and wraps the input plan.
Expand Down
19 changes: 17 additions & 2 deletions datafusion/expr/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,22 @@ pub enum PlannerResult<T> {
pub struct PlannedRelation {
/// The logical plan for the relation
pub plan: LogicalPlan,
/// Optional table alias for the relation
/// Optional alias for the complete relation.
///
/// DataFusion applies this alias after the extension planner's logical
/// plan has been created. If the extension recursively plans an inner
/// [`TableFactor`] with [`RelationPlannerContext::plan`], it should remove
/// this alias from the inner relation and return it here instead. Otherwise
/// the same alias may be applied to both the inner and outer plans.
pub alias: Option<TableAlias>,
}

#[cfg(feature = "sql")]
impl PlannedRelation {
/// Create a new `PlannedRelation` with the given plan and alias
/// Create a new `PlannedRelation` with the given plan and optional alias.
///
/// The plan should not already contain this alias. DataFusion applies it
/// after the relation planner returns.
pub fn new(plan: LogicalPlan, alias: Option<TableAlias>) -> Self {
Self { plan, alias }
}
Expand Down Expand Up @@ -414,6 +423,12 @@ pub trait RelationPlannerContext {

/// Plans the specified relation through the full planner pipeline, starting
/// from the first registered relation planner.
///
/// This also applies any alias present on `relation`. When unwrapping a
/// relation such as `TABLESAMPLE`, remove the outer relation's alias before
/// calling this method, then return that alias as part of the final
/// [`PlannedRelation`]. This keeps the alias around the complete relation
/// and ensures it is applied exactly once.
fn plan(&mut self, relation: TableFactor) -> Result<LogicalPlan>;

/// Converts a SQL expression into a logical expression using the current
Expand Down
13 changes: 11 additions & 2 deletions docs/source/library-user-guide/extending-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ There are two main approaches when implementing a [`RelationPlanner`]:
represent the operation in the logical plan, along with a custom [`ExecutionPlan`]
to execute it. Both are required for end-to-end execution.

When returning a [`PlannedRelation`], pass along the alias from the relation and
let DataFusion apply it to the finished plan. If your planner removes a wrapper
or modifier and recursively calls `ctx.plan(...)`, remove that outer alias from
the inner relation first. Otherwise, the recursive call applies the alias once
and DataFusion applies it a second time when your planner returns.

#### Example: Basic RelationPlanner Structure

```rust
Expand Down Expand Up @@ -314,11 +320,13 @@ impl RelationPlanner for MyRelationPlanner {
// Transform or wrap the plan as needed
// ...

Ok(RelationPlanning::Planned(PlannedRelation::new(input, alias)))
Ok(RelationPlanning::Planned(Box::new(PlannedRelation::new(
input, alias,
))))
}

// Return Original for relations you don't handle
other => Ok(RelationPlanning::Original(other)),
other => Ok(RelationPlanning::Original(Box::new(other))),
}
}
}
Expand Down Expand Up @@ -380,6 +388,7 @@ SELECT * FROM sales
[`sessioncontext`]: https://docs.rs/datafusion/latest/datafusion/execution/context/struct.SessionContext.html
[`sessionstatebuilder`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionStateBuilder.html
[`relationplannercontext`]: https://docs.rs/datafusion/latest/datafusion/logical_expr/planner/trait.RelationPlannerContext.html
[`plannedrelation`]: https://docs.rs/datafusion/latest/datafusion/logical_expr/planner/struct.PlannedRelation.html
[exprplanner api documentation]: https://docs.rs/datafusion/latest/datafusion/logical_expr/planner/trait.ExprPlanner.html
[typeplanner api documentation]: https://docs.rs/datafusion/latest/datafusion/logical_expr/planner/trait.TypePlanner.html
[relationplanner api documentation]: https://docs.rs/datafusion/latest/datafusion/logical_expr/planner/trait.RelationPlanner.html
Expand Down