use super::{roundtrip_test, roundtrip_test_and_return};
use arrow::datatypes::{Fields, TimeUnit};
use datafusion::arrow::compute::kernels::sort::SortOptions;
use datafusion::arrow::datatypes::{DataType, Field, Schema};
use datafusion::physical_expr::LexOrdering;
use datafusion::physical_plan::analyze::AnalyzeExec;
use datafusion::physical_plan::empty::EmptyExec;
use datafusion::physical_plan::explain::ExplainExec;
use datafusion::physical_plan::expressions::{PhysicalSortExpr, col, lit};
use datafusion::physical_plan::metrics::MetricCategory;
use datafusion::physical_plan::placeholder_row::PlaceholderRowExec;
use datafusion::physical_plan::projection::{ProjectionExec, ProjectionExpr};
use datafusion::physical_plan::repartition::RepartitionExec;
use datafusion::physical_plan::sorts::sort::SortExec;
use datafusion::physical_plan::union::{InterleaveExec, UnionExec};
use datafusion::physical_plan::unnest::{ListUnnest, UnnestExec};
use datafusion::physical_plan::{
ExecutionPlan, Partitioning, RangePartitioning, SplitPoint,
};
use datafusion::prelude::SessionContext;
use datafusion::scalar::ScalarValue;
use datafusion_common::display::{PlanType, StringifiedPlan};
use datafusion_common::format::ExplainFormat;
use datafusion_common::{DataFusionError, Result, UnnestOptions};
use datafusion_proto::physical_plan::to_proto::serialize_physical_expr_with_converter;
use datafusion_proto::physical_plan::{
AsExecutionPlan, DefaultPhysicalExtensionCodec, DefaultPhysicalProtoConverter,
PhysicalPlanDecodeContext,
};
use datafusion_proto::protobuf;
use datafusion_proto::protobuf::PhysicalPlanNode;
use prost::Message;
use std::sync::Arc;
use std::vec;
#[test]
fn roundtrip_analyze() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("plan_type", DataType::Utf8, false),
Field::new("plan", DataType::Utf8, false),
]));
let input = Arc::new(PlaceholderRowExec::new(Arc::clone(&schema)));
let metric_categories = vec![MetricCategory::Rows, MetricCategory::Timing];
let analyze = Arc::new(
AnalyzeExec::builder(true, true, input, Arc::clone(&schema))
.with_metric_categories(Some(metric_categories.clone()))
.with_format(ExplainFormat::Tree)
.build(),
);
let ctx = SessionContext::new();
let roundtripped = roundtrip_test_and_return(
analyze,
&ctx,
&DefaultPhysicalExtensionCodec {},
&DefaultPhysicalProtoConverter {},
)?;
let roundtripped = roundtripped.downcast_ref::<AnalyzeExec>().unwrap();
assert_eq!(roundtripped.schema(), schema);
assert!(roundtripped.verbose());
assert!(roundtripped.show_statistics());
assert_eq!(
roundtripped.metric_categories(),
Some(metric_categories.as_slice())
);
assert_eq!(roundtripped.format(), &ExplainFormat::Tree);
assert!(
roundtripped
.input()
.downcast_ref::<PlaceholderRowExec>()
.is_some()
);
Ok(())
}
#[test]
fn roundtrip_explain() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("plan_type", DataType::Utf8, false),
Field::new("plan", DataType::Utf8, false),
]));
let stringified_plans = vec![
StringifiedPlan::new(PlanType::InitialLogicalPlan, "initial logical"),
StringifiedPlan::new(
PlanType::AnalyzedLogicalPlan {
analyzer_name: "analyzer".to_string(),
},
"analyzed logical",
),
StringifiedPlan::new(PlanType::FinalAnalyzedLogicalPlan, "final analyzed"),
StringifiedPlan::new(
PlanType::OptimizedLogicalPlan {
optimizer_name: "logical optimizer".to_string(),
},
"optimized logical",
),
StringifiedPlan::new(PlanType::FinalLogicalPlan, "final logical"),
StringifiedPlan::new(PlanType::InitialPhysicalPlan, "initial physical"),
StringifiedPlan::new(
PlanType::InitialPhysicalPlanWithStats,
"initial physical with stats",
),
StringifiedPlan::new(
PlanType::InitialPhysicalPlanWithSchema,
"initial physical with schema",
),
StringifiedPlan::new(
PlanType::OptimizedPhysicalPlan {
optimizer_name: "physical optimizer".to_string(),
},
"optimized physical",
),
StringifiedPlan::new(PlanType::FinalPhysicalPlan, "final physical"),
StringifiedPlan::new(
PlanType::FinalPhysicalPlanWithStats,
"final physical with stats",
),
StringifiedPlan::new(
PlanType::FinalPhysicalPlanWithSchema,
"final physical with schema",
),
StringifiedPlan::new(PlanType::PhysicalPlanError, "physical plan error"),
];
let explain = Arc::new(ExplainExec::new(
Arc::clone(&schema),
stringified_plans.clone(),
true,
));
let ctx = SessionContext::new();
let roundtripped = roundtrip_test_and_return(
explain,
&ctx,
&DefaultPhysicalExtensionCodec {},
&DefaultPhysicalProtoConverter {},
)?;
let roundtripped = roundtripped.downcast_ref::<ExplainExec>().unwrap();
assert_eq!(roundtripped.schema(), schema);
assert_eq!(roundtripped.stringified_plans(), stringified_plans);
assert!(roundtripped.verbose());
Ok(())
}
#[test]
fn roundtrip_union() -> Result<()> {
let field_a = Field::new("col", DataType::Int64, false);
let schema_left = Schema::new(vec![field_a.clone()]);
let schema_right = Schema::new(vec![field_a]);
let left = EmptyExec::new(Arc::new(schema_left));
let right = EmptyExec::new(Arc::new(schema_right));
let inputs: Vec<Arc<dyn ExecutionPlan>> = vec![Arc::new(left), Arc::new(right)];
let union = UnionExec::try_new(inputs)?;
roundtrip_test(union)
}
#[tokio::test]
async fn roundtrip_union_with_mismatched_nullability_executes() -> Result<()> {
let literal_leg = |value: ScalarValue| -> Result<Arc<dyn ExecutionPlan>> {
Ok(Arc::new(ProjectionExec::try_new(
vec![ProjectionExpr {
expr: lit(value),
alias: "a".to_string(),
}],
Arc::new(PlaceholderRowExec::new(Arc::new(Schema::empty()))),
)?))
};
let non_nullable_leg = literal_leg(ScalarValue::Int64(Some(1)))?;
let nullable_leg = literal_leg(ScalarValue::Int64(None))?;
let union: Arc<dyn ExecutionPlan> =
UnionExec::try_new(vec![non_nullable_leg, nullable_leg])?;
assert!(union.schema().field(0).is_nullable());
assert!(
format!("{union:?}").contains("CastExpr"),
"expected a coercing CastExpr in plan:\n{union:?}"
);
let ctx = SessionContext::new();
let bytes = datafusion_proto::bytes::physical_plan_to_bytes(Arc::clone(&union))?;
let roundtripped = datafusion_proto::bytes::physical_plan_from_bytes(
bytes.as_ref(),
ctx.task_ctx().as_ref(),
)?;
assert!(roundtripped.schema().field(0).is_nullable());
assert!(
format!("{roundtripped:?}").contains("CastExpr"),
"expected a coercing CastExpr after roundtrip:\n{roundtripped:?}"
);
let batches =
datafusion::physical_plan::collect(roundtripped, ctx.task_ctx()).await?;
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);
for batch in &batches {
assert!(batch.schema().field(0).is_nullable());
}
Ok(())
}
#[test]
fn roundtrip_repartition_preserve_order() -> Result<()> {
let field_a = Field::new("a", DataType::Int64, false);
let schema = Arc::new(Schema::new(vec![field_a]));
let sort_exprs: LexOrdering = [PhysicalSortExpr {
expr: col("a", &schema)?,
options: SortOptions::default(),
}]
.into();
let source1 = SortExec::new(
sort_exprs.clone(),
Arc::new(EmptyExec::new(Arc::clone(&schema))),
);
let source2 = SortExec::new(sort_exprs, Arc::new(EmptyExec::new(schema)));
let union = UnionExec::try_new(vec![
Arc::new(source1) as Arc<dyn ExecutionPlan>,
Arc::new(source2) as Arc<dyn ExecutionPlan>,
])?;
let repartition = RepartitionExec::try_new(union, Partitioning::RoundRobinBatch(10))?
.with_preserve_order();
assert!(repartition.preserve_order());
roundtrip_test(Arc::new(repartition))
}
#[test]
fn roundtrip_range_partitioning() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
let input = Arc::new(EmptyExec::new(Arc::clone(&schema)));
let range_partitioning = Partitioning::Range(RangePartitioning::new(
[PhysicalSortExpr::new_default(col("a", &schema)?)].into(),
vec![SplitPoint::new(vec![ScalarValue::Int64(Some(10))])],
));
let repartition = RepartitionExec::try_new(input, range_partitioning)?;
roundtrip_test(Arc::new(repartition))
}
#[test]
fn parse_hash_partitioning_delegates_to_shared_decoder() -> Result<()> {
use datafusion_proto::physical_plan::from_proto::parse_protobuf_hash_partitioning;
let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
let ctx = SessionContext::new();
let task_ctx = ctx.task_ctx();
let codec = DefaultPhysicalExtensionCodec {};
let decode_ctx = PhysicalPlanDecodeContext::new(&task_ctx, &codec);
let proto_converter = DefaultPhysicalProtoConverter {};
let hash_expr = serialize_physical_expr_with_converter(
&col("a", &schema)?,
&codec,
&proto_converter,
)?;
let hash = protobuf::PhysicalHashRepartition {
hash_expr: vec![hash_expr],
partition_count: 4,
};
let partitioning = parse_protobuf_hash_partitioning(
Some(&hash),
&decode_ctx,
&schema,
&proto_converter,
)?;
let Some(Partitioning::Hash(exprs, count)) = partitioning else {
panic!("expected hash partitioning, got {partitioning:?}");
};
assert_eq!(count, 4);
assert_eq!(exprs.len(), 1);
assert_eq!(exprs[0].to_string(), col("a", &schema)?.to_string());
assert!(
parse_protobuf_hash_partitioning(None, &decode_ctx, &schema, &proto_converter)?
.is_none()
);
let oversized = protobuf::PhysicalHashRepartition {
hash_expr: vec![serialize_physical_expr_with_converter(
&col("a", &schema)?,
&codec,
&proto_converter,
)?],
partition_count: u64::MAX,
};
let decoded = parse_protobuf_hash_partitioning(
Some(&oversized),
&decode_ctx,
&schema,
&proto_converter,
);
#[cfg(target_pointer_width = "64")]
{
let Some(Partitioning::Hash(_, count)) = decoded? else {
panic!("expected hash partitioning");
};
assert_eq!(count, usize::MAX);
}
#[cfg(not(target_pointer_width = "64"))]
assert!(
decoded
.unwrap_err()
.to_string()
.contains("Partition count 18446744073709551615 exceeds usize::MAX")
);
Ok(())
}
#[test]
fn roundtrip_interleave() -> Result<()> {
let field_a = Field::new("col", DataType::Int64, false);
let schema_left = Schema::new(vec![field_a.clone()]);
let schema_right = Schema::new(vec![field_a]);
let partition = Partitioning::Hash(vec![], 3);
let left = RepartitionExec::try_new(
Arc::new(EmptyExec::new(Arc::new(schema_left))),
partition.clone(),
)?;
let right = RepartitionExec::try_new(
Arc::new(EmptyExec::new(Arc::new(schema_right))),
partition,
)?;
let inputs: Vec<Arc<dyn ExecutionPlan>> = vec![Arc::new(left), Arc::new(right)];
let interleave = InterleaveExec::try_new(inputs)?;
roundtrip_test(Arc::new(interleave))
}
#[tokio::test]
async fn roundtrip_interleave_with_mismatched_nullability_executes() -> Result<()> {
let partition = Partitioning::Hash(vec![], 3);
let literal_leg = |value: ScalarValue| -> Result<Arc<dyn ExecutionPlan>> {
let projection = ProjectionExec::try_new(
vec![ProjectionExpr {
expr: lit(value),
alias: "a".to_string(),
}],
Arc::new(PlaceholderRowExec::new(Arc::new(Schema::empty()))),
)?;
Ok(Arc::new(RepartitionExec::try_new(
Arc::new(projection),
partition.clone(),
)?))
};
let non_nullable_leg = literal_leg(ScalarValue::Int64(Some(1)))?;
let nullable_leg = literal_leg(ScalarValue::Int64(None))?;
let interleave: Arc<dyn ExecutionPlan> = Arc::new(InterleaveExec::try_new(vec![
non_nullable_leg,
nullable_leg,
])?);
assert!(interleave.schema().field(0).is_nullable());
assert!(
format!("{interleave:?}").contains("CastExpr"),
"expected a coercing CastExpr in plan:\n{interleave:?}"
);
let ctx = SessionContext::new();
let bytes = datafusion_proto::bytes::physical_plan_to_bytes(Arc::clone(&interleave))?;
let roundtripped = datafusion_proto::bytes::physical_plan_from_bytes(
bytes.as_ref(),
ctx.task_ctx().as_ref(),
)?;
assert!(roundtripped.schema().field(0).is_nullable());
assert!(
format!("{roundtripped:?}").contains("CastExpr"),
"expected a coercing CastExpr after roundtrip:\n{roundtripped:?}"
);
let batches =
datafusion::physical_plan::collect(roundtripped, ctx.task_ctx()).await?;
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);
for batch in &batches {
assert!(batch.schema().field(0).is_nullable());
}
Ok(())
}
#[test]
fn roundtrip_unnest() -> Result<()> {
let fa = Field::new("a", DataType::Int64, true);
let fb0 = Field::new_list_field(DataType::Utf8, true);
let fb = Field::new_list("b", fb0.clone(), false);
let fc1 = Field::new("c1", DataType::Boolean, false);
let fc2 = Field::new("c2", DataType::Date64, true);
let fc = Field::new_struct("c", Fields::from(vec![fc1.clone(), fc2.clone()]), true);
let fd0 = Field::new_list_field(DataType::Float32, false);
let fd = Field::new_list("d", fd0.clone(), true);
let fe1 = Field::new("e1", DataType::UInt16, false);
let fe2 = Field::new("e2", DataType::Duration(TimeUnit::Millisecond), true);
let fe3 = Field::new("e3", DataType::Timestamp(TimeUnit::Millisecond, None), true);
let fe_fields = Fields::from(vec![fe1.clone(), fe2.clone(), fe3.clone()]);
let fe = Field::new_struct("e", fe_fields, false);
let fb0 = fb0.with_name("b");
let fd0 = fd0.with_name("d");
let input_schema = Arc::new(Schema::new(vec![fa.clone(), fb, fc, fd, fe]));
let output_schema =
Arc::new(Schema::new(vec![fa, fb0, fc1, fc2, fd0, fe1, fe2, fe3]));
let input = Arc::new(EmptyExec::new(input_schema));
let options = UnnestOptions {
null_handling: datafusion_common::NullHandling::Drop,
recursions: vec![datafusion_common::RecursionUnnestOption {
input_column: datafusion_common::Column::new_unqualified("b"),
output_column: datafusion_common::Column::new_unqualified("b"),
depth: 2,
}],
};
let unnest = UnnestExec::new(
input,
vec![
ListUnnest {
index_in_input_schema: 1,
depth: 1,
},
ListUnnest {
index_in_input_schema: 1,
depth: 2,
},
ListUnnest {
index_in_input_schema: 3,
depth: 2,
},
],
vec![2, 4],
output_schema,
options.clone(),
)?;
let ctx = SessionContext::new();
let codec = DefaultPhysicalExtensionCodec {};
let proto_converter = DefaultPhysicalProtoConverter {};
let result =
roundtrip_test_and_return(Arc::new(unnest), &ctx, &codec, &proto_converter)?;
let result = result.downcast_ref::<UnnestExec>().unwrap();
assert_eq!(result.options(), &options);
Ok(())
}
#[tokio::test]
async fn analyze_roundtrip_unoptimized() -> Result<()> {
let ctx = SessionContext::new();
let session_state =
datafusion::execution::SessionStateBuilder::new_from_existing(ctx.state())
.with_physical_optimizer_rules(vec![])
.build();
let logical_plan = session_state
.create_logical_plan("explain analyze select 1")
.await?;
let plan = session_state.create_physical_plan(&logical_plan).await?;
let node = PhysicalPlanNode::try_from_physical_plan(
plan.clone(),
&DefaultPhysicalExtensionCodec {},
)?;
let node = PhysicalPlanNode::decode(node.encode_to_vec().as_slice())
.map_err(|e| DataFusionError::External(Box::new(e)))?;
let unoptimized =
node.try_into_physical_plan(&ctx.task_ctx(), &DefaultPhysicalExtensionCodec {})?;
let physical_planner =
datafusion::physical_planner::DefaultPhysicalPlanner::default();
physical_planner.optimize_physical_plan(unoptimized, &session_state, |_, _| {})?;
Ok(())
}