Extending SQL Syntax
Extending SQL Syntax#
DataFusion provides a flexible extension system that allows you to customize SQL parsing and planning without modifying the core codebase. This is useful when you need to:
- Support custom operators from other SQL dialects (e.g., PostgreSQL’s
->for JSON) - Add custom data types not natively supported
- Implement SQL constructs like
TABLESAMPLE,PIVOT/UNPIVOT, orMATCH_RECOGNIZE
You can read more about this topic in the Extending SQL in DataFusion: from ->> to TABLESAMPLE blog.
Architecture Overview#
When DataFusion processes a SQL query, it goes through these stages:
┌─────────────┐ ┌─────────┐ ┌──────────────────────┐ ┌─────────────┐
│ SQL String │───▶│ Parser │───▶│ SqlToRel │───▶│ LogicalPlan │
└─────────────┘ └─────────┘ │ (SQL to LogicalPlan) │ └─────────────┘
└──────────────────────┘
│
│ uses
▼
┌───────────────────────┐
│ Extension Planners │
│ • ExprPlanner │
│ • TypePlanner │
│ • RelationPlanner │
└───────────────────────┘The extension planners intercept specific parts of the SQL AST during the SqlToRel phase and allow you to customize how they are converted to DataFusion’s logical plan.
Extension Points#
DataFusion provides three planner traits for extending SQL:
| Trait | Purpose | Registration Method |
|---|---|---|
ExprPlanner | Custom expressions and operators | ctx.register_expr_planner() |
TypePlanner | Custom SQL data types | SessionStateBuilder::with_type_planner() |
RelationPlanner | Custom FROM clause elements (relations) | ctx.register_relation_planner() |
Planner Precedence: Multiple ExprPlanners and RelationPlanners can be registered; they are invoked in reverse registration order (last registered wins). Return Original(...) to delegate to the next planner. Only one TypePlanner can be active at a time.
ExprPlanner: Custom Expressions and Operators#
Use ExprPlanner to customize how SQL expressions are converted to DataFusion logical expressions. This is useful for:
- Custom binary operators (e.g.,
->,->>,@>,?) - Custom field access patterns
- Custom aggregate or window function handling
Available Methods#
| Category | Methods |
|---|---|
| Operators | plan_binary_op, plan_any |
| Literals | plan_array_literal, plan_dictionary_literal, plan_struct_literal |
| Functions | plan_extract, plan_substring, plan_overlay, plan_position, plan_make_map |
| Identifiers | plan_field_access, plan_compound_identifier |
| Aggregates/Windows | plan_aggregate, plan_window |
See the ExprPlanner API documentation for full method signatures.
Example: Custom Arrow Operator#
This example maps the -> operator to string concatenation:
use datafusion_expr::planner::{ExprPlanner, PlannerResult, RawBinaryExpr};
#[derive(Debug)]
struct MyCustomPlanner;
impl ExprPlanner for MyCustomPlanner {
fn plan_binary_op(
&self,
expr: RawBinaryExpr,
_schema: &DFSchema,
) -> Result<PlannerResult<RawBinaryExpr>> {
match &expr.op {
// Map `->` to string concatenation
BinaryOperator::Arrow => {
Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr {
left: Box::new(expr.left.clone()),
right: Box::new(expr.right.clone()),
op: Operator::StringConcat,
})))
}
_ => Ok(PlannerResult::Original(expr)),
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Use postgres dialect to enable `->` operator parsing
let config = SessionConfig::new()
.set_str("datafusion.sql_parser.dialect", "postgres");
let mut ctx = SessionContext::new_with_config(config);
// Register the custom planner
ctx.register_expr_planner(Arc::new(MyCustomPlanner))?;
// Now `->` works as string concatenation
let results = ctx.sql("SELECT 'hello'->'world'").await?.collect().await?;
// Returns: "helloworld"
Ok(())
}For more details, see the ExprPlanner API documentation and the expr_planner test examples.
TypePlanner: Custom Data Types#
Use TypePlanner to map SQL data types to Arrow/DataFusion types. This is useful when you need to support SQL types that aren’t natively recognized.
Example: Custom DATETIME Type#
use datafusion_expr::planner::TypePlanner;
#[derive(Debug)]
struct MyTypePlanner;
impl TypePlanner for MyTypePlanner {
fn plan_type_field(&self, sql_type: &ast::DataType) -> Result<Option<FieldRef>> {
match sql_type {
// Map DATETIME(precision) to Arrow Timestamp
ast::DataType::Datetime(precision) => {
let time_unit = match precision {
Some(0) => TimeUnit::Second,
Some(3) => TimeUnit::Millisecond,
Some(6) => TimeUnit::Microsecond,
None | Some(9) => TimeUnit::Nanosecond,
_ => return Ok(None), // Let default handling take over
};
Ok(Some(
DataType::Timestamp(time_unit, None).into_nullable_field_ref()
))
}
_ => Ok(None), // Return None for types we don't handle
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let state = SessionStateBuilder::new()
.with_default_features()
.with_type_planner(Arc::new(MyTypePlanner))
.build();
let ctx = SessionContext::new_with_state(state);
// Now DATETIME type is recognized
ctx.sql("CREATE TABLE events (ts DATETIME(3))").await?;
Ok(())
}Example: Supporting the UUID Type#
use datafusion_expr::planner::TypePlanner;
#[derive(Debug)]
struct MyTypePlanner;
impl TypePlanner for MyTypePlanner {
fn plan_type_field(&self, sql_type: &ast::DataType) -> Result<Option<FieldRef>> {
match sql_type {
sqlparser::ast::DataType::Uuid => Ok(Some(Arc::new(
Field::new("", DataType::FixedSizeBinary(16), true).with_metadata(
[("ARROW:extension:name".to_string(), "arrow.uuid".to_string())]
.into(),
),
))),
_ => Ok(None),
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let state = SessionStateBuilder::new()
.with_default_features()
.with_type_planner(Arc::new(MyTypePlanner))
.build();
let ctx = SessionContext::new_with_state(state);
// Now UUID type is recognized
ctx.sql("CREATE TABLE idx (uuid UUID)").await?;
Ok(())
}For more details, see the TypePlanner API documentation.
RelationPlanner: Custom FROM Clause Elements#
Use RelationPlanner to handle custom relations in the FROM clause. This enables you to implement SQL constructs like:
TABLESAMPLEfor sampling dataPIVOT/UNPIVOTfor data reshapingMATCH_RECOGNIZEfor pattern matching- Any custom relation syntax parsed by sqlparser
The RelationPlannerContext#
When implementing RelationPlanner, you receive a RelationPlannerContext that provides utilities for planning:
| Method | Purpose |
|---|---|
plan(relation) | Recursively plan a nested relation |
get_cte(name) | Look up a CTE visible in the current query scope |
sql_to_expr(expr, schema) | Convert SQL expression to DataFusion Expr |
context_provider() | Access session configuration, tables, functions |
See the RelationPlanner API documentation for additional methods like normalize_ident() and object_name_to_table_reference().
Relation planners are called before DataFusion’s built-in CTE and catalog lookup. If your planner recognizes ordinary relations by name, check get_cte() after determining the relation is an ordinary name that your planner would otherwise handle. Deliberately reserved names and custom syntax can keep their existing precedence. Return RelationPlanning::Original when a CTE is visible so the next planner can handle the relation. When every ordinary name-based planner in the chain performs this check, planning reaches DataFusion’s built-in CTE resolution. Normalize the parsed name first so the lookup follows DataFusion’s existing name matching. Only do this for a table without function arguments: a CTE named numbers shadows numbers, but it does not shadow a call such as numbers(10).
// Run this after recognizing `relation` as an ordinary name handled here.
if let TableFactor::Table { name, args: None, .. } = &relation {
let table_ref = ctx.object_name_to_table_reference(name.clone())?;
if ctx.get_cte(&table_ref).is_some() {
return Ok(RelationPlanning::Original(Box::new(relation)));
}
}Implementation Strategies#
There are two main approaches when implementing a RelationPlanner:
- Rewrite to Standard SQL: Transform custom syntax into equivalent standard operations that DataFusion already knows how to execute (e.g., PIVOT → GROUP BY with CASE expressions). This is the simplest approach when possible.
- Custom Logical and Physical Nodes: Create a
UserDefinedLogicalNodeto represent the operation in the logical plan, along with a customExecutionPlanto execute it. Both are required for end-to-end execution.
Example: Basic RelationPlanner Structure#
use datafusion_expr::planner::{
PlannedRelation, RelationPlanner, RelationPlannerContext, RelationPlanning,
};
use datafusion_sql::sqlparser::ast::TableFactor;
#[derive(Debug)]
struct MyRelationPlanner;
impl RelationPlanner for MyRelationPlanner {
fn plan_relation(
&self,
relation: TableFactor,
ctx: &mut dyn RelationPlannerContext,
) -> Result<RelationPlanning> {
match relation {
// Handle your custom relation
TableFactor::Pivot { table, alias, .. } => {
// Plan the input table
let input = ctx.plan(*table)?;
// Transform or wrap the plan as needed
// ...
Ok(RelationPlanning::Planned(PlannedRelation::new(input, alias)))
}
// Return Original for relations you don't handle
other => Ok(RelationPlanning::Original(other)),
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let ctx = SessionContext::new();
// Register the custom planner
ctx.register_relation_planner(Arc::new(MyRelationPlanner))?;
Ok(())
}Complete Examples#
The DataFusion repository includes comprehensive examples demonstrating each approach:
TABLESAMPLE (Custom Logical and Physical Nodes)#
The table_sample.rs example shows a complete end-to-end implementation of how to support queries such as:
SELECT * FROM table TABLESAMPLE BERNOULLI(10 PERCENT) REPEATABLE(42)PIVOT/UNPIVOT (Rewrite Strategy)#
The pivot_unpivot.rs example demonstrates rewriting custom syntax to standard SQL for queries such as:
SELECT * FROM sales
PIVOT (SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))Recap#
- Use
ExprPlannerfor custom operators and expression handling - Use [
TypePlannerfor custom SQL data types - Use
RelationPlannerfor custom FROM clause syntax (TABLESAMPLE, PIVOT, etc.) - Register planners via
SessionContextorSessionStateBuilder
See Also#
- API Documentation:
ExprPlanner,TypePlanner,RelationPlanner - relation_planner examples - Complete TABLESAMPLE, PIVOT/UNPIVOT implementations
- expr_planner test examples - Custom operator examples
- Custom Expression Planning in the UDF guide
评论
登录后参与评论
KnowForge