## Coverage Report

#### Created: 2025-11-22 06:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/datafusion/datafusion/functions/src/datetime/date_part.rs

    Line
    Count
    Source
    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
    use std::any::Any;
    19
    use std::str::FromStr;
    20
    use std::sync::Arc;
    21
    22
    use arrow::array::{Array, ArrayRef, Float64Array, Int32Array};
    23
    use arrow::compute::kernels::cast_utils::IntervalUnit;
    24
    use arrow::compute::{binary, date_part, DatePart};
    25
    use arrow::datatypes::DataType::{
    26
        Date32, Date64, Duration, Interval, Time32, Time64, Timestamp,
    27
    };
    28
    use arrow::datatypes::TimeUnit::{Microsecond, Millisecond, Nanosecond, Second};
    29
    use arrow::datatypes::{DataType, Field, FieldRef, TimeUnit};
    30
    use datafusion_common::types::{logical_date, NativeType};
    31
    32
    use datafusion_common::{
    33
        cast::{
    34
            as_date32_array, as_date64_array, as_int32_array, as_time32_millisecond_array,
    35
            as_time32_second_array, as_time64_microsecond_array, as_time64_nanosecond_array,
    36
            as_timestamp_microsecond_array, as_timestamp_millisecond_array,
    37
            as_timestamp_nanosecond_array, as_timestamp_second_array,
    38
        },
    39
        exec_err, internal_err, not_impl_err,
    40
        types::logical_string,
    41
        utils::take_function_args,
    42
        Result, ScalarValue,
    43
    };
    44
    use datafusion_expr::{
    45
        ColumnarValue, Documentation, ReturnFieldArgs, ScalarUDFImpl, Signature,
    46
        TypeSignature, Volatility,
    47
    };
    48
    use datafusion_expr_common::signature::{Coercion, TypeSignatureClass};
    49
    use datafusion_macros::user_doc;
    50
    51
    #[user_doc(
    52
        doc_section(label = "Time and Date Functions"),
    53
        description = "Returns the specified part of the date as an integer.",
    54
        syntax_example = "date_part(part, expression)",
    55
        alternative_syntax = "extract(field FROM source)",
    56
        argument(
    57
            name = "part",
    58
            description = r#"Part of the date to return. The following date parts are supported:
    59
    60
        - year
    61
        - quarter (emits value in inclusive range [1, 4] based on which quartile of the year the date is in)
    62
        - month
    63
        - week (week of the year)
    64
        - day (day of the month)
    65
        - hour
    66
        - minute
    67
        - second
    68
        - millisecond
    69
        - microsecond
    70
        - nanosecond
    71
        - dow (day of the week where Sunday is 0)
    72
        - doy (day of the year)
    73
        - epoch (seconds since Unix epoch)
    74
        - isodow (day of the week where Monday is 0)
    75
    "#
    76
        ),
    77
        argument(
    78
            name = "expression",
    79
            description = "Time expression to operate on. Can be a constant, column, or function."
    80
        )
    81
    )]
    82
    #[derive(Debug, PartialEq, Eq, Hash)]
    83
    pub struct DatePartFunc {
    84
        signature: Signature,
    85
        aliases: Vec<String>,
    86
    }
    87
    88
    impl Default for DatePartFunc {
    89
    0
        fn default() -> Self {
    90
    0
            Self::new()
    91
    0
        }
    92
    }
    93
    94
    impl DatePartFunc {
    95
    0
        pub fn new() -> Self {
    96
    0
            Self {
    97
    0
                signature: Signature::one_of(
    98
    0
                    vec![
    99
    0
                        TypeSignature::Coercible(vec![
    100
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    101
    0
                            Coercion::new_implicit(
    102
    0
                                TypeSignatureClass::Timestamp,
    103
    0
                                // Not consistent with Postgres and DuckDB but to avoid regression we implicit cast string to timestamp
    104
    0
                                vec![TypeSignatureClass::Native(logical_string())],
    105
    0
                                NativeType::Timestamp(Nanosecond, None),
    106
    0
                            ),
    107
    0
                        ]),
    108
    0
                        TypeSignature::Coercible(vec![
    109
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    110
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_date())),
    111
    0
                        ]),
    112
    0
                        TypeSignature::Coercible(vec![
    113
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    114
    0
                            Coercion::new_exact(TypeSignatureClass::Time),
    115
    0
                        ]),
    116
    0
                        TypeSignature::Coercible(vec![
    117
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    118
    0
                            Coercion::new_exact(TypeSignatureClass::Interval),
    119
    0
                        ]),
    120
    0
                        TypeSignature::Coercible(vec![
    121
    0
                            Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    122
    0
                            Coercion::new_exact(TypeSignatureClass::Duration),
    123
    0
                        ]),
    124
    0
                    ],
    125
    0
                    Volatility::Immutable,
    126
    0
                ),
    127
    0
                aliases: vec![String::from("datepart")],
    128
    0
            }
    129
    0
        }
    130
    }
    131
    132
    impl ScalarUDFImpl for DatePartFunc {
    133
    0
        fn as_any(&self) -> &dyn Any {
    134
    0
            self
    135
    0
        }
    136
    137
    0
        fn name(&self) -> &str {
    138
    0
            "date_part"
    139
    0
        }
    140
    141
    0
        fn signature(&self) -> &Signature {
    142
    0
            &self.signature
    143
    0
        }
    144
    145
    0
        fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
    146
    0
            internal_err!("return_field_from_args should be called instead")
    147
    0
        }
    148
    149
    0
        fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result<FieldRef> {
    150
    0
            let [field, _] = take_function_args(self.name(), args.scalar_arguments)?;
    151
    152
    0
            field
    153
    0
                .and_then(|sv| {
    154
    0
                    sv.try_as_str()
    155
    0
                        .flatten()
    156
    0
                        .filter(|s| !s.is_empty())
    157
    0
                        .map(|part| {
    158
    0
                            if is_epoch(part) {
    159
    0
                                Field::new(self.name(), DataType::Float64, true)
    160
                            } else {
    161
    0
                                Field::new(self.name(), DataType::Int32, true)
    162
                            }
    163
    0
                        })
    164
    0
                })
    165
    0
                .map(Arc::new)
    166
    0
                .map_or_else(
    167
    0
                    || exec_err!("{} requires non-empty constant string", self.name()),
    168
                    Ok,
    169
                )
    170
    0
        }
    171
    172
    0
        fn invoke_with_args(
    173
    0
            &self,
    174
    0
            args: datafusion_expr::ScalarFunctionArgs,
    175
    0
        ) -> Result<ColumnarValue> {
    176
    0
            let args = args.args;
    177
    0
            let [part, array] = take_function_args(self.name(), args)?;
    178
    179
    0
            let part = if let ColumnarValue::Scalar(ScalarValue::Utf8(Some(v))) = part {
    180
    0
                v
    181
    0
            } else if let ColumnarValue::Scalar(ScalarValue::Utf8View(Some(v))) = part {
    182
    0
                v
    183
            } else {
    184
    0
                return exec_err!(
    185
    0
                    "First argument of `DATE_PART` must be non-null scalar Utf8"
    186
                );
    187
            };
    188
    189
    0
            let is_scalar = matches!(array, ColumnarValue::Scalar(_));
    190
    191
    0
            let array = match array {
    192
    0
                ColumnarValue::Array(array) => Arc::clone(&array),
    193
    0
                ColumnarValue::Scalar(scalar) => scalar.to_array()?,
    194
            };
    195
    196
    0
            let part_trim = part_normalization(&part);
    197
    198
            // using IntervalUnit here means we hand off all the work of supporting plurals (like "seconds")
    199
            // and synonyms ( like "ms,msec,msecond,millisecond") to Arrow
    200
    0
            let arr = if let Ok(interval_unit) = IntervalUnit::from_str(part_trim) {
    201
    0
                match interval_unit {
    202
    0
                    IntervalUnit::Year => date_part(array.as_ref(), DatePart::Year)?,
    203
    0
                    IntervalUnit::Month => date_part(array.as_ref(), DatePart::Month)?,
    204
    0
                    IntervalUnit::Week => date_part(array.as_ref(), DatePart::Week)?,
    205
    0
                    IntervalUnit::Day => date_part(array.as_ref(), DatePart::Day)?,
    206
    0
                    IntervalUnit::Hour => date_part(array.as_ref(), DatePart::Hour)?,
    207
    0
                    IntervalUnit::Minute => date_part(array.as_ref(), DatePart::Minute)?,
    208
    0
                    IntervalUnit::Second => seconds_as_i32(array.as_ref(), Second)?,
    209
    0
                    IntervalUnit::Millisecond => seconds_as_i32(array.as_ref(), Millisecond)?,
    210
    0
                    IntervalUnit::Microsecond => seconds_as_i32(array.as_ref(), Microsecond)?,
    211
    0
                    IntervalUnit::Nanosecond => seconds_as_i32(array.as_ref(), Nanosecond)?,
    212
                    // century and decade are not supported by `DatePart`, although they are supported in postgres
    213
    0
                    _ => return exec_err!("Date part '{part}' not supported"),
    214
                }
    215
            } else {
    216
                // special cases that can be extracted (in postgres) but are not interval units
    217
    0
                match part_trim.to_lowercase().as_str() {
    218
    0
                    "qtr" | "quarter" => date_part(array.as_ref(), DatePart::Quarter)?,
    219
    0
                    "doy" => date_part(array.as_ref(), DatePart::DayOfYear)?,
    220
    0
                    "dow" => date_part(array.as_ref(), DatePart::DayOfWeekSunday0)?,
    221
    0
                    "isodow" => date_part(array.as_ref(), DatePart::DayOfWeekMonday0)?,
    222
    0
                    "epoch" => epoch(array.as_ref())?,
    223
    0
                    _ => return exec_err!("Date part '{part}' not supported"),
    224
                }
    225
            };
    226
    227
    0
            Ok(if is_scalar {
    228
    0
                ColumnarValue::Scalar(ScalarValue::try_from_array(arr.as_ref(), 0)?)
    229
            } else {
    230
    0
                ColumnarValue::Array(arr)
    231
            })
    232
    0
        }
    233
    234
    0
        fn aliases(&self) -> &[String] {
    235
    0
            &self.aliases
    236
    0
        }
    237
    238
    0
        fn documentation(&self) -> Option<&Documentation> {
    239
    0
            self.doc()
    240
    0
        }
    241
    }
    242
    243
    0
    fn is_epoch(part: &str) -> bool {
    244
    0
        let part = part_normalization(part);
    245
    0
        matches!(part.to_lowercase().as_str(), "epoch")
    246
    0
    }
    247
    248
    // Try to remove quote if exist, if the quote is invalid, return original string and let the downstream function handle the error
    249
    0
    fn part_normalization(part: &str) -> &str {
    250
    0
        part.strip_prefix(|c| c == '\'' || c == '\"')
    251
    0
            .and_then(|s| s.strip_suffix(|c| c == '\'' || c == '\"'))
    252
    0
            .unwrap_or(part)
    253
    0
    }
    254
    255
    /// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the
    256
    /// result to a total number of seconds, milliseconds, microseconds or
    257
    /// nanoseconds
    258
    0
    fn seconds_as_i32(array: &dyn Array, unit: TimeUnit) -> Result<ArrayRef> {
    259
        // Nanosecond is neither supported in Postgres nor DuckDB, to avoid dealing
    260
        // with overflow and precision issue we don't support nanosecond
    261
    0
        if unit == Nanosecond {
    262
    0
            return not_impl_err!("Date part {unit:?} not supported");
    263
    0
        }
    264
    265
    0
        let conversion_factor = match unit {
    266
    0
            Second => 1_000_000_000,
    267
    0
            Millisecond => 1_000_000,
    268
    0
            Microsecond => 1_000,
    269
    0
            Nanosecond => 1,
    270
        };
    271
    272
    0
        let second_factor = match unit {
    273
    0
            Second => 1,
    274
    0
            Millisecond => 1_000,
    275
    0
            Microsecond => 1_000_000,
    276
    0
            Nanosecond => 1_000_000_000,
    277
        };
    278
    279
    0
        let secs = date_part(array, DatePart::Second)?;
    280
        // This assumes array is primitive and not a dictionary
    281
    0
        let secs = as_int32_array(secs.as_ref())?;
    282
    0
        let subsecs = date_part(array, DatePart::Nanosecond)?;
    283
    0
        let subsecs = as_int32_array(subsecs.as_ref())?;
    284
    285
        // Special case where there are no nulls.
    286
    0
        if subsecs.null_count() == 0 {
    287
    0
            let r: Int32Array = binary(secs, subsecs, |secs, subsecs| {
    288
    0
                secs * second_factor + (subsecs % 1_000_000_000) / conversion_factor
    289
    0
            })?;
    290
    0
            Ok(Arc::new(r))
    291
        } else {
    292
            // Nulls in secs are preserved, nulls in subsecs are treated as zero to account for the case
    293
            // where the number of nanoseconds overflows.
    294
    0
            let r: Int32Array = secs
    295
    0
                .iter()
    296
    0
                .zip(subsecs)
    297
    0
                .map(|(secs, subsecs)| {
    298
    0
                    secs.map(|secs| {
    299
    0
                        let subsecs = subsecs.unwrap_or(0);
    300
    0
                        secs * second_factor + (subsecs % 1_000_000_000) / conversion_factor
    301
    0
                    })
    302
    0
                })
    303
    0
                .collect();
    304
    0
            Ok(Arc::new(r))
    305
        }
    306
    0
    }
    307
    308
    /// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the
    309
    /// result to a total number of seconds, milliseconds, microseconds or
    310
    /// nanoseconds
    311
    ///
    312
    /// Given epoch return f64, this is a duplicated function to optimize for f64 type
    313
    0
    fn seconds(array: &dyn Array, unit: TimeUnit) -> Result<ArrayRef> {
    314
    0
        let sf = match unit {
    315
    0
            Second => 1_f64,
    316
    0
            Millisecond => 1_000_f64,
    317
    0
            Microsecond => 1_000_000_f64,
    318
    0
            Nanosecond => 1_000_000_000_f64,
    319
        };
    320
    0
        let secs = date_part(array, DatePart::Second)?;
    321
        // This assumes array is primitive and not a dictionary
    322
    0
        let secs = as_int32_array(secs.as_ref())?;
    323
    0
        let subsecs = date_part(array, DatePart::Nanosecond)?;
    324
    0
        let subsecs = as_int32_array(subsecs.as_ref())?;
    325
    326
        // Special case where there are no nulls.
    327
    0
        if subsecs.null_count() == 0 {
    328
    0
            let r: Float64Array = binary(secs, subsecs, |secs, subsecs| {
    329
    0
                (secs as f64 + ((subsecs % 1_000_000_000) as f64 / 1_000_000_000_f64)) * sf
    330
    0
            })?;
    331
    0
            Ok(Arc::new(r))
    332
        } else {
    333
            // Nulls in secs are preserved, nulls in subsecs are treated as zero to account for the case
    334
            // where the number of nanoseconds overflows.
    335
    0
            let r: Float64Array = secs
    336
    0
                .iter()
    337
    0
                .zip(subsecs)
    338
    0
                .map(|(secs, subsecs)| {
    339
    0
                    secs.map(|secs| {
    340
    0
                        let subsecs = subsecs.unwrap_or(0);
    341
    0
                        (secs as f64 + ((subsecs % 1_000_000_000) as f64 / 1_000_000_000_f64))
    342
    0
                            * sf
    343
    0
                    })
    344
    0
                })
    345
    0
                .collect();
    346
    0
            Ok(Arc::new(r))
    347
        }
    348
    0
    }
    349
    350
    0
    fn epoch(array: &dyn Array) -> Result<ArrayRef> {
    351
        const SECONDS_IN_A_DAY: f64 = 86400_f64;
    352
    353
    0
        let f: Float64Array = match array.data_type() {
    354
    0
            Timestamp(Second, _) => as_timestamp_second_array(array)?.unary(|x| x as f64),
    355
            Timestamp(Millisecond, _) => {
    356
    0
                as_timestamp_millisecond_array(array)?.unary(|x| x as f64 / 1_000_f64)
    357
            }
    358
            Timestamp(Microsecond, _) => {
    359
    0
                as_timestamp_microsecond_array(array)?.unary(|x| x as f64 / 1_000_000_f64)
    360
            }
    361
            Timestamp(Nanosecond, _) => {
    362
    0
                as_timestamp_nanosecond_array(array)?.unary(|x| x as f64 / 1_000_000_000_f64)
    363
            }
    364
    0
            Date32 => as_date32_array(array)?.unary(|x| x as f64 * SECONDS_IN_A_DAY),
    365
    0
            Date64 => as_date64_array(array)?.unary(|x| x as f64 / 1_000_f64),
    366
    0
            Time32(Second) => as_time32_second_array(array)?.unary(|x| x as f64),
    367
            Time32(Millisecond) => {
    368
    0
                as_time32_millisecond_array(array)?.unary(|x| x as f64 / 1_000_f64)
    369
            }
    370
            Time64(Microsecond) => {
    371
    0
                as_time64_microsecond_array(array)?.unary(|x| x as f64 / 1_000_000_f64)
    372
            }
    373
            Time64(Nanosecond) => {
    374
    0
                as_time64_nanosecond_array(array)?.unary(|x| x as f64 / 1_000_000_000_f64)
    375
            }
    376
    0
            Interval(_) | Duration(_) => return seconds(array, Second),
    377
    0
            d => return exec_err!("Cannot convert {d:?} to epoch"),
    378
        };
    379
    0
        Ok(Arc::new(f))
    380
    0
    }