Skip to content

Commit c8d2648

Browse files
committed
add tests
1 parent 94fb0c3 commit c8d2648

4 files changed

Lines changed: 609 additions & 1 deletion

File tree

datafusion/physical-expr-adapter/src/schema_rewriter.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,4 +1734,131 @@ mod tests {
17341734
assert_eq!(cast_expr.input_field().data_type(), &DataType::Int32);
17351735
assert_eq!(cast_expr.target_field().data_type(), &DataType::Int64);
17361736
}
1737+
1738+
/// A mock expression with an in-scope child and an out-of-scope child.
1739+
/// Used to verify that scoped traversal does not modify out-of-scope children.
1740+
#[derive(Debug, Hash, Clone)]
1741+
struct ScopedExprMock {
1742+
in_scope_child: Arc<dyn PhysicalExpr>,
1743+
out_of_scope_child: Arc<dyn PhysicalExpr>,
1744+
}
1745+
1746+
impl PartialEq for ScopedExprMock {
1747+
fn eq(&self, other: &Self) -> bool {
1748+
self.in_scope_child.as_ref() == other.in_scope_child.as_ref()
1749+
&& self.out_of_scope_child.as_ref()
1750+
== other.out_of_scope_child.as_ref()
1751+
}
1752+
}
1753+
1754+
impl Eq for ScopedExprMock {}
1755+
1756+
impl std::fmt::Display for ScopedExprMock {
1757+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1758+
write!(
1759+
f,
1760+
"scoped_mock({}, {})",
1761+
self.in_scope_child, self.out_of_scope_child
1762+
)
1763+
}
1764+
}
1765+
1766+
impl PhysicalExpr for ScopedExprMock {
1767+
fn as_any(&self) -> &dyn std::any::Any {
1768+
self
1769+
}
1770+
1771+
fn return_field(
1772+
&self,
1773+
input_schema: &Schema,
1774+
) -> Result<Arc<Field>> {
1775+
self.in_scope_child.return_field(input_schema)
1776+
}
1777+
1778+
fn evaluate(
1779+
&self,
1780+
_batch: &RecordBatch,
1781+
) -> Result<datafusion_expr::ColumnarValue> {
1782+
unimplemented!("ScopedExprMock does not support evaluation")
1783+
}
1784+
1785+
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
1786+
vec![&self.in_scope_child, &self.out_of_scope_child]
1787+
}
1788+
1789+
fn with_new_children(
1790+
self: Arc<Self>,
1791+
children: Vec<Arc<dyn PhysicalExpr>>,
1792+
) -> Result<Arc<dyn PhysicalExpr>> {
1793+
assert_eq!(children.len(), 2);
1794+
let mut iter = children.into_iter();
1795+
Ok(Arc::new(Self {
1796+
in_scope_child: iter.next().unwrap(),
1797+
out_of_scope_child: iter.next().unwrap(),
1798+
}))
1799+
}
1800+
1801+
fn children_in_scope(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
1802+
vec![&self.in_scope_child]
1803+
}
1804+
1805+
fn with_new_children_in_scope(
1806+
self: Arc<Self>,
1807+
children_in_scope: Vec<Arc<dyn PhysicalExpr>>,
1808+
) -> Result<Arc<dyn PhysicalExpr>> {
1809+
assert_eq!(children_in_scope.len(), 1);
1810+
Ok(Arc::new(Self {
1811+
in_scope_child: children_in_scope.into_iter().next().unwrap(),
1812+
out_of_scope_child: Arc::clone(&self.out_of_scope_child),
1813+
}))
1814+
}
1815+
1816+
fn fmt_sql(
1817+
&self,
1818+
f: &mut std::fmt::Formatter<'_>,
1819+
) -> std::fmt::Result {
1820+
std::fmt::Display::fmt(&self, f)
1821+
}
1822+
}
1823+
1824+
#[test]
1825+
fn test_replace_columns_with_literals_does_not_modify_out_of_scope_children() {
1826+
// The in-scope child references column "a" which should be replaced
1827+
let in_scope_child: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
1828+
// The out-of-scope child also references a column "a" but should NOT be replaced
1829+
let out_of_scope_child: Arc<dyn PhysicalExpr> =
1830+
Arc::new(Column::new("a", 0));
1831+
1832+
let expr: Arc<dyn PhysicalExpr> = Arc::new(ScopedExprMock {
1833+
in_scope_child,
1834+
out_of_scope_child,
1835+
});
1836+
1837+
let mut replacements = HashMap::new();
1838+
replacements.insert("a", ScalarValue::Int32(Some(42)));
1839+
1840+
let result = replace_columns_with_literals(expr, &replacements).unwrap();
1841+
1842+
let mock = result
1843+
.as_any()
1844+
.downcast_ref::<ScopedExprMock>()
1845+
.expect("Should still be ScopedExprMock");
1846+
1847+
// The in-scope child "a" should be replaced with literal 42
1848+
let in_scope_lit = mock
1849+
.in_scope_child
1850+
.as_any()
1851+
.downcast_ref::<Literal>()
1852+
.expect("in_scope_child should be replaced with Literal");
1853+
assert_eq!(in_scope_lit.value(), &ScalarValue::Int32(Some(42)));
1854+
1855+
// The out-of-scope child "a@0" should be UNCHANGED (still a Column)
1856+
let out_of_scope_col = mock
1857+
.out_of_scope_child
1858+
.as_any()
1859+
.downcast_ref::<Column>()
1860+
.expect("out_of_scope_child should still be Column");
1861+
assert_eq!(out_of_scope_col.name(), "a");
1862+
assert_eq!(out_of_scope_col.index(), 0);
1863+
}
17371864
}

datafusion/physical-expr/src/projection.rs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,7 @@ pub(crate) mod tests {
12001200
use super::*;
12011201
use crate::equivalence::{EquivalenceProperties, convert_to_orderings};
12021202
use crate::expressions::{BinaryExpr, col};
1203-
use crate::utils::tests::TestScalarUDF;
1203+
use crate::utils::tests::{ScopedExprMock, TestScalarUDF};
12041204
use crate::{PhysicalExprRef, ScalarFunctionExpr};
12051205

12061206
use arrow::compute::SortOptions;
@@ -3038,4 +3038,162 @@ pub(crate) mod tests {
30383038

30393039
Ok(())
30403040
}
3041+
3042+
#[test]
3043+
fn test_update_expr_does_not_modify_out_of_scope_children() -> Result<()> {
3044+
// Outer schema: [a, b, c]
3045+
// Expression: ScopedExprMock(in_scope=a@0, out_of_scope=x@0)
3046+
// Projection: [c@2 as c_new, a@0 as a_new, b@1 as b_new]
3047+
// After unproject: in_scope should become c@2, out_of_scope should stay x@0
3048+
let in_scope_child: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a_new", 1));
3049+
let out_of_scope_child: Arc<dyn PhysicalExpr> =
3050+
Arc::new(Column::new("x", 0));
3051+
3052+
let expr: Arc<dyn PhysicalExpr> = Arc::new(ScopedExprMock {
3053+
in_scope_child,
3054+
out_of_scope_child,
3055+
});
3056+
3057+
let projected_exprs = vec![
3058+
ProjectionExpr {
3059+
expr: Arc::new(Column::new("c", 2)),
3060+
alias: "c_new".to_string(),
3061+
},
3062+
ProjectionExpr {
3063+
expr: Arc::new(Column::new("a", 0)),
3064+
alias: "a_new".to_string(),
3065+
},
3066+
ProjectionExpr {
3067+
expr: Arc::new(Column::new("b", 1)),
3068+
alias: "b_new".to_string(),
3069+
},
3070+
];
3071+
3072+
let result =
3073+
update_expr(&expr, &projected_exprs, true)?.expect("Should be valid");
3074+
3075+
let mock = result
3076+
.as_any()
3077+
.downcast_ref::<ScopedExprMock>()
3078+
.expect("Should still be ScopedExprMock");
3079+
3080+
// The in-scope child "a_new@1" should be unprojected to "a@0"
3081+
let in_scope_col = mock
3082+
.in_scope_child
3083+
.as_any()
3084+
.downcast_ref::<Column>()
3085+
.expect("in_scope_child should be Column");
3086+
assert_eq!(in_scope_col.name(), "a");
3087+
assert_eq!(in_scope_col.index(), 0);
3088+
3089+
// The out-of-scope child "x@0" should be UNCHANGED
3090+
let out_of_scope_col = mock
3091+
.out_of_scope_child
3092+
.as_any()
3093+
.downcast_ref::<Column>()
3094+
.expect("out_of_scope_child should be Column");
3095+
assert_eq!(out_of_scope_col.name(), "x");
3096+
assert_eq!(out_of_scope_col.index(), 0);
3097+
3098+
Ok(())
3099+
}
3100+
3101+
#[test]
3102+
fn test_project_ordering_does_not_modify_out_of_scope_children() {
3103+
// Schema: [a: Int32, b: Int32]
3104+
let schema = Arc::new(Schema::new(vec![
3105+
Field::new("a", DataType::Int32, false),
3106+
Field::new("b", DataType::Int32, false),
3107+
]));
3108+
3109+
let in_scope_child: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3110+
let out_of_scope_child: Arc<dyn PhysicalExpr> =
3111+
Arc::new(Column::new("x", 0));
3112+
3113+
let scoped_expr: Arc<dyn PhysicalExpr> = Arc::new(ScopedExprMock {
3114+
in_scope_child,
3115+
out_of_scope_child,
3116+
});
3117+
3118+
let ordering =
3119+
LexOrdering::new(vec![PhysicalSortExpr::new(scoped_expr, SortOptions::new(false, false))])
3120+
.unwrap();
3121+
3122+
let result = project_ordering(&ordering, &schema).expect("Should project");
3123+
3124+
let projected_expr = &result.first().expr;
3125+
let mock = projected_expr
3126+
.as_any()
3127+
.downcast_ref::<ScopedExprMock>()
3128+
.expect("Should still be ScopedExprMock");
3129+
3130+
// The in-scope column "a" should be reindexed (stays at 0 in this case)
3131+
let in_scope_col = mock
3132+
.in_scope_child
3133+
.as_any()
3134+
.downcast_ref::<Column>()
3135+
.expect("in_scope_child should be Column");
3136+
assert_eq!(in_scope_col.name(), "a");
3137+
assert_eq!(in_scope_col.index(), 0);
3138+
3139+
// The out-of-scope child "x@0" should be UNCHANGED
3140+
let out_of_scope_col = mock
3141+
.out_of_scope_child
3142+
.as_any()
3143+
.downcast_ref::<Column>()
3144+
.expect("out_of_scope_child should be Column");
3145+
assert_eq!(out_of_scope_col.name(), "x");
3146+
assert_eq!(out_of_scope_col.index(), 0);
3147+
}
3148+
3149+
#[test]
3150+
fn test_projection_mapping_does_not_modify_out_of_scope_children() -> Result<()> {
3151+
// Input schema: [a: Int32, b: Int32]
3152+
let input_schema = Arc::new(Schema::new(vec![
3153+
Field::new("a", DataType::Int32, false),
3154+
Field::new("b", DataType::Int32, false),
3155+
]));
3156+
3157+
let in_scope_child: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3158+
let out_of_scope_child: Arc<dyn PhysicalExpr> =
3159+
Arc::new(Column::new("x", 0));
3160+
3161+
let scoped_expr: Arc<dyn PhysicalExpr> = Arc::new(ScopedExprMock {
3162+
in_scope_child,
3163+
out_of_scope_child,
3164+
});
3165+
3166+
// Project: [ScopedExprMock as "result"]
3167+
let projection_exprs = vec![(scoped_expr, "result".to_string())];
3168+
3169+
let mapping = ProjectionMapping::try_new(projection_exprs, &input_schema)?;
3170+
3171+
// The source expression in the mapping should have its in-scope column
3172+
// validated but the out-of-scope column left untouched
3173+
let (source_expr, _targets) = mapping.iter().next().unwrap();
3174+
let mock = source_expr
3175+
.as_any()
3176+
.downcast_ref::<ScopedExprMock>()
3177+
.expect("Should still be ScopedExprMock");
3178+
3179+
// In-scope child: "a@0" should still be "a@0" (name matches schema)
3180+
let in_scope_col = mock
3181+
.in_scope_child
3182+
.as_any()
3183+
.downcast_ref::<Column>()
3184+
.expect("in_scope_child should be Column");
3185+
assert_eq!(in_scope_col.name(), "a");
3186+
assert_eq!(in_scope_col.index(), 0);
3187+
3188+
// Out-of-scope child: "x@0" should be UNCHANGED
3189+
let out_of_scope_col = mock
3190+
.out_of_scope_child
3191+
.as_any()
3192+
.downcast_ref::<Column>()
3193+
.expect("out_of_scope_child should be Column");
3194+
assert_eq!(out_of_scope_col.name(), "x");
3195+
assert_eq!(out_of_scope_col.index(), 0);
3196+
3197+
Ok(())
3198+
}
30413199
}

0 commit comments

Comments
 (0)