risingwave_common/types/
to_text.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::{Result, Write};
16use std::num::FpCategory;
17
18use super::{DataType, DatumRef, ScalarRefImpl};
19use crate::dispatch_scalar_ref_variants;
20
21/// Converts `ScalarRef` to pgwire "TEXT" format.
22///
23/// ## Relationship with casting to varchar
24///
25/// For most types, this is also the implementation for casting to varchar, but there are exceptions.
26/// e.g., The TEXT format for boolean is `t` / `f` while they cast to varchar `true` / `false`.
27/// - <https://github.com/postgres/postgres/blob/REL_16_3/src/include/catalog/pg_cast.dat#L438-L439>
28/// - <https://www.postgresql.org/docs/16/sql-createcast.html#:~:text=A%20small%20number%20of%20the%20built%2Din%20types%20do%20indeed%20have%20different%20behaviors%20for%20conversions%2C%20mostly%20because%20of%20requirements%20of%20the%20SQL%20standard>
29///
30/// ## Relationship with `ToString`/`Display`
31///
32/// For some types, the implementation diverge from Rust's standard `ToString`/`Display`,
33/// to match PostgreSQL's representation.
34///
35/// ---
36///
37/// FIXME: `ToText` should depend on a lot of other stuff
38/// but we have not implemented them yet: timezone, date style, interval style, bytea output, etc
39pub trait ToText {
40    /// Write the text to the writer *regardless* of its data type
41    ///
42    /// See `ToText::to_text` for more details.
43    fn write<W: Write>(&self, f: &mut W) -> Result;
44
45    /// Write the text to the writer according to its data type
46    fn write_with_type<W: Write>(&self, _ty: &DataType, f: &mut W) -> Result;
47
48    /// Convert to text according to its data type
49    fn to_text_with_type(&self, ty: &DataType) -> String {
50        let mut s = String::new();
51        self.write_with_type(ty, &mut s).unwrap();
52        s
53    }
54
55    /// `to_text` is a special version of `to_text_with_type`, it convert the scalar to default type
56    /// text. E.g. for Int64, it will convert to text as a Int64 type.
57    /// We should prefer to use `to_text_with_type` because it's more clear and readable.
58    ///
59    /// Note: currently the `DataType` param is actually unnecessary.
60    /// Previously, Timestamptz is also represented as int64, and we need the data type to distinguish them.
61    /// Now we have 1-1 mapping, and it happens to be the case that PostgreSQL default `ToText` format does
62    /// not need additional metadata like field names contained in `DataType`.
63    fn to_text(&self) -> String {
64        let mut s = String::new();
65        self.write(&mut s).unwrap();
66        s
67    }
68
69    /// Returns an displayable wrapper implemented with `ToText::write`.
70    fn text_display(&self) -> impl std::fmt::Display + '_ {
71        std::fmt::from_fn(|f| self.write(f))
72    }
73}
74
75macro_rules! implement_using_to_string {
76    ($({ $scalar_type:ty , $data_type:ident} ),*) => {
77        $(
78            impl ToText for $scalar_type {
79                fn write<W: Write>(&self, f: &mut W) -> Result {
80                    write!(f, "{self}")
81                }
82                fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
83                    match ty {
84                        DataType::$data_type => self.write(f),
85                        _ => unreachable!(),
86                    }
87                }
88            }
89        )*
90    };
91}
92
93macro_rules! implement_using_itoa {
94    ($({ $scalar_type:ty , $data_type:ident} ),*) => {
95        $(
96            impl ToText for $scalar_type {
97                fn write<W: Write>(&self, f: &mut W) -> Result {
98                    write!(f, "{}", itoa::Buffer::new().format(*self))
99                }
100                fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
101                    match ty {
102                        DataType::$data_type => self.write(f),
103                        _ => unreachable!(),
104                    }
105                }
106            }
107        )*
108    };
109}
110
111implement_using_to_string! {
112    { String ,Varchar },
113    { &str ,Varchar}
114}
115
116implement_using_itoa! {
117    { i16, Int16 },
118    { i32, Int32 },
119    { i64, Int64 }
120}
121
122macro_rules! implement_using_ryu {
123    ($({ $scalar_type:ty, $data_type:ident } ),*) => {
124            $(
125            impl ToText for $scalar_type {
126                fn write<W: Write>(&self, f: &mut W) -> Result {
127                    let inner = self.0;
128                    match inner.classify() {
129                        FpCategory::Infinite if inner.is_sign_negative() => write!(f, "-Infinity"),
130                        FpCategory::Infinite => write!(f, "Infinity"),
131                        FpCategory::Zero if inner.is_sign_negative() => write!(f, "-0"),
132                        FpCategory::Nan => write!(f, "NaN"),
133                        _ => {
134                            let mut buf = ryu::Buffer::new();
135                            let mut s = buf.format_finite(self.0);
136                            if let Some(trimmed) = s.strip_suffix(".0") {
137                                s = trimmed;
138                            }
139                            if let Some(mut idx) = s.as_bytes().iter().position(|x| *x == b'e') {
140                                idx += 1;
141                                write!(f, "{}", &s[..idx])?;
142                                if s.as_bytes()[idx] == b'-' {
143                                    write!(f, "-")?;
144                                    idx += 1;
145                                } else {
146                                    write!(f, "+")?;
147                                }
148                                if idx + 1 == s.len() {
149                                    write!(f, "0")?;
150                                }
151                                write!(f, "{}", &s[idx..])?;
152                            } else {
153                                write!(f, "{}", s)?;
154                            }
155                            Ok(())
156                        }
157                    }
158                }
159                fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
160                    match ty {
161                        DataType::$data_type => self.write(f),
162                        _ => unreachable!(),
163                    }
164                }
165            }
166        )*
167    };
168}
169
170implement_using_ryu! {
171    { crate::types::F32, Float32 },
172    { crate::types::F64, Float64 }
173}
174
175impl ToText for bool {
176    fn write<W: Write>(&self, f: &mut W) -> Result {
177        if *self {
178            write!(f, "t")
179        } else {
180            write!(f, "f")
181        }
182    }
183
184    fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
185        match ty {
186            DataType::Boolean => self.write(f),
187            _ => unreachable!(),
188        }
189    }
190}
191
192impl ToText for &[u8] {
193    fn write<W: Write>(&self, f: &mut W) -> Result {
194        write!(f, "\\x{}", hex::encode(self))
195    }
196
197    fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
198        match ty {
199            DataType::Bytea => self.write(f),
200            _ => unreachable!(),
201        }
202    }
203}
204
205impl ToText for ScalarRefImpl<'_> {
206    fn write<W: Write>(&self, f: &mut W) -> Result {
207        dispatch_scalar_ref_variants!(self, v, { v.write(f) })
208    }
209
210    fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
211        dispatch_scalar_ref_variants!(self, v, { v.write_with_type(ty, f) })
212    }
213}
214
215impl ToText for DatumRef<'_> {
216    fn write<W: Write>(&self, f: &mut W) -> Result {
217        match self {
218            Some(data) => data.write(f),
219            None => write!(f, "NULL"),
220        }
221    }
222
223    fn write_with_type<W: Write>(&self, ty: &DataType, f: &mut W) -> Result {
224        match self {
225            Some(data) => data.write_with_type(ty, f),
226            None => write!(f, "NULL"),
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use crate::types::ToText;
234    use crate::types::ordered_float::OrderedFloat;
235
236    #[test]
237    fn test_float_to_text() {
238        // f64 -> text.
239        let ret: OrderedFloat<f64> = OrderedFloat::<f64>::from(1.234567890123456);
240        tracing::info!("ret: {}", ret.to_text());
241        assert_eq!("1.234567890123456".to_owned(), ret.to_text());
242
243        // f32 -> text.
244        let ret: OrderedFloat<f32> = OrderedFloat::<f32>::from(1.234567);
245        assert_eq!("1.234567".to_owned(), ret.to_text());
246    }
247}