premix_core/
lib.rs

1pub use async_trait;
2pub use sqlx;
3
4pub mod prelude {
5    pub use crate::{Executor, IntoExecutor, Model, Premix, UpdateResult};
6}
7use sqlx::{Database, Executor as SqlxExecutor, IntoArguments};
8
9pub struct Premix;
10pub mod migrator;
11pub use migrator::{Migration, Migrator};
12
13// Chapter 18: Multi-Database Support
14// We define a trait that encapsulates all the requirements for a database to work with Premix.
15pub trait SqlDialect: Database + Sized + Send + Sync
16where
17    Self::Connection: Send,
18{
19    fn placeholder(n: usize) -> String;
20    fn auto_increment_pk() -> &'static str;
21    fn rows_affected(res: &Self::QueryResult) -> u64;
22    fn last_insert_id(res: &Self::QueryResult) -> i64;
23
24    fn current_timestamp_fn() -> &'static str {
25        "CURRENT_TIMESTAMP"
26    }
27    fn int_type() -> &'static str {
28        "INTEGER"
29    }
30    fn text_type() -> &'static str {
31        "TEXT"
32    }
33    fn bool_type() -> &'static str {
34        "BOOLEAN"
35    }
36    fn float_type() -> &'static str {
37        "REAL"
38    }
39}
40
41#[cfg(feature = "sqlite")]
42impl SqlDialect for sqlx::Sqlite {
43    fn placeholder(_n: usize) -> String {
44        "?".to_string()
45    }
46    fn auto_increment_pk() -> &'static str {
47        "INTEGER PRIMARY KEY"
48    }
49    fn rows_affected(res: &sqlx::sqlite::SqliteQueryResult) -> u64 {
50        res.rows_affected()
51    }
52    fn last_insert_id(res: &sqlx::sqlite::SqliteQueryResult) -> i64 {
53        res.last_insert_rowid()
54    }
55}
56
57#[cfg(feature = "postgres")]
58impl SqlDialect for sqlx::Postgres {
59    fn placeholder(n: usize) -> String {
60        format!("${}", n)
61    }
62    fn auto_increment_pk() -> &'static str {
63        "SERIAL PRIMARY KEY"
64    }
65    fn rows_affected(res: &sqlx::postgres::PgQueryResult) -> u64 {
66        res.rows_affected()
67    }
68    fn last_insert_id(_res: &sqlx::postgres::PgQueryResult) -> i64 {
69        0
70    }
71}
72
73#[cfg(feature = "mysql")]
74impl SqlDialect for sqlx::MySql {
75    fn placeholder(_n: usize) -> String {
76        "?".to_string()
77    }
78    fn auto_increment_pk() -> &'static str {
79        "INTEGER AUTO_INCREMENT PRIMARY KEY"
80    }
81    fn rows_affected(res: &sqlx::mysql::MySqlQueryResult) -> u64 {
82        res.rows_affected()
83    }
84    fn last_insert_id(res: &sqlx::mysql::MySqlQueryResult) -> i64 {
85        res.last_insert_id() as i64
86    }
87}
88
89// Chapter 7: Stronger Executor Abstraction
90pub enum Executor<'a, DB: Database> {
91    Pool(&'a sqlx::Pool<DB>),
92    Conn(&'a mut DB::Connection),
93}
94
95unsafe impl<'a, DB: Database> Send for Executor<'a, DB> where DB::Connection: Send {}
96unsafe impl<'a, DB: Database> Sync for Executor<'a, DB> where DB::Connection: Sync {}
97
98impl<'a, DB: Database> From<&'a sqlx::Pool<DB>> for Executor<'a, DB> {
99    fn from(pool: &'a sqlx::Pool<DB>) -> Self {
100        Self::Pool(pool)
101    }
102}
103
104impl<'a, DB: Database> From<&'a mut DB::Connection> for Executor<'a, DB> {
105    fn from(conn: &'a mut DB::Connection) -> Self {
106        Self::Conn(conn)
107    }
108}
109
110pub trait IntoExecutor<'a>: Send + 'a {
111    type DB: SqlDialect;
112    fn into_executor(self) -> Executor<'a, Self::DB>;
113}
114
115impl<'a, DB: SqlDialect> IntoExecutor<'a> for &'a sqlx::Pool<DB> {
116    type DB = DB;
117    fn into_executor(self) -> Executor<'a, DB> {
118        Executor::Pool(self)
119    }
120}
121
122#[cfg(feature = "sqlite")]
123impl<'a> IntoExecutor<'a> for &'a mut sqlx::SqliteConnection {
124    type DB = sqlx::Sqlite;
125    fn into_executor(self) -> Executor<'a, Self::DB> {
126        Executor::Conn(self)
127    }
128}
129
130#[cfg(feature = "postgres")]
131impl<'a> IntoExecutor<'a> for &'a mut sqlx::postgres::PgConnection {
132    type DB = sqlx::Postgres;
133    fn into_executor(self) -> Executor<'a, Self::DB> {
134        Executor::Conn(self)
135    }
136}
137
138impl<'a, DB: SqlDialect> IntoExecutor<'a> for Executor<'a, DB> {
139    type DB = DB;
140    fn into_executor(self) -> Executor<'a, DB> {
141        self
142    }
143}
144
145impl<'a, DB: Database> Executor<'a, DB> {
146    pub async fn execute<'q, A>(
147        &mut self,
148        query: sqlx::query::Query<'q, DB, A>,
149    ) -> Result<DB::QueryResult, sqlx::Error>
150    where
151        A: sqlx::IntoArguments<'q, DB> + 'q,
152        DB: SqlDialect,
153        for<'c> &'c mut DB::Connection: sqlx::Executor<'c, Database = DB>,
154    {
155        match self {
156            Self::Pool(pool) => query.execute(*pool).await,
157            Self::Conn(conn) => query.execute(&mut **conn).await,
158        }
159    }
160
161    pub async fn fetch_all<'q, T, A>(
162        &mut self,
163        query: sqlx::query::QueryAs<'q, DB, T, A>,
164    ) -> Result<Vec<T>, sqlx::Error>
165    where
166        T: for<'r> sqlx::FromRow<'r, DB::Row> + Send + Unpin,
167        A: sqlx::IntoArguments<'q, DB> + 'q,
168        DB: SqlDialect,
169        for<'c> &'c mut DB::Connection: sqlx::Executor<'c, Database = DB>,
170    {
171        match self {
172            Self::Pool(pool) => query.fetch_all(*pool).await,
173            Self::Conn(conn) => query.fetch_all(&mut **conn).await,
174        }
175    }
176
177    pub async fn fetch_optional<'q, T, A>(
178        &mut self,
179        query: sqlx::query::QueryAs<'q, DB, T, A>,
180    ) -> Result<Option<T>, sqlx::Error>
181    where
182        T: for<'r> sqlx::FromRow<'r, DB::Row> + Send + Unpin,
183        A: sqlx::IntoArguments<'q, DB> + 'q,
184        DB: SqlDialect,
185        for<'c> &'c mut DB::Connection: sqlx::Executor<'c, Database = DB>,
186    {
187        match self {
188            Self::Pool(pool) => query.fetch_optional(*pool).await,
189            Self::Conn(conn) => query.fetch_optional(&mut **conn).await,
190        }
191    }
192}
193
194// Chapter 8: Weak Hook Pattern
195#[inline(never)]
196fn default_model_hook_result() -> Result<(), sqlx::Error> {
197    Ok(())
198}
199
200#[async_trait::async_trait]
201pub trait ModelHooks {
202    #[inline(never)]
203    async fn before_save(&mut self) -> Result<(), sqlx::Error> {
204        default_model_hook_result()
205    }
206    #[inline(never)]
207    async fn after_save(&mut self) -> Result<(), sqlx::Error> {
208        default_model_hook_result()
209    }
210}
211
212#[async_trait::async_trait]
213impl<T: Send + Sync> ModelHooks for T {}
214
215// Chapter 9: Optimistic Locking
216#[derive(Debug, PartialEq)]
217pub enum UpdateResult {
218    Success,
219    VersionConflict,
220    NotFound,
221    NotImplemented,
222}
223
224// Chapter 10: Validation
225#[derive(Debug, Clone)]
226pub struct ValidationError {
227    pub field: String,
228    pub message: String,
229}
230
231pub trait ModelValidation {
232    fn validate(&self) -> Result<(), Vec<ValidationError>> {
233        Ok(())
234    }
235}
236
237impl<T> ModelValidation for T {}
238
239#[async_trait::async_trait]
240pub trait Model<DB: Database>: Sized + Send + Sync + Unpin
241where
242    DB: SqlDialect,
243    for<'r> Self: sqlx::FromRow<'r, DB::Row>,
244{
245    fn table_name() -> &'static str;
246    fn create_table_sql() -> String;
247    fn list_columns() -> Vec<String>;
248
249    /// Saves the current instance to the database.
250    async fn save<'a, E>(&mut self, executor: E) -> Result<(), sqlx::Error>
251    where
252        E: IntoExecutor<'a, DB = DB>;
253
254    async fn update<'a, E>(&mut self, executor: E) -> Result<UpdateResult, sqlx::Error>
255    where
256        E: IntoExecutor<'a, DB = DB>;
257
258    // Chapter 16: Soft Delete support
259    async fn delete<'a, E>(&mut self, executor: E) -> Result<(), sqlx::Error>
260    where
261        E: IntoExecutor<'a, DB = DB>;
262    fn has_soft_delete() -> bool;
263
264    /// Finds a record by its Primary Key.
265    async fn find_by_id<'a, E>(executor: E, id: i32) -> Result<Option<Self>, sqlx::Error>
266    where
267        E: IntoExecutor<'a, DB = DB>;
268
269    /// Use raw SQL and map rows into the current model type.
270    fn raw_sql<'q>(
271        sql: &'q str,
272    ) -> sqlx::query::QueryAs<'q, DB, Self, <DB as Database>::Arguments<'q>> {
273        sqlx::query_as::<DB, Self>(sql)
274    }
275
276    #[inline(never)]
277    async fn eager_load<'a, E>(
278        _models: &mut [Self],
279        _relation: &str,
280        _executor: E,
281    ) -> Result<(), sqlx::Error>
282    where
283        E: IntoExecutor<'a, DB = DB>,
284    {
285        default_model_hook_result()
286    }
287    fn find<'a, E>(executor: E) -> QueryBuilder<'a, Self, DB>
288    where
289        E: IntoExecutor<'a, DB = DB>,
290    {
291        QueryBuilder::new(executor.into_executor())
292    }
293
294    // Convenience helpers
295    fn find_in_pool(pool: &sqlx::Pool<DB>) -> QueryBuilder<'_, Self, DB> {
296        QueryBuilder::new(Executor::Pool(pool))
297    }
298
299    fn find_in_tx(conn: &mut DB::Connection) -> QueryBuilder<'_, Self, DB> {
300        QueryBuilder::new(Executor::Conn(conn))
301    }
302}
303
304pub struct QueryBuilder<'a, T, DB: Database> {
305    executor: Executor<'a, DB>,
306    filters: Vec<String>,
307    limit: Option<i32>,
308    offset: Option<i32>,
309    includes: Vec<String>,
310    include_deleted: bool, // Chapter 16
311    _marker: std::marker::PhantomData<T>,
312}
313
314impl<'a, T, DB> QueryBuilder<'a, T, DB>
315where
316    DB: SqlDialect,
317    T: Model<DB>,
318{
319    pub fn new(executor: Executor<'a, DB>) -> Self {
320        Self {
321            executor,
322            filters: Vec::new(),
323            limit: None,
324            offset: None,
325            includes: Vec::new(),
326            include_deleted: false,
327            _marker: std::marker::PhantomData,
328        }
329    }
330
331    pub fn filter(mut self, condition: impl Into<String>) -> Self {
332        self.filters.push(condition.into());
333        self
334    }
335
336    pub fn limit(mut self, limit: i32) -> Self {
337        self.limit = Some(limit);
338        self
339    }
340
341    pub fn offset(mut self, offset: i32) -> Self {
342        self.offset = Some(offset);
343        self
344    }
345
346    pub fn include(mut self, relation: impl Into<String>) -> Self {
347        self.includes.push(relation.into());
348        self
349    }
350
351    // Chapter 16: Soft Delete toggle
352    pub fn with_deleted(mut self) -> Self {
353        self.include_deleted = true;
354        self
355    }
356
357    /// Returns the SELECT SQL that would be executed for this query.
358    pub fn to_sql(&self) -> String {
359        let mut sql = format!(
360            "SELECT * FROM {}{}",
361            T::table_name(),
362            self.build_where_clause()
363        );
364
365        if let Some(limit) = self.limit {
366            sql.push_str(&format!(" LIMIT {}", limit));
367        }
368
369        if let Some(offset) = self.offset {
370            sql.push_str(&format!(" OFFSET {}", offset));
371        }
372
373        sql
374    }
375
376    /// Returns the UPDATE SQL that would be executed for this query.
377    pub fn to_update_sql(&self, values: &serde_json::Value) -> Result<String, sqlx::Error> {
378        let obj = values.as_object().ok_or_else(|| {
379            sqlx::Error::Protocol("Bulk update requires a JSON object".to_string())
380        })?;
381
382        let mut i = 1;
383        let set_clause = obj
384            .keys()
385            .map(|k| {
386                let p = DB::placeholder(i);
387                i += 1;
388                format!("{} = {}", k, p)
389            })
390            .collect::<Vec<_>>()
391            .join(", ");
392
393        Ok(format!(
394            "UPDATE {} SET {}{}",
395            T::table_name(),
396            set_clause,
397            self.build_where_clause()
398        ))
399    }
400
401    /// Returns the DELETE (or soft delete) SQL that would be executed for this query.
402    pub fn to_delete_sql(&self) -> String {
403        if T::has_soft_delete() {
404            format!(
405                "UPDATE {} SET deleted_at = {}{}",
406                T::table_name(),
407                DB::current_timestamp_fn(),
408                self.build_where_clause()
409            )
410        } else {
411            format!(
412                "DELETE FROM {}{}",
413                T::table_name(),
414                self.build_where_clause()
415            )
416        }
417    }
418
419    fn build_where_clause(&self) -> String {
420        let mut filters = self.filters.clone();
421
422        // Chapter 16: Handle Soft Delete filtering
423        if T::has_soft_delete() && !self.include_deleted {
424            filters.push("deleted_at IS NULL".to_string());
425        }
426
427        if filters.is_empty() {
428            "".to_string()
429        } else {
430            format!(" WHERE {}", filters.join(" AND "))
431        }
432    }
433}
434
435impl<'a, T, DB> QueryBuilder<'a, T, DB>
436where
437    DB: SqlDialect,
438    T: Model<DB>,
439    for<'q> <DB as Database>::Arguments<'q>: IntoArguments<'q, DB>,
440    for<'c> &'c mut <DB as Database>::Connection: SqlxExecutor<'c, Database = DB>,
441    for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
442    DB::Connection: Send,
443    T: Send,
444{
445    pub async fn all(mut self) -> Result<Vec<T>, sqlx::Error> {
446        let mut sql = format!(
447            "SELECT * FROM {}{}",
448            T::table_name(),
449            self.build_where_clause()
450        );
451
452        if let Some(limit) = self.limit {
453            sql.push_str(&format!(" LIMIT {}", limit));
454        }
455
456        if let Some(offset) = self.offset {
457            sql.push_str(&format!(" OFFSET {}", offset));
458        }
459
460        let mut results: Vec<T> = match &mut self.executor {
461            Executor::Pool(pool) => sqlx::query_as::<DB, T>(&sql).fetch_all(*pool).await?,
462            Executor::Conn(conn) => sqlx::query_as::<DB, T>(&sql).fetch_all(&mut **conn).await?,
463        };
464
465        for relation in self.includes {
466            match &mut self.executor {
467                Executor::Pool(pool) => {
468                    T::eager_load(&mut results, &relation, Executor::Pool(*pool)).await?;
469                }
470                Executor::Conn(conn) => {
471                    T::eager_load(&mut results, &relation, Executor::Conn(&mut **conn)).await?;
472                }
473            }
474        }
475
476        Ok(results)
477    }
478
479    // Chapter 17: Bulk Operations
480    #[inline(never)]
481    pub async fn update(mut self, values: serde_json::Value) -> Result<u64, sqlx::Error>
482    where
483        String: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
484        i64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
485        f64: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
486        bool: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
487        Option<String>: for<'q> sqlx::Encode<'q, DB> + sqlx::Type<DB>,
488    {
489        if self.filters.is_empty() {
490            return Err(sqlx::Error::Protocol(
491                "Refusing bulk update without filters".to_string(),
492            ));
493        }
494        let obj = values.as_object().ok_or_else(|| {
495            sqlx::Error::Protocol("Bulk update requires a JSON object".to_string())
496        })?;
497
498        let mut i = 1;
499        let set_clause = obj
500            .keys()
501            .map(|k| {
502                let p = DB::placeholder(i);
503                i += 1;
504                format!("{} = {}", k, p)
505            })
506            .collect::<Vec<_>>()
507            .join(", ");
508
509        let sql = format!(
510            "UPDATE {} SET {}{}",
511            T::table_name(),
512            set_clause,
513            self.build_where_clause()
514        );
515
516        let mut query = sqlx::query::<DB>(&sql);
517        for val in obj.values() {
518            match val {
519                serde_json::Value::String(s) => query = query.bind(s.clone()),
520                serde_json::Value::Number(n) => {
521                    if let Some(v) = n.as_i64() {
522                        query = query.bind(v);
523                    } else if let Some(v) = n.as_f64() {
524                        query = query.bind(v);
525                    }
526                }
527                serde_json::Value::Bool(b) => query = query.bind(*b),
528                serde_json::Value::Null => query = query.bind(Option::<String>::None),
529                _ => {
530                    return Err(sqlx::Error::Protocol(
531                        "Unsupported type in bulk update".to_string(),
532                    ));
533                }
534            }
535        }
536
537        match &mut self.executor {
538            Executor::Pool(pool) => {
539                let res = query.execute(*pool).await?;
540                Ok(DB::rows_affected(&res))
541            }
542            Executor::Conn(conn) => {
543                let res = query.execute(&mut **conn).await?;
544                Ok(DB::rows_affected(&res))
545            }
546        }
547    }
548
549    pub async fn delete(mut self) -> Result<u64, sqlx::Error> {
550        if self.filters.is_empty() {
551            return Err(sqlx::Error::Protocol(
552                "Refusing bulk delete without filters".to_string(),
553            ));
554        }
555        let sql = if T::has_soft_delete() {
556            format!(
557                "UPDATE {} SET deleted_at = {}{}",
558                T::table_name(),
559                DB::current_timestamp_fn(),
560                self.build_where_clause()
561            )
562        } else {
563            format!(
564                "DELETE FROM {}{}",
565                T::table_name(),
566                self.build_where_clause()
567            )
568        };
569
570        match &mut self.executor {
571            Executor::Pool(pool) => {
572                let res = sqlx::query::<DB>(&sql).execute(*pool).await?;
573                Ok(DB::rows_affected(&res))
574            }
575            Executor::Conn(conn) => {
576                let res = sqlx::query::<DB>(&sql).execute(&mut **conn).await?;
577                Ok(DB::rows_affected(&res))
578            }
579        }
580    }
581}
582
583impl Premix {
584    pub async fn sync<DB, M>(pool: &sqlx::Pool<DB>) -> Result<(), sqlx::Error>
585    where
586        DB: SqlDialect,
587        M: Model<DB>,
588        for<'q> <DB as Database>::Arguments<'q>: IntoArguments<'q, DB>,
589        for<'c> &'c mut <DB as Database>::Connection: SqlxExecutor<'c, Database = DB>,
590        for<'c> &'c str: sqlx::ColumnIndex<DB::Row>,
591    {
592        let sql = M::create_table_sql();
593        sqlx::query::<DB>(&sql).execute(pool).await?;
594        Ok(())
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use sqlx::{Sqlite, SqlitePool, sqlite::SqliteRow};
601
602    use super::*;
603
604    #[derive(Debug)]
605    struct SoftDeleteModel {
606        id: i32,
607        status: String,
608        deleted_at: Option<String>,
609    }
610
611    #[derive(Debug)]
612    struct HardDeleteModel {
613        id: i32,
614    }
615
616    #[derive(Debug, sqlx::FromRow)]
617    struct DbModel {
618        id: i32,
619        status: String,
620        deleted_at: Option<String>,
621    }
622
623    #[derive(Debug, sqlx::FromRow)]
624    struct DbHardModel {
625        id: i32,
626        status: String,
627    }
628
629    #[derive(Debug, sqlx::FromRow)]
630    struct SyncModel {
631        id: i64,
632        name: String,
633    }
634
635    #[cfg(feature = "postgres")]
636    const PG_TABLE: &str = "pg_core_items";
637
638    #[cfg(feature = "postgres")]
639    #[derive(Debug, sqlx::FromRow)]
640    struct PgModel {
641        id: i32,
642        name: String,
643    }
644
645    #[cfg(feature = "postgres")]
646    #[async_trait::async_trait]
647    impl Model<sqlx::Postgres> for PgModel {
648        fn table_name() -> &'static str {
649            PG_TABLE
650        }
651        fn create_table_sql() -> String {
652            format!(
653                "CREATE TABLE IF NOT EXISTS {} (id SERIAL PRIMARY KEY, name TEXT)",
654                PG_TABLE
655            )
656        }
657        fn list_columns() -> Vec<String> {
658            vec!["id".into(), "name".into()]
659        }
660        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
661        where
662            E: IntoExecutor<'a, DB = sqlx::Postgres>,
663        {
664            Ok(())
665        }
666        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
667        where
668            E: IntoExecutor<'a, DB = sqlx::Postgres>,
669        {
670            Ok(UpdateResult::NotImplemented)
671        }
672        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
673        where
674            E: IntoExecutor<'a, DB = sqlx::Postgres>,
675        {
676            Ok(())
677        }
678        fn has_soft_delete() -> bool {
679            false
680        }
681        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
682        where
683            E: IntoExecutor<'a, DB = sqlx::Postgres>,
684        {
685            Ok(None)
686        }
687    }
688
689    #[cfg(feature = "postgres")]
690    fn pg_url() -> String {
691        std::env::var("DATABASE_URL").unwrap_or_else(|_| {
692            "postgres://postgres:admin123@localhost:5432/premix_bench".to_string()
693        })
694    }
695
696    impl<'r> sqlx::FromRow<'r, SqliteRow> for SoftDeleteModel {
697        fn from_row(_row: &SqliteRow) -> Result<Self, sqlx::Error> {
698            Err(sqlx::Error::RowNotFound)
699        }
700    }
701
702    impl<'r> sqlx::FromRow<'r, SqliteRow> for HardDeleteModel {
703        fn from_row(_row: &SqliteRow) -> Result<Self, sqlx::Error> {
704            Err(sqlx::Error::RowNotFound)
705        }
706    }
707
708    #[async_trait::async_trait]
709    impl Model<Sqlite> for DbModel {
710        fn table_name() -> &'static str {
711            "db_users"
712        }
713        fn create_table_sql() -> String {
714            String::new()
715        }
716        fn list_columns() -> Vec<String> {
717            vec!["id".into(), "status".into(), "deleted_at".into()]
718        }
719        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
720        where
721            E: IntoExecutor<'a, DB = Sqlite>,
722        {
723            Ok(())
724        }
725        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
726        where
727            E: IntoExecutor<'a, DB = Sqlite>,
728        {
729            Ok(UpdateResult::NotImplemented)
730        }
731        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
732        where
733            E: IntoExecutor<'a, DB = Sqlite>,
734        {
735            Ok(())
736        }
737        fn has_soft_delete() -> bool {
738            true
739        }
740        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
741        where
742            E: IntoExecutor<'a, DB = Sqlite>,
743        {
744            Ok(None)
745        }
746    }
747
748    #[async_trait::async_trait]
749    impl Model<Sqlite> for DbHardModel {
750        fn table_name() -> &'static str {
751            "db_hard_users"
752        }
753        fn create_table_sql() -> String {
754            String::new()
755        }
756        fn list_columns() -> Vec<String> {
757            vec!["id".into(), "status".into()]
758        }
759        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
760        where
761            E: IntoExecutor<'a, DB = Sqlite>,
762        {
763            Ok(())
764        }
765        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
766        where
767            E: IntoExecutor<'a, DB = Sqlite>,
768        {
769            Ok(UpdateResult::NotImplemented)
770        }
771        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
772        where
773            E: IntoExecutor<'a, DB = Sqlite>,
774        {
775            Ok(())
776        }
777        fn has_soft_delete() -> bool {
778            false
779        }
780        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
781        where
782            E: IntoExecutor<'a, DB = Sqlite>,
783        {
784            Ok(None)
785        }
786    }
787
788    #[async_trait::async_trait]
789    impl Model<Sqlite> for SyncModel {
790        fn table_name() -> &'static str {
791            "sync_items"
792        }
793        fn create_table_sql() -> String {
794            "CREATE TABLE IF NOT EXISTS sync_items (id INTEGER PRIMARY KEY, name TEXT);".to_string()
795        }
796        fn list_columns() -> Vec<String> {
797            vec!["id".into(), "name".into()]
798        }
799        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
800        where
801            E: IntoExecutor<'a, DB = Sqlite>,
802        {
803            Ok(())
804        }
805        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
806        where
807            E: IntoExecutor<'a, DB = Sqlite>,
808        {
809            Ok(UpdateResult::NotImplemented)
810        }
811        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
812        where
813            E: IntoExecutor<'a, DB = Sqlite>,
814        {
815            Ok(())
816        }
817        fn has_soft_delete() -> bool {
818            false
819        }
820        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
821        where
822            E: IntoExecutor<'a, DB = Sqlite>,
823        {
824            Ok(None)
825        }
826    }
827
828    #[async_trait::async_trait]
829    impl Model<Sqlite> for SoftDeleteModel {
830        fn table_name() -> &'static str {
831            "users"
832        }
833        fn create_table_sql() -> String {
834            String::new()
835        }
836        fn list_columns() -> Vec<String> {
837            Vec::new()
838        }
839        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
840        where
841            E: IntoExecutor<'a, DB = Sqlite>,
842        {
843            Ok(())
844        }
845        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
846        where
847            E: IntoExecutor<'a, DB = Sqlite>,
848        {
849            Ok(UpdateResult::NotImplemented)
850        }
851        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
852        where
853            E: IntoExecutor<'a, DB = Sqlite>,
854        {
855            Ok(())
856        }
857        fn has_soft_delete() -> bool {
858            true
859        }
860        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
861        where
862            E: IntoExecutor<'a, DB = Sqlite>,
863        {
864            Ok(None)
865        }
866    }
867
868    #[async_trait::async_trait]
869    impl Model<Sqlite> for HardDeleteModel {
870        fn table_name() -> &'static str {
871            "hard_users"
872        }
873        fn create_table_sql() -> String {
874            String::new()
875        }
876        fn list_columns() -> Vec<String> {
877            Vec::new()
878        }
879        async fn save<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
880        where
881            E: IntoExecutor<'a, DB = Sqlite>,
882        {
883            Ok(())
884        }
885        async fn update<'a, E>(&mut self, _executor: E) -> Result<UpdateResult, sqlx::Error>
886        where
887            E: IntoExecutor<'a, DB = Sqlite>,
888        {
889            Ok(UpdateResult::NotImplemented)
890        }
891        async fn delete<'a, E>(&mut self, _executor: E) -> Result<(), sqlx::Error>
892        where
893            E: IntoExecutor<'a, DB = Sqlite>,
894        {
895            Ok(())
896        }
897        fn has_soft_delete() -> bool {
898            false
899        }
900        async fn find_by_id<'a, E>(_executor: E, _id: i32) -> Result<Option<Self>, sqlx::Error>
901        where
902            E: IntoExecutor<'a, DB = Sqlite>,
903        {
904            Ok(None)
905        }
906    }
907
908    #[tokio::test]
909    async fn query_builder_to_sql_includes_soft_delete_filter() {
910        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
911        let query = SoftDeleteModel::find_in_pool(&pool)
912            .filter("age > 18")
913            .limit(10)
914            .offset(5);
915        let sql = query.to_sql();
916        assert!(sql.contains("FROM users"));
917        assert!(sql.contains("age > 18"));
918        assert!(sql.contains("deleted_at IS NULL"));
919        assert!(sql.contains("LIMIT 10"));
920        assert!(sql.contains("OFFSET 5"));
921    }
922
923    #[tokio::test]
924    async fn query_builder_to_sql_without_filters_has_no_where_for_hard_delete() {
925        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
926        let query = HardDeleteModel::find_in_pool(&pool);
927        let sql = query.to_sql();
928        assert!(sql.contains("FROM hard_users"));
929        assert!(!sql.contains(" WHERE "));
930    }
931
932    #[tokio::test]
933    async fn query_builder_with_deleted_skips_soft_delete_filter() {
934        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
935        let query = SoftDeleteModel::find_in_pool(&pool)
936            .filter("age > 18")
937            .with_deleted();
938        let sql = query.to_sql();
939        assert!(sql.contains("age > 18"));
940        assert!(!sql.contains("deleted_at IS NULL"));
941    }
942
943    #[tokio::test]
944    async fn query_builder_to_update_sql_includes_fields() {
945        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
946        let query = SoftDeleteModel::find_in_pool(&pool).filter("status = 'inactive'");
947        let sql = query
948            .to_update_sql(&serde_json::json!({ "status": "active", "age": 1 }))
949            .unwrap();
950        assert!(sql.contains("UPDATE users SET"));
951        assert!(sql.contains("status"));
952        assert!(sql.contains("age"));
953        assert!(sql.contains("WHERE"));
954    }
955
956    #[tokio::test]
957    async fn query_builder_to_update_sql_rejects_non_object() {
958        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
959        let query = SoftDeleteModel::find_in_pool(&pool);
960        let err = query.to_update_sql(&serde_json::json!("bad")).unwrap_err();
961        assert!(
962            err.to_string()
963                .contains("Bulk update requires a JSON object")
964        );
965    }
966
967    #[tokio::test]
968    async fn query_builder_to_delete_sql_soft_delete() {
969        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
970        let query = SoftDeleteModel::find_in_pool(&pool).filter("id = 1");
971        let sql = query.to_delete_sql();
972        assert!(sql.starts_with("UPDATE users SET deleted_at"));
973    }
974
975    #[tokio::test]
976    async fn query_builder_to_delete_sql_hard_delete() {
977        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
978        let query = HardDeleteModel::find_in_pool(&pool).filter("id = 1");
979        let sql = query.to_delete_sql();
980        assert!(sql.starts_with("DELETE FROM hard_users"));
981    }
982
983    #[test]
984    fn model_raw_sql_compiles() {
985        let _query = SoftDeleteModel::raw_sql("SELECT * FROM users");
986    }
987
988    #[test]
989    fn sqlite_placeholder_uses_question_mark() {
990        assert_eq!(Sqlite::placeholder(1), "?");
991        assert_eq!(Sqlite::placeholder(5), "?");
992    }
993
994    #[test]
995    fn sqlite_timestamp_fn_is_constant() {
996        assert_eq!(Sqlite::current_timestamp_fn(), "CURRENT_TIMESTAMP");
997    }
998
999    #[test]
1000    fn sqlite_type_helpers_are_static() {
1001        assert_eq!(Sqlite::int_type(), "INTEGER");
1002        assert_eq!(Sqlite::text_type(), "TEXT");
1003        assert_eq!(Sqlite::bool_type(), "BOOLEAN");
1004        assert_eq!(Sqlite::float_type(), "REAL");
1005    }
1006
1007    #[test]
1008    fn sqlite_auto_increment_pk_is_integer() {
1009        assert!(Sqlite::auto_increment_pk().contains("INTEGER"));
1010    }
1011
1012    #[tokio::test]
1013    async fn executor_execute_and_fetch() {
1014        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1015        sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT);")
1016            .execute(&pool)
1017            .await
1018            .unwrap();
1019
1020        let mut executor = Executor::Pool(&pool);
1021        executor
1022            .execute(sqlx::query("INSERT INTO items (name) VALUES ('a');"))
1023            .await
1024            .unwrap();
1025
1026        let mut executor = Executor::Pool(&pool);
1027        let row = executor
1028            .fetch_optional(sqlx::query_as::<Sqlite, (i64, String)>(
1029                "SELECT id, name FROM items WHERE name = 'a'",
1030            ))
1031            .await
1032            .unwrap();
1033        let (id, name) = row.unwrap();
1034        assert_eq!(name, "a");
1035        assert!(id > 0);
1036
1037        let mut executor = Executor::Pool(&pool);
1038        let rows = executor
1039            .fetch_all(sqlx::query_as::<Sqlite, (i64, String)>(
1040                "SELECT id, name FROM items",
1041            ))
1042            .await
1043            .unwrap();
1044        assert_eq!(rows.len(), 1);
1045    }
1046
1047    #[tokio::test]
1048    async fn executor_execute_and_fetch_with_conn() {
1049        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1050        sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT);")
1051            .execute(&pool)
1052            .await
1053            .unwrap();
1054
1055        let mut conn = pool.acquire().await.unwrap();
1056        let mut executor: Executor<'_, Sqlite> = Executor::Conn(&mut *conn);
1057        executor
1058            .execute(sqlx::query("INSERT INTO items (name) VALUES ('b');"))
1059            .await
1060            .unwrap();
1061
1062        let mut executor: Executor<'_, Sqlite> = Executor::Conn(&mut *conn);
1063        let row = executor
1064            .fetch_optional(sqlx::query_as::<Sqlite, (i64, String)>(
1065                "SELECT id, name FROM items WHERE name = 'b'",
1066            ))
1067            .await
1068            .unwrap();
1069        let (id, name) = row.unwrap();
1070        assert_eq!(name, "b");
1071        assert!(id > 0);
1072
1073        let mut executor: Executor<'_, Sqlite> = Executor::Conn(&mut *conn);
1074        let rows = executor
1075            .fetch_all(sqlx::query_as::<Sqlite, (i64, String)>(
1076                "SELECT id, name FROM items",
1077            ))
1078            .await
1079            .unwrap();
1080        assert_eq!(rows.len(), 1);
1081    }
1082
1083    #[tokio::test]
1084    async fn model_find_builds_query() {
1085        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1086        let sql = DbModel::find(&pool).filter("status = 'active'").to_sql();
1087        assert!(sql.contains("status = 'active'"));
1088    }
1089
1090    #[tokio::test]
1091    async fn sqlite_last_insert_id_matches_rowid() {
1092        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1093        sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT);")
1094            .execute(&pool)
1095            .await
1096            .unwrap();
1097
1098        let mut conn = pool.acquire().await.unwrap();
1099        let res = sqlx::query("INSERT INTO items (name) VALUES ('alpha');")
1100            .execute(&mut *conn)
1101            .await
1102            .unwrap();
1103        let last_id = <Sqlite as SqlDialect>::last_insert_id(&res);
1104
1105        let rowid: i64 = sqlx::query_scalar("SELECT last_insert_rowid()")
1106            .fetch_one(&mut *conn)
1107            .await
1108            .unwrap();
1109        assert_eq!(last_id, rowid);
1110    }
1111
1112    #[tokio::test]
1113    async fn query_builder_update_executes() {
1114        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1115        sqlx::query(
1116            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, flag INTEGER, deleted_at TEXT);",
1117        )
1118            .execute(&pool)
1119            .await
1120            .unwrap();
1121        sqlx::query("INSERT INTO db_users (status) VALUES ('inactive');")
1122            .execute(&pool)
1123            .await
1124            .unwrap();
1125
1126        let updated = DbModel::find_in_pool(&pool)
1127            .filter("status = 'inactive'")
1128            .update(serde_json::json!({ "status": "active" }))
1129            .await
1130            .unwrap();
1131        assert_eq!(updated, 1);
1132
1133        let count: i64 =
1134            sqlx::query_scalar("SELECT COUNT(*) FROM db_users WHERE status = 'active'")
1135                .fetch_one(&pool)
1136                .await
1137                .unwrap();
1138        assert_eq!(count, 1);
1139    }
1140
1141    #[tokio::test]
1142    async fn query_builder_update_binds_bool_and_null() {
1143        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1144        sqlx::query(
1145            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, flag INTEGER, deleted_at TEXT);",
1146        )
1147            .execute(&pool)
1148            .await
1149            .unwrap();
1150        sqlx::query("INSERT INTO db_users (status) VALUES ('inactive');")
1151            .execute(&pool)
1152            .await
1153            .unwrap();
1154
1155        let updated = DbModel::find_in_pool(&pool)
1156            .filter("id = 1")
1157            .update(serde_json::json!({ "status": "active", "flag": true, "deleted_at": null }))
1158            .await
1159            .unwrap();
1160        assert_eq!(updated, 1);
1161    }
1162
1163    #[tokio::test]
1164    async fn query_builder_update_binds_float() {
1165        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1166        sqlx::query("CREATE TABLE db_users (id INTEGER PRIMARY KEY, ratio REAL, deleted_at TEXT);")
1167            .execute(&pool)
1168            .await
1169            .unwrap();
1170        sqlx::query("INSERT INTO db_users (ratio) VALUES (0.5);")
1171            .execute(&pool)
1172            .await
1173            .unwrap();
1174
1175        let updated = DbModel::find_in_pool(&pool)
1176            .filter("id = 1")
1177            .update(serde_json::json!({ "ratio": 1.75 }))
1178            .await
1179            .unwrap();
1180        assert_eq!(updated, 1);
1181
1182        let ratio: f64 = sqlx::query_scalar("SELECT ratio FROM db_users WHERE id = 1")
1183            .fetch_one(&pool)
1184            .await
1185            .unwrap();
1186        assert_eq!(ratio, 1.75);
1187    }
1188
1189    #[tokio::test]
1190    async fn query_builder_update_rejects_unsupported_type() {
1191        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1192        sqlx::query(
1193            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1194        )
1195        .execute(&pool)
1196        .await
1197        .unwrap();
1198        sqlx::query("INSERT INTO db_users (status) VALUES ('inactive');")
1199            .execute(&pool)
1200            .await
1201            .unwrap();
1202
1203        let err = DbModel::find_in_pool(&pool)
1204            .filter("id = 1")
1205            .update(serde_json::json!({ "meta": { "a": 1 } }))
1206            .await
1207            .unwrap_err();
1208        assert!(err.to_string().contains("Unsupported type in bulk update"));
1209    }
1210
1211    #[tokio::test]
1212    async fn query_builder_soft_delete_executes() {
1213        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1214        sqlx::query(
1215            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1216        )
1217        .execute(&pool)
1218        .await
1219        .unwrap();
1220        sqlx::query("INSERT INTO db_users (status) VALUES ('active');")
1221            .execute(&pool)
1222            .await
1223            .unwrap();
1224
1225        let deleted = DbModel::find_in_pool(&pool)
1226            .filter("status = 'active'")
1227            .delete()
1228            .await
1229            .unwrap();
1230        assert_eq!(deleted, 1);
1231
1232        let count: i64 =
1233            sqlx::query_scalar("SELECT COUNT(*) FROM db_users WHERE deleted_at IS NOT NULL")
1234                .fetch_one(&pool)
1235                .await
1236                .unwrap();
1237        assert_eq!(count, 1);
1238    }
1239
1240    #[tokio::test]
1241    async fn query_builder_hard_delete_executes() {
1242        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1243        sqlx::query("CREATE TABLE db_hard_users (id INTEGER PRIMARY KEY, status TEXT);")
1244            .execute(&pool)
1245            .await
1246            .unwrap();
1247        sqlx::query("INSERT INTO db_hard_users (status) VALUES ('active');")
1248            .execute(&pool)
1249            .await
1250            .unwrap();
1251
1252        let deleted = DbHardModel::find_in_pool(&pool)
1253            .filter("status = 'active'")
1254            .delete()
1255            .await
1256            .unwrap();
1257        assert_eq!(deleted, 1);
1258
1259        let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM db_hard_users")
1260            .fetch_one(&pool)
1261            .await
1262            .unwrap();
1263        assert_eq!(count, 0);
1264    }
1265
1266    #[tokio::test]
1267    async fn query_builder_all_with_limit_offset() {
1268        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1269        sqlx::query(
1270            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1271        )
1272        .execute(&pool)
1273        .await
1274        .unwrap();
1275        sqlx::query("INSERT INTO db_users (status) VALUES ('a'), ('b'), ('c');")
1276            .execute(&pool)
1277            .await
1278            .unwrap();
1279
1280        let rows = DbModel::find_in_pool(&pool)
1281            .include("posts")
1282            .limit(1)
1283            .offset(1)
1284            .all()
1285            .await
1286            .unwrap();
1287        assert_eq!(rows.len(), 1);
1288    }
1289
1290    #[tokio::test]
1291    async fn query_builder_all_excludes_soft_deleted_by_default() {
1292        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1293        sqlx::query(
1294            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1295        )
1296        .execute(&pool)
1297        .await
1298        .unwrap();
1299        sqlx::query(
1300            "INSERT INTO db_users (status, deleted_at) VALUES ('active', NULL), ('gone', 'x');",
1301        )
1302        .execute(&pool)
1303        .await
1304        .unwrap();
1305
1306        let rows = DbModel::find_in_pool(&pool).all().await.unwrap();
1307        assert_eq!(rows.len(), 1);
1308
1309        let rows = DbModel::find_in_pool(&pool)
1310            .with_deleted()
1311            .all()
1312            .await
1313            .unwrap();
1314        assert_eq!(rows.len(), 2);
1315    }
1316
1317    #[tokio::test]
1318    async fn query_builder_all_in_tx_uses_conn_executor() {
1319        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1320        sqlx::query("CREATE TABLE db_hard_users (id INTEGER PRIMARY KEY, status TEXT);")
1321            .execute(&pool)
1322            .await
1323            .unwrap();
1324        sqlx::query("INSERT INTO db_hard_users (status) VALUES ('active');")
1325            .execute(&pool)
1326            .await
1327            .unwrap();
1328
1329        let mut tx = pool.begin().await.unwrap();
1330        let rows = DbHardModel::find_in_tx(&mut tx).all().await.unwrap();
1331        assert_eq!(rows.len(), 1);
1332        tx.commit().await.unwrap();
1333    }
1334
1335    #[tokio::test]
1336    async fn query_builder_update_in_tx_uses_conn_executor() {
1337        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1338        sqlx::query(
1339            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1340        )
1341        .execute(&pool)
1342        .await
1343        .unwrap();
1344        sqlx::query("INSERT INTO db_users (status) VALUES ('inactive');")
1345            .execute(&pool)
1346            .await
1347            .unwrap();
1348
1349        let mut tx = pool.begin().await.unwrap();
1350        let updated = DbModel::find_in_tx(&mut tx)
1351            .filter("status = 'inactive'")
1352            .update(serde_json::json!({ "status": "active" }))
1353            .await
1354            .unwrap();
1355        assert_eq!(updated, 1);
1356        tx.commit().await.unwrap();
1357    }
1358
1359    #[tokio::test]
1360    async fn query_builder_delete_in_tx_uses_conn_executor() {
1361        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1362        sqlx::query("CREATE TABLE db_hard_users (id INTEGER PRIMARY KEY, status TEXT);")
1363            .execute(&pool)
1364            .await
1365            .unwrap();
1366        sqlx::query("INSERT INTO db_hard_users (status) VALUES ('active');")
1367            .execute(&pool)
1368            .await
1369            .unwrap();
1370
1371        let mut tx = pool.begin().await.unwrap();
1372        let deleted = DbHardModel::find_in_tx(&mut tx)
1373            .filter("status = 'active'")
1374            .delete()
1375            .await
1376            .unwrap();
1377        assert_eq!(deleted, 1);
1378        tx.commit().await.unwrap();
1379    }
1380
1381    #[tokio::test]
1382    async fn query_builder_update_without_filters_is_rejected() {
1383        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1384        let err = DbModel::find_in_pool(&pool)
1385            .update(serde_json::json!({ "status": "active" }))
1386            .await
1387            .unwrap_err();
1388        assert!(
1389            err.to_string()
1390                .contains("Refusing bulk update without filters")
1391        );
1392    }
1393
1394    #[tokio::test]
1395    async fn query_builder_delete_without_filters_is_rejected() {
1396        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1397        let err = DbHardModel::find_in_pool(&pool).delete().await.unwrap_err();
1398        assert!(
1399            err.to_string()
1400                .contains("Refusing bulk delete without filters")
1401        );
1402    }
1403
1404    #[tokio::test]
1405    async fn query_builder_update_rollback_does_not_persist() {
1406        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1407        sqlx::query(
1408            "CREATE TABLE db_users (id INTEGER PRIMARY KEY, status TEXT, deleted_at TEXT);",
1409        )
1410        .execute(&pool)
1411        .await
1412        .unwrap();
1413        sqlx::query("INSERT INTO db_users (status) VALUES ('inactive');")
1414            .execute(&pool)
1415            .await
1416            .unwrap();
1417
1418        let mut tx = pool.begin().await.unwrap();
1419        let updated = DbModel::find_in_tx(&mut tx)
1420            .filter("status = 'inactive'")
1421            .update(serde_json::json!({ "status": "active" }))
1422            .await
1423            .unwrap();
1424        assert_eq!(updated, 1);
1425        tx.rollback().await.unwrap();
1426
1427        let count: i64 =
1428            sqlx::query_scalar("SELECT COUNT(*) FROM db_users WHERE status = 'active'")
1429                .fetch_one(&pool)
1430                .await
1431                .unwrap();
1432        assert_eq!(count, 0);
1433    }
1434
1435    #[tokio::test]
1436    async fn default_model_hooks_are_noops() {
1437        let mut model = SoftDeleteModel {
1438            id: 1,
1439            status: "active".to_string(),
1440            deleted_at: None,
1441        };
1442        model.before_save().await.unwrap();
1443        model.after_save().await.unwrap();
1444    }
1445
1446    #[test]
1447    fn default_model_validation_is_ok() {
1448        let model = SoftDeleteModel {
1449            id: 1,
1450            status: "active".to_string(),
1451            deleted_at: None,
1452        };
1453        assert!(model.validate().is_ok());
1454    }
1455
1456    #[tokio::test]
1457    async fn eager_load_default_is_ok() {
1458        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1459        let mut models = vec![SoftDeleteModel {
1460            id: 1,
1461            status: "active".to_string(),
1462            deleted_at: None,
1463        }];
1464        SoftDeleteModel::eager_load(&mut models, "posts", &pool)
1465            .await
1466            .unwrap();
1467    }
1468
1469    #[tokio::test]
1470    async fn premix_sync_creates_table() {
1471        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1472        Premix::sync::<Sqlite, SyncModel>(&pool).await.unwrap();
1473
1474        let name: Option<String> = sqlx::query_scalar(
1475            "SELECT name FROM sqlite_master WHERE type='table' AND name='sync_items'",
1476        )
1477        .fetch_optional(&pool)
1478        .await
1479        .unwrap();
1480        assert_eq!(name.as_deref(), Some("sync_items"));
1481    }
1482
1483    #[tokio::test]
1484    async fn model_stub_methods_are_noops() {
1485        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1486
1487        let mut db = DbModel {
1488            id: 1,
1489            status: "active".to_string(),
1490            deleted_at: None,
1491        };
1492        db.save(&pool).await.unwrap();
1493        assert_eq!(
1494            db.update(&pool).await.unwrap(),
1495            UpdateResult::NotImplemented
1496        );
1497        db.delete(&pool).await.unwrap();
1498        assert!(DbModel::find_by_id(&pool, 1).await.unwrap().is_none());
1499
1500        let mut hard = DbHardModel {
1501            id: 2,
1502            status: "inactive".to_string(),
1503        };
1504        hard.save(&pool).await.unwrap();
1505        assert_eq!(
1506            hard.update(&pool).await.unwrap(),
1507            UpdateResult::NotImplemented
1508        );
1509        hard.delete(&pool).await.unwrap();
1510        assert!(DbHardModel::find_by_id(&pool, 2).await.unwrap().is_none());
1511
1512        let mut soft = SoftDeleteModel {
1513            id: 3,
1514            status: "active".to_string(),
1515            deleted_at: None,
1516        };
1517        soft.save(&pool).await.unwrap();
1518        assert_eq!(
1519            soft.update(&pool).await.unwrap(),
1520            UpdateResult::NotImplemented
1521        );
1522        soft.delete(&pool).await.unwrap();
1523        assert!(
1524            SoftDeleteModel::find_by_id(&pool, 3)
1525                .await
1526                .unwrap()
1527                .is_none()
1528        );
1529
1530        let mut hard_only = HardDeleteModel { id: 4 };
1531        hard_only.save(&pool).await.unwrap();
1532        assert_eq!(
1533            hard_only.update(&pool).await.unwrap(),
1534            UpdateResult::NotImplemented
1535        );
1536        hard_only.delete(&pool).await.unwrap();
1537        assert!(
1538            HardDeleteModel::find_by_id(&pool, 4)
1539                .await
1540                .unwrap()
1541                .is_none()
1542        );
1543
1544        let mut sync = SyncModel {
1545            id: 5,
1546            name: "sync".to_string(),
1547        };
1548        sync.save(&pool).await.unwrap();
1549        assert_eq!(
1550            sync.update(&pool).await.unwrap(),
1551            UpdateResult::NotImplemented
1552        );
1553        sync.delete(&pool).await.unwrap();
1554        assert!(SyncModel::find_by_id(&pool, 5).await.unwrap().is_none());
1555    }
1556
1557    #[cfg(feature = "postgres")]
1558    #[tokio::test]
1559    async fn postgres_dialect_and_query_builder_work() {
1560        let url = pg_url();
1561        let pool = match sqlx::PgPool::connect(&url).await {
1562            Ok(pool) => pool,
1563            Err(_) => return,
1564        };
1565        sqlx::query(&format!("DROP TABLE IF EXISTS {}", PG_TABLE))
1566            .execute(&pool)
1567            .await
1568            .unwrap();
1569        sqlx::query(&format!(
1570            "CREATE TABLE {} (id SERIAL PRIMARY KEY, name TEXT)",
1571            PG_TABLE
1572        ))
1573        .execute(&pool)
1574        .await
1575        .unwrap();
1576
1577        assert_eq!(sqlx::Postgres::placeholder(1), "$1");
1578        assert_eq!(sqlx::Postgres::auto_increment_pk(), "SERIAL PRIMARY KEY");
1579
1580        let mut conn = pool.acquire().await.unwrap();
1581        let mut executor = (&mut *conn).into_executor();
1582        let res = executor
1583            .execute(sqlx::query(&format!(
1584                "INSERT INTO {} (name) VALUES ('alpha')",
1585                PG_TABLE
1586            )))
1587            .await
1588            .unwrap();
1589        assert_eq!(<sqlx::Postgres as SqlDialect>::rows_affected(&res), 1);
1590        assert_eq!(<sqlx::Postgres as SqlDialect>::last_insert_id(&res), 0);
1591
1592        let updated = PgModel::find_in_pool(&pool)
1593            .filter("name = 'alpha'")
1594            .update(serde_json::json!({ "name": "beta" }))
1595            .await
1596            .unwrap();
1597        assert_eq!(updated, 1);
1598
1599        let names: Vec<String> = sqlx::query_scalar(&format!("SELECT name FROM {}", PG_TABLE))
1600            .fetch_all(&pool)
1601            .await
1602            .unwrap();
1603        assert_eq!(names, vec!["beta".to_string()]);
1604
1605        let sql = PgModel::find_in_pool(&pool)
1606            .filter("id = 1")
1607            .to_update_sql(&serde_json::json!({ "name": "gamma" }))
1608            .unwrap();
1609        assert!(sql.contains("name = $1"));
1610
1611        sqlx::query(&format!("DROP TABLE IF EXISTS {}", PG_TABLE))
1612            .execute(&pool)
1613            .await
1614            .unwrap();
1615    }
1616
1617    #[test]
1618    fn test_models_use_fields() {
1619        let soft = SoftDeleteModel {
1620            id: 1,
1621            status: "active".to_string(),
1622            deleted_at: None,
1623        };
1624        let hard = HardDeleteModel { id: 2 };
1625        let db = DbModel {
1626            id: 3,
1627            status: "ok".to_string(),
1628            deleted_at: Some("x".to_string()),
1629        };
1630        let db_hard = DbHardModel {
1631            id: 4,
1632            status: "ok".to_string(),
1633        };
1634        let sync = SyncModel {
1635            id: 5,
1636            name: "sync".to_string(),
1637        };
1638        assert_eq!(soft.id, 1);
1639        assert_eq!(soft.status, "active");
1640        assert!(soft.deleted_at.is_none());
1641        assert_eq!(hard.id, 2);
1642        assert_eq!(db.id, 3);
1643        assert_eq!(db.status, "ok");
1644        assert_eq!(db.deleted_at.as_deref(), Some("x"));
1645        assert_eq!(db_hard.id, 4);
1646        assert_eq!(db_hard.status, "ok");
1647        assert_eq!(sync.id, 5);
1648        assert_eq!(sync.name, "sync");
1649    }
1650
1651    #[tokio::test]
1652    async fn executor_from_pool_and_conn() {
1653        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1654        let _pool_exec: Executor<'_, Sqlite> = (&pool).into();
1655
1656        let mut conn = pool.acquire().await.unwrap();
1657        let _conn_exec: Executor<'_, Sqlite> = (&mut *conn).into();
1658    }
1659
1660    #[tokio::test]
1661    async fn executor_into_executor_identity() {
1662        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1663        let exec = Executor::Pool(&pool);
1664        let _same: Executor<'_, Sqlite> = exec.into_executor();
1665    }
1666
1667    #[tokio::test]
1668    async fn model_hooks_defaults_are_noops() {
1669        struct HookDummy;
1670        let mut dummy = HookDummy;
1671        dummy.before_save().await.unwrap();
1672        dummy.after_save().await.unwrap();
1673    }
1674
1675    #[tokio::test]
1676    async fn model_hooks_default_impls_cover_trait_body() {
1677        struct HookDummy;
1678        let mut dummy = HookDummy;
1679        ModelHooks::before_save(&mut dummy).await.unwrap();
1680        ModelHooks::after_save(&mut dummy).await.unwrap();
1681        default_model_hook_result().unwrap();
1682    }
1683
1684    #[tokio::test]
1685    async fn eager_load_default_impl_covers_trait_body() {
1686        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1687        let mut models = vec![SoftDeleteModel {
1688            id: 1,
1689            status: "active".to_string(),
1690            deleted_at: None,
1691        }];
1692        <SoftDeleteModel as Model<Sqlite>>::eager_load(&mut models, "posts", &pool)
1693            .await
1694            .unwrap();
1695    }
1696
1697    #[tokio::test]
1698    async fn query_builder_include_uses_conn_executor() {
1699        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1700        sqlx::query(&SyncModel::create_table_sql())
1701            .execute(&pool)
1702            .await
1703            .unwrap();
1704
1705        let mut conn = pool.acquire().await.unwrap();
1706        let results = SyncModel::find(&mut *conn)
1707            .include("missing")
1708            .all()
1709            .await
1710            .unwrap();
1711        assert!(results.is_empty());
1712    }
1713
1714    #[tokio::test]
1715    async fn bulk_update_rejects_non_object() {
1716        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1717        let err = SoftDeleteModel::find_in_pool(&pool)
1718            .filter("id = 1")
1719            .update(serde_json::json!("bad"))
1720            .await
1721            .unwrap_err();
1722        assert!(
1723            err.to_string()
1724                .contains("Bulk update requires a JSON object")
1725        );
1726    }
1727
1728    #[tokio::test]
1729    async fn bulk_update_rejects_unsupported_value_type() {
1730        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1731        let err = SoftDeleteModel::find_in_pool(&pool)
1732            .filter("id = 1")
1733            .update(serde_json::json!({ "status": ["bad"] }))
1734            .await
1735            .unwrap_err();
1736        assert!(err.to_string().contains("Unsupported type in bulk update"));
1737    }
1738
1739    #[tokio::test]
1740    async fn bulk_update_binds_integers() {
1741        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
1742        sqlx::query("CREATE TABLE users (id INTEGER PRIMARY KEY, age INTEGER, deleted_at TEXT)")
1743            .execute(&pool)
1744            .await
1745            .unwrap();
1746        sqlx::query("INSERT INTO users (id, age, deleted_at) VALUES (1, 10, NULL)")
1747            .execute(&pool)
1748            .await
1749            .unwrap();
1750
1751        let rows = SoftDeleteModel::find_in_pool(&pool)
1752            .filter("id = 1")
1753            .update(serde_json::json!({ "age": 11 }))
1754            .await
1755            .unwrap();
1756        assert_eq!(rows, 1);
1757    }
1758}