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]
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
443 pub const fn supports_generated_columns(&self) -> bool {
444 true
445 }
446
447 #[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}