sqlx_exasol_impl/
type_info.rs

1use std::fmt::{Arguments, Display};
2
3use arrayvec::ArrayString;
4use serde::{Deserialize, Serialize};
5use sqlx_core::type_info::TypeInfo;
6
7/// Information about an Exasol data type and implementor of [`TypeInfo`].
8// Note that the [`DataTypeName`] is automatically constructed from the provided [`ExaDataType`].
9#[derive(Debug, Clone, Copy, Deserialize)]
10#[serde(from = "ExaDataType")]
11pub struct ExaTypeInfo {
12    pub(crate) name: DataTypeName,
13    pub(crate) data_type: ExaDataType,
14}
15
16impl ExaTypeInfo {
17    #[doc(hidden)]
18    #[allow(clippy::must_use_candidate)]
19    pub fn __type_feature_gate(&self) -> Option<&'static str> {
20        match self.data_type {
21            ExaDataType::Date
22            | ExaDataType::Timestamp
23            | ExaDataType::TimestampWithLocalTimeZone => Some("time"),
24            ExaDataType::Decimal(decimal)
25                if decimal.scale > 0 || decimal.precision > Some(Decimal::MAX_64BIT_PRECISION) =>
26            {
27                Some("bigdecimal")
28            }
29            _ => None,
30        }
31    }
32}
33
34/// Manually implemented because we only want to serialize the `data_type` field while also
35/// flattening the structure.
36// NOTE: On [`Deserialize`] we simply convert from the [`ExaDataType`] to this.
37impl Serialize for ExaTypeInfo {
38    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39    where
40        S: serde::Serializer,
41    {
42        self.data_type.serialize(serializer)
43    }
44}
45
46impl From<ExaDataType> for ExaTypeInfo {
47    fn from(data_type: ExaDataType) -> Self {
48        let name = data_type.full_name();
49        Self { name, data_type }
50    }
51}
52
53impl PartialEq for ExaTypeInfo {
54    fn eq(&self, other: &Self) -> bool {
55        self.data_type == other.data_type
56    }
57}
58
59impl Display for ExaTypeInfo {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.name)
62    }
63}
64
65impl TypeInfo for ExaTypeInfo {
66    fn is_null(&self) -> bool {
67        false
68    }
69
70    /// We're going against `sqlx` here, but knowing the full data type definition is actually very
71    /// helpful when displaying error messages, so... ¯\_(ツ)_/¯. This is also due to Exasol's
72    /// limited number of data types. How would it look saying that a `DECIMAL` column does not fit
73    /// in some other `DECIMAL` data type?
74    ///
75    /// In fact, error messages seem to be the only place where this is being used, particularly
76    /// when trying to decode a value but the data type provided by the database does not
77    /// match/fit inside the Rust data type.
78    fn name(&self) -> &str {
79        self.name.as_ref()
80    }
81
82    /// Checks compatibility with other data types.
83    ///
84    /// Returns true if this [`ExaTypeInfo`] instance is able to accommodate the `other` instance.
85    fn type_compatible(&self, other: &Self) -> bool
86    where
87        Self: Sized,
88    {
89        self.data_type.compatible(&other.data_type)
90    }
91}
92
93/// Datatype definitions enum, as Exasol sees them.
94///
95/// If you manually construct them, be aware that there is a [`DataTypeName`] automatically
96/// constructed when converting to [`ExaTypeInfo`] and there are compatibility checks set in place.
97///
98/// In case of incompatibility, the definition is displayed for troubleshooting.
99#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
100#[serde(rename_all = "UPPERCASE")]
101#[serde(tag = "type")]
102pub enum ExaDataType {
103    /// The BOOLEAN data type.
104    Boolean,
105    /// The CHAR data type.
106    #[serde(rename_all = "camelCase")]
107    Char { size: u32, character_set: Charset },
108    /// The DATE data type.
109    Date,
110    /// The DECIMAL data type.
111    Decimal(Decimal),
112    /// The DOUBLE data type.
113    Double,
114    /// The `GEOMETRY` data type.
115    #[serde(rename_all = "camelCase")]
116    Geometry { srid: u16 },
117    /// The `INTERVAL DAY TO SECOND` data type.
118    #[serde(rename = "INTERVAL DAY TO SECOND")]
119    #[serde(rename_all = "camelCase")]
120    IntervalDayToSecond { precision: u32, fraction: u32 },
121    /// The `INTERVAL YEAR TO MONTH` data type.
122    #[serde(rename = "INTERVAL YEAR TO MONTH")]
123    #[serde(rename_all = "camelCase")]
124    IntervalYearToMonth { precision: u32 },
125    /// The TIMESTAMP data type.
126    Timestamp,
127    /// The TIMESTAMP WITH LOCAL TIME ZONE data type.
128    #[serde(rename = "TIMESTAMP WITH LOCAL TIME ZONE")]
129    TimestampWithLocalTimeZone,
130    /// The VARCHAR data type.
131    #[serde(rename_all = "camelCase")]
132    Varchar { size: u32, character_set: Charset },
133    /// The Exasol `HASHTYPE` data type.
134    ///
135    /// NOTE: Exasol returns the size of the column string representation which depends on the
136    /// `HASHTYPE_FORMAT` database parameter. We set the parameter to `HEX` whenever we open
137    /// a connection to allow us to reliably use the column size, particularly for UUIDs.
138    ///
139    /// However, other values (especially the ones to be encoded) through
140    /// [`crate::types::HashType`] cannot be strictly checked because they could be in different
141    /// formats, like hex, base64, etc. In that case we avoid the size check by relying on
142    /// [`None`].
143    ///
144    /// Database columns and prepared statements parameters will **always** be [`Some`].
145    HashType { size: Option<u16> },
146}
147
148impl ExaDataType {
149    // Data type names
150    const BOOLEAN: &'static str = "BOOLEAN";
151    const CHAR: &'static str = "CHAR";
152    const DATE: &'static str = "DATE";
153    const DECIMAL: &'static str = "DECIMAL";
154    const DOUBLE: &'static str = "DOUBLE PRECISION";
155    const GEOMETRY: &'static str = "GEOMETRY";
156    const INTERVAL_DAY_TO_SECOND: &'static str = "INTERVAL DAY TO SECOND";
157    const INTERVAL_YEAR_TO_MONTH: &'static str = "INTERVAL YEAR TO MONTH";
158    const TIMESTAMP: &'static str = "TIMESTAMP";
159    const TIMESTAMP_WITH_LOCAL_TIME_ZONE: &'static str = "TIMESTAMP WITH LOCAL TIME ZONE";
160    const VARCHAR: &'static str = "VARCHAR";
161    const HASHTYPE: &'static str = "HASHTYPE";
162
163    // Datatype constants
164    //
165    /// Accuracy is limited to milliseconds, see: <https://docs.exasol.com/db/latest/sql_references/data_types/datatypedetails.htm#Interval>.
166    ///
167    /// The fraction has the weird behavior of shifting the milliseconds up the value and mixing it
168    /// with the seconds, minutes, hours or even the days when the value exceeds 3 (the max
169    /// milliseconds digits limit) even though the maximum value is 9.
170    ///
171    /// See: <https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_dsinterval.htm?Highlight=fraction%20interval>
172    ///
173    /// Therefore, we'll only be handling fractions smaller or equal to 3.
174    #[allow(dead_code, reason = "used by optional dependency")]
175    pub(crate) const INTERVAL_DTS_MAX_FRACTION: u32 = 3;
176    #[allow(dead_code, reason = "used by optional dependency")]
177    pub(crate) const INTERVAL_DTS_MAX_PRECISION: u32 = 9;
178    pub(crate) const INTERVAL_YTM_MAX_PRECISION: u32 = 9;
179    pub(crate) const VARCHAR_MAX_LEN: u32 = 2_000_000;
180    #[cfg_attr(not(test), expect(dead_code))]
181    pub(crate) const CHAR_MAX_LEN: u32 = 2_000;
182    // 1024 * 2 because we set HASHTYPE_FORMAT to HEX.
183    #[cfg_attr(not(test), expect(dead_code))]
184    pub(crate) const HASHTYPE_MAX_LEN: u16 = 2048;
185
186    /// Returns `true` if this instance is compatible with the other one provided.
187    ///
188    /// Compatibility means that the [`self`] instance is bigger/able to accommodate the other
189    /// instance.
190    pub fn compatible(&self, other: &Self) -> bool {
191        match (self, other) {
192            (Self::HashType { size: Some(s1) }, Self::HashType { size: Some(s2) }) => s1 == s2,
193            (Self::Boolean, Self::Boolean)
194            | (
195                Self::Char { .. } | Self::Varchar { .. },
196                Self::Char { .. } | Self::Varchar { .. },
197            )
198            | (Self::Date, Self::Date)
199            | (Self::Double, Self::Double)
200            | (Self::Geometry { .. }, Self::Geometry { .. })
201            | (Self::IntervalDayToSecond { .. }, Self::IntervalDayToSecond { .. })
202            | (Self::IntervalYearToMonth { .. }, Self::IntervalYearToMonth { .. })
203            | (Self::Timestamp, Self::Timestamp)
204            | (Self::TimestampWithLocalTimeZone, Self::TimestampWithLocalTimeZone)
205            | (Self::HashType { .. }, Self::HashType { .. }) => true,
206            (Self::Decimal(d1), Self::Decimal(d2)) => d1.compatible(*d2),
207            _ => false,
208        }
209    }
210
211    fn full_name(&self) -> DataTypeName {
212        match self {
213            Self::Boolean => Self::BOOLEAN.into(),
214            Self::Date => Self::DATE.into(),
215            Self::Double => Self::DOUBLE.into(),
216            Self::Timestamp => Self::TIMESTAMP.into(),
217            Self::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE.into(),
218            Self::Char {
219                size,
220                character_set,
221            }
222            | Self::Varchar {
223                size,
224                character_set,
225            } => format_args!("{}({}) {}", self.as_ref(), size, character_set).into(),
226            Self::Decimal(d) => match d.precision {
227                Some(p) => format_args!("{}({}, {})", self.as_ref(), p, d.scale).into(),
228                None => format_args!("{}(*, {})", self.as_ref(), d.scale).into(),
229            },
230            Self::Geometry { srid } => format_args!("{}({srid})", self.as_ref()).into(),
231            Self::IntervalDayToSecond {
232                precision,
233                fraction,
234            } => format_args!("INTERVAL DAY({precision}) TO SECOND({fraction})").into(),
235            Self::IntervalYearToMonth { precision } => {
236                format_args!("INTERVAL YEAR({precision}) TO MONTH").into()
237            }
238            Self::HashType { size } => match size {
239                // We get the HEX len, which is double the byte count.
240                Some(s) => format_args!("{}({} BYTE)", self.as_ref(), s / 2).into(),
241                None => format_args!("{}", self.as_ref()).into(),
242            },
243        }
244    }
245}
246
247impl AsRef<str> for ExaDataType {
248    fn as_ref(&self) -> &str {
249        match self {
250            Self::Boolean => Self::BOOLEAN,
251            Self::Char { .. } => Self::CHAR,
252            Self::Date => Self::DATE,
253            Self::Decimal(_) => Self::DECIMAL,
254            Self::Double => Self::DOUBLE,
255            Self::Geometry { .. } => Self::GEOMETRY,
256            Self::IntervalDayToSecond { .. } => Self::INTERVAL_DAY_TO_SECOND,
257            Self::IntervalYearToMonth { .. } => Self::INTERVAL_YEAR_TO_MONTH,
258            Self::Timestamp => Self::TIMESTAMP,
259            Self::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE,
260            Self::Varchar { .. } => Self::VARCHAR,
261            Self::HashType { .. } => Self::HASHTYPE,
262        }
263    }
264}
265
266/// A data type's name, composed from an instance of [`ExaDataType`]. For performance's sake, since
267/// data type names are small, we either store them statically or as inlined strings.
268///
269/// *IMPORTANT*: Creating absurd [`ExaDataType`] can result in panics if the name exceeds the
270/// inlined strings max capacity. Valid values always fit.
271#[derive(Debug, Clone, Copy)]
272pub enum DataTypeName {
273    Static(&'static str),
274    Inline(ArrayString<30>),
275}
276
277impl AsRef<str> for DataTypeName {
278    fn as_ref(&self) -> &str {
279        match self {
280            DataTypeName::Static(s) => s,
281            DataTypeName::Inline(s) => s.as_str(),
282        }
283    }
284}
285
286impl Display for DataTypeName {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        write!(f, "{}", self.as_ref())
289    }
290}
291
292impl From<&'static str> for DataTypeName {
293    fn from(value: &'static str) -> Self {
294        Self::Static(value)
295    }
296}
297
298impl From<Arguments<'_>> for DataTypeName {
299    fn from(value: Arguments<'_>) -> Self {
300        Self::Inline(ArrayString::try_from(value).expect("inline data type name too large"))
301    }
302}
303
304/// The `DECIMAL` data type.
305#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
306#[serde(rename_all = "camelCase")]
307pub struct Decimal {
308    /// The absence of precision means universal compatibility.
309    pub(crate) precision: Option<u8>,
310    pub(crate) scale: u8,
311}
312
313impl Decimal {
314    /// Max precision values for signed integers.
315    pub(crate) const MAX_8BIT_PRECISION: u8 = 3;
316    pub(crate) const MAX_16BIT_PRECISION: u8 = 5;
317    pub(crate) const MAX_32BIT_PRECISION: u8 = 10;
318    pub(crate) const MAX_64BIT_PRECISION: u8 = 20;
319
320    /// Max supported values.
321    pub(crate) const MAX_PRECISION: u8 = 36;
322    #[allow(dead_code)]
323    pub(crate) const MAX_SCALE: u8 = 36;
324
325    /// The purpose of this is to be able to tell if some [`Decimal`] fits inside another
326    /// [`Decimal`].
327    ///
328    /// Therefore, we consider cases such as:
329    /// - DECIMAL(10, 1) != DECIMAL(9, 2)
330    /// - DECIMAL(10, 1) != DECIMAL(10, 2)
331    /// - DECIMAL(10, 1) < DECIMAL(11, 2)
332    /// - DECIMAL(10, 1) < DECIMAL(17, 4)
333    ///
334    /// - DECIMAL(10, 1) > DECIMAL(9, 1)
335    /// - DECIMAL(10, 1) = DECIMAL(10, 1)
336    /// - DECIMAL(10, 1) < DECIMAL(11, 1)
337    ///
338    /// This boils down to:
339    /// `a.scale >= b.scale AND (a.precision - a.scale) >= (b.precision - b.scale)`
340    ///
341    /// However, decimal Rust types require special handling because they can hold virtually any
342    /// decoded value. Therefore, an absent precision means that the comparison must be skipped.
343    #[rustfmt::skip] // just to skip rules formatting
344    fn compatible(self, dec: Decimal) -> bool {
345        let (precision, scale) = match dec.precision {
346            Some(precision) =>  (precision, dec.scale),
347            // Short-circuit if we are encoding a Rust decimal type as they have arbitrary precision.
348             None => return true,
349        };
350
351        // If we're decoding to a Rust decimal type then we can accept any DECIMAL precision.
352        let self_diff = self.precision.map_or(Decimal::MAX_PRECISION, |p| p - self.scale);
353        let other_diff = precision - scale;
354
355        self.scale >= scale && self_diff >= other_diff
356    }
357}
358
359/// Exasol supported character sets.
360#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
361#[serde(rename_all = "UPPERCASE")]
362pub enum Charset {
363    Utf8,
364    Ascii,
365}
366
367impl AsRef<str> for Charset {
368    fn as_ref(&self) -> &str {
369        match self {
370            Charset::Utf8 => "UTF8",
371            Charset::Ascii => "ASCII",
372        }
373    }
374}
375
376impl Display for Charset {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        write!(f, "{}", self.as_ref())
379    }
380}
381
382/// Mainly adding these so that we ensure the inlined type names won't panic when created with their
383/// max values.
384///
385/// If the max values work, the lower ones inherently will too.
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_boolean_name() {
392        let data_type = ExaDataType::Boolean;
393        assert_eq!(data_type.full_name().as_ref(), "BOOLEAN");
394    }
395
396    #[test]
397    fn test_max_char_name() {
398        let data_type = ExaDataType::Char {
399            size: ExaDataType::CHAR_MAX_LEN,
400            character_set: Charset::Ascii,
401        };
402        assert_eq!(
403            data_type.full_name().as_ref(),
404            format!("CHAR({}) ASCII", ExaDataType::CHAR_MAX_LEN)
405        );
406    }
407
408    #[test]
409    fn test_date_name() {
410        let data_type = ExaDataType::Date;
411        assert_eq!(data_type.full_name().as_ref(), "DATE");
412    }
413
414    #[test]
415    fn test_max_decimal_name() {
416        let decimal = Decimal {
417            precision: Some(Decimal::MAX_PRECISION),
418            scale: Decimal::MAX_SCALE,
419        };
420        let data_type = ExaDataType::Decimal(decimal);
421        assert_eq!(
422            data_type.full_name().as_ref(),
423            format!(
424                "DECIMAL({}, {})",
425                Decimal::MAX_PRECISION,
426                Decimal::MAX_SCALE
427            )
428        );
429    }
430
431    #[test]
432    fn test_double_name() {
433        let data_type = ExaDataType::Double;
434        assert_eq!(data_type.full_name().as_ref(), "DOUBLE PRECISION");
435    }
436
437    #[test]
438    fn test_max_geometry_name() {
439        let data_type = ExaDataType::Geometry { srid: u16::MAX };
440        assert_eq!(
441            data_type.full_name().as_ref(),
442            format!("GEOMETRY({})", u16::MAX)
443        );
444    }
445
446    #[test]
447    fn test_max_interval_day_name() {
448        let data_type = ExaDataType::IntervalDayToSecond {
449            precision: ExaDataType::INTERVAL_DTS_MAX_PRECISION,
450            fraction: ExaDataType::INTERVAL_DTS_MAX_FRACTION,
451        };
452        assert_eq!(
453            data_type.full_name().as_ref(),
454            format!(
455                "INTERVAL DAY({}) TO SECOND({})",
456                ExaDataType::INTERVAL_DTS_MAX_PRECISION,
457                ExaDataType::INTERVAL_DTS_MAX_FRACTION
458            )
459        );
460    }
461
462    #[test]
463    fn test_max_interval_year_name() {
464        let data_type = ExaDataType::IntervalYearToMonth {
465            precision: ExaDataType::INTERVAL_YTM_MAX_PRECISION,
466        };
467        assert_eq!(
468            data_type.full_name().as_ref(),
469            format!(
470                "INTERVAL YEAR({}) TO MONTH",
471                ExaDataType::INTERVAL_YTM_MAX_PRECISION,
472            )
473        );
474    }
475
476    #[test]
477    fn test_timestamp_name() {
478        let data_type = ExaDataType::Timestamp;
479        assert_eq!(data_type.full_name().as_ref(), "TIMESTAMP");
480    }
481
482    #[test]
483    fn test_timestamp_with_tz_name() {
484        let data_type = ExaDataType::TimestampWithLocalTimeZone;
485        assert_eq!(
486            data_type.full_name().as_ref(),
487            "TIMESTAMP WITH LOCAL TIME ZONE"
488        );
489    }
490
491    #[test]
492    fn test_max_varchar_name() {
493        let data_type = ExaDataType::Varchar {
494            size: ExaDataType::VARCHAR_MAX_LEN,
495            character_set: Charset::Ascii,
496        };
497        assert_eq!(
498            data_type.full_name().as_ref(),
499            format!("VARCHAR({}) ASCII", ExaDataType::VARCHAR_MAX_LEN)
500        );
501    }
502
503    #[test]
504    fn test_max_hashbyte_name() {
505        let data_type = ExaDataType::HashType {
506            size: Some(ExaDataType::HASHTYPE_MAX_LEN),
507        };
508        assert_eq!(
509            data_type.full_name().as_ref(),
510            format!("HASHTYPE({} BYTE)", ExaDataType::HASHTYPE_MAX_LEN / 2)
511        );
512    }
513}