diff --git a/datafusion/functions/src/datetime/common.rs b/datafusion/functions/src/datetime/common.rs index 2db64beafa9b7..cb2c67b51bb93 100644 --- a/datafusion/functions/src/datetime/common.rs +++ b/datafusion/functions/src/datetime/common.rs @@ -23,22 +23,89 @@ use arrow::array::{ StringArrayType, StringViewArray, }; use arrow::compute::DecimalCast; -use arrow::compute::kernels::cast_utils::string_to_datetime; -use arrow::datatypes::{DataType, TimeUnit}; +use arrow::compute::kernels::cast_utils::{ + string_to_datetime, string_to_timestamp_nanos, +}; +use arrow::datatypes::{ArrowTimestampType, DataType, TimeUnit}; use arrow_buffer::ArrowNativeType; use chrono::LocalResult::Single; use chrono::format::{Parsed, StrftimeItems, parse}; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, MappedLocalTime, TimeDelta, TimeZone, Utc}; use datafusion_common::cast::as_generic_string_array; use datafusion_common::{ DataFusionError, Result, ScalarValue, exec_datafusion_err, exec_err, internal_datafusion_err, unwrap_or_internal_err, }; use datafusion_expr::ColumnarValue; +use std::ops::Add; /// Error message if nanosecond conversion request beyond supported interval const ERR_NANOSECONDS_NOT_SUPPORTED: &str = "The dates that can be represented as nanoseconds have to be between 1677-09-21T00:12:44.0 and 2262-04-11T23:47:16.854775804"; +pub fn adjust_to_local_time(ts: i64, tz: Tz) -> Result { + fn convert_timestamp(ts: i64, converter: F) -> Result> + where + F: Fn(i64) -> MappedLocalTime>, + { + match converter(ts) { + MappedLocalTime::Ambiguous(earliest, latest) => exec_err!( + "Ambiguous timestamp. Do you mean {:?} or {:?}", + earliest, + latest + ), + MappedLocalTime::None => exec_err!( + "The local time does not exist because there is a gap in the local time." + ), + Single(date_time) => Ok(date_time), + } + } + + let date_time = match T::UNIT { + TimeUnit::Nanosecond => Utc.timestamp_nanos(ts), + TimeUnit::Microsecond => convert_timestamp(ts, |ts| Utc.timestamp_micros(ts))?, + TimeUnit::Millisecond => convert_timestamp(ts, |ts| Utc.timestamp_millis_opt(ts))?, + TimeUnit::Second => convert_timestamp(ts, |ts| Utc.timestamp_opt(ts, 0))?, + }; + + // Get the timezone offset for this datetime + let tz_offset = tz.offset_from_utc_datetime(&date_time.naive_utc()); + // Convert offset to seconds - offset is formatted like "+01:00" or "-05:00" + let offset_str = format!("{tz_offset}"); + let offset_seconds: i64 = if let Some(stripped) = offset_str.strip_prefix('-') { + let parts: Vec<&str> = stripped.split(':').collect(); + let hours: i64 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0); + let mins: i64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + -((hours * 3600) + (mins * 60)) + } else { + let parts: Vec<&str> = offset_str.split(':').collect(); + let hours: i64 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0); + let mins: i64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + (hours * 3600) + (mins * 60) + }; + + let adjusted_date_time = date_time.add( + TimeDelta::try_seconds(offset_seconds) + .ok_or_else(|| internal_datafusion_err!("Offset seconds should be less than i64::MAX / 1_000 or greater than -i64::MAX / 1_000"))?, + ); + + // convert back to i64 + match T::UNIT { + TimeUnit::Nanosecond => adjusted_date_time.timestamp_nanos_opt().ok_or_else(|| { + internal_datafusion_err!( + "Failed to convert DateTime to timestamp in nanosecond. This error may occur if the date is out of range. The supported date ranges are between 1677-09-21T00:12:43.145224192 and 2262-04-11T23:47:16.854775807" + ) + }), + TimeUnit::Microsecond => Ok(adjusted_date_time.timestamp_micros()), + TimeUnit::Millisecond => Ok(adjusted_date_time.timestamp_millis()), + TimeUnit::Second => Ok(adjusted_date_time.timestamp()), + } +} + +/// Calls string_to_timestamp_nanos and converts the error type +pub(crate) fn string_to_timestamp_nanos_shim(s: &str) -> Result { + string_to_timestamp_nanos(s).map_err(|e| e.into()) +} + static UTC: LazyLock = LazyLock::new(|| "UTC".parse().expect("UTC is always valid")); /// Converts a string representation of a date‑time into a timestamp expressed in @@ -452,8 +519,7 @@ where ) } other => exec_err!( - "Unsupported data type {other:?} for function substr,\ - expected Utf8View, Utf8 or LargeUtf8." + "Unsupported data type {other:?} for function substr, expected Utf8View, Utf8 or LargeUtf8." ), }, other => exec_err!( @@ -487,12 +553,14 @@ where DataType::Utf8View => Ok(a.as_string_view().value(pos)), DataType::LargeUtf8 => Ok(a.as_string::().value(pos)), DataType::Utf8 => Ok(a.as_string::().value(pos)), - other => exec_err!("Unexpected type encountered '{other}'"), + other => exec_err!("Unexpected type encountered '{}'", other), }, ColumnarValue::Scalar(s) => match s.try_as_str() { Some(Some(v)) => Ok(v), Some(None) => continue, // null string - None => exec_err!("Unexpected scalar type encountered '{s}'"), + None => { + exec_err!("Unexpected scalar type encountered '{}'", s) + } }, }?; @@ -540,6 +608,6 @@ fn scalar_value(dt: &DataType, r: Option) -> Result { TimeUnit::Microsecond => Ok(ScalarValue::TimestampMicrosecond(r, tz.clone())), TimeUnit::Nanosecond => Ok(ScalarValue::TimestampNanosecond(r, tz.clone())), }, - t => Err(internal_datafusion_err!("Unsupported data type: {t:?}")), + t => Err(internal_datafusion_err!("Unsupported data type: {:?}", t)), } } diff --git a/datafusion/functions/src/datetime/date_part.rs b/datafusion/functions/src/datetime/date_part.rs index 375200d07280b..6e3c38402df37 100644 --- a/datafusion/functions/src/datetime/date_part.rs +++ b/datafusion/functions/src/datetime/date_part.rs @@ -19,16 +19,23 @@ use std::any::Any; use std::str::FromStr; use std::sync::Arc; -use arrow::array::{Array, ArrayRef, Float64Array, Int32Array}; +use arrow::array::timezone::Tz; +use arrow::array::{Array, ArrayRef, Float64Array, Int32Array, PrimitiveBuilder}; use arrow::compute::kernels::cast_utils::IntervalUnit; use arrow::compute::{DatePart, binary, date_part}; use arrow::datatypes::DataType::{ Date32, Date64, Duration, Interval, Time32, Time64, Timestamp, }; use arrow::datatypes::TimeUnit::{Microsecond, Millisecond, Nanosecond, Second}; -use arrow::datatypes::{DataType, Field, FieldRef, TimeUnit}; +use arrow::datatypes::{ + ArrowTimestampType, DataType, Field, FieldRef, TimeUnit, TimestampMicrosecondType, + TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, +}; + +use datafusion_common::cast::as_primitive_array; use datafusion_common::types::{NativeType, logical_date}; +use super::adjust_to_local_time; use datafusion_common::{ Result, ScalarValue, cast::{ @@ -56,7 +63,7 @@ use datafusion_macros::user_doc; argument( name = "part", description = r#"Part of the date to return. The following date parts are supported: - + - year - quarter (emits value in inclusive range [1, 4] based on which quartile of the year the date is in) - month @@ -122,9 +129,9 @@ impl DatePartFunc { Coercion::new_exact(TypeSignatureClass::Duration), ]), ], - Volatility::Immutable, + Volatility::Stable, ), - aliases: vec![String::from("datepart")], + aliases: vec![String::from("datepart"), String::from("extract")], } } } @@ -173,6 +180,7 @@ impl ScalarUDFImpl for DatePartFunc { &self, args: datafusion_expr::ScalarFunctionArgs, ) -> Result { + let config = &args.config_options; let args = args.args; let [part, array] = take_function_args(self.name(), args)?; @@ -193,7 +201,72 @@ impl ScalarUDFImpl for DatePartFunc { ColumnarValue::Scalar(scalar) => scalar.to_array()?, }; + let (is_timezone_aware, tz_str_opt) = match array.data_type() { + Timestamp(_, Some(tz_str)) => (true, Some(Arc::clone(tz_str))), + _ => (false, None), + }; + let part_trim = part_normalization(&part); + let is_epoch = is_epoch(part_trim); + + // Epoch is timezone-independent - it always returns seconds since 1970-01-01 UTC + let array = if is_epoch { + array + } else if is_timezone_aware { + // For timezone-aware timestamps, extract in their own timezone + match tz_str_opt.as_ref() { + Some(tz_str) => { + let tz = interpret_session_timezone(tz_str)?; + match array.data_type() { + Timestamp(time_unit, _) => match time_unit { + Nanosecond => adjust_timestamp_array::< + TimestampNanosecondType, + >(&array, tz)?, + Microsecond => adjust_timestamp_array::< + TimestampMicrosecondType, + >(&array, tz)?, + Millisecond => adjust_timestamp_array::< + TimestampMillisecondType, + >(&array, tz)?, + Second => { + adjust_timestamp_array::(&array, tz)? + } + }, + _ => array, + } + } + None => array, + } + } else if let Timestamp(time_unit, None) = array.data_type() { + // For naive timestamps, interpret in session timezone if available + match config.execution.time_zone.as_ref() { + Some(tz_str) => { + let tz = interpret_session_timezone(tz_str)?; + + match time_unit { + Nanosecond => { + adjust_timestamp_array::(&array, tz)? + } + Microsecond => { + adjust_timestamp_array::( + &array, tz, + )? + } + Millisecond => { + adjust_timestamp_array::( + &array, tz, + )? + } + Second => { + adjust_timestamp_array::(&array, tz)? + } + } + } + None => array, + } + } else { + array + }; // using IntervalUnit here means we hand off all the work of supporting plurals (like "seconds") // and synonyms ( like "ms,msec,msecond,millisecond") to Arrow @@ -209,7 +282,6 @@ impl ScalarUDFImpl for DatePartFunc { IntervalUnit::Millisecond => seconds_as_i32(array.as_ref(), Millisecond)?, IntervalUnit::Microsecond => seconds_as_i32(array.as_ref(), Microsecond)?, IntervalUnit::Nanosecond => seconds_as_i32(array.as_ref(), Nanosecond)?, - // century and decade are not supported by `DatePart`, although they are supported in postgres _ => return exec_err!("Date part '{part}' not supported"), } } else { @@ -240,21 +312,43 @@ impl ScalarUDFImpl for DatePartFunc { } } +fn adjust_timestamp_array( + array: &ArrayRef, + tz: Tz, +) -> Result { + let mut builder = PrimitiveBuilder::::new(); + let primitive_array = as_primitive_array::(array)?; + for ts_opt in primitive_array.iter() { + match ts_opt { + None => builder.append_null(), + Some(ts) => { + let adjusted_ts = adjust_to_local_time::(ts, tz)?; + builder.append_value(adjusted_ts); + } + } + } + Ok(Arc::new(builder.finish())) +} + fn is_epoch(part: &str) -> bool { - let part = part_normalization(part); matches!(part.to_lowercase().as_str(), "epoch") } -// Try to remove quote if exist, if the quote is invalid, return original string and let the downstream function handle the error +// Try to remove quote if exist, if the quote is invalid, return original string +// and let the downstream function handle the error. fn part_normalization(part: &str) -> &str { part.strip_prefix(|c| c == '\'' || c == '\"') .and_then(|s| s.strip_suffix(|c| c == '\'' || c == '\"')) .unwrap_or(part) } -/// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the -/// result to a total number of seconds, milliseconds, microseconds or -/// nanoseconds +fn interpret_session_timezone(tz_str: &str) -> Result { + match tz_str.parse::() { + Ok(tz) => Ok(tz), + Err(err) => exec_err!("Invalid timezone '{tz_str}': {err}"), + } +} + fn seconds_as_i32(array: &dyn Array, unit: TimeUnit) -> Result { // Nanosecond is neither supported in Postgres nor DuckDB, to avoid dealing // with overflow and precision issue we don't support nanosecond @@ -277,7 +371,6 @@ fn seconds_as_i32(array: &dyn Array, unit: TimeUnit) -> Result { }; let secs = date_part(array, DatePart::Second)?; - // This assumes array is primitive and not a dictionary let secs = as_int32_array(secs.as_ref())?; let subsecs = date_part(array, DatePart::Nanosecond)?; let subsecs = as_int32_array(subsecs.as_ref())?; @@ -305,11 +398,8 @@ fn seconds_as_i32(array: &dyn Array, unit: TimeUnit) -> Result { } } -/// Invoke [`date_part`] on an `array` (e.g. Timestamp) and convert the -/// result to a total number of seconds, milliseconds, microseconds or -/// nanoseconds -/// -/// Given epoch return f64, this is a duplicated function to optimize for f64 type +// Converts seconds to f64 with the specified time unit. +// Used for Interval and Duration types that need floating-point precision. fn seconds(array: &dyn Array, unit: TimeUnit) -> Result { let sf = match unit { Second => 1_f64, @@ -318,7 +408,6 @@ fn seconds(array: &dyn Array, unit: TimeUnit) -> Result { Nanosecond => 1_000_000_000_f64, }; let secs = date_part(array, DatePart::Second)?; - // This assumes array is primitive and not a dictionary let secs = as_int32_array(secs.as_ref())?; let subsecs = date_part(array, DatePart::Nanosecond)?; let subsecs = as_int32_array(subsecs.as_ref())?; diff --git a/datafusion/functions/src/datetime/mod.rs b/datafusion/functions/src/datetime/mod.rs index 39b9453295df6..4f3e45d761c34 100644 --- a/datafusion/functions/src/datetime/mod.rs +++ b/datafusion/functions/src/datetime/mod.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use datafusion_expr::ScalarUDF; pub mod common; +pub use common::adjust_to_local_time; pub mod current_date; pub mod current_time; pub mod date_bin; diff --git a/datafusion/functions/src/datetime/to_local_time.rs b/datafusion/functions/src/datetime/to_local_time.rs index 86c949711d011..178714c78bd37 100644 --- a/datafusion/functions/src/datetime/to_local_time.rs +++ b/datafusion/functions/src/datetime/to_local_time.rs @@ -16,7 +16,6 @@ // under the License. use std::any::Any; -use std::ops::Add; use std::sync::Arc; use arrow::array::timezone::Tz; @@ -27,13 +26,10 @@ use arrow::datatypes::{ ArrowTimestampType, DataType, TimestampMicrosecondType, TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, }; -use chrono::{DateTime, MappedLocalTime, Offset, TimeDelta, TimeZone, Utc}; +use crate::datetime::adjust_to_local_time; use datafusion_common::cast::as_primitive_array; -use datafusion_common::{ - Result, ScalarValue, exec_err, internal_datafusion_err, internal_err, - utils::take_function_args, -}; +use datafusion_common::{Result, ScalarValue, internal_err, utils::take_function_args}; use datafusion_expr::{ Coercion, ColumnarValue, Documentation, ScalarUDFImpl, Signature, TypeSignatureClass, Volatility, @@ -324,60 +320,12 @@ fn to_local_time(time_value: &ColumnarValue) -> Result { /// ``` /// /// See `test_adjust_to_local_time()` for example -fn adjust_to_local_time(ts: i64, tz: Tz) -> Result { - fn convert_timestamp(ts: i64, converter: F) -> Result> - where - F: Fn(i64) -> MappedLocalTime>, - { - match converter(ts) { - MappedLocalTime::Ambiguous(earliest, latest) => exec_err!( - "Ambiguous timestamp. Do you mean {:?} or {:?}", - earliest, - latest - ), - MappedLocalTime::None => exec_err!( - "The local time does not exist because there is a gap in the local time." - ), - MappedLocalTime::Single(date_time) => Ok(date_time), - } - } - - let date_time = match T::UNIT { - Nanosecond => Utc.timestamp_nanos(ts), - Microsecond => convert_timestamp(ts, |ts| Utc.timestamp_micros(ts))?, - Millisecond => convert_timestamp(ts, |ts| Utc.timestamp_millis_opt(ts))?, - Second => convert_timestamp(ts, |ts| Utc.timestamp_opt(ts, 0))?, - }; - - let offset_seconds: i64 = tz - .offset_from_utc_datetime(&date_time.naive_utc()) - .fix() - .local_minus_utc() as i64; - - let adjusted_date_time = date_time.add( - // This should not fail under normal circumstances as the - // maximum possible offset is 26 hours (93,600 seconds) - TimeDelta::try_seconds(offset_seconds) - .ok_or_else(|| internal_datafusion_err!("Offset seconds should be less than i64::MAX / 1_000 or greater than -i64::MAX / 1_000"))?, - ); - - // convert the naive datetime back to i64 - match T::UNIT { - Nanosecond => adjusted_date_time.timestamp_nanos_opt().ok_or_else(|| - internal_datafusion_err!( - "Failed to convert DateTime to timestamp in nanosecond. This error may occur if the date is out of range. The supported date ranges are between 1677-09-21T00:12:43.145224192 and 2262-04-11T23:47:16.854775807" - ) - ), - Microsecond => Ok(adjusted_date_time.timestamp_micros()), - Millisecond => Ok(adjusted_date_time.timestamp_millis()), - Second => Ok(adjusted_date_time.timestamp()), - } -} - #[cfg(test)] mod tests { use std::sync::Arc; + use super::ToLocalTimeFunc; + use crate::datetime::adjust_to_local_time; use arrow::array::{Array, TimestampNanosecondArray, types::TimestampNanosecondType}; use arrow::compute::kernels::cast_utils::string_to_timestamp_nanos; use arrow::datatypes::{DataType, Field, TimeUnit}; @@ -386,8 +334,6 @@ mod tests { use datafusion_common::config::ConfigOptions; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl}; - use super::{ToLocalTimeFunc, adjust_to_local_time}; - #[test] fn test_adjust_to_local_time() { let timestamp_str = "2020-03-31T13:40:00"; diff --git a/datafusion/sqllogictest/test_files/extract_tz.slt b/datafusion/sqllogictest/test_files/extract_tz.slt new file mode 100644 index 0000000000000..e0dc37e6965d8 --- /dev/null +++ b/datafusion/sqllogictest/test_files/extract_tz.slt @@ -0,0 +1,175 @@ +# 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. + +# Tests for timezone-aware extract SQL statement support. +# Test with different timezone +statement ok +SET datafusion.execution.time_zone = '-03:00'; + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-11-18 10:00:00'); +---- +7 + +query II +SELECT EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 10:45:30'), + EXTRACT(SECOND FROM TIMESTAMP '2023-10-30 10:45:30'); +---- +45 30 + +query III +SELECT EXTRACT(YEAR FROM DATE '2023-10-30'), + EXTRACT(MONTH FROM DATE '2023-10-30'), + EXTRACT(DAY FROM DATE '2023-10-30'); +---- +2023 10 30 + +query I +SELECT EXTRACT(HOUR FROM CAST(NULL AS TIMESTAMP)); +---- +NULL + +statement ok +SET datafusion.execution.time_zone = '+04:00'; + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2023-10-30 02:00:00'); +---- +6 + +query III +SELECT EXTRACT(HOUR FROM TIMESTAMP '2023-10-30 18:20:59'), + EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 18:20:59'), + EXTRACT(SECOND FROM TIMESTAMP '2023-10-30 18:20:59'); +---- +22 20 59 + +query II +SELECT EXTRACT(DOW FROM DATE '2025-11-01'), + EXTRACT(DOY FROM DATE '2026-12-31'); +---- +6 365 + +statement ok +SET datafusion.execution.time_zone = '+00:00'; + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-10-30 10:45:30+02:00'); +---- +8 + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-10-30 10:45:30-05:00'); +---- +15 + +query II +SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-11-30 10:45:30Z'), + EXTRACT(MONTH FROM TIMESTAMP '2023-10-30 10:45:30Z'); +---- +2026 10 + +query III +SELECT EXTRACT(HOUR FROM TIMESTAMP '2023-10-30 18:20:59+04:00'), + EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 18:20:59+04:00'), + EXTRACT(SECOND FROM TIMESTAMP '2023-10-30 18:20:59+04:00'); +---- +14 20 59 + +query II +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-10-30 10:25:30+02:30'), + EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 18:20:59-04:30'); +---- +7 50 + +query III +SELECT EXTRACT(HOUR FROM TIMESTAMP '2023-10-30 18:20:59-08:00'), + EXTRACT(DAY FROM TIMESTAMP '2023-10-30 18:20:59-07:00'), + EXTRACT(DAY FROM TIMESTAMP '2023-10-30 07:20:59+12:00'); +---- +2 31 29 + +query IIIIII +SELECT EXTRACT(YEAR FROM TIMESTAMP '2023-12-31 18:20:59-08:45'), + EXTRACT(MONTH FROM TIMESTAMP '2023-12-31 18:20:59-08:45'), + EXTRACT(DAY FROM TIMESTAMP '2023-12-31 18:20:59-08:45'), + EXTRACT(HOUR FROM TIMESTAMP '2023-12-31 18:20:59-08:45'), + EXTRACT(MINUTE FROM TIMESTAMP '2023-12-31 18:20:59-08:45'), + EXTRACT(SECOND FROM TIMESTAMP '2023-12-31 18:20:59-08:45'); +---- +2024 1 1 3 5 59 + +query IIIIII +SELECT EXTRACT(YEAR FROM TIMESTAMP '2024-01-01 03:05:59+08:45'), + EXTRACT(MONTH FROM TIMESTAMP '2024-01-01 03:05:59+08:45'), + EXTRACT(DAY FROM TIMESTAMP '2024-01-01 03:05:59+08:45'), + EXTRACT(HOUR FROM TIMESTAMP '2024-01-01 03:05:59+08:45'), + EXTRACT(MINUTE FROM TIMESTAMP '2024-01-01 03:05:59+08:45'), + EXTRACT(SECOND FROM TIMESTAMP '2024-01-01 03:05:59+08:45'); +---- +2023 12 31 18 20 59 + +statement ok +SET datafusion.execution.time_zone = 'Asia/Kolkata'; + +query IIII +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-11-22 15:30:45'), +EXTRACT(MINUTE FROM TIMESTAMP '2025-11-22 15:30:45'), +EXTRACT(DOW FROM TIMESTAMP '2025-11-22 00:00:00'), +EXTRACT(SECOND FROM TIMESTAMP '2024-01-01 03:05:59'); +---- +21 0 6 59 + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-01-15 10:00:00'); +---- +15 + +statement ok +SET datafusion.execution.time_zone = 'America/New_York'; + +query IIII +SELECT +EXTRACT(HOUR FROM TIMESTAMP '2025-11-22 15:30:45'), +EXTRACT(MINUTE FROM TIMESTAMP '2025-11-22 15:30:45'), +EXTRACT(DOW FROM TIMESTAMP '2025-11-22 00:00:00'), +EXTRACT(SECOND FROM TIMESTAMP '2024-01-01 03:05:59'); +---- +10 30 5 59 + +query I +SELECT EXTRACT(HOUR FROM TIMESTAMP '2025-01-15 10:00:00'); +---- +5 + +statement ok +SET datafusion.execution.time_zone = '-03:30'; + +query II +SELECT EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 10:45:30'), +EXTRACT(SECOND FROM TIMESTAMP '2023-10-30 10:45:30'); +---- +15 30 + +statement ok +SET datafusion.execution.time_zone = 'America/St_Johns'; + +query II +SELECT EXTRACT(MINUTE FROM TIMESTAMP '2023-10-30 10:45:30'), +EXTRACT(SECOND FROM TIMESTAMP '2023-10-30 10:45:30'); +---- +15 30 diff --git a/docs/source/user-guide/sql/scalar_functions.md b/docs/source/user-guide/sql/scalar_functions.md index 4079802d9e630..744ef26c6dd7b 100644 --- a/docs/source/user-guide/sql/scalar_functions.md +++ b/docs/source/user-guide/sql/scalar_functions.md @@ -2387,6 +2387,7 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo - [date_trunc](#date_trunc) - [datepart](#datepart) - [datetrunc](#datetrunc) +- [extract](#extract) - [from_unixtime](#from_unixtime) - [make_date](#make_date) - [make_time](#make_time) @@ -2545,6 +2546,7 @@ extract(field FROM source) #### Aliases - datepart +- extract ### `date_trunc` @@ -2593,6 +2595,10 @@ _Alias of [date_part](#date_part)._ _Alias of [date_trunc](#date_trunc)._ +### `extract` + +_Alias of [date_part](#date_part)._ + ### `from_unixtime` Converts an integer to RFC3339 timestamp format (`YYYY-MM-DDT00:00:00.000000000Z`). Integers and unsigned integers are interpreted as seconds since the unix epoch (`1970-01-01T00:00:00Z`) return the corresponding timestamp.