Skip to main content

sqlmodel_sqlite/
types.rs

1//! Type encoding and decoding between Rust and SQLite.
2//!
3//! SQLite has a simple type system with 5 storage classes:
4//! - INTEGER: Signed integer (1, 2, 3, 4, 6, or 8 bytes)
5//! - REAL: 8-byte IEEE floating point
6//! - TEXT: UTF-8 or UTF-16 string
7//! - BLOB: Binary data
8//! - NULL: The NULL value
9//!
10//! We map these to/from sqlmodel-core's Value type.
11
12// Allow casts in FFI code where we need to match C types exactly
13#![allow(clippy::cast_possible_truncation)]
14#![allow(clippy::cast_sign_loss)]
15#![allow(clippy::cast_lossless)]
16#![allow(clippy::checked_conversions)]
17
18use crate::ffi;
19use sqlmodel_core::Value;
20use std::ffi::{CStr, c_int};
21
22/// Bind a Value to a prepared statement parameter.
23///
24/// # Safety
25/// - `stmt` must be a valid, non-null prepared statement handle
26/// - `index` must be a valid 1-based parameter index
27pub unsafe fn bind_value(stmt: *mut ffi::sqlite3_stmt, index: c_int, value: &Value) -> c_int {
28    // SAFETY: All FFI calls require unsafe in Rust 2024
29    unsafe {
30        match value {
31            Value::Null => ffi::sqlite3_bind_null(stmt, index),
32
33            Value::Bool(b) => ffi::sqlite3_bind_int(stmt, index, if *b { 1 } else { 0 }),
34
35            Value::TinyInt(v) => ffi::sqlite3_bind_int(stmt, index, i32::from(*v)),
36
37            Value::SmallInt(v) => ffi::sqlite3_bind_int(stmt, index, i32::from(*v)),
38
39            Value::Int(v) => ffi::sqlite3_bind_int(stmt, index, *v),
40
41            Value::BigInt(v) => ffi::sqlite3_bind_int64(stmt, index, *v),
42
43            Value::Float(v) => ffi::sqlite3_bind_double(stmt, index, f64::from(*v)),
44
45            Value::Double(v) => ffi::sqlite3_bind_double(stmt, index, *v),
46
47            Value::Decimal(s) => {
48                let bytes = s.as_bytes();
49                ffi::sqlite3_bind_text(
50                    stmt,
51                    index,
52                    bytes.as_ptr().cast(),
53                    bytes.len() as c_int,
54                    ffi::sqlite_transient(),
55                )
56            }
57
58            Value::Text(s) => {
59                let bytes = s.as_bytes();
60                ffi::sqlite3_bind_text(
61                    stmt,
62                    index,
63                    bytes.as_ptr().cast(),
64                    bytes.len() as c_int,
65                    ffi::sqlite_transient(),
66                )
67            }
68
69            Value::Bytes(b) => ffi::sqlite3_bind_blob(
70                stmt,
71                index,
72                b.as_ptr().cast(),
73                b.len() as c_int,
74                ffi::sqlite_transient(),
75            ),
76
77            // Date stored as ISO-8601 text (YYYY-MM-DD)
78            Value::Date(days) => {
79                let date = days_to_date(*days);
80                let bytes = date.as_bytes();
81                ffi::sqlite3_bind_text(
82                    stmt,
83                    index,
84                    bytes.as_ptr().cast(),
85                    bytes.len() as c_int,
86                    ffi::sqlite_transient(),
87                )
88            }
89
90            // Time stored as ISO-8601 text (HH:MM:SS.sss)
91            Value::Time(micros) => {
92                let time = micros_to_time(*micros);
93                let bytes = time.as_bytes();
94                ffi::sqlite3_bind_text(
95                    stmt,
96                    index,
97                    bytes.as_ptr().cast(),
98                    bytes.len() as c_int,
99                    ffi::sqlite_transient(),
100                )
101            }
102
103            // Timestamp stored as ISO-8601 text
104            Value::Timestamp(micros) | Value::TimestampTz(micros) => {
105                let ts = micros_to_timestamp(*micros);
106                let bytes = ts.as_bytes();
107                ffi::sqlite3_bind_text(
108                    stmt,
109                    index,
110                    bytes.as_ptr().cast(),
111                    bytes.len() as c_int,
112                    ffi::sqlite_transient(),
113                )
114            }
115
116            // UUID stored as 16-byte blob
117            Value::Uuid(bytes) => ffi::sqlite3_bind_blob(
118                stmt,
119                index,
120                bytes.as_ptr().cast(),
121                16,
122                ffi::sqlite_transient(),
123            ),
124
125            // JSON stored as text
126            Value::Json(json) => {
127                let s = json.to_string();
128                let bytes = s.as_bytes();
129                ffi::sqlite3_bind_text(
130                    stmt,
131                    index,
132                    bytes.as_ptr().cast(),
133                    bytes.len() as c_int,
134                    ffi::sqlite_transient(),
135                )
136            }
137
138            // Arrays stored as JSON text
139            Value::Array(arr) => {
140                let json = serde_json::Value::Array(arr.iter().map(value_to_json).collect());
141                let s = json.to_string();
142                let bytes = s.as_bytes();
143                ffi::sqlite3_bind_text(
144                    stmt,
145                    index,
146                    bytes.as_ptr().cast(),
147                    bytes.len() as c_int,
148                    ffi::sqlite_transient(),
149                )
150            }
151
152            // Default should never reach bind_value - query builder puts "DEFAULT"
153            // directly in SQL text. Bind NULL as defensive fallback.
154            Value::Default => ffi::sqlite3_bind_null(stmt, index),
155        }
156    }
157}
158
159/// Read a column value from a result row.
160///
161/// # Safety
162/// - `stmt` must be a valid prepared statement that has just returned SQLITE_ROW
163/// - `index` must be a valid 0-based column index
164pub unsafe fn read_column(stmt: *mut ffi::sqlite3_stmt, index: c_int) -> Value {
165    // SAFETY: All FFI calls require unsafe in Rust 2024
166    unsafe {
167        let col_type = ffi::sqlite3_column_type(stmt, index);
168
169        match col_type {
170            ffi::SQLITE_NULL => Value::Null,
171
172            ffi::SQLITE_INTEGER => {
173                let v = ffi::sqlite3_column_int64(stmt, index);
174                // Choose the smallest representation
175                if v >= i32::MIN as i64 && v <= i32::MAX as i64 {
176                    Value::Int(v as i32)
177                } else {
178                    Value::BigInt(v)
179                }
180            }
181
182            ffi::SQLITE_FLOAT => {
183                let v = ffi::sqlite3_column_double(stmt, index);
184                Value::Double(v)
185            }
186
187            ffi::SQLITE_TEXT => {
188                let ptr = ffi::sqlite3_column_text(stmt, index);
189                let len = ffi::sqlite3_column_bytes(stmt, index);
190                if ptr.is_null() {
191                    Value::Null
192                } else {
193                    let slice = std::slice::from_raw_parts(ptr.cast::<u8>(), len as usize);
194                    let s = String::from_utf8_lossy(slice).into_owned();
195                    Value::Text(s)
196                }
197            }
198
199            ffi::SQLITE_BLOB => {
200                let ptr = ffi::sqlite3_column_blob(stmt, index);
201                let len = ffi::sqlite3_column_bytes(stmt, index);
202                if ptr.is_null() || len == 0 {
203                    Value::Bytes(Vec::new())
204                } else {
205                    let slice = std::slice::from_raw_parts(ptr.cast::<u8>(), len as usize);
206                    Value::Bytes(slice.to_vec())
207                }
208            }
209
210            _ => Value::Null,
211        }
212    }
213}
214
215/// Get the column name from a result.
216///
217/// # Safety
218/// - `stmt` must be a valid prepared statement
219/// - `index` must be a valid 0-based column index
220pub unsafe fn column_name(stmt: *mut ffi::sqlite3_stmt, index: c_int) -> Option<String> {
221    // SAFETY: All FFI calls require unsafe in Rust 2024
222    unsafe {
223        let ptr = ffi::sqlite3_column_name(stmt, index);
224        if ptr.is_null() {
225            None
226        } else {
227            CStr::from_ptr(ptr).to_str().ok().map(String::from)
228        }
229    }
230}
231
232/// Convert days since Unix epoch to ISO-8601 date string.
233fn days_to_date(days: i32) -> String {
234    // Simple calculation - for a proper implementation, use a date library
235    // This is a naive implementation for basic testing
236    let epoch = 719_528; // Days from year 0 to 1970-01-01
237    let total_days = days + epoch;
238
239    // Calculate year, month, day from total days
240    let (year, month, day) = days_to_ymd(total_days);
241    format!("{:04}-{:02}-{:02}", year, month, day)
242}
243
244/// Convert total days since year 0 to year/month/day.
245fn days_to_ymd(days: i32) -> (i32, u32, u32) {
246    // Simplified algorithm - good enough for testing
247    let mut remaining = days;
248    let mut year = 0i32;
249
250    // Find year
251    while remaining >= days_in_year(year) {
252        remaining -= days_in_year(year);
253        year += 1;
254    }
255    while remaining < 0 {
256        year -= 1;
257        remaining += days_in_year(year);
258    }
259
260    // Find month
261    let mut month = 1u32;
262    while remaining >= days_in_month(year, month) as i32 {
263        remaining -= days_in_month(year, month) as i32;
264        month += 1;
265    }
266
267    let day = (remaining + 1) as u32;
268    (year, month, day)
269}
270
271fn is_leap_year(year: i32) -> bool {
272    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
273}
274
275fn days_in_year(year: i32) -> i32 {
276    if is_leap_year(year) { 366 } else { 365 }
277}
278
279fn days_in_month(year: i32, month: u32) -> u32 {
280    match month {
281        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
282        4 | 6 | 9 | 11 => 30,
283        2 => {
284            if is_leap_year(year) {
285                29
286            } else {
287                28
288            }
289        }
290        _ => 30,
291    }
292}
293
294/// Convert microseconds since midnight to ISO-8601 time string.
295fn micros_to_time(micros: i64) -> String {
296    let total_secs = micros / 1_000_000;
297    let hours = (total_secs / 3600) % 24;
298    let minutes = (total_secs / 60) % 60;
299    let seconds = total_secs % 60;
300    let millis = (micros % 1_000_000) / 1000;
301
302    if millis > 0 {
303        format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
304    } else {
305        format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
306    }
307}
308
309/// Convert microseconds since Unix epoch to ISO-8601 timestamp.
310fn micros_to_timestamp(micros: i64) -> String {
311    let secs = micros / 1_000_000;
312    let days = (secs / 86400) as i32;
313    let time_of_day = (micros % 86_400_000_000).unsigned_abs() as i64;
314
315    let date = days_to_date(days);
316    let time = micros_to_time(time_of_day);
317
318    format!("{}T{}", date, time)
319}
320
321/// Convert a Value to a serde_json::Value for array serialization.
322fn value_to_json(value: &Value) -> serde_json::Value {
323    match value {
324        Value::Null => serde_json::Value::Null,
325        Value::Bool(b) => serde_json::Value::Bool(*b),
326        Value::TinyInt(v) => serde_json::Value::Number((*v).into()),
327        Value::SmallInt(v) => serde_json::Value::Number((*v).into()),
328        Value::Int(v) => serde_json::Value::Number((*v).into()),
329        Value::BigInt(v) => serde_json::Value::Number((*v).into()),
330        Value::Float(v) => serde_json::Number::from_f64(f64::from(*v))
331            .map_or(serde_json::Value::Null, serde_json::Value::Number),
332        Value::Double(v) => serde_json::Number::from_f64(*v)
333            .map_or(serde_json::Value::Null, serde_json::Value::Number),
334        Value::Decimal(s) | Value::Text(s) => serde_json::Value::String(s.clone()),
335        Value::Bytes(b) => serde_json::Value::String(base64_encode(b)),
336        Value::Date(d) => serde_json::Value::String(days_to_date(*d)),
337        Value::Time(t) => serde_json::Value::String(micros_to_time(*t)),
338        Value::Timestamp(ts) | Value::TimestampTz(ts) => {
339            serde_json::Value::String(micros_to_timestamp(*ts))
340        }
341        Value::Uuid(bytes) => serde_json::Value::String(uuid_to_string(bytes)),
342        Value::Json(j) => j.clone(),
343        Value::Array(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
344        Value::Default => serde_json::Value::Null,
345    }
346}
347
348/// Simple base64 encoding for bytes in JSON.
349fn base64_encode(data: &[u8]) -> String {
350    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
351
352    let mut result = String::new();
353    for chunk in data.chunks(3) {
354        let b0 = chunk[0];
355        let b1 = chunk.get(1).copied().unwrap_or(0);
356        let b2 = chunk.get(2).copied().unwrap_or(0);
357
358        result.push(ALPHABET[(b0 >> 2) as usize] as char);
359        result.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
360
361        if chunk.len() > 1 {
362            result.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
363        } else {
364            result.push('=');
365        }
366
367        if chunk.len() > 2 {
368            result.push(ALPHABET[(b2 & 0x3f) as usize] as char);
369        } else {
370            result.push('=');
371        }
372    }
373    result
374}
375
376/// Convert UUID bytes to hyphenated string format.
377fn uuid_to_string(bytes: &[u8; 16]) -> String {
378    format!(
379        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
380        bytes[0],
381        bytes[1],
382        bytes[2],
383        bytes[3],
384        bytes[4],
385        bytes[5],
386        bytes[6],
387        bytes[7],
388        bytes[8],
389        bytes[9],
390        bytes[10],
391        bytes[11],
392        bytes[12],
393        bytes[13],
394        bytes[14],
395        bytes[15]
396    )
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_days_to_date() {
405        // 1970-01-01 is day 0
406        assert_eq!(days_to_date(0), "1970-01-01");
407        // 1970-01-02 is day 1
408        assert_eq!(days_to_date(1), "1970-01-02");
409    }
410
411    #[test]
412    fn test_micros_to_time() {
413        assert_eq!(micros_to_time(0), "00:00:00");
414        assert_eq!(micros_to_time(3_600_000_000), "01:00:00");
415        assert_eq!(micros_to_time(3_661_123_000), "01:01:01.123");
416    }
417
418    #[test]
419    fn test_base64_encode() {
420        assert_eq!(base64_encode(b""), "");
421        assert_eq!(base64_encode(b"f"), "Zg==");
422        assert_eq!(base64_encode(b"fo"), "Zm8=");
423        assert_eq!(base64_encode(b"foo"), "Zm9v");
424        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
425    }
426
427    #[test]
428    fn test_uuid_to_string() {
429        let uuid = [
430            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
431            0x0f, 0x10,
432        ];
433        assert_eq!(
434            uuid_to_string(&uuid),
435            "01020304-0506-0708-090a-0b0c0d0e0f10"
436        );
437    }
438}