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        // Temporal types (text protocol transmits them as strings).
406        FieldType::Date | FieldType::NewDate => decode_text_date(text.as_ref()),
407        FieldType::Time | FieldType::Time2 => decode_text_time(text.as_ref()),
408        FieldType::DateTime
409        | FieldType::Timestamp
410        | FieldType::DateTime2
411        | FieldType::Timestamp2 => decode_text_datetime_or_timestamp(text.as_ref()),
412
413        // All other types (strings, dates, times) as text
414        _ => Value::Text(text.into_owned()),
415    }
416}
417
418fn decode_text_date(original: &str) -> Value {
419    let trimmed = original.trim();
420    if trimmed.is_empty() {
421        return Value::Text(original.to_string());
422    }
423    // MySQL "zero date" sentinel.
424    if trimmed == "0000-00-00" {
425        return Value::Text("0000-00-00".to_string());
426    }
427
428    let bytes = trimmed.as_bytes();
429    if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
430        return Value::Text(original.to_string());
431    }
432
433    let year_i32 = parse_4_digits(bytes, 0).and_then(|v| i32::try_from(v).ok());
434    let month_u32 = parse_2_digits(bytes, 5);
435    let day_u32 = parse_2_digits(bytes, 8);
436
437    match (year_i32, month_u32, day_u32) {
438        (Some(year), Some(month), Some(day)) => ymd_to_days_since_unix_epoch(year, month, day)
439            .map_or_else(|| Value::Text(original.to_string()), Value::Date),
440        _ => Value::Text(original.to_string()),
441    }
442}
443
444fn decode_text_time(original: &str) -> Value {
445    let trimmed = original.trim();
446    if trimmed.is_empty() {
447        return Value::Text(original.to_string());
448    }
449    // Preserve the common "zero time" sentinel as text (parity with binary len=0 behavior).
450    if trimmed == "00:00:00" || trimmed == "-00:00:00" {
451        return Value::Text(trimmed.to_string());
452    }
453
454    let mut bytes = trimmed.as_bytes();
455    let mut negative = false;
456    if let Some((&first, rest)) = bytes.split_first() {
457        if first == b'-' {
458            negative = true;
459            bytes = rest;
460        }
461    }
462
463    // Support both "HH:MM:SS[.ffffff]" and "D HH:MM:SS[.ffffff]".
464    let (days_part, time_part) = match bytes.iter().position(|&c| c == b' ') {
465        Some(sp) => (&bytes[..sp], &bytes[sp + 1..]),
466        None => (&[][..], bytes),
467    };
468
469    let days: i64 = if days_part.is_empty() {
470        0
471    } else {
472        let Ok(ds) = std::str::from_utf8(days_part) else {
473            return Value::Text(original.to_string());
474        };
475        let Ok(d) = ds.parse::<i64>() else {
476            return Value::Text(original.to_string());
477        };
478        d
479    };
480
481    // Split off fractional seconds.
482    let (hms, frac) = match time_part.iter().position(|&c| c == b'.') {
483        Some(dot) => (&time_part[..dot], Some(&time_part[dot + 1..])),
484        None => (time_part, None),
485    };
486
487    let mut it = hms.split(|&c| c == b':');
488    let Some(hh) = it.next() else {
489        return Value::Text(original.to_string());
490    };
491    let Some(mm) = it.next() else {
492        return Value::Text(original.to_string());
493    };
494    let Some(ss) = it.next() else {
495        return Value::Text(original.to_string());
496    };
497    if it.next().is_some() {
498        return Value::Text(original.to_string());
499    }
500
501    let Ok(hh_s) = std::str::from_utf8(hh) else {
502        return Value::Text(original.to_string());
503    };
504    let Ok(mm_s) = std::str::from_utf8(mm) else {
505        return Value::Text(original.to_string());
506    };
507    let Ok(ss_s) = std::str::from_utf8(ss) else {
508        return Value::Text(original.to_string());
509    };
510
511    let Ok(hours) = hh_s.parse::<i64>() else {
512        return Value::Text(original.to_string());
513    };
514    let Ok(minutes) = mm_s.parse::<i64>() else {
515        return Value::Text(original.to_string());
516    };
517    let Ok(seconds) = ss_s.parse::<i64>() else {
518        return Value::Text(original.to_string());
519    };
520
521    if !(0..=59).contains(&minutes) || !(0..=59).contains(&seconds) {
522        return Value::Text(original.to_string());
523    }
524
525    let micros: i64 = match frac {
526        None => 0,
527        Some(fr) => {
528            // MySQL emits up to 6 digits. Pad right with zeros.
529            let mut us: i64 = 0;
530            let mut n = 0usize;
531            for &c in fr {
532                if n == 6 {
533                    break;
534                }
535                if !c.is_ascii_digit() {
536                    return Value::Text(original.to_string());
537                }
538                us = us.saturating_mul(10).saturating_add(i64::from(c - b'0'));
539                n += 1;
540            }
541            for _ in n..6 {
542                us = us.saturating_mul(10);
543            }
544            us
545        }
546    };
547
548    let total_seconds = days
549        .saturating_mul(24)
550        .saturating_add(hours)
551        .saturating_mul(3600)
552        .saturating_add(minutes.saturating_mul(60))
553        .saturating_add(seconds);
554    let total_micros = total_seconds
555        .saturating_mul(1_000_000)
556        .saturating_add(micros);
557
558    let signed = if negative {
559        -total_micros
560    } else {
561        total_micros
562    };
563    Value::Time(signed)
564}
565
566fn decode_text_datetime_or_timestamp(original: &str) -> Value {
567    let trimmed = original.trim();
568    if trimmed.is_empty() {
569        return Value::Text(original.to_string());
570    }
571    // MySQL "zero datetime" sentinel.
572    if trimmed.starts_with("0000-00-00") {
573        return Value::Text(trimmed.to_string());
574    }
575
576    // Accept "YYYY-MM-DD", "YYYY-MM-DD HH:MM:SS", "YYYY-MM-DDTHH:MM:SS" with optional ".ffffff".
577    let (date_part, rest) = if trimmed.len() >= 10 {
578        (&trimmed[..10], &trimmed[10..])
579    } else {
580        return Value::Text(original.to_string());
581    };
582    let date_b = date_part.as_bytes();
583    if date_b.len() != 10 || date_b[4] != b'-' || date_b[7] != b'-' {
584        return Value::Text(original.to_string());
585    }
586
587    let year = parse_4_digits(date_b, 0).and_then(|v| i32::try_from(v).ok());
588    let month = parse_2_digits(date_b, 5);
589    let day = parse_2_digits(date_b, 8);
590    let (Some(year), Some(month), Some(day)) = (year, month, day) else {
591        return Value::Text(original.to_string());
592    };
593
594    let Some(days) = ymd_to_days_since_unix_epoch(year, month, day) else {
595        return Value::Text(original.to_string());
596    };
597
598    // Default midnight if no time part.
599    let mut hour: u32 = 0;
600    let mut minute: u32 = 0;
601    let mut second: u32 = 0;
602    let mut micros: u32 = 0;
603
604    let rest = rest.trim_start();
605    if !rest.is_empty() {
606        let rest = rest.strip_prefix('T').unwrap_or(rest);
607        let rest = rest.trim_start();
608
609        // Split off fractional seconds.
610        let (hms_part, frac) = match rest.find('.') {
611            Some(dot) => (&rest[..dot], Some(&rest[dot + 1..])),
612            None => (rest, None),
613        };
614
615        if hms_part.len() < 8 {
616            return Value::Text(original.to_string());
617        }
618        let hms_b = hms_part.as_bytes();
619        if hms_b.len() != 8 || hms_b[2] != b':' || hms_b[5] != b':' {
620            return Value::Text(original.to_string());
621        }
622        let Some(hh) = parse_2_digits(hms_b, 0) else {
623            return Value::Text(original.to_string());
624        };
625        let Some(mm) = parse_2_digits(hms_b, 3) else {
626            return Value::Text(original.to_string());
627        };
628        let Some(ss) = parse_2_digits(hms_b, 6) else {
629            return Value::Text(original.to_string());
630        };
631        if hh > 23 || mm > 59 || ss > 59 {
632            return Value::Text(original.to_string());
633        }
634        hour = hh;
635        minute = mm;
636        second = ss;
637
638        if let Some(frac) = frac {
639            let frac = frac.trim();
640            let fb = frac.as_bytes();
641            let mut us: u32 = 0;
642            let mut n = 0usize;
643            for &c in fb {
644                if n == 6 {
645                    break;
646                }
647                if !c.is_ascii_digit() {
648                    return Value::Text(original.to_string());
649                }
650                us = us.saturating_mul(10).saturating_add(u32::from(c - b'0'));
651                n += 1;
652            }
653            for _ in n..6 {
654                us = us.saturating_mul(10);
655            }
656            micros = us;
657        }
658    }
659
660    let day_us = i128::from(days) * 86_400_i128 * 1_000_000_i128;
661    let tod_us =
662        (i128::from(hour) * 3_600_i128 + i128::from(minute) * 60_i128 + i128::from(second))
663            * 1_000_000_i128
664            + i128::from(micros);
665    let total = day_us + tod_us;
666    let Ok(total_i64) = i64::try_from(total) else {
667        return Value::Text(original.to_string());
668    };
669
670    Value::Timestamp(total_i64)
671}
672
673fn parse_2_digits(bytes: &[u8], offset: usize) -> Option<u32> {
674    if bytes.len() < offset + 2 {
675        return None;
676    }
677    let d0 = bytes[offset];
678    let d1 = bytes[offset + 1];
679    if !d0.is_ascii_digit() || !d1.is_ascii_digit() {
680        return None;
681    }
682    Some(u32::from(d0 - b'0') * 10 + u32::from(d1 - b'0'))
683}
684
685fn parse_4_digits(bytes: &[u8], offset: usize) -> Option<u32> {
686    if bytes.len() < offset + 4 {
687        return None;
688    }
689    let mut v: u32 = 0;
690    for i in 0..4 {
691        let d = bytes[offset + i];
692        if !d.is_ascii_digit() {
693            return None;
694        }
695        v = v * 10 + u32::from(d - b'0');
696    }
697    Some(v)
698}
699
700/// Decode a binary protocol value to a sqlmodel Value.
701///
702/// In binary protocol, values are encoded in type-specific binary formats.
703pub fn decode_binary_value(field_type: FieldType, data: &[u8], is_unsigned: bool) -> Value {
704    match field_type {
705        // TINY (1 byte)
706        FieldType::Tiny => {
707            if data.is_empty() {
708                return Value::Null;
709            }
710            // Both signed and unsigned map to i8 (interpretation differs at application level)
711            let _ = is_unsigned;
712            Value::TinyInt(data[0] as i8)
713        }
714
715        // SHORT (2 bytes, little-endian)
716        FieldType::Short | FieldType::Year => {
717            if data.len() < 2 {
718                return Value::Null;
719            }
720            let val = u16::from_le_bytes([data[0], data[1]]);
721            // Both signed and unsigned map to i16 (interpretation differs at application level)
722            let _ = is_unsigned;
723            Value::SmallInt(val as i16)
724        }
725
726        // LONG/INT24 (4 bytes, little-endian)
727        FieldType::Long | FieldType::Int24 => {
728            if data.len() < 4 {
729                return Value::Null;
730            }
731            let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
732            // Both signed and unsigned map to i32 (interpretation differs at application level)
733            let _ = is_unsigned;
734            Value::Int(val as i32)
735        }
736
737        // LONGLONG (8 bytes, little-endian)
738        FieldType::LongLong => {
739            if data.len() < 8 {
740                return Value::Null;
741            }
742            let val = u64::from_le_bytes([
743                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
744            ]);
745            // Both signed and unsigned map to i64 (interpretation differs at application level)
746            let _ = is_unsigned;
747            Value::BigInt(val as i64)
748        }
749
750        // FLOAT (4 bytes)
751        FieldType::Float => {
752            if data.len() < 4 {
753                return Value::Null;
754            }
755            let val = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
756            Value::Float(val)
757        }
758
759        // DOUBLE (8 bytes)
760        FieldType::Double => {
761            if data.len() < 8 {
762                return Value::Null;
763            }
764            let val = f64::from_le_bytes([
765                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
766            ]);
767            Value::Double(val)
768        }
769
770        // Binary types
771        FieldType::TinyBlob
772        | FieldType::MediumBlob
773        | FieldType::LongBlob
774        | FieldType::Blob
775        | FieldType::Geometry
776        | FieldType::Bit => Value::Bytes(data.to_vec()),
777
778        // JSON
779        FieldType::Json => {
780            let text = String::from_utf8_lossy(data);
781            serde_json::from_str(&text).map_or_else(|_| Value::Bytes(data.to_vec()), Value::Json)
782        }
783
784        // Date/Time types - binary format encodes components
785        FieldType::Date
786        | FieldType::NewDate
787        | FieldType::Time
788        | FieldType::DateTime
789        | FieldType::Timestamp
790        | FieldType::Time2
791        | FieldType::DateTime2
792        | FieldType::Timestamp2 => decode_binary_temporal_value(field_type, data),
793
794        // Decimal types - keep as text for precision
795        FieldType::Decimal | FieldType::NewDecimal => {
796            Value::Text(String::from_utf8_lossy(data).into_owned())
797        }
798
799        // String types
800        _ => Value::Text(String::from_utf8_lossy(data).into_owned()),
801    }
802}
803
804fn is_leap_year(year: i32) -> bool {
805    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
806}
807
808fn days_in_month(year: i32, month: u32) -> u32 {
809    match month {
810        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
811        4 | 6 | 9 | 11 => 30,
812        2 => {
813            if is_leap_year(year) {
814                29
815            } else {
816                28
817            }
818        }
819        _ => 0,
820    }
821}
822
823/// Convert (year, month, day) to days since Unix epoch (1970-01-01), if valid.
824///
825/// Inverse of `days_to_ymd()` in `protocol/prepared.rs` (Howard Hinnant algorithm).
826fn ymd_to_days_since_unix_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
827    if year <= 0 || !(1..=12).contains(&month) {
828        return None;
829    }
830    let dim = days_in_month(year, month);
831    if day == 0 || day > dim {
832        return None;
833    }
834
835    let mut y = i64::from(year);
836    let m = i64::from(month);
837    let d = i64::from(day);
838
839    // Shift Jan/Feb to previous year.
840    y -= if m <= 2 { 1 } else { 0 };
841
842    let era = if y >= 0 { y } else { y - 399 } / 400;
843    let yoe = y - era * 400; // [0, 399]
844    let mp = m + if m > 2 { -3 } else { 9 }; // March=0..Feb=11
845    let doy = (153 * mp + 2) / 5 + d - 1; // [0, 365]
846    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
847    let z = era * 146_097 + doe - 719_468; // 1970-01-01 -> 0
848
849    i32::try_from(z).ok()
850}
851
852fn decode_binary_temporal_value(field_type: FieldType, data: &[u8]) -> Value {
853    match field_type {
854        FieldType::Date | FieldType::NewDate => {
855            if data.len() < 4 {
856                return Value::Text("0000-00-00".to_string());
857            }
858            let year = i32::from(u16::from_le_bytes([data[0], data[1]]));
859            let month = u32::from(data[2]);
860            let day = u32::from(data[3]);
861
862            match ymd_to_days_since_unix_epoch(year, month, day) {
863                Some(days) => Value::Date(days),
864                None => Value::Text(format!("{year:04}-{month:02}-{day:02}")),
865            }
866        }
867
868        FieldType::Time | FieldType::Time2 => {
869            if data.len() < 8 {
870                return Value::Text("00:00:00".to_string());
871            }
872            let is_negative = data[0] != 0;
873            let days = i64::from(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
874            let hours = i64::from(data[5]);
875            let minutes = i64::from(data[6]);
876            let seconds = i64::from(data[7]);
877            let micros = if data.len() >= 12 {
878                i64::from(u32::from_le_bytes([data[8], data[9], data[10], data[11]]))
879            } else {
880                0
881            };
882
883            let total_seconds = days
884                .saturating_mul(24)
885                .saturating_add(hours) // hours is 0..23 in MySQL binary format
886                .saturating_mul(3600)
887                .saturating_add(minutes.saturating_mul(60))
888                .saturating_add(seconds);
889            let total_micros = total_seconds
890                .saturating_mul(1_000_000)
891                .saturating_add(micros);
892
893            let signed = if is_negative {
894                -total_micros
895            } else {
896                total_micros
897            };
898            Value::Time(signed)
899        }
900
901        FieldType::DateTime
902        | FieldType::Timestamp
903        | FieldType::DateTime2
904        | FieldType::Timestamp2 => {
905            if data.len() < 4 {
906                return Value::Text("0000-00-00 00:00:00".to_string());
907            }
908
909            let year = i32::from(u16::from_le_bytes([data[0], data[1]]));
910            let month = u32::from(data[2]);
911            let day = u32::from(data[3]);
912
913            let (hour, minute, second, micros) = if data.len() >= 7 {
914                let hour = u32::from(data[4]);
915                let minute = u32::from(data[5]);
916                let second = u32::from(data[6]);
917                let micros = if data.len() >= 11 {
918                    u32::from_le_bytes([data[7], data[8], data[9], data[10]])
919                } else {
920                    0
921                };
922                (hour, minute, second, micros)
923            } else {
924                (0, 0, 0, 0)
925            };
926
927            let Some(days) = ymd_to_days_since_unix_epoch(year, month, day) else {
928                // Preserve zero/invalid date semantics without inventing a bogus epoch value.
929                return Value::Text(format!(
930                    "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
931                ));
932            };
933
934            let day_us = i128::from(days) * 86_400_i128 * 1_000_000_i128;
935            let tod_us =
936                (i128::from(hour) * 3_600_i128 + i128::from(minute) * 60_i128 + i128::from(second))
937                    * 1_000_000_i128
938                    + i128::from(micros);
939            let total = day_us + tod_us;
940            let Ok(total_i64) = i64::try_from(total) else {
941                return Value::Text(format!(
942                    "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
943                ));
944            };
945
946            Value::Timestamp(total_i64)
947        }
948
949        _ => Value::Text(String::from_utf8_lossy(data).into_owned()),
950    }
951}
952
953/// Encode a sqlmodel Value for binary protocol.
954///
955/// Returns the encoded bytes for the value.
956pub fn encode_binary_value(value: &Value, field_type: FieldType) -> Vec<u8> {
957    match value {
958        Value::Null => vec![],
959
960        Value::Bool(b) => vec![u8::from(*b)],
961
962        Value::TinyInt(i) => vec![*i as u8],
963
964        Value::SmallInt(i) => i.to_le_bytes().to_vec(),
965
966        Value::Int(i) => i.to_le_bytes().to_vec(),
967
968        Value::BigInt(i) => match field_type {
969            FieldType::Tiny => vec![*i as u8],
970            FieldType::Short | FieldType::Year => (*i as i16).to_le_bytes().to_vec(),
971            FieldType::Long | FieldType::Int24 => (*i as i32).to_le_bytes().to_vec(),
972            _ => i.to_le_bytes().to_vec(),
973        },
974
975        Value::Float(f) => f.to_le_bytes().to_vec(),
976
977        Value::Double(f) => f.to_le_bytes().to_vec(),
978
979        Value::Decimal(s) => encode_length_prefixed_bytes(s.as_bytes()),
980
981        Value::Text(s) => {
982            let bytes = s.as_bytes();
983            encode_length_prefixed_bytes(bytes)
984        }
985
986        Value::Bytes(b) => encode_length_prefixed_bytes(b),
987
988        Value::Json(j) => {
989            let s = j.to_string();
990            encode_length_prefixed_bytes(s.as_bytes())
991        }
992
993        // Date is days since epoch (i32)
994        Value::Date(d) => d.to_le_bytes().to_vec(),
995
996        // Time is microseconds since midnight (i64)
997        Value::Time(t) => t.to_le_bytes().to_vec(),
998
999        // Timestamp is microseconds since epoch (i64)
1000        Value::Timestamp(t) | Value::TimestampTz(t) => t.to_le_bytes().to_vec(),
1001
1002        // UUID is 16 bytes
1003        Value::Uuid(u) => encode_length_prefixed_bytes(u),
1004
1005        // Array - encode as JSON for MySQL
1006        Value::Array(arr) => {
1007            let json = serde_json::to_string(arr).unwrap_or_default();
1008            encode_length_prefixed_bytes(json.as_bytes())
1009        }
1010
1011        // Default should never reach encode - query builder puts "DEFAULT"
1012        // directly in SQL text. Return empty bytes as defensive fallback.
1013        Value::Default => vec![],
1014    }
1015}
1016
1017/// Decode a binary protocol value and return bytes consumed.
1018///
1019/// This is used when parsing binary result set rows where we need to know
1020/// how many bytes each value occupies.
1021///
1022/// # Returns
1023///
1024/// Tuple of (decoded value, bytes consumed)
1025pub fn decode_binary_value_with_len(
1026    data: &[u8],
1027    field_type: FieldType,
1028    _is_unsigned: bool,
1029) -> (Value, usize) {
1030    match field_type {
1031        // Fixed-size integer types
1032        FieldType::Tiny => {
1033            if data.is_empty() {
1034                return (Value::Null, 0);
1035            }
1036            (Value::TinyInt(data[0] as i8), 1)
1037        }
1038
1039        FieldType::Short | FieldType::Year => {
1040            if data.len() < 2 {
1041                return (Value::Null, 0);
1042            }
1043            let val = u16::from_le_bytes([data[0], data[1]]);
1044            (Value::SmallInt(val as i16), 2)
1045        }
1046
1047        FieldType::Long | FieldType::Int24 => {
1048            if data.len() < 4 {
1049                return (Value::Null, 0);
1050            }
1051            let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1052            (Value::Int(val as i32), 4)
1053        }
1054
1055        FieldType::LongLong => {
1056            if data.len() < 8 {
1057                return (Value::Null, 0);
1058            }
1059            let val = u64::from_le_bytes([
1060                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
1061            ]);
1062            (Value::BigInt(val as i64), 8)
1063        }
1064
1065        FieldType::Float => {
1066            if data.len() < 4 {
1067                return (Value::Null, 0);
1068            }
1069            let val = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1070            (Value::Float(val), 4)
1071        }
1072
1073        FieldType::Double => {
1074            if data.len() < 8 {
1075                return (Value::Null, 0);
1076            }
1077            let val = f64::from_le_bytes([
1078                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
1079            ]);
1080            (Value::Double(val), 8)
1081        }
1082
1083        // Date types - variable length with length prefix byte
1084        FieldType::Date | FieldType::NewDate => {
1085            if data.is_empty() {
1086                return (Value::Null, 0);
1087            }
1088            let len = data[0] as usize;
1089            if len == 0 {
1090                return (Value::Text("0000-00-00".to_string()), 1);
1091            }
1092            if data.len() < 1 + len || len < 4 {
1093                return (Value::Null, 1);
1094            }
1095            let value = decode_binary_temporal_value(field_type, &data[1..=len]);
1096            (value, 1 + len)
1097        }
1098
1099        FieldType::Time | FieldType::Time2 => {
1100            if data.is_empty() {
1101                return (Value::Null, 0);
1102            }
1103            let len = data[0] as usize;
1104            if len == 0 {
1105                return (Value::Text("00:00:00".to_string()), 1);
1106            }
1107            if data.len() < 1 + len || len < 8 {
1108                return (Value::Text("00:00:00".to_string()), 1);
1109            }
1110            let value = decode_binary_temporal_value(field_type, &data[1..=len]);
1111            (value, 1 + len)
1112        }
1113
1114        FieldType::DateTime
1115        | FieldType::Timestamp
1116        | FieldType::DateTime2
1117        | FieldType::Timestamp2 => {
1118            if data.is_empty() {
1119                return (Value::Null, 0);
1120            }
1121            let len = data[0] as usize;
1122            if len == 0 {
1123                return (Value::Text("0000-00-00 00:00:00".to_string()), 1);
1124            }
1125            if data.len() < 1 + len {
1126                return (Value::Null, 1);
1127            }
1128            let value = decode_binary_temporal_value(field_type, &data[1..=len]);
1129            (value, 1 + len)
1130        }
1131
1132        // Variable-length types with length-encoded prefix
1133        FieldType::Decimal
1134        | FieldType::NewDecimal
1135        | FieldType::VarChar
1136        | FieldType::VarString
1137        | FieldType::String
1138        | FieldType::Enum
1139        | FieldType::Set
1140        | FieldType::TinyBlob
1141        | FieldType::MediumBlob
1142        | FieldType::LongBlob
1143        | FieldType::Blob
1144        | FieldType::Json
1145        | FieldType::Geometry
1146        | FieldType::Bit => {
1147            let (str_len, prefix_len) = read_lenenc_int(data);
1148            if str_len == 0 && prefix_len == 0 {
1149                return (Value::Null, 0);
1150            }
1151            let total_len = prefix_len + str_len;
1152            if data.len() < total_len {
1153                return (Value::Null, prefix_len);
1154            }
1155            let str_data = &data[prefix_len..total_len];
1156            let value = match field_type {
1157                FieldType::TinyBlob
1158                | FieldType::MediumBlob
1159                | FieldType::LongBlob
1160                | FieldType::Blob
1161                | FieldType::Geometry
1162                | FieldType::Bit => Value::Bytes(str_data.to_vec()),
1163                FieldType::Json => {
1164                    let text = String::from_utf8_lossy(str_data);
1165                    serde_json::from_str(&text)
1166                        .map_or_else(|_| Value::Bytes(str_data.to_vec()), Value::Json)
1167                }
1168                _ => Value::Text(String::from_utf8_lossy(str_data).into_owned()),
1169            };
1170            (value, total_len)
1171        }
1172
1173        // Null type
1174        FieldType::Null => (Value::Null, 0),
1175    }
1176}
1177
1178/// Read a length-encoded integer from data.
1179///
1180/// Returns (value, bytes consumed).
1181fn read_lenenc_int(data: &[u8]) -> (usize, usize) {
1182    if data.is_empty() {
1183        return (0, 0);
1184    }
1185    match data[0] {
1186        0..=250 => (data[0] as usize, 1),
1187        0xFC => {
1188            if data.len() < 3 {
1189                return (0, 1);
1190            }
1191            let val = u16::from_le_bytes([data[1], data[2]]) as usize;
1192            (val, 3)
1193        }
1194        0xFD => {
1195            if data.len() < 4 {
1196                return (0, 1);
1197            }
1198            let val = u32::from_le_bytes([data[1], data[2], data[3], 0]) as usize;
1199            (val, 4)
1200        }
1201        0xFE => {
1202            if data.len() < 9 {
1203                return (0, 1);
1204            }
1205            let val = u64::from_le_bytes([
1206                data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
1207            ]) as usize;
1208            (val, 9)
1209        }
1210        // 0xFB is NULL indicator, 0xFF is error - both handled by the exhaustive match above
1211        251..=255 => (0, 1),
1212    }
1213}
1214
1215/// Encode bytes with a length prefix.
1216fn encode_length_prefixed_bytes(data: &[u8]) -> Vec<u8> {
1217    let len = data.len();
1218    let mut result = Vec::with_capacity(len + 9);
1219
1220    if len < 251 {
1221        result.push(len as u8);
1222    } else if len < 0x10000 {
1223        result.push(0xFC);
1224        result.extend_from_slice(&(len as u16).to_le_bytes());
1225    } else if len < 0x0100_0000 {
1226        result.push(0xFD);
1227        result.push((len & 0xFF) as u8);
1228        result.push(((len >> 8) & 0xFF) as u8);
1229        result.push(((len >> 16) & 0xFF) as u8);
1230    } else {
1231        result.push(0xFE);
1232        result.extend_from_slice(&(len as u64).to_le_bytes());
1233    }
1234
1235    result.extend_from_slice(data);
1236    result
1237}
1238
1239/// Escape a string for use in MySQL text protocol.
1240///
1241/// This escapes special characters to prevent SQL injection.
1242fn escape_string(s: &str) -> String {
1243    let mut result = String::with_capacity(s.len() + 2);
1244    result.push('\'');
1245    for ch in s.chars() {
1246        match ch {
1247            '\'' => result.push_str("''"),
1248            '\\' => result.push_str("\\\\"),
1249            '\0' => result.push_str("\\0"),
1250            '\n' => result.push_str("\\n"),
1251            '\r' => result.push_str("\\r"),
1252            '\x1a' => result.push_str("\\Z"), // Ctrl+Z
1253            _ => result.push(ch),
1254        }
1255    }
1256    result.push('\'');
1257    result
1258}
1259
1260/// Escape bytes for use in MySQL text protocol.
1261fn escape_bytes(data: &[u8]) -> String {
1262    let mut result = String::with_capacity(data.len() * 2 + 3);
1263    result.push_str("X'");
1264    for byte in data {
1265        result.push_str(&format!("{byte:02X}"));
1266    }
1267    result.push('\'');
1268    result
1269}
1270
1271/// Format a sqlmodel Value for use in MySQL text protocol SQL.
1272///
1273/// This converts a Value to a properly escaped SQL literal string.
1274pub fn format_value_for_sql(value: &Value) -> String {
1275    match value {
1276        Value::Null => "NULL".to_string(),
1277        Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
1278        Value::TinyInt(i) => i.to_string(),
1279        Value::SmallInt(i) => i.to_string(),
1280        Value::Int(i) => i.to_string(),
1281        Value::BigInt(i) => i.to_string(),
1282        Value::Float(f) => {
1283            if f.is_nan() {
1284                "NULL".to_string()
1285            } else if f.is_infinite() {
1286                if f.is_sign_positive() {
1287                    "1e308".to_string() // Close to infinity
1288                } else {
1289                    "-1e308".to_string()
1290                }
1291            } else {
1292                f.to_string()
1293            }
1294        }
1295        Value::Double(f) => {
1296            if f.is_nan() {
1297                "NULL".to_string()
1298            } else if f.is_infinite() {
1299                if f.is_sign_positive() {
1300                    "1e308".to_string()
1301                } else {
1302                    "-1e308".to_string()
1303                }
1304            } else {
1305                f.to_string()
1306            }
1307        }
1308        Value::Decimal(s) => s.clone(),
1309        Value::Text(s) => escape_string(s),
1310        Value::Bytes(b) => escape_bytes(b),
1311        Value::Json(j) => escape_string(&j.to_string()),
1312        Value::Date(d) => format!("'{}'", d), // ISO date format
1313        Value::Time(t) => format!("'{}'", t), // microseconds as-is for now
1314        Value::Timestamp(t) | Value::TimestampTz(t) => format!("'{}'", t),
1315        Value::Uuid(u) => escape_bytes(u),
1316        Value::Array(arr) => {
1317            // MySQL doesn't have native arrays, encode as JSON
1318            let json = serde_json::to_string(arr).unwrap_or_default();
1319            escape_string(&json)
1320        }
1321        Value::Default => "DEFAULT".to_string(),
1322    }
1323}
1324
1325/// Interpolate parameters into a SQL query string.
1326///
1327/// Replaces `$1`, `$2`, etc. placeholders with properly escaped values.
1328/// Also supports `?` placeholders (MySQL style) - replaced in order.
1329pub fn interpolate_params(sql: &str, params: &[Value]) -> String {
1330    if params.is_empty() {
1331        return sql.to_string();
1332    }
1333
1334    let mut result = String::with_capacity(sql.len() + params.len() * 20);
1335    let mut chars = sql.chars().peekable();
1336    let mut param_index = 0;
1337
1338    while let Some(ch) = chars.next() {
1339        match ch {
1340            // MySQL-style ? placeholder
1341            '?' => {
1342                if param_index < params.len() {
1343                    result.push_str(&format_value_for_sql(&params[param_index]));
1344                    param_index += 1;
1345                } else {
1346                    result.push('?');
1347                }
1348            }
1349            // PostgreSQL-style $N placeholder
1350            '$' => {
1351                let mut num_str = String::new();
1352                while let Some(&next_ch) = chars.peek() {
1353                    if next_ch.is_ascii_digit() {
1354                        num_str.push(chars.next().unwrap());
1355                    } else {
1356                        break;
1357                    }
1358                }
1359                if num_str.is_empty() {
1360                    result.push('$');
1361                } else if let Ok(n) = num_str.parse::<usize>() {
1362                    if n > 0 && n <= params.len() {
1363                        result.push_str(&format_value_for_sql(&params[n - 1]));
1364                    } else {
1365                        result.push('$');
1366                        result.push_str(&num_str);
1367                    }
1368                } else {
1369                    result.push('$');
1370                    result.push_str(&num_str);
1371                }
1372            }
1373            // Handle string literals (don't replace placeholders inside)
1374            '\'' => {
1375                result.push(ch);
1376                while let Some(next_ch) = chars.next() {
1377                    result.push(next_ch);
1378                    if next_ch == '\'' {
1379                        // Check for escaped quote
1380                        if chars.peek() == Some(&'\'') {
1381                            result.push(chars.next().unwrap());
1382                        } else {
1383                            break;
1384                        }
1385                    }
1386                }
1387            }
1388            // Handle double-quoted identifiers
1389            '"' => {
1390                result.push(ch);
1391                while let Some(next_ch) = chars.next() {
1392                    result.push(next_ch);
1393                    if next_ch == '"' {
1394                        if chars.peek() == Some(&'"') {
1395                            result.push(chars.next().unwrap());
1396                        } else {
1397                            break;
1398                        }
1399                    }
1400                }
1401            }
1402            // Handle backtick identifiers (MySQL-specific)
1403            '`' => {
1404                result.push(ch);
1405                while let Some(next_ch) = chars.next() {
1406                    result.push(next_ch);
1407                    if next_ch == '`' {
1408                        if chars.peek() == Some(&'`') {
1409                            result.push(chars.next().unwrap());
1410                        } else {
1411                            break;
1412                        }
1413                    }
1414                }
1415            }
1416            _ => result.push(ch),
1417        }
1418    }
1419
1420    result
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425    use super::*;
1426
1427    #[test]
1428    fn test_escape_string() {
1429        assert_eq!(escape_string("hello"), "'hello'");
1430        assert_eq!(escape_string("it's"), "'it''s'");
1431        assert_eq!(escape_string("a\\b"), "'a\\\\b'");
1432        assert_eq!(escape_string("line\nbreak"), "'line\\nbreak'");
1433    }
1434
1435    #[test]
1436    fn test_format_value() {
1437        assert_eq!(format_value_for_sql(&Value::Null), "NULL");
1438        assert_eq!(format_value_for_sql(&Value::Int(42)), "42");
1439        assert_eq!(
1440            format_value_for_sql(&Value::Text("hello".to_string())),
1441            "'hello'"
1442        );
1443        assert_eq!(format_value_for_sql(&Value::Bool(true)), "TRUE");
1444    }
1445
1446    #[test]
1447    fn test_interpolate_params_question_mark() {
1448        let sql = "SELECT * FROM users WHERE id = ? AND name = ?";
1449        let params = vec![Value::Int(1), Value::Text("Alice".to_string())];
1450        let result = interpolate_params(sql, &params);
1451        assert_eq!(
1452            result,
1453            "SELECT * FROM users WHERE id = 1 AND name = 'Alice'"
1454        );
1455    }
1456
1457    #[test]
1458    fn test_interpolate_params_dollar() {
1459        let sql = "SELECT * FROM users WHERE id = $1 AND name = $2";
1460        let params = vec![Value::Int(1), Value::Text("Alice".to_string())];
1461        let result = interpolate_params(sql, &params);
1462        assert_eq!(
1463            result,
1464            "SELECT * FROM users WHERE id = 1 AND name = 'Alice'"
1465        );
1466    }
1467
1468    #[test]
1469    fn test_interpolate_no_replace_in_string() {
1470        let sql = "SELECT * FROM users WHERE name = '$1' AND id = ?";
1471        let params = vec![Value::Int(42)];
1472        let result = interpolate_params(sql, &params);
1473        assert_eq!(result, "SELECT * FROM users WHERE name = '$1' AND id = 42");
1474    }
1475
1476    #[test]
1477    fn test_field_type_from_u8() {
1478        assert_eq!(FieldType::from_u8(0x01), FieldType::Tiny);
1479        assert_eq!(FieldType::from_u8(0x03), FieldType::Long);
1480        assert_eq!(FieldType::from_u8(0x08), FieldType::LongLong);
1481        assert_eq!(FieldType::from_u8(0xFC), FieldType::Blob);
1482        assert_eq!(FieldType::from_u8(0xF5), FieldType::Json);
1483    }
1484
1485    #[test]
1486    fn test_field_type_categories() {
1487        assert!(FieldType::Tiny.is_integer());
1488        assert!(FieldType::Long.is_integer());
1489        assert!(FieldType::LongLong.is_integer());
1490
1491        assert!(FieldType::Float.is_float());
1492        assert!(FieldType::Double.is_float());
1493
1494        assert!(FieldType::Decimal.is_decimal());
1495        assert!(FieldType::NewDecimal.is_decimal());
1496
1497        assert!(FieldType::VarChar.is_string());
1498        assert!(FieldType::String.is_string());
1499
1500        assert!(FieldType::Blob.is_blob());
1501        assert!(FieldType::TinyBlob.is_blob());
1502
1503        assert!(FieldType::Date.is_temporal());
1504        assert!(FieldType::DateTime.is_temporal());
1505        assert!(FieldType::Timestamp.is_temporal());
1506    }
1507
1508    #[test]
1509    fn test_decode_text_integer() {
1510        let val = decode_text_value(FieldType::Long, b"42", false);
1511        assert!(matches!(val, Value::Int(42)));
1512
1513        let val = decode_text_value(FieldType::LongLong, b"-100", false);
1514        assert!(matches!(val, Value::BigInt(-100)));
1515    }
1516
1517    #[test]
1518    #[allow(clippy::approx_constant)]
1519    fn test_decode_text_float() {
1520        let val = decode_text_value(FieldType::Double, b"3.14", false);
1521        assert!(matches!(val, Value::Double(f) if (f - 3.14).abs() < 0.001));
1522    }
1523
1524    #[test]
1525    fn test_decode_text_string() {
1526        let val = decode_text_value(FieldType::VarChar, b"hello", false);
1527        assert!(matches!(val, Value::Text(s) if s == "hello"));
1528    }
1529
1530    #[test]
1531    fn test_decode_text_date_to_value_date() {
1532        let val = decode_text_value(FieldType::Date, b"2024-02-29", false);
1533        let expected_days = ymd_to_days_since_unix_epoch(2024, 2, 29).unwrap();
1534        assert_eq!(val, Value::Date(expected_days));
1535    }
1536
1537    #[test]
1538    fn test_decode_text_date_zero_preserved_as_text() {
1539        let val = decode_text_value(FieldType::Date, b"0000-00-00", false);
1540        assert_eq!(val, Value::Text("0000-00-00".to_string()));
1541    }
1542
1543    #[test]
1544    fn test_decode_text_time_to_value_time() {
1545        // 25:00:00.000001 (text protocol can emit hours > 23)
1546        let val = decode_text_value(FieldType::Time, b"25:00:00.000001", false);
1547        let expected = (25_i64 * 3600_i64) * 1_000_000_i64 + 1;
1548        assert_eq!(val, Value::Time(expected));
1549    }
1550
1551    #[test]
1552    fn test_decode_text_time_negative_to_value_time() {
1553        let val = decode_text_value(FieldType::Time, b"-01:02:03.4", false);
1554        // Fractional seconds are right-padded to 6 digits: ".4" -> 400_000 us.
1555        let expected = -(((3600_i64 + 2 * 60 + 3) * 1_000_000_i64) + 400_000_i64);
1556        assert_eq!(val, Value::Time(expected));
1557    }
1558
1559    #[test]
1560    fn test_decode_text_time_zero_preserved_as_text() {
1561        let val = decode_text_value(FieldType::Time, b"00:00:00", false);
1562        assert_eq!(val, Value::Text("00:00:00".to_string()));
1563    }
1564
1565    #[test]
1566    fn test_decode_text_datetime_to_value_timestamp() {
1567        let val = decode_text_value(FieldType::DateTime, b"2020-01-02 03:04:05.000006", false);
1568
1569        let days = i64::from(ymd_to_days_since_unix_epoch(2020, 1, 2).unwrap());
1570        let tod_us = ((3_i64 * 3600 + 4 * 60 + 5) * 1_000_000) + 6;
1571        let expected = days * 86_400 * 1_000_000 + tod_us;
1572        assert_eq!(val, Value::Timestamp(expected));
1573    }
1574
1575    #[test]
1576    fn test_decode_text_timestamp_zero_preserved_as_text() {
1577        let val = decode_text_value(FieldType::Timestamp, b"0000-00-00 00:00:00", false);
1578        assert_eq!(val, Value::Text("0000-00-00 00:00:00".to_string()));
1579    }
1580
1581    #[test]
1582    fn test_decode_binary_tiny() {
1583        let val = decode_binary_value(FieldType::Tiny, &[42], false);
1584        assert!(matches!(val, Value::TinyInt(42)));
1585
1586        let val = decode_binary_value(FieldType::Tiny, &[255u8], true);
1587        assert!(matches!(val, Value::TinyInt(-1))); // 255u8 as i8 = -1
1588
1589        let val = decode_binary_value(FieldType::Tiny, &[255], false);
1590        assert!(matches!(val, Value::TinyInt(-1)));
1591    }
1592
1593    #[test]
1594    fn test_decode_binary_long() {
1595        let val = decode_binary_value(FieldType::Long, &[0x2A, 0x00, 0x00, 0x00], false);
1596        assert!(matches!(val, Value::Int(42)));
1597    }
1598
1599    #[test]
1600    #[allow(clippy::approx_constant)]
1601    fn test_decode_binary_double() {
1602        let pi_bytes = 3.14159_f64.to_le_bytes();
1603        let val = decode_binary_value(FieldType::Double, &pi_bytes, false);
1604        assert!(matches!(val, Value::Double(f) if (f - 3.14159).abs() < 0.00001));
1605    }
1606
1607    #[test]
1608    fn test_decode_binary_date_with_len_to_value_date() {
1609        // 2024-02-29
1610        let mut buf = Vec::new();
1611        buf.push(4);
1612        buf.extend_from_slice(&2024_u16.to_le_bytes());
1613        buf.push(2);
1614        buf.push(29);
1615
1616        let (val, consumed) = decode_binary_value_with_len(&buf, FieldType::Date, false);
1617        assert_eq!(consumed, 5);
1618
1619        let expected_days = ymd_to_days_since_unix_epoch(2024, 2, 29).unwrap();
1620        assert_eq!(val, Value::Date(expected_days));
1621    }
1622
1623    #[test]
1624    fn test_decode_binary_time_with_len_to_value_time() {
1625        // +1 day 02:03:04.000005
1626        let mut buf = Vec::new();
1627        buf.push(12);
1628        buf.push(0); // positive
1629        buf.extend_from_slice(&1_u32.to_le_bytes());
1630        buf.push(2);
1631        buf.push(3);
1632        buf.push(4);
1633        buf.extend_from_slice(&5_u32.to_le_bytes());
1634
1635        let (val, consumed) = decode_binary_value_with_len(&buf, FieldType::Time, false);
1636        assert_eq!(consumed, 13);
1637
1638        let total_seconds = (24_i64 + 2) * 3600 + 3 * 60 + 4;
1639        let expected = total_seconds * 1_000_000 + 5;
1640        assert_eq!(val, Value::Time(expected));
1641    }
1642
1643    #[test]
1644    fn test_decode_binary_datetime_with_len_to_value_timestamp() {
1645        // 2020-01-02 03:04:05.000006
1646        let mut buf = Vec::new();
1647        buf.push(11);
1648        buf.extend_from_slice(&2020_u16.to_le_bytes());
1649        buf.push(1);
1650        buf.push(2);
1651        buf.push(3);
1652        buf.push(4);
1653        buf.push(5);
1654        buf.extend_from_slice(&6_u32.to_le_bytes());
1655
1656        let (val, consumed) = decode_binary_value_with_len(&buf, FieldType::DateTime, false);
1657        assert_eq!(consumed, 12);
1658
1659        let days = i64::from(ymd_to_days_since_unix_epoch(2020, 1, 2).unwrap());
1660        let tod_us = ((3_i64 * 3600 + 4 * 60 + 5) * 1_000_000) + 6;
1661        let expected = days * 86_400 * 1_000_000 + tod_us;
1662        assert_eq!(val, Value::Timestamp(expected));
1663    }
1664
1665    #[test]
1666    fn test_column_flags() {
1667        let col = ColumnDef {
1668            catalog: "def".to_string(),
1669            schema: "test".to_string(),
1670            table: "users".to_string(),
1671            org_table: "users".to_string(),
1672            name: "id".to_string(),
1673            org_name: "id".to_string(),
1674            charset: 33,
1675            column_length: 11,
1676            column_type: FieldType::Long,
1677            flags: column_flags::NOT_NULL
1678                | column_flags::PRIMARY_KEY
1679                | column_flags::AUTO_INCREMENT
1680                | column_flags::UNSIGNED,
1681            decimals: 0,
1682        };
1683
1684        assert!(col.is_not_null());
1685        assert!(col.is_primary_key());
1686        assert!(col.is_auto_increment());
1687        assert!(col.is_unsigned());
1688        assert!(!col.is_binary());
1689    }
1690
1691    #[test]
1692    fn test_encode_length_prefixed() {
1693        // Short string
1694        let result = encode_length_prefixed_bytes(b"hello");
1695        assert_eq!(result[0], 5);
1696        assert_eq!(&result[1..], b"hello");
1697
1698        // Empty
1699        let result = encode_length_prefixed_bytes(b"");
1700        assert_eq!(result, vec![0]);
1701    }
1702}