Skip to main content

mz_sql/session/vars/
value.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
10use std::any::Any;
11use std::borrow::Cow;
12use std::fmt::{self, Debug};
13use std::num::NonZeroU32;
14use std::str::FromStr;
15use std::time::Duration;
16
17use chrono::{DateTime, Utc};
18use ipnet::IpNet;
19use itertools::Itertools;
20use mz_pgwire_common::Severity;
21use mz_repr::adt::numeric::Numeric;
22use mz_repr::adt::timestamp::CheckedTimestamp;
23use mz_repr::strconv;
24use mz_rocksdb_types::config::{CompactionStyle, CompressionType};
25use mz_sql_parser::ast::{Ident, TransactionIsolationLevel};
26use mz_tracing::{CloneableEnvFilter, SerializableDirective};
27use serde::{Deserialize, Serialize};
28use uncased::UncasedStr;
29
30use super::VarInput;
31use super::errors::VarParseError;
32
33/// Defines a value that get stored as part of a System or Session variable.
34///
35/// This trait is partially object safe, see [`VarDefinition`] for more details.
36///
37/// [`VarDefinition`]: crate::session::vars::definitions::VarDefinition
38pub trait Value: Any + AsAny + Debug + Send + Sync {
39    fn type_name() -> Cow<'static, str>
40    where
41        Self: Sized;
42
43    fn parse(input: VarInput) -> Result<Self, VarParseError>
44    where
45        Self: Sized;
46
47    fn format(&self) -> String;
48
49    fn box_clone(&self) -> Box<dyn Value>;
50
51    /// Parse an instance of `Self` from [`VarInput`], returning it as a `Box<dyn Value>`.
52    fn parse_dyn_value(input: VarInput) -> Result<Box<dyn Value>, VarParseError>
53    where
54        Self: Sized,
55    {
56        Self::parse(input).map(|val| {
57            let dyn_val: Box<dyn Value> = Box::new(val);
58            dyn_val
59        })
60    }
61}
62
63// Note(parkmycar): We have a blanket impl for `PartialEq` instead of requiring it as a trait
64// bound because otherwise it's tricky to make `Value` object safe.
65impl PartialEq for Box<dyn Value> {
66    fn eq(&self, other: &Box<dyn Value>) -> bool {
67        self.format().eq(&other.format())
68    }
69}
70impl Eq for Box<dyn Value> {}
71
72impl<'a> PartialEq for &'a dyn Value {
73    fn eq(&self, other: &&dyn Value) -> bool {
74        self.format().eq(&other.format())
75    }
76}
77impl<'a> Eq for &'a dyn Value {}
78
79/// Helper trait to cast a `&dyn T` to a `&dyn Any`.
80///
81/// In Rust all types that are `'static` implement [`std::any::Any`] and thus can be casted to a
82/// `&dyn Any`. But once you create a trait object, the type is erased and thus so is the
83/// implementation for [`Any`]. This trait essentially adds a types' [`Any`] impl to the vtable
84/// created when casted to trait object, if [`AsAny`] is a supertrait.
85///
86/// See [`Value`] for an example of using [`AsAny`].
87pub trait AsAny {
88    fn as_any(&self) -> &dyn Any;
89}
90
91impl<T: Any> AsAny for T {
92    fn as_any(&self) -> &dyn Any {
93        self
94    }
95}
96
97/// Helper method to extract a single value from any kind of [`VarInput`].
98fn extract_single_value(input: VarInput<'_>) -> Result<&str, VarParseError> {
99    match input {
100        VarInput::Flat(value) => Ok(value),
101        VarInput::SqlSet([value]) => Ok(value),
102        VarInput::SqlSet(values) => Err(VarParseError::InvalidParameterValue {
103            invalid_values: values.to_vec(),
104            reason: "expects a single value".into(),
105        }),
106    }
107}
108
109impl<V: Value + Clone> Value for Option<V> {
110    fn type_name() -> Cow<'static, str>
111    where
112        Self: Sized,
113    {
114        format!("optional {}", V::type_name()).into()
115    }
116
117    fn parse(input: VarInput) -> Result<Self, VarParseError>
118    where
119        Self: Sized,
120    {
121        let s = extract_single_value(input)?;
122        match s {
123            "" => Ok(None),
124            _ => <V as Value>::parse(VarInput::Flat(s)).map(Some),
125        }
126    }
127
128    fn box_clone(&self) -> Box<dyn Value> {
129        Box::new(self.clone())
130    }
131
132    fn format(&self) -> String {
133        match self {
134            Some(s) => s.format(),
135            None => "".to_string(),
136        }
137    }
138}
139
140impl Value for String {
141    fn type_name() -> Cow<'static, str>
142    where
143        Self: Sized,
144    {
145        "string".into()
146    }
147
148    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
149    where
150        Self: Sized,
151    {
152        let s = extract_single_value(input)?;
153        Ok(s.to_string())
154    }
155
156    fn box_clone(&self) -> Box<dyn Value> {
157        Box::new(self.clone())
158    }
159
160    fn format(&self) -> String {
161        self.to_string()
162    }
163}
164
165impl Value for Cow<'static, str> {
166    fn type_name() -> Cow<'static, str>
167    where
168        Self: Sized,
169    {
170        "string".into()
171    }
172
173    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
174    where
175        Self: Sized,
176    {
177        let s = extract_single_value(input)?;
178        Ok(s.to_string().into())
179    }
180
181    fn box_clone(&self) -> Box<dyn Value> {
182        Box::new(self.clone())
183    }
184
185    fn format(&self) -> String {
186        self.to_string()
187    }
188}
189
190impl Value for bool {
191    fn type_name() -> Cow<'static, str>
192    where
193        Self: Sized,
194    {
195        "boolean".into()
196    }
197
198    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
199    where
200        Self: Sized,
201    {
202        let s = extract_single_value(input)?;
203        match s {
204            "t" | "true" | "on" => Ok(true),
205            "f" | "false" | "off" => Ok(false),
206            _ => Err(VarParseError::InvalidParameterType),
207        }
208    }
209
210    fn box_clone(&self) -> Box<dyn Value> {
211        Box::new(self.clone())
212    }
213
214    fn format(&self) -> String {
215        match self {
216            true => "on".into(),
217            false => "off".into(),
218        }
219    }
220}
221
222impl Value for Option<CheckedTimestamp<DateTime<Utc>>> {
223    fn type_name() -> Cow<'static, str>
224    where
225        Self: Sized,
226    {
227        "timestamptz".into()
228    }
229
230    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
231    where
232        Self: Sized,
233    {
234        let s = extract_single_value(input)?;
235        strconv::parse_timestamptz(s)
236            .map_err(|_| VarParseError::InvalidParameterType)
237            .map(Some)
238    }
239
240    fn box_clone(&self) -> Box<dyn Value> {
241        Box::new(self.clone())
242    }
243
244    fn format(&self) -> String {
245        self.map(|t| t.to_string()).unwrap_or_default()
246    }
247}
248
249impl Value for Numeric {
250    fn type_name() -> Cow<'static, str>
251    where
252        Self: Sized,
253    {
254        "numeric".into()
255    }
256
257    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
258    where
259        Self: Sized,
260    {
261        let s = extract_single_value(input)?;
262        s.parse::<Numeric>()
263            .map_err(|_| VarParseError::InvalidParameterType)
264    }
265
266    fn box_clone(&self) -> Box<dyn Value> {
267        Box::new(self.clone())
268    }
269
270    fn format(&self) -> String {
271        self.to_standard_notation_string()
272    }
273}
274
275const SEC_TO_MIN: u64 = 60u64;
276const SEC_TO_HOUR: u64 = 60u64 * 60;
277const SEC_TO_DAY: u64 = 60u64 * 60 * 24;
278const MICRO_TO_MILLI: u32 = 1000u32;
279
280impl Value for Duration {
281    fn type_name() -> Cow<'static, str>
282    where
283        Self: Sized,
284    {
285        "duration".into()
286    }
287
288    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
289    where
290        Self: Sized,
291    {
292        let s = extract_single_value(input)?;
293        let s = s.trim();
294        // Find where the leading run of ASCII digits ends. `str::find` returns a
295        // byte index, so it is safe to slice with directly. We restrict to ASCII
296        // digits (rather than `char::is_numeric`) because that is exactly what
297        // `u64::parse` accepts; matching broader Unicode numerics such as '²'
298        // would yield a byte offset that can land mid-character and panic.
299        let split_pos = s.find(|p: char| !p.is_ascii_digit()).unwrap_or(s.len());
300
301        // Error if the numeric values don't parse, i.e. there aren't any.
302        let d = s[..split_pos]
303            .parse::<u64>()
304            .map_err(|_| VarParseError::InvalidParameterType)?;
305
306        // We've already trimmed end
307        let (f, m): (fn(u64) -> Duration, u64) = match s[split_pos..].trim_start() {
308            "us" => (Duration::from_micros, 1),
309            // Default unit is milliseconds
310            "ms" | "" => (Duration::from_millis, 1),
311            "s" => (Duration::from_secs, 1),
312            "min" => (Duration::from_secs, SEC_TO_MIN),
313            "h" => (Duration::from_secs, SEC_TO_HOUR),
314            "d" => (Duration::from_secs, SEC_TO_DAY),
315            o => {
316                return Err(VarParseError::InvalidParameterValue {
317                    invalid_values: vec![o.to_string()],
318                    reason: format!("expected us, ms, s, min, h, or d but got {o:?}"),
319                });
320            }
321        };
322
323        let d = f(d
324            .checked_mul(m)
325            .ok_or_else(|| VarParseError::InvalidParameterValue {
326                invalid_values: vec![s.to_string()],
327                reason: "expected value to fit in u64".into(),
328            })?);
329        Ok(d)
330    }
331
332    fn box_clone(&self) -> Box<dyn Value> {
333        Box::new(self.clone())
334    }
335
336    // The strategy for formatting these strings is to find the least
337    // significant unit of time that can be printed as an integer––we know this
338    // is always possible because the input can only be an integer of a single
339    // unit of time.
340    fn format(&self) -> String {
341        let micros = self.subsec_micros();
342        if micros > 0 {
343            match micros {
344                ms if ms != 0 && ms % MICRO_TO_MILLI == 0 => {
345                    format!(
346                        "{} ms",
347                        self.as_secs() * 1000 + u64::from(ms / MICRO_TO_MILLI)
348                    )
349                }
350                us => format!("{} us", self.as_secs() * 1_000_000 + u64::from(us)),
351            }
352        } else {
353            match self.as_secs() {
354                zero if zero == u64::MAX => "0".to_string(),
355                d if d != 0 && d % SEC_TO_DAY == 0 => format!("{} d", d / SEC_TO_DAY),
356                h if h != 0 && h % SEC_TO_HOUR == 0 => format!("{} h", h / SEC_TO_HOUR),
357                m if m != 0 && m % SEC_TO_MIN == 0 => format!("{} min", m / SEC_TO_MIN),
358                s => format!("{} s", s),
359            }
360        }
361    }
362}
363
364impl Value for serde_json::Value {
365    fn type_name() -> Cow<'static, str>
366    where
367        Self: Sized,
368    {
369        "jsonb".into()
370    }
371
372    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
373    where
374        Self: Sized,
375    {
376        let s = extract_single_value(input)?;
377        serde_json::from_str(s).map_err(|_| VarParseError::InvalidParameterType)
378    }
379
380    fn box_clone(&self) -> Box<dyn Value> {
381        Box::new(self.clone())
382    }
383
384    fn format(&self) -> String {
385        self.to_string()
386    }
387}
388
389/// This style should actually be some more complex struct, but we only support this configuration
390/// of it, so this is fine for the time being.
391#[derive(Debug, Clone, Eq, PartialEq)]
392pub struct DateStyle(pub [&'static str; 2]);
393
394pub static DEFAULT_DATE_STYLE: DateStyle = DateStyle(["ISO", "MDY"]);
395
396impl Value for DateStyle {
397    fn type_name() -> Cow<'static, str>
398    where
399        Self: Sized,
400    {
401        "string list".into()
402    }
403
404    /// This impl is unlike most others because we have under-implemented its backing struct.
405    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
406    where
407        Self: Sized,
408    {
409        let input = match input {
410            VarInput::Flat(v) => mz_sql_parser::parser::split_identifier_string(v)
411                .map_err(|_| VarParseError::InvalidParameterType)?,
412            // Unlike parsing `Vec<Ident>`, we further split each element.
413            // This matches PostgreSQL.
414            VarInput::SqlSet(values) => {
415                let mut out = vec![];
416                for v in values {
417                    let idents = mz_sql_parser::parser::split_identifier_string(v)
418                        .map_err(|_| VarParseError::InvalidParameterType)?;
419                    out.extend(idents)
420                }
421                out
422            }
423        };
424
425        for input in input {
426            if !DEFAULT_DATE_STYLE
427                .0
428                .iter()
429                .any(|valid| UncasedStr::new(valid) == &input)
430            {
431                return Err(VarParseError::FixedValueParameter);
432            }
433        }
434
435        Ok(DEFAULT_DATE_STYLE.clone())
436    }
437
438    fn box_clone(&self) -> Box<dyn Value> {
439        Box::new(self.clone())
440    }
441
442    fn format(&self) -> String {
443        self.0.join(", ")
444    }
445}
446
447impl Value for Vec<Ident> {
448    fn type_name() -> Cow<'static, str>
449    where
450        Self: Sized,
451    {
452        "identifier list".into()
453    }
454
455    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
456    where
457        Self: Sized,
458    {
459        let holder;
460        let values = match input {
461            VarInput::Flat(value) => {
462                holder = mz_sql_parser::parser::split_identifier_string(value)
463                    .map_err(|_| VarParseError::InvalidParameterType)?;
464                &holder
465            }
466            // Unlike parsing `Vec<String>`, we do *not* further split each
467            // element. This matches PostgreSQL.
468            VarInput::SqlSet(values) => values,
469        };
470        // An empty identifier formats as `""`, which the `Flat` path rejects
471        // when the durable catalog re-parses it. A lone `''` means the empty
472        // list instead.
473        if values.iter().any(|v| v.is_empty()) {
474            if values.len() == 1 {
475                return Ok(vec![]);
476            }
477            return Err(VarParseError::InvalidParameterValue {
478                invalid_values: values.to_vec(),
479                reason: "empty identifier".into(),
480            });
481        }
482        let values = values
483            .iter()
484            .map(Ident::new)
485            .collect::<Result<_, _>>()
486            .map_err(|e| VarParseError::InvalidParameterValue {
487                invalid_values: values.to_vec(),
488                reason: e.to_string(),
489            })?;
490        Ok(values)
491    }
492
493    fn box_clone(&self) -> Box<dyn Value> {
494        Box::new(self.clone())
495    }
496
497    fn format(&self) -> String {
498        self.iter().map(|ident| ident.to_string()).join(", ")
499    }
500}
501
502impl Value for Vec<IpNet> {
503    fn type_name() -> Cow<'static, str>
504    where
505        Self: Sized,
506    {
507        "CIDR list".into()
508    }
509
510    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
511    where
512        Self: Sized,
513    {
514        let values = input.to_vec();
515        let values: Vec<IpNet> = values
516            .iter()
517            .flat_map(|i| i.split(','))
518            .map(|d| IpNet::from_str(d.trim()))
519            .collect::<Result<_, _>>()
520            .map_err(|e| VarParseError::InvalidParameterValue {
521                invalid_values: values,
522                reason: e.to_string(),
523            })?;
524        Ok(values)
525    }
526
527    fn box_clone(&self) -> Box<dyn Value> {
528        Box::new(self.clone())
529    }
530
531    fn format(&self) -> String {
532        self.iter().map(|ident| ident.to_string()).join(", ")
533    }
534}
535
536impl Value for Vec<SerializableDirective> {
537    fn type_name() -> Cow<'static, str>
538    where
539        Self: Sized,
540    {
541        "directive list".into()
542    }
543
544    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
545    where
546        Self: Sized,
547    {
548        let values = input.to_vec();
549        let dirs: Result<_, _> = values
550            .iter()
551            .flat_map(|i| i.split(','))
552            .map(|d| SerializableDirective::from_str(d.trim()))
553            .collect();
554        dirs.map_err(|e| VarParseError::InvalidParameterValue {
555            invalid_values: values.to_vec(),
556            reason: e.to_string(),
557        })
558    }
559
560    fn box_clone(&self) -> Box<dyn Value> {
561        Box::new(self.clone())
562    }
563
564    fn format(&self) -> String {
565        self.iter().map(|d| d.to_string()).join(", ")
566    }
567}
568
569// This unorthodox design lets us escape complex errors from value parsing.
570#[derive(Clone, Debug, Eq, PartialEq)]
571pub struct Failpoints;
572
573impl Value for Failpoints {
574    fn type_name() -> Cow<'static, str>
575    where
576        Self: Sized,
577    {
578        "failpoints config".into()
579    }
580
581    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
582    where
583        Self: Sized,
584    {
585        let values = input.to_vec();
586        for mut cfg in values.iter().map(|v| v.trim().split(';')).flatten() {
587            cfg = cfg.trim();
588            if cfg.is_empty() {
589                continue;
590            }
591            let mut splits = cfg.splitn(2, '=');
592            let failpoint = splits
593                .next()
594                .ok_or_else(|| VarParseError::InvalidParameterValue {
595                    invalid_values: input.to_vec(),
596                    reason: "missing failpoint name".into(),
597                })?;
598            let action = splits
599                .next()
600                .ok_or_else(|| VarParseError::InvalidParameterValue {
601                    invalid_values: input.to_vec(),
602                    reason: "missing failpoint action".into(),
603                })?;
604            // NOTE: This configures the failpoint for the whole process, so the
605            // unsafe-mode gate on the variable only protects paths that check
606            // visibility before parsing.
607            fail::cfg(failpoint, action).map_err(|e| VarParseError::InvalidParameterValue {
608                invalid_values: input.to_vec(),
609                reason: e.to_string(),
610            })?;
611        }
612
613        Ok(Failpoints)
614    }
615
616    fn box_clone(&self) -> Box<dyn Value> {
617        Box::new(self.clone())
618    }
619
620    fn format(&self) -> String {
621        "<omitted>".to_string()
622    }
623}
624
625/// Severity levels can used to be used to filter which messages get sent
626/// to a client.
627///
628/// The ordering of severity levels used for client-level filtering differs from the
629/// one used for server-side logging in two aspects: INFO messages are always sent,
630/// and the LOG severity is considered as below NOTICE, while it is above ERROR for
631/// server-side logs.
632#[derive(Clone, Copy, Debug, Eq, PartialEq)]
633pub enum ClientSeverity {
634    /// Sends only INFO, ERROR, FATAL and PANIC level messages.
635    Error,
636    /// Sends only WARNING, INFO, ERROR, FATAL and PANIC level messages.
637    Warning,
638    /// Sends only NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
639    Notice,
640    /// Sends only LOG, NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
641    Log,
642    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
643    Debug1,
644    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
645    Debug2,
646    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
647    Debug3,
648    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
649    Debug4,
650    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
651    Debug5,
652    /// Sends only NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
653    /// Not listed as a valid value, but accepted by Postgres
654    Info,
655}
656
657impl Serialize for ClientSeverity {
658    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
659    where
660        S: serde::Serializer,
661    {
662        serializer.serialize_str(self.as_str())
663    }
664}
665
666impl ClientSeverity {
667    fn as_str(&self) -> &'static str {
668        match self {
669            ClientSeverity::Error => "error",
670            ClientSeverity::Warning => "warning",
671            ClientSeverity::Notice => "notice",
672            ClientSeverity::Info => "info",
673            ClientSeverity::Log => "log",
674            ClientSeverity::Debug1 => "debug1",
675            ClientSeverity::Debug2 => "debug2",
676            ClientSeverity::Debug3 => "debug3",
677            ClientSeverity::Debug4 => "debug4",
678            ClientSeverity::Debug5 => "debug5",
679        }
680    }
681
682    fn valid_values() -> Vec<&'static str> {
683        // INFO left intentionally out, to match Postgres
684        vec![
685            ClientSeverity::Debug5.as_str(),
686            ClientSeverity::Debug4.as_str(),
687            ClientSeverity::Debug3.as_str(),
688            ClientSeverity::Debug2.as_str(),
689            ClientSeverity::Debug1.as_str(),
690            ClientSeverity::Log.as_str(),
691            ClientSeverity::Notice.as_str(),
692            ClientSeverity::Warning.as_str(),
693            ClientSeverity::Error.as_str(),
694        ]
695    }
696
697    /// Checks if a message of a given severity level should be sent to a client.
698    ///
699    /// The ordering of severity levels used for client-level filtering differs from the
700    /// one used for server-side logging in two aspects: INFO messages are always sent,
701    /// and the LOG severity is considered as below NOTICE, while it is above ERROR for
702    /// server-side logs.
703    ///
704    /// Postgres only considers the session setting after the client authentication
705    /// handshake is completed. Since this function is only called after client authentication
706    /// is done, we are not treating this case right now, but be aware if refactoring it.
707    pub fn should_output_to_client(&self, severity: &Severity) -> bool {
708        match (self, severity) {
709            // INFO messages are always sent
710            (_, Severity::Info) => true,
711            (ClientSeverity::Error, Severity::Error | Severity::Fatal | Severity::Panic) => true,
712            (
713                ClientSeverity::Warning,
714                Severity::Error | Severity::Fatal | Severity::Panic | Severity::Warning,
715            ) => true,
716            (
717                ClientSeverity::Notice,
718                Severity::Error
719                | Severity::Fatal
720                | Severity::Panic
721                | Severity::Warning
722                | Severity::Notice,
723            ) => true,
724            (
725                ClientSeverity::Info,
726                Severity::Error
727                | Severity::Fatal
728                | Severity::Panic
729                | Severity::Warning
730                | Severity::Notice,
731            ) => true,
732            (
733                ClientSeverity::Log,
734                Severity::Error
735                | Severity::Fatal
736                | Severity::Panic
737                | Severity::Warning
738                | Severity::Notice
739                | Severity::Log,
740            ) => true,
741            (
742                ClientSeverity::Debug1
743                | ClientSeverity::Debug2
744                | ClientSeverity::Debug3
745                | ClientSeverity::Debug4
746                | ClientSeverity::Debug5,
747                _,
748            ) => true,
749
750            (
751                ClientSeverity::Error,
752                Severity::Warning | Severity::Notice | Severity::Log | Severity::Debug,
753            ) => false,
754            (ClientSeverity::Warning, Severity::Notice | Severity::Log | Severity::Debug) => false,
755            (ClientSeverity::Notice, Severity::Log | Severity::Debug) => false,
756            (ClientSeverity::Info, Severity::Log | Severity::Debug) => false,
757            (ClientSeverity::Log, Severity::Debug) => false,
758        }
759    }
760}
761
762impl Value for ClientSeverity {
763    fn type_name() -> Cow<'static, str>
764    where
765        Self: Sized,
766    {
767        "string".into()
768    }
769
770    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
771    where
772        Self: Sized,
773    {
774        let s = extract_single_value(input)?;
775        let s = UncasedStr::new(s);
776
777        if s == ClientSeverity::Error.as_str() {
778            Ok(ClientSeverity::Error)
779        } else if s == ClientSeverity::Warning.as_str() {
780            Ok(ClientSeverity::Warning)
781        } else if s == ClientSeverity::Notice.as_str() {
782            Ok(ClientSeverity::Notice)
783        } else if s == ClientSeverity::Info.as_str() {
784            Ok(ClientSeverity::Info)
785        } else if s == ClientSeverity::Log.as_str() {
786            Ok(ClientSeverity::Log)
787        } else if s == ClientSeverity::Debug1.as_str() {
788            Ok(ClientSeverity::Debug1)
789        // Postgres treats `debug` as an input as equivalent to `debug2`
790        } else if s == ClientSeverity::Debug2.as_str() || s == "debug" {
791            Ok(ClientSeverity::Debug2)
792        } else if s == ClientSeverity::Debug3.as_str() {
793            Ok(ClientSeverity::Debug3)
794        } else if s == ClientSeverity::Debug4.as_str() {
795            Ok(ClientSeverity::Debug4)
796        } else if s == ClientSeverity::Debug5.as_str() {
797            Ok(ClientSeverity::Debug5)
798        } else {
799            Err(VarParseError::ConstrainedParameter {
800                invalid_values: input.to_vec(),
801                valid_values: Some(ClientSeverity::valid_values()),
802            })
803        }
804    }
805
806    fn box_clone(&self) -> Box<dyn Value> {
807        Box::new(self.clone())
808    }
809
810    fn format(&self) -> String {
811        self.as_str().into()
812    }
813}
814
815/// List of valid time zones.
816///
817/// Names are following the tz database, but only time zones equivalent
818/// to UTC±00:00 are supported.
819#[derive(Clone, Copy, Debug, Eq, PartialEq)]
820pub enum TimeZone {
821    /// UTC
822    UTC,
823    /// GMT
824    GMT,
825    /// Fixed offset from UTC, currently only "+00:00" is supported.
826    /// A string representation is kept here for compatibility with Postgres.
827    FixedOffset(&'static str),
828}
829
830impl TimeZone {
831    fn as_str(&self) -> &'static str {
832        match self {
833            TimeZone::UTC => "UTC",
834            TimeZone::GMT => "GMT",
835            TimeZone::FixedOffset(s) => s,
836        }
837    }
838}
839
840impl Value for TimeZone {
841    fn type_name() -> Cow<'static, str>
842    where
843        Self: Sized,
844    {
845        // TODO(parkmycar): It seems like we should change this?
846        "string".into()
847    }
848
849    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
850    where
851        Self: Sized,
852    {
853        let s = extract_single_value(input)?;
854        let s = UncasedStr::new(s);
855
856        if s == TimeZone::UTC.as_str() {
857            Ok(TimeZone::UTC)
858        } else if s == TimeZone::GMT.as_str() {
859            Ok(TimeZone::GMT)
860        } else if s == "+00:00" {
861            Ok(TimeZone::FixedOffset("+00:00"))
862        } else {
863            Err(VarParseError::ConstrainedParameter {
864                invalid_values: input.to_vec(),
865                valid_values: None,
866            })
867        }
868    }
869
870    fn box_clone(&self) -> Box<dyn Value> {
871        Box::new(self.clone())
872    }
873
874    fn format(&self) -> String {
875        self.as_str().into()
876    }
877}
878
879/// List of valid isolation levels.
880#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
881pub enum IsolationLevel {
882    ReadUncommitted,
883    ReadCommitted,
884    RepeatableRead,
885    Serializable,
886    /* TODO(jkosh44) Move this comment to user facing docs when this isolation level becomes available to users.
887     * The Strong Session Serializable isolation level combines the Serializable isolation level
888     * (https://jepsen.io/consistency/models/serializable) with the Sequential consistency model
889     * (https://jepsen.io/consistency/models/sequential). See
890     * http://dbmsmusings.blogspot.com/2019/06/correctness-anomalies-under.html and
891     * https://cs.uwaterloo.ca/~kmsalem/pubs/DaudjeeICDE04.pdf. Operations within a single session
892     * are linearizable, but operations across sessions are not linearizable.
893     *
894     * Operations in sessions that use Strong Session Serializable are not linearizable with
895     * operations in sessions that use Strict Serializable. For example, consider the following
896     * sequence of events in order:
897     *
898     *   1. Session s0 executes read at timestamp t0 under Strong Session Serializable.
899     *   2. Session s1 executes read at timestamp t1 under Strict Serializable.
900     *
901     * If t0 > t1, then this is not considered a consistency violation. This matches with the
902     * semantics of Serializable, which can execute queries arbitrarily in the future without
903     * violating the consistency of Strict Serializable queries.
904     *
905     * All operations within a session that use Strong Session Serializable are only
906     * linearizable within operations within the same session that also use Strong Session
907     * Serializable. For example, consider the following sequence of events in order:
908     *
909     *   1. Session s0 executes read at timestamp t0 under Strong Session Serializable.
910     *   2. Session s0 executes read at timestamp t1 under I.
911     *
912     * If I is Strong Session Serializable then t0 > t1 is guaranteed. If I is any other isolation
913     * level then t0 < t1 is not considered a consistency violation. This matches the semantics of
914     * Serializable, which can execute queries arbitrarily in the future without violating the
915     * consistency of Strict Serializable queries within the same session.
916     *
917     * The items left TODO before this is considered ready for prod are:
918     *
919     * - Add more tests.
920     * - Linearize writes to system tables under this isolation (most of these are the side effect
921     *   of some DDL).
922     */
923    StrongSessionSerializable,
924    StrictSerializable,
925    /// Bounded staleness — pick `T` such that `T >= now_ms - D` and the
926    /// query does not wait on input frontiers. Errors if no such `T` exists
927    /// in the no-wait window. See
928    /// `doc/developer/design/20260429_bounded_staleness_isolation.md`.
929    BoundedStaleness(std::time::Duration),
930}
931
932impl IsolationLevel {
933    const READ_UNCOMMITTED: &'static str = "read uncommitted";
934    const READ_COMMITTED: &'static str = "read committed";
935    const REPEATABLE_READ: &'static str = "repeatable read";
936    const SERIALIZABLE: &'static str = "serializable";
937    const STRONG_SESSION_SERIALIZABLE: &'static str = "strong session serializable";
938    const STRICT_SERIALIZABLE: &'static str = "strict serializable";
939    const BOUNDED_STALENESS: &'static str = "bounded staleness";
940    const BOUNDED_STALENESS_HINT: &'static str = "bounded staleness <duration>";
941
942    /// Returns true if the isolation level is of bounded staleness.
943    pub fn is_bounded_staleness(&self) -> bool {
944        matches!(self, Self::BoundedStaleness(_))
945    }
946
947    /// Unit-cardinality variant identifier, suitable for Prometheus labels and
948    /// equality checks against parsed input. The duration on `BoundedStaleness`
949    /// is omitted; use [`fmt::Display`] for the user-facing rendering.
950    pub fn as_variant_str(&self) -> &'static str {
951        match self {
952            Self::ReadUncommitted => Self::READ_UNCOMMITTED,
953            Self::ReadCommitted => Self::READ_COMMITTED,
954            Self::RepeatableRead => Self::REPEATABLE_READ,
955            Self::Serializable => Self::SERIALIZABLE,
956            Self::StrongSessionSerializable => Self::STRONG_SESSION_SERIALIZABLE,
957            Self::StrictSerializable => Self::STRICT_SERIALIZABLE,
958            Self::BoundedStaleness(_) => Self::BOUNDED_STALENESS,
959        }
960    }
961
962    fn valid_values() -> Vec<&'static str> {
963        vec![
964            Self::READ_UNCOMMITTED,
965            Self::READ_COMMITTED,
966            Self::REPEATABLE_READ,
967            Self::SERIALIZABLE,
968            // TODO(jkosh44) Add STRONG_SESSION_SERIALIZABLE when it becomes available to users.
969            Self::STRICT_SERIALIZABLE,
970            Self::BOUNDED_STALENESS_HINT,
971        ]
972    }
973}
974
975impl fmt::Display for IsolationLevel {
976    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
977        match self {
978            Self::BoundedStaleness(d) => write!(
979                f,
980                "{} {}",
981                Self::BOUNDED_STALENESS,
982                humantime::format_duration(*d)
983            ),
984            other => f.write_str(other.as_variant_str()),
985        }
986    }
987}
988
989impl Value for IsolationLevel {
990    fn type_name() -> Cow<'static, str>
991    where
992        Self: Sized,
993    {
994        "string".into()
995    }
996
997    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
998    where
999        Self: Sized,
1000    {
1001        let invalid = || VarParseError::ConstrainedParameter {
1002            invalid_values: input.to_vec(),
1003            valid_values: Some(IsolationLevel::valid_values()),
1004        };
1005
1006        let s = extract_single_value(input)?;
1007        let lower = s.to_ascii_lowercase();
1008        let lower = lower.trim();
1009
1010        match lower {
1011            // Weak isolations all upgrade to Serializable.
1012            Self::READ_UNCOMMITTED
1013            | Self::READ_COMMITTED
1014            | Self::REPEATABLE_READ
1015            | Self::SERIALIZABLE => Ok(Self::Serializable),
1016            Self::STRONG_SESSION_SERIALIZABLE => Ok(Self::StrongSessionSerializable),
1017            Self::STRICT_SERIALIZABLE => Ok(Self::StrictSerializable),
1018            other => {
1019                let rest = other
1020                    .strip_prefix(Self::BOUNDED_STALENESS)
1021                    .ok_or_else(invalid)?;
1022                // Require whitespace between the keyword and the duration, so
1023                // e.g. `bounded staleness5s` is rejected.
1024                if !rest.starts_with(char::is_whitespace) {
1025                    return Err(invalid());
1026                }
1027                let rest = rest.trim();
1028                if rest.is_empty() {
1029                    return Err(invalid());
1030                }
1031                let d = humantime::parse_duration(rest).map_err(|_| invalid())?;
1032                // Reject anything below 1ms: downstream we truncate to whole
1033                // milliseconds, so e.g. `999us` would silently behave like
1034                // `0ms` and degenerate to "no staleness allowed", which is
1035                // exactly the case the zero-rejection meant to forbid.
1036                if d < std::time::Duration::from_millis(1) {
1037                    return Err(invalid());
1038                }
1039                Ok(Self::BoundedStaleness(d))
1040            }
1041        }
1042    }
1043
1044    fn box_clone(&self) -> Box<dyn Value> {
1045        Box::new(self.clone())
1046    }
1047
1048    fn format(&self) -> String {
1049        self.to_string()
1050    }
1051}
1052
1053impl From<TransactionIsolationLevel> for IsolationLevel {
1054    fn from(transaction_isolation_level: TransactionIsolationLevel) -> Self {
1055        match transaction_isolation_level {
1056            TransactionIsolationLevel::ReadUncommitted => Self::ReadUncommitted,
1057            TransactionIsolationLevel::ReadCommitted => Self::ReadCommitted,
1058            TransactionIsolationLevel::RepeatableRead => Self::RepeatableRead,
1059            TransactionIsolationLevel::Serializable => Self::Serializable,
1060            TransactionIsolationLevel::StrongSessionSerializable => Self::StrongSessionSerializable,
1061            TransactionIsolationLevel::StrictSerializable => Self::StrictSerializable,
1062        }
1063    }
1064}
1065
1066impl Value for CloneableEnvFilter {
1067    fn type_name() -> Cow<'static, str>
1068    where
1069        Self: Sized,
1070    {
1071        "EnvFilter".into()
1072    }
1073
1074    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1075    where
1076        Self: Sized,
1077    {
1078        let s = extract_single_value(input)?;
1079        CloneableEnvFilter::from_str(s).map_err(|e| VarParseError::InvalidParameterValue {
1080            invalid_values: vec![s.to_string()],
1081            reason: e.to_string(),
1082        })
1083    }
1084
1085    fn box_clone(&self) -> Box<dyn Value> {
1086        Box::new(self.clone())
1087    }
1088
1089    fn format(&self) -> String {
1090        self.to_string()
1091    }
1092}
1093
1094#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1095pub enum ClientEncoding {
1096    Utf8,
1097}
1098
1099impl ClientEncoding {
1100    fn as_str(&self) -> &'static str {
1101        match self {
1102            ClientEncoding::Utf8 => "UTF8",
1103        }
1104    }
1105
1106    fn valid_values() -> Vec<&'static str> {
1107        vec![ClientEncoding::Utf8.as_str()]
1108    }
1109}
1110
1111impl Value for ClientEncoding {
1112    fn type_name() -> Cow<'static, str>
1113    where
1114        Self: Sized,
1115    {
1116        "string".into()
1117    }
1118
1119    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1120    where
1121        Self: Sized,
1122    {
1123        let s = extract_single_value(input)?;
1124        let s = UncasedStr::new(s);
1125        if s == Self::Utf8.as_str() {
1126            Ok(Self::Utf8)
1127        } else {
1128            Err(VarParseError::ConstrainedParameter {
1129                invalid_values: vec![s.to_string()],
1130                valid_values: Some(ClientEncoding::valid_values()),
1131            })
1132        }
1133    }
1134
1135    fn box_clone(&self) -> Box<dyn Value> {
1136        Box::new(self.clone())
1137    }
1138
1139    fn format(&self) -> String {
1140        self.as_str().to_string()
1141    }
1142}
1143
1144#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1145pub enum IntervalStyle {
1146    Postgres,
1147}
1148
1149impl IntervalStyle {
1150    fn as_str(&self) -> &'static str {
1151        match self {
1152            IntervalStyle::Postgres => "postgres",
1153        }
1154    }
1155
1156    fn valid_values() -> Vec<&'static str> {
1157        vec![IntervalStyle::Postgres.as_str()]
1158    }
1159}
1160
1161impl Value for IntervalStyle {
1162    fn type_name() -> Cow<'static, str>
1163    where
1164        Self: Sized,
1165    {
1166        "string".into()
1167    }
1168
1169    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1170    where
1171        Self: Sized,
1172    {
1173        let s = extract_single_value(input)?;
1174        let s = UncasedStr::new(s);
1175        if s == Self::Postgres.as_str() {
1176            Ok(Self::Postgres)
1177        } else {
1178            Err(VarParseError::ConstrainedParameter {
1179                invalid_values: vec![s.to_string()],
1180                valid_values: Some(IntervalStyle::valid_values()),
1181            })
1182        }
1183    }
1184
1185    fn box_clone(&self) -> Box<dyn Value> {
1186        Box::new(self.clone())
1187    }
1188
1189    fn format(&self) -> String {
1190        self.as_str().to_string()
1191    }
1192}
1193
1194/// Macro to implement [`Value`] for simpler types, i.e. ones that already implement `FromStr` and
1195/// `ToString`.
1196///
1197/// Note: Macros can be hot garbage, if at any point folks think this is too complicated please
1198/// feel free to refactor!
1199macro_rules! impl_value_for_simple {
1200    ($t: ty, $name: literal) => {
1201        impl Value for $t {
1202            fn type_name() -> Cow<'static, str>
1203            where
1204                Self: Sized,
1205            {
1206                $name.into()
1207            }
1208
1209            fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1210            where
1211                Self: Sized,
1212            {
1213                let s = extract_single_value(input)?;
1214                s.parse::<Self>()
1215                    .map_err(|_| VarParseError::InvalidParameterType)
1216            }
1217
1218            fn box_clone(&self) -> Box<dyn Value> {
1219                Box::new(self.clone())
1220            }
1221
1222            fn format(&self) -> String {
1223                self.to_string()
1224            }
1225        }
1226    };
1227}
1228
1229impl_value_for_simple!(i32, "integer");
1230impl_value_for_simple!(u32, "unsigned integer");
1231impl_value_for_simple!(u64, "64-bit unsigned integer");
1232impl_value_for_simple!(usize, "unsigned integer");
1233impl_value_for_simple!(f64, "double-precision floating-point number");
1234
1235impl_value_for_simple!(NonZeroU32, "unsigned integer");
1236
1237impl_value_for_simple!(mz_repr::Timestamp, "mz-timestamp");
1238impl_value_for_simple!(mz_repr::bytes::ByteSize, "bytes");
1239impl_value_for_simple!(CompactionStyle, "rocksdb_compaction_style");
1240impl_value_for_simple!(CompressionType, "rocksdb_compression_type");
1241
1242#[cfg(test)]
1243mod tests {
1244    use mz_ore::assert_err;
1245
1246    use super::*;
1247
1248    #[mz_ore::test]
1249    fn test_value_duration() {
1250        fn inner(t: &'static str, e: Duration, expected_format: Option<&'static str>) {
1251            let d = Duration::parse(VarInput::Flat(t)).expect("invalid duration");
1252            assert_eq!(d, e);
1253            let mut d_format = d.format();
1254            d_format.retain(|c| !c.is_whitespace());
1255            if let Some(expected) = expected_format {
1256                assert_eq!(d_format, expected);
1257            } else {
1258                assert_eq!(
1259                    t.chars().filter(|c| !c.is_whitespace()).collect::<String>(),
1260                    d_format
1261                )
1262            }
1263        }
1264        inner("1", Duration::from_millis(1), Some("1ms"));
1265        inner("0", Duration::from_secs(0), Some("0s"));
1266        inner("1ms", Duration::from_millis(1), None);
1267        inner("1000ms", Duration::from_millis(1000), Some("1s"));
1268        inner("1001ms", Duration::from_millis(1001), None);
1269        inner("1us", Duration::from_micros(1), None);
1270        inner("1000us", Duration::from_micros(1000), Some("1ms"));
1271        inner("1s", Duration::from_secs(1), None);
1272        inner("60s", Duration::from_secs(60), Some("1min"));
1273        inner("3600s", Duration::from_secs(3600), Some("1h"));
1274        inner("3660s", Duration::from_secs(3660), Some("61min"));
1275        inner("1min", Duration::from_secs(1 * SEC_TO_MIN), None);
1276        inner("60min", Duration::from_secs(60 * SEC_TO_MIN), Some("1h"));
1277        inner("1h", Duration::from_secs(1 * SEC_TO_HOUR), None);
1278        inner("24h", Duration::from_secs(24 * SEC_TO_HOUR), Some("1d"));
1279        inner("1d", Duration::from_secs(1 * SEC_TO_DAY), None);
1280        inner("2d", Duration::from_secs(2 * SEC_TO_DAY), None);
1281        inner("  1   s ", Duration::from_secs(1), None);
1282        inner("1s ", Duration::from_secs(1), None);
1283        inner("   1s", Duration::from_secs(1), None);
1284        inner("0d", Duration::from_secs(0), Some("0s"));
1285        inner(
1286            "18446744073709551615",
1287            Duration::from_millis(u64::MAX),
1288            Some("18446744073709551615ms"),
1289        );
1290        inner(
1291            "18446744073709551615 s",
1292            Duration::from_secs(u64::MAX),
1293            Some("0"),
1294        );
1295
1296        fn errs(t: &'static str) {
1297            assert_err!(Duration::parse(VarInput::Flat(t)));
1298        }
1299        errs("1 m");
1300        errs("1 sec");
1301        errs("1 min 1 s");
1302        errs("1m1s");
1303        errs("1.1");
1304        errs("1.1 min");
1305        errs("-1 s");
1306        errs("");
1307        errs("   ");
1308        errs("x");
1309        errs("s");
1310        errs("18446744073709551615 min");
1311        // Unicode numerics such as '²' (U+00B2) are multi-byte and must not be
1312        // treated as parseable digits: their char index differs from their byte
1313        // offset, so slicing on them would land mid-character and panic.
1314        errs("²");
1315        errs("1²ms");
1316        errs("½");
1317        errs("1ms");
1318    }
1319
1320    #[mz_ore::test]
1321    fn test_value_ident_list() {
1322        fn sql_set(values: &[&str]) -> Result<Vec<Ident>, VarParseError> {
1323            let values: Vec<String> = values.iter().map(|v| v.to_string()).collect();
1324            Vec::<Ident>::parse(VarInput::SqlSet(&values))
1325        }
1326        fn flat(value: &str) -> Result<Vec<Ident>, VarParseError> {
1327            Vec::<Ident>::parse(VarInput::Flat(value))
1328        }
1329
1330        // The durable catalog stores `format()` and re-parses it with `Flat`.
1331        for input in [&[""][..], &["a", "b"], &["A b", "c\"d"]] {
1332            let parsed = sql_set(input).expect("valid input");
1333            let formatted = parsed.format();
1334            assert_eq!(
1335                flat(&formatted).ok(),
1336                Some(parsed),
1337                "{input:?} does not round-trip through {formatted:?}"
1338            );
1339        }
1340
1341        assert_eq!(sql_set(&[""]).ok(), Some(vec![]));
1342        assert_eq!(flat("''").ok(), Some(vec![]));
1343        assert_err!(sql_set(&["a", ""]));
1344        assert_err!(flat("a, ''"));
1345    }
1346
1347    #[mz_ore::test]
1348    fn test_should_output_to_client() {
1349        #[rustfmt::skip]
1350        let test_cases = [
1351            (ClientSeverity::Debug1, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1352            (ClientSeverity::Debug2, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1353            (ClientSeverity::Debug3, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1354            (ClientSeverity::Debug4, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1355            (ClientSeverity::Debug5, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1356            (ClientSeverity::Log, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1357            (ClientSeverity::Log, vec![Severity::Debug], false),
1358            (ClientSeverity::Info, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1359            (ClientSeverity::Info, vec![Severity::Debug, Severity::Log], false),
1360            (ClientSeverity::Notice, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1361            (ClientSeverity::Notice, vec![Severity::Debug, Severity::Log], false),
1362            (ClientSeverity::Warning, vec![Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1363            (ClientSeverity::Warning, vec![Severity::Debug, Severity::Log, Severity::Notice], false),
1364            (ClientSeverity::Error, vec![Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1365            (ClientSeverity::Error, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning], false),
1366        ];
1367
1368        for test_case in test_cases {
1369            run_test(test_case)
1370        }
1371
1372        fn run_test(test_case: (ClientSeverity, Vec<Severity>, bool)) {
1373            let client_min_messages_setting = test_case.0;
1374            let expected = test_case.2;
1375            for message_severity in test_case.1 {
1376                assert!(
1377                    client_min_messages_setting.should_output_to_client(&message_severity)
1378                        == expected
1379                )
1380            }
1381        }
1382    }
1383}
1384
1385#[cfg(test)]
1386mod bounded_staleness_tests {
1387    use super::*;
1388    use std::time::Duration;
1389
1390    fn parse_iso(s: &str) -> Result<IsolationLevel, VarParseError> {
1391        IsolationLevel::parse(VarInput::Flat(s))
1392    }
1393
1394    #[mz_ore::test]
1395    fn parses_bounded_staleness() {
1396        assert_eq!(
1397            parse_iso("bounded staleness 5s").unwrap(),
1398            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1399        );
1400        assert_eq!(
1401            parse_iso("bounded staleness 500ms").unwrap(),
1402            IsolationLevel::BoundedStaleness(Duration::from_millis(500))
1403        );
1404        // Upper-case input normalises.
1405        assert_eq!(
1406            parse_iso("BOUNDED STALENESS 5s").unwrap(),
1407            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1408        );
1409        // Leading/trailing whitespace tolerated.
1410        assert_eq!(
1411            parse_iso("  bounded staleness 5s  ").unwrap(),
1412            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1413        );
1414    }
1415
1416    #[mz_ore::test]
1417    fn rejects_zero_duration() {
1418        assert!(parse_iso("bounded staleness 0s").is_err());
1419        assert!(parse_iso("bounded staleness 0ms").is_err());
1420    }
1421
1422    #[mz_ore::test]
1423    fn rejects_sub_millisecond_duration() {
1424        // Truncated to 0ms downstream; treated as the same degenerate case as
1425        // a literal zero.
1426        assert!(parse_iso("bounded staleness 1us").is_err());
1427        assert!(parse_iso("bounded staleness 999us").is_err());
1428        assert!(parse_iso("bounded staleness 999ns").is_err());
1429    }
1430
1431    #[mz_ore::test]
1432    fn parses_multi_component_duration() {
1433        // `1m30s` round-trips through humantime which formats as `1m 30s`,
1434        // so the parser must accept the space-separated form.
1435        let lvl = parse_iso("bounded staleness 1m30s").unwrap();
1436        assert_eq!(
1437            lvl,
1438            IsolationLevel::BoundedStaleness(Duration::from_secs(90))
1439        );
1440        let formatted = lvl.format();
1441        assert_eq!(parse_iso(&formatted).unwrap(), lvl);
1442    }
1443
1444    #[mz_ore::test]
1445    fn rejects_unparseable_duration() {
1446        assert!(parse_iso("bounded staleness banana").is_err());
1447        assert!(parse_iso("bounded staleness").is_err());
1448        // Whitespace between the keyword and the duration is required.
1449        assert!(parse_iso("bounded staleness5s").is_err());
1450    }
1451
1452    #[mz_ore::test]
1453    fn round_trips_format() {
1454        let lvl = IsolationLevel::BoundedStaleness(Duration::from_secs(5));
1455        assert_eq!(lvl.format(), "bounded staleness 5s");
1456        let parsed = parse_iso(&lvl.format()).unwrap();
1457        assert_eq!(parsed, lvl);
1458    }
1459
1460    #[mz_ore::test]
1461    fn accepts_long_durations() {
1462        // No upper cap; a multi-hour bound is a perfectly valid (if loose)
1463        // staleness contract.
1464        assert!(parse_iso("bounded staleness 1h").is_ok());
1465        assert!(parse_iso("bounded staleness 24h").is_ok());
1466    }
1467}