Skip to main content

mz_repr/adt/
char.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.char.rs"));
23
24// https://github.com/postgres/postgres/blob/REL_14_0/src/include/access/htup_details.h#L577-L584
25const MAX_LENGTH: u32 = 10_485_760;
26
27/// A marker type indicating that a Rust string should be interpreted as a
28/// [`SqlScalarType::Char`].
29///
30/// [`SqlScalarType::Char`]: crate::SqlScalarType::Char
31#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
32pub struct Char<S: AsRef<str>>(pub S);
33
34/// The `length` of a [`SqlScalarType::Char`].
35///
36/// This newtype wrapper ensures that the length is within the valid range.
37///
38/// [`SqlScalarType::Char`]: crate::SqlScalarType::Char
39#[derive(
40    Debug,
41    Clone,
42    Copy,
43    Eq,
44    PartialEq,
45    Ord,
46    PartialOrd,
47    Hash,
48    Serialize,
49    Deserialize
50)]
51// The serde name distinguishes this type from the `char_length` SQL function's
52// generated struct, which is also named `CharLength`. The stable LIR schema
53// registry requires container names to be unique.
54#[serde(rename = "CharMaxLength")]
55pub struct CharLength(pub(crate) u32);
56
57impl CharLength {
58    /// A length of one.
59    pub const ONE: CharLength = CharLength(1);
60
61    /// Consumes the newtype wrapper, returning the inner `u32`.
62    pub fn into_u32(self) -> u32 {
63        self.0
64    }
65}
66
67impl TryFrom<i64> for CharLength {
68    type Error = InvalidCharLengthError;
69
70    fn try_from(length: i64) -> Result<Self, Self::Error> {
71        match u32::try_from(length) {
72            Ok(length) if length > 0 && length < MAX_LENGTH => Ok(CharLength(length)),
73            _ => Err(InvalidCharLengthError),
74        }
75    }
76}
77
78#[cfg(any(test, feature = "proptest"))]
79impl Arbitrary for CharLength {
80    type Parameters = ();
81    type Strategy = BoxedStrategy<CharLength>;
82
83    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
84        // We cap the maximum CharLength to prevent generating massive strings
85        // which can greatly slow down tests and are relatively uninteresting.
86        // The lower bound is the type's own: a `char(0)` does not exist, and
87        // `arb_datum_for_scalar` builds a string of `length` chars from this.
88        (1..300u32).prop_map(CharLength).boxed()
89    }
90}
91
92/// The error returned when constructing a [`CharLength`] from an invalid value.
93#[derive(Debug, Clone)]
94pub struct InvalidCharLengthError;
95
96impl fmt::Display for InvalidCharLengthError {
97    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98        write!(
99            f,
100            "length for type character must be between 1 and {}",
101            MAX_LENGTH
102        )
103    }
104}
105
106impl Error for InvalidCharLengthError {}
107
108/// Controls how to handle trailing whitespace at the end of bpchar data.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
110enum CharWhiteSpace {
111    /// Trim all whitespace from strings, which is appropriate for storing
112    /// bpchar data in Materialize. bpchar data is stored in datums with its
113    /// trailing whitespace trimmed to enforce the same equality semantics as
114    /// PG, while also allowing us to bit-wise equality on rows.
115    Trim,
116    /// Blank pad strings, which is appropriate for returning bpchar data out of Materialize.
117    Pad,
118}
119
120impl CharWhiteSpace {
121    fn process_str(&self, s: &str, length: Option<usize>) -> String {
122        use CharWhiteSpace::*;
123        match self {
124            Trim => s.trim_end().to_string(),
125            Pad => match length {
126                Some(length) => format!("{:width$}", s, width = length),
127                // This occurs when e.g. printing lists
128                None => s.to_string(),
129            },
130        }
131    }
132}
133
134/// Returns `s` as a `String` with options to enforce char and varchar
135/// semantics.
136///
137/// # Arguments
138/// * `s` - The `str` to format
139/// * `length` - An optional maximum length for the string
140/// * `fail_on_len` - Return an error if `s`'s character count exceeds the
141///   specified maximum length.
142/// * `white_space` - Express how to handle trailing whitespace on `s`
143///
144/// This function should only fail when `fail_on_len` is `true` _and_ `length`
145/// is present and exceeded.
146fn format_char_str(
147    s: &str,
148    length: Option<CharLength>,
149    fail_on_len: bool,
150    white_space: CharWhiteSpace,
151) -> Result<String, anyhow::Error> {
152    Ok(match length {
153        // Note that length is 1-indexed, so finding `None` means the string's
154        // characters don't exceed the length, while finding `Some` means it
155        // does.
156        Some(l) => {
157            let l = usize::cast_from(l.into_u32());
158            // The number of chars in a string is always less or equal to the length of the string.
159            // Hence, if the string is shorter than the length, we do not have to check for
160            // the maximum length.
161            if s.len() < l {
162                return Ok(white_space.process_str(s, Some(l)));
163            }
164            match s.char_indices().nth(l) {
165                None => white_space.process_str(s, Some(l)),
166                Some((idx, _)) => {
167                    if !fail_on_len || s[idx..].chars().all(|c| c.is_ascii_whitespace()) {
168                        white_space.process_str(&s[..idx], Some(l))
169                    } else {
170                        bail!("{} exceeds maximum length of {}", s, l)
171                    }
172                }
173            }
174        }
175        None => white_space.process_str(s, None),
176    })
177}
178
179/// Ensures that `s` has fewer than `length` characters, and returns a `String`
180/// version of it with all whitespace trimmed from the end.
181///
182/// The value returned is appropriate to store in `Datum::String`, but _is not_
183/// appropriate to return to clients.
184///
185/// This function should only fail when `fail_on_len` is `true` _and_ `length`
186/// is present and exceeded.
187pub fn format_str_trim(
188    s: &str,
189    length: Option<CharLength>,
190    fail_on_len: bool,
191) -> Result<String, anyhow::Error> {
192    format_char_str(s, length, fail_on_len, CharWhiteSpace::Trim)
193}
194
195/// Ensures that `s` has fewer than `length` characters, and returns a `String`
196/// version of it with blank padding so that its width is `length` characters.
197///
198/// The value returned is appropriate to return to clients, but _is not_
199/// appropriate to store in `Datum::String`.
200pub fn format_str_pad(s: &str, length: Option<CharLength>) -> String {
201    format_char_str(s, length, false, CharWhiteSpace::Pad).unwrap()
202}
203
204impl RustType<ProtoCharLength> for CharLength {
205    fn into_proto(&self) -> ProtoCharLength {
206        ProtoCharLength { value: self.0 }
207    }
208
209    // NOTE: `from_proto` is a trust boundary for durable and protocol state, so it
210    // enforces the same domain as `TryFrom<i64>` rather than trusting the wire.
211    fn from_proto(proto: ProtoCharLength) -> Result<Self, TryFromProtoError> {
212        CharLength::try_from(i64::from(proto.value)).map_err(|e| {
213            TryFromProtoError::InvalidFieldError(format!(
214                "ProtoCharLength::value {}: {e}",
215                proto.value
216            ))
217        })
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use mz_ore::assert_ok;
224    use mz_proto::protobuf_roundtrip;
225    use proptest::prelude::*;
226
227    use super::*;
228
229    proptest! {
230        #[mz_ore::test]
231        fn char_length_protobuf_roundtrip(expect in any::<CharLength>()) {
232            let actual = protobuf_roundtrip::<_, ProtoCharLength>(&expect);
233            assert_ok!(actual);
234            assert_eq!(actual.unwrap(), expect);
235        }
236    }
237}