questdb/ingress/timestamp.rs
1/*******************************************************************************
2 * ___ _ ____ ____
3 * / _ \ _ _ ___ ___| |_| _ \| __ )
4 * | | | | | | |/ _ \/ __| __| | | | _ \
5 * | |_| | |_| | __/\__ \ |_| |_| | |_) |
6 * \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 * Copyright (c) 2014-2019 Appsicle
9 * Copyright (c) 2019-2025 QuestDB
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 *
23 ******************************************************************************/
24
25use crate::error;
26use std::time::{Duration, SystemTime, UNIX_EPOCH};
27
28#[cfg(feature = "chrono_timestamp")]
29use chrono::{DateTime, TimeZone};
30
31/// Convert a `SystemTime` to a `Duration` to/from the UNIX epoch.
32/// Returns a tuple of (is_negative, duration).
33#[inline]
34fn sys_time_to_duration(time: SystemTime, extract_fn: impl FnOnce(Duration) -> u128) -> i128 {
35 if time >= UNIX_EPOCH {
36 extract_fn(time.duration_since(UNIX_EPOCH).expect("time >= UNIX_EPOCH")) as i128
37 } else {
38 -(extract_fn(UNIX_EPOCH.duration_since(time).expect("time < UNIX_EPOCH")) as i128)
39 }
40}
41
42#[inline]
43fn sys_time_convert(
44 time: SystemTime,
45 extract_fn: impl FnOnce(Duration) -> u128,
46) -> crate::Result<i64> {
47 let number = sys_time_to_duration(time, extract_fn);
48 match i64::try_from(number) {
49 Ok(number) => Ok(number),
50 Err(_) => Err(error::fmt!(
51 InvalidTimestamp,
52 "Timestamp {:?} is out of range",
53 time
54 )),
55 }
56}
57
58#[inline]
59fn extract_current_timestamp(extract_fn: impl FnOnce(Duration) -> u128) -> crate::Result<i64> {
60 let time = SystemTime::now();
61 sys_time_convert(time, extract_fn)
62}
63
64/// A `i64` timestamp expressed as microseconds since the UNIX epoch (UTC).
65///
66/// # Examples
67///
68/// ```
69/// # use questdb::Result;
70/// use questdb::ingress::TimestampMicros;
71///
72/// # fn main() -> Result<()> {
73/// let ts = TimestampMicros::now();
74/// # Ok(())
75/// # }
76/// ```
77///
78/// or
79///
80/// ```
81/// # use questdb::Result;
82/// use questdb::ingress::TimestampMicros;
83///
84/// # fn main() -> Result<()> {
85/// let ts = TimestampMicros::new(1695312859886554);
86/// # Ok(())
87/// # }
88/// ```
89///
90/// or
91///
92/// ```
93/// # use questdb::Result;
94/// use questdb::ingress::TimestampMicros;
95///
96/// # fn main() -> Result<()> {
97/// let ts = TimestampMicros::from_systemtime(std::time::SystemTime::now())?;
98/// # Ok(())
99/// # }
100/// ```
101///
102/// or
103///
104/// ```
105/// # use questdb::Result;
106/// use questdb::ingress::TimestampMicros;
107///
108/// # fn main() -> Result<()> {
109/// #[cfg(feature = "chrono_timestamp")]
110/// let ts = TimestampMicros::from_datetime(chrono::Utc::now());
111/// # Ok(())
112/// # }
113/// ```
114#[derive(Copy, Clone, Debug)]
115pub struct TimestampMicros(i64);
116
117impl TimestampMicros {
118 /// Current UTC timestamp in microseconds.
119 pub fn now() -> Self {
120 Self(extract_current_timestamp(|d| d.as_micros()).expect("now in range of micros"))
121 }
122
123 /// Create a new timestamp from the given number of microseconds
124 /// since the UNIX epoch (UTC).
125 pub fn new(micros: i64) -> Self {
126 Self(micros)
127 }
128
129 #[cfg(feature = "chrono_timestamp")]
130 pub fn from_datetime<T: TimeZone>(dt: DateTime<T>) -> Self {
131 Self::new(dt.timestamp_micros())
132 }
133
134 pub fn from_systemtime(time: SystemTime) -> crate::Result<Self> {
135 sys_time_convert(time, |d| d.as_micros()).map(Self)
136 }
137
138 /// Get the numeric value of the timestamp.
139 pub fn as_i64(&self) -> i64 {
140 self.0
141 }
142}
143
144/// A `i64` timestamp expressed as nanoseconds since the UNIX epoch (UTC).
145///
146/// # Examples
147///
148/// ```
149/// # use questdb::Result;
150/// use questdb::ingress::TimestampNanos;
151///
152/// # fn main() -> Result<()> {
153/// let ts = TimestampNanos::now();
154/// # Ok(())
155/// # }
156/// ```
157///
158/// or
159///
160/// ```
161/// # use questdb::Result;
162/// use questdb::ingress::TimestampNanos;
163///
164/// # fn main() -> Result<()> {
165/// let ts = TimestampNanos::new(1659548315647406592);
166/// # Ok(())
167/// # }
168/// ```
169///
170/// or
171///
172/// ```
173/// # use questdb::Result;
174/// use questdb::ingress::TimestampNanos;
175///
176/// # fn main() -> Result<()> {
177/// let ts = TimestampNanos::from_systemtime(std::time::SystemTime::now())?;
178/// # Ok(())
179/// # }
180/// ```
181///
182/// or
183///
184/// ```
185/// # use questdb::Result;
186/// use questdb::ingress::TimestampNanos;
187///
188/// # fn main() -> Result<()> {
189/// # #[cfg(feature = "chrono_timestamp")]
190/// let ts = TimestampNanos::from_datetime(chrono::Utc::now());
191/// # Ok(())
192/// # }
193/// ```
194///
195#[derive(Copy, Clone, Debug)]
196pub struct TimestampNanos(i64);
197
198impl TimestampNanos {
199 /// Current UTC timestamp in nanoseconds.
200 pub fn now() -> Self {
201 Self(extract_current_timestamp(|d| d.as_nanos()).expect("now in range of nanos"))
202 }
203
204 /// Create a new timestamp from the given number of nanoseconds
205 /// since the UNIX epoch (UTC).
206 pub fn new(nanos: i64) -> Self {
207 Self(nanos)
208 }
209
210 #[cfg(feature = "chrono_timestamp")]
211 pub fn from_datetime<T: TimeZone>(dt: DateTime<T>) -> crate::Result<Self> {
212 match dt.timestamp_nanos_opt() {
213 Some(nanos) => Ok(Self::new(nanos)),
214 None => Err(error::fmt!(
215 InvalidTimestamp,
216 "Timestamp {:?} is out of range",
217 dt
218 )),
219 }
220 }
221
222 pub fn from_systemtime(time: SystemTime) -> crate::Result<Self> {
223 sys_time_convert(time, |d| d.as_nanos()).map(Self)
224 }
225
226 /// Get the numeric value of the timestamp.
227 pub fn as_i64(&self) -> i64 {
228 self.0
229 }
230}
231
232impl TryFrom<TimestampMicros> for TimestampNanos {
233 type Error = crate::Error;
234
235 fn try_from(ts: TimestampMicros) -> crate::Result<Self> {
236 let nanos = ts.as_i64().checked_mul(1000i64);
237 match nanos {
238 Some(nanos) => Ok(Self(nanos)),
239 None => Err(error::fmt!(
240 InvalidTimestamp,
241 "Timestamp {:?} is out of range",
242 ts
243 )),
244 }
245 }
246}
247
248impl From<TimestampNanos> for TimestampMicros {
249 fn from(ts: TimestampNanos) -> Self {
250 Self(ts.as_i64() / 1000i64)
251 }
252}
253
254/// A timestamp expressed as micros or nanos.
255/// You should seldom use this directly. Instead use one of:
256/// * `TimestampNanos`
257/// * `TimestampMicros`
258///
259/// Both of these types can `try_into()` the `Timestamp` type.
260///
261/// Both of these can be constructed from `std::time::SystemTime`,
262/// or from `chrono::DateTime`.
263#[derive(Copy, Clone, Debug)]
264pub enum Timestamp {
265 Micros(TimestampMicros),
266 Nanos(TimestampNanos),
267}
268
269impl From<TimestampMicros> for Timestamp {
270 fn from(ts: TimestampMicros) -> Self {
271 Self::Micros(ts)
272 }
273}
274
275impl From<TimestampNanos> for Timestamp {
276 fn from(ts: TimestampNanos) -> Self {
277 Self::Nanos(ts)
278 }
279}
280
281impl TryFrom<Timestamp> for TimestampMicros {
282 type Error = crate::Error;
283
284 fn try_from(ts: Timestamp) -> crate::Result<Self> {
285 match ts {
286 Timestamp::Micros(ts) => Ok(ts),
287 Timestamp::Nanos(ts) => Ok(ts.into()),
288 }
289 }
290}
291
292impl TryFrom<Timestamp> for TimestampNanos {
293 type Error = crate::Error;
294
295 fn try_from(ts: Timestamp) -> crate::Result<Self> {
296 match ts {
297 Timestamp::Micros(ts) => Ok(ts.try_into()?),
298 Timestamp::Nanos(ts) => Ok(ts),
299 }
300 }
301}