mz_repr/adt/
varchar.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::error::Error;
11use std::fmt;
12
13use anyhow::bail;
14use mz_lowertest::MzReflect;
15use mz_ore::cast::CastFrom;
16use mz_proto::{RustType, TryFromProtoError};
17use proptest::arbitrary::Arbitrary;
18use proptest::strategy::{BoxedStrategy, Strategy};
19use serde::{Deserialize, Serialize};
20
21include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.varchar.rs"));
22
23// https://github.com/postgres/postgres/blob/REL_14_0/src/include/access/htup_details.h#L577-L584
24pub const MAX_MAX_LENGTH: u32 = 10_485_760;
25
26/// A marker type indicating that a Rust string should be interpreted as a
27/// [`ScalarType::VarChar`].
28///
29/// [`ScalarType::VarChar`]: crate::ScalarType::VarChar
30#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
31pub struct VarChar<S: AsRef<str>>(pub S);
32
33/// The `max_length` of a [`ScalarType::VarChar`].
34///
35/// This newtype wrapper ensures that the length is within the valid range.
36///
37/// [`ScalarType::VarChar`]: crate::ScalarType::VarChar
38#[derive(
39    Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, MzReflect,
40)]
41pub struct VarCharMaxLength(pub(crate) u32);
42
43impl VarCharMaxLength {
44    /// Consumes the newtype wrapper, returning the inner `u32`.
45    pub fn into_u32(self) -> u32 {
46        self.0
47    }
48}
49
50impl TryFrom<i64> for VarCharMaxLength {
51    type Error = InvalidVarCharMaxLengthError;
52
53    fn try_from(max_length: i64) -> Result<Self, Self::Error> {
54        match u32::try_from(max_length) {
55            Ok(max_length) if max_length > 0 && max_length < MAX_MAX_LENGTH => {
56                Ok(VarCharMaxLength(max_length))
57            }
58            _ => Err(InvalidVarCharMaxLengthError),
59        }
60    }
61}
62
63impl RustType<ProtoVarCharMaxLength> for VarCharMaxLength {
64    fn into_proto(&self) -> ProtoVarCharMaxLength {
65        ProtoVarCharMaxLength { value: self.0 }
66    }
67
68    fn from_proto(proto: ProtoVarCharMaxLength) -> Result<Self, TryFromProtoError> {
69        Ok(VarCharMaxLength(proto.value))
70    }
71}
72
73impl Arbitrary for VarCharMaxLength {
74    type Parameters = ();
75    type Strategy = BoxedStrategy<VarCharMaxLength>;
76
77    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
78        proptest::arbitrary::any::<u32>()
79            // We cap the maximum VarCharMaxLength to prevent generating
80            // massive strings which can greatly slow down tests and are
81            // relatively uninteresting.
82            .prop_map(|len| VarCharMaxLength(len % 300))
83            .boxed()
84    }
85}
86
87/// The error returned when constructing a [`VarCharMaxLength`] from an invalid
88/// value.
89#[derive(Debug, Clone)]
90pub struct InvalidVarCharMaxLengthError;
91
92impl fmt::Display for InvalidVarCharMaxLengthError {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        write!(
95            f,
96            "length for type character varying must be between 1 and {}",
97            MAX_MAX_LENGTH
98        )
99    }
100}
101
102impl Error for InvalidVarCharMaxLengthError {}
103
104pub fn format_str(
105    s: &str,
106    length: Option<VarCharMaxLength>,
107    fail_on_len: bool,
108) -> Result<&str, anyhow::Error> {
109    Ok(match length {
110        // Note that length is 1-indexed, so finding `None` means the string's
111        // characters don't exceed the length, while finding `Some` means it
112        // does.
113        Some(l) => {
114            let l = usize::cast_from(l.into_u32());
115            match s.char_indices().nth(l) {
116                None => s,
117                Some((idx, _)) => {
118                    if !fail_on_len || s[idx..].chars().all(|c| c.is_ascii_whitespace()) {
119                        &s[..idx]
120                    } else {
121                        bail!("{} exceeds maximum length of {}", s, l)
122                    }
123                }
124            }
125        }
126        None => s,
127    })
128}
129
130#[cfg(test)]
131mod tests {
132    use mz_ore::assert_ok;
133    use mz_proto::protobuf_roundtrip;
134    use proptest::prelude::*;
135
136    use super::*;
137
138    proptest! {
139        #[mz_ore::test]
140        fn var_char_max_length_protobuf_roundtrip(expect in any::<VarCharMaxLength>()) {
141            let actual = protobuf_roundtrip::<_, ProtoVarCharMaxLength>(&expect);
142            assert_ok!(actual);
143            assert_eq!(actual.unwrap(), expect);
144        }
145    }
146}