Skip to main content

sqlmodel_mysql/
types.rs

1//! MySQL type system and type conversion.
2//!
3//! This module provides:
4//! - MySQL field type constants
5//! - Encoding/decoding between Rust types and MySQL wire format
6//! - Type information for column definitions
7//!
8//! # MySQL Type System
9//!
10//! MySQL uses field type codes in result sets and binary protocol.
11//! The encoding differs between text protocol (all strings) and
12//! binary protocol (type-specific binary encoding).
13
14#![allow(clippy::cast_possible_truncation)]
15
16use sqlmodel_core::Value;
17
18/// MySQL field type codes.
19///
20/// These are the `MYSQL_TYPE_*` constants from the MySQL C API.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[repr(u8)]
23pub enum FieldType {
24    /// DECIMAL (MYSQL_TYPE_DECIMAL)
25    Decimal = 0x00,
26    /// TINYINT (MYSQL_TYPE_TINY)
27    Tiny = 0x01,
28    /// SMALLINT (MYSQL_TYPE_SHORT)
29    Short = 0x02,
30    /// INT (MYSQL_TYPE_LONG)
31    Long = 0x03,
32    /// FLOAT (MYSQL_TYPE_FLOAT)
33    Float = 0x04,
34    /// DOUBLE (MYSQL_TYPE_DOUBLE)
35    Double = 0x05,
36    /// NULL (MYSQL_TYPE_NULL)
37    Null = 0x06,
38    /// TIMESTAMP (MYSQL_TYPE_TIMESTAMP)
39    Timestamp = 0x07,
40    /// BIGINT (MYSQL_TYPE_LONGLONG)
41    LongLong = 0x08,
42    /// MEDIUMINT (MYSQL_TYPE_INT24)
43    Int24 = 0x09,
44    /// DATE (MYSQL_TYPE_DATE)
45    Date = 0x0A,
46    /// TIME (MYSQL_TYPE_TIME)
47    Time = 0x0B,
48    /// DATETIME (MYSQL_TYPE_DATETIME)
49    DateTime = 0x0C,
50    /// YEAR (MYSQL_TYPE_YEAR)
51    Year = 0x0D,
52    /// NEWDATE (MYSQL_TYPE_NEWDATE) - internal use
53    NewDate = 0x0E,
54    /// VARCHAR (MYSQL_TYPE_VARCHAR)
55    VarChar = 0x0F,
56    /// BIT (MYSQL_TYPE_BIT)
57    Bit = 0x10,
58    /// TIMESTAMP2 (MYSQL_TYPE_TIMESTAMP2) - MySQL 5.6+
59    Timestamp2 = 0x11,
60    /// DATETIME2 (MYSQL_TYPE_DATETIME2) - MySQL 5.6+
61    DateTime2 = 0x12,
62    /// TIME2 (MYSQL_TYPE_TIME2) - MySQL 5.6+
63    Time2 = 0x13,
64    /// JSON (MYSQL_TYPE_JSON) - MySQL 5.7.8+
65    Json = 0xF5,
66    /// NEWDECIMAL (MYSQL_TYPE_NEWDECIMAL)
67    NewDecimal = 0xF6,
68    /// ENUM (MYSQL_TYPE_ENUM)
69    Enum = 0xF7,
70    /// SET (MYSQL_TYPE_SET)
71    Set = 0xF8,
72    /// TINYBLOB (MYSQL_TYPE_TINY_BLOB)
73    TinyBlob = 0xF9,
74    /// MEDIUMBLOB (MYSQL_TYPE_MEDIUM_BLOB)
75    MediumBlob = 0xFA,
76    /// LONGBLOB (MYSQL_TYPE_LONG_BLOB)
77    LongBlob = 0xFB,
78    /// BLOB (MYSQL_TYPE_BLOB)
79    Blob = 0xFC,
80    /// VARCHAR (MYSQL_TYPE_VAR_STRING)
81    VarString = 0xFD,
82    /// CHAR (MYSQL_TYPE_STRING)
83    String = 0xFE,
84    /// GEOMETRY (MYSQL_TYPE_GEOMETRY)
85    Geometry = 0xFF,
86}
87
88impl FieldType {
89    /// Parse a field type from a byte.
90    #[must_use]
91    pub fn from_u8(value: u8) -> Self {
92        match value {
93            0x00 => FieldType::Decimal,
94            0x01 => FieldType::Tiny,
95            0x02 => FieldType::Short,
96            0x03 => FieldType::Long,
97            0x04 => FieldType::Float,
98            0x05 => FieldType::Double,
99            0x06 => FieldType::Null,
100            0x07 => FieldType::Timestamp,
101            0x08 => FieldType::LongLong,
102            0x09 => FieldType::Int24,
103            0x0A => FieldType::Date,
104            0x0B => FieldType::Time,
105            0x0C => FieldType::DateTime,
106            0x0D => FieldType::Year,
107            0x0E => FieldType::NewDate,
108            0x0F => FieldType::VarChar,
109            0x10 => FieldType::Bit,
110            0x11 => FieldType::Timestamp2,
111            0x12 => FieldType::DateTime2,
112            0x13 => FieldType::Time2,
113            0xF5 => FieldType::Json,
114            0xF6 => FieldType::NewDecimal,
115            0xF7 => FieldType::Enum,
116            0xF8 => FieldType::Set,
117            0xF9 => FieldType::TinyBlob,
118            0xFA => FieldType::MediumBlob,
119            0xFB => FieldType::LongBlob,
120            0xFC => FieldType::Blob,
121            0xFD => FieldType::VarString,
122            0xFE => FieldType::String,
123            0xFF => FieldType::Geometry,
124            _ => FieldType::String, // Unknown types treated as string
125        }
126    }
127
128    /// Check if this is an integer type.
129    #[must_use]
130    pub const fn is_integer(self) -> bool {
131        matches!(
132            self,
133            FieldType::Tiny
134                | FieldType::Short
135                | FieldType::Long
136                | FieldType::LongLong
137                | FieldType::Int24
138                | FieldType::Year
139        )
140    }
141
142    /// Check if this is a floating-point type.
143    #[must_use]
144    pub const fn is_float(self) -> bool {
145        matches!(self, FieldType::Float | FieldType::Double)
146    }
147
148    /// Check if this is a decimal type.
149    #[must_use]
150    pub const fn is_decimal(self) -> bool {
151        matches!(self, FieldType::Decimal | FieldType::NewDecimal)
152    }
153
154    /// Check if this is a string type.
155    #[must_use]
156    pub const fn is_string(self) -> bool {
157        matches!(
158            self,
159            FieldType::VarChar
160                | FieldType::VarString
161                | FieldType::String
162                | FieldType::Enum
163                | FieldType::Set
164        )
165    }
166
167    /// Check if this is a binary/blob type.
168    #[must_use]
169    pub const fn is_blob(self) -> bool {
170        matches!(
171            self,
172            FieldType::TinyBlob
173                | FieldType::MediumBlob
174                | FieldType::LongBlob
175                | FieldType::Blob
176                | FieldType::Geometry
177        )
178    }
179
180    /// Check if this is a date/time type.
181    #[must_use]
182    pub const fn is_temporal(self) -> bool {
183        matches!(
184            self,
185            FieldType::Date
186                | FieldType::Time
187                | FieldType::DateTime
188                | FieldType::Timestamp
189                | FieldType::NewDate
190                | FieldType::Timestamp2
191                | FieldType::DateTime2
192                | FieldType::Time2
193        )
194    }
195
196    /// Get the type name as a string.
197    #[must_use]
198    pub const fn name(self) -> &'static str {
199        match self {
200            FieldType::Decimal => "DECIMAL",
201            FieldType::Tiny => "TINYINT",
202            FieldType::Short => "SMALLINT",
203            FieldType::Long => "INT",
204            FieldType::Float => "FLOAT",
205            FieldType::Double => "DOUBLE",
206            FieldType::Null => "NULL",
207            FieldType::Timestamp => "TIMESTAMP",
208            FieldType::LongLong => "BIGINT",
209            FieldType::Int24 => "MEDIUMINT",
210            FieldType::Date => "DATE",
211            FieldType::Time => "TIME",
212            FieldType::DateTime => "DATETIME",
213            FieldType::Year => "YEAR",
214            FieldType::NewDate => "DATE",
215            FieldType::VarChar => "VARCHAR",
216            FieldType::Bit => "BIT",
217            FieldType::Timestamp2 => "TIMESTAMP",
218            FieldType::DateTime2 => "DATETIME",
219            FieldType::Time2 => "TIME",
220            FieldType::Json => "JSON",
221            FieldType::NewDecimal => "DECIMAL",
222            FieldType::Enum => "ENUM",
223            FieldType::Set => "SET",
224            FieldType::TinyBlob => "TINYBLOB",
225            FieldType::MediumBlob => "MEDIUMBLOB",
226            FieldType::LongBlob => "LONGBLOB",
227            FieldType::Blob => "BLOB",
228            FieldType::VarString => "VARCHAR",
229            FieldType::String => "CHAR",
230            FieldType::Geometry => "GEOMETRY",
231        }
232    }
233}
234
235/// Column flags in result set metadata.
236#[allow(dead_code)]
237pub mod column_flags {
238    pub const NOT_NULL: u16 = 1;
239    pub const PRIMARY_KEY: u16 = 2;
240    pub const UNIQUE_KEY: u16 = 4;
241    pub const MULTIPLE_KEY: u16 = 8;
242    pub const BLOB: u16 = 16;
243    pub const UNSIGNED: u16 = 32;
244    pub const ZEROFILL: u16 = 64;
245    pub const BINARY: u16 = 128;
246    pub const ENUM: u16 = 256;
247    pub const AUTO_INCREMENT: u16 = 512;
248    pub const TIMESTAMP: u16 = 1024;
249    pub const SET: u16 = 2048;
250    pub const NO_DEFAULT_VALUE: u16 = 4096;
251    pub const ON_UPDATE_NOW: u16 = 8192;
252    pub const NUM: u16 = 32768;
253}
254
255/// Column definition from a result set.
256#[derive(Debug, Clone)]
257pub struct ColumnDef {
258    /// Catalog name (always "def")
259    pub catalog: String,
260    /// Schema (database) name
261    pub schema: String,
262    /// Table name (or alias)
263    pub table: String,
264    /// Original table name
265    pub org_table: String,
266    /// Column name (or alias)
267    pub name: String,
268    /// Original column name
269    pub org_name: String,
270    /// Character set number
271    pub charset: u16,
272    /// Column length
273    pub column_length: u32,
274    /// Column type
275    pub column_type: FieldType,
276    /// Column flags
277    pub flags: u16,
278    /// Number of decimals
279    pub decimals: u8,
280}
281
282impl ColumnDef {
283    /// Check if the column is NOT NULL.
284    #[must_use]
285    pub const fn is_not_null(&self) -> bool {
286        self.flags & column_flags::NOT_NULL != 0
287    }
288
289    /// Check if the column is a primary key.
290    #[must_use]
291    pub const fn is_primary_key(&self) -> bool {
292        self.flags & column_flags::PRIMARY_KEY != 0
293    }
294
295    /// Check if the column is unsigned.
296    #[must_use]
297    pub const fn is_unsigned(&self) -> bool {
298        self.flags & column_flags::UNSIGNED != 0
299    }
300
301    /// Check if the column is auto-increment.
302    #[must_use]
303    pub const fn is_auto_increment(&self) -> bool {
304        self.flags & column_flags::AUTO_INCREMENT != 0
305    }
306
307    /// Check if the column is binary.
308    #[must_use]
309    pub const fn is_binary(&self) -> bool {
310        self.flags & column_flags::BINARY != 0
311    }
312
313    /// Check if the column is a BLOB type.
314    #[must_use]
315    pub const fn is_blob(&self) -> bool {
316        self.flags & column_flags::BLOB != 0
317    }
318}
319
320/// Decode a text protocol value to a sqlmodel Value.
321///
322/// In text protocol, all values are transmitted as strings.
323/// This function parses the string based on the column type.
324pub fn decode_text_value(field_type: FieldType, data: &[u8], is_unsigned: bool) -> Value {
325    let text = String::from_utf8_lossy(data);
326
327    match field_type {
328        // TINYINT (8-bit)
329        FieldType::Tiny => {
330            if is_unsigned {
331                text.parse::<u8>().map_or_else(
332                    |_| Value::Text(text.into_owned()),
333                    |v| Value::TinyInt(v as i8),
334                )
335            } else {
336                text.parse::<i8>()
337                    .map_or_else(|_| Value::Text(text.into_owned()), Value::TinyInt)
338            }
339        }
340        // SMALLINT (16-bit)
341        FieldType::Short | FieldType::Year => {
342            if is_unsigned {
343                text.parse::<u16>().map_or_else(
344                    |_| Value::Text(text.into_owned()),
345                    |v| Value::SmallInt(v as i16),
346                )
347            } else {
348                text.parse::<i16>()
349                    .map_or_else(|_| Value::Text(text.into_owned()), Value::SmallInt)
350            }
351        }
352        // INT/MEDIUMINT (32-bit)
353        FieldType::Long | FieldType::Int24 => {
354            if is_unsigned {
355                text.parse::<u32>()
356                    .map_or_else(|_| Value::Text(text.into_owned()), |v| Value::Int(v as i32))
357            } else {
358                text.parse::<i32>()
359                    .map_or_else(|_| Value::Text(text.into_owned()), Value::Int)
360            }
361        }
362        // BIGINT (64-bit)
363        FieldType::LongLong => {
364            if is_unsigned {
365                text.parse::<u64>().map_or_else(
366                    |_| Value::Text(text.into_owned()),
367                    |v| Value::BigInt(v as i64),
368                )
369            } else {
370                text.parse::<i64>()
371                    .map_or_else(|_| Value::Text(text.into_owned()), Value::BigInt)
372            }
373        }
374
375        // FLOAT (32-bit)
376        FieldType::Float => text
377            .parse::<f32>()
378            .map_or_else(|_| Value::Text(text.into_owned()), Value::Float),
379
380        // DOUBLE (64-bit)
381        FieldType::Double => text
382            .parse::<f64>()
383            .map_or_else(|_| Value::Text(text.into_owned()), Value::Double),
384
385        // Decimal (keep as text to preserve precision)
386        FieldType::Decimal | FieldType::NewDecimal => Value::Text(text.into_owned()),
387
388        // Binary/blob types
389        FieldType::TinyBlob
390        | FieldType::MediumBlob
391        | FieldType::LongBlob
392        | FieldType::Blob
393        | FieldType::Geometry
394        | FieldType::Bit => Value::Bytes(data.to_vec()),
395
396        // JSON
397        FieldType::Json => {
398            // Try to parse as JSON, fall back to text
399            serde_json::from_str(&text).map_or_else(|_| Value::Text(text.into_owned()), Value::Json)
400        }
401
402        // NULL type
403        FieldType::Null => Value::Null,
404
405        // All other types (strings, dates, times) as text
406        _ => Value::Text(text.into_owned()),
407    }
408}
409
410/// Decode a binary protocol value to a sqlmodel Value.
411///
412/// In binary protocol, values are encoded in type-specific binary formats.
413pub fn decode_binary_value(field_type: FieldType, data: &[u8], is_unsigned: bool) -> Value {
414    match field_type {
415        // TINY (1 byte)
416        FieldType::Tiny => {
417            if data.is_empty() {
418                return Value::Null;
419            }
420            // Both signed and unsigned map to i8 (interpretation differs at application level)
421            let _ = is_unsigned;
422            Value::TinyInt(data[0] as i8)
423        }
424
425        // SHORT (2 bytes, little-endian)
426        FieldType::Short | FieldType::Year => {
427            if data.len() < 2 {
428                return Value::Null;
429            }
430            let val = u16::from_le_bytes([data[0], data[1]]);
431            // Both signed and unsigned map to i16 (interpretation differs at application level)
432            let _ = is_unsigned;
433            Value::SmallInt(val as i16)
434        }
435
436        // LONG/INT24 (4 bytes, little-endian)
437        FieldType::Long | FieldType::Int24 => {
438            if data.len() < 4 {
439                return Value::Null;
440            }
441            let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
442            // Both signed and unsigned map to i32 (interpretation differs at application level)
443            let _ = is_unsigned;
444            Value::Int(val as i32)
445        }
446
447        // LONGLONG (8 bytes, little-endian)
448        FieldType::LongLong => {
449            if data.len() < 8 {
450                return Value::Null;
451            }
452            let val = u64::from_le_bytes([
453                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
454            ]);
455            // Both signed and unsigned map to i64 (interpretation differs at application level)
456            let _ = is_unsigned;
457            Value::BigInt(val as i64)
458        }
459
460        // FLOAT (4 bytes)
461        FieldType::Float => {
462            if data.len() < 4 {
463                return Value::Null;
464            }
465            let val = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
466            Value::Float(val)
467        }
468
469        // DOUBLE (8 bytes)
470        FieldType::Double => {
471            if data.len() < 8 {
472                return Value::Null;
473            }
474            let val = f64::from_le_bytes([
475                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
476            ]);
477            Value::Double(val)
478        }
479
480        // Binary types
481        FieldType::TinyBlob
482        | FieldType::MediumBlob
483        | FieldType::LongBlob
484        | FieldType::Blob
485        | FieldType::Geometry
486        | FieldType::Bit => Value::Bytes(data.to_vec()),
487
488        // JSON
489        FieldType::Json => {
490            let text = String::from_utf8_lossy(data);
491            serde_json::from_str(&text).map_or_else(|_| Value::Bytes(data.to_vec()), Value::Json)
492        }
493
494        // Date/Time types - binary format encodes components
495        FieldType::Date
496        | FieldType::NewDate
497        | FieldType::Time
498        | FieldType::DateTime
499        | FieldType::Timestamp
500        | FieldType::Time2
501        | FieldType::DateTime2
502        | FieldType::Timestamp2 => {
503            // For now, keep as text (we'd need more complex parsing for binary date/time)
504            // The text representation is more portable
505            Value::Text(decode_binary_datetime(field_type, data))
506        }
507
508        // Decimal types - keep as text for precision
509        FieldType::Decimal | FieldType::NewDecimal => {
510            Value::Text(String::from_utf8_lossy(data).into_owned())
511        }
512
513        // String types
514        _ => Value::Text(String::from_utf8_lossy(data).into_owned()),
515    }
516}
517
518/// Decode binary date/time values to ISO format strings.
519fn decode_binary_datetime(field_type: FieldType, data: &[u8]) -> String {
520    match field_type {
521        FieldType::Date | FieldType::NewDate => {
522            if data.len() >= 4 {
523                let year = u16::from_le_bytes([data[0], data[1]]);
524                let month = data[2];
525                let day = data[3];
526                format!("{year:04}-{month:02}-{day:02}")
527            } else {
528                // Empty or insufficient data returns zero date
529                "0000-00-00".to_string()
530            }
531        }
532
533        FieldType::Time | FieldType::Time2 => {
534            if data.len() >= 8 {
535                let is_negative = data[0] != 0;
536                let days = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
537                let hours = data[5];
538                let minutes = data[6];
539                let seconds = data[7];
540                let total_hours = days * 24 + u32::from(hours);
541                let sign = if is_negative { "-" } else { "" };
542                format!("{sign}{total_hours:02}:{minutes:02}:{seconds:02}")
543            } else {
544                // Empty or insufficient data returns zero time
545                "00:00:00".to_string()
546            }
547        }
548
549        FieldType::DateTime
550        | FieldType::Timestamp
551        | FieldType::DateTime2
552        | FieldType::Timestamp2 => {
553            if data.len() >= 7 {
554                let year = u16::from_le_bytes([data[0], data[1]]);
555                let month = data[2];
556                let day = data[3];
557                let hour = data[4];
558                let minute = data[5];
559                let second = data[6];
560
561                if data.len() >= 11 {
562                    let microseconds = u32::from_le_bytes([data[7], data[8], data[9], data[10]]);
563                    format!(
564                        "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{microseconds:06}"
565                    )
566                } else {
567                    format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}")
568                }
569            } else if data.len() >= 4 {
570                let year = u16::from_le_bytes([data[0], data[1]]);
571                let month = data[2];
572                let day = data[3];
573                format!("{year:04}-{month:02}-{day:02} 00:00:00")
574            } else {
575                "0000-00-00 00:00:00".to_string()
576            }
577        }
578
579        _ => String::from_utf8_lossy(data).into_owned(),
580    }
581}
582
583/// Encode a sqlmodel Value for binary protocol.
584///
585/// Returns the encoded bytes for the value.
586pub fn encode_binary_value(value: &Value, field_type: FieldType) -> Vec<u8> {
587    match value {
588        Value::Null => vec![],
589
590        Value::Bool(b) => vec![u8::from(*b)],
591
592        Value::TinyInt(i) => vec![*i as u8],
593
594        Value::SmallInt(i) => i.to_le_bytes().to_vec(),
595
596        Value::Int(i) => i.to_le_bytes().to_vec(),
597
598        Value::BigInt(i) => match field_type {
599            FieldType::Tiny => vec![*i as u8],
600            FieldType::Short | FieldType::Year => (*i as i16).to_le_bytes().to_vec(),
601            FieldType::Long | FieldType::Int24 => (*i as i32).to_le_bytes().to_vec(),
602            _ => i.to_le_bytes().to_vec(),
603        },
604
605        Value::Float(f) => f.to_le_bytes().to_vec(),
606
607        Value::Double(f) => f.to_le_bytes().to_vec(),
608
609        Value::Decimal(s) => encode_length_prefixed_bytes(s.as_bytes()),
610
611        Value::Text(s) => {
612            let bytes = s.as_bytes();
613            encode_length_prefixed_bytes(bytes)
614        }
615
616        Value::Bytes(b) => encode_length_prefixed_bytes(b),
617
618        Value::Json(j) => {
619            let s = j.to_string();
620            encode_length_prefixed_bytes(s.as_bytes())
621        }
622
623        // Date is days since epoch (i32)
624        Value::Date(d) => d.to_le_bytes().to_vec(),
625
626        // Time is microseconds since midnight (i64)
627        Value::Time(t) => t.to_le_bytes().to_vec(),
628
629        // Timestamp is microseconds since epoch (i64)
630        Value::Timestamp(t) | Value::TimestampTz(t) => t.to_le_bytes().to_vec(),
631
632        // UUID is 16 bytes
633        Value::Uuid(u) => encode_length_prefixed_bytes(u),
634
635        // Array - encode as JSON for MySQL
636        Value::Array(arr) => {
637            let json = serde_json::to_string(arr).unwrap_or_default();
638            encode_length_prefixed_bytes(json.as_bytes())
639        }
640
641        // Default should never reach encode - query builder puts "DEFAULT"
642        // directly in SQL text. Return empty bytes as defensive fallback.
643        Value::Default => vec![],
644    }
645}
646
647/// Decode a binary protocol value and return bytes consumed.
648///
649/// This is used when parsing binary result set rows where we need to know
650/// how many bytes each value occupies.
651///
652/// # Returns
653///
654/// Tuple of (decoded value, bytes consumed)
655pub fn decode_binary_value_with_len(
656    data: &[u8],
657    field_type: FieldType,
658    _is_unsigned: bool,
659) -> (Value, usize) {
660    match field_type {
661        // Fixed-size integer types
662        FieldType::Tiny => {
663            if data.is_empty() {
664                return (Value::Null, 0);
665            }
666            (Value::TinyInt(data[0] as i8), 1)
667        }
668
669        FieldType::Short | FieldType::Year => {
670            if data.len() < 2 {
671                return (Value::Null, 0);
672            }
673            let val = u16::from_le_bytes([data[0], data[1]]);
674            (Value::SmallInt(val as i16), 2)
675        }
676
677        FieldType::Long | FieldType::Int24 => {
678            if data.len() < 4 {
679                return (Value::Null, 0);
680            }
681            let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
682            (Value::Int(val as i32), 4)
683        }
684
685        FieldType::LongLong => {
686            if data.len() < 8 {
687                return (Value::Null, 0);
688            }
689            let val = u64::from_le_bytes([
690                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
691            ]);
692            (Value::BigInt(val as i64), 8)
693        }
694
695        FieldType::Float => {
696            if data.len() < 4 {
697                return (Value::Null, 0);
698            }
699            let val = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
700            (Value::Float(val), 4)
701        }
702
703        FieldType::Double => {
704            if data.len() < 8 {
705                return (Value::Null, 0);
706            }
707            let val = f64::from_le_bytes([
708                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
709            ]);
710            (Value::Double(val), 8)
711        }
712
713        // Date types - variable length with length prefix byte
714        FieldType::Date | FieldType::NewDate => {
715            if data.is_empty() {
716                return (Value::Null, 0);
717            }
718            let len = data[0] as usize;
719            if len == 0 {
720                return (Value::Text("0000-00-00".to_string()), 1);
721            }
722            if data.len() < 1 + len || len < 4 {
723                return (Value::Null, 1);
724            }
725            let year = u16::from_le_bytes([data[1], data[2]]);
726            let month = data[3];
727            let day = data[4];
728            (
729                Value::Text(format!("{year:04}-{month:02}-{day:02}")),
730                1 + len,
731            )
732        }
733
734        FieldType::Time | FieldType::Time2 => {
735            if data.is_empty() {
736                return (Value::Null, 0);
737            }
738            let len = data[0] as usize;
739            if len == 0 {
740                return (Value::Text("00:00:00".to_string()), 1);
741            }
742            if data.len() < 1 + len || len < 8 {
743                return (Value::Text("00:00:00".to_string()), 1);
744            }
745            let is_negative = data[1] != 0;
746            let days = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
747            let hours = data[6];
748            let minutes = data[7];
749            let seconds = data[8];
750            let total_hours = days * 24 + u32::from(hours);
751            let sign = if is_negative { "-" } else { "" };
752            (
753                Value::Text(format!("{sign}{total_hours:02}:{minutes:02}:{seconds:02}")),
754                1 + len,
755            )
756        }
757
758        FieldType::DateTime
759        | FieldType::Timestamp
760        | FieldType::DateTime2
761        | FieldType::Timestamp2 => {
762            if data.is_empty() {
763                return (Value::Null, 0);
764            }
765            let len = data[0] as usize;
766            if len == 0 {
767                return (Value::Text("0000-00-00 00:00:00".to_string()), 1);
768            }
769            if data.len() < 1 + len {
770                return (Value::Null, 1);
771            }
772            if len >= 4 {
773                let year = u16::from_le_bytes([data[1], data[2]]);
774                let month = data[3];
775                let day = data[4];
776                if len >= 7 {
777                    let hour = data[5];
778                    let minute = data[6];
779                    let second = data[7];
780                    if len >= 11 {
781                        let microseconds =
782                            u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
783                        return (
784                            Value::Text(format!(
785                                "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{microseconds:06}"
786                            )),
787                            1 + len,
788                        );
789                    }
790                    return (
791                        Value::Text(format!(
792                            "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
793                        )),
794                        1 + len,
795                    );
796                }
797                return (
798                    Value::Text(format!("{year:04}-{month:02}-{day:02} 00:00:00")),
799                    1 + len,
800                );
801            }
802            (Value::Text("0000-00-00 00:00:00".to_string()), 1 + len)
803        }
804
805        // Variable-length types with length-encoded prefix
806        FieldType::Decimal
807        | FieldType::NewDecimal
808        | FieldType::VarChar
809        | FieldType::VarString
810        | FieldType::String
811        | FieldType::Enum
812        | FieldType::Set
813        | FieldType::TinyBlob
814        | FieldType::MediumBlob
815        | FieldType::LongBlob
816        | FieldType::Blob
817        | FieldType::Json
818        | FieldType::Geometry
819        | FieldType::Bit => {
820            let (str_len, prefix_len) = read_lenenc_int(data);
821            if str_len == 0 && prefix_len == 0 {
822                return (Value::Null, 0);
823            }
824            let total_len = prefix_len + str_len;
825            if data.len() < total_len {
826                return (Value::Null, prefix_len);
827            }
828            let str_data = &data[prefix_len..total_len];
829            let value = match field_type {
830                FieldType::TinyBlob
831                | FieldType::MediumBlob
832                | FieldType::LongBlob
833                | FieldType::Blob
834                | FieldType::Geometry
835                | FieldType::Bit => Value::Bytes(str_data.to_vec()),
836                FieldType::Json => {
837                    let text = String::from_utf8_lossy(str_data);
838                    serde_json::from_str(&text)
839                        .map_or_else(|_| Value::Bytes(str_data.to_vec()), Value::Json)
840                }
841                _ => Value::Text(String::from_utf8_lossy(str_data).into_owned()),
842            };
843            (value, total_len)
844        }
845
846        // Null type
847        FieldType::Null => (Value::Null, 0),
848    }
849}
850
851/// Read a length-encoded integer from data.
852///
853/// Returns (value, bytes consumed).
854fn read_lenenc_int(data: &[u8]) -> (usize, usize) {
855    if data.is_empty() {
856        return (0, 0);
857    }
858    match data[0] {
859        0..=250 => (data[0] as usize, 1),
860        0xFC => {
861            if data.len() < 3 {
862                return (0, 1);
863            }
864            let val = u16::from_le_bytes([data[1], data[2]]) as usize;
865            (val, 3)
866        }
867        0xFD => {
868            if data.len() < 4 {
869                return (0, 1);
870            }
871            let val = u32::from_le_bytes([data[1], data[2], data[3], 0]) as usize;
872            (val, 4)
873        }
874        0xFE => {
875            if data.len() < 9 {
876                return (0, 1);
877            }
878            let val = u64::from_le_bytes([
879                data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
880            ]) as usize;
881            (val, 9)
882        }
883        // 0xFB is NULL indicator, 0xFF is error - both handled by the exhaustive match above
884        251..=255 => (0, 1),
885    }
886}
887
888/// Encode bytes with a length prefix.
889fn encode_length_prefixed_bytes(data: &[u8]) -> Vec<u8> {
890    let len = data.len();
891    let mut result = Vec::with_capacity(len + 9);
892
893    if len < 251 {
894        result.push(len as u8);
895    } else if len < 0x10000 {
896        result.push(0xFC);
897        result.extend_from_slice(&(len as u16).to_le_bytes());
898    } else if len < 0x0100_0000 {
899        result.push(0xFD);
900        result.push((len & 0xFF) as u8);
901        result.push(((len >> 8) & 0xFF) as u8);
902        result.push(((len >> 16) & 0xFF) as u8);
903    } else {
904        result.push(0xFE);
905        result.extend_from_slice(&(len as u64).to_le_bytes());
906    }
907
908    result.extend_from_slice(data);
909    result
910}
911
912/// Escape a string for use in MySQL text protocol.
913///
914/// This escapes special characters to prevent SQL injection.
915fn escape_string(s: &str) -> String {
916    let mut result = String::with_capacity(s.len() + 2);
917    result.push('\'');
918    for ch in s.chars() {
919        match ch {
920            '\'' => result.push_str("''"),
921            '\\' => result.push_str("\\\\"),
922            '\0' => result.push_str("\\0"),
923            '\n' => result.push_str("\\n"),
924            '\r' => result.push_str("\\r"),
925            '\x1a' => result.push_str("\\Z"), // Ctrl+Z
926            _ => result.push(ch),
927        }
928    }
929    result.push('\'');
930    result
931}
932
933/// Escape bytes for use in MySQL text protocol.
934fn escape_bytes(data: &[u8]) -> String {
935    let mut result = String::with_capacity(data.len() * 2 + 3);
936    result.push_str("X'");
937    for byte in data {
938        result.push_str(&format!("{byte:02X}"));
939    }
940    result.push('\'');
941    result
942}
943
944/// Format a sqlmodel Value for use in MySQL text protocol SQL.
945///
946/// This converts a Value to a properly escaped SQL literal string.
947pub fn format_value_for_sql(value: &Value) -> String {
948    match value {
949        Value::Null => "NULL".to_string(),
950        Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
951        Value::TinyInt(i) => i.to_string(),
952        Value::SmallInt(i) => i.to_string(),
953        Value::Int(i) => i.to_string(),
954        Value::BigInt(i) => i.to_string(),
955        Value::Float(f) => {
956            if f.is_nan() {
957                "NULL".to_string()
958            } else if f.is_infinite() {
959                if f.is_sign_positive() {
960                    "1e308".to_string() // Close to infinity
961                } else {
962                    "-1e308".to_string()
963                }
964            } else {
965                f.to_string()
966            }
967        }
968        Value::Double(f) => {
969            if f.is_nan() {
970                "NULL".to_string()
971            } else if f.is_infinite() {
972                if f.is_sign_positive() {
973                    "1e308".to_string()
974                } else {
975                    "-1e308".to_string()
976                }
977            } else {
978                f.to_string()
979            }
980        }
981        Value::Decimal(s) => s.clone(),
982        Value::Text(s) => escape_string(s),
983        Value::Bytes(b) => escape_bytes(b),
984        Value::Json(j) => escape_string(&j.to_string()),
985        Value::Date(d) => format!("'{}'", d), // ISO date format
986        Value::Time(t) => format!("'{}'", t), // microseconds as-is for now
987        Value::Timestamp(t) | Value::TimestampTz(t) => format!("'{}'", t),
988        Value::Uuid(u) => escape_bytes(u),
989        Value::Array(arr) => {
990            // MySQL doesn't have native arrays, encode as JSON
991            let json = serde_json::to_string(arr).unwrap_or_default();
992            escape_string(&json)
993        }
994        Value::Default => "DEFAULT".to_string(),
995    }
996}
997
998/// Interpolate parameters into a SQL query string.
999///
1000/// Replaces `$1`, `$2`, etc. placeholders with properly escaped values.
1001/// Also supports `?` placeholders (MySQL style) - replaced in order.
1002pub fn interpolate_params(sql: &str, params: &[Value]) -> String {
1003    if params.is_empty() {
1004        return sql.to_string();
1005    }
1006
1007    let mut result = String::with_capacity(sql.len() + params.len() * 20);
1008    let mut chars = sql.chars().peekable();
1009    let mut param_index = 0;
1010
1011    while let Some(ch) = chars.next() {
1012        match ch {
1013            // MySQL-style ? placeholder
1014            '?' => {
1015                if param_index < params.len() {
1016                    result.push_str(&format_value_for_sql(&params[param_index]));
1017                    param_index += 1;
1018                } else {
1019                    result.push('?');
1020                }
1021            }
1022            // PostgreSQL-style $N placeholder
1023            '$' => {
1024                let mut num_str = String::new();
1025                while let Some(&next_ch) = chars.peek() {
1026                    if next_ch.is_ascii_digit() {
1027                        num_str.push(chars.next().unwrap());
1028                    } else {
1029                        break;
1030                    }
1031                }
1032                if num_str.is_empty() {
1033                    result.push('$');
1034                } else if let Ok(n) = num_str.parse::<usize>() {
1035                    if n > 0 && n <= params.len() {
1036                        result.push_str(&format_value_for_sql(&params[n - 1]));
1037                    } else {
1038                        result.push('$');
1039                        result.push_str(&num_str);
1040                    }
1041                } else {
1042                    result.push('$');
1043                    result.push_str(&num_str);
1044                }
1045            }
1046            // Handle string literals (don't replace placeholders inside)
1047            '\'' => {
1048                result.push(ch);
1049                while let Some(next_ch) = chars.next() {
1050                    result.push(next_ch);
1051                    if next_ch == '\'' {
1052                        // Check for escaped quote
1053                        if chars.peek() == Some(&'\'') {
1054                            result.push(chars.next().unwrap());
1055                        } else {
1056                            break;
1057                        }
1058                    }
1059                }
1060            }
1061            // Handle double-quoted identifiers
1062            '"' => {
1063                result.push(ch);
1064                while let Some(next_ch) = chars.next() {
1065                    result.push(next_ch);
1066                    if next_ch == '"' {
1067                        if chars.peek() == Some(&'"') {
1068                            result.push(chars.next().unwrap());
1069                        } else {
1070                            break;
1071                        }
1072                    }
1073                }
1074            }
1075            // Handle backtick identifiers (MySQL-specific)
1076            '`' => {
1077                result.push(ch);
1078                while let Some(next_ch) = chars.next() {
1079                    result.push(next_ch);
1080                    if next_ch == '`' {
1081                        if chars.peek() == Some(&'`') {
1082                            result.push(chars.next().unwrap());
1083                        } else {
1084                            break;
1085                        }
1086                    }
1087                }
1088            }
1089            _ => result.push(ch),
1090        }
1091    }
1092
1093    result
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use super::*;
1099
1100    #[test]
1101    fn test_escape_string() {
1102        assert_eq!(escape_string("hello"), "'hello'");
1103        assert_eq!(escape_string("it's"), "'it''s'");
1104        assert_eq!(escape_string("a\\b"), "'a\\\\b'");
1105        assert_eq!(escape_string("line\nbreak"), "'line\\nbreak'");
1106    }
1107
1108    #[test]
1109    fn test_format_value() {
1110        assert_eq!(format_value_for_sql(&Value::Null), "NULL");
1111        assert_eq!(format_value_for_sql(&Value::Int(42)), "42");
1112        assert_eq!(
1113            format_value_for_sql(&Value::Text("hello".to_string())),
1114            "'hello'"
1115        );
1116        assert_eq!(format_value_for_sql(&Value::Bool(true)), "TRUE");
1117    }
1118
1119    #[test]
1120    fn test_interpolate_params_question_mark() {
1121        let sql = "SELECT * FROM users WHERE id = ? AND name = ?";
1122        let params = vec![Value::Int(1), Value::Text("Alice".to_string())];
1123        let result = interpolate_params(sql, &params);
1124        assert_eq!(
1125            result,
1126            "SELECT * FROM users WHERE id = 1 AND name = 'Alice'"
1127        );
1128    }
1129
1130    #[test]
1131    fn test_interpolate_params_dollar() {
1132        let sql = "SELECT * FROM users WHERE id = $1 AND name = $2";
1133        let params = vec![Value::Int(1), Value::Text("Alice".to_string())];
1134        let result = interpolate_params(sql, &params);
1135        assert_eq!(
1136            result,
1137            "SELECT * FROM users WHERE id = 1 AND name = 'Alice'"
1138        );
1139    }
1140
1141    #[test]
1142    fn test_interpolate_no_replace_in_string() {
1143        let sql = "SELECT * FROM users WHERE name = '$1' AND id = ?";
1144        let params = vec![Value::Int(42)];
1145        let result = interpolate_params(sql, &params);
1146        assert_eq!(result, "SELECT * FROM users WHERE name = '$1' AND id = 42");
1147    }
1148
1149    #[test]
1150    fn test_field_type_from_u8() {
1151        assert_eq!(FieldType::from_u8(0x01), FieldType::Tiny);
1152        assert_eq!(FieldType::from_u8(0x03), FieldType::Long);
1153        assert_eq!(FieldType::from_u8(0x08), FieldType::LongLong);
1154        assert_eq!(FieldType::from_u8(0xFC), FieldType::Blob);
1155        assert_eq!(FieldType::from_u8(0xF5), FieldType::Json);
1156    }
1157
1158    #[test]
1159    fn test_field_type_categories() {
1160        assert!(FieldType::Tiny.is_integer());
1161        assert!(FieldType::Long.is_integer());
1162        assert!(FieldType::LongLong.is_integer());
1163
1164        assert!(FieldType::Float.is_float());
1165        assert!(FieldType::Double.is_float());
1166
1167        assert!(FieldType::Decimal.is_decimal());
1168        assert!(FieldType::NewDecimal.is_decimal());
1169
1170        assert!(FieldType::VarChar.is_string());
1171        assert!(FieldType::String.is_string());
1172
1173        assert!(FieldType::Blob.is_blob());
1174        assert!(FieldType::TinyBlob.is_blob());
1175
1176        assert!(FieldType::Date.is_temporal());
1177        assert!(FieldType::DateTime.is_temporal());
1178        assert!(FieldType::Timestamp.is_temporal());
1179    }
1180
1181    #[test]
1182    fn test_decode_text_integer() {
1183        let val = decode_text_value(FieldType::Long, b"42", false);
1184        assert!(matches!(val, Value::Int(42)));
1185
1186        let val = decode_text_value(FieldType::LongLong, b"-100", false);
1187        assert!(matches!(val, Value::BigInt(-100)));
1188    }
1189
1190    #[test]
1191    #[allow(clippy::approx_constant)]
1192    fn test_decode_text_float() {
1193        let val = decode_text_value(FieldType::Double, b"3.14", false);
1194        if let Value::Double(f) = val {
1195            assert!((f - 3.14).abs() < 0.001);
1196        } else {
1197            panic!("Expected double");
1198        }
1199    }
1200
1201    #[test]
1202    fn test_decode_text_string() {
1203        let val = decode_text_value(FieldType::VarChar, b"hello", false);
1204        assert!(matches!(val, Value::Text(s) if s == "hello"));
1205    }
1206
1207    #[test]
1208    fn test_decode_binary_tiny() {
1209        let val = decode_binary_value(FieldType::Tiny, &[42], false);
1210        assert!(matches!(val, Value::TinyInt(42)));
1211
1212        let val = decode_binary_value(FieldType::Tiny, &[255u8], true);
1213        assert!(matches!(val, Value::TinyInt(-1))); // 255u8 as i8 = -1
1214
1215        let val = decode_binary_value(FieldType::Tiny, &[255], false);
1216        assert!(matches!(val, Value::TinyInt(-1)));
1217    }
1218
1219    #[test]
1220    fn test_decode_binary_long() {
1221        let val = decode_binary_value(FieldType::Long, &[0x2A, 0x00, 0x00, 0x00], false);
1222        assert!(matches!(val, Value::Int(42)));
1223    }
1224
1225    #[test]
1226    #[allow(clippy::approx_constant)]
1227    fn test_decode_binary_double() {
1228        let pi_bytes = 3.14159_f64.to_le_bytes();
1229        let val = decode_binary_value(FieldType::Double, &pi_bytes, false);
1230        if let Value::Double(f) = val {
1231            assert!((f - 3.14159).abs() < 0.00001);
1232        } else {
1233            panic!("Expected double");
1234        }
1235    }
1236
1237    #[test]
1238    fn test_column_flags() {
1239        let col = ColumnDef {
1240            catalog: "def".to_string(),
1241            schema: "test".to_string(),
1242            table: "users".to_string(),
1243            org_table: "users".to_string(),
1244            name: "id".to_string(),
1245            org_name: "id".to_string(),
1246            charset: 33,
1247            column_length: 11,
1248            column_type: FieldType::Long,
1249            flags: column_flags::NOT_NULL
1250                | column_flags::PRIMARY_KEY
1251                | column_flags::AUTO_INCREMENT
1252                | column_flags::UNSIGNED,
1253            decimals: 0,
1254        };
1255
1256        assert!(col.is_not_null());
1257        assert!(col.is_primary_key());
1258        assert!(col.is_auto_increment());
1259        assert!(col.is_unsigned());
1260        assert!(!col.is_binary());
1261    }
1262
1263    #[test]
1264    fn test_encode_length_prefixed() {
1265        // Short string
1266        let result = encode_length_prefixed_bytes(b"hello");
1267        assert_eq!(result[0], 5);
1268        assert_eq!(&result[1..], b"hello");
1269
1270        // Empty
1271        let result = encode_length_prefixed_bytes(b"");
1272        assert_eq!(result, vec![0]);
1273    }
1274}