premix_core/
lib.rs

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