mz_persist_types/
timestamp.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Stats-related timestamp code.
11
12use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
13use mz_ore::cast::CastFrom;
14
15/// Parses a specific subset of ISO8061 timestamps.
16///
17/// This has very specific semantics so that it can enable pushdown on string
18/// timestamps in JSON. See doc/user/content/sql/functions/pushdown.md for
19/// details.
20pub fn try_parse_monotonic_iso8601_timestamp(a: &str) -> Option<NaiveDateTime> {
21    fn parse_lit(str: &mut &[u8], byte: u8) -> Option<()> {
22        if *str.split_off_first()? == byte {
23            Some(())
24        } else {
25            None
26        }
27    }
28
29    fn parse_int(str: &mut &[u8], digits: usize) -> Option<u32> {
30        let mut acc = 0u32;
31        for digit in str.split_off(..digits)? {
32            if !digit.is_ascii_digit() {
33                return None;
34            }
35            acc = acc * 10 + u32::cast_from(*digit - b'0');
36        }
37        Some(acc)
38    }
39
40    // The following assumes this is ASCII so do a quick check first.
41    if !a.is_ascii() {
42        return None;
43    }
44    let bytes = &mut a.as_bytes();
45
46    let yyyy = parse_int(bytes, 4)?;
47    parse_lit(bytes, b'-')?;
48    let mm = parse_int(bytes, 2)?;
49    parse_lit(bytes, b'-')?;
50    let dd = parse_int(bytes, 2)?;
51    parse_lit(bytes, b'T')?;
52    let hh = parse_int(bytes, 2)?;
53    parse_lit(bytes, b':')?;
54    let mi = parse_int(bytes, 2)?;
55    parse_lit(bytes, b':')?;
56    let ss = parse_int(bytes, 2)?;
57    parse_lit(bytes, b'.')?;
58    let ms = parse_int(bytes, 3)?;
59    parse_lit(bytes, b'Z')?;
60
61    if !bytes.is_empty() {
62        return None;
63    }
64
65    // YYYY is a max 4-digit unsigned int, which can always be represented as a positive i32.
66    let date = NaiveDate::from_ymd_opt(i32::try_from(yyyy).ok()?, mm, dd)?;
67    let time = NaiveTime::from_hms_milli_opt(hh, mi, ss, ms)?;
68    Some(NaiveDateTime::new(date, time))
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[mz_ore::test]
76    fn monotonic_iso8601() {
77        // The entire point of this method is that the lexicographic order
78        // corresponds to chronological order (ignoring None/NULL). So, verify.
79        let mut inputs = vec![
80            "-005-01-01T00:00:00.000Z",
81            "-002-01-01T00:00:00.000Z",
82            "0000-01-01T00:00:00.000Z",
83            "0001-01-01T00:00:00.000Z",
84            "+000-01-01T00:00:00.000Z",
85            "+001-01-01T00:00:00.000Z",
86            "2015-00-00T00:00:00.000Z",
87            "2015-09-00T00:00:00.000Z",
88            "2015-09-18T00:00:00.000Z",
89            "2015-09-18T23:00:00.000Z",
90            "2015-09-18T23:56:00.000Z",
91            "2015-09-18T23:56:04.000Z",
92            "2015-09-18T23:56:04.123Z",
93            "2015-09-18T23:56:04.1234Z",
94            "2015-09-18T23:56:04.124Z",
95            "2015-09-18T23:56:05.000Z",
96            "2015-09-18T23:57:00.000Z",
97            "2015-09-18T23:57:00.000Zextra",
98            "2015-09-18T24:00:00.000Z",
99            "2015-09-19T00:00:00.000Z",
100            "2015-10-00T00:00:00.000Z",
101            "2016-10-00T00:00:00.000Z",
102            "9999-12-31T23:59:59.999Z",
103        ];
104        // Sort the inputs so we can't accidentally pass by hardcoding them in
105        // the wrong order.
106        inputs.sort();
107        let outputs = inputs
108            .into_iter()
109            .flat_map(try_parse_monotonic_iso8601_timestamp)
110            .collect::<Vec<_>>();
111        // Sanity check that we don't trivially pass by always returning None.
112        assert!(!outputs.is_empty());
113        let mut outputs_sorted = outputs.clone();
114        outputs_sorted.sort();
115        assert_eq!(outputs, outputs_sorted);
116    }
117}