Skip to content

Commit 37aad28

Browse files
authored
Feat: Make current_time aware of execution timezone. (#18040)
## Which issue does this PR close? - Closes #17996. ## Rationale for this change - The current_time() function currently uses UTC tz. This PR updates current_time() to use the tz set in 'datafusion.execution.time_zone' ## What changes are included in this PR? - current_time() returns a tz aware date via the 'datafusion.execution.time_zone' config option. ## Are these changes tested? - Tested with Datafusion CLI with slt covering popular scenarios added.
1 parent 54fff60 commit 37aad28

File tree

3 files changed

+218
-5
lines changed

3 files changed

+218
-5
lines changed

datafusion/functions/src/datetime/current_time.rs

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,28 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use arrow::array::timezone::Tz;
1819
use arrow::datatypes::DataType;
1920
use arrow::datatypes::DataType::Time64;
2021
use arrow::datatypes::TimeUnit::Nanosecond;
21-
use std::any::Any;
22-
22+
use chrono::TimeZone;
23+
use chrono::Timelike;
2324
use datafusion_common::{internal_err, Result, ScalarValue};
2425
use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyInfo};
2526
use datafusion_expr::{
2627
ColumnarValue, Documentation, Expr, ScalarUDFImpl, Signature, Volatility,
2728
};
2829
use datafusion_macros::user_doc;
30+
use std::any::Any;
2931

3032
#[user_doc(
3133
doc_section(label = "Time and Date Functions"),
3234
description = r#"
33-
Returns the current UTC time.
35+
Returns the current time in the session time zone.
3436
3537
The `current_time()` return value is determined at query time and will return the same time, no matter when in the query plan the function executes.
38+
39+
The session time zone can be set using the statement 'SET datafusion.execution.time_zone = desired time zone'. The time zone can be a value like +00:00, 'Europe/London' etc.
3640
"#,
3741
syntax_example = "current_time()"
3842
)]
@@ -93,7 +97,20 @@ impl ScalarUDFImpl for CurrentTimeFunc {
9397
info: &dyn SimplifyInfo,
9498
) -> Result<ExprSimplifyResult> {
9599
let now_ts = info.execution_props().query_execution_start_time;
96-
let nano = now_ts.timestamp_nanos_opt().map(|ts| ts % 86400000000000);
100+
101+
// Try to get timezone from config and convert to local time
102+
let nano = info
103+
.execution_props()
104+
.config_options()
105+
.and_then(|config| config.execution.time_zone.parse::<Tz>().ok())
106+
.map_or_else(
107+
|| datetime_to_time_nanos(&now_ts),
108+
|tz| {
109+
let local_now = tz.from_utc_datetime(&now_ts.naive_utc());
110+
datetime_to_time_nanos(&local_now)
111+
},
112+
);
113+
97114
Ok(ExprSimplifyResult::Simplified(Expr::Literal(
98115
ScalarValue::Time64Nanosecond(nano),
99116
None,
@@ -104,3 +121,97 @@ impl ScalarUDFImpl for CurrentTimeFunc {
104121
self.doc()
105122
}
106123
}
124+
125+
// Helper function for conversion of datetime to a timestamp.
126+
fn datetime_to_time_nanos<Tz: TimeZone>(dt: &chrono::DateTime<Tz>) -> Option<i64> {
127+
let hour = dt.hour() as i64;
128+
let minute = dt.minute() as i64;
129+
let second = dt.second() as i64;
130+
let nanosecond = dt.nanosecond() as i64;
131+
Some((hour * 3600 + minute * 60 + second) * 1_000_000_000 + nanosecond)
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
use arrow::datatypes::{DataType, TimeUnit::Nanosecond};
138+
use chrono::{DateTime, Utc};
139+
use datafusion_common::{Result, ScalarValue};
140+
use datafusion_expr::execution_props::ExecutionProps;
141+
use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyInfo};
142+
use std::sync::Arc;
143+
144+
struct MockSimplifyInfo {
145+
execution_props: ExecutionProps,
146+
}
147+
148+
impl SimplifyInfo for MockSimplifyInfo {
149+
fn is_boolean_type(&self, _expr: &Expr) -> Result<bool> {
150+
Ok(false)
151+
}
152+
153+
fn nullable(&self, _expr: &Expr) -> Result<bool> {
154+
Ok(true)
155+
}
156+
157+
fn execution_props(&self) -> &ExecutionProps {
158+
&self.execution_props
159+
}
160+
161+
fn get_data_type(&self, _expr: &Expr) -> Result<DataType> {
162+
Ok(Time64(Nanosecond))
163+
}
164+
}
165+
166+
fn set_session_timezone_env(tz: &str, start_time: DateTime<Utc>) -> MockSimplifyInfo {
167+
let mut config = datafusion_common::config::ConfigOptions::default();
168+
config.execution.time_zone = tz.to_string();
169+
let mut execution_props =
170+
ExecutionProps::new().with_query_execution_start_time(start_time);
171+
execution_props.config_options = Some(Arc::new(config));
172+
MockSimplifyInfo { execution_props }
173+
}
174+
175+
#[test]
176+
fn test_current_time_timezone_offset() {
177+
// Use a fixed start time for consistent testing
178+
let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
179+
180+
// Test with UTC+05:00
181+
let info_plus_5 = set_session_timezone_env("+05:00", start_time);
182+
let result_plus_5 = CurrentTimeFunc::new()
183+
.simplify(vec![], &info_plus_5)
184+
.unwrap();
185+
186+
// Test with UTC-05:00
187+
let info_minus_5 = set_session_timezone_env("-05:00", start_time);
188+
let result_minus_5 = CurrentTimeFunc::new()
189+
.simplify(vec![], &info_minus_5)
190+
.unwrap();
191+
192+
// Extract nanoseconds from results
193+
let nanos_plus_5 = match result_plus_5 {
194+
ExprSimplifyResult::Simplified(Expr::Literal(
195+
ScalarValue::Time64Nanosecond(Some(n)),
196+
_,
197+
)) => n,
198+
_ => panic!("Expected Time64Nanosecond literal"),
199+
};
200+
201+
let nanos_minus_5 = match result_minus_5 {
202+
ExprSimplifyResult::Simplified(Expr::Literal(
203+
ScalarValue::Time64Nanosecond(Some(n)),
204+
_,
205+
)) => n,
206+
_ => panic!("Expected Time64Nanosecond literal"),
207+
};
208+
209+
// Calculate the difference: UTC+05:00 should be 10 hours ahead of UTC-05:00
210+
let difference = nanos_plus_5 - nanos_minus_5;
211+
212+
// 10 hours in nanoseconds
213+
let expected_offset = 10i64 * 3600 * 1_000_000_000;
214+
215+
assert_eq!(difference, expected_offset, "Expected 10-hour offset difference in nanoseconds between UTC+05:00 and UTC-05:00");
216+
}
217+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
##########
19+
## current_time with timezone tests
20+
##########
21+
22+
# Test 1: Verify current_time is consistent within the same query (default UTC)
23+
query B
24+
SELECT current_time() = current_time();
25+
----
26+
true
27+
28+
# Test 2: Verify data type is correct
29+
query T
30+
SELECT arrow_typeof(current_time());
31+
----
32+
Time64(Nanosecond)
33+
34+
# Test 3: Set timezone to +08:00 and verify current_time is still stable
35+
statement ok
36+
SET datafusion.execution.time_zone = '+08:00';
37+
38+
query B
39+
SELECT current_time() = current_time();
40+
----
41+
true
42+
43+
# Test 4: Verify current_time returns Time64 type in different timezone
44+
query T
45+
SELECT arrow_typeof(current_time());
46+
----
47+
Time64(Nanosecond)
48+
49+
# Test 5: Test with negative offset timezone
50+
statement ok
51+
SET datafusion.execution.time_zone = '-05:00';
52+
53+
query B
54+
SELECT current_time() = current_time();
55+
----
56+
true
57+
58+
# Test 6: Test with named timezone (America/New_York)
59+
statement ok
60+
SET datafusion.execution.time_zone = 'America/New_York';
61+
62+
query B
63+
SELECT current_time() = current_time();
64+
----
65+
true
66+
67+
# Test 7: Verify current_time is stable within a query
68+
query B
69+
SELECT
70+
current_time() = current_time() AND
71+
current_time() = current_time();
72+
----
73+
true
74+
75+
# Test 8: Reset to UTC
76+
statement ok
77+
SET datafusion.execution.time_zone = '+00:00';
78+
79+
query B
80+
SELECT current_time() = current_time();
81+
----
82+
true
83+
84+
# Test 9: Verify current_time with Asia/Tokyo timezone
85+
statement ok
86+
SET datafusion.execution.time_zone = 'Asia/Tokyo';
87+
88+
query B
89+
SELECT current_time() = current_time();
90+
----
91+
true
92+
93+
# Test 10: Verify current_time with Europe/London timezone
94+
statement ok
95+
SET datafusion.execution.time_zone = 'Europe/London';
96+
97+
query B
98+
SELECT current_time() = current_time();
99+
----
100+
true

docs/source/user-guide/sql/scalar_functions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2417,10 +2417,12 @@ current_date()
24172417

24182418
### `current_time`
24192419

2420-
Returns the current UTC time.
2420+
Returns the current time in the session time zone.
24212421

24222422
The `current_time()` return value is determined at query time and will return the same time, no matter when in the query plan the function executes.
24232423

2424+
The session time zone can be set using the statement 'SET datafusion.execution.time_zone = desired time zone'. The time zone can be a value like +00:00, 'Europe/London' etc.
2425+
24242426
```sql
24252427
current_time()
24262428
```

0 commit comments

Comments
 (0)