Skip to main content

drizzle_types/mysql/
sql_type.rs

1//! MySQL column type declarations.
2//!
3//! [`MySQLType`] retains the MySQL information that cannot be reconstructed
4//! from a Rust marker, notably integer signedness and inline `ENUM`/`SET`
5//! values. Length, precision, scale, character set, collation, and column
6//! constraints belong to schema DDL metadata rather than this type enum.
7
8use crate::alloc_prelude::{Cow, Vec};
9
10/// A MySQL column type declaration.
11///
12/// This models MySQL 8.0.31's common native scalar, character, binary, JSON,
13/// temporal, and inline collection types. `ENUM` and `SET` values are kept as
14/// data because they are part of the column declaration, not named schema
15/// objects as they are in PostgreSQL.
16///
17/// [`Self::sql`] returns a type keyword. DDL renderers must append
18/// [`Self::inline_values`] for `ENUM` and `SET`, because correct literal
19/// escaping belongs to the DDL renderer's SQL-mode policy.
20#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum MySQLType {
23    /// `TINYINT`, a signed 1-byte integer.
24    Tinyint,
25    /// `TINYINT UNSIGNED`, an unsigned 1-byte integer.
26    TinyintUnsigned,
27    /// `SMALLINT`, a signed 2-byte integer.
28    Smallint,
29    /// `SMALLINT UNSIGNED`, an unsigned 2-byte integer.
30    SmallintUnsigned,
31    /// `MEDIUMINT`, a signed 3-byte integer.
32    Mediumint,
33    /// `MEDIUMINT UNSIGNED`, an unsigned 3-byte integer.
34    MediumintUnsigned,
35    /// `INT`, a signed 4-byte integer.
36    Int,
37    /// `INT UNSIGNED`, an unsigned 4-byte integer.
38    IntUnsigned,
39    /// `BIGINT`, a signed 8-byte integer.
40    Bigint,
41    /// `BIGINT UNSIGNED`, an unsigned 8-byte integer.
42    BigintUnsigned,
43    /// `DECIMAL`, an exact fixed-point number.
44    Decimal,
45    /// `DECIMAL UNSIGNED`, a non-negative exact fixed-point number.
46    DecimalUnsigned,
47    /// `FLOAT`, a single-precision approximate number.
48    Float,
49    /// `FLOAT UNSIGNED`, a non-negative single-precision approximate number.
50    FloatUnsigned,
51    /// `DOUBLE`, a double-precision approximate number.
52    Double,
53    /// `DOUBLE UNSIGNED`, a non-negative double-precision approximate number.
54    DoubleUnsigned,
55    /// `REAL`, MySQL's SQL-mode-sensitive approximate number spelling.
56    Real,
57    /// `REAL UNSIGNED`, the non-negative form of MySQL's `REAL` spelling.
58    RealUnsigned,
59    /// `BOOLEAN`, an alias for `TINYINT(1)` in MySQL.
60    Boolean,
61    /// `BIT`, a bit-value type.
62    Bit,
63    /// `CHAR`, a fixed-length character string.
64    Char,
65    /// `VARCHAR`, a variable-length character string.
66    Varchar,
67    /// `TINYTEXT`.
68    Tinytext,
69    /// `TEXT`.
70    #[default]
71    Text,
72    /// `MEDIUMTEXT`.
73    Mediumtext,
74    /// `LONGTEXT`.
75    Longtext,
76    /// `BINARY`, a fixed-length binary string.
77    Binary,
78    /// `VARBINARY`, a variable-length binary string.
79    Varbinary,
80    /// `TINYBLOB`.
81    Tinyblob,
82    /// `BLOB`.
83    Blob,
84    /// `MEDIUMBLOB`.
85    Mediumblob,
86    /// `LONGBLOB`.
87    Longblob,
88    /// MySQL's native binary JSON type.
89    Json,
90    /// `DATE`.
91    Date,
92    /// `TIME`.
93    Time,
94    /// `DATETIME`, which does not apply time-zone conversion.
95    Datetime,
96    /// `TIMESTAMP`, which MySQL converts through the session time zone.
97    Timestamp,
98    /// `YEAR`.
99    Year,
100    /// A MySQL inline `ENUM` declaration and its ordered allowed values.
101    Enum(Vec<Cow<'static, str>>),
102    /// A MySQL inline `SET` declaration and its allowed values.
103    Set(Vec<Cow<'static, str>>),
104}
105
106impl MySQLType {
107    /// Parse a macro attribute name as a non-parameterized MySQL type.
108    ///
109    /// `ENUM` and `SET` require inline values, so construct them with
110    /// [`Self::enum_values`] or [`Self::set_values`] instead.
111    #[must_use]
112    pub const fn parse_attribute(name: &str) -> Option<Self> {
113        if name.eq_ignore_ascii_case("tinyint") {
114            Some(Self::Tinyint)
115        } else if name.eq_ignore_ascii_case("tinyint_unsigned") {
116            Some(Self::TinyintUnsigned)
117        } else if name.eq_ignore_ascii_case("smallint") {
118            Some(Self::Smallint)
119        } else if name.eq_ignore_ascii_case("smallint_unsigned") {
120            Some(Self::SmallintUnsigned)
121        } else if name.eq_ignore_ascii_case("mediumint") {
122            Some(Self::Mediumint)
123        } else if name.eq_ignore_ascii_case("mediumint_unsigned") {
124            Some(Self::MediumintUnsigned)
125        } else if name.eq_ignore_ascii_case("int") || name.eq_ignore_ascii_case("integer") {
126            Some(Self::Int)
127        } else if name.eq_ignore_ascii_case("int_unsigned")
128            || name.eq_ignore_ascii_case("integer_unsigned")
129        {
130            Some(Self::IntUnsigned)
131        } else if name.eq_ignore_ascii_case("bigint") {
132            Some(Self::Bigint)
133        } else if name.eq_ignore_ascii_case("bigint_unsigned") {
134            Some(Self::BigintUnsigned)
135        } else if name.eq_ignore_ascii_case("decimal")
136            || name.eq_ignore_ascii_case("numeric")
137            || name.eq_ignore_ascii_case("dec")
138            || name.eq_ignore_ascii_case("fixed")
139        {
140            Some(Self::Decimal)
141        } else if name.eq_ignore_ascii_case("decimal_unsigned")
142            || name.eq_ignore_ascii_case("numeric_unsigned")
143            || name.eq_ignore_ascii_case("dec_unsigned")
144            || name.eq_ignore_ascii_case("fixed_unsigned")
145        {
146            Some(Self::DecimalUnsigned)
147        } else if name.eq_ignore_ascii_case("float") {
148            Some(Self::Float)
149        } else if name.eq_ignore_ascii_case("float_unsigned") {
150            Some(Self::FloatUnsigned)
151        } else if name.eq_ignore_ascii_case("double")
152            || name.eq_ignore_ascii_case("double_precision")
153        {
154            Some(Self::Double)
155        } else if name.eq_ignore_ascii_case("double_unsigned")
156            || name.eq_ignore_ascii_case("double_precision_unsigned")
157        {
158            Some(Self::DoubleUnsigned)
159        } else if name.eq_ignore_ascii_case("real") {
160            Some(Self::Real)
161        } else if name.eq_ignore_ascii_case("real_unsigned") {
162            Some(Self::RealUnsigned)
163        } else if name.eq_ignore_ascii_case("boolean") || name.eq_ignore_ascii_case("bool") {
164            Some(Self::Boolean)
165        } else if name.eq_ignore_ascii_case("bit") {
166            Some(Self::Bit)
167        } else if name.eq_ignore_ascii_case("char") || name.eq_ignore_ascii_case("character") {
168            Some(Self::Char)
169        } else if name.eq_ignore_ascii_case("varchar")
170            || name.eq_ignore_ascii_case("character_varying")
171        {
172            Some(Self::Varchar)
173        } else if name.eq_ignore_ascii_case("tinytext") {
174            Some(Self::Tinytext)
175        } else if name.eq_ignore_ascii_case("text") {
176            Some(Self::Text)
177        } else if name.eq_ignore_ascii_case("mediumtext") {
178            Some(Self::Mediumtext)
179        } else if name.eq_ignore_ascii_case("longtext") {
180            Some(Self::Longtext)
181        } else if name.eq_ignore_ascii_case("binary") {
182            Some(Self::Binary)
183        } else if name.eq_ignore_ascii_case("varbinary") {
184            Some(Self::Varbinary)
185        } else if name.eq_ignore_ascii_case("tinyblob") {
186            Some(Self::Tinyblob)
187        } else if name.eq_ignore_ascii_case("blob") {
188            Some(Self::Blob)
189        } else if name.eq_ignore_ascii_case("mediumblob") {
190            Some(Self::Mediumblob)
191        } else if name.eq_ignore_ascii_case("longblob") {
192            Some(Self::Longblob)
193        } else if name.eq_ignore_ascii_case("json") {
194            Some(Self::Json)
195        } else if name.eq_ignore_ascii_case("date") {
196            Some(Self::Date)
197        } else if name.eq_ignore_ascii_case("time") {
198            Some(Self::Time)
199        } else if name.eq_ignore_ascii_case("datetime") {
200            Some(Self::Datetime)
201        } else if name.eq_ignore_ascii_case("timestamp") {
202            Some(Self::Timestamp)
203        } else if name.eq_ignore_ascii_case("year") {
204            Some(Self::Year)
205        } else {
206            None
207        }
208    }
209
210    /// Build a MySQL inline `ENUM` declaration from its allowed values.
211    #[must_use]
212    pub fn enum_values<I, S>(values: I) -> Self
213    where
214        I: IntoIterator<Item = S>,
215        S: Into<Cow<'static, str>>,
216    {
217        Self::Enum(values.into_iter().map(Into::into).collect())
218    }
219
220    /// Build a MySQL inline `SET` declaration from its allowed values.
221    #[must_use]
222    pub fn set_values<I, S>(values: I) -> Self
223    where
224        I: IntoIterator<Item = S>,
225        S: Into<Cow<'static, str>>,
226    {
227        Self::Set(values.into_iter().map(Into::into).collect())
228    }
229
230    /// Return the inline values for an `ENUM` or `SET` declaration.
231    #[must_use]
232    pub fn inline_values(&self) -> Option<&[Cow<'static, str>]> {
233        match self {
234            Self::Enum(values) | Self::Set(values) => Some(values),
235            _ => None,
236        }
237    }
238
239    /// Return this declaration's SQL type keyword.
240    ///
241    /// For `ENUM` and `SET`, use [`Self::inline_values`] to render their
242    /// declaration values in a DDL renderer.
243    #[must_use]
244    pub const fn sql(&self) -> &'static str {
245        match self {
246            Self::Tinyint => "TINYINT",
247            Self::TinyintUnsigned => "TINYINT UNSIGNED",
248            Self::Smallint => "SMALLINT",
249            Self::SmallintUnsigned => "SMALLINT UNSIGNED",
250            Self::Mediumint => "MEDIUMINT",
251            Self::MediumintUnsigned => "MEDIUMINT UNSIGNED",
252            Self::Int => "INT",
253            Self::IntUnsigned => "INT UNSIGNED",
254            Self::Bigint => "BIGINT",
255            Self::BigintUnsigned => "BIGINT UNSIGNED",
256            Self::Decimal => "DECIMAL",
257            Self::DecimalUnsigned => "DECIMAL UNSIGNED",
258            Self::Float => "FLOAT",
259            Self::FloatUnsigned => "FLOAT UNSIGNED",
260            Self::Double => "DOUBLE",
261            Self::DoubleUnsigned => "DOUBLE UNSIGNED",
262            Self::Real => "REAL",
263            Self::RealUnsigned => "REAL UNSIGNED",
264            Self::Boolean => "BOOLEAN",
265            Self::Bit => "BIT",
266            Self::Char => "CHAR",
267            Self::Varchar => "VARCHAR",
268            Self::Tinytext => "TINYTEXT",
269            Self::Text => "TEXT",
270            Self::Mediumtext => "MEDIUMTEXT",
271            Self::Longtext => "LONGTEXT",
272            Self::Binary => "BINARY",
273            Self::Varbinary => "VARBINARY",
274            Self::Tinyblob => "TINYBLOB",
275            Self::Blob => "BLOB",
276            Self::Mediumblob => "MEDIUMBLOB",
277            Self::Longblob => "LONGBLOB",
278            Self::Json => "JSON",
279            Self::Date => "DATE",
280            Self::Time => "TIME",
281            Self::Datetime => "DATETIME",
282            Self::Timestamp => "TIMESTAMP",
283            Self::Year => "YEAR",
284            Self::Enum(_) => "ENUM",
285            Self::Set(_) => "SET",
286        }
287    }
288
289    /// Return whether this declaration carries MySQL's `UNSIGNED` modifier.
290    #[must_use]
291    pub const fn is_unsigned(&self) -> bool {
292        matches!(
293            self,
294            Self::TinyintUnsigned
295                | Self::SmallintUnsigned
296                | Self::MediumintUnsigned
297                | Self::IntUnsigned
298                | Self::BigintUnsigned
299                | Self::DecimalUnsigned
300                | Self::FloatUnsigned
301                | Self::DoubleUnsigned
302                | Self::RealUnsigned
303        )
304    }
305
306    /// Validate optional numeric, length, width, or temporal arguments.
307    #[must_use]
308    #[doc(hidden)]
309    pub fn validate_args(&self, args: &[u16]) -> Option<&'static str> {
310        match self {
311            Self::Tinyint
312            | Self::TinyintUnsigned
313            | Self::Smallint
314            | Self::SmallintUnsigned
315            | Self::Mediumint
316            | Self::MediumintUnsigned
317            | Self::Int
318            | Self::IntUnsigned
319            | Self::Bigint
320            | Self::BigintUnsigned
321            | Self::Year
322                if args.len() > 1 =>
323            {
324                Some("integer and YEAR types accept at most one width argument")
325            }
326            Self::Varchar | Self::Varbinary if args.len() != 1 => {
327                Some("VARCHAR and VARBINARY require exactly one length argument")
328            }
329            Self::Char | Self::Binary if args.len() > 1 => {
330                Some("CHAR and BINARY accept at most one length argument")
331            }
332            Self::Bit if args.len() > 1 => Some("BIT accepts at most one width argument"),
333            Self::Decimal
334            | Self::DecimalUnsigned
335            | Self::Float
336            | Self::FloatUnsigned
337            | Self::Double
338            | Self::DoubleUnsigned
339            | Self::Real
340            | Self::RealUnsigned
341                if args.len() > 2 =>
342            {
343                Some("numeric types accept precision and optional scale")
344            }
345            Self::Double | Self::DoubleUnsigned | Self::Real | Self::RealUnsigned
346                if args.len() == 1 =>
347            {
348                Some("DOUBLE and REAL require both precision and scale")
349            }
350            Self::Time | Self::Datetime | Self::Timestamp if args.len() > 1 => {
351                Some("temporal types accept at most one fractional-seconds precision")
352            }
353            Self::Tinytext
354            | Self::Text
355            | Self::Mediumtext
356            | Self::Longtext
357            | Self::Tinyblob
358            | Self::Blob
359            | Self::Mediumblob
360            | Self::Longblob
361            | Self::Json
362            | Self::Date
363            | Self::Boolean
364                if !args.is_empty() =>
365            {
366                Some("this MySQL type does not accept arguments")
367            }
368            Self::Bit if args.first().is_some_and(|value| !(1..=64).contains(value)) => {
369                Some("BIT width must be between 1 and 64")
370            }
371            Self::Char | Self::Binary if args.first().is_some_and(|value| *value > 255) => {
372                Some("CHAR/BINARY length must not exceed 255")
373            }
374            Self::Decimal | Self::DecimalUnsigned
375                if args.first().is_some_and(|value| !(1..=65).contains(value)) =>
376            {
377                Some("DECIMAL precision must be between 1 and 65")
378            }
379            Self::Decimal | Self::DecimalUnsigned
380                if args.get(1).is_some_and(|scale| {
381                    *scale > 30 || args.first().is_some_and(|precision| scale > precision)
382                }) =>
383            {
384                Some("DECIMAL scale must not exceed 30 or its precision")
385            }
386            Self::Float | Self::FloatUnsigned if args.len() == 1 && args[0] > 24 => {
387                Some("FLOAT binary precision must not exceed 24; use DOUBLE for double precision")
388            }
389            Self::Float | Self::FloatUnsigned if args.len() == 2 && args[0] > 255 => {
390                Some("FLOAT display width must not exceed 255")
391            }
392            Self::Double | Self::DoubleUnsigned | Self::Real | Self::RealUnsigned
393                if args.first().is_some_and(|precision| *precision > 255) =>
394            {
395                Some("FLOAT, DOUBLE, and REAL precision must not exceed 255")
396            }
397            Self::Float
398            | Self::FloatUnsigned
399            | Self::Double
400            | Self::DoubleUnsigned
401            | Self::Real
402            | Self::RealUnsigned
403                if args.get(1).is_some_and(|scale| {
404                    *scale > 30 || args.first().is_some_and(|precision| scale > precision)
405                }) =>
406            {
407                Some("FLOAT, DOUBLE, and REAL scale must not exceed 30 or its precision")
408            }
409            Self::Time | Self::Datetime | Self::Timestamp
410                if args.first().is_some_and(|value| *value > 6) =>
411            {
412                Some("fractional-seconds precision must be between 0 and 6")
413            }
414            _ => None,
415        }
416    }
417
418    /// Return whether this is an integer declaration eligible for
419    /// `AUTO_INCREMENT`.
420    #[must_use]
421    pub const fn supports_auto_increment(&self) -> bool {
422        matches!(
423            self,
424            Self::Tinyint
425                | Self::TinyintUnsigned
426                | Self::Smallint
427                | Self::SmallintUnsigned
428                | Self::Mediumint
429                | Self::MediumintUnsigned
430                | Self::Int
431                | Self::IntUnsigned
432                | Self::Bigint
433                | Self::BigintUnsigned
434        )
435    }
436
437    /// Return whether this declaration can be the declared type of a MySQL
438    /// generated column.
439    ///
440    /// MySQL's generated-column restrictions concern the expression and its
441    /// other column attributes rather than one of these native type families.
442    #[must_use]
443    pub const fn supports_generated_columns(&self) -> bool {
444        true
445    }
446
447    /// Return whether a column attribute is compatible with this type alone.
448    ///
449    /// Constraints involving multiple attributes, such as the prohibition on
450    /// an `AUTO_INCREMENT` generated column, belong to a column DDL model.
451    #[must_use]
452    pub fn is_valid_flag(&self, flag: &str) -> bool {
453        match flag {
454            "primary" | "primary_key" | "unique" | "not_null" | "check" | "references"
455            | "default" | "default_fn" => true,
456            "autoincrement" | "auto_increment" => self.supports_auto_increment(),
457            "generated" | "generated_stored" | "generated_virtual" => {
458                self.supports_generated_columns()
459            }
460            "json" => matches!(self, Self::Json),
461            "enum" => matches!(self, Self::Enum(_)),
462            "set" => matches!(self, Self::Set(_)),
463            _ => false,
464        }
465    }
466}
467
468impl core::fmt::Display for MySQLType {
469    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
470        f.write_str(self.sql())
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn attribute_names_preserve_mysql_integer_signedness() {
480        assert_eq!(
481            MySQLType::parse_attribute("mediumint_unsigned"),
482            Some(MySQLType::MediumintUnsigned)
483        );
484        assert_eq!(MySQLType::parse_attribute("integer"), Some(MySQLType::Int));
485        assert_eq!(
486            MySQLType::parse_attribute("bigint_unsigned"),
487            Some(MySQLType::BigintUnsigned)
488        );
489        assert_eq!(MySQLType::parse_attribute("enum"), None);
490        assert_eq!(
491            MySQLType::parse_attribute("decimal_unsigned"),
492            Some(MySQLType::DecimalUnsigned)
493        );
494        assert_eq!(
495            MySQLType::parse_attribute("float_unsigned"),
496            Some(MySQLType::FloatUnsigned)
497        );
498        assert_eq!(MySQLType::parse_attribute("real"), Some(MySQLType::Real));
499        assert_eq!(
500            MySQLType::parse_attribute("real_unsigned"),
501            Some(MySQLType::RealUnsigned)
502        );
503    }
504
505    #[test]
506    fn inline_enum_and_set_values_remain_metadata() {
507        let state = MySQLType::enum_values(["draft", "published"]);
508        let tags = MySQLType::set_values(["rust", "sql"]);
509
510        assert_eq!(state.sql(), "ENUM");
511        assert_eq!(
512            state.inline_values(),
513            Some(&[Cow::Borrowed("draft"), Cow::Borrowed("published")][..])
514        );
515        assert_eq!(tags.sql(), "SET");
516        assert_eq!(
517            tags.inline_values(),
518            Some(&[Cow::Borrowed("rust"), Cow::Borrowed("sql")][..])
519        );
520    }
521
522    #[test]
523    fn mysql_column_capabilities_are_type_specific() {
524        assert!(MySQLType::BigintUnsigned.supports_auto_increment());
525        assert!(!MySQLType::Decimal.supports_auto_increment());
526        assert!(MySQLType::DecimalUnsigned.is_unsigned());
527        assert!(MySQLType::FloatUnsigned.is_unsigned());
528        assert!(MySQLType::DoubleUnsigned.is_unsigned());
529        assert!(MySQLType::RealUnsigned.is_unsigned());
530        assert!(!MySQLType::Boolean.supports_auto_increment());
531        assert!(MySQLType::Json.supports_generated_columns());
532        assert!(MySQLType::Int.is_valid_flag("auto_increment"));
533        assert!(!MySQLType::Text.is_valid_flag("auto_increment"));
534        assert!(MySQLType::enum_values(["one"]).is_valid_flag("enum"));
535    }
536}