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