| // 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. | |
| use arrow::array::{Array, Float64Array}; | |
| use datafusion::common::{DataFusionError, ScalarValue}; | |
| use datafusion::physical_plan::ColumnarValue; | |
| use std::sync::Arc; | |
| /// Spark-compatible two-argument logarithm: `log(base, value)`. | |
| /// | |
| /// Returns `log(value) / log(base)`, matching Spark's `Logarithm` expression. | |
| /// Returns null when `base <= 0` or `value <= 0`, matching Spark's `nullSafeEval`. | |
| pub fn spark_log(args: &[ColumnarValue]) -> Result<ColumnarValue, DataFusionError> { | |
| if args.len() != 2 { | |
| return Err(DataFusionError::Internal(format!( | |
| "spark_log requires 2 arguments, got {}", | |
| args.len() | |
| ))); | |
| } | |
| // Spark's Logarithm: log(base, value) = ln(value) / ln(base) | |
| // Returns null when base <= 0 or value <= 0 | |
| fn compute(base: f64, value: f64) -> Option<f64> { | |
| if base <= 0.0 || value <= 0.0 { | |
| None | |
| } else { | |
| Some(value.ln() / base.ln()) | |
| } | |
| } | |
| match (&args[0], &args[1]) { | |
| (ColumnarValue::Array(base_arr), ColumnarValue::Array(val_arr)) => { | |
| let bases = base_arr | |
| .as_any() | |
| .downcast_ref::<Float64Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Internal(format!( | |
| "spark_log expected Float64 for base, got {:?}", | |
| base_arr.data_type() | |
| )) | |
| })?; | |
| let values = val_arr | |
| .as_any() | |
| .downcast_ref::<Float64Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Internal(format!( | |
| "spark_log expected Float64 for value, got {:?}", | |
| val_arr.data_type() | |
| )) | |
| })?; | |
| let result: Float64Array = bases | |
| .iter() | |
| .zip(values.iter()) | |
| .map(|(b, v)| match (b, v) { | |
| (Some(base), Some(value)) => compute(base, value), | |
| _ => None, | |
| }) | |
| .collect(); | |
| Ok(ColumnarValue::Array(Arc::new(result))) | |
| } | |
| (ColumnarValue::Scalar(base_scalar), ColumnarValue::Array(val_arr)) => { | |
| let base = match base_scalar { | |
| ScalarValue::Float64(Some(b)) => *b, | |
| ScalarValue::Float64(None) => { | |
| let result = Float64Array::new_null(val_arr.len()); | |
| return Ok(ColumnarValue::Array(Arc::new(result))); | |
| } | |
| _ => { | |
| return Err(DataFusionError::Internal(format!( | |
| "spark_log expected Float64 scalar for base, got {base_scalar:?}", | |
| ))); | |
| } | |
| }; | |
| let values = val_arr | |
| .as_any() | |
| .downcast_ref::<Float64Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Internal(format!( | |
| "spark_log expected Float64 for value, got {:?}", | |
| val_arr.data_type() | |
| )) | |
| })?; | |
| let result: Float64Array = values | |
| .iter() | |
| .map(|v| v.and_then(|value| compute(base, value))) | |
| .collect(); | |
| Ok(ColumnarValue::Array(Arc::new(result))) | |
| } | |
| (ColumnarValue::Array(base_arr), ColumnarValue::Scalar(val_scalar)) => { | |
| let value = match val_scalar { | |
| ScalarValue::Float64(Some(v)) => *v, | |
| ScalarValue::Float64(None) => { | |
| let result = Float64Array::new_null(base_arr.len()); | |
| return Ok(ColumnarValue::Array(Arc::new(result))); | |
| } | |
| _ => { | |
| return Err(DataFusionError::Internal(format!( | |
| "spark_log expected Float64 scalar for value, got {val_scalar:?}", | |
| ))); | |
| } | |
| }; | |
| let bases = base_arr | |
| .as_any() | |
| .downcast_ref::<Float64Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Internal(format!( | |
| "spark_log expected Float64 for base, got {:?}", | |
| base_arr.data_type() | |
| )) | |
| })?; | |
| let result: Float64Array = bases | |
| .iter() | |
| .map(|b| b.and_then(|base| compute(base, value))) | |
| .collect(); | |
| Ok(ColumnarValue::Array(Arc::new(result))) | |
| } | |
| (ColumnarValue::Scalar(base_scalar), ColumnarValue::Scalar(val_scalar)) => { | |
| let result = match (base_scalar, val_scalar) { | |
| (ScalarValue::Float64(Some(base)), ScalarValue::Float64(Some(value))) => { | |
| ScalarValue::Float64(compute(*base, *value)) | |
| } | |
| (ScalarValue::Float64(_), ScalarValue::Float64(_)) => ScalarValue::Float64(None), | |
| _ => { | |
| return Err(DataFusionError::Internal(format!( | |
| "spark_log expected Float64 scalars, got {base_scalar:?} and {val_scalar:?}", | |
| ))); | |
| } | |
| }; | |
| Ok(ColumnarValue::Scalar(result)) | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod test { | |
| use super::*; | |
| use arrow::array::Array; | |
| #[test] | |
| fn test_spark_log_basic() { | |
| let bases = Float64Array::from(vec![10.0, 2.0, 10.0]); | |
| let values = Float64Array::from(vec![100.0, 8.0, 1.0]); | |
| let result = spark_log(&[ | |
| ColumnarValue::Array(Arc::new(bases)), | |
| ColumnarValue::Array(Arc::new(values)), | |
| ]) | |
| .unwrap(); | |
| if let ColumnarValue::Array(arr) = result { | |
| let arr = arr.as_any().downcast_ref::<Float64Array>().unwrap(); | |
| assert!((arr.value(0) - 2.0).abs() < 1e-10); | |
| assert!((arr.value(1) - 3.0).abs() < 1e-10); | |
| assert!((arr.value(2) - 0.0).abs() < 1e-10); | |
| } else { | |
| panic!("expected array result"); | |
| } | |
| } | |
| #[test] | |
| fn test_spark_log_non_positive_returns_null() { | |
| let bases = Float64Array::from(vec![Some(0.0), Some(-1.0), Some(10.0), Some(10.0)]); | |
| let values = Float64Array::from(vec![Some(10.0), Some(10.0), Some(0.0), Some(-1.0)]); | |
| let result = spark_log(&[ | |
| ColumnarValue::Array(Arc::new(bases)), | |
| ColumnarValue::Array(Arc::new(values)), | |
| ]) | |
| .unwrap(); | |
| if let ColumnarValue::Array(arr) = result { | |
| let arr = arr.as_any().downcast_ref::<Float64Array>().unwrap(); | |
| assert!(arr.is_null(0)); | |
| assert!(arr.is_null(1)); | |
| assert!(arr.is_null(2)); | |
| assert!(arr.is_null(3)); | |
| } else { | |
| panic!("expected array result"); | |
| } | |
| } | |
| #[test] | |
| fn test_spark_log_null_propagation() { | |
| let bases = Float64Array::from(vec![Some(10.0), None]); | |
| let values = Float64Array::from(vec![None, Some(10.0)]); | |
| let result = spark_log(&[ | |
| ColumnarValue::Array(Arc::new(bases)), | |
| ColumnarValue::Array(Arc::new(values)), | |
| ]) | |
| .unwrap(); | |
| if let ColumnarValue::Array(arr) = result { | |
| let arr = arr.as_any().downcast_ref::<Float64Array>().unwrap(); | |
| assert!(arr.is_null(0)); | |
| assert!(arr.is_null(1)); | |
| } else { | |
| panic!("expected array result"); | |
| } | |
| } | |
| #[test] | |
| fn test_spark_log_base_one_returns_nan() { | |
| // log(1, 1) = ln(1) / ln(1) = 0/0 = NaN | |
| let bases = Float64Array::from(vec![1.0]); | |
| let values = Float64Array::from(vec![1.0]); | |
| let result = spark_log(&[ | |
| ColumnarValue::Array(Arc::new(bases)), | |
| ColumnarValue::Array(Arc::new(values)), | |
| ]) | |
| .unwrap(); | |
| if let ColumnarValue::Array(arr) = result { | |
| let arr = arr.as_any().downcast_ref::<Float64Array>().unwrap(); | |
| assert!(arr.value(0).is_nan()); | |
| } else { | |
| panic!("expected array result"); | |
| } | |
| } | |
| } |