Skip to main content

datafusion_functions/datetime/
date_part.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
18use std::iter::repeat_n;
19use std::str::FromStr;
20use std::sync::Arc;
21
22use arrow::array::timezone::Tz;
23use arrow::array::{Array, ArrayRef, Float64Array, Int32Array, Int64Array};
24use arrow::compute::kernels::cast_utils::IntervalUnit;
25use arrow::compute::{DatePart, binary, date_part};
26use arrow::datatypes::DataType::{
27    Date32, Date64, Duration, Interval, Time32, Time64, Timestamp,
28};
29use arrow::datatypes::TimeUnit::{Microsecond, Millisecond, Nanosecond, Second};
30use arrow::datatypes::{
31    ArrowTimestampType, DataType, Date32Type, Date64Type, Field, FieldRef,
32    IntervalUnit as ArrowIntervalUnit, TimeUnit, TimestampMicrosecondType,
33    TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType,
34};
35use chrono::{Datelike, NaiveDate};
36use datafusion_common::types::{NativeType, logical_date};
37
38use datafusion_common::{
39    Result, ScalarValue,
40    cast::{
41        as_date32_array, as_date64_array, as_int32_array, as_interval_dt_array,
42        as_interval_mdn_array, as_interval_ym_array, as_time32_millisecond_array,
43        as_time32_second_array, as_time64_microsecond_array, as_time64_nanosecond_array,
44        as_timestamp_microsecond_array, as_timestamp_millisecond_array,
45        as_timestamp_nanosecond_array, as_timestamp_second_array,
46    },
47    exec_err, internal_err, not_impl_err,
48    types::logical_string,
49    utils::take_function_args,
50};
51use datafusion_expr::preimage::PreimageResult;
52use datafusion_expr::simplify::SimplifyContext;
53use datafusion_expr::{
54    ColumnarValue, Documentation, Expr, ReturnFieldArgs, ScalarFunctionArgs,
55    ScalarUDFImpl, Signature, TypeSignature, Volatility, interval_arithmetic,
56};
57use datafusion_expr_common::signature::{Coercion, TypeSignatureClass};
58use datafusion_macros::user_doc;
59
60#[user_doc(
61    doc_section(label = "Time and Date Functions"),
62    description = "Returns the specified part of the date as an integer.",
63    syntax_example = "date_part(part, expression)",
64    alternative_syntax = "extract(field FROM source)",
65    argument(
66        name = "part",
67        description = r#"Part of the date to return. The following date parts are supported:
68
69    - year
70    - isoyear (ISO 8601 week-numbering year)
71    - quarter (emits value in inclusive range [1, 4] based on which quartile of the year the date is in)
72    - month
73    - week (week of the year)
74    - day (day of the month)
75    - hour
76    - minute
77    - second
78    - millisecond
79    - microsecond
80    - nanosecond
81    - dow (day of the week where Sunday is 0)
82    - doy (day of the year)
83    - epoch (seconds since Unix epoch for timestamps/dates, total seconds for intervals)
84    - isodow (ISO 8601 day of the week where Monday is 1 and Sunday is 7)
85"#
86    ),
87    argument(
88        name = "expression",
89        description = "Time expression to operate on. Can be a constant, column, or function."
90    ),
91    sql_example = r#"```sql
92> SELECT date_part('year', '2024-05-01T00:00:00');
93+-----------------------------------------------------+
94| date_part(Utf8("year"),Utf8("2024-05-01T00:00:00")) |
95+-----------------------------------------------------+
96| 2024                                                |
97+-----------------------------------------------------+
98> SELECT extract(day FROM timestamp '2024-05-01T00:00:00');
99+----------------------------------------------------+
100| date_part(Utf8("DAY"),Utf8("2024-05-01T00:00:00")) |
101+----------------------------------------------------+
102| 1                                                  |
103+----------------------------------------------------+
104```"#
105)]
106#[derive(Debug, PartialEq, Eq, Hash)]
107pub struct DatePartFunc {
108    signature: Signature,
109    aliases: Vec<String>,
110}
111
112impl Default for DatePartFunc {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl DatePartFunc {
119    pub fn new() -> Self {
120        Self {
121            signature: Signature::one_of(
122                vec![
123                    TypeSignature::Coercible(vec![
124                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
125                        Coercion::new_implicit(
126                            TypeSignatureClass::Timestamp,
127                            // Not consistent with Postgres and DuckDB but to avoid regression we implicit cast string to timestamp
128                            vec![TypeSignatureClass::Native(logical_string())],
129                            NativeType::Timestamp(Nanosecond, None),
130                        ),
131                    ]),
132                    TypeSignature::Coercible(vec![
133                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
134                        Coercion::new_exact(TypeSignatureClass::Native(logical_date())),
135                    ]),
136                    TypeSignature::Coercible(vec![
137                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
138                        Coercion::new_exact(TypeSignatureClass::Time),
139                    ]),
140                    TypeSignature::Coercible(vec![
141                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
142                        Coercion::new_exact(TypeSignatureClass::Interval),
143                    ]),
144                    TypeSignature::Coercible(vec![
145                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
146                        Coercion::new_exact(TypeSignatureClass::Duration),
147                    ]),
148                ],
149                Volatility::Immutable,
150            ),
151            aliases: vec![String::from("datepart")],
152        }
153    }
154}
155
156impl ScalarUDFImpl for DatePartFunc {
157    fn name(&self) -> &str {
158        "date_part"
159    }
160
161    fn signature(&self) -> &Signature {
162        &self.signature
163    }
164
165    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
166        internal_err!("return_field_from_args should be called instead")
167    }
168
169    fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result<FieldRef> {
170        let [field, _] = take_function_args(self.name(), args.scalar_arguments)?;
171        let nullable = args.arg_fields[1].is_nullable();
172
173        field
174            .and_then(|sv| {
175                sv.try_as_str()
176                    .flatten()
177                    .filter(|s| !s.is_empty())
178                    .map(|part| {
179                        if is_epoch(part) {
180                            Field::new(self.name(), DataType::Float64, nullable)
181                        } else if is_nanosecond(part) {
182                            // See notes on [seconds_ns] for rationale
183                            Field::new(self.name(), DataType::Int64, nullable)
184                        } else {
185                            Field::new(self.name(), DataType::Int32, nullable)
186                        }
187                    })
188            })
189            .map(Arc::new)
190            .map_or_else(
191                || exec_err!("{} requires non-empty constant string", self.name()),
192                Ok,
193            )
194    }
195
196    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
197        let args = args.args;
198        let [part, array] = take_function_args(self.name(), args)?;
199
200        let part = if let ColumnarValue::Scalar(ScalarValue::Utf8(Some(v))) = part {
201            v
202        } else if let ColumnarValue::Scalar(ScalarValue::Utf8View(Some(v))) = part {
203            v
204        } else {
205            return exec_err!(
206                "First argument of `DATE_PART` must be non-null scalar Utf8"
207            );
208        };
209
210        let is_scalar = matches!(array, ColumnarValue::Scalar(_));
211
212        let array = match array {
213            ColumnarValue::Array(array) => Arc::clone(&array),
214            ColumnarValue::Scalar(scalar) => scalar.to_array()?,
215        };
216
217        let part_trim = part_normalization(&part);
218
219        // using IntervalUnit here means we hand off all the work of supporting plurals (like "seconds")
220        // and synonyms ( like "ms,msec,msecond,millisecond") to Arrow
221        let arr = if let Ok(interval_unit) = IntervalUnit::from_str(part_trim) {
222            match interval_unit {
223                IntervalUnit::Year => date_part(array.as_ref(), DatePart::Year)?,
224                IntervalUnit::Month => date_part(array.as_ref(), DatePart::Month)?,
225                IntervalUnit::Week => date_part(array.as_ref(), DatePart::Week)?,
226                IntervalUnit::Day => date_part(array.as_ref(), DatePart::Day)?,
227                IntervalUnit::Hour => date_part(array.as_ref(), DatePart::Hour)?,
228                IntervalUnit::Minute => date_part(array.as_ref(), DatePart::Minute)?,
229                IntervalUnit::Second => seconds_as_i32(array.as_ref(), Second)?,
230                IntervalUnit::Millisecond => seconds_as_i32(array.as_ref(), Millisecond)?,
231                IntervalUnit::Microsecond => seconds_as_i32(array.as_ref(), Microsecond)?,
232                IntervalUnit::Nanosecond => seconds_ns(array.as_ref())?,
233                // century and decade are not supported by `DatePart`, although they are supported in postgres
234                _ => return exec_err!("Date part '{part}' not supported"),
235            }
236        } else {
237            // special cases that can be extracted (in postgres) but are not interval units
238            match part_trim.to_lowercase().as_str() {
239                "isoyear" => date_part(array.as_ref(), DatePart::YearISO)?,
240                "qtr" | "quarter" => date_part(array.as_ref(), DatePart::Quarter)?,
241                "doy" => date_part(array.as_ref(), DatePart::DayOfYear)?,
242                "dow" => date_part(array.as_ref(), DatePart::DayOfWeekSunday0)?,
243                "isodow" => {
244                    // Postgres `isodow` is 1..=7 with Mon=1
245                    date_part(array.as_ref(), DatePart::DayOfWeekMonday1)?
246                }
247                "epoch" => epoch(array.as_ref())?,
248                _ => return exec_err!("Date part '{part}' not supported"),
249            }
250        };
251
252        Ok(if is_scalar {
253            ColumnarValue::Scalar(ScalarValue::try_from_array(arr.as_ref(), 0)?)
254        } else {
255            ColumnarValue::Array(arr)
256        })
257    }
258
259    // Only casting the year is supported since pruning other IntervalUnit is not possible
260    // date_part(col, YEAR) = 2024 => col >= '2024-01-01' and col < '2025-01-01'
261    // But for anything less than YEAR simplifying is not possible without specifying the bigger interval
262    // date_part(col, MONTH) = 1 => col = '2023-01-01' or col = '2024-01-01' or ... or col = '3000-01-01'
263    fn preimage(
264        &self,
265        args: &[Expr],
266        lit_expr: &Expr,
267        info: &SimplifyContext,
268    ) -> Result<PreimageResult> {
269        let [part, col_expr] = take_function_args(self.name(), args)?;
270
271        // Get the interval unit from the part argument
272        let interval_unit = part
273            .as_literal()
274            .and_then(|sv| sv.try_as_str().flatten())
275            .map(part_normalization)
276            .and_then(|s| IntervalUnit::from_str(s).ok());
277
278        // only support extracting year
279        match interval_unit {
280            Some(IntervalUnit::Year) => (),
281            _ => return Ok(PreimageResult::None),
282        }
283
284        // Check if the argument is a literal (e.g. date_part(YEAR, col) = 2024)
285        let Some(argument_literal) = lit_expr.as_literal() else {
286            return Ok(PreimageResult::None);
287        };
288
289        // Extract i32 year from Scalar value
290        let year = match argument_literal {
291            ScalarValue::Int32(Some(y)) => *y,
292            _ => return Ok(PreimageResult::None),
293        };
294
295        // Can only extract year from Date32/64 and Timestamp column
296        let target_type = match info.get_data_type(col_expr)? {
297            Date32 | Date64 | Timestamp(_, _) => &info.get_data_type(col_expr)?,
298            _ => return Ok(PreimageResult::None),
299        };
300
301        // Compute the Interval bounds
302        let Some(start_time) = NaiveDate::from_ymd_opt(year, 1, 1) else {
303            return Ok(PreimageResult::None);
304        };
305        let Some(end_time) = start_time.with_year(year + 1) else {
306            return Ok(PreimageResult::None);
307        };
308
309        // Convert to ScalarValues
310        let (Some(lower), Some(upper)) = (
311            date_to_scalar(start_time, target_type),
312            date_to_scalar(end_time, target_type),
313        ) else {
314            return Ok(PreimageResult::None);
315        };
316        let interval = Box::new(interval_arithmetic::Interval::try_new(lower, upper)?);
317
318        Ok(PreimageResult::Range {
319            expr: col_expr.clone(),
320            interval,
321        })
322    }
323
324    fn aliases(&self) -> &[String] {
325        &self.aliases
326    }
327
328    fn documentation(&self) -> Option<&Documentation> {
329        self.doc()
330    }
331}
332
333fn is_epoch(part: &str) -> bool {
334    let part = part_normalization(part);
335    matches!(part.to_lowercase().as_str(), "epoch")
336}
337
338fn is_nanosecond(part: &str) -> bool {
339    IntervalUnit::from_str(part_normalization(part))
340        .map(|p| matches!(p, IntervalUnit::Nanosecond))
341        .unwrap_or(false)
342}
343
344fn date_to_scalar(date: NaiveDate, target_type: &DataType) -> Option<ScalarValue> {
345    Some(match target_type {
346        Date32 => ScalarValue::Date32(Some(Date32Type::from_naive_date(date))),
347        Date64 => ScalarValue::Date64(Some(Date64Type::from_naive_date(date))),
348
349        Timestamp(unit, tz_opt) => {
350            let naive_midnight = date.and_hms_opt(0, 0, 0)?;
351            let tz: Option<Tz> = tz_opt.clone().and_then(|s| s.parse().ok());
352
353            match unit {
354                Second => ScalarValue::TimestampSecond(
355                    TimestampSecondType::from_naive_datetime(naive_midnight, tz.as_ref()),
356                    tz_opt.clone(),
357                ),
358                Millisecond => ScalarValue::TimestampMillisecond(
359                    TimestampMillisecondType::from_naive_datetime(
360                        naive_midnight,
361                        tz.as_ref(),
362                    ),
363                    tz_opt.clone(),
364                ),
365                Microsecond => ScalarValue::TimestampMicrosecond(
366                    TimestampMicrosecondType::from_naive_datetime(
367                        naive_midnight,
368                        tz.as_ref(),
369                    ),
370                    tz_opt.clone(),
371                ),
372                Nanosecond => ScalarValue::TimestampNanosecond(
373                    TimestampNanosecondType::from_naive_datetime(
374                        naive_midnight,
375                        tz.as_ref(),
376                    ),
377                    tz_opt.clone(),
378                ),
379            }
380        }
381        _ => return None,
382    })
383}
384
385// Try to remove quote if exist, if the quote is invalid, return original string and let the downstream function handle the error
386fn part_normalization(part: &str) -> &str {
387    part.strip_prefix(|c| c == '\'' || c == '\"')
388        .and_then(|s| s.strip_suffix(|c| c == '\'' || c == '\"'))
389        .unwrap_or(part)
390}
391
392/// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the
393/// result to a total number of seconds, milliseconds, microseconds or
394/// nanoseconds as an `Int32Array`
395fn seconds_as_i32(array: &dyn Array, unit: TimeUnit) -> Result<ArrayRef> {
396    // Nanosecond is neither supported in Postgres nor DuckDB, to avoid dealing
397    // with overflow and precision issue we don't support nanosecond
398    if unit == Nanosecond {
399        return not_impl_err!("Date part {unit:?} not supported");
400    }
401
402    // Fast path with seconds - no need to compute nanoseconds
403    if unit == Second {
404        return Ok(date_part(array, DatePart::Second)?);
405    }
406
407    // Fast path for Date32 and Date64 - no seconds
408    if array.data_type() == &Date32 || array.data_type() == &Date64 {
409        return Ok(Arc::new(Int32Array::from_iter_values_with_nulls(
410            repeat_n(0, array.len()),
411            array.nulls().cloned(),
412        )));
413    }
414
415    let conversion_factor = match unit {
416        Second => 1_000_000_000,
417        Millisecond => 1_000_000,
418        Microsecond => 1_000,
419        Nanosecond => 1,
420    };
421
422    let second_factor = match unit {
423        Second => 1,
424        Millisecond => 1_000,
425        Microsecond => 1_000_000,
426        Nanosecond => 1_000_000_000,
427    };
428
429    let secs = date_part(array, DatePart::Second)?;
430    // This assumes array is primitive and not a dictionary
431    let secs = as_int32_array(secs.as_ref())?;
432    let subsecs = date_part(array, DatePart::Nanosecond)?;
433    let subsecs = as_int32_array(subsecs.as_ref())?;
434
435    // Special case where there are no nulls.
436    if subsecs.null_count() == 0 {
437        let r: Int32Array = binary(secs, subsecs, |secs, subsecs| {
438            secs * second_factor + (subsecs % 1_000_000_000) / conversion_factor
439        })?;
440        Ok(Arc::new(r))
441    } else {
442        // Nulls in secs are preserved, nulls in subsecs are treated as zero to account for the case
443        // where the number of nanoseconds overflows.
444        let r: Int32Array = secs
445            .iter()
446            .zip(subsecs)
447            .map(|(secs, subsecs)| {
448                secs.map(|secs| {
449                    let subsecs = subsecs.unwrap_or(0);
450                    secs * second_factor + (subsecs % 1_000_000_000) / conversion_factor
451                })
452            })
453            .collect();
454        Ok(Arc::new(r))
455    }
456}
457
458/// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the
459/// result to a total number of seconds, milliseconds, microseconds or
460/// nanoseconds
461///
462/// Given epoch return f64, this is a duplicated function to optimize for f64 type
463fn seconds(array: &dyn Array, unit: TimeUnit) -> Result<ArrayRef> {
464    let sf = match unit {
465        Second => 1_f64,
466        Millisecond => 1_000_f64,
467        Microsecond => 1_000_000_f64,
468        Nanosecond => 1_000_000_000_f64,
469    };
470    let secs = date_part(array, DatePart::Second)?;
471    // This assumes array is primitive and not a dictionary
472    let secs = as_int32_array(secs.as_ref())?;
473    let subsecs = date_part(array, DatePart::Nanosecond)?;
474    let subsecs = as_int32_array(subsecs.as_ref())?;
475
476    // Special case where there are no nulls.
477    if subsecs.null_count() == 0 {
478        let r: Float64Array = binary(secs, subsecs, |secs, subsecs| {
479            (secs as f64 + ((subsecs % 1_000_000_000) as f64 / 1_000_000_000_f64)) * sf
480        })?;
481        Ok(Arc::new(r))
482    } else {
483        // Nulls in secs are preserved, nulls in subsecs are treated as zero to account for the case
484        // where the number of nanoseconds overflows.
485        let r: Float64Array = secs
486            .iter()
487            .zip(subsecs)
488            .map(|(secs, subsecs)| {
489                secs.map(|secs| {
490                    let subsecs = subsecs.unwrap_or(0);
491                    (secs as f64 + ((subsecs % 1_000_000_000) as f64 / 1_000_000_000_f64))
492                        * sf
493                })
494            })
495            .collect();
496        Ok(Arc::new(r))
497    }
498}
499
500fn epoch(array: &dyn Array) -> Result<ArrayRef> {
501    const SECONDS_IN_A_DAY: f64 = 86400_f64;
502    // Note: Month-to-second conversion uses 30 days as an approximation.
503    // This matches PostgreSQL's behavior for interval epoch extraction,
504    // but does not represent exact calendar months (which vary 28-31 days).
505    // See: https://doxygen.postgresql.org/datatype_2timestamp_8h.html
506    const DAYS_PER_MONTH: f64 = 30_f64;
507
508    let f: Float64Array = match array.data_type() {
509        Timestamp(Second, _) => as_timestamp_second_array(array)?.unary(|x| x as f64),
510        Timestamp(Millisecond, _) => {
511            as_timestamp_millisecond_array(array)?.unary(|x| x as f64 / 1_000_f64)
512        }
513        Timestamp(Microsecond, _) => {
514            as_timestamp_microsecond_array(array)?.unary(|x| x as f64 / 1_000_000_f64)
515        }
516        Timestamp(Nanosecond, _) => {
517            as_timestamp_nanosecond_array(array)?.unary(|x| x as f64 / 1_000_000_000_f64)
518        }
519        Date32 => as_date32_array(array)?.unary(|x| x as f64 * SECONDS_IN_A_DAY),
520        Date64 => as_date64_array(array)?.unary(|x| x as f64 / 1_000_f64),
521        Time32(Second) => as_time32_second_array(array)?.unary(|x| x as f64),
522        Time32(Millisecond) => {
523            as_time32_millisecond_array(array)?.unary(|x| x as f64 / 1_000_f64)
524        }
525        Time64(Microsecond) => {
526            as_time64_microsecond_array(array)?.unary(|x| x as f64 / 1_000_000_f64)
527        }
528        Time64(Nanosecond) => {
529            as_time64_nanosecond_array(array)?.unary(|x| x as f64 / 1_000_000_000_f64)
530        }
531        Interval(ArrowIntervalUnit::YearMonth) => as_interval_ym_array(array)?
532            .unary(|x| x as f64 * DAYS_PER_MONTH * SECONDS_IN_A_DAY),
533        Interval(ArrowIntervalUnit::DayTime) => as_interval_dt_array(array)?.unary(|x| {
534            x.days as f64 * SECONDS_IN_A_DAY + x.milliseconds as f64 / 1_000_f64
535        }),
536        Interval(ArrowIntervalUnit::MonthDayNano) => {
537            as_interval_mdn_array(array)?.unary(|x| {
538                x.months as f64 * DAYS_PER_MONTH * SECONDS_IN_A_DAY
539                    + x.days as f64 * SECONDS_IN_A_DAY
540                    + x.nanoseconds as f64 / 1_000_000_000_f64
541            })
542        }
543        Duration(_) => return seconds(array, Second),
544        d => return exec_err!("Cannot convert {d:?} to epoch"),
545    };
546    Ok(Arc::new(f))
547}
548
549/// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the
550/// result to a total number of nanoseconds as an Int64 array.
551///
552/// This returns an Int64 rather than Int32 because  there 1 billion
553/// `nanosecond`s in each second, so representing up to 60 seconds as
554/// nanoseconds can be values up to 60 billion, which does not fit in Int32.
555fn seconds_ns(array: &dyn Array) -> Result<ArrayRef> {
556    // Fast path for Date32 and Date64 - no nanoseconds
557    if array.data_type() == &Date32 || array.data_type() == &Date64 {
558        return Ok(Arc::new(Int64Array::from_iter_values_with_nulls(
559            repeat_n(0, array.len()),
560            array.nulls().cloned(),
561        )));
562    }
563
564    let secs = date_part(array, DatePart::Second)?;
565    // This assumes array is primitive and not a dictionary
566    let secs = as_int32_array(secs.as_ref())?;
567    let subsecs = date_part(array, DatePart::Nanosecond)?;
568    let subsecs = as_int32_array(subsecs.as_ref())?;
569
570    // Special case where there are no nulls.
571    if subsecs.null_count() == 0 {
572        let r: Int64Array = binary(secs, subsecs, |secs, subsecs| {
573            (secs as i64) * 1_000_000_000 + (subsecs as i64)
574        })?;
575        Ok(Arc::new(r))
576    } else {
577        // Nulls in secs are preserved, nulls in subsecs are treated as zero to account for the case
578        // where the number of nanoseconds overflows.
579        let r: Int64Array = secs
580            .iter()
581            .zip(subsecs)
582            .map(|(secs, subsecs)| {
583                secs.map(|secs| {
584                    let subsecs = subsecs.unwrap_or(0);
585                    (secs as i64) * 1_000_000_000 + (subsecs as i64)
586                })
587            })
588            .collect();
589        Ok(Arc::new(r))
590    }
591}