datafusion_functions/math/
log.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//! Math function: `log()`.
19
20use std::any::Any;
21use std::sync::Arc;
22
23use super::power::PowerFunc;
24
25use arrow::array::{ArrayRef, AsArray};
26use arrow::datatypes::{DataType, Float32Type, Float64Type};
27use datafusion_common::{
28    exec_err, internal_err, plan_datafusion_err, plan_err, Result, ScalarValue,
29};
30use datafusion_expr::expr::ScalarFunction;
31use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyInfo};
32use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
33use datafusion_expr::{
34    lit, ColumnarValue, Documentation, Expr, ScalarFunctionArgs, ScalarUDF,
35    TypeSignature::*,
36};
37use datafusion_expr::{ScalarUDFImpl, Signature, Volatility};
38use datafusion_macros::user_doc;
39
40#[user_doc(
41    doc_section(label = "Math Functions"),
42    description = "Returns the base-x logarithm of a number. Can either provide a specified base, or if omitted then takes the base-10 of a number.",
43    syntax_example = r#"log(base, numeric_expression)
44log(numeric_expression)"#,
45    standard_argument(name = "base", prefix = "Base numeric"),
46    standard_argument(name = "numeric_expression", prefix = "Numeric")
47)]
48#[derive(Debug, PartialEq, Eq, Hash)]
49pub struct LogFunc {
50    signature: Signature,
51}
52
53impl Default for LogFunc {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl LogFunc {
60    pub fn new() -> Self {
61        use DataType::*;
62        Self {
63            signature: Signature::one_of(
64                vec![
65                    Exact(vec![Float32]),
66                    Exact(vec![Float64]),
67                    Exact(vec![Float32, Float32]),
68                    Exact(vec![Float64, Float64]),
69                ],
70                Volatility::Immutable,
71            ),
72        }
73    }
74}
75
76impl ScalarUDFImpl for LogFunc {
77    fn as_any(&self) -> &dyn Any {
78        self
79    }
80    fn name(&self) -> &str {
81        "log"
82    }
83
84    fn signature(&self) -> &Signature {
85        &self.signature
86    }
87
88    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
89        match &arg_types[0] {
90            DataType::Float32 => Ok(DataType::Float32),
91            _ => Ok(DataType::Float64),
92        }
93    }
94
95    fn output_ordering(&self, input: &[ExprProperties]) -> Result<SortProperties> {
96        let (base_sort_properties, num_sort_properties) = if input.len() == 1 {
97            // log(x) defaults to log(10, x)
98            (SortProperties::Singleton, input[0].sort_properties)
99        } else {
100            (input[0].sort_properties, input[1].sort_properties)
101        };
102        match (num_sort_properties, base_sort_properties) {
103            (first @ SortProperties::Ordered(num), SortProperties::Ordered(base))
104                if num.descending != base.descending
105                    && num.nulls_first == base.nulls_first =>
106            {
107                Ok(first)
108            }
109            (
110                first @ (SortProperties::Ordered(_) | SortProperties::Singleton),
111                SortProperties::Singleton,
112            ) => Ok(first),
113            (SortProperties::Singleton, second @ SortProperties::Ordered(_)) => {
114                Ok(-second)
115            }
116            _ => Ok(SortProperties::Unordered),
117        }
118    }
119
120    // Support overloaded log(base, x) and log(x) which defaults to log(10, x)
121    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
122        let args = ColumnarValue::values_to_arrays(&args.args)?;
123
124        let mut base = ColumnarValue::Scalar(ScalarValue::Float32(Some(10.0)));
125
126        let mut x = &args[0];
127        if args.len() == 2 {
128            x = &args[1];
129            base = ColumnarValue::Array(Arc::clone(&args[0]));
130        }
131        // note in f64::log params order is different than in sql. e.g in sql log(base, x) == f64::log(x, base)
132        let arr: ArrayRef = match args[0].data_type() {
133            DataType::Float64 => match base {
134                ColumnarValue::Scalar(ScalarValue::Float32(Some(base))) => {
135                    Arc::new(x.as_primitive::<Float64Type>().unary::<_, Float64Type>(
136                        |value: f64| f64::log(value, base as f64),
137                    ))
138                }
139                ColumnarValue::Array(base) => {
140                    let x = x.as_primitive::<Float64Type>();
141                    let base = base.as_primitive::<Float64Type>();
142                    let result = arrow::compute::binary::<_, _, _, Float64Type>(
143                        x,
144                        base,
145                        f64::log,
146                    )?;
147                    Arc::new(result) as _
148                }
149                _ => {
150                    return exec_err!("log function requires a scalar or array for base")
151                }
152            },
153
154            DataType::Float32 => match base {
155                ColumnarValue::Scalar(ScalarValue::Float32(Some(base))) => Arc::new(
156                    x.as_primitive::<Float32Type>()
157                        .unary::<_, Float32Type>(|value: f32| f32::log(value, base)),
158                ),
159                ColumnarValue::Array(base) => {
160                    let x = x.as_primitive::<Float32Type>();
161                    let base = base.as_primitive::<Float32Type>();
162                    let result = arrow::compute::binary::<_, _, _, Float32Type>(
163                        x,
164                        base,
165                        f32::log,
166                    )?;
167                    Arc::new(result) as _
168                }
169                _ => {
170                    return exec_err!("log function requires a scalar or array for base")
171                }
172            },
173            other => {
174                return exec_err!("Unsupported data type {other:?} for function log")
175            }
176        };
177
178        Ok(ColumnarValue::Array(arr))
179    }
180
181    fn documentation(&self) -> Option<&Documentation> {
182        self.doc()
183    }
184
185    /// Simplify the `log` function by the relevant rules:
186    /// 1. Log(a, 1) ===> 0
187    /// 2. Log(a, Power(a, b)) ===> b
188    /// 3. Log(a, a) ===> 1
189    fn simplify(
190        &self,
191        mut args: Vec<Expr>,
192        info: &dyn SimplifyInfo,
193    ) -> Result<ExprSimplifyResult> {
194        // Args are either
195        // log(number)
196        // log(base, number)
197        let num_args = args.len();
198        if num_args > 2 {
199            return plan_err!("Expected log to have 1 or 2 arguments, got {num_args}");
200        }
201        let number = args.pop().ok_or_else(|| {
202            plan_datafusion_err!("Expected log to have 1 or 2 arguments, got 0")
203        })?;
204        let number_datatype = info.get_data_type(&number)?;
205        // default to base 10
206        let base = if let Some(base) = args.pop() {
207            base
208        } else {
209            lit(ScalarValue::new_ten(&number_datatype)?)
210        };
211
212        match number {
213            Expr::Literal(value, _)
214                if value == ScalarValue::new_one(&number_datatype)? =>
215            {
216                Ok(ExprSimplifyResult::Simplified(lit(ScalarValue::new_zero(
217                    &info.get_data_type(&base)?,
218                )?)))
219            }
220            Expr::ScalarFunction(ScalarFunction { func, mut args })
221                if is_pow(&func) && args.len() == 2 && base == args[0] =>
222            {
223                let b = args.pop().unwrap(); // length checked above
224                Ok(ExprSimplifyResult::Simplified(b))
225            }
226            number => {
227                if number == base {
228                    Ok(ExprSimplifyResult::Simplified(lit(ScalarValue::new_one(
229                        &number_datatype,
230                    )?)))
231                } else {
232                    let args = match num_args {
233                        1 => vec![number],
234                        2 => vec![base, number],
235                        _ => {
236                            return internal_err!(
237                                "Unexpected number of arguments in log::simplify"
238                            )
239                        }
240                    };
241                    Ok(ExprSimplifyResult::Original(args))
242                }
243            }
244        }
245    }
246}
247
248/// Returns true if the function is `PowerFunc`
249fn is_pow(func: &ScalarUDF) -> bool {
250    func.inner().as_any().downcast_ref::<PowerFunc>().is_some()
251}
252
253#[cfg(test)]
254mod tests {
255    use std::collections::HashMap;
256
257    use super::*;
258
259    use arrow::array::{Float32Array, Float64Array, Int64Array};
260    use arrow::compute::SortOptions;
261    use arrow::datatypes::Field;
262    use datafusion_common::cast::{as_float32_array, as_float64_array};
263    use datafusion_common::config::ConfigOptions;
264    use datafusion_common::DFSchema;
265    use datafusion_expr::execution_props::ExecutionProps;
266    use datafusion_expr::simplify::SimplifyContext;
267
268    #[test]
269    #[should_panic]
270    fn test_log_invalid_base_type() {
271        let arg_fields = vec![
272            Field::new("a", DataType::Float64, false).into(),
273            Field::new("a", DataType::Int64, false).into(),
274        ];
275        let args = ScalarFunctionArgs {
276            args: vec![
277                ColumnarValue::Array(Arc::new(Float64Array::from(vec![
278                    10.0, 100.0, 1000.0, 10000.0,
279                ]))), // num
280                ColumnarValue::Array(Arc::new(Int64Array::from(vec![5, 10, 15, 20]))),
281            ],
282            arg_fields,
283            number_rows: 4,
284            return_field: Field::new("f", DataType::Float64, true).into(),
285            config_options: Arc::new(ConfigOptions::default()),
286        };
287        let _ = LogFunc::new().invoke_with_args(args);
288    }
289
290    #[test]
291    fn test_log_invalid_value() {
292        let arg_field = Field::new("a", DataType::Int64, false).into();
293        let args = ScalarFunctionArgs {
294            args: vec![
295                ColumnarValue::Array(Arc::new(Int64Array::from(vec![10]))), // num
296            ],
297            arg_fields: vec![arg_field],
298            number_rows: 1,
299            return_field: Field::new("f", DataType::Float64, true).into(),
300            config_options: Arc::new(ConfigOptions::default()),
301        };
302
303        let result = LogFunc::new().invoke_with_args(args);
304        result.expect_err("expected error");
305    }
306
307    #[test]
308    fn test_log_scalar_f32_unary() {
309        let arg_field = Field::new("a", DataType::Float32, false).into();
310        let args = ScalarFunctionArgs {
311            args: vec![
312                ColumnarValue::Scalar(ScalarValue::Float32(Some(10.0))), // num
313            ],
314            arg_fields: vec![arg_field],
315            number_rows: 1,
316            return_field: Field::new("f", DataType::Float32, true).into(),
317            config_options: Arc::new(ConfigOptions::default()),
318        };
319        let result = LogFunc::new()
320            .invoke_with_args(args)
321            .expect("failed to initialize function log");
322
323        match result {
324            ColumnarValue::Array(arr) => {
325                let floats = as_float32_array(&arr)
326                    .expect("failed to convert result to a Float32Array");
327
328                assert_eq!(floats.len(), 1);
329                assert!((floats.value(0) - 1.0).abs() < 1e-10);
330            }
331            ColumnarValue::Scalar(_) => {
332                panic!("Expected an array value")
333            }
334        }
335    }
336
337    #[test]
338    fn test_log_scalar_f64_unary() {
339        let arg_field = Field::new("a", DataType::Float64, false).into();
340        let args = ScalarFunctionArgs {
341            args: vec![
342                ColumnarValue::Scalar(ScalarValue::Float64(Some(10.0))), // num
343            ],
344            arg_fields: vec![arg_field],
345            number_rows: 1,
346            return_field: Field::new("f", DataType::Float64, true).into(),
347            config_options: Arc::new(ConfigOptions::default()),
348        };
349        let result = LogFunc::new()
350            .invoke_with_args(args)
351            .expect("failed to initialize function log");
352
353        match result {
354            ColumnarValue::Array(arr) => {
355                let floats = as_float64_array(&arr)
356                    .expect("failed to convert result to a Float64Array");
357
358                assert_eq!(floats.len(), 1);
359                assert!((floats.value(0) - 1.0).abs() < 1e-10);
360            }
361            ColumnarValue::Scalar(_) => {
362                panic!("Expected an array value")
363            }
364        }
365    }
366
367    #[test]
368    fn test_log_scalar_f32() {
369        let arg_fields = vec![
370            Field::new("a", DataType::Float32, false).into(),
371            Field::new("a", DataType::Float32, false).into(),
372        ];
373        let args = ScalarFunctionArgs {
374            args: vec![
375                ColumnarValue::Scalar(ScalarValue::Float32(Some(2.0))), // num
376                ColumnarValue::Scalar(ScalarValue::Float32(Some(32.0))), // num
377            ],
378            arg_fields,
379            number_rows: 1,
380            return_field: Field::new("f", DataType::Float32, true).into(),
381            config_options: Arc::new(ConfigOptions::default()),
382        };
383        let result = LogFunc::new()
384            .invoke_with_args(args)
385            .expect("failed to initialize function log");
386
387        match result {
388            ColumnarValue::Array(arr) => {
389                let floats = as_float32_array(&arr)
390                    .expect("failed to convert result to a Float32Array");
391
392                assert_eq!(floats.len(), 1);
393                assert!((floats.value(0) - 5.0).abs() < 1e-10);
394            }
395            ColumnarValue::Scalar(_) => {
396                panic!("Expected an array value")
397            }
398        }
399    }
400
401    #[test]
402    fn test_log_scalar_f64() {
403        let arg_fields = vec![
404            Field::new("a", DataType::Float64, false).into(),
405            Field::new("a", DataType::Float64, false).into(),
406        ];
407        let args = ScalarFunctionArgs {
408            args: vec![
409                ColumnarValue::Scalar(ScalarValue::Float64(Some(2.0))), // num
410                ColumnarValue::Scalar(ScalarValue::Float64(Some(64.0))), // num
411            ],
412            arg_fields,
413            number_rows: 1,
414            return_field: Field::new("f", DataType::Float64, true).into(),
415            config_options: Arc::new(ConfigOptions::default()),
416        };
417        let result = LogFunc::new()
418            .invoke_with_args(args)
419            .expect("failed to initialize function log");
420
421        match result {
422            ColumnarValue::Array(arr) => {
423                let floats = as_float64_array(&arr)
424                    .expect("failed to convert result to a Float64Array");
425
426                assert_eq!(floats.len(), 1);
427                assert!((floats.value(0) - 6.0).abs() < 1e-10);
428            }
429            ColumnarValue::Scalar(_) => {
430                panic!("Expected an array value")
431            }
432        }
433    }
434
435    #[test]
436    fn test_log_f64_unary() {
437        let arg_field = Field::new("a", DataType::Float64, false).into();
438        let args = ScalarFunctionArgs {
439            args: vec![
440                ColumnarValue::Array(Arc::new(Float64Array::from(vec![
441                    10.0, 100.0, 1000.0, 10000.0,
442                ]))), // num
443            ],
444            arg_fields: vec![arg_field],
445            number_rows: 4,
446            return_field: Field::new("f", DataType::Float64, true).into(),
447            config_options: Arc::new(ConfigOptions::default()),
448        };
449        let result = LogFunc::new()
450            .invoke_with_args(args)
451            .expect("failed to initialize function log");
452
453        match result {
454            ColumnarValue::Array(arr) => {
455                let floats = as_float64_array(&arr)
456                    .expect("failed to convert result to a Float64Array");
457
458                assert_eq!(floats.len(), 4);
459                assert!((floats.value(0) - 1.0).abs() < 1e-10);
460                assert!((floats.value(1) - 2.0).abs() < 1e-10);
461                assert!((floats.value(2) - 3.0).abs() < 1e-10);
462                assert!((floats.value(3) - 4.0).abs() < 1e-10);
463            }
464            ColumnarValue::Scalar(_) => {
465                panic!("Expected an array value")
466            }
467        }
468    }
469
470    #[test]
471    fn test_log_f32_unary() {
472        let arg_field = Field::new("a", DataType::Float32, false).into();
473        let args = ScalarFunctionArgs {
474            args: vec![
475                ColumnarValue::Array(Arc::new(Float32Array::from(vec![
476                    10.0, 100.0, 1000.0, 10000.0,
477                ]))), // num
478            ],
479            arg_fields: vec![arg_field],
480            number_rows: 4,
481            return_field: Field::new("f", DataType::Float32, true).into(),
482            config_options: Arc::new(ConfigOptions::default()),
483        };
484        let result = LogFunc::new()
485            .invoke_with_args(args)
486            .expect("failed to initialize function log");
487
488        match result {
489            ColumnarValue::Array(arr) => {
490                let floats = as_float32_array(&arr)
491                    .expect("failed to convert result to a Float64Array");
492
493                assert_eq!(floats.len(), 4);
494                assert!((floats.value(0) - 1.0).abs() < 1e-10);
495                assert!((floats.value(1) - 2.0).abs() < 1e-10);
496                assert!((floats.value(2) - 3.0).abs() < 1e-10);
497                assert!((floats.value(3) - 4.0).abs() < 1e-10);
498            }
499            ColumnarValue::Scalar(_) => {
500                panic!("Expected an array value")
501            }
502        }
503    }
504
505    #[test]
506    fn test_log_f64() {
507        let arg_fields = vec![
508            Field::new("a", DataType::Float64, false).into(),
509            Field::new("a", DataType::Float64, false).into(),
510        ];
511        let args = ScalarFunctionArgs {
512            args: vec![
513                ColumnarValue::Array(Arc::new(Float64Array::from(vec![
514                    2.0, 2.0, 3.0, 5.0,
515                ]))), // base
516                ColumnarValue::Array(Arc::new(Float64Array::from(vec![
517                    8.0, 4.0, 81.0, 625.0,
518                ]))), // num
519            ],
520            arg_fields,
521            number_rows: 4,
522            return_field: Field::new("f", DataType::Float64, true).into(),
523            config_options: Arc::new(ConfigOptions::default()),
524        };
525        let result = LogFunc::new()
526            .invoke_with_args(args)
527            .expect("failed to initialize function log");
528
529        match result {
530            ColumnarValue::Array(arr) => {
531                let floats = as_float64_array(&arr)
532                    .expect("failed to convert result to a Float64Array");
533
534                assert_eq!(floats.len(), 4);
535                assert!((floats.value(0) - 3.0).abs() < 1e-10);
536                assert!((floats.value(1) - 2.0).abs() < 1e-10);
537                assert!((floats.value(2) - 4.0).abs() < 1e-10);
538                assert!((floats.value(3) - 4.0).abs() < 1e-10);
539            }
540            ColumnarValue::Scalar(_) => {
541                panic!("Expected an array value")
542            }
543        }
544    }
545
546    #[test]
547    fn test_log_f32() {
548        let arg_fields = vec![
549            Field::new("a", DataType::Float32, false).into(),
550            Field::new("a", DataType::Float32, false).into(),
551        ];
552        let args = ScalarFunctionArgs {
553            args: vec![
554                ColumnarValue::Array(Arc::new(Float32Array::from(vec![
555                    2.0, 2.0, 3.0, 5.0,
556                ]))), // base
557                ColumnarValue::Array(Arc::new(Float32Array::from(vec![
558                    8.0, 4.0, 81.0, 625.0,
559                ]))), // num
560            ],
561            arg_fields,
562            number_rows: 4,
563            return_field: Field::new("f", DataType::Float32, true).into(),
564            config_options: Arc::new(ConfigOptions::default()),
565        };
566        let result = LogFunc::new()
567            .invoke_with_args(args)
568            .expect("failed to initialize function log");
569
570        match result {
571            ColumnarValue::Array(arr) => {
572                let floats = as_float32_array(&arr)
573                    .expect("failed to convert result to a Float32Array");
574
575                assert_eq!(floats.len(), 4);
576                assert!((floats.value(0) - 3.0).abs() < f32::EPSILON);
577                assert!((floats.value(1) - 2.0).abs() < f32::EPSILON);
578                assert!((floats.value(2) - 4.0).abs() < f32::EPSILON);
579                assert!((floats.value(3) - 4.0).abs() < f32::EPSILON);
580            }
581            ColumnarValue::Scalar(_) => {
582                panic!("Expected an array value")
583            }
584        }
585    }
586    #[test]
587    // Test log() simplification errors
588    fn test_log_simplify_errors() {
589        let props = ExecutionProps::new();
590        let schema =
591            Arc::new(DFSchema::new_with_metadata(vec![], HashMap::new()).unwrap());
592        let context = SimplifyContext::new(&props).with_schema(schema);
593        // Expect 0 args to error
594        let _ = LogFunc::new().simplify(vec![], &context).unwrap_err();
595        // Expect 3 args to error
596        let _ = LogFunc::new()
597            .simplify(vec![lit(1), lit(2), lit(3)], &context)
598            .unwrap_err();
599    }
600
601    #[test]
602    // Test that non-simplifiable log() expressions are unchanged after simplification
603    fn test_log_simplify_original() {
604        let props = ExecutionProps::new();
605        let schema =
606            Arc::new(DFSchema::new_with_metadata(vec![], HashMap::new()).unwrap());
607        let context = SimplifyContext::new(&props).with_schema(schema);
608        // One argument with no simplifications
609        let result = LogFunc::new().simplify(vec![lit(2)], &context).unwrap();
610        let ExprSimplifyResult::Original(args) = result else {
611            panic!("Expected ExprSimplifyResult::Original")
612        };
613        assert_eq!(args.len(), 1);
614        assert_eq!(args[0], lit(2));
615        // Two arguments with no simplifications
616        let result = LogFunc::new()
617            .simplify(vec![lit(2), lit(3)], &context)
618            .unwrap();
619        let ExprSimplifyResult::Original(args) = result else {
620            panic!("Expected ExprSimplifyResult::Original")
621        };
622        assert_eq!(args.len(), 2);
623        assert_eq!(args[0], lit(2));
624        assert_eq!(args[1], lit(3));
625    }
626
627    #[test]
628    fn test_log_output_ordering() {
629        // [Unordered, Ascending, Descending, Literal]
630        let orders = vec![
631            ExprProperties::new_unknown(),
632            ExprProperties::new_unknown().with_order(SortProperties::Ordered(
633                SortOptions {
634                    descending: false,
635                    nulls_first: true,
636                },
637            )),
638            ExprProperties::new_unknown().with_order(SortProperties::Ordered(
639                SortOptions {
640                    descending: true,
641                    nulls_first: true,
642                },
643            )),
644            ExprProperties::new_unknown().with_order(SortProperties::Singleton),
645        ];
646
647        let log = LogFunc::new();
648
649        // Test log(num)
650        for order in orders.iter().cloned() {
651            let result = log.output_ordering(std::slice::from_ref(&order)).unwrap();
652            assert_eq!(result, order.sort_properties);
653        }
654
655        // Test log(base, num), where `nulls_first` is the same
656        let mut results = Vec::with_capacity(orders.len() * orders.len());
657        for base_order in orders.iter() {
658            for num_order in orders.iter().cloned() {
659                let result = log
660                    .output_ordering(&[base_order.clone(), num_order])
661                    .unwrap();
662                results.push(result);
663            }
664        }
665        let expected = vec![
666            // base: Unordered
667            SortProperties::Unordered,
668            SortProperties::Unordered,
669            SortProperties::Unordered,
670            SortProperties::Unordered,
671            // base: Ascending, num: Unordered
672            SortProperties::Unordered,
673            // base: Ascending, num: Ascending
674            SortProperties::Unordered,
675            // base: Ascending, num: Descending
676            SortProperties::Ordered(SortOptions {
677                descending: true,
678                nulls_first: true,
679            }),
680            // base: Ascending, num: Literal
681            SortProperties::Ordered(SortOptions {
682                descending: true,
683                nulls_first: true,
684            }),
685            // base: Descending, num: Unordered
686            SortProperties::Unordered,
687            // base: Descending, num: Ascending
688            SortProperties::Ordered(SortOptions {
689                descending: false,
690                nulls_first: true,
691            }),
692            // base: Descending, num: Descending
693            SortProperties::Unordered,
694            // base: Descending, num: Literal
695            SortProperties::Ordered(SortOptions {
696                descending: false,
697                nulls_first: true,
698            }),
699            // base: Literal, num: Unordered
700            SortProperties::Unordered,
701            // base: Literal, num: Ascending
702            SortProperties::Ordered(SortOptions {
703                descending: false,
704                nulls_first: true,
705            }),
706            // base: Literal, num: Descending
707            SortProperties::Ordered(SortOptions {
708                descending: true,
709                nulls_first: true,
710            }),
711            // base: Literal, num: Literal
712            SortProperties::Singleton,
713        ];
714        assert_eq!(results, expected);
715
716        // Test with different `nulls_first`
717        let base_order = ExprProperties::new_unknown().with_order(
718            SortProperties::Ordered(SortOptions {
719                descending: true,
720                nulls_first: true,
721            }),
722        );
723        let num_order = ExprProperties::new_unknown().with_order(
724            SortProperties::Ordered(SortOptions {
725                descending: false,
726                nulls_first: false,
727            }),
728        );
729        assert_eq!(
730            log.output_ordering(&[base_order, num_order]).unwrap(),
731            SortProperties::Unordered
732        );
733    }
734}