rustbasic_core/sql/driver/
mod.rs1pub 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#[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
38pub 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
46pub 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#[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#[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}