Skip to main content

rustbasic_core/sql/driver/
mod.rs

1pub mod error;
2
3#[cfg(feature = "mysql")]
4pub mod mysql;
5
6#[cfg(feature = "sqlite")]
7pub mod sqlite;
8
9pub use error::SqlError;
10
11// Unified data types
12#[derive(Debug, Clone, PartialEq)]
13pub enum SqlValue {
14    Null,
15    Text(String),
16    Blob(Vec<u8>),
17    Integer(i64),
18    Real(f64),
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct SqlColumn {
23    pub name: String,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct SqlRow {
28    pub columns: Vec<SqlColumn>,
29    pub values: Vec<SqlValue>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct QueryResult {
34    pub rows_affected: u64,
35    pub last_insert_id: u64,
36}
37
38// ==========================================
39// 1. Unified Connection Trait
40// ==========================================
41pub trait SqlConnection: Send {
42    fn execute(&mut self, sql: &str, params: &[SqlValue]) -> Result<QueryResult, SqlError>;
43    fn query(&mut self, sql: &str, params: &[SqlValue]) -> Result<Vec<SqlRow>, SqlError>;
44}
45
46// ==========================================
47// 2. ToSql Trait and Implementations
48// ==========================================
49pub trait ToSql {
50    fn to_sql(&self) -> SqlValue;
51}
52
53impl ToSql for String {
54    fn to_sql(&self) -> SqlValue {
55        SqlValue::Text(self.clone())
56    }
57}
58
59impl ToSql for &str {
60    fn to_sql(&self) -> SqlValue {
61        SqlValue::Text(self.to_string())
62    }
63}
64
65impl ToSql for i64 {
66    fn to_sql(&self) -> SqlValue {
67        SqlValue::Integer(*self)
68    }
69}
70
71impl ToSql for i32 {
72    fn to_sql(&self) -> SqlValue {
73        SqlValue::Integer(*self as i64)
74    }
75}
76
77impl ToSql for i16 {
78    fn to_sql(&self) -> SqlValue {
79        SqlValue::Integer(*self as i64)
80    }
81}
82
83impl ToSql for i8 {
84    fn to_sql(&self) -> SqlValue {
85        SqlValue::Integer(*self as i64)
86    }
87}
88
89impl ToSql for u64 {
90    fn to_sql(&self) -> SqlValue {
91        SqlValue::Integer(*self as i64)
92    }
93}
94
95impl ToSql for u32 {
96    fn to_sql(&self) -> SqlValue {
97        SqlValue::Integer(*self as i64)
98    }
99}
100
101impl ToSql for u16 {
102    fn to_sql(&self) -> SqlValue {
103        SqlValue::Integer(*self as i64)
104    }
105}
106
107impl ToSql for u8 {
108    fn to_sql(&self) -> SqlValue {
109        SqlValue::Integer(*self as i64)
110    }
111}
112
113impl ToSql for f64 {
114    fn to_sql(&self) -> SqlValue {
115        SqlValue::Real(*self)
116    }
117}
118
119impl ToSql for f32 {
120    fn to_sql(&self) -> SqlValue {
121        SqlValue::Real(*self as f64)
122    }
123}
124
125impl ToSql for bool {
126    fn to_sql(&self) -> SqlValue {
127        SqlValue::Integer(if *self { 1 } else { 0 })
128    }
129}
130
131impl ToSql for Vec<u8> {
132    fn to_sql(&self) -> SqlValue {
133        SqlValue::Blob(self.clone())
134    }
135}
136
137impl ToSql for &[u8] {
138    fn to_sql(&self) -> SqlValue {
139        SqlValue::Blob(self.to_vec())
140    }
141}
142
143impl<T: ToSql> ToSql for Option<T> {
144    fn to_sql(&self) -> SqlValue {
145        match self {
146            Some(v) => v.to_sql(),
147            None => SqlValue::Null,
148        }
149    }
150}
151
152// Helper macro for binding parameters conveniently
153#[macro_export]
154macro_rules! sql_params {
155    ($($val:expr),* $(,)?) => {
156        vec![
157            $(
158                $crate::sql::driver::ToSql::to_sql(&$val)
159            ),*
160        ]
161    };
162}
163
164// ==========================================
165// 3. Unified URL connection builder
166// ==========================================
167#[cfg(feature = "mysql")]
168struct MysqlUrl {
169    host: String,
170    port: u16,
171    user: String,
172    password: String,
173    database: String,
174}
175
176#[cfg(feature = "mysql")]
177fn parse_mysql_url(url: &str) -> Result<MysqlUrl, SqlError> {
178    if !url.starts_with("mysql://") {
179        return Err(SqlError::Other("Invalid MySQL URL scheme".into()));
180    }
181    let s = &url["mysql://".len()..];
182    
183    let (creds, host_db) = if let Some(idx) = s.find('@') {
184        (&s[..idx], &s[idx + 1..])
185    } else {
186        ("", s)
187    };
188    
189    let mut user = String::new();
190    let mut password = String::new();
191    if !creds.is_empty() {
192        if let Some(colon_idx) = creds.find(':') {
193            user = creds[..colon_idx].to_string();
194            password = creds[colon_idx + 1..].to_string();
195        } else {
196            user = creds.to_string();
197        }
198    }
199    
200    let (host_port, database) = if let Some(slash_idx) = host_db.find('/') {
201        (&host_db[..slash_idx], host_db[slash_idx + 1..].to_string())
202    } else {
203        (host_db, String::new())
204    };
205    
206    let mut host = "127.0.0.1".to_string();
207    let mut port = 3306;
208    if !host_port.is_empty() {
209        if let Some(colon_idx) = host_port.find(':') {
210            host = host_port[..colon_idx].to_string();
211            if let Ok(p) = host_port[colon_idx + 1..].parse::<u16>() {
212                port = p;
213            }
214        } else {
215            host = host_port.to_string();
216        }
217    }
218    
219    Ok(MysqlUrl {
220        host,
221        port,
222        user,
223        password,
224        database,
225    })
226}
227
228pub fn connect(url: &str) -> Result<Box<dyn SqlConnection>, SqlError> {
229    if url.starts_with("sqlite://") {
230        let path = &url["sqlite://".len()..];
231        #[cfg(feature = "sqlite")]
232        {
233            let conn = sqlite::SqliteConnection::connect(path)?;
234            Ok(Box::new(conn))
235        }
236        #[cfg(not(feature = "sqlite"))]
237        {
238            let _ = path;
239            Err(SqlError::Other("SQLite feature not enabled".into()))
240        }
241    } else if url.starts_with("mysql://") {
242        #[cfg(feature = "mysql")]
243        {
244            let parsed = parse_mysql_url(url)?;
245            let conn = mysql::MySqlConnection::connect(
246                &parsed.host,
247                parsed.port,
248                &parsed.user,
249                &parsed.password,
250                &parsed.database,
251            )?;
252            Ok(Box::new(conn))
253        }
254        #[cfg(not(feature = "mysql"))]
255        {
256            Err(SqlError::Other("MySQL feature not enabled".into()))
257        }
258    } else {
259        Err(SqlError::Other(format!("Unsupported database URL scheme: {}", url)))
260    }
261}
262
263pub trait RowIndex {
264    fn index(&self, row: &SqlRow) -> Result<usize, SqlError>;
265}
266
267impl RowIndex for usize {
268    fn index(&self, row: &SqlRow) -> Result<usize, SqlError> {
269        if *self < row.len() {
270            Ok(*self)
271        } else {
272            Err(SqlError::ColumnIndexOutOfBounds {
273                len: row.len(),
274                index: *self,
275            })
276        }
277    }
278}
279
280impl RowIndex for &str {
281    fn index(&self, row: &SqlRow) -> Result<usize, SqlError> {
282        row.columns
283            .iter()
284            .position(|col| col.name == *self)
285            .ok_or_else(|| SqlError::ColumnNotFound((*self).to_string()))
286    }
287}
288
289impl RowIndex for String {
290    fn index(&self, row: &SqlRow) -> Result<usize, SqlError> {
291        row.columns
292            .iter()
293            .position(|col| col.name == *self)
294            .ok_or_else(|| SqlError::ColumnNotFound((*self).to_string()))
295    }
296}
297
298pub trait FromSql: Sized {
299    fn from_sql(value: &SqlValue) -> Result<Self, SqlError>;
300}
301
302impl<T: FromSql> FromSql for Option<T> {
303    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
304        match value {
305            SqlValue::Null => Ok(None),
306            other => T::from_sql(other).map(Some),
307        }
308    }
309}
310
311impl FromSql for String {
312    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
313        match value {
314            SqlValue::Text(s) => Ok(s.clone()),
315            SqlValue::Integer(i) => Ok(i.to_string()),
316            SqlValue::Real(f) => Ok(f.to_string()),
317            SqlValue::Blob(b) => String::from_utf8(b.clone())
318                .map_err(|e| SqlError::Decode(format!("Invalid UTF-8 in blob: {}", e))),
319            SqlValue::Null => Err(SqlError::Decode("Cannot decode NULL to String".into())),
320        }
321    }
322}
323
324impl FromSql for i64 {
325    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
326        match value {
327            SqlValue::Integer(i) => Ok(*i),
328            SqlValue::Text(s) => s.parse::<i64>()
329                .map_err(|e| SqlError::Decode(format!("Failed to parse i64: {}", e))),
330            SqlValue::Real(f) => Ok(*f as i64),
331            _ => Err(SqlError::Decode("Cannot decode to i64".into())),
332        }
333    }
334}
335
336impl FromSql for i32 {
337    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
338        match value {
339            SqlValue::Integer(i) => Ok(*i as i32),
340            SqlValue::Text(s) => s.parse::<i32>()
341                .map_err(|e| SqlError::Decode(format!("Failed to parse i32: {}", e))),
342            SqlValue::Real(f) => Ok(*f as i32),
343            _ => Err(SqlError::Decode("Cannot decode to i32".into())),
344        }
345    }
346}
347
348impl FromSql for i16 {
349    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
350        match value {
351            SqlValue::Integer(i) => Ok(*i as i16),
352            SqlValue::Text(s) => s.parse::<i16>()
353                .map_err(|e| SqlError::Decode(format!("Failed to parse i16: {}", e))),
354            SqlValue::Real(f) => Ok(*f as i16),
355            _ => Err(SqlError::Decode("Cannot decode to i16".into())),
356        }
357    }
358}
359
360impl FromSql for i8 {
361    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
362        match value {
363            SqlValue::Integer(i) => Ok(*i as i8),
364            SqlValue::Text(s) => s.parse::<i8>()
365                .map_err(|e| SqlError::Decode(format!("Failed to parse i8: {}", e))),
366            SqlValue::Real(f) => Ok(*f as i8),
367            _ => Err(SqlError::Decode("Cannot decode to i8".into())),
368        }
369    }
370}
371
372impl FromSql for u64 {
373    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
374        match value {
375            SqlValue::Integer(i) => Ok(*i as u64),
376            SqlValue::Text(s) => s.parse::<u64>()
377                .map_err(|e| SqlError::Decode(format!("Failed to parse u64: {}", e))),
378            SqlValue::Real(f) => Ok(*f as u64),
379            _ => Err(SqlError::Decode("Cannot decode to u64".into())),
380        }
381    }
382}
383
384impl FromSql for u32 {
385    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
386        match value {
387            SqlValue::Integer(i) => Ok(*i as u32),
388            SqlValue::Text(s) => s.parse::<u32>()
389                .map_err(|e| SqlError::Decode(format!("Failed to parse u32: {}", e))),
390            SqlValue::Real(f) => Ok(*f as u32),
391            _ => Err(SqlError::Decode("Cannot decode to u32".into())),
392        }
393    }
394}
395
396impl FromSql for u16 {
397    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
398        match value {
399            SqlValue::Integer(i) => Ok(*i as u16),
400            SqlValue::Text(s) => s.parse::<u16>()
401                .map_err(|e| SqlError::Decode(format!("Failed to parse u16: {}", e))),
402            SqlValue::Real(f) => Ok(*f as u16),
403            _ => Err(SqlError::Decode("Cannot decode to u16".into())),
404        }
405    }
406}
407
408impl FromSql for u8 {
409    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
410        match value {
411            SqlValue::Integer(i) => Ok(*i as u8),
412            SqlValue::Text(s) => s.parse::<u8>()
413                .map_err(|e| SqlError::Decode(format!("Failed to parse u8: {}", e))),
414            SqlValue::Real(f) => Ok(*f as u8),
415            _ => Err(SqlError::Decode("Cannot decode to u8".into())),
416        }
417    }
418}
419
420impl FromSql for f64 {
421    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
422        match value {
423            SqlValue::Real(f) => Ok(*f),
424            SqlValue::Integer(i) => Ok(*i as f64),
425            SqlValue::Text(s) => s.parse::<f64>()
426                .map_err(|e| SqlError::Decode(format!("Failed to parse f64: {}", e))),
427            _ => Err(SqlError::Decode("Cannot decode to f64".into())),
428        }
429    }
430}
431
432impl FromSql for f32 {
433    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
434        match value {
435            SqlValue::Real(f) => Ok(*f as f32),
436            SqlValue::Integer(i) => Ok(*i as f32),
437            SqlValue::Text(s) => s.parse::<f32>()
438                .map_err(|e| SqlError::Decode(format!("Failed to parse f32: {}", e))),
439            _ => Err(SqlError::Decode("Cannot decode to f32".into())),
440        }
441    }
442}
443
444impl FromSql for bool {
445    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
446        match value {
447            SqlValue::Integer(i) => Ok(*i != 0),
448            SqlValue::Text(s) => {
449                let s_lower = s.to_lowercase();
450                if s_lower == "true" || s_lower == "1" || s_lower == "t" || s_lower == "y" || s_lower == "yes" {
451                    Ok(true)
452                } else if s_lower == "false" || s_lower == "0" || s_lower == "f" || s_lower == "n" || s_lower == "no" || s_lower.is_empty() {
453                    Ok(false)
454                } else {
455                    Err(SqlError::Decode(format!("Cannot decode '{}' to bool", s)))
456                }
457            }
458            _ => Err(SqlError::Decode("Cannot decode to bool".into())),
459        }
460    }
461}
462
463impl FromSql for Vec<u8> {
464    fn from_sql(value: &SqlValue) -> Result<Self, SqlError> {
465        match value {
466            SqlValue::Blob(b) => Ok(b.clone()),
467            SqlValue::Text(s) => Ok(s.as_bytes().to_vec()),
468            _ => Err(SqlError::Decode("Cannot decode to Vec<u8>".into())),
469        }
470    }
471}
472
473impl SqlRow {
474    pub fn len(&self) -> usize {
475        self.values.len()
476    }
477    
478    pub fn is_empty(&self) -> bool {
479        self.values.is_empty()
480    }
481    
482    pub fn column(&self, index: usize) -> &SqlColumn {
483        &self.columns[index]
484    }
485    
486    pub fn get_value(&self, name: &str) -> Option<&SqlValue> {
487        self.columns.iter().position(|col| col.name == name)
488            .map(|idx| &self.values[idx])
489    }
490
491    pub fn try_get<T, I>(&self, index: I) -> Result<T, SqlError>
492    where
493        T: FromSql,
494        I: RowIndex,
495    {
496        let idx = index.index(self)?;
497        let val = &self.values[idx];
498        T::from_sql(val)
499    }
500
501    pub fn get<T, I>(&self, index: I) -> T
502    where
503        T: FromSql,
504        I: RowIndex,
505    {
506        self.try_get(index).unwrap()
507    }
508}