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    // NOTE: `from_proto` is a trust boundary for durable and protocol state, so it
79    // enforces the same domain as `TryFrom<i64>` rather than trusting the wire.
80    fn from_proto(proto: ProtoVarCharMaxLength) -> Result<Self, TryFromProtoError> {
81        VarCharMaxLength::try_from(i64::from(proto.value)).map_err(|e| {
82            TryFromProtoError::InvalidFieldError(format!(
83                "ProtoVarCharMaxLength::value {}: {e}",
84                proto.value
85            ))
86        })
87    }
88}
89
90#[cfg(any(test, feature = "proptest"))]
91impl Arbitrary for VarCharMaxLength {
92    type Parameters = ();
93    type Strategy = BoxedStrategy<VarCharMaxLength>;
94
95    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
96        // We cap the maximum VarCharMaxLength to prevent generating massive
97        // strings which can greatly slow down tests and are relatively
98        // uninteresting. The lower bound is the type's own: a `varchar(0)` does
99        // not exist, and `arb_datum_for_scalar` builds a string of up to
100        // `max_length` chars from this.
101        (1..300u32).prop_map(VarCharMaxLength).boxed()
102    }
103}
104
105/// The error returned when constructing a [`VarCharMaxLength`] from an invalid
106/// value.
107#[derive(Debug, Clone)]
108pub struct InvalidVarCharMaxLengthError;
109
110impl fmt::Display for InvalidVarCharMaxLengthError {
111    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112        write!(
113            f,
114            "length for type character varying must be between 1 and {}",
115            MAX_MAX_LENGTH
116        )
117    }
118}
119
120impl Error for InvalidVarCharMaxLengthError {}
121
122pub fn format_str(
123    s: &str,
124    length: Option<VarCharMaxLength>,
125    fail_on_len: bool,
126) -> Result<&str, anyhow::Error> {
127    Ok(match length {
128        // Note that length is 1-indexed, so finding `None` means the string's
129        // characters don't exceed the length, while finding `Some` means it
130        // does.
131        Some(l) => {
132            let l = usize::cast_from(l.into_u32());
133            match s.char_indices().nth(l) {
134                None => s,
135                Some((idx, _)) => {
136                    if !fail_on_len || s[idx..].chars().all(|c| c.is_ascii_whitespace()) {
137                        &s[..idx]
138                    } else {
139                        bail!("{} exceeds maximum length of {}", s, l)
140                    }
141                }
142            }
143        }
144        None => s,
145    })
146}
147
148#[cfg(test)]
149mod tests {
150    use mz_ore::assert_ok;
151    use mz_proto::protobuf_roundtrip;
152    use proptest::prelude::*;
153
154    use super::*;
155
156    proptest! {
157        #[mz_ore::test]
158        fn var_char_max_length_protobuf_roundtrip(expect in any::<VarCharMaxLength>()) {
159            let actual = protobuf_roundtrip::<_, ProtoVarCharMaxLength>(&expect);
160            assert_ok!(actual);
161            assert_eq!(actual.unwrap(), expect);
162        }
163    }
164}