libsql_orm/
model.rs

1//! Model trait and core database operations
2//!
3//! This module provides the main Model trait that enables ORM functionality
4//! for structs that derive from it. Features include:
5//!
6//! - **Custom Table Names**: Use `#[table_name("custom")]` to override default naming
7//! - **Boolean Type Safety**: Automatic conversion between SQLite integers (0/1) and Rust booleans
8//! - **Column Attributes**: Customize column properties with `#[orm_column(...)]`
9//! - **Full CRUD Operations**: Create, read, update, delete with type safety
10//!
11//! # Examples
12//!
13//! ```rust
14//! use libsql_orm::Model;
15//! use serde::{Serialize, Deserialize};
16//!
17//! #[derive(Model, Serialize, Deserialize)]
18//! #[table_name("user_accounts")]  // Custom table name
19//! struct User {
20//!     pub id: Option<i64>,
21//!     pub name: String,
22//!     pub is_active: bool,     // ✅ Automatic boolean conversion
23//!     pub is_verified: bool,   // ✅ Type-safe operations
24//! }
25//! ```
26
27use crate::{
28    Aggregate, Database, Error, FilterOperator, PaginatedResult, Pagination, QueryBuilder, Result,
29    SearchFilter, Sort,
30};
31use std::collections::HashMap;
32
33use serde::{de::DeserializeOwned, Serialize};
34
35/// Mask numeric IDs for logging
36fn mask_id(id: i64) -> String {
37    if id < 100 {
38        return "*".repeat(id.to_string().len());
39    }
40    let id_str = id.to_string();
41    let visible_digits = 2;
42    let masked_digits = id_str.len() - visible_digits;
43    format!("{}{}", &id_str[..visible_digits], "*".repeat(masked_digits))
44}
45
46/// Core trait for all database models
47#[allow(async_fn_in_trait)]
48pub trait Model: Serialize + DeserializeOwned + Send + Sync + Clone {
49    /// Get the table name for this model
50    fn table_name() -> &'static str;
51
52    /// Get the primary key column name
53    fn primary_key() -> &'static str {
54        "id"
55    }
56
57    /// Get the primary key value
58    fn get_primary_key(&self) -> Option<i64>;
59
60    /// Set the primary key value
61    fn set_primary_key(&mut self, id: i64);
62
63    /// Get all column names for this model
64    fn columns() -> Vec<&'static str>;
65
66    /// Generate SQL for creating the table
67    fn migration_sql() -> String;
68
69    /// Convert the model to a HashMap for database operations
70    fn to_map(&self) -> Result<HashMap<String, crate::Value>>;
71
72    /// Create a model from a HashMap
73    fn from_map(map: HashMap<String, crate::Value>) -> Result<Self>;
74
75    /// Create a new record in the database
76    async fn create(&self, db: &Database) -> Result<Self> {
77        let map = self.to_map()?;
78        let columns: Vec<String> = map.keys().cloned().collect();
79        let values: Vec<String> = map.keys().map(|_| "?".to_string()).collect();
80
81        let sql = format!(
82            "INSERT INTO {} ({}) VALUES ({})",
83            Self::table_name(),
84            columns.join(", "),
85            values.join(", ")
86        );
87
88        Self::log_info(&format!("Creating record in table: {}", Self::table_name()));
89        Self::log_debug(&format!("SQL: {sql}"));
90
91        let params: Vec<crate::compat::LibsqlValue> = map
92            .values()
93            .map(|v| Self::value_to_libsql_value(v))
94            .collect();
95
96        db.execute(&sql, params).await?;
97        let id = 1i64; // Placeholder - libsql WASM doesn't support last_insert_rowid
98
99        let mut result = self.clone();
100        result.set_primary_key(id);
101
102        Self::log_info(&format!(
103            "Successfully created record with ID: {}",
104            mask_id(id)
105        ));
106        Ok(result)
107    }
108
109    /// Create or update a record based on whether it has a primary key
110    async fn create_or_update(&self, db: &Database) -> Result<Self> {
111        if let Some(id) = self.get_primary_key() {
112            Self::log_info(&format!(
113                "Updating existing record with ID: {}",
114                mask_id(id)
115            ));
116            // Check if record exists
117            match Self::find_by_id(id, db).await? {
118                Some(_) => {
119                    // Record exists, update it
120                    self.update(db).await
121                }
122                None => {
123                    // Record doesn't exist, create it
124                    Self::log_warn(&format!(
125                        "Record with ID {} not found, creating new record",
126                        mask_id(id)
127                    ));
128                    self.create(db).await
129                }
130            }
131        } else {
132            // No primary key, create new record
133            Self::log_info("Creating new record (no primary key provided)");
134            self.create(db).await
135        }
136    }
137
138    /// Create or update a record based on unique constraints
139    async fn upsert(&self, unique_columns: &[&str], db: &Database) -> Result<Self> {
140        let map = self.to_map()?;
141
142        // Build WHERE clause for unique columns
143        let mut where_conditions = Vec::new();
144        let mut where_params = Vec::new();
145
146        for &column in unique_columns {
147            if let Some(value) = map.get(column) {
148                where_conditions.push(format!("{column} = ?"));
149                where_params.push(Self::value_to_libsql_value(value));
150            }
151        }
152
153        if where_conditions.is_empty() {
154            return Err(Error::Validation(
155                "No unique columns provided for upsert".to_string(),
156            ));
157        }
158
159        let where_clause = where_conditions.join(" AND ");
160        let sql = format!(
161            "SELECT {} FROM {} WHERE {}",
162            Self::primary_key(),
163            Self::table_name(),
164            where_clause
165        );
166
167        Self::log_info(&format!(
168            "Checking for existing record in table: {}",
169            Self::table_name()
170        ));
171        Self::log_debug(&format!("SQL: {sql}"));
172
173        let mut rows = db.query(&sql, where_params).await?;
174
175        if let Some(row) = rows.next().await? {
176            // Record exists, update it
177            if let Some(existing_id) = row.get_value(0).ok().and_then(|v| match v {
178                crate::compat::LibsqlValue::Integer(i) => Some(i),
179                _ => None,
180            }) {
181                Self::log_info(&format!(
182                    "Found existing record with ID: {}, updating",
183                    mask_id(existing_id)
184                ));
185                let mut updated_self = self.clone();
186                updated_self.set_primary_key(existing_id);
187                updated_self.update(db).await
188            } else {
189                Err(Error::Query(
190                    "Failed to get primary key from existing record".to_string(),
191                ))
192            }
193        } else {
194            // Record doesn't exist, create it
195            Self::log_info("No existing record found, creating new one");
196            self.create(db).await
197        }
198    }
199
200    /// Create multiple records in the database
201    async fn bulk_create(models: &[Self], db: &Database) -> Result<Vec<Self>> {
202        if models.is_empty() {
203            return Ok(Vec::new());
204        }
205
206        let mut results = Vec::new();
207        // Note: Manual transaction handling for WASM
208        db.execute("BEGIN", vec![crate::compat::null_value(); 0])
209            .await?;
210
211        for model in models {
212            let map = model.to_map()?;
213            let columns: Vec<String> = map.keys().cloned().collect();
214            let values: Vec<String> = map.keys().map(|_| "?".to_string()).collect();
215
216            let sql = format!(
217                "INSERT INTO {} ({}) VALUES ({})",
218                Self::table_name(),
219                columns.join(", "),
220                values.join(", ")
221            );
222
223            let params: Vec<crate::compat::LibsqlValue> = map
224                .values()
225                .map(|v| Self::value_to_libsql_value(v))
226                .collect();
227
228            db.execute(&sql, params).await?;
229            let id = 1i64; // Placeholder - libsql WASM doesn't support last_insert_rowid
230
231            let mut result = model.clone();
232            result.set_primary_key(id);
233            results.push(result);
234        }
235
236        db.execute("COMMIT", vec![crate::compat::null_value(); 0])
237            .await?;
238        Ok(results)
239    }
240
241    /// Find a record by its primary key
242    async fn find_by_id(id: i64, db: &Database) -> Result<Option<Self>> {
243        let sql = format!(
244            "SELECT * FROM {} WHERE {} = ?",
245            Self::table_name(),
246            Self::primary_key()
247        );
248
249        Self::log_debug(&format!("Finding record by ID: {}", mask_id(id)));
250        Self::log_debug(&format!("SQL: {sql}"));
251
252        let mut rows = db
253            .inner
254            .query(&sql, vec![crate::compat::integer_value(id)])
255            .await?;
256
257        if let Some(row) = rows.next().await? {
258            let map = Self::row_to_map(&row)?;
259            Self::log_debug(&format!("Found record with ID: {}", mask_id(id)));
260            Ok(Some(Self::from_map(map)?))
261        } else {
262            Self::log_debug(&format!("No record found with ID: {}", mask_id(id)));
263            Ok(None)
264        }
265    }
266
267    /// Find a single record by a specific condition
268    async fn find_one(filter: FilterOperator, db: &Database) -> Result<Option<Self>> {
269        let builder = QueryBuilder::new(Self::table_name())
270            .r#where(filter)
271            .limit(1);
272
273        let results = builder.execute::<Self>(db).await?;
274        Ok(results.into_iter().next())
275    }
276
277    /// Find all records
278    async fn find_all(db: &Database) -> Result<Vec<Self>> {
279        let builder = QueryBuilder::new(Self::table_name());
280        builder.execute::<Self>(db).await
281    }
282
283    /// Find records with a filter
284    async fn find_where(filter: FilterOperator, db: &Database) -> Result<Vec<Self>> {
285        let builder = QueryBuilder::new(Self::table_name()).r#where(filter);
286        builder.execute::<Self>(db).await
287    }
288
289    /// Find records with pagination
290    async fn find_paginated(
291        pagination: &Pagination,
292        db: &Database,
293    ) -> Result<PaginatedResult<Self>> {
294        let builder = QueryBuilder::new(Self::table_name());
295        builder.execute_paginated::<Self>(db, pagination).await
296    }
297
298    /// Find records with filter and pagination
299    async fn find_where_paginated(
300        filter: FilterOperator,
301        pagination: &Pagination,
302        db: &Database,
303    ) -> Result<PaginatedResult<Self>> {
304        let builder = QueryBuilder::new(Self::table_name()).r#where(filter);
305        builder.execute_paginated::<Self>(db, pagination).await
306    }
307
308    /// Search records with text search
309    async fn search(
310        search_filter: &SearchFilter,
311        pagination: Option<&Pagination>,
312        db: &Database,
313    ) -> Result<PaginatedResult<Self>> {
314        let filter = search_filter.to_filter_operator();
315        let pagination = pagination.unwrap_or(&Pagination::default()).clone();
316
317        Self::find_where_paginated(filter, &pagination, db).await
318    }
319
320    /// Count all records
321    async fn count(db: &Database) -> Result<u64> {
322        let sql = format!("SELECT COUNT(*) FROM {}", Self::table_name());
323        let mut rows = db.query(&sql, vec![crate::compat::null_value(); 0]).await?;
324
325        if let Some(row) = rows.next().await? {
326            row.get_value(0)
327                .ok()
328                .and_then(|v| match v {
329                    crate::compat::LibsqlValue::Integer(i) => Some(i as u64),
330                    _ => None,
331                })
332                .ok_or_else(|| Error::Query("Failed to get count".to_string()))
333        } else {
334            Err(Error::Query("No count result".to_string()))
335        }
336    }
337
338    /// Count records with a filter
339    async fn count_where(filter: FilterOperator, db: &Database) -> Result<u64> {
340        let builder = QueryBuilder::new(Self::table_name()).r#where(filter);
341
342        let (sql, params) = builder.build_count()?;
343        let mut rows = db.query(&sql, params).await?;
344
345        if let Some(row) = rows.next().await? {
346            row.get_value(0)
347                .ok()
348                .and_then(|v| match v {
349                    crate::compat::LibsqlValue::Integer(i) => Some(i as u64),
350                    _ => None,
351                })
352                .ok_or_else(|| Error::Query("Failed to get count".to_string()))
353        } else {
354            Err(Error::Query("No count result".to_string()))
355        }
356    }
357
358    /// Update a record
359    async fn update(&self, db: &Database) -> Result<Self> {
360        let id = self.get_primary_key().ok_or_else(|| {
361            Error::Validation("Cannot update record without primary key".to_string())
362        })?;
363
364        let map = self.to_map()?;
365        let set_clauses: Vec<String> = map
366            .keys()
367            .filter(|&k| k != Self::primary_key())
368            .map(|k| format!("{k} = ?"))
369            .collect();
370
371        let sql = format!(
372            "UPDATE {} SET {} WHERE {} = ?",
373            Self::table_name(),
374            set_clauses.join(", "),
375            Self::primary_key()
376        );
377
378        Self::log_info(&format!("Updating record with ID: {}", mask_id(id)));
379        Self::log_debug(&format!("SQL: {sql}"));
380
381        let mut params: Vec<crate::compat::LibsqlValue> = map
382            .iter()
383            .filter(|(k, _)| k != &Self::primary_key())
384            .map(|(_, v)| Self::value_to_libsql_value(v))
385            .collect();
386        params.push(crate::compat::integer_value(id));
387
388        db.execute(&sql, params).await?;
389        Self::log_info(&format!(
390            "Successfully updated record with ID: {}",
391            mask_id(id)
392        ));
393        Ok(self.clone())
394    }
395
396    /// Update multiple records
397    async fn bulk_update(models: &[Self], db: &Database) -> Result<Vec<Self>> {
398        if models.is_empty() {
399            return Ok(Vec::new());
400        }
401
402        let mut results = Vec::new();
403        // Note: Manual transaction handling for WASM
404        db.execute("BEGIN", vec![crate::compat::null_value(); 0])
405            .await?;
406
407        for model in models {
408            let result = model.update(db).await?;
409            results.push(result);
410        }
411
412        db.execute("COMMIT", vec![crate::compat::null_value(); 0])
413            .await?;
414        Ok(results)
415    }
416
417    /// Delete a record
418    async fn delete(&self, db: &Database) -> Result<bool> {
419        let id = self.get_primary_key().ok_or_else(|| {
420            Error::Validation("Cannot delete record without primary key".to_string())
421        })?;
422
423        let sql = format!(
424            "DELETE FROM {} WHERE {} = ?",
425            Self::table_name(),
426            Self::primary_key()
427        );
428
429        Self::log_info(&format!("Deleting record with ID: {}", mask_id(id)));
430        Self::log_debug(&format!("SQL: {sql}"));
431
432        db.execute(&sql, vec![crate::compat::integer_value(id)])
433            .await?;
434        Self::log_info(&format!(
435            "Successfully deleted record with ID: {}",
436            mask_id(id)
437        ));
438        Ok(true)
439    }
440
441    /// Delete multiple records
442    async fn bulk_delete(ids: &[i64], db: &Database) -> Result<u64> {
443        if ids.is_empty() {
444            return Ok(0);
445        }
446
447        let placeholders: Vec<String> = ids.iter().map(|_| "?".to_string()).collect();
448        let sql = format!(
449            "DELETE FROM {} WHERE {} IN ({})",
450            Self::table_name(),
451            Self::primary_key(),
452            placeholders.join(", ")
453        );
454
455        let params: Vec<crate::compat::LibsqlValue> = ids.iter().map(|&id| crate::compat::integer_value(id)).collect();
456        db.execute(&sql, params).await?;
457        Ok(ids.len() as u64)
458    }
459
460    /// Delete records with a filter
461    async fn delete_where(filter: FilterOperator, db: &Database) -> Result<u64> {
462        let builder = QueryBuilder::new(Self::table_name()).r#where(filter);
463
464        let (sql, params) = builder.build()?;
465        let delete_sql = sql.replace("SELECT *", "DELETE");
466        db.execute(&delete_sql, params).await?;
467
468        // Note: SQLite doesn't return the number of affected rows directly
469        // This is a simplified implementation
470        Ok(1)
471    }
472
473    /// List records with optional sorting and pagination
474    async fn list(
475        sort: Option<Vec<Sort>>,
476        pagination: Option<&Pagination>,
477        db: &Database,
478    ) -> Result<PaginatedResult<Self>> {
479        let mut builder = QueryBuilder::new(Self::table_name());
480
481        if let Some(sorts) = sort {
482            builder = builder.order_by_multiple(sorts);
483        }
484
485        let pagination = pagination.unwrap_or(&Pagination::default()).clone();
486        builder.execute_paginated::<Self>(db, &pagination).await
487    }
488
489    /// List records with filter, sorting, and pagination
490    async fn list_where(
491        filter: FilterOperator,
492        sort: Option<Vec<Sort>>,
493        pagination: Option<&Pagination>,
494        db: &Database,
495    ) -> Result<PaginatedResult<Self>> {
496        let mut builder = QueryBuilder::new(Self::table_name()).r#where(filter);
497
498        if let Some(sorts) = sort {
499            builder = builder.order_by_multiple(sorts);
500        }
501
502        let pagination = pagination.unwrap_or(&Pagination::default()).clone();
503        builder.execute_paginated::<Self>(db, &pagination).await
504    }
505
506    /// Execute a custom query
507    async fn query(builder: QueryBuilder, db: &Database) -> Result<Vec<Self>> {
508        builder.execute::<Self>(db).await
509    }
510
511    /// Execute a custom query with pagination
512    async fn query_paginated(
513        builder: QueryBuilder,
514        pagination: &Pagination,
515        db: &Database,
516    ) -> Result<PaginatedResult<Self>> {
517        builder.execute_paginated::<Self>(db, pagination).await
518    }
519
520    /// Get aggregate value
521    async fn aggregate(
522        function: Aggregate,
523        column: &str,
524        filter: Option<FilterOperator>,
525        db: &Database,
526    ) -> Result<Option<f64>> {
527        let mut builder =
528            QueryBuilder::new(Self::table_name()).aggregate(function, column, None::<String>);
529
530        if let Some(filter) = filter {
531            builder = builder.r#where(filter);
532        }
533
534        let (sql, params) = builder.build()?;
535        let mut rows = db.query(&sql, params).await?;
536
537        if let Some(row) = rows.next().await? {
538            let value = row
539                .get_value(0)
540                .ok()
541                .and_then(|v| match v {
542                    crate::compat::LibsqlValue::Integer(i) => Some(i as f64),
543                    crate::compat::LibsqlValue::Real(f) => Some(f),
544                    _ => None,
545                })
546                .ok_or_else(|| Error::Query("Failed to get aggregate value".to_string()))?;
547            Ok(Some(value))
548        } else {
549            Ok(None)
550        }
551    }
552
553    /// Convert a database row to a HashMap
554    fn row_to_map(row: &crate::compat::LibsqlRow) -> Result<HashMap<String, crate::Value>> {
555        let mut map = HashMap::new();
556        for i in 0..row.column_count() {
557            if let Some(column_name) = row.column_name(i) {
558                let value = row.get_value(i).unwrap_or(crate::compat::null_value());
559                map.insert(column_name.to_string(), Self::libsql_value_to_value(&value));
560            }
561        }
562        Ok(map)
563    }
564
565    /// Convert our Value type to crate::compat::LibsqlValue
566    fn value_to_libsql_value(value: &crate::Value) -> crate::compat::LibsqlValue {
567        match value {
568            crate::Value::Null => crate::compat::null_value(),
569            crate::Value::Integer(i) => crate::compat::LibsqlValue::Integer(*i),
570            crate::Value::Real(f) => crate::compat::LibsqlValue::Real(*f),
571            crate::Value::Text(s) => crate::compat::LibsqlValue::Text(s.clone()),
572            crate::Value::Blob(b) => crate::compat::LibsqlValue::Blob(b.clone()),
573            crate::Value::Boolean(b) => crate::compat::LibsqlValue::Integer(if *b { 1 } else { 0 }),
574        }
575    }
576
577    /// Convert crate::compat::LibsqlValue to our Value type
578    fn libsql_value_to_value(value: &crate::compat::LibsqlValue) -> crate::Value {
579        match value {
580            crate::compat::LibsqlValue::Null => crate::Value::Null,
581            crate::compat::LibsqlValue::Integer(i) => crate::Value::Integer(*i),
582            crate::compat::LibsqlValue::Real(f) => crate::Value::Real(*f),
583            crate::compat::LibsqlValue::Text(s) => crate::Value::Text(s.clone()),
584            crate::compat::LibsqlValue::Blob(b) => crate::Value::Blob(b.clone()),
585        }
586    }
587
588    /// Log an info message
589    fn log_info(message: &str) {
590        #[cfg(target_arch = "wasm32")]
591        {
592            #[cfg(feature = "web-sys")]
593            web_sys::console::log_1(&format!("[INFO] {}: {}", Self::table_name(), message).into());
594        }
595        #[cfg(not(target_arch = "wasm32"))]
596        {
597            log::info!("[{}] {}", Self::table_name(), message);
598        }
599    }
600
601    /// Log a debug message
602    fn log_debug(message: &str) {
603        #[cfg(target_arch = "wasm32")]
604        {
605            #[cfg(feature = "web-sys")]
606            web_sys::console::log_1(&format!("[DEBUG] {}: {}", Self::table_name(), message).into());
607        }
608        #[cfg(not(target_arch = "wasm32"))]
609        {
610            log::debug!("[{}] {}", Self::table_name(), message);
611        }
612    }
613
614    /// Log a warning message
615    fn log_warn(message: &str) {
616        #[cfg(target_arch = "wasm32")]
617        {
618            #[cfg(feature = "web-sys")]
619            web_sys::console::warn_1(&format!("[WARN] {}: {}", Self::table_name(), message).into());
620        }
621        #[cfg(not(target_arch = "wasm32"))]
622        {
623            log::warn!("[{}] {}", Self::table_name(), message);
624        }
625    }
626
627    /// Log an error message
628    fn log_error(message: &str) {
629        #[cfg(target_arch = "wasm32")]
630        {
631            #[cfg(feature = "web-sys")]
632            web_sys::console::error_1(
633                &format!("[ERROR] {}: {}", Self::table_name(), message).into(),
634            );
635        }
636        #[cfg(not(target_arch = "wasm32"))]
637        {
638            log::error!("[{}] {}", Self::table_name(), message);
639        }
640    }
641}