sqlx_exasol/
type_info.rs

1use std::{
2    cmp::Ordering,
3    fmt::{Arguments, Display},
4};
5
6use arrayvec::ArrayString;
7use serde::{Deserialize, Serialize};
8use sqlx_core::type_info::TypeInfo;
9
10/// Information about an Exasol data type and implementor of [`TypeInfo`].
11// Note that the [`DataTypeName`] is automatically constructed from the provided [`ExaDataType`].
12#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
13#[serde(from = "ExaDataType")]
14#[serde(rename_all = "camelCase")]
15pub struct ExaTypeInfo {
16    #[serde(skip_serializing)]
17    pub(crate) name: DataTypeName,
18    data_type: ExaDataType,
19}
20
21impl ExaTypeInfo {
22    /// Checks compatibility with other data types.
23    ///
24    /// Returns true if the [`ExaTypeInfo`] instance is compatible/bigger/able to
25    /// accommodate the `other` instance.
26    #[must_use]
27    pub fn compatible(&self, other: &Self) -> bool {
28        self.data_type.compatible(&other.data_type)
29    }
30}
31
32impl From<ExaDataType> for ExaTypeInfo {
33    fn from(data_type: ExaDataType) -> Self {
34        let name = data_type.full_name();
35        Self { name, data_type }
36    }
37}
38
39impl PartialEq for ExaTypeInfo {
40    fn eq(&self, other: &Self) -> bool {
41        self.data_type == other.data_type
42    }
43}
44
45impl Display for ExaTypeInfo {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}", self.name)
48    }
49}
50
51impl TypeInfo for ExaTypeInfo {
52    fn is_null(&self) -> bool {
53        matches!(self.data_type, ExaDataType::Null)
54    }
55
56    /// We're going against `sqlx` here, but knowing the full data type definition
57    /// is actually very helpful when displaying error messages, so... ¯\_(ツ)_/¯.
58    ///
59    /// In fact, error messages seem to be the only place where this is being used,
60    /// particularly when trying to decode a value but the data type provided by the
61    /// database does not match/fit inside the Rust data type.
62    fn name(&self) -> &str {
63        self.name.as_ref()
64    }
65}
66
67/// Datatype definitions enum, as Exasol sees them.
68///
69/// If you manually construct them, be aware that there is a [`DataTypeName`] automatically
70/// constructed when converting to [`ExaTypeInfo`] and there are compatibility checks set in place.
71///
72/// In case of incompatibility, the definition is displayed for troubleshooting.
73#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
74#[serde(rename_all = "UPPERCASE")]
75#[serde(tag = "type")]
76pub enum ExaDataType {
77    Null,
78    Boolean,
79    Char(StringLike),
80    Date,
81    Decimal(Decimal),
82    Double,
83    Geometry(Geometry),
84    #[serde(rename = "INTERVAL DAY TO SECOND")]
85    IntervalDayToSecond(IntervalDayToSecond),
86    #[serde(rename = "INTERVAL YEAR TO MONTH")]
87    IntervalYearToMonth(IntervalYearToMonth),
88    Timestamp,
89    #[serde(rename = "TIMESTAMP WITH LOCAL TIME ZONE")]
90    TimestampWithLocalTimeZone,
91    Varchar(StringLike),
92    HashType(HashType),
93}
94
95impl ExaDataType {
96    const NULL: &'static str = "NULL";
97    const BOOLEAN: &'static str = "BOOLEAN";
98    const CHAR: &'static str = "CHAR";
99    const DATE: &'static str = "DATE";
100    const DECIMAL: &'static str = "DECIMAL";
101    const DOUBLE: &'static str = "DOUBLE PRECISION";
102    const GEOMETRY: &'static str = "GEOMETRY";
103    const INTERVAL_DAY_TO_SECOND: &'static str = "INTERVAL DAY TO SECOND";
104    const INTERVAL_YEAR_TO_MONTH: &'static str = "INTERVAL YEAR TO MONTH";
105    const TIMESTAMP: &'static str = "TIMESTAMP";
106    const TIMESTAMP_WITH_LOCAL_TIME_ZONE: &'static str = "TIMESTAMP WITH LOCAL TIME ZONE";
107    const VARCHAR: &'static str = "VARCHAR";
108    const HASHTYPE: &'static str = "HASHTYPE";
109
110    /// Returns `true` if this instance is compatible with the other one provided.
111    ///
112    /// Compatibility means that the [`self`] instance is bigger/able to accommodate the other
113    /// instance.
114    pub fn compatible(&self, other: &Self) -> bool {
115        match self {
116            ExaDataType::Null => true,
117            ExaDataType::Boolean => matches!(other, ExaDataType::Boolean | ExaDataType::Null),
118            ExaDataType::Char(c) | ExaDataType::Varchar(c) => c.compatible(other),
119            ExaDataType::Date => matches!(
120                other,
121                ExaDataType::Date
122                    | ExaDataType::Char(_)
123                    | ExaDataType::Varchar(_)
124                    | ExaDataType::Null
125            ),
126            ExaDataType::Decimal(d) => d.compatible(other),
127            ExaDataType::Double => match other {
128                ExaDataType::Double | ExaDataType::Null => true,
129                ExaDataType::Decimal(d) if d.scale > 0 => true,
130                _ => false,
131            },
132            ExaDataType::Geometry(g) => g.compatible(other),
133            ExaDataType::IntervalDayToSecond(ids) => ids.compatible(other),
134            ExaDataType::IntervalYearToMonth(iym) => iym.compatible(other),
135            ExaDataType::Timestamp => matches!(
136                other,
137                ExaDataType::Timestamp
138                    | ExaDataType::TimestampWithLocalTimeZone
139                    | ExaDataType::Char(_)
140                    | ExaDataType::Varchar(_)
141                    | ExaDataType::Null
142            ),
143            ExaDataType::TimestampWithLocalTimeZone => matches!(
144                other,
145                ExaDataType::TimestampWithLocalTimeZone
146                    | ExaDataType::Timestamp
147                    | ExaDataType::Char(_)
148                    | ExaDataType::Varchar(_)
149                    | ExaDataType::Null
150            ),
151            ExaDataType::HashType(_) => matches!(
152                other,
153                ExaDataType::HashType(_)
154                    | ExaDataType::Varchar(_)
155                    | ExaDataType::Char(_)
156                    | ExaDataType::Null
157            ),
158        }
159    }
160
161    fn full_name(&self) -> DataTypeName {
162        match self {
163            ExaDataType::Null => Self::NULL.into(),
164            ExaDataType::Boolean => Self::BOOLEAN.into(),
165            ExaDataType::Date => Self::DATE.into(),
166            ExaDataType::Double => Self::DOUBLE.into(),
167            ExaDataType::Timestamp => Self::TIMESTAMP.into(),
168            ExaDataType::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE.into(),
169            ExaDataType::Char(c) | ExaDataType::Varchar(c) => {
170                format_args!("{}({}) {}", self.as_ref(), c.size, c.character_set).into()
171            }
172            ExaDataType::Decimal(d) => {
173                format_args!("{}({}, {})", self.as_ref(), d.precision, d.scale).into()
174            }
175            ExaDataType::Geometry(g) => format_args!("{}({})", self.as_ref(), g.srid).into(),
176            ExaDataType::IntervalDayToSecond(ids) => format_args!(
177                "INTERVAL DAY({}) TO SECOND({})",
178                ids.precision, ids.fraction
179            )
180            .into(),
181            ExaDataType::IntervalYearToMonth(iym) => {
182                format_args!("INTERVAL YEAR({}) TO MONTH", iym.precision).into()
183            }
184            ExaDataType::HashType(_) => format_args!("{}", self.as_ref()).into(),
185        }
186    }
187}
188
189impl AsRef<str> for ExaDataType {
190    fn as_ref(&self) -> &str {
191        match self {
192            ExaDataType::Null => Self::NULL,
193            ExaDataType::Boolean => Self::BOOLEAN,
194            ExaDataType::Char(_) => Self::CHAR,
195            ExaDataType::Date => Self::DATE,
196            ExaDataType::Decimal(_) => Self::DECIMAL,
197            ExaDataType::Double => Self::DOUBLE,
198            ExaDataType::Geometry(_) => Self::GEOMETRY,
199            ExaDataType::IntervalDayToSecond(_) => Self::INTERVAL_DAY_TO_SECOND,
200            ExaDataType::IntervalYearToMonth(_) => Self::INTERVAL_YEAR_TO_MONTH,
201            ExaDataType::Timestamp => Self::TIMESTAMP,
202            ExaDataType::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE,
203            ExaDataType::Varchar(_) => Self::VARCHAR,
204            ExaDataType::HashType(_) => Self::HASHTYPE,
205        }
206    }
207}
208
209/// A data type's name, composed from an instance of [`ExaDataType`]. For performance's sake, since
210/// data type names are small, we either store them statically or as inlined strings.
211///
212/// *IMPORTANT*: Creating absurd [`ExaDataType`] can result in panics if the name exceeds the
213/// inlined strings max capacity. Valid values always fit.
214#[derive(Debug, Clone, Copy)]
215pub enum DataTypeName {
216    Static(&'static str),
217    Inline(ArrayString<30>),
218}
219
220impl AsRef<str> for DataTypeName {
221    fn as_ref(&self) -> &str {
222        match self {
223            DataTypeName::Static(s) => s,
224            DataTypeName::Inline(s) => s.as_str(),
225        }
226    }
227}
228
229impl Display for DataTypeName {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        write!(f, "{}", self.as_ref())
232    }
233}
234
235impl From<&'static str> for DataTypeName {
236    fn from(value: &'static str) -> Self {
237        Self::Static(value)
238    }
239}
240
241impl From<Arguments<'_>> for DataTypeName {
242    fn from(value: Arguments<'_>) -> Self {
243        Self::Inline(ArrayString::try_from(value).expect("inline data type name too large"))
244    }
245}
246
247/// Common inner type for string like data types, such as `VARCHAR` or `CHAR`.
248#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
249#[serde(rename_all = "camelCase")]
250pub struct StringLike {
251    size: usize,
252    character_set: Charset,
253}
254
255impl StringLike {
256    pub const MAX_VARCHAR_LEN: usize = 2_000_000;
257    pub const MAX_CHAR_LEN: usize = 2000;
258
259    pub fn new(size: usize, character_set: Charset) -> Self {
260        Self {
261            size,
262            character_set,
263        }
264    }
265
266    pub fn size(&self) -> usize {
267        self.size
268    }
269
270    pub fn character_set(&self) -> Charset {
271        self.character_set
272    }
273
274    /// Strings are complex and ensuring one fits inside
275    /// a database column would imply a lot of overhead.
276    ///
277    /// So just let the database do its thing and throw an error.
278    #[allow(clippy::unused_self)]
279    pub fn compatible(&self, ty: &ExaDataType) -> bool {
280        matches!(
281            ty,
282            ExaDataType::Char(_)
283                | ExaDataType::Varchar(_)
284                | ExaDataType::Null
285                | ExaDataType::Date
286                | ExaDataType::Geometry(_)
287                | ExaDataType::HashType(_)
288                | ExaDataType::IntervalDayToSecond(_)
289                | ExaDataType::IntervalYearToMonth(_)
290                | ExaDataType::Timestamp
291                | ExaDataType::TimestampWithLocalTimeZone
292        )
293    }
294}
295
296/// Exasol supported character sets.
297#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
298#[serde(rename_all = "UPPERCASE")]
299pub enum Charset {
300    Utf8,
301    Ascii,
302}
303
304impl AsRef<str> for Charset {
305    fn as_ref(&self) -> &str {
306        match self {
307            Charset::Utf8 => "UTF8",
308            Charset::Ascii => "ASCII",
309        }
310    }
311}
312
313impl Display for Charset {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        write!(f, "{}", self.as_ref())
316    }
317}
318
319/// The `DECIMAL` data type.
320#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
321#[serde(rename_all = "camelCase")]
322pub struct Decimal {
323    precision: u32,
324    scale: u32,
325}
326
327impl Decimal {
328    pub const MAX_8BIT_PRECISION: u32 = 3;
329    pub const MAX_16BIT_PRECISION: u32 = 5;
330    pub const MAX_32BIT_PRECISION: u32 = 10;
331    pub const MAX_64BIT_PRECISION: u32 = 20;
332    /// It's fine for this precision to "overflow".
333    /// The database will simply reject values too large.
334    pub const MAX_128BIT_PRECISION: u32 = 39;
335    pub const MAX_PRECISION: u32 = 36;
336    pub const MAX_SCALE: u32 = 35;
337
338    pub fn new(precision: u32, scale: u32) -> Self {
339        Self { precision, scale }
340    }
341
342    pub fn precision(self) -> u32 {
343        self.precision
344    }
345
346    pub fn scale(self) -> u32 {
347        self.scale
348    }
349
350    pub fn compatible(self, ty: &ExaDataType) -> bool {
351        match ty {
352            ExaDataType::Decimal(d) => self >= *d,
353            ExaDataType::Double => self.scale > 0,
354            ExaDataType::Null => true,
355            _ => false,
356        }
357    }
358}
359
360#[rustfmt::skip] // just to skip rules formatting
361/// The purpose of this is to be able to tell if some [`Decimal`] fits inside another [`Decimal`].
362///
363/// Therefore, we consider cases such as:
364/// - DECIMAL(10, 1) != DECIMAL(9, 2)
365/// - DECIMAL(10, 1) != DECIMAL(10, 2)
366/// - DECIMAL(10, 1) < DECIMAL(11, 2)
367/// - DECIMAL(10, 1) < DECIMAL(17, 4)
368///
369/// - DECIMAL(10, 1) > DECIMAL(9, 1)
370/// - DECIMAL(10, 1) = DECIMAL(10, 1)
371/// - DECIMAL(10, 1) < DECIMAL(11, 1)
372///
373/// So, our rule will be:
374/// - if a.scale > b.scale, a > b if and only if (a.precision - a.scale) >= (b.precision - b.scale)
375/// - if a.scale == b.scale, a == b if and only if (a.precision - a.scale) == (b.precision - b.scale)
376/// - if a.scale < b.scale, a < b if and only if (a.precision - a.scale) <= (b.precision - b.scale)
377impl PartialOrd for Decimal {
378    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
379        let self_diff = self.precision - self.scale;
380        let other_diff = other.precision - other.scale;
381
382        let scale_cmp = self.scale.partial_cmp(&other.scale);
383        let diff_cmp = self_diff.partial_cmp(&other_diff);
384
385        #[allow(clippy::match_same_arms)] // false positive
386        match (scale_cmp, diff_cmp) {
387            (Some(Ordering::Greater), Some(Ordering::Greater)) => Some(Ordering::Greater),
388            (Some(Ordering::Greater), Some(Ordering::Equal)) => Some(Ordering::Greater),
389            (Some(Ordering::Equal), ord) => ord,
390            (Some(Ordering::Less), Some(Ordering::Less)) => Some(Ordering::Less),
391            (Some(Ordering::Less), Some(Ordering::Equal)) => Some(Ordering::Less),
392            _ => None,
393        }
394    }
395}
396
397/// The `GEOMETRY` data type.
398#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
399#[serde(rename_all = "camelCase")]
400pub struct Geometry {
401    srid: u16,
402}
403
404impl Geometry {
405    pub fn new(srid: u16) -> Self {
406        Self { srid }
407    }
408
409    pub fn srid(self) -> u16 {
410        self.srid
411    }
412
413    pub fn compatible(self, ty: &ExaDataType) -> bool {
414        match ty {
415            ExaDataType::Geometry(g) => self.srid == g.srid,
416            ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
417            _ => false,
418        }
419    }
420}
421
422/// The `INTERVAL DAY TO SECOND` data type.
423#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
424#[serde(rename_all = "camelCase")]
425pub struct IntervalDayToSecond {
426    precision: u32,
427    fraction: u32,
428}
429
430impl PartialOrd for IntervalDayToSecond {
431    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
432        let precision_cmp = self.precision.partial_cmp(&other.precision);
433        let fraction_cmp = self.fraction.partial_cmp(&other.fraction);
434
435        match (precision_cmp, fraction_cmp) {
436            (Some(Ordering::Equal), Some(Ordering::Equal)) => Some(Ordering::Equal),
437            (Some(Ordering::Equal | Ordering::Less), Some(Ordering::Less))
438            | (Some(Ordering::Less), Some(Ordering::Equal)) => Some(Ordering::Less),
439            (Some(Ordering::Equal | Ordering::Greater), Some(Ordering::Greater))
440            | (Some(Ordering::Greater), Some(Ordering::Equal)) => Some(Ordering::Greater),
441            _ => None,
442        }
443    }
444}
445
446impl IntervalDayToSecond {
447    /// The fraction has the weird behavior of shifting the milliseconds up the value and mixing it
448    /// with the seconds, minutes, hours or even the days when the value exceeds 3 (the max
449    /// milliseconds digits limit).
450    ///
451    /// See: <`https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_dsinterval.htm?Highlight=fraction%20interval`>
452    ///
453    /// Therefore, we'll only be handling fractions smaller or equal to 3, as I don't even know how
454    /// to handle values above that.
455    pub const MAX_SUPPORTED_FRACTION: u32 = 3;
456    pub const MAX_PRECISION: u32 = 9;
457
458    pub fn new(precision: u32, fraction: u32) -> Self {
459        Self {
460            precision,
461            fraction,
462        }
463    }
464
465    pub fn precision(self) -> u32 {
466        self.precision
467    }
468
469    pub fn fraction(self) -> u32 {
470        self.fraction
471    }
472
473    pub fn compatible(self, ty: &ExaDataType) -> bool {
474        match ty {
475            ExaDataType::IntervalDayToSecond(i) => self >= *i,
476            ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
477            _ => false,
478        }
479    }
480}
481
482/// The `INTERVAL YEAR TO MONTH` data type.
483#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
484#[serde(rename_all = "camelCase")]
485pub struct IntervalYearToMonth {
486    precision: u32,
487}
488
489impl IntervalYearToMonth {
490    pub const MAX_PRECISION: u32 = 9;
491
492    pub fn new(precision: u32) -> Self {
493        Self { precision }
494    }
495
496    pub fn precision(self) -> u32 {
497        self.precision
498    }
499
500    pub fn compatible(self, ty: &ExaDataType) -> bool {
501        match ty {
502            ExaDataType::IntervalYearToMonth(i) => self >= *i,
503            ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
504            _ => false,
505        }
506    }
507}
508
509/// The Exasol `HASHTYPE` data type.
510///
511/// NOTE: Exasol returns the size of the column string representation which depends on the
512/// `HASHTYPE_FORMAT` database parameter. So a UUID could have a size of 32 for HEX, 36 for
513/// UUID, 22 for BASE64, etc.
514///
515/// That makes it impossible to compare a type's predefined size and the size returned for a
516/// column to check for compatibility.
517#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
518#[serde(rename_all = "camelCase")]
519pub struct HashType {}
520
521/// Mainly adding these so that we ensure the inlined type names won't panic when created with their
522/// max values.
523///
524/// If the max values work, the lower ones inherently will too.
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_null_name() {
531        let data_type = ExaDataType::Null;
532        assert_eq!(data_type.full_name().as_ref(), "NULL");
533    }
534
535    #[test]
536    fn test_boolean_name() {
537        let data_type = ExaDataType::Boolean;
538        assert_eq!(data_type.full_name().as_ref(), "BOOLEAN");
539    }
540
541    #[test]
542    fn test_max_char_name() {
543        let string_like = StringLike::new(StringLike::MAX_CHAR_LEN, Charset::Ascii);
544        let data_type = ExaDataType::Char(string_like);
545        assert_eq!(
546            data_type.full_name().as_ref(),
547            format!("CHAR({}) ASCII", StringLike::MAX_CHAR_LEN)
548        );
549    }
550
551    #[test]
552    fn test_date_name() {
553        let data_type = ExaDataType::Date;
554        assert_eq!(data_type.full_name().as_ref(), "DATE");
555    }
556
557    #[test]
558    fn test_max_decimal_name() {
559        let decimal = Decimal::new(Decimal::MAX_PRECISION, Decimal::MAX_SCALE);
560        let data_type = ExaDataType::Decimal(decimal);
561        assert_eq!(
562            data_type.full_name().as_ref(),
563            format!(
564                "DECIMAL({}, {})",
565                Decimal::MAX_PRECISION,
566                Decimal::MAX_SCALE
567            )
568        );
569    }
570
571    #[test]
572    fn test_double_name() {
573        let data_type = ExaDataType::Double;
574        assert_eq!(data_type.full_name().as_ref(), "DOUBLE PRECISION");
575    }
576
577    #[test]
578    fn test_max_geometry_name() {
579        let geometry = Geometry::new(u16::MAX);
580        let data_type = ExaDataType::Geometry(geometry);
581        assert_eq!(
582            data_type.full_name().as_ref(),
583            format!("GEOMETRY({})", u16::MAX)
584        );
585    }
586
587    #[test]
588    fn test_max_interval_day_name() {
589        let ids = IntervalDayToSecond::new(
590            IntervalDayToSecond::MAX_PRECISION,
591            IntervalDayToSecond::MAX_SUPPORTED_FRACTION,
592        );
593        let data_type = ExaDataType::IntervalDayToSecond(ids);
594        assert_eq!(
595            data_type.full_name().as_ref(),
596            format!(
597                "INTERVAL DAY({}) TO SECOND({})",
598                IntervalDayToSecond::MAX_PRECISION,
599                IntervalDayToSecond::MAX_SUPPORTED_FRACTION
600            )
601        );
602    }
603
604    #[test]
605    fn test_max_interval_year_name() {
606        let iym = IntervalYearToMonth::new(IntervalYearToMonth::MAX_PRECISION);
607        let data_type = ExaDataType::IntervalYearToMonth(iym);
608        assert_eq!(
609            data_type.full_name().as_ref(),
610            format!(
611                "INTERVAL YEAR({}) TO MONTH",
612                IntervalYearToMonth::MAX_PRECISION,
613            )
614        );
615    }
616
617    #[test]
618    fn test_timestamp_name() {
619        let data_type = ExaDataType::Timestamp;
620        assert_eq!(data_type.full_name().as_ref(), "TIMESTAMP");
621    }
622
623    #[test]
624    fn test_timestamp_with_tz_name() {
625        let data_type = ExaDataType::TimestampWithLocalTimeZone;
626        assert_eq!(
627            data_type.full_name().as_ref(),
628            "TIMESTAMP WITH LOCAL TIME ZONE"
629        );
630    }
631
632    #[test]
633    fn test_max_varchar_name() {
634        let string_like = StringLike::new(StringLike::MAX_VARCHAR_LEN, Charset::Ascii);
635        let data_type = ExaDataType::Varchar(string_like);
636        assert_eq!(
637            data_type.full_name().as_ref(),
638            format!("VARCHAR({}) ASCII", StringLike::MAX_VARCHAR_LEN)
639        );
640    }
641}