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    /// Parses a type name from a column attribute, ignoring case. Returns
108    /// `None` for an unknown name.
109    ///
110    /// Unsigned types use an `_unsigned` suffix (`"int_unsigned"`), and
111    /// common aliases are accepted (`"integer"`, `"numeric"`, `"bool"`,
112    /// `"character_varying"`, ...). `ENUM` and `SET` need their values, so
113    /// build them with [`Self::enum_values`] or [`Self::set_values`] instead.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use drizzle_types::mysql::MySQLType;
119    ///
120    /// assert_eq!(MySQLType::parse_attribute("int_unsigned"), Some(MySQLType::IntUnsigned));
121    /// assert_eq!(MySQLType::parse_attribute("Integer"), Some(MySQLType::Int));
122    /// assert_eq!(MySQLType::parse_attribute("enum"), None);
123    /// ```
124    #[must_use]
125    pub const fn parse_attribute(name: &str) -> Option<Self> {
126        if name.eq_ignore_ascii_case("tinyint") {
127            Some(Self::Tinyint)
128        } else if name.eq_ignore_ascii_case("tinyint_unsigned") {
129            Some(Self::TinyintUnsigned)
130        } else if name.eq_ignore_ascii_case("smallint") {
131            Some(Self::Smallint)
132        } else if name.eq_ignore_ascii_case("smallint_unsigned") {
133            Some(Self::SmallintUnsigned)
134        } else if name.eq_ignore_ascii_case("mediumint") {
135            Some(Self::Mediumint)
136        } else if name.eq_ignore_ascii_case("mediumint_unsigned") {
137            Some(Self::MediumintUnsigned)
138        } else if name.eq_ignore_ascii_case("int") || name.eq_ignore_ascii_case("integer") {
139            Some(Self::Int)
140        } else if name.eq_ignore_ascii_case("int_unsigned")
141            || name.eq_ignore_ascii_case("integer_unsigned")
142        {
143            Some(Self::IntUnsigned)
144        } else if name.eq_ignore_ascii_case("bigint") {
145            Some(Self::Bigint)
146        } else if name.eq_ignore_ascii_case("bigint_unsigned") {
147            Some(Self::BigintUnsigned)
148        } else if name.eq_ignore_ascii_case("decimal")
149            || name.eq_ignore_ascii_case("numeric")
150            || name.eq_ignore_ascii_case("dec")
151            || name.eq_ignore_ascii_case("fixed")
152        {
153            Some(Self::Decimal)
154        } else if name.eq_ignore_ascii_case("decimal_unsigned")
155            || name.eq_ignore_ascii_case("numeric_unsigned")
156            || name.eq_ignore_ascii_case("dec_unsigned")
157            || name.eq_ignore_ascii_case("fixed_unsigned")
158        {
159            Some(Self::DecimalUnsigned)
160        } else if name.eq_ignore_ascii_case("float") {
161            Some(Self::Float)
162        } else if name.eq_ignore_ascii_case("float_unsigned") {
163            Some(Self::FloatUnsigned)
164        } else if name.eq_ignore_ascii_case("double")
165            || name.eq_ignore_ascii_case("double_precision")
166        {
167            Some(Self::Double)
168        } else if name.eq_ignore_ascii_case("double_unsigned")
169            || name.eq_ignore_ascii_case("double_precision_unsigned")
170        {
171            Some(Self::DoubleUnsigned)
172        } else if name.eq_ignore_ascii_case("real") {
173            Some(Self::Real)
174        } else if name.eq_ignore_ascii_case("real_unsigned") {
175            Some(Self::RealUnsigned)
176        } else if name.eq_ignore_ascii_case("boolean") || name.eq_ignore_ascii_case("bool") {
177            Some(Self::Boolean)
178        } else if name.eq_ignore_ascii_case("bit") {
179            Some(Self::Bit)
180        } else if name.eq_ignore_ascii_case("char") || name.eq_ignore_ascii_case("character") {
181            Some(Self::Char)
182        } else if name.eq_ignore_ascii_case("varchar")
183            || name.eq_ignore_ascii_case("character_varying")
184        {
185            Some(Self::Varchar)
186        } else if name.eq_ignore_ascii_case("tinytext") {
187            Some(Self::Tinytext)
188        } else if name.eq_ignore_ascii_case("text") {
189            Some(Self::Text)
190        } else if name.eq_ignore_ascii_case("mediumtext") {
191            Some(Self::Mediumtext)
192        } else if name.eq_ignore_ascii_case("longtext") {
193            Some(Self::Longtext)
194        } else if name.eq_ignore_ascii_case("binary") {
195            Some(Self::Binary)
196        } else if name.eq_ignore_ascii_case("varbinary") {
197            Some(Self::Varbinary)
198        } else if name.eq_ignore_ascii_case("tinyblob") {
199            Some(Self::Tinyblob)
200        } else if name.eq_ignore_ascii_case("blob") {
201            Some(Self::Blob)
202        } else if name.eq_ignore_ascii_case("mediumblob") {
203            Some(Self::Mediumblob)
204        } else if name.eq_ignore_ascii_case("longblob") {
205            Some(Self::Longblob)
206        } else if name.eq_ignore_ascii_case("json") {
207            Some(Self::Json)
208        } else if name.eq_ignore_ascii_case("date") {
209            Some(Self::Date)
210        } else if name.eq_ignore_ascii_case("time") {
211            Some(Self::Time)
212        } else if name.eq_ignore_ascii_case("datetime") {
213            Some(Self::Datetime)
214        } else if name.eq_ignore_ascii_case("timestamp") {
215            Some(Self::Timestamp)
216        } else if name.eq_ignore_ascii_case("year") {
217            Some(Self::Year)
218        } else {
219            None
220        }
221    }
222
223    /// Creates an inline `ENUM` type from its allowed values, in order.
224    #[must_use]
225    pub fn enum_values<I, S>(values: I) -> Self
226    where
227        I: IntoIterator<Item = S>,
228        S: Into<Cow<'static, str>>,
229    {
230        Self::Enum(values.into_iter().map(Into::into).collect())
231    }
232
233    /// Creates an inline `SET` type from its allowed values.
234    #[must_use]
235    pub fn set_values<I, S>(values: I) -> Self
236    where
237        I: IntoIterator<Item = S>,
238        S: Into<Cow<'static, str>>,
239    {
240        Self::Set(values.into_iter().map(Into::into).collect())
241    }
242
243    /// Returns the values of an `ENUM` or `SET` type, or `None` for any other
244    /// type.
245    #[must_use]
246    pub fn inline_values(&self) -> Option<&[Cow<'static, str>]> {
247        match self {
248            Self::Enum(values) | Self::Set(values) => Some(values),
249            _ => None,
250        }
251    }
252
253    /// Returns the SQL type keyword, such as `"INT UNSIGNED"`, without any
254    /// arguments.
255    ///
256    /// `ENUM` and `SET` return just `"ENUM"` / `"SET"`; their values come
257    /// from [`Self::inline_values`].
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use drizzle_types::mysql::MySQLType;
263    ///
264    /// assert_eq!(MySQLType::BigintUnsigned.sql(), "BIGINT UNSIGNED");
265    /// assert_eq!(MySQLType::enum_values(["a", "b"]).sql(), "ENUM");
266    /// ```
267    #[must_use]
268    pub const fn sql(&self) -> &'static str {
269        match self {
270            Self::Tinyint => "TINYINT",
271            Self::TinyintUnsigned => "TINYINT UNSIGNED",
272            Self::Smallint => "SMALLINT",
273            Self::SmallintUnsigned => "SMALLINT UNSIGNED",
274            Self::Mediumint => "MEDIUMINT",
275            Self::MediumintUnsigned => "MEDIUMINT UNSIGNED",
276            Self::Int => "INT",
277            Self::IntUnsigned => "INT UNSIGNED",
278            Self::Bigint => "BIGINT",
279            Self::BigintUnsigned => "BIGINT UNSIGNED",
280            Self::Decimal => "DECIMAL",
281            Self::DecimalUnsigned => "DECIMAL UNSIGNED",
282            Self::Float => "FLOAT",
283            Self::FloatUnsigned => "FLOAT UNSIGNED",
284            Self::Double => "DOUBLE",
285            Self::DoubleUnsigned => "DOUBLE UNSIGNED",
286            Self::Real => "REAL",
287            Self::RealUnsigned => "REAL UNSIGNED",
288            Self::Boolean => "BOOLEAN",
289            Self::Bit => "BIT",
290            Self::Char => "CHAR",
291            Self::Varchar => "VARCHAR",
292            Self::Tinytext => "TINYTEXT",
293            Self::Text => "TEXT",
294            Self::Mediumtext => "MEDIUMTEXT",
295            Self::Longtext => "LONGTEXT",
296            Self::Binary => "BINARY",
297            Self::Varbinary => "VARBINARY",
298            Self::Tinyblob => "TINYBLOB",
299            Self::Blob => "BLOB",
300            Self::Mediumblob => "MEDIUMBLOB",
301            Self::Longblob => "LONGBLOB",
302            Self::Json => "JSON",
303            Self::Date => "DATE",
304            Self::Time => "TIME",
305            Self::Datetime => "DATETIME",
306            Self::Timestamp => "TIMESTAMP",
307            Self::Year => "YEAR",
308            Self::Enum(_) => "ENUM",
309            Self::Set(_) => "SET",
310        }
311    }
312
313    /// Returns `true` for the `... UNSIGNED` variants.
314    #[must_use]
315    pub const fn is_unsigned(&self) -> bool {
316        matches!(
317            self,
318            Self::TinyintUnsigned
319                | Self::SmallintUnsigned
320                | Self::MediumintUnsigned
321                | Self::IntUnsigned
322                | Self::BigintUnsigned
323                | Self::DecimalUnsigned
324                | Self::FloatUnsigned
325                | Self::DoubleUnsigned
326                | Self::RealUnsigned
327        )
328    }
329
330    /// Checks the type's arguments (length, width, precision/scale or
331    /// fractional-seconds precision), as in `VARCHAR(255)` or
332    /// `DECIMAL(10, 2)`. Returns an error message when they are invalid.
333    #[must_use]
334    #[doc(hidden)]
335    pub fn validate_args(&self, args: &[u16]) -> Option<&'static str> {
336        match self {
337            Self::Tinyint
338            | Self::TinyintUnsigned
339            | Self::Smallint
340            | Self::SmallintUnsigned
341            | Self::Mediumint
342            | Self::MediumintUnsigned
343            | Self::Int
344            | Self::IntUnsigned
345            | Self::Bigint
346            | Self::BigintUnsigned
347            | Self::Year
348                if args.len() > 1 =>
349            {
350                Some("integer and YEAR types accept at most one width argument")
351            }
352            Self::Varchar | Self::Varbinary if args.len() != 1 => {
353                Some("VARCHAR and VARBINARY require exactly one length argument")
354            }
355            Self::Char | Self::Binary if args.len() > 1 => {
356                Some("CHAR and BINARY accept at most one length argument")
357            }
358            Self::Bit if args.len() > 1 => Some("BIT accepts at most one width argument"),
359            Self::Decimal
360            | Self::DecimalUnsigned
361            | Self::Float
362            | Self::FloatUnsigned
363            | Self::Double
364            | Self::DoubleUnsigned
365            | Self::Real
366            | Self::RealUnsigned
367                if args.len() > 2 =>
368            {
369                Some("numeric types accept precision and optional scale")
370            }
371            Self::Double | Self::DoubleUnsigned | Self::Real | Self::RealUnsigned
372                if args.len() == 1 =>
373            {
374                Some("DOUBLE and REAL require both precision and scale")
375            }
376            Self::Time | Self::Datetime | Self::Timestamp if args.len() > 1 => {
377                Some("temporal types accept at most one fractional-seconds precision")
378            }
379            Self::Tinytext
380            | Self::Text
381            | Self::Mediumtext
382            | Self::Longtext
383            | Self::Tinyblob
384            | Self::Blob
385            | Self::Mediumblob
386            | Self::Longblob
387            | Self::Json
388            | Self::Date
389            | Self::Boolean
390                if !args.is_empty() =>
391            {
392                Some("this MySQL type does not accept arguments")
393            }
394            Self::Bit if args.first().is_some_and(|value| !(1..=64).contains(value)) => {
395                Some("BIT width must be between 1 and 64")
396            }
397            Self::Char | Self::Binary if args.first().is_some_and(|value| *value > 255) => {
398                Some("CHAR/BINARY length must not exceed 255")
399            }
400            Self::Decimal | Self::DecimalUnsigned
401                if args.first().is_some_and(|value| !(1..=65).contains(value)) =>
402            {
403                Some("DECIMAL precision must be between 1 and 65")
404            }
405            Self::Decimal | Self::DecimalUnsigned
406                if args.get(1).is_some_and(|scale| {
407                    *scale > 30 || args.first().is_some_and(|precision| scale > precision)
408                }) =>
409            {
410                Some("DECIMAL scale must not exceed 30 or its precision")
411            }
412            Self::Float | Self::FloatUnsigned if args.len() == 1 && args[0] > 24 => {
413                Some("FLOAT binary precision must not exceed 24; use DOUBLE for double precision")
414            }
415            Self::Float | Self::FloatUnsigned if args.len() == 2 && args[0] > 255 => {
416                Some("FLOAT display width must not exceed 255")
417            }
418            Self::Double | Self::DoubleUnsigned | Self::Real | Self::RealUnsigned
419                if args.first().is_some_and(|precision| *precision > 255) =>
420            {
421                Some("FLOAT, DOUBLE, and REAL precision must not exceed 255")
422            }
423            Self::Float
424            | Self::FloatUnsigned
425            | Self::Double
426            | Self::DoubleUnsigned
427            | Self::Real
428            | Self::RealUnsigned
429                if args.get(1).is_some_and(|scale| {
430                    *scale > 30 || args.first().is_some_and(|precision| scale > precision)
431                }) =>
432            {
433                Some("FLOAT, DOUBLE, and REAL scale must not exceed 30 or its precision")
434            }
435            Self::Time | Self::Datetime | Self::Timestamp
436                if args.first().is_some_and(|value| *value > 6) =>
437            {
438                Some("fractional-seconds precision must be between 0 and 6")
439            }
440            _ => None,
441        }
442    }
443
444    /// Returns `true` for the integer types, which can be `AUTO_INCREMENT`.
445    #[must_use]
446    pub const fn supports_auto_increment(&self) -> bool {
447        matches!(
448            self,
449            Self::Tinyint
450                | Self::TinyintUnsigned
451                | Self::Smallint
452                | Self::SmallintUnsigned
453                | Self::Mediumint
454                | Self::MediumintUnsigned
455                | Self::Int
456                | Self::IntUnsigned
457                | Self::Bigint
458                | Self::BigintUnsigned
459        )
460    }
461
462    /// Returns whether a column of this type can be a generated column.
463    /// Always `true`: MySQL restricts the expression, not the column type.
464    #[must_use]
465    pub const fn supports_generated_columns(&self) -> bool {
466        true
467    }
468
469    /// Returns whether the column attribute `flag` (such as `"unique"` or
470    /// `"auto_increment"`) is allowed on a column of this type. Unknown flags
471    /// return `false`.
472    ///
473    /// This checks the type alone. Rules that involve two attributes, such
474    /// as no `AUTO_INCREMENT` on a generated column, are checked elsewhere.
475    #[must_use]
476    pub fn is_valid_flag(&self, flag: &str) -> bool {
477        match flag {
478            "primary" | "primary_key" | "unique" | "not_null" | "check" | "references"
479            | "default" | "default_fn" => true,
480            "autoincrement" | "auto_increment" => self.supports_auto_increment(),
481            "generated" | "generated_stored" | "generated_virtual" => {
482                self.supports_generated_columns()
483            }
484            "json" => matches!(self, Self::Json),
485            "enum" => matches!(self, Self::Enum(_)),
486            "set" => matches!(self, Self::Set(_)),
487            _ => false,
488        }
489    }
490}
491
492impl core::fmt::Display for MySQLType {
493    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
494        f.write_str(self.sql())
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn attribute_names_preserve_mysql_integer_signedness() {
504        assert_eq!(
505            MySQLType::parse_attribute("mediumint_unsigned"),
506            Some(MySQLType::MediumintUnsigned)
507        );
508        assert_eq!(MySQLType::parse_attribute("integer"), Some(MySQLType::Int));
509        assert_eq!(
510            MySQLType::parse_attribute("bigint_unsigned"),
511            Some(MySQLType::BigintUnsigned)
512        );
513        assert_eq!(MySQLType::parse_attribute("enum"), None);
514        assert_eq!(
515            MySQLType::parse_attribute("decimal_unsigned"),
516            Some(MySQLType::DecimalUnsigned)
517        );
518        assert_eq!(
519            MySQLType::parse_attribute("float_unsigned"),
520            Some(MySQLType::FloatUnsigned)
521        );
522        assert_eq!(MySQLType::parse_attribute("real"), Some(MySQLType::Real));
523        assert_eq!(
524            MySQLType::parse_attribute("real_unsigned"),
525            Some(MySQLType::RealUnsigned)
526        );
527    }
528
529    #[test]
530    fn inline_enum_and_set_values_remain_metadata() {
531        let state = MySQLType::enum_values(["draft", "published"]);
532        let tags = MySQLType::set_values(["rust", "sql"]);
533
534        assert_eq!(state.sql(), "ENUM");
535        assert_eq!(
536            state.inline_values(),
537            Some(&[Cow::Borrowed("draft"), Cow::Borrowed("published")][..])
538        );
539        assert_eq!(tags.sql(), "SET");
540        assert_eq!(
541            tags.inline_values(),
542            Some(&[Cow::Borrowed("rust"), Cow::Borrowed("sql")][..])
543        );
544    }
545
546    #[test]
547    fn mysql_column_capabilities_are_type_specific() {
548        assert!(MySQLType::BigintUnsigned.supports_auto_increment());
549        assert!(!MySQLType::Decimal.supports_auto_increment());
550        assert!(MySQLType::DecimalUnsigned.is_unsigned());
551        assert!(MySQLType::FloatUnsigned.is_unsigned());
552        assert!(MySQLType::DoubleUnsigned.is_unsigned());
553        assert!(MySQLType::RealUnsigned.is_unsigned());
554        assert!(!MySQLType::Boolean.supports_auto_increment());
555        assert!(MySQLType::Json.supports_generated_columns());
556        assert!(MySQLType::Int.is_valid_flag("auto_increment"));
557        assert!(!MySQLType::Text.is_valid_flag("auto_increment"));
558        assert!(MySQLType::enum_values(["one"]).is_valid_flag("enum"));
559    }
560}