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