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
15pub 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
91pub 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#[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#[derive(Debug, PartialEq)]
216pub enum UpdateResult {
217 Success,
218 VersionConflict,
219 NotFound,
220 NotImplemented,
221}
222
223#[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 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 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 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 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 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, _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 pub fn with_deleted(mut self) -> Self {
350 self.include_deleted = true;
351 self
352 }
353
354 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 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 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 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 #[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}