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            fail::cfg(failpoint, action).map_err(|e| VarParseError::InvalidParameterValue {
605                invalid_values: input.to_vec(),
606                reason: e.to_string(),
607            })?;
608        }
609
610        Ok(Failpoints)
611    }
612
613    fn box_clone(&self) -> Box<dyn Value> {
614        Box::new(self.clone())
615    }
616
617    fn format(&self) -> String {
618        "<omitted>".to_string()
619    }
620}
621
622/// Severity levels can used to be used to filter which messages get sent
623/// to a client.
624///
625/// The ordering of severity levels used for client-level filtering differs from the
626/// one used for server-side logging in two aspects: INFO messages are always sent,
627/// and the LOG severity is considered as below NOTICE, while it is above ERROR for
628/// server-side logs.
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
630pub enum ClientSeverity {
631    /// Sends only INFO, ERROR, FATAL and PANIC level messages.
632    Error,
633    /// Sends only WARNING, INFO, ERROR, FATAL and PANIC level messages.
634    Warning,
635    /// Sends only NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
636    Notice,
637    /// Sends only LOG, NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
638    Log,
639    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
640    Debug1,
641    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
642    Debug2,
643    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
644    Debug3,
645    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
646    Debug4,
647    /// Sends all messages to the client, since all DEBUG levels are treated as the same right now.
648    Debug5,
649    /// Sends only NOTICE, WARNING, INFO, ERROR, FATAL and PANIC level messages.
650    /// Not listed as a valid value, but accepted by Postgres
651    Info,
652}
653
654impl Serialize for ClientSeverity {
655    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
656    where
657        S: serde::Serializer,
658    {
659        serializer.serialize_str(self.as_str())
660    }
661}
662
663impl ClientSeverity {
664    fn as_str(&self) -> &'static str {
665        match self {
666            ClientSeverity::Error => "error",
667            ClientSeverity::Warning => "warning",
668            ClientSeverity::Notice => "notice",
669            ClientSeverity::Info => "info",
670            ClientSeverity::Log => "log",
671            ClientSeverity::Debug1 => "debug1",
672            ClientSeverity::Debug2 => "debug2",
673            ClientSeverity::Debug3 => "debug3",
674            ClientSeverity::Debug4 => "debug4",
675            ClientSeverity::Debug5 => "debug5",
676        }
677    }
678
679    fn valid_values() -> Vec<&'static str> {
680        // INFO left intentionally out, to match Postgres
681        vec![
682            ClientSeverity::Debug5.as_str(),
683            ClientSeverity::Debug4.as_str(),
684            ClientSeverity::Debug3.as_str(),
685            ClientSeverity::Debug2.as_str(),
686            ClientSeverity::Debug1.as_str(),
687            ClientSeverity::Log.as_str(),
688            ClientSeverity::Notice.as_str(),
689            ClientSeverity::Warning.as_str(),
690            ClientSeverity::Error.as_str(),
691        ]
692    }
693
694    /// Checks if a message of a given severity level should be sent to a client.
695    ///
696    /// The ordering of severity levels used for client-level filtering differs from the
697    /// one used for server-side logging in two aspects: INFO messages are always sent,
698    /// and the LOG severity is considered as below NOTICE, while it is above ERROR for
699    /// server-side logs.
700    ///
701    /// Postgres only considers the session setting after the client authentication
702    /// handshake is completed. Since this function is only called after client authentication
703    /// is done, we are not treating this case right now, but be aware if refactoring it.
704    pub fn should_output_to_client(&self, severity: &Severity) -> bool {
705        match (self, severity) {
706            // INFO messages are always sent
707            (_, Severity::Info) => true,
708            (ClientSeverity::Error, Severity::Error | Severity::Fatal | Severity::Panic) => true,
709            (
710                ClientSeverity::Warning,
711                Severity::Error | Severity::Fatal | Severity::Panic | Severity::Warning,
712            ) => true,
713            (
714                ClientSeverity::Notice,
715                Severity::Error
716                | Severity::Fatal
717                | Severity::Panic
718                | Severity::Warning
719                | Severity::Notice,
720            ) => true,
721            (
722                ClientSeverity::Info,
723                Severity::Error
724                | Severity::Fatal
725                | Severity::Panic
726                | Severity::Warning
727                | Severity::Notice,
728            ) => true,
729            (
730                ClientSeverity::Log,
731                Severity::Error
732                | Severity::Fatal
733                | Severity::Panic
734                | Severity::Warning
735                | Severity::Notice
736                | Severity::Log,
737            ) => true,
738            (
739                ClientSeverity::Debug1
740                | ClientSeverity::Debug2
741                | ClientSeverity::Debug3
742                | ClientSeverity::Debug4
743                | ClientSeverity::Debug5,
744                _,
745            ) => true,
746
747            (
748                ClientSeverity::Error,
749                Severity::Warning | Severity::Notice | Severity::Log | Severity::Debug,
750            ) => false,
751            (ClientSeverity::Warning, Severity::Notice | Severity::Log | Severity::Debug) => false,
752            (ClientSeverity::Notice, Severity::Log | Severity::Debug) => false,
753            (ClientSeverity::Info, Severity::Log | Severity::Debug) => false,
754            (ClientSeverity::Log, Severity::Debug) => false,
755        }
756    }
757}
758
759impl Value for ClientSeverity {
760    fn type_name() -> Cow<'static, str>
761    where
762        Self: Sized,
763    {
764        "string".into()
765    }
766
767    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
768    where
769        Self: Sized,
770    {
771        let s = extract_single_value(input)?;
772        let s = UncasedStr::new(s);
773
774        if s == ClientSeverity::Error.as_str() {
775            Ok(ClientSeverity::Error)
776        } else if s == ClientSeverity::Warning.as_str() {
777            Ok(ClientSeverity::Warning)
778        } else if s == ClientSeverity::Notice.as_str() {
779            Ok(ClientSeverity::Notice)
780        } else if s == ClientSeverity::Info.as_str() {
781            Ok(ClientSeverity::Info)
782        } else if s == ClientSeverity::Log.as_str() {
783            Ok(ClientSeverity::Log)
784        } else if s == ClientSeverity::Debug1.as_str() {
785            Ok(ClientSeverity::Debug1)
786        // Postgres treats `debug` as an input as equivalent to `debug2`
787        } else if s == ClientSeverity::Debug2.as_str() || s == "debug" {
788            Ok(ClientSeverity::Debug2)
789        } else if s == ClientSeverity::Debug3.as_str() {
790            Ok(ClientSeverity::Debug3)
791        } else if s == ClientSeverity::Debug4.as_str() {
792            Ok(ClientSeverity::Debug4)
793        } else if s == ClientSeverity::Debug5.as_str() {
794            Ok(ClientSeverity::Debug5)
795        } else {
796            Err(VarParseError::ConstrainedParameter {
797                invalid_values: input.to_vec(),
798                valid_values: Some(ClientSeverity::valid_values()),
799            })
800        }
801    }
802
803    fn box_clone(&self) -> Box<dyn Value> {
804        Box::new(self.clone())
805    }
806
807    fn format(&self) -> String {
808        self.as_str().into()
809    }
810}
811
812/// List of valid time zones.
813///
814/// Names are following the tz database, but only time zones equivalent
815/// to UTC±00:00 are supported.
816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
817pub enum TimeZone {
818    /// UTC
819    UTC,
820    /// GMT
821    GMT,
822    /// Fixed offset from UTC, currently only "+00:00" is supported.
823    /// A string representation is kept here for compatibility with Postgres.
824    FixedOffset(&'static str),
825}
826
827impl TimeZone {
828    fn as_str(&self) -> &'static str {
829        match self {
830            TimeZone::UTC => "UTC",
831            TimeZone::GMT => "GMT",
832            TimeZone::FixedOffset(s) => s,
833        }
834    }
835}
836
837impl Value for TimeZone {
838    fn type_name() -> Cow<'static, str>
839    where
840        Self: Sized,
841    {
842        // TODO(parkmycar): It seems like we should change this?
843        "string".into()
844    }
845
846    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
847    where
848        Self: Sized,
849    {
850        let s = extract_single_value(input)?;
851        let s = UncasedStr::new(s);
852
853        if s == TimeZone::UTC.as_str() {
854            Ok(TimeZone::UTC)
855        } else if s == TimeZone::GMT.as_str() {
856            Ok(TimeZone::GMT)
857        } else if s == "+00:00" {
858            Ok(TimeZone::FixedOffset("+00:00"))
859        } else {
860            Err(VarParseError::ConstrainedParameter {
861                invalid_values: input.to_vec(),
862                valid_values: None,
863            })
864        }
865    }
866
867    fn box_clone(&self) -> Box<dyn Value> {
868        Box::new(self.clone())
869    }
870
871    fn format(&self) -> String {
872        self.as_str().into()
873    }
874}
875
876/// List of valid isolation levels.
877#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
878pub enum IsolationLevel {
879    ReadUncommitted,
880    ReadCommitted,
881    RepeatableRead,
882    Serializable,
883    /* TODO(jkosh44) Move this comment to user facing docs when this isolation level becomes available to users.
884     * The Strong Session Serializable isolation level combines the Serializable isolation level
885     * (https://jepsen.io/consistency/models/serializable) with the Sequential consistency model
886     * (https://jepsen.io/consistency/models/sequential). See
887     * http://dbmsmusings.blogspot.com/2019/06/correctness-anomalies-under.html and
888     * https://cs.uwaterloo.ca/~kmsalem/pubs/DaudjeeICDE04.pdf. Operations within a single session
889     * are linearizable, but operations across sessions are not linearizable.
890     *
891     * Operations in sessions that use Strong Session Serializable are not linearizable with
892     * operations in sessions that use Strict Serializable. For example, consider the following
893     * sequence of events in order:
894     *
895     *   1. Session s0 executes read at timestamp t0 under Strong Session Serializable.
896     *   2. Session s1 executes read at timestamp t1 under Strict Serializable.
897     *
898     * If t0 > t1, then this is not considered a consistency violation. This matches with the
899     * semantics of Serializable, which can execute queries arbitrarily in the future without
900     * violating the consistency of Strict Serializable queries.
901     *
902     * All operations within a session that use Strong Session Serializable are only
903     * linearizable within operations within the same session that also use Strong Session
904     * Serializable. For example, consider the following sequence of events in order:
905     *
906     *   1. Session s0 executes read at timestamp t0 under Strong Session Serializable.
907     *   2. Session s0 executes read at timestamp t1 under I.
908     *
909     * If I is Strong Session Serializable then t0 > t1 is guaranteed. If I is any other isolation
910     * level then t0 < t1 is not considered a consistency violation. This matches the semantics of
911     * Serializable, which can execute queries arbitrarily in the future without violating the
912     * consistency of Strict Serializable queries within the same session.
913     *
914     * The items left TODO before this is considered ready for prod are:
915     *
916     * - Add more tests.
917     * - Linearize writes to system tables under this isolation (most of these are the side effect
918     *   of some DDL).
919     */
920    StrongSessionSerializable,
921    StrictSerializable,
922    /// Bounded staleness — pick `T` such that `T >= now_ms - D` and the
923    /// query does not wait on input frontiers. Errors if no such `T` exists
924    /// in the no-wait window. See
925    /// `doc/developer/design/20260429_bounded_staleness_isolation.md`.
926    BoundedStaleness(std::time::Duration),
927}
928
929impl IsolationLevel {
930    const READ_UNCOMMITTED: &'static str = "read uncommitted";
931    const READ_COMMITTED: &'static str = "read committed";
932    const REPEATABLE_READ: &'static str = "repeatable read";
933    const SERIALIZABLE: &'static str = "serializable";
934    const STRONG_SESSION_SERIALIZABLE: &'static str = "strong session serializable";
935    const STRICT_SERIALIZABLE: &'static str = "strict serializable";
936    const BOUNDED_STALENESS: &'static str = "bounded staleness";
937    const BOUNDED_STALENESS_HINT: &'static str = "bounded staleness <duration>";
938
939    /// Returns true if the isolation level is of bounded staleness.
940    pub fn is_bounded_staleness(&self) -> bool {
941        matches!(self, Self::BoundedStaleness(_))
942    }
943
944    /// Unit-cardinality variant identifier, suitable for Prometheus labels and
945    /// equality checks against parsed input. The duration on `BoundedStaleness`
946    /// is omitted; use [`fmt::Display`] for the user-facing rendering.
947    pub fn as_variant_str(&self) -> &'static str {
948        match self {
949            Self::ReadUncommitted => Self::READ_UNCOMMITTED,
950            Self::ReadCommitted => Self::READ_COMMITTED,
951            Self::RepeatableRead => Self::REPEATABLE_READ,
952            Self::Serializable => Self::SERIALIZABLE,
953            Self::StrongSessionSerializable => Self::STRONG_SESSION_SERIALIZABLE,
954            Self::StrictSerializable => Self::STRICT_SERIALIZABLE,
955            Self::BoundedStaleness(_) => Self::BOUNDED_STALENESS,
956        }
957    }
958
959    fn valid_values() -> Vec<&'static str> {
960        vec![
961            Self::READ_UNCOMMITTED,
962            Self::READ_COMMITTED,
963            Self::REPEATABLE_READ,
964            Self::SERIALIZABLE,
965            // TODO(jkosh44) Add STRONG_SESSION_SERIALIZABLE when it becomes available to users.
966            Self::STRICT_SERIALIZABLE,
967            Self::BOUNDED_STALENESS_HINT,
968        ]
969    }
970}
971
972impl fmt::Display for IsolationLevel {
973    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
974        match self {
975            Self::BoundedStaleness(d) => write!(
976                f,
977                "{} {}",
978                Self::BOUNDED_STALENESS,
979                humantime::format_duration(*d)
980            ),
981            other => f.write_str(other.as_variant_str()),
982        }
983    }
984}
985
986impl Value for IsolationLevel {
987    fn type_name() -> Cow<'static, str>
988    where
989        Self: Sized,
990    {
991        "string".into()
992    }
993
994    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
995    where
996        Self: Sized,
997    {
998        let invalid = || VarParseError::ConstrainedParameter {
999            invalid_values: input.to_vec(),
1000            valid_values: Some(IsolationLevel::valid_values()),
1001        };
1002
1003        let s = extract_single_value(input)?;
1004        let lower = s.to_ascii_lowercase();
1005        let lower = lower.trim();
1006
1007        match lower {
1008            // Weak isolations all upgrade to Serializable.
1009            Self::READ_UNCOMMITTED
1010            | Self::READ_COMMITTED
1011            | Self::REPEATABLE_READ
1012            | Self::SERIALIZABLE => Ok(Self::Serializable),
1013            Self::STRONG_SESSION_SERIALIZABLE => Ok(Self::StrongSessionSerializable),
1014            Self::STRICT_SERIALIZABLE => Ok(Self::StrictSerializable),
1015            other => {
1016                let rest = other
1017                    .strip_prefix(Self::BOUNDED_STALENESS)
1018                    .ok_or_else(invalid)?;
1019                // Require whitespace between the keyword and the duration, so
1020                // e.g. `bounded staleness5s` is rejected.
1021                if !rest.starts_with(char::is_whitespace) {
1022                    return Err(invalid());
1023                }
1024                let rest = rest.trim();
1025                if rest.is_empty() {
1026                    return Err(invalid());
1027                }
1028                let d = humantime::parse_duration(rest).map_err(|_| invalid())?;
1029                // Reject anything below 1ms: downstream we truncate to whole
1030                // milliseconds, so e.g. `999us` would silently behave like
1031                // `0ms` and degenerate to "no staleness allowed", which is
1032                // exactly the case the zero-rejection meant to forbid.
1033                if d < std::time::Duration::from_millis(1) {
1034                    return Err(invalid());
1035                }
1036                Ok(Self::BoundedStaleness(d))
1037            }
1038        }
1039    }
1040
1041    fn box_clone(&self) -> Box<dyn Value> {
1042        Box::new(self.clone())
1043    }
1044
1045    fn format(&self) -> String {
1046        self.to_string()
1047    }
1048}
1049
1050impl From<TransactionIsolationLevel> for IsolationLevel {
1051    fn from(transaction_isolation_level: TransactionIsolationLevel) -> Self {
1052        match transaction_isolation_level {
1053            TransactionIsolationLevel::ReadUncommitted => Self::ReadUncommitted,
1054            TransactionIsolationLevel::ReadCommitted => Self::ReadCommitted,
1055            TransactionIsolationLevel::RepeatableRead => Self::RepeatableRead,
1056            TransactionIsolationLevel::Serializable => Self::Serializable,
1057            TransactionIsolationLevel::StrongSessionSerializable => Self::StrongSessionSerializable,
1058            TransactionIsolationLevel::StrictSerializable => Self::StrictSerializable,
1059        }
1060    }
1061}
1062
1063impl Value for CloneableEnvFilter {
1064    fn type_name() -> Cow<'static, str>
1065    where
1066        Self: Sized,
1067    {
1068        "EnvFilter".into()
1069    }
1070
1071    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1072    where
1073        Self: Sized,
1074    {
1075        let s = extract_single_value(input)?;
1076        CloneableEnvFilter::from_str(s).map_err(|e| VarParseError::InvalidParameterValue {
1077            invalid_values: vec![s.to_string()],
1078            reason: e.to_string(),
1079        })
1080    }
1081
1082    fn box_clone(&self) -> Box<dyn Value> {
1083        Box::new(self.clone())
1084    }
1085
1086    fn format(&self) -> String {
1087        self.to_string()
1088    }
1089}
1090
1091#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1092pub enum ClientEncoding {
1093    Utf8,
1094}
1095
1096impl ClientEncoding {
1097    fn as_str(&self) -> &'static str {
1098        match self {
1099            ClientEncoding::Utf8 => "UTF8",
1100        }
1101    }
1102
1103    fn valid_values() -> Vec<&'static str> {
1104        vec![ClientEncoding::Utf8.as_str()]
1105    }
1106}
1107
1108impl Value for ClientEncoding {
1109    fn type_name() -> Cow<'static, str>
1110    where
1111        Self: Sized,
1112    {
1113        "string".into()
1114    }
1115
1116    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1117    where
1118        Self: Sized,
1119    {
1120        let s = extract_single_value(input)?;
1121        let s = UncasedStr::new(s);
1122        if s == Self::Utf8.as_str() {
1123            Ok(Self::Utf8)
1124        } else {
1125            Err(VarParseError::ConstrainedParameter {
1126                invalid_values: vec![s.to_string()],
1127                valid_values: Some(ClientEncoding::valid_values()),
1128            })
1129        }
1130    }
1131
1132    fn box_clone(&self) -> Box<dyn Value> {
1133        Box::new(self.clone())
1134    }
1135
1136    fn format(&self) -> String {
1137        self.as_str().to_string()
1138    }
1139}
1140
1141#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1142pub enum IntervalStyle {
1143    Postgres,
1144}
1145
1146impl IntervalStyle {
1147    fn as_str(&self) -> &'static str {
1148        match self {
1149            IntervalStyle::Postgres => "postgres",
1150        }
1151    }
1152
1153    fn valid_values() -> Vec<&'static str> {
1154        vec![IntervalStyle::Postgres.as_str()]
1155    }
1156}
1157
1158impl Value for IntervalStyle {
1159    fn type_name() -> Cow<'static, str>
1160    where
1161        Self: Sized,
1162    {
1163        "string".into()
1164    }
1165
1166    fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1167    where
1168        Self: Sized,
1169    {
1170        let s = extract_single_value(input)?;
1171        let s = UncasedStr::new(s);
1172        if s == Self::Postgres.as_str() {
1173            Ok(Self::Postgres)
1174        } else {
1175            Err(VarParseError::ConstrainedParameter {
1176                invalid_values: vec![s.to_string()],
1177                valid_values: Some(IntervalStyle::valid_values()),
1178            })
1179        }
1180    }
1181
1182    fn box_clone(&self) -> Box<dyn Value> {
1183        Box::new(self.clone())
1184    }
1185
1186    fn format(&self) -> String {
1187        self.as_str().to_string()
1188    }
1189}
1190
1191/// Macro to implement [`Value`] for simpler types, i.e. ones that already implement `FromStr` and
1192/// `ToString`.
1193///
1194/// Note: Macros can be hot garbage, if at any point folks think this is too complicated please
1195/// feel free to refactor!
1196macro_rules! impl_value_for_simple {
1197    ($t: ty, $name: literal) => {
1198        impl Value for $t {
1199            fn type_name() -> Cow<'static, str>
1200            where
1201                Self: Sized,
1202            {
1203                $name.into()
1204            }
1205
1206            fn parse(input: VarInput<'_>) -> Result<Self, VarParseError>
1207            where
1208                Self: Sized,
1209            {
1210                let s = extract_single_value(input)?;
1211                s.parse::<Self>()
1212                    .map_err(|_| VarParseError::InvalidParameterType)
1213            }
1214
1215            fn box_clone(&self) -> Box<dyn Value> {
1216                Box::new(self.clone())
1217            }
1218
1219            fn format(&self) -> String {
1220                self.to_string()
1221            }
1222        }
1223    };
1224}
1225
1226impl_value_for_simple!(i32, "integer");
1227impl_value_for_simple!(u32, "unsigned integer");
1228impl_value_for_simple!(u64, "64-bit unsigned integer");
1229impl_value_for_simple!(usize, "unsigned integer");
1230impl_value_for_simple!(f64, "double-precision floating-point number");
1231
1232impl_value_for_simple!(NonZeroU32, "unsigned integer");
1233
1234impl_value_for_simple!(mz_repr::Timestamp, "mz-timestamp");
1235impl_value_for_simple!(mz_repr::bytes::ByteSize, "bytes");
1236impl_value_for_simple!(CompactionStyle, "rocksdb_compaction_style");
1237impl_value_for_simple!(CompressionType, "rocksdb_compression_type");
1238
1239#[cfg(test)]
1240mod tests {
1241    use mz_ore::assert_err;
1242
1243    use super::*;
1244
1245    #[mz_ore::test]
1246    fn test_value_duration() {
1247        fn inner(t: &'static str, e: Duration, expected_format: Option<&'static str>) {
1248            let d = Duration::parse(VarInput::Flat(t)).expect("invalid duration");
1249            assert_eq!(d, e);
1250            let mut d_format = d.format();
1251            d_format.retain(|c| !c.is_whitespace());
1252            if let Some(expected) = expected_format {
1253                assert_eq!(d_format, expected);
1254            } else {
1255                assert_eq!(
1256                    t.chars().filter(|c| !c.is_whitespace()).collect::<String>(),
1257                    d_format
1258                )
1259            }
1260        }
1261        inner("1", Duration::from_millis(1), Some("1ms"));
1262        inner("0", Duration::from_secs(0), Some("0s"));
1263        inner("1ms", Duration::from_millis(1), None);
1264        inner("1000ms", Duration::from_millis(1000), Some("1s"));
1265        inner("1001ms", Duration::from_millis(1001), None);
1266        inner("1us", Duration::from_micros(1), None);
1267        inner("1000us", Duration::from_micros(1000), Some("1ms"));
1268        inner("1s", Duration::from_secs(1), None);
1269        inner("60s", Duration::from_secs(60), Some("1min"));
1270        inner("3600s", Duration::from_secs(3600), Some("1h"));
1271        inner("3660s", Duration::from_secs(3660), Some("61min"));
1272        inner("1min", Duration::from_secs(1 * SEC_TO_MIN), None);
1273        inner("60min", Duration::from_secs(60 * SEC_TO_MIN), Some("1h"));
1274        inner("1h", Duration::from_secs(1 * SEC_TO_HOUR), None);
1275        inner("24h", Duration::from_secs(24 * SEC_TO_HOUR), Some("1d"));
1276        inner("1d", Duration::from_secs(1 * SEC_TO_DAY), None);
1277        inner("2d", Duration::from_secs(2 * SEC_TO_DAY), None);
1278        inner("  1   s ", Duration::from_secs(1), None);
1279        inner("1s ", Duration::from_secs(1), None);
1280        inner("   1s", Duration::from_secs(1), None);
1281        inner("0d", Duration::from_secs(0), Some("0s"));
1282        inner(
1283            "18446744073709551615",
1284            Duration::from_millis(u64::MAX),
1285            Some("18446744073709551615ms"),
1286        );
1287        inner(
1288            "18446744073709551615 s",
1289            Duration::from_secs(u64::MAX),
1290            Some("0"),
1291        );
1292
1293        fn errs(t: &'static str) {
1294            assert_err!(Duration::parse(VarInput::Flat(t)));
1295        }
1296        errs("1 m");
1297        errs("1 sec");
1298        errs("1 min 1 s");
1299        errs("1m1s");
1300        errs("1.1");
1301        errs("1.1 min");
1302        errs("-1 s");
1303        errs("");
1304        errs("   ");
1305        errs("x");
1306        errs("s");
1307        errs("18446744073709551615 min");
1308        // Unicode numerics such as '²' (U+00B2) are multi-byte and must not be
1309        // treated as parseable digits: their char index differs from their byte
1310        // offset, so slicing on them would land mid-character and panic.
1311        errs("²");
1312        errs("1²ms");
1313        errs("½");
1314        errs("1ms");
1315    }
1316
1317    #[mz_ore::test]
1318    fn test_value_ident_list() {
1319        fn sql_set(values: &[&str]) -> Result<Vec<Ident>, VarParseError> {
1320            let values: Vec<String> = values.iter().map(|v| v.to_string()).collect();
1321            Vec::<Ident>::parse(VarInput::SqlSet(&values))
1322        }
1323        fn flat(value: &str) -> Result<Vec<Ident>, VarParseError> {
1324            Vec::<Ident>::parse(VarInput::Flat(value))
1325        }
1326
1327        // The durable catalog stores `format()` and re-parses it with `Flat`.
1328        for input in [&[""][..], &["a", "b"], &["A b", "c\"d"]] {
1329            let parsed = sql_set(input).expect("valid input");
1330            let formatted = parsed.format();
1331            assert_eq!(
1332                flat(&formatted).ok(),
1333                Some(parsed),
1334                "{input:?} does not round-trip through {formatted:?}"
1335            );
1336        }
1337
1338        assert_eq!(sql_set(&[""]).ok(), Some(vec![]));
1339        assert_eq!(flat("''").ok(), Some(vec![]));
1340        assert_err!(sql_set(&["a", ""]));
1341        assert_err!(flat("a, ''"));
1342    }
1343
1344    #[mz_ore::test]
1345    fn test_should_output_to_client() {
1346        #[rustfmt::skip]
1347        let test_cases = [
1348            (ClientSeverity::Debug1, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1349            (ClientSeverity::Debug2, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1350            (ClientSeverity::Debug3, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1351            (ClientSeverity::Debug4, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1352            (ClientSeverity::Debug5, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1353            (ClientSeverity::Log, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1354            (ClientSeverity::Log, vec![Severity::Debug], false),
1355            (ClientSeverity::Info, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1356            (ClientSeverity::Info, vec![Severity::Debug, Severity::Log], false),
1357            (ClientSeverity::Notice, vec![Severity::Notice, Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1358            (ClientSeverity::Notice, vec![Severity::Debug, Severity::Log], false),
1359            (ClientSeverity::Warning, vec![Severity::Warning, Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1360            (ClientSeverity::Warning, vec![Severity::Debug, Severity::Log, Severity::Notice], false),
1361            (ClientSeverity::Error, vec![Severity::Error, Severity::Fatal, Severity:: Panic, Severity::Info], true),
1362            (ClientSeverity::Error, vec![Severity::Debug, Severity::Log, Severity::Notice, Severity::Warning], false),
1363        ];
1364
1365        for test_case in test_cases {
1366            run_test(test_case)
1367        }
1368
1369        fn run_test(test_case: (ClientSeverity, Vec<Severity>, bool)) {
1370            let client_min_messages_setting = test_case.0;
1371            let expected = test_case.2;
1372            for message_severity in test_case.1 {
1373                assert!(
1374                    client_min_messages_setting.should_output_to_client(&message_severity)
1375                        == expected
1376                )
1377            }
1378        }
1379    }
1380}
1381
1382#[cfg(test)]
1383mod bounded_staleness_tests {
1384    use super::*;
1385    use std::time::Duration;
1386
1387    fn parse_iso(s: &str) -> Result<IsolationLevel, VarParseError> {
1388        IsolationLevel::parse(VarInput::Flat(s))
1389    }
1390
1391    #[mz_ore::test]
1392    fn parses_bounded_staleness() {
1393        assert_eq!(
1394            parse_iso("bounded staleness 5s").unwrap(),
1395            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1396        );
1397        assert_eq!(
1398            parse_iso("bounded staleness 500ms").unwrap(),
1399            IsolationLevel::BoundedStaleness(Duration::from_millis(500))
1400        );
1401        // Upper-case input normalises.
1402        assert_eq!(
1403            parse_iso("BOUNDED STALENESS 5s").unwrap(),
1404            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1405        );
1406        // Leading/trailing whitespace tolerated.
1407        assert_eq!(
1408            parse_iso("  bounded staleness 5s  ").unwrap(),
1409            IsolationLevel::BoundedStaleness(Duration::from_secs(5))
1410        );
1411    }
1412
1413    #[mz_ore::test]
1414    fn rejects_zero_duration() {
1415        assert!(parse_iso("bounded staleness 0s").is_err());
1416        assert!(parse_iso("bounded staleness 0ms").is_err());
1417    }
1418
1419    #[mz_ore::test]
1420    fn rejects_sub_millisecond_duration() {
1421        // Truncated to 0ms downstream; treated as the same degenerate case as
1422        // a literal zero.
1423        assert!(parse_iso("bounded staleness 1us").is_err());
1424        assert!(parse_iso("bounded staleness 999us").is_err());
1425        assert!(parse_iso("bounded staleness 999ns").is_err());
1426    }
1427
1428    #[mz_ore::test]
1429    fn parses_multi_component_duration() {
1430        // `1m30s` round-trips through humantime which formats as `1m 30s`,
1431        // so the parser must accept the space-separated form.
1432        let lvl = parse_iso("bounded staleness 1m30s").unwrap();
1433        assert_eq!(
1434            lvl,
1435            IsolationLevel::BoundedStaleness(Duration::from_secs(90))
1436        );
1437        let formatted = lvl.format();
1438        assert_eq!(parse_iso(&formatted).unwrap(), lvl);
1439    }
1440
1441    #[mz_ore::test]
1442    fn rejects_unparseable_duration() {
1443        assert!(parse_iso("bounded staleness banana").is_err());
1444        assert!(parse_iso("bounded staleness").is_err());
1445        // Whitespace between the keyword and the duration is required.
1446        assert!(parse_iso("bounded staleness5s").is_err());
1447    }
1448
1449    #[mz_ore::test]
1450    fn round_trips_format() {
1451        let lvl = IsolationLevel::BoundedStaleness(Duration::from_secs(5));
1452        assert_eq!(lvl.format(), "bounded staleness 5s");
1453        let parsed = parse_iso(&lvl.format()).unwrap();
1454        assert_eq!(parsed, lvl);
1455    }
1456
1457    #[mz_ore::test]
1458    fn accepts_long_durations() {
1459        // No upper cap; a multi-hour bound is a perfectly valid (if loose)
1460        // staleness contract.
1461        assert!(parse_iso("bounded staleness 1h").is_ok());
1462        assert!(parse_iso("bounded staleness 24h").is_ok());
1463    }
1464}