1use chrono::{LocalResult, Utc};
16use chrono_tz::Tz;
17use num_traits::CheckedNeg;
18use risingwave_common::types::{
19 CheckedAdd, F64, Interval, IntoOrdered, Timestamp, Timestamptz, write_date_time_tz,
20};
21use risingwave_expr::{ExprError, Result, function};
22use thiserror_ext::AsReport;
23
24#[inline(always)]
26pub fn time_zone_err(inner_err: String) -> ExprError {
27 ExprError::InvalidParam {
28 name: "time_zone",
29 reason: inner_err.into(),
30 }
31}
32
33#[function("clock_timestamp() -> timestamptz", volatile)]
38fn clock_timestamp() -> Timestamptz {
39 Utc::now().into()
40}
41
42#[function("sec_to_timestamptz(float8) -> timestamptz")]
43pub fn f64_sec_to_timestamptz(elem: F64) -> Result<Timestamptz> {
44 let micros = (elem.0 * 1e6)
46 .into_ordered()
47 .try_into()
48 .map_err(|_| ExprError::NumericOutOfRange)?;
49 Timestamptz::from_micros(micros).ok_or(ExprError::NumericOutOfRange)
50}
51
52#[function("at_time_zone(timestamptz, varchar) -> timestamp")]
53pub fn timestamptz_at_time_zone(input: Timestamptz, time_zone: &str) -> Result<Timestamp> {
54 let time_zone = Timestamptz::lookup_time_zone(time_zone).map_err(time_zone_err)?;
55 Ok(timestamptz_at_time_zone_internal(input, time_zone))
56}
57
58pub fn timestamptz_at_time_zone_internal(input: Timestamptz, time_zone: Tz) -> Timestamp {
59 let instant_local = input.to_datetime_in_zone(time_zone);
60 let naive = instant_local.naive_local();
61 Timestamp(naive)
62}
63
64#[function("at_time_zone(timestamp, varchar) -> timestamptz")]
65pub fn timestamp_at_time_zone(input: Timestamp, time_zone: &str) -> Result<Timestamptz> {
66 let time_zone = Timestamptz::lookup_time_zone(time_zone).map_err(time_zone_err)?;
67 timestamp_at_time_zone_internal(input, time_zone)
68}
69
70pub fn timestamp_at_time_zone_internal(input: Timestamp, time_zone: Tz) -> Result<Timestamptz> {
71 let instant_local = match input.0.and_local_timezone(time_zone) {
73 LocalResult::Single(t) => t,
74 LocalResult::None => {
79 (input.0 - chrono::Duration::hours(3))
80 .and_local_timezone(time_zone)
81 .single()
82 .ok_or_else(|| ExprError::InvalidParam {
83 name: "local timestamp",
84 reason: format!(
85 "fail to interpret local timestamp \"{}\" in time zone \"{}\"",
86 input, time_zone
87 )
88 .into(),
89 })?
90 + chrono::Duration::hours(3)
91 }
92 LocalResult::Ambiguous(_, latest) => latest,
94 };
95 let usec = instant_local.timestamp_micros();
96 Ok(Timestamptz::from_micros_uncheck(usec))
97}
98
99#[function("cast_with_time_zone(timestamptz, varchar) -> varchar")]
100pub fn timestamptz_to_string(
101 elem: Timestamptz,
102 time_zone: &str,
103 writer: &mut impl std::fmt::Write,
104) -> Result<()> {
105 let time_zone = Timestamptz::lookup_time_zone(time_zone).map_err(time_zone_err)?;
106 let instant_local = elem.to_datetime_in_zone(time_zone);
107 write_date_time_tz(instant_local, writer).map_err(|e| ExprError::Internal(e.into()))
108}
109
110#[function("cast_with_time_zone(varchar, varchar) -> timestamptz")]
113pub fn str_to_timestamptz(elem: &str, time_zone: &str) -> Result<Timestamptz> {
114 elem.parse().or_else(|_| {
115 timestamp_at_time_zone(
116 elem.parse::<Timestamp>()
117 .map_err(|err| ExprError::Parse(err.to_report_string().into()))?,
118 time_zone,
119 )
120 })
121}
122
123#[function("subtract(timestamptz, timestamptz) -> interval")]
125pub fn timestamptz_timestamptz_sub(l: Timestamptz, r: Timestamptz) -> Result<Interval> {
126 let usecs = l
127 .timestamp_micros()
128 .checked_sub(r.timestamp_micros())
129 .ok_or(ExprError::NumericOverflow)?;
130 let interval = Interval::from_month_day_usec(0, 0, usecs);
131 let interval = interval.justify_hour().ok_or(ExprError::NumericOverflow)?;
133 Ok(interval)
134}
135
136#[function("subtract_with_time_zone(timestamptz, interval, varchar) -> timestamptz")]
137pub fn timestamptz_interval_sub(
138 input: Timestamptz,
139 interval: Interval,
140 time_zone: &str,
141) -> Result<Timestamptz> {
142 timestamptz_interval_add(
143 input,
144 interval.checked_neg().ok_or(ExprError::NumericOverflow)?,
145 time_zone,
146 )
147}
148
149#[function("add_with_time_zone(timestamptz, interval, varchar) -> timestamptz")]
150pub fn timestamptz_interval_add(
151 input: Timestamptz,
152 interval: Interval,
153 time_zone: &str,
154) -> Result<Timestamptz> {
155 let time_zone = Timestamptz::lookup_time_zone(time_zone).map_err(time_zone_err)?;
156 timestamptz_interval_add_internal(input, interval, time_zone)
157}
158
159pub fn timestamptz_interval_add_internal(
160 input: Timestamptz,
161 interval: Interval,
162 time_zone: Tz,
163) -> Result<Timestamptz> {
164 use num_traits::Zero as _;
165
166 let qualitative = interval.truncate_day();
169 let quantitative = interval - qualitative;
171
172 let mut t = input;
173 if !qualitative.is_zero() {
174 let naive = timestamptz_at_time_zone_internal(t, time_zone);
177 let naive = naive
178 .checked_add(qualitative)
179 .ok_or(ExprError::NumericOverflow)?;
180 t = timestamp_at_time_zone_internal(naive, time_zone)?;
181 }
182 let t = timestamptz_interval_quantitative(t, quantitative, i64::checked_add)?;
183 Ok(t)
184}
185
186#[function("add(timestamptz, interval) -> timestamptz")]
189pub fn timestamptz_interval_add_legacy(l: Timestamptz, r: Interval) -> Result<Timestamptz> {
190 timestamptz_interval_quantitative(l, r, i64::checked_add)
191}
192
193#[function("subtract(timestamptz, interval) -> timestamptz")]
194pub fn timestamptz_interval_sub_legacy(l: Timestamptz, r: Interval) -> Result<Timestamptz> {
195 timestamptz_interval_quantitative(l, r, i64::checked_sub)
196}
197
198#[function("add(interval, timestamptz) -> timestamptz")]
199pub fn interval_timestamptz_add_legacy(l: Interval, r: Timestamptz) -> Result<Timestamptz> {
200 timestamptz_interval_add_legacy(r, l)
201}
202
203#[inline(always)]
204fn timestamptz_interval_quantitative(
205 l: Timestamptz,
206 r: Interval,
207 f: fn(i64, i64) -> Option<i64>,
208) -> Result<Timestamptz> {
209 if r.months() != 0 || r.days() != 0 {
211 return Err(ExprError::UnsupportedFunction(
212 "timestamp with time zone +/- interval of days".into(),
213 ));
214 }
215 let delta_usecs = r.usecs();
216 let usecs = f(l.timestamp_micros(), delta_usecs).ok_or(ExprError::NumericOutOfRange)?;
217 Timestamptz::from_micros(usecs).ok_or(ExprError::NumericOutOfRange)
218}
219
220#[cfg(test)]
221mod tests {
222 use risingwave_common::util::iter_util::ZipEqFast;
223
224 use super::*;
225
226 #[test]
227 fn test_time_zone_conversion() {
228 let zones = ["US/Pacific", "ASIA/SINGAPORE", "europe/zurich"];
229 #[rustfmt::skip]
230 let test_cases = [
231 ["2022-01-01 00:00:00Z", "2021-12-31 16:00:00", "2022-01-01 08:00:00", "2022-01-01 01:00:00"],
233 ["2022-07-01 00:00:00Z", "2022-06-30 17:00:00", "2022-07-01 08:00:00", "2022-07-01 02:00:00"],
235 ["2022-03-13 09:59:00Z", "2022-03-13 01:59:00", "2022-03-13 17:59:00", "2022-03-13 10:59:00"],
237 ["2022-03-13 10:00:00Z", "2022-03-13 03:00:00", "2022-03-13 18:00:00", "2022-03-13 11:00:00"],
238 ["2022-03-27 00:59:00Z", "2022-03-26 17:59:00", "2022-03-27 08:59:00", "2022-03-27 01:59:00"],
240 ["2022-03-27 01:00:00Z", "2022-03-26 18:00:00", "2022-03-27 09:00:00", "2022-03-27 03:00:00"],
241 ["2022-10-29 23:59:00Z", "2022-10-29 16:59:00", "2022-10-30 07:59:00", "2022-10-30 01:59:00"],
243 ["2022-10-30 02:00:00Z", "2022-10-29 19:00:00", "2022-10-30 10:00:00", "2022-10-30 03:00:00"],
244 ["2022-11-06 07:59:00Z", "2022-11-06 00:59:00", "2022-11-06 15:59:00", "2022-11-06 08:59:00"],
246 ["2022-11-06 10:00:00Z", "2022-11-06 02:00:00", "2022-11-06 18:00:00", "2022-11-06 11:00:00"],
247 ];
248 for case in test_cases {
249 let usecs = str_to_timestamptz(case[0], "UTC").unwrap();
250 case.iter()
251 .skip(1)
252 .zip_eq_fast(zones)
253 .for_each(|(local, zone)| {
254 let local = local.parse().unwrap();
255
256 let actual = timestamptz_at_time_zone(usecs, zone).unwrap();
257 assert_eq!(local, actual);
258
259 let actual = timestamp_at_time_zone(local, zone).unwrap();
260 assert_eq!(usecs, actual);
261 });
262 }
263 }
264
265 #[test]
266 #[rustfmt::skip]
267 fn test_time_zone_conversion_daylight_forward() {
268 test("2022-03-13 02:00:00", "US/Pacific", "2022-03-13 10:00:00+00:00");
270 test("2022-03-13 03:00:00", "US/Pacific", "2022-03-13 10:00:00+00:00");
271 test("2022-03-27 02:00:00", "europe/zurich", "2022-03-27 01:00:00+00:00");
273 test("2022-03-27 03:00:00", "europe/zurich", "2022-03-27 01:00:00+00:00");
274 test("2023-10-01 02:00:00", "Australia/Lord_Howe", "2023-09-30 15:30:00+00:00");
276 test("2023-10-01 02:30:00", "Australia/Lord_Howe", "2023-09-30 15:30:00+00:00");
277 #[track_caller]
284 fn test(local: &str, zone: &str, instant: &str) {
285 let actual = timestamp_at_time_zone(local.parse().unwrap(), zone).unwrap().to_string();
286 assert_eq!(actual, instant);
287 }
288 }
289
290 #[test]
291 fn test_time_zone_conversion_daylight_backward() {
292 #[rustfmt::skip]
293 let test_cases = [
294 ("2022-10-30 00:00:00Z", "2022-10-30 02:00:00", "europe/zurich", false),
295 ("2022-10-30 00:59:00Z", "2022-10-30 02:59:00", "europe/zurich", false),
296 ("2022-10-30 01:00:00Z", "2022-10-30 02:00:00", "europe/zurich", true),
297 ("2022-10-30 01:59:00Z", "2022-10-30 02:59:00", "europe/zurich", true),
298 ("2022-11-06 08:00:00Z", "2022-11-06 01:00:00", "US/Pacific", false),
299 ("2022-11-06 08:59:00Z", "2022-11-06 01:59:00", "US/Pacific", false),
300 ("2022-11-06 09:00:00Z", "2022-11-06 01:00:00", "US/Pacific", true),
301 ("2022-11-06 09:59:00Z", "2022-11-06 01:59:00", "US/Pacific", true),
302 ];
303 for (instant, local, zone, preferred) in test_cases {
304 let usecs = str_to_timestamptz(instant, "UTC").unwrap();
305 let local = local.parse().unwrap();
306
307 let actual = timestamptz_at_time_zone(usecs, zone).unwrap();
308 assert_eq!(local, actual);
309
310 if preferred {
311 let actual = timestamp_at_time_zone(local, zone).unwrap();
312 assert_eq!(usecs, actual)
313 }
314 }
315 }
316
317 #[test]
318 fn test_timestamptz_to_and_from_string() {
319 let str1 = "0001-11-15 15:35:40.999999+08:00";
320 let timestamptz1 = str_to_timestamptz(str1, "UTC").unwrap();
321 assert_eq!(timestamptz1.timestamp_micros(), -62108094259000001);
322
323 let mut writer = String::new();
324 timestamptz_to_string(timestamptz1, "UTC", &mut writer).unwrap();
325 assert_eq!(writer, "0001-11-15 07:35:40.999999+00:00");
326
327 let str2 = "1969-12-31 23:59:59.999999+00:00";
328 let timestamptz2 = str_to_timestamptz(str2, "UTC").unwrap();
329 assert_eq!(timestamptz2.timestamp_micros(), -1);
330
331 let mut writer = String::new();
332 timestamptz_to_string(timestamptz2, "UTC", &mut writer).unwrap();
333 assert_eq!(writer, str2);
334
335 let str3 = "2022-01-01 00:00:00+08:00";
337 let timestamptz3 = str_to_timestamptz(str3, "UTC").unwrap();
338
339 let timestamp_from_no_tz =
340 str_to_timestamptz("2022-01-01 00:00:00", "Asia/Singapore").unwrap();
341 assert_eq!(timestamptz3, timestamp_from_no_tz);
342 }
343}