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.
910//! Utilities to build objects from the `repr` crate for unit testing.
11//!
12//! These test utilities are relied by crates other than `repr`.
1314use chrono::NaiveDateTime;
15use mz_lowertest::deserialize_optional_generic;
16use mz_ore::str::StrExt;
17use mz_repr::adt::numeric::Numeric;
18use mz_repr::adt::timestamp::CheckedTimestamp;
19use mz_repr::strconv::parse_jsonb;
20use mz_repr::{Datum, Row, RowArena, ScalarType};
21use proc_macro2::TokenTree;
2223/* #endregion */
2425fn parse_litval<'a, F>(litval: &'a str, littyp: &str) -> Result<F, String>
26where
27F: std::str::FromStr,
28 F::Err: ToString,
29{
30 litval.parse::<F>().map_err(|e| {
31format!(
32"error when parsing {} into {}: {}",
33 litval,
34 littyp,
35 e.to_string()
36 )
37 })
38}
3940/// Constructs a `Row` from a sequence of `litval` and `littyp`.
41///
42/// See [get_scalar_type_or_default] for creating a `ScalarType`.
43///
44/// Generally, each `litval` can be parsed into a Datum in the manner you would
45/// imagine. Exceptions:
46/// * A Timestamp should be in the format `"\"%Y-%m-%d %H:%M:%S%.f\""` or
47/// `"\"%Y-%m-%d %H:%M:%S\""`
48///
49/// Not all types are supported yet. Currently supported types:
50/// * string, bool, timestamp
51/// * all flavors of numeric types
52pub fn test_spec_to_row<'a, I>(datum_iter: I) -> Result<Row, String>
53where
54I: Iterator<Item = (&'a str, &'a ScalarType)>,
55{
56let temp_storage = RowArena::new();
57 Row::try_pack(datum_iter.map(|(litval, littyp)| {
58if litval == "null" {
59Ok(Datum::Null)
60 } else {
61match littyp {
62 ScalarType::Bool => Ok(Datum::from(parse_litval::<bool>(litval, "bool")?)),
63 ScalarType::Numeric { .. } => {
64Ok(Datum::from(parse_litval::<Numeric>(litval, "Numeric")?))
65 }
66 ScalarType::Int16 => Ok(Datum::from(parse_litval::<i16>(litval, "i16")?)),
67 ScalarType::Int32 => Ok(Datum::from(parse_litval::<i32>(litval, "i32")?)),
68 ScalarType::Int64 => Ok(Datum::from(parse_litval::<i64>(litval, "i64")?)),
69 ScalarType::Float32 => Ok(Datum::from(parse_litval::<f32>(litval, "f32")?)),
70 ScalarType::Float64 => Ok(Datum::from(parse_litval::<f64>(litval, "f64")?)),
71 ScalarType::String => Ok(Datum::from(
72 temp_storage.push_string(mz_lowertest::unquote(litval)),
73 )),
74 ScalarType::Timestamp { .. } => {
75let datetime = if litval.contains('.') {
76 NaiveDateTime::parse_from_str(litval, "\"%Y-%m-%d %H:%M:%S%.f\"")
77 } else {
78 NaiveDateTime::parse_from_str(litval, "\"%Y-%m-%d %H:%M:%S\"")
79 };
80Ok(Datum::from(
81 CheckedTimestamp::from_timestamplike(
82 datetime
83 .map_err(|e| format!("Error while parsing NaiveDateTime: {}", e))?,
84 )
85 .unwrap(),
86 ))
87 }
88 ScalarType::Jsonb => parse_jsonb(&mz_lowertest::unquote(litval))
89 .map(|jsonb| temp_storage.push_unary_row(jsonb.into_row()))
90 .map_err(|parse| format!("Invalid JSON literal: {:?}", parse)),
91_ => Err(format!("Unsupported literal type {:?}", littyp)),
92 }
93 }
94 }))
95}
9697/// Convert a Datum to a String such that [test_spec_to_row] can convert the
98/// String back into a row containing the same Datum.
99///
100/// Currently supports only Datums supported by [test_spec_to_row].
101pub fn datum_to_test_spec(datum: Datum) -> String {
102let result = format!("{}", datum);
103match datum {
104 Datum::Timestamp(_) => result.quoted().to_string(),
105_ => result,
106 }
107}
108109/// Parses `ScalarType` from `scalar_type_stream` or infers it from `litval`
110///
111/// See [mz_lowertest::to_json] for the syntax for specifying a `ScalarType`.
112/// If `scalar_type_stream` is empty, will attempt to guess a `ScalarType` for
113/// the literal:
114/// * If `litval` is "true", "false", or "null", will return `Bool`.
115/// * Else if starts with `'"'`, will return String.
116/// * Else if contains `'.'`, will return Float64.
117/// * Otherwise, returns Int64.
118pub fn get_scalar_type_or_default<I>(
119 litval: &str,
120 scalar_type_stream: &mut I,
121) -> Result<ScalarType, String>
122where
123I: Iterator<Item = TokenTree>,
124{
125let typ: Option<ScalarType> = deserialize_optional_generic(scalar_type_stream, "ScalarType")?;
126match typ {
127Some(typ) => Ok(typ),
128None => {
129if ["true", "false", "null"].contains(&litval) {
130Ok(ScalarType::Bool)
131 } else if litval.starts_with('\"') {
132Ok(ScalarType::String)
133 } else if litval.contains('.') {
134Ok(ScalarType::Float64)
135 } else {
136Ok(ScalarType::Int64)
137 }
138 }
139 }
140}
141142/// If the stream starts with a sequence of tokens that can be parsed as a datum,
143/// return those tokens as one string.
144///
145/// Sequences of tokens that can be parsed as a datum:
146/// * A Literal token, which is anything in quotations or a positive number
147/// * An null, false, or true Ident token
148/// * Punct(-) + a literal token
149///
150/// If the stream starts with a sequence of tokens that can be parsed as a
151/// datum, 1) returns Ok(Some(..)) 2) advances the stream to the first token
152/// that is not part of the sequence.
153/// If the stream does not start with tokens that can be parsed as a datum:
154/// * Return Ok(None) if `rest_of_stream` has not been advanced.
155/// * Returns Err(..) otherwise.
156pub fn extract_literal_string<I>(
157 first_arg: &TokenTree,
158 rest_of_stream: &mut I,
159) -> Result<Option<String>, String>
160where
161I: Iterator<Item = TokenTree>,
162{
163match first_arg {
164 TokenTree::Ident(ident) => {
165if ["true", "false", "null"].contains(&&ident.to_string()[..]) {
166Ok(Some(ident.to_string()))
167 } else {
168Ok(None)
169 }
170 }
171 TokenTree::Literal(literal) => Ok(Some(literal.to_string())),
172 TokenTree::Punct(punct) if punct.as_char() == '-' => {
173match rest_of_stream.next() {
174Some(TokenTree::Literal(literal)) => {
175Ok(Some(format!("{}{}", punct.as_char(), literal)))
176 }
177None => Ok(None),
178// Must error instead of handling the tokens using default
179 // behavior since `stream_iter` has advanced.
180Some(other) => Err(format!(
181"`{}` `{}` is not a valid literal",
182 punct.as_char(),
183 other
184 )),
185 }
186 }
187_ => Ok(None),
188 }
189}
190191/// Parse a token as a vec of strings that can be parsed as datums in a row.
192///
193/// The token is assumed to be of the form `[datum1 datum2 .. datumn]`.
194pub fn parse_vec_of_literals(token: &TokenTree) -> Result<Vec<String>, String> {
195match token {
196 TokenTree::Group(group) => {
197let mut inner_iter = group.stream().into_iter();
198let mut result = Vec::new();
199while let Some(symbol) = inner_iter.next() {
200match extract_literal_string(&symbol, &mut inner_iter)? {
201Some(dat) => result.push(dat),
202None => {
203return Err(format!(
204"TokenTree `{}` cannot be interpreted as a literal.",
205 symbol
206 ));
207 }
208 }
209 }
210Ok(result)
211 }
212 invalid => Err(format!(
213"TokenTree `{}` cannot be parsed as a vec of literals",
214 invalid
215 )),
216 }
217}