Skip to main content

uqa_sql/expr/
current_time.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL current date/time values read from the owning execution context, and the transaction clock the thread's statement runs under.
8
9use std::cell::Cell;
10use std::marker::PhantomData;
11use std::rc::Rc;
12
13use super::{age_between, coerce_temporal, EvalContext, Result, SQLError, TemporalValue, Value};
14
15thread_local! {
16    /// The start of the transaction whose statement the current thread executes, in Unix microseconds, while a [`TransactionClockScope`] is entered.
17    static TRANSACTION_CLOCK: Cell<Option<i64>> = const { Cell::new(None) };
18}
19
20/// Read the platform wall clock as Unix microseconds.
21#[must_use]
22pub fn clock_timestamp_micros() -> i64 {
23    chrono::Utc::now().timestamp_micros()
24}
25
26/// The transaction start entered for the current thread, or `None` outside any statement.
27#[must_use]
28pub fn transaction_clock_micros() -> Option<i64> {
29    TRANSACTION_CLOCK.get()
30}
31
32/// The transaction start the special date and time inputs `now`, `today`, `tomorrow` and `yesterday` resolve against, as `GetCurrentTransactionStartTimestamp` supplies it to the input functions; an evaluation outside any statement reads the wall clock.
33#[must_use]
34pub(crate) fn transaction_timestamp_or_clock() -> i64 {
35    transaction_clock_micros().unwrap_or_else(clock_timestamp_micros)
36}
37
38/// Enters a transaction start for the current thread until it drops, restoring the clock entered before it. The engine enters its transaction timestamp at every statement boundary, and the parallel executor enters the dispatching thread's clock on each worker.
39pub struct TransactionClockScope {
40    previous: Option<i64>,
41    _thread: PhantomData<Rc<()>>,
42}
43
44impl TransactionClockScope {
45    #[must_use]
46    pub fn enter(micros: i64) -> Self {
47        Self {
48            previous: TRANSACTION_CLOCK.replace(Some(micros)),
49            _thread: PhantomData,
50        }
51    }
52}
53
54impl Drop for TransactionClockScope {
55    fn drop(&mut self) {
56        TRANSACTION_CLOCK.set(self.previous);
57    }
58}
59
60pub(super) fn eval_current_time(
61    name: &str,
62    args: &[Value],
63    context: Option<&EvalContext<'_>>,
64) -> Option<Result<Value>> {
65    if !(matches!(
66        name,
67        "now"
68            | "transaction_timestamp"
69            | "statement_timestamp"
70            | "current_timestamp"
71            | "current_date"
72            | "current_time"
73            | "localtime"
74            | "localtimestamp"
75    ) || name == "age" && args.len() == 1)
76    {
77        return None;
78    }
79    Some((|| {
80        if name == "age" && matches!(args, [Value::Null]) {
81            return Ok(Value::Null);
82        }
83        if name != "age" && !args.is_empty() {
84            return Err(SQLError::BadArity {
85                name: name.into(),
86                expected: "0".into(),
87                actual: args.len(),
88            });
89        }
90        let statement = name == "statement_timestamp";
91        let micros = context
92            .and_then(|context| context.engine)
93            .and_then(|engine| {
94                if statement {
95                    engine.statement_timestamp_micros()
96                } else {
97                    engine.transaction_timestamp_micros()
98                }
99            })
100            .or_else(|| (!statement).then(transaction_clock_micros).flatten())
101            .unwrap_or_else(clock_timestamp_micros);
102        const MICROS_PER_DAY: i64 = 86_400_000_000;
103        let value = match name {
104            "current_date" => TemporalValue::Date {
105                days: i32::try_from(micros.div_euclid(MICROS_PER_DAY)).map_err(|_| {
106                    SQLError::Internal("current date exceeds its day carrier".into())
107                })?,
108            },
109            "current_time" => TemporalValue::TimeTz {
110                micros: micros.rem_euclid(MICROS_PER_DAY),
111                offset_minutes: 0,
112            },
113            "localtime" => TemporalValue::Time {
114                micros: micros.rem_euclid(MICROS_PER_DAY),
115            },
116            "localtimestamp" => TemporalValue::Timestamp { micros },
117            "age" => {
118                return age_between(
119                    &TemporalValue::Timestamp {
120                        micros: micros.div_euclid(MICROS_PER_DAY) * MICROS_PER_DAY,
121                    },
122                    &coerce_temporal(&args[0])?,
123                );
124            }
125            _ => TemporalValue::TimestampTz { micros },
126        };
127        Ok(Value::Temporal(value))
128    })())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn temporal_coercion_uses_the_transaction_clock() {
137        let _clock = TransactionClockScope::enter(90_123_456_789);
138        for (input, micros) in [
139            ("now", 90_123_456_789),
140            ("today", 86_400_000_000),
141            ("tomorrow", 172_800_000_000),
142            ("yesterday", 0),
143        ] {
144            assert_eq!(
145                coerce_temporal(&Value::Str(input.into())).unwrap(),
146                TemporalValue::Timestamp { micros },
147                "{input}"
148            );
149        }
150    }
151
152    #[test]
153    fn temporal_comparisons_use_the_transaction_clock() {
154        use crate::expr::{compare_with_control, values_equal_with_control};
155        use uqa_core::memory::ProductionControl;
156        let _clock = TransactionClockScope::enter(90_123_456_789);
157        let control = ProductionControl::uncontrolled();
158        let now = Value::Str("now".into());
159        for temporal in [
160            TemporalValue::Date { days: 1 },
161            TemporalValue::Time {
162                micros: 3_723_456_789,
163            },
164            TemporalValue::TimeTz {
165                micros: 3_723_456_789,
166                offset_minutes: 0,
167            },
168            TemporalValue::Timestamp {
169                micros: 90_123_456_789,
170            },
171            TemporalValue::TimestampTz {
172                micros: 90_123_456_789,
173            },
174        ] {
175            let value = Value::Temporal(temporal);
176            for (left, right) in [(&value, &now), (&now, &value)] {
177                assert!(values_equal_with_control(left, right, &control).unwrap());
178                assert_eq!(
179                    compare_with_control(left, right, &control).unwrap(),
180                    std::cmp::Ordering::Equal
181                );
182            }
183        }
184    }
185
186    #[test]
187    fn temporal_range_bounds_use_the_transaction_clock() {
188        use crate::ast::RangeSubtype;
189        use crate::expr::parse_range;
190        let _clock = TransactionClockScope::enter(90_123_456_789);
191        for (kind, expected) in [
192            (
193                RangeSubtype::Timestamp,
194                TemporalValue::Timestamp {
195                    micros: 90_123_456_789,
196                },
197            ),
198            (
199                RangeSubtype::TimestampTz,
200                TemporalValue::TimestampTz {
201                    micros: 90_123_456_789,
202                },
203            ),
204        ] {
205            let range = parse_range("[now,now]", kind).unwrap();
206            assert_eq!(range.lower(), Some(&Value::Temporal(expected.clone())));
207            assert_eq!(range.upper(), Some(&Value::Temporal(expected)));
208            assert!(!range.is_empty());
209        }
210        let range = parse_range("[today,tomorrow)", RangeSubtype::Date).unwrap();
211        assert_eq!(
212            range.lower(),
213            Some(&Value::Temporal(TemporalValue::Date { days: 1 }))
214        );
215        assert_eq!(
216            range.upper(),
217            Some(&Value::Temporal(TemporalValue::Date { days: 2 }))
218        );
219    }
220
221    #[test]
222    fn transaction_clock_scopes_nest_and_restore() {
223        assert_eq!(transaction_clock_micros(), None);
224        {
225            let _outer = TransactionClockScope::enter(10);
226            assert_eq!(transaction_clock_micros(), Some(10));
227            assert_eq!(transaction_timestamp_or_clock(), 10);
228            {
229                let _inner = TransactionClockScope::enter(20);
230                assert_eq!(transaction_clock_micros(), Some(20));
231            }
232            assert_eq!(transaction_clock_micros(), Some(10));
233        }
234        assert_eq!(transaction_clock_micros(), None);
235        assert!(transaction_timestamp_or_clock() > 1_700_000_000_000_000);
236    }
237}