diff --git a/src/query/functions/src/scalars/datetime.rs b/src/query/functions/src/scalars/datetime.rs index b8755f33c10e8..ca3fe2d84d554 100644 --- a/src/query/functions/src/scalars/datetime.rs +++ b/src/query/functions/src/scalars/datetime.rs @@ -110,6 +110,9 @@ pub fn register(registry: &mut FunctionRegistry) { // [date | timestamp] +/- number register_timestamp_add_sub(registry); + + // convert_timezone( target_timezone, 'timestamp') + register_convert_timezone(registry); } /// Check if timestamp is within range, and return the timestamp in micros. @@ -134,6 +137,59 @@ fn int64_domain_to_timestamp_domain>( }) } +fn register_convert_timezone(registry: &mut FunctionRegistry) { + // 2 arguments function [target_timezone, src_timestamp] + registry.register_passthrough_nullable_2_arg::( + "convert_timezone", + |_, _, _| FunctionDomain::MayThrow, + vectorize_with_builder_2_arg::( + |target_tz, src_timestamp, output, ctx| { + if let Some(validity) = &ctx.validity { + if !validity.get_bit(output.len()) { + output.push(0); + return; + } + } + // Convert source timestamp from source timezone to target timezone + let p_src_timestamp = src_timestamp.to_timestamp(ctx.func_ctx.tz.tz); + let src_dst_from_utc = p_src_timestamp.offset().fix().local_minus_utc(); + let t_tz: Tz = match target_tz.parse() { + Ok(tz) => tz, + Err(e) => { + ctx.set_error( + output.len(), + format!("cannot parse target `timezone`. {}", e), + ); + output.push(0); + return; + } + }; + + let result_timestamp = p_src_timestamp.with_timezone(&t_tz).timestamp_micros(); + let target_dst_from_utc = p_src_timestamp + .with_timezone(&t_tz) + .offset() + .fix() + .local_minus_utc(); + let offset_as_micros_sec = (target_dst_from_utc - src_dst_from_utc) as i64; + match offset_as_micros_sec.checked_mul(MICROS_PER_SEC) { + Some(offset) => match result_timestamp.checked_add(offset) { + Some(res) => output.push(res), + None => { + ctx.set_error(output.len(), "calc final time error".to_string()); + output.push(0); + } + }, + None => { + ctx.set_error(output.len(), "calc time offset error".to_string()); + output.push(0); + } + } + }, + ), + ); +} + fn register_string_to_timestamp(registry: &mut FunctionRegistry) { registry.register_aliases("to_date", &["str_to_date", "date"]); registry.register_aliases("to_year", &["str_to_year", "year"]); diff --git a/src/query/functions/tests/it/scalars/testdata/function_list.txt b/src/query/functions/tests/it/scalars/testdata/function_list.txt index 7d4cf1b16a970..4629201713f4e 100644 --- a/src/query/functions/tests/it/scalars/testdata/function_list.txt +++ b/src/query/functions/tests/it/scalars/testdata/function_list.txt @@ -1224,6 +1224,8 @@ Functions overloads: 26 contains(Array(Boolean), Boolean) :: Boolean 27 contains(Array(Boolean) NULL, Boolean NULL) :: Boolean NULL 28 contains(Array(T0) NULL, T0) :: Boolean +0 convert_timezone(String, Timestamp) :: Timestamp +1 convert_timezone(String NULL, Timestamp NULL) :: Timestamp NULL 0 cos(Float64) :: Float64 1 cos(Float64 NULL) :: Float64 NULL 0 cosine_distance(Array(Float32), Array(Float32)) :: Float32 diff --git a/tests/sqllogictests/suites/query/functions/02_0012_function_datetimes_tz.test b/tests/sqllogictests/suites/query/functions/02_0012_function_datetimes_tz.test index f0e6d27187833..57b4984d6740a 100644 --- a/tests/sqllogictests/suites/query/functions/02_0012_function_datetimes_tz.test +++ b/tests/sqllogictests/suites/query/functions/02_0012_function_datetimes_tz.test @@ -658,7 +658,6 @@ select to_datetime('1', '%s') statement error 1006 select to_timestamp('200,2000', '%s,%Y'); - statement ok set timezone='UTC'; @@ -769,3 +768,59 @@ query T SELECT substr(DATE_ADD(month, 1, now())::String, 1,4)=substr(now()::String, 1,4); ---- 1 + + +statement ok +set timezone='Asia/Shanghai'; + +query T +SELECT convert_timezone('America/Los_Angeles', '2024-11-01 11:36:10') +---- +2024-10-31 20:36:10.000000 + +statement ok +set timezone='UTC'; + +statement ok +create or replace table t(a string null, c timestamp null); + +statement ok +insert into t values('America/Los_Angeles','1970-01-01 00:00:00'), ('America/Los_Angeles','2024-10-31 22:21:15'), (null, '1970-01-01 00:00:00'), ('Asia/Shanghai', '1970-01-01 00:00:00'), ('Asia/Shanghai', '2024-10-31 22:21:15'),('Asia/Shanghai', null), (null, null); + +# UTC to America/Los_Angeles, 20240-03-10 ~ 2024-11-03 is UTC-7(dst), other is UTC-8 +query T +select a, c, CONVERT_TIMEZONE(a, c) from t order by a,c; +---- +America/Los_Angeles 1970-01-01 00:00:00.000000 1969-12-31 16:00:00.000000 +America/Los_Angeles 2024-10-31 22:21:15.000000 2024-10-31 15:21:15.000000 +Asia/Shanghai 1970-01-01 00:00:00.000000 1970-01-01 08:00:00.000000 +Asia/Shanghai 2024-10-31 22:21:15.000000 2024-11-01 06:21:15.000000 +Asia/Shanghai NULL NULL +NULL 1970-01-01 00:00:00.000000 NULL +NULL NULL NULL + +statement ok +set timezone='Asia/Shanghai'; + + +statement error 1006 +select convert_timezone('Asia/Shanghai', '1947-04-15 00:00:00'); + +statement ok +set enable_dst_hour_fix=1; + +# 1947-04-15 00:00:00 is not exists in Asia/Shanghai. Such timings cannot be guaranteed to meet completely +# consider use date_add/sub calc the offset. +query T +select convert_timezone('UTC', '1947-04-15 00:00:00'); +---- +1947-04-14 15:00:00.000000 + +statement ok +unset enable_dst_hour_fix; + +statement ok +drop table if exists t; + +statement ok +unset timezone;