1use crate::alloc_prelude::{Cow, Vec};
9
10#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum MySQLType {
23 Tinyint,
25 TinyintUnsigned,
27 Smallint,
29 SmallintUnsigned,
31 Mediumint,
33 MediumintUnsigned,
35 Int,
37 IntUnsigned,
39 Bigint,
41 BigintUnsigned,
43 Decimal,
45 DecimalUnsigned,
47 Float,
49 FloatUnsigned,
51 Double,
53 DoubleUnsigned,
55 Real,
57 RealUnsigned,
59 Boolean,
61 Bit,
63 Char,
65 Varchar,
67 Tinytext,
69 #[default]
71 Text,
72 Mediumtext,
74 Longtext,
76 Binary,
78 Varbinary,
80 Tinyblob,
82 Blob,
84 Mediumblob,
86 Longblob,
88 Json,
90 Date,
92 Time,
94 Datetime,
96 Timestamp,
98 Year,
100 Enum(Vec<Cow<'static, str>>),
102 Set(Vec<Cow<'static, str>>),
104}
105
106impl MySQLType {
107 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
465 pub const fn supports_generated_columns(&self) -> bool {
466 true
467 }
468
469 #[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}