blob: 0dec807f8043a5c9f59eaa1da41704f1bdc51449 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! See `main.rs` for how to run it.
//!
//! This example demonstrates how to use the `PhysicalExtensionCodec` trait's
//! interception methods to implement expression deduplication during deserialization.
//!
//! This pattern is inspired by PR #18192, which introduces expression caching
//! to reduce memory usage when deserializing plans with duplicate expressions.
//!
//! The key insight is that identical expressions serialize to identical protobuf bytes.
//! By caching deserialized expressions keyed by their protobuf bytes, we can:
//! 1. Return the same Arc for duplicate expressions
//! 2. Reduce memory allocation during deserialization
//! 3. Enable downstream optimizations that rely on Arc pointer equality
//!
//! This demonstrates the decorator pattern enabled by the `PhysicalExtensionCodec` trait,
//! where all expression serialization/deserialization routes through the codec methods.
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::{Arc, RwLock};
use arrow::datatypes::{DataType, Field, Schema};
use datafusion::common::Result;
use datafusion::execution::TaskContext;
use datafusion::logical_expr::Operator;
use datafusion::physical_expr::PhysicalExpr;
use datafusion::physical_plan::ExecutionPlan;
use datafusion::physical_plan::expressions::{BinaryExpr, col};
use datafusion::physical_plan::filter::FilterExec;
use datafusion::physical_plan::placeholder_row::PlaceholderRowExec;
use datafusion::prelude::SessionContext;
use datafusion_proto::physical_plan::from_proto::parse_physical_expr_with_converter;
use datafusion_proto::physical_plan::to_proto::serialize_physical_expr_with_converter;
use datafusion_proto::physical_plan::{
DefaultPhysicalExtensionCodec, PhysicalExtensionCodec,
PhysicalProtoConverterExtension,
};
use datafusion_proto::protobuf::{PhysicalExprNode, PhysicalPlanNode};
use prost::Message;
/// Example showing how to implement expression deduplication using the codec decorator pattern.
///
/// This demonstrates:
/// 1. Creating a CachingCodec that caches expressions by their protobuf bytes
/// 2. Intercepting deserialization to return cached Arcs for duplicate expressions
/// 3. Verifying that duplicate expressions share the same Arc after deserialization
///
/// Deduplication is keyed by the protobuf bytes representing the expression,
/// in reality deduplication could be done based on e.g. the pointer address of the
/// serialized expression in memory, but this is simpler to demonstrate.
///
/// In this case our expression is trivial and just for demonstration purposes.
/// In real scenarios, expressions can be much more complex, e.g. a large InList
/// expression could be megabytes in size, so deduplication can save significant memory
/// in addition to more correctly representing the original plan structure.
pub async fn expression_deduplication() -> Result<()> {
println!("=== Expression Deduplication Example ===\n");
// Create a schema for our test expressions
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Boolean, false)]));
// Step 1: Create expressions with duplicates
println!("Step 1: Creating expressions with duplicates...");
// Create expression: col("a")
let a = col("a", &schema)?;
// Create a clone to show duplicates
let a_clone = Arc::clone(&a);
// Combine: a OR a_clone
let combined_expr =
Arc::new(BinaryExpr::new(a, Operator::Or, a_clone)) as Arc<dyn PhysicalExpr>;
println!(" Created expression: a OR a with duplicates");
println!(" Note: a appears twice in the expression tree\n");
// Step 2: Create a filter plan with this expression
println!("Step 2: Creating physical plan with the expression...");
let input = Arc::new(PlaceholderRowExec::new(Arc::clone(&schema)));
let filter_plan: Arc<dyn ExecutionPlan> =
Arc::new(FilterExec::try_new(combined_expr, input)?);
println!(" Created FilterExec with duplicate sub-expressions\n");
// Step 3: Serialize with the caching codec
println!("Step 3: Serializing plan...");
let extension_codec = DefaultPhysicalExtensionCodec {};
let caching_converter = CachingCodec::new();
let proto =
caching_converter.execution_plan_to_proto(&filter_plan, &extension_codec)?;
// Serialize to bytes
let mut bytes = Vec::new();
proto.encode(&mut bytes).unwrap();
println!(" Serialized plan to {} bytes\n", bytes.len());
// Step 4: Deserialize with the caching codec
println!("Step 4: Deserializing plan with CachingCodec...");
let ctx = SessionContext::new();
let deserialized_plan = proto.try_into_physical_plan_with_converter(
&ctx.task_ctx(),
&extension_codec,
&caching_converter,
)?;
// Step 5: check that we deduplicated expressions
println!("Step 5: Checking for deduplicated expressions...");
let Some(filter_exec) = deserialized_plan.as_any().downcast_ref::<FilterExec>()
else {
panic!("Deserialized plan is not a FilterExec");
};
let predicate = Arc::clone(filter_exec.predicate());
let binary_expr = predicate
.as_any()
.downcast_ref::<BinaryExpr>()
.expect("Predicate is not a BinaryExpr");
let left = &binary_expr.left();
let right = &binary_expr.right();
// Check if left and right point to the same Arc
let deduplicated = Arc::ptr_eq(left, right);
if deduplicated {
println!(" Success: Duplicate expressions were deduplicated!");
println!(
" Cache Stats: hits={}, misses={}",
caching_converter.stats.read().unwrap().cache_hits,
caching_converter.stats.read().unwrap().cache_misses,
);
} else {
println!(" Failure: Duplicate expressions were NOT deduplicated.");
}
Ok(())
}
// ============================================================================
// CachingCodec - Implements expression deduplication
// ============================================================================
/// Statistics for cache performance monitoring
#[derive(Debug, Default)]
struct CacheStats {
cache_hits: usize,
cache_misses: usize,
}
/// A codec that caches deserialized expressions to enable deduplication.
///
/// When deserializing, if we've already seen the same protobuf bytes,
/// we return the cached Arc instead of creating a new allocation.
#[derive(Debug, Default)]
struct CachingCodec {
/// Cache mapping protobuf bytes -> deserialized expression
expr_cache: RwLock<HashMap<Vec<u8>, Arc<dyn PhysicalExpr>>>,
/// Statistics for demonstration
stats: RwLock<CacheStats>,
}
impl CachingCodec {
fn new() -> Self {
Self::default()
}
}
impl PhysicalExtensionCodec for CachingCodec {
// Required: decode custom extension nodes
fn try_decode(
&self,
_buf: &[u8],
_inputs: &[Arc<dyn ExecutionPlan>],
_ctx: &TaskContext,
) -> Result<Arc<dyn ExecutionPlan>> {
datafusion::common::not_impl_err!("No custom extension nodes")
}
// Required: encode custom execution plans
fn try_encode(
&self,
_node: Arc<dyn ExecutionPlan>,
_buf: &mut Vec<u8>,
) -> Result<()> {
datafusion::common::not_impl_err!("No custom extension nodes")
}
}
impl PhysicalProtoConverterExtension for CachingCodec {
fn proto_to_execution_plan(
&self,
ctx: &TaskContext,
extension_codec: &dyn PhysicalExtensionCodec,
proto: &PhysicalPlanNode,
) -> Result<Arc<dyn ExecutionPlan>> {
proto.try_into_physical_plan_with_converter(ctx, extension_codec, self)
}
fn execution_plan_to_proto(
&self,
plan: &Arc<dyn ExecutionPlan>,
extension_codec: &dyn PhysicalExtensionCodec,
) -> Result<PhysicalPlanNode> {
PhysicalPlanNode::try_from_physical_plan_with_converter(
Arc::clone(plan),
extension_codec,
self,
)
}
// CACHING IMPLEMENTATION: Intercept expression deserialization
fn proto_to_physical_expr(
&self,
proto: &PhysicalExprNode,
ctx: &TaskContext,
input_schema: &Schema,
codec: &dyn PhysicalExtensionCodec,
) -> Result<Arc<dyn PhysicalExpr>> {
// Create cache key from protobuf bytes
let mut key = Vec::new();
proto.encode(&mut key).map_err(|e| {
datafusion::error::DataFusionError::Internal(format!(
"Failed to encode proto for cache key: {e}"
))
})?;
// Check cache first
{
let cache = self.expr_cache.read().unwrap();
if let Some(cached) = cache.get(&key) {
// Cache hit! Update stats and return cached Arc
let mut stats = self.stats.write().unwrap();
stats.cache_hits += 1;
return Ok(Arc::clone(cached));
}
}
// Cache miss - deserialize and store
let expr =
parse_physical_expr_with_converter(proto, ctx, input_schema, codec, self)?;
// Store in cache
{
let mut cache = self.expr_cache.write().unwrap();
cache.insert(key, Arc::clone(&expr));
let mut stats = self.stats.write().unwrap();
stats.cache_misses += 1;
}
Ok(expr)
}
fn physical_expr_to_proto(
&self,
expr: &Arc<dyn PhysicalExpr>,
codec: &dyn PhysicalExtensionCodec,
) -> Result<PhysicalExprNode> {
serialize_physical_expr_with_converter(expr, codec, self)
}
}