Skip to main content

datafusion_physical_expr_adapter/
rewrite.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Rewrite expressions in preparation for files being scanned, such as scan-metadata scalar UDFs.
19//!
20//! Functions like [`file_row_index()`] and [`input_file_name()`] are placeholders
21//! whose value is only known during a file scan. The helpers here replace those
22//! UDFs with ordinary physical expressions bound to the current file: a column
23//! reference into a source-provided row-index column, or a per-file literal, etc.
24//!
25//! [`file_row_index()`]: datafusion_functions::core::file_row_index::FileRowIndexFunc
26//! [`input_file_name()`]: datafusion_functions::core::input_file_name::InputFileNameFunc
27
28use std::sync::Arc;
29
30use arrow::datatypes::{DataType, Field};
31use datafusion_common::{
32    Result, ScalarValue,
33    tree_node::{Transformed, TreeNode, TreeNodeRecursion},
34};
35use datafusion_expr::ScalarUDFImpl;
36use datafusion_functions::core::file_row_index::FileRowIndexFunc;
37use datafusion_functions::core::input_file_name::InputFileNameFunc;
38use datafusion_physical_expr::ScalarFunctionExpr;
39use datafusion_physical_expr::expressions::{CastExpr, Column, Literal};
40use datafusion_physical_expr::projection::{ProjectionExpr, ProjectionExprs};
41use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
42
43/// Return true if a [`PhysicalExpr`] references scalar UDF `T`.
44///
45/// This matches the concrete [`ScalarUDFImpl`] type rather than the function
46/// name, so unrelated UDFs with the same name are not treated as matches.
47pub fn expr_references_scalar_udf<T: ScalarUDFImpl>(
48    expr: &Arc<dyn PhysicalExpr>,
49) -> bool {
50    let mut found = false;
51
52    expr.apply(|node| {
53        if ScalarFunctionExpr::try_downcast_func::<T>(node.as_ref()).is_some() {
54            found = true;
55            return Ok(TreeNodeRecursion::Stop);
56        }
57        Ok(TreeNodeRecursion::Continue)
58    })
59    .expect("Infallible traversal of PhysicalExpr tree failed");
60
61    found
62}
63
64/// Rewrite occurrences of scalar UDF `T` in a [`PhysicalExpr`] using
65/// `replacement`.
66///
67/// The rewrite matches the concrete [`ScalarUDFImpl`] type rather than the
68/// function name. `replacement` is called with each matching
69/// [`ScalarFunctionExpr`] after its children have been rewritten.
70fn rewrite_scalar_udf<T, F>(
71    expr: Arc<dyn PhysicalExpr>,
72    mut replacement: F,
73) -> Result<Arc<dyn PhysicalExpr>>
74where
75    T: ScalarUDFImpl,
76    F: FnMut(&ScalarFunctionExpr) -> Result<Arc<dyn PhysicalExpr>>,
77{
78    expr.transform_up(|node| {
79        if let Some(scalar_fn) = ScalarFunctionExpr::try_downcast_func::<T>(node.as_ref())
80        {
81            Ok(Transformed::yes(replacement(scalar_fn)?))
82        } else {
83            Ok(Transformed::no(node))
84        }
85    })
86    .map(|transformed| transformed.data)
87}
88
89/// Rewrite [`file_row_index()`][FileRowIndexFunc] in a [`PhysicalExpr`] to
90/// read from a source-provided row-index column.
91///
92/// `row_index_idx` is the index of `row_index_name` in the schema that the
93/// rewritten expression will be evaluated against. The rewrite uses ordinary
94/// physical expressions: a [`Column`] that reads the source row-index values
95/// wrapped in a [`CastExpr`] that exposes the public `file_row_index: Int64`
96/// return field without source-specific extension metadata.
97pub fn rewrite_file_row_index_expr(
98    expr: Arc<dyn PhysicalExpr>,
99    row_index_name: &str,
100    row_index_idx: usize,
101) -> Result<Arc<dyn PhysicalExpr>> {
102    rewrite_scalar_udf::<FileRowIndexFunc, _>(expr, |_| {
103        let source = Arc::new(Column::new(row_index_name, row_index_idx));
104        let target_field = Arc::new(Field::new("file_row_index", DataType::Int64, true));
105        Ok(Arc::new(CastExpr::new_with_target_field(
106            source,
107            target_field,
108            None,
109        )))
110    })
111}
112
113/// Rewrite [`file_row_index()`][FileRowIndexFunc] in pushed [`ProjectionExprs`]
114/// to read from a source-provided row-index column.
115///
116///
117/// For example if `row_index_column` is `__datafusion_row_idx` this function rewrites all
118/// instances of [`file_row_index()`][FileRowIndexFunc] to
119/// `__datafusion_row_index` [`Column`] references.
120///
121/// `base_projection` is the current projection already pushed into a source.
122/// The row-index source column is appended to that base projection if it is not
123/// already present. `projection` is rewritten to read from the projected
124/// row-index column and then merged on top of the extended base projection.
125pub fn rewrite_file_row_index_projection(
126    base_projection: &ProjectionExprs,
127    projection: &ProjectionExprs,
128    row_index_col: &Column,
129) -> Result<ProjectionExprs> {
130    let mut base_exprs = base_projection.as_ref().to_vec();
131    let row_index_projection_idx =
132        base_projection.projected_column_position(row_index_col);
133
134    // If the column doesn't exist in the projection yet
135    if row_index_projection_idx.is_none() {
136        base_exprs.push(ProjectionExpr {
137            expr: Arc::new(row_index_col.clone()),
138            alias: row_index_col.name().to_owned(),
139        });
140    }
141
142    let rewritten_projection = projection.clone().try_map_exprs(|expr| {
143        rewrite_file_row_index_expr(
144            expr,
145            row_index_col.name(),
146            row_index_projection_idx.unwrap_or(base_exprs.len() - 1),
147        )
148    })?;
149
150    ProjectionExprs::new(base_exprs).try_merge(&rewritten_projection)
151}
152
153/// Rewrite [`input_file_name()`][InputFileNameFunc] in pushed
154/// [`ProjectionExprs`] to a per-file [`Literal`] holding `file_name`.
155///
156/// If the projection contains no [`input_file_name()`][InputFileNameFunc] UDF it
157/// is returned unchanged, without allocating the literal or rebuilding the
158/// projection tree (the common case for queries that don't use the function).
159pub fn rewrite_input_file_name_in_projection(
160    projection: ProjectionExprs,
161    file_name: &str,
162) -> Result<ProjectionExprs> {
163    if !projection
164        .iter()
165        .any(|p| expr_references_scalar_udf::<InputFileNameFunc>(&p.expr))
166    {
167        return Ok(projection);
168    }
169
170    let file_name_lit =
171        Arc::new(Literal::new(ScalarValue::Utf8(Some(file_name.to_string()))))
172            as Arc<dyn PhysicalExpr>;
173
174    projection.try_map_exprs(|expr| {
175        rewrite_scalar_udf::<InputFileNameFunc, _>(expr, |_| {
176            Ok(Arc::clone(&file_name_lit))
177        })
178    })
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    use arrow::datatypes::Schema;
186    use datafusion_common::config::ConfigOptions;
187    use datafusion_expr::{Operator, ScalarUDF};
188    use datafusion_physical_expr::expressions;
189    use std::collections::HashMap;
190
191    fn file_row_index_expr() -> Arc<dyn PhysicalExpr> {
192        Arc::new(ScalarFunctionExpr::new(
193            "file_row_index",
194            Arc::new(ScalarUDF::from(FileRowIndexFunc::new())),
195            vec![],
196            Arc::new(Field::new("file_row_index", DataType::Int64, true)),
197            Arc::new(ConfigOptions::default()),
198        ))
199    }
200
201    fn input_file_name_expr() -> Arc<dyn PhysicalExpr> {
202        Arc::new(ScalarFunctionExpr::new(
203            "input_file_name",
204            Arc::new(ScalarUDF::from(InputFileNameFunc::new())),
205            vec![],
206            Arc::new(Field::new("input_file_name", DataType::Utf8, true)),
207            Arc::new(ConfigOptions::default()),
208        ))
209    }
210
211    #[test]
212    fn test_rewrite_scalar_udf_replaces_nested_typed_udf() -> Result<()> {
213        let expr = Arc::new(expressions::BinaryExpr::new(
214            file_row_index_expr(),
215            Operator::Plus,
216            expressions::lit(ScalarValue::Int64(Some(1))),
217        )) as Arc<dyn PhysicalExpr>;
218
219        let rewritten = rewrite_scalar_udf::<FileRowIndexFunc, _>(expr, |_| {
220            Ok(expressions::lit(ScalarValue::Int64(Some(7))))
221        })?;
222
223        let binary = rewritten
224            .downcast_ref::<expressions::BinaryExpr>()
225            .expect("rewritten expression should remain binary");
226        assert_eq!(binary.op(), &Operator::Plus);
227
228        let left = binary
229            .left()
230            .downcast_ref::<Literal>()
231            .expect("left side should be rewritten to a literal");
232        assert_eq!(left.value(), &ScalarValue::Int64(Some(7)));
233
234        let right = binary
235            .right()
236            .downcast_ref::<Literal>()
237            .expect("right side should remain the original literal");
238        assert_eq!(right.value(), &ScalarValue::Int64(Some(1)));
239        Ok(())
240    }
241
242    #[test]
243    fn test_rewrite_input_file_name_in_projection() -> Result<()> {
244        let file_name = "part=west/data.parquet";
245        let projection = ProjectionExprs::new([
246            ProjectionExpr::new(input_file_name_expr(), "file_name"),
247            ProjectionExpr::new(
248                Arc::new(expressions::BinaryExpr::new(
249                    input_file_name_expr(),
250                    Operator::Eq,
251                    expressions::lit(ScalarValue::Utf8(Some(file_name.to_string()))),
252                )),
253                "matches_file",
254            ),
255        ]);
256
257        let rewritten = rewrite_input_file_name_in_projection(projection, file_name)?;
258        let rewritten = rewritten.as_ref();
259        assert_eq!(rewritten[0].alias, "file_name");
260        assert_eq!(rewritten[1].alias, "matches_file");
261
262        let file_name_lit = rewritten[0]
263            .expr
264            .downcast_ref::<Literal>()
265            .expect("input_file_name should rewrite to a literal");
266        assert_eq!(
267            file_name_lit.value(),
268            &ScalarValue::Utf8(Some(file_name.to_string()))
269        );
270
271        let binary = rewritten[1]
272            .expr
273            .downcast_ref::<expressions::BinaryExpr>()
274            .expect("nested expression should remain binary");
275        assert_eq!(binary.op(), &Operator::Eq);
276
277        let left = binary
278            .left()
279            .downcast_ref::<Literal>()
280            .expect("nested input_file_name should rewrite to a literal");
281        assert_eq!(
282            left.value(),
283            &ScalarValue::Utf8(Some(file_name.to_string()))
284        );
285
286        let right = binary
287            .right()
288            .downcast_ref::<Literal>()
289            .expect("comparison literal should remain unchanged");
290        assert_eq!(
291            right.value(),
292            &ScalarValue::Utf8(Some(file_name.to_string()))
293        );
294        Ok(())
295    }
296
297    #[test]
298    fn test_rewrite_file_row_index_expr_to_source_column() -> Result<()> {
299        let expr = rewrite_file_row_index_expr(
300            file_row_index_expr(),
301            "__datafusion_file_row_index",
302            2,
303        )?;
304
305        let cast_expr = expr
306            .downcast_ref::<CastExpr>()
307            .expect("file row index expression should be a cast");
308        assert_eq!(cast_expr.cast_type(), &DataType::Int64);
309        let target_field = cast_expr.target_field();
310        assert_eq!(target_field.name(), "file_row_index");
311        assert_eq!(target_field.data_type(), &DataType::Int64);
312        assert!(target_field.is_nullable());
313        assert!(target_field.metadata().is_empty());
314
315        let source = cast_expr
316            .expr()
317            .downcast_ref::<Column>()
318            .expect("source column");
319        assert_eq!(source.name(), "__datafusion_file_row_index");
320        assert_eq!(source.index(), 2);
321
322        let input_schema = Schema::new(vec![
323            Field::new("value", DataType::Int64, true),
324            Field::new("__datafusion_file_row_index", DataType::Int64, false)
325                .with_metadata(HashMap::from([(
326                    "source".to_string(),
327                    "virtual".to_string(),
328                )])),
329        ]);
330        let return_field = expr.return_field(&input_schema)?;
331        assert_eq!(return_field.name(), "file_row_index");
332        assert_eq!(return_field.data_type(), &DataType::Int64);
333        assert!(return_field.is_nullable());
334        assert!(return_field.metadata().is_empty());
335        Ok(())
336    }
337}