Skip to main content

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