1use crate::dialect::{Dialect, MySqlDialect};
60use crate::Value;
61use std::marker::PhantomData;
62
63pub trait Column<M>: Send + Sync + Clone {
78 fn name(&self) -> &'static str;
80
81 fn table(&self) -> &'static str;
83}
84
85#[derive(Debug, Clone)]
91pub enum WhereClause {
92 Eq(String, Value),
94 Ne(String, Value),
96 Gt(String, Value),
98 Ge(String, Value),
100 Lt(String, Value),
102 Le(String, Value),
104 Like(String, Value),
106 IsNull(String),
108 IsNotNull(String),
110 In(String, Vec<Value>),
112 NotIn(String, Vec<Value>),
114 Between(String, Value, Value),
116 Raw(String),
118}
119
120impl WhereClause {
121 fn render(&self, dialect: &dyn Dialect) -> String {
123 match self {
124 WhereClause::Eq(col, v) => format!(
126 "{} = {}",
127 dialect.quote(col),
128 v.to_param_with_dialect(dialect)
129 ),
130 WhereClause::Ne(col, v) => format!(
131 "{} != {}",
132 dialect.quote(col),
133 v.to_param_with_dialect(dialect)
134 ),
135 WhereClause::Gt(col, v) => format!(
136 "{} > {}",
137 dialect.quote(col),
138 v.to_param_with_dialect(dialect)
139 ),
140 WhereClause::Ge(col, v) => format!(
141 "{} >= {}",
142 dialect.quote(col),
143 v.to_param_with_dialect(dialect)
144 ),
145 WhereClause::Lt(col, v) => format!(
146 "{} < {}",
147 dialect.quote(col),
148 v.to_param_with_dialect(dialect)
149 ),
150 WhereClause::Le(col, v) => format!(
151 "{} <= {}",
152 dialect.quote(col),
153 v.to_param_with_dialect(dialect)
154 ),
155 WhereClause::Like(col, v) => format!(
156 "{} LIKE {}",
157 dialect.quote(col),
158 v.to_param_with_dialect(dialect)
159 ),
160 WhereClause::IsNull(col) => format!("{} IS NULL", dialect.quote(col)),
161 WhereClause::IsNotNull(col) => format!("{} IS NOT NULL", dialect.quote(col)),
162 WhereClause::In(col, vs) => {
163 let values: Vec<String> = vs
164 .iter()
165 .map(|v| v.to_param_with_dialect(dialect).to_string())
166 .collect();
167 format!("{} IN ({})", dialect.quote(col), values.join(", "))
168 }
169 WhereClause::NotIn(col, vs) => {
170 let values: Vec<String> = vs
171 .iter()
172 .map(|v| v.to_param_with_dialect(dialect).to_string())
173 .collect();
174 format!("{} NOT IN ({})", dialect.quote(col), values.join(", "))
175 }
176 WhereClause::Between(col, a, b) => format!(
177 "{} BETWEEN {} AND {}",
178 dialect.quote(col),
179 a.to_param_with_dialect(dialect),
180 b.to_param_with_dialect(dialect)
181 ),
182 WhereClause::Raw(sql) => sql.clone(),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum OrderDirection {
194 Asc,
196 Desc,
198}
199
200#[derive(Debug, Clone)]
202pub struct OrderBy {
203 pub column: String,
205 pub direction: OrderDirection,
207}
208
209pub struct LambdaWrapper<M> {
241 table: String,
243 selects: Vec<String>,
245 wheres: Vec<WhereClause>,
247 orders: Vec<OrderBy>,
249 limit: Option<u64>,
251 offset: Option<u64>,
253 dialect: Box<dyn Dialect>,
255 soft_delete: Option<SoftDeleteConfig>,
260 tenant: Option<TenantConfig>,
264 _marker: PhantomData<M>,
265}
266
267#[derive(Debug, Clone)]
269pub struct SoftDeleteConfig {
270 pub column: String,
272 pub not_deleted_value: Value,
274 pub deleted_value: Value,
276}
277
278#[derive(Debug, Clone)]
280pub struct TenantConfig {
281 pub column: String,
283 pub tenant_id: Value,
285}
286
287impl<M> LambdaWrapper<M> {
288 pub fn new(table: impl Into<String>) -> Self {
290 Self {
291 table: table.into(),
292 selects: Vec::new(),
293 wheres: Vec::new(),
294 orders: Vec::new(),
295 limit: None,
296 offset: None,
297 dialect: Box::new(MySqlDialect),
298 soft_delete: None,
299 tenant: None,
300 _marker: PhantomData,
301 }
302 }
303
304 pub fn with_dialect(table: impl Into<String>, dialect: Box<dyn Dialect>) -> Self {
306 Self {
307 table: table.into(),
308 selects: Vec::new(),
309 wheres: Vec::new(),
310 orders: Vec::new(),
311 limit: None,
312 offset: None,
313 dialect,
314 soft_delete: None,
315 tenant: None,
316 _marker: PhantomData,
317 }
318 }
319
320 pub fn with_soft_delete(mut self, config: SoftDeleteConfig) -> Self {
343 if crate::sql_safety::validate_identifier(&config.column, "soft_delete column").is_ok() {
345 self.soft_delete = Some(config);
346 }
347 self
348 }
349
350 pub fn with_tenant(mut self, config: TenantConfig) -> Self {
370 if crate::sql_safety::validate_identifier(&config.column, "tenant column").is_ok() {
372 self.tenant = Some(config);
373 }
374 self
375 }
376
377 fn collect_implicit_wheres(&self) -> Vec<String> {
380 let mut implicit = Vec::new();
381 if let Some(ref sd) = self.soft_delete {
382 let col = self.dialect.quote(&sd.column);
383 let val = sd
384 .not_deleted_value
385 .to_param_with_dialect(self.dialect.as_ref());
386 implicit.push(format!("{} = {}", col, val));
387 }
388 if let Some(ref t) = self.tenant {
389 let col = self.dialect.quote(&t.column);
390 let val = t.tenant_id.to_param_with_dialect(self.dialect.as_ref());
391 implicit.push(format!("{} = {}", col, val));
392 }
393 implicit
394 }
395
396 fn render_where_clause(&self) -> String {
398 let user_conds: Vec<String> = self
399 .wheres
400 .iter()
401 .map(|w| w.render(self.dialect.as_ref()))
402 .collect();
403 let implicit_conds = self.collect_implicit_wheres();
404 let all_conds: Vec<String> = user_conds.into_iter().chain(implicit_conds).collect();
405 if all_conds.is_empty() {
406 String::new()
407 } else {
408 format!(" WHERE {}", all_conds.join(" AND "))
410 }
411 }
412
413 pub fn select<C: Column<M>>(&mut self, col: C) -> &mut Self {
420 let name = col.name();
421 if crate::sql_safety::validate_identifier(name, "lambda select column").is_ok() {
424 self.selects.push(name.to_string());
425 }
426 self
427 }
428
429 pub fn select_many<C: Column<M>>(&mut self, cols: &[C]) -> &mut Self {
433 for c in cols {
434 let name = c.name();
435 if crate::sql_safety::validate_identifier(name, "lambda select column").is_ok() {
436 self.selects.push(name.to_string());
437 }
438 }
439 self
440 }
441
442 pub fn select_all(&mut self) -> &mut Self {
444 self.selects.clear();
445 self
446 }
447
448 pub fn eq<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
452 self.wheres
453 .push(WhereClause::Eq(col.name().to_string(), value));
454 self
455 }
456
457 pub fn ne<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
459 self.wheres
460 .push(WhereClause::Ne(col.name().to_string(), value));
461 self
462 }
463
464 pub fn gt<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
466 self.wheres
467 .push(WhereClause::Gt(col.name().to_string(), value));
468 self
469 }
470
471 pub fn ge<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
473 self.wheres
474 .push(WhereClause::Ge(col.name().to_string(), value));
475 self
476 }
477
478 pub fn lt<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
480 self.wheres
481 .push(WhereClause::Lt(col.name().to_string(), value));
482 self
483 }
484
485 pub fn le<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
487 self.wheres
488 .push(WhereClause::Le(col.name().to_string(), value));
489 self
490 }
491
492 pub fn like<C: Column<M>>(&mut self, col: C, value: Value) -> &mut Self {
494 self.wheres
495 .push(WhereClause::Like(col.name().to_string(), value));
496 self
497 }
498
499 pub fn is_null<C: Column<M>>(&mut self, col: C) -> &mut Self {
501 self.wheres
502 .push(WhereClause::IsNull(col.name().to_string()));
503 self
504 }
505
506 pub fn is_not_null<C: Column<M>>(&mut self, col: C) -> &mut Self {
508 self.wheres
509 .push(WhereClause::IsNotNull(col.name().to_string()));
510 self
511 }
512
513 pub fn r#in<C: Column<M>>(&mut self, col: C, values: Vec<Value>) -> &mut Self {
515 self.wheres
516 .push(WhereClause::In(col.name().to_string(), values));
517 self
518 }
519
520 pub fn not_in<C: Column<M>>(&mut self, col: C, values: Vec<Value>) -> &mut Self {
522 self.wheres
523 .push(WhereClause::NotIn(col.name().to_string(), values));
524 self
525 }
526
527 pub fn between<C: Column<M>>(&mut self, col: C, a: Value, b: Value) -> &mut Self {
529 self.wheres
530 .push(WhereClause::Between(col.name().to_string(), a, b));
531 self
532 }
533
534 pub fn raw_where(&mut self, sql: impl Into<String>) -> &mut Self {
542 self.wheres.push(WhereClause::Raw(sql.into()));
543 self
544 }
545
546 pub fn order_by_asc<C: Column<M>>(&mut self, col: C) -> &mut Self {
550 self.orders.push(OrderBy {
551 column: col.name().to_string(),
552 direction: OrderDirection::Asc,
553 });
554 self
555 }
556
557 pub fn order_by_desc<C: Column<M>>(&mut self, col: C) -> &mut Self {
559 self.orders.push(OrderBy {
560 column: col.name().to_string(),
561 direction: OrderDirection::Desc,
562 });
563 self
564 }
565
566 pub fn limit(&mut self, n: u64) -> &mut Self {
568 self.limit = Some(n);
569 self
570 }
571
572 pub fn offset(&mut self, n: u64) -> &mut Self {
574 self.offset = Some(n);
575 self
576 }
577
578 pub fn page(&mut self, page: u64, page_size: u64) -> &mut Self {
582 self.limit = Some(page_size);
583 if page > 1 {
584 self.offset = Some((page - 1) * page_size);
585 } else {
586 self.offset = None;
587 }
588 self
589 }
590
591 pub fn build_select(&self) -> String {
597 let quoted_table = self.dialect.quote(&self.table);
598
599 let select_sql = if self.selects.is_empty() {
601 "*".to_string()
602 } else {
603 self.selects
604 .iter()
605 .map(|c| self.dialect.quote(c))
606 .collect::<Vec<_>>()
607 .join(", ")
608 };
609
610 let mut sql = format!("SELECT {} FROM {}", select_sql, quoted_table);
611
612 sql.push_str(&self.render_where_clause());
614
615 if !self.orders.is_empty() {
617 let orders: Vec<String> = self
618 .orders
619 .iter()
620 .map(|o| {
621 let dir = match o.direction {
622 OrderDirection::Asc => "ASC",
623 OrderDirection::Desc => "DESC",
624 };
625 format!("{} {}", self.dialect.quote(&o.column), dir)
626 })
627 .collect();
628 sql.push_str(" ORDER BY ");
629 sql.push_str(&orders.join(", "));
630 }
631
632 if let Some(l) = self.limit {
634 sql.push_str(&format!(" LIMIT {}", l));
635 }
636 if let Some(o) = self.offset {
637 sql.push_str(&format!(" OFFSET {}", o));
638 }
639
640 sql
641 }
642
643 pub fn build_count(&self) -> String {
647 let quoted_table = self.dialect.quote(&self.table);
648 let mut sql = format!("SELECT COUNT(*) FROM {}", quoted_table);
649 sql.push_str(&self.render_where_clause());
650 sql
651 }
652
653 pub fn build_exists(&self) -> String {
657 let inner = self.build_select();
658 let inner = if let Some(pos) = inner.find(" FROM ") {
660 format!("SELECT 1{}", &inner[pos..])
661 } else {
662 inner
663 };
664 format!("SELECT EXISTS({}) AS exists_flag", inner)
665 }
666
667 pub fn build_delete(&self) -> String {
674 let quoted_table = self.dialect.quote(&self.table);
675
676 if let Some(ref sd) = self.soft_delete {
678 let col = self.dialect.quote(&sd.column);
679 let val = sd
680 .deleted_value
681 .to_param_with_dialect(self.dialect.as_ref());
682 let mut sql = format!("UPDATE {} SET {} = {}", quoted_table, col, val);
683
684 let user_conds: Vec<String> = self
686 .wheres
687 .iter()
688 .map(|w| w.render(self.dialect.as_ref()))
689 .collect();
690 let mut all_conds = user_conds;
691 if let Some(ref t) = self.tenant {
693 let tcol = self.dialect.quote(&t.column);
694 let tval = t.tenant_id.to_param_with_dialect(self.dialect.as_ref());
695 all_conds.push(format!("{} = {}", tcol, tval));
696 }
697 let not_deleted_cond = format!(
699 "{} = {}",
700 col,
701 sd.not_deleted_value
702 .to_param_with_dialect(self.dialect.as_ref())
703 );
704 all_conds.push(not_deleted_cond);
705
706 if !all_conds.is_empty() {
707 sql.push_str(" WHERE ");
708 sql.push_str(&all_conds.join(" AND "));
709 }
710 return sql;
711 }
712
713 let mut sql = format!("DELETE FROM {}", quoted_table);
715 sql.push_str(&self.render_where_clause());
716 sql
717 }
718
719 pub fn where_count(&self) -> usize {
723 self.wheres.len()
724 }
725
726 pub fn select_count(&self) -> usize {
728 self.selects.len()
729 }
730
731 pub fn table(&self) -> &str {
733 &self.table
734 }
735
736 pub fn reset(&mut self) -> &mut Self {
738 self.selects.clear();
739 self.wheres.clear();
740 self.orders.clear();
741 self.limit = None;
742 self.offset = None;
743 self
744 }
745}
746
747#[macro_export]
793macro_rules! define_columns {
794 (
795 $columns_struct:ident for $model:ident table = $table:literal {
796 $( $field:ident => $name:literal ),* $(,)?
797 }
798 ) => {
799 #[derive(Debug, Clone, Copy)]
803 pub struct $columns_struct {
804 pub name: &'static str,
806 pub table: &'static str,
808 }
809
810 impl $crate::lambda::Column<$model> for $columns_struct {
811 fn name(&self) -> &'static str {
812 self.name
813 }
814
815 fn table(&self) -> &'static str {
816 self.table
817 }
818 }
819
820 impl $columns_struct {
821 $(
823 #[allow(non_upper_case_globals, dead_code)]
824 pub const $field: $columns_struct = $columns_struct { name: $name, table: $table };
825 )*
826 }
827 };
828}
829
830#[cfg(test)]
835mod tests {
836 use super::*;
837 use crate::dialect::PostgreSqlDialect;
838 use crate::get_dialect;
839 use crate::DbType;
840
841 struct User;
843
844 define_columns! {
846 UserColumns for User table = "users" {
847 Id => "id",
848 Name => "name",
849 Age => "age",
850 Email => "email",
851 }
852 }
853
854 struct Order;
856
857 define_columns! {
858 OrderColumns for Order table = "orders" {
859 OrderId => "order_id",
860 UserId => "user_id",
861 Total => "total",
862 }
863 }
864
865 #[test]
868 fn test_column_name_and_table() {
869 assert_eq!(UserColumns::Id.name(), "id");
870 assert_eq!(UserColumns::Id.table(), "users");
871 assert_eq!(UserColumns::Name.name(), "name");
872 assert_eq!(UserColumns::Age.name(), "age");
873 assert_eq!(UserColumns::Email.name(), "email");
874 }
875
876 #[test]
877 fn test_column_for_different_models() {
878 assert_eq!(OrderColumns::OrderId.name(), "order_id");
879 assert_eq!(OrderColumns::OrderId.table(), "orders");
880 assert_eq!(OrderColumns::UserId.name(), "user_id");
881 }
882
883 #[test]
886 fn test_new_wrapper() {
887 let w = LambdaWrapper::<User>::new("users");
888 assert_eq!(w.table(), "users");
889 assert_eq!(w.where_count(), 0);
890 assert_eq!(w.select_count(), 0);
891 }
892
893 #[test]
894 fn test_select_single() {
895 let mut w = LambdaWrapper::<User>::new("users");
896 w.select(UserColumns::Id);
897 let sql = w.build_select();
898 assert!(sql.contains("SELECT `id` FROM `users`"));
899 }
900
901 #[test]
902 fn test_select_multiple() {
903 let mut w = LambdaWrapper::<User>::new("users");
904 w.select(UserColumns::Id)
905 .select(UserColumns::Name)
906 .select(UserColumns::Age);
907 let sql = w.build_select();
908 assert!(sql.contains("`id`, `name`, `age`"));
909 }
910
911 #[test]
912 fn test_select_many() {
913 let mut w = LambdaWrapper::<User>::new("users");
914 w.select_many(&[UserColumns::Id, UserColumns::Name, UserColumns::Age]);
915 let sql = w.build_select();
916 assert!(sql.contains("`id`, `name`, `age`"));
917 }
918
919 #[test]
920 fn test_select_all_clears_selects() {
921 let mut w = LambdaWrapper::<User>::new("users");
922 w.select(UserColumns::Id);
923 assert_eq!(w.select_count(), 1);
924 w.select_all();
925 assert_eq!(w.select_count(), 0);
926 let sql = w.build_select();
927 assert!(sql.contains("SELECT * FROM"));
928 }
929
930 #[test]
931 fn test_default_select_is_star() {
932 let w = LambdaWrapper::<User>::new("users");
933 let sql = w.build_select();
934 assert!(sql.contains("SELECT * FROM `users`"));
935 }
936
937 #[test]
940 fn test_where_eq() {
941 let mut w = LambdaWrapper::<User>::new("users");
942 w.eq(UserColumns::Id, Value::I64(1));
943 let sql = w.build_select();
944 assert!(sql.contains("WHERE `id` = 1"));
945 }
946
947 #[test]
948 fn test_where_ne() {
949 let mut w = LambdaWrapper::<User>::new("users");
950 w.ne(UserColumns::Id, Value::I64(1));
951 let sql = w.build_select();
952 assert!(sql.contains("`id` != 1"));
953 }
954
955 #[test]
956 fn test_where_gt_ge_lt_le() {
957 let mut w = LambdaWrapper::<User>::new("users");
958 w.gt(UserColumns::Age, Value::I64(18))
959 .ge(UserColumns::Age, Value::I64(20))
960 .lt(UserColumns::Age, Value::I64(65))
961 .le(UserColumns::Age, Value::I64(60));
962 let sql = w.build_select();
963 assert!(sql.contains("`age` > 18"));
964 assert!(sql.contains("`age` >= 20"));
965 assert!(sql.contains("`age` < 65"));
966 assert!(sql.contains("`age` <= 60"));
967 }
968
969 #[test]
970 fn test_where_like() {
971 let mut w = LambdaWrapper::<User>::new("users");
972 w.like(UserColumns::Name, Value::String("%alice%".to_string()));
973 let sql = w.build_select();
974 assert!(sql.contains("`name` LIKE '%alice%'"));
975 }
976
977 #[test]
978 fn test_where_is_null() {
979 let mut w = LambdaWrapper::<User>::new("users");
980 w.is_null(UserColumns::Email);
981 let sql = w.build_select();
982 assert!(sql.contains("`email` IS NULL"));
983 }
984
985 #[test]
986 fn test_where_is_not_null() {
987 let mut w = LambdaWrapper::<User>::new("users");
988 w.is_not_null(UserColumns::Email);
989 let sql = w.build_select();
990 assert!(sql.contains("`email` IS NOT NULL"));
991 }
992
993 #[test]
994 fn test_where_in() {
995 let mut w = LambdaWrapper::<User>::new("users");
996 w.r#in(
997 UserColumns::Id,
998 vec![Value::I64(1), Value::I64(2), Value::I64(3)],
999 );
1000 let sql = w.build_select();
1001 assert!(sql.contains("`id` IN (1, 2, 3)"));
1002 }
1003
1004 #[test]
1005 fn test_where_not_in() {
1006 let mut w = LambdaWrapper::<User>::new("users");
1007 w.not_in(UserColumns::Id, vec![Value::I64(1), Value::I64(2)]);
1008 let sql = w.build_select();
1009 assert!(sql.contains("`id` NOT IN (1, 2)"));
1010 }
1011
1012 #[test]
1013 fn test_where_between() {
1014 let mut w = LambdaWrapper::<User>::new("users");
1015 w.between(UserColumns::Age, Value::I64(18), Value::I64(65));
1016 let sql = w.build_select();
1017 assert!(sql.contains("`age` BETWEEN 18 AND 65"));
1018 }
1019
1020 #[test]
1021 fn test_where_multiple_anded() {
1022 let mut w = LambdaWrapper::<User>::new("users");
1023 w.eq(UserColumns::Id, Value::I64(1))
1024 .gt(UserColumns::Age, Value::I64(18))
1025 .like(UserColumns::Name, Value::String("alice%".to_string()));
1026 let sql = w.build_select();
1027 assert!(sql.contains("`id` = 1"));
1028 assert!(sql.contains("`age` > 18"));
1029 assert!(sql.contains("`name` LIKE 'alice%'"));
1030 assert!(sql.contains(" AND "));
1032 }
1033
1034 #[test]
1035 fn test_where_raw() {
1036 let mut w = LambdaWrapper::<User>::new("users");
1037 w.raw_where("name = 'alice' OR name = 'bob'");
1038 let sql = w.build_select();
1039 assert!(sql.contains("name = 'alice' OR name = 'bob'"));
1040 }
1041
1042 #[test]
1045 fn test_order_by_asc() {
1046 let mut w = LambdaWrapper::<User>::new("users");
1047 w.order_by_asc(UserColumns::Name);
1048 let sql = w.build_select();
1049 assert!(sql.contains("ORDER BY `name` ASC"));
1050 }
1051
1052 #[test]
1053 fn test_order_by_desc() {
1054 let mut w = LambdaWrapper::<User>::new("users");
1055 w.order_by_desc(UserColumns::Id);
1056 let sql = w.build_select();
1057 assert!(sql.contains("ORDER BY `id` DESC"));
1058 }
1059
1060 #[test]
1061 fn test_order_by_multiple() {
1062 let mut w = LambdaWrapper::<User>::new("users");
1063 w.order_by_asc(UserColumns::Name)
1064 .order_by_desc(UserColumns::Id);
1065 let sql = w.build_select();
1066 assert!(sql.contains("ORDER BY `name` ASC, `id` DESC"));
1067 }
1068
1069 #[test]
1070 fn test_limit() {
1071 let mut w = LambdaWrapper::<User>::new("users");
1072 w.limit(10);
1073 let sql = w.build_select();
1074 assert!(sql.contains("LIMIT 10"));
1075 }
1076
1077 #[test]
1078 fn test_offset() {
1079 let mut w = LambdaWrapper::<User>::new("users");
1080 w.limit(10).offset(20);
1081 let sql = w.build_select();
1082 assert!(sql.contains("LIMIT 10"));
1083 assert!(sql.contains("OFFSET 20"));
1084 }
1085
1086 #[test]
1087 fn test_page() {
1088 let mut w = LambdaWrapper::<User>::new("users");
1089 w.page(3, 20); let sql = w.build_select();
1091 assert!(sql.contains("LIMIT 20"));
1092 assert!(sql.contains("OFFSET 40")); }
1094
1095 #[test]
1096 fn test_page_1_no_offset() {
1097 let mut w = LambdaWrapper::<User>::new("users");
1098 w.page(1, 10);
1099 let sql = w.build_select();
1100 assert!(sql.contains("LIMIT 10"));
1101 assert!(!sql.contains("OFFSET")); }
1103
1104 #[test]
1107 fn test_build_count() {
1108 let mut w = LambdaWrapper::<User>::new("users");
1109 w.gt(UserColumns::Age, Value::I64(18));
1110 let sql = w.build_count();
1111 assert!(sql.contains("SELECT COUNT(*) FROM `users`"));
1112 assert!(sql.contains("`age` > 18"));
1113 assert!(!sql.contains("ORDER BY"));
1115 assert!(!sql.contains("LIMIT"));
1116 }
1117
1118 #[test]
1119 fn test_build_exists() {
1120 let mut w = LambdaWrapper::<User>::new("users");
1121 w.eq(UserColumns::Id, Value::I64(1));
1122 let sql = w.build_exists();
1123 assert!(sql.starts_with("SELECT EXISTS("));
1124 assert!(sql.contains("SELECT 1 FROM `users`"));
1125 assert!(sql.contains("`id` = 1"));
1126 assert!(sql.ends_with(") AS exists_flag"));
1127 }
1128
1129 #[test]
1130 fn test_build_delete() {
1131 let mut w = LambdaWrapper::<User>::new("users");
1132 w.eq(UserColumns::Id, Value::I64(1));
1133 let sql = w.build_delete();
1134 assert!(sql.starts_with("DELETE FROM `users`"));
1135 assert!(sql.contains("WHERE `id` = 1"));
1136 }
1137
1138 #[test]
1141 fn test_soft_delete_select_appends_filter() {
1142 let w = LambdaWrapper::<User>::new("users").with_soft_delete(SoftDeleteConfig {
1144 column: "deleted".to_string(),
1145 not_deleted_value: Value::I64(0),
1146 deleted_value: Value::I64(1),
1147 });
1148 let sql = w.build_select();
1149 assert!(
1150 sql.contains("`deleted` = 0"),
1151 "软删除 SELECT 应追加 `deleted` = 0: {}",
1152 sql
1153 );
1154 }
1155
1156 #[test]
1157 fn test_soft_delete_count_appends_filter() {
1158 let w = LambdaWrapper::<User>::new("users").with_soft_delete(SoftDeleteConfig {
1160 column: "deleted".to_string(),
1161 not_deleted_value: Value::I64(0),
1162 deleted_value: Value::I64(1),
1163 });
1164 let sql = w.build_count();
1165 assert!(
1166 sql.contains("`deleted` = 0"),
1167 "软删除 COUNT 应追加 `deleted` = 0: {}",
1168 sql
1169 );
1170 }
1171
1172 #[test]
1173 fn test_soft_delete_exists_appends_filter() {
1174 let w = LambdaWrapper::<User>::new("users").with_soft_delete(SoftDeleteConfig {
1176 column: "deleted".to_string(),
1177 not_deleted_value: Value::I64(0),
1178 deleted_value: Value::I64(1),
1179 });
1180 let sql = w.build_exists();
1181 assert!(
1182 sql.contains("`deleted` = 0"),
1183 "软删除 EXISTS 应追加 `deleted` = 0: {}",
1184 sql
1185 );
1186 }
1187
1188 #[test]
1189 fn test_soft_delete_delete_becomes_update() {
1190 let mut w = LambdaWrapper::<User>::new("users").with_soft_delete(SoftDeleteConfig {
1192 column: "deleted".to_string(),
1193 not_deleted_value: Value::I64(0),
1194 deleted_value: Value::I64(1),
1195 });
1196 w.eq(UserColumns::Id, Value::I64(42));
1197 let sql = w.build_delete();
1198 assert!(
1199 sql.starts_with("UPDATE `users` SET `deleted` = 1"),
1200 "软删除应为 UPDATE:{}",
1201 sql
1202 );
1203 assert!(sql.contains("`id` = 42"), "应保留用户 WHERE 条件:{}", sql);
1204 assert!(
1205 sql.contains("`deleted` = 0"),
1206 "应追加未删除条件防止重复删除:{}",
1207 sql
1208 );
1209 assert!(
1210 !sql.starts_with("DELETE"),
1211 "软删除不应生成 DELETE 语句:{}",
1212 sql
1213 );
1214 }
1215
1216 #[test]
1217 fn test_tenant_select_appends_filter() {
1218 let w = LambdaWrapper::<User>::new("users").with_tenant(TenantConfig {
1220 column: "tenant_id".to_string(),
1221 tenant_id: Value::I64(42),
1222 });
1223 let sql = w.build_select();
1224 assert!(
1225 sql.contains("`tenant_id` = 42"),
1226 "多租户 SELECT 应追加 `tenant_id` = 42: {}",
1227 sql
1228 );
1229 }
1230
1231 #[test]
1232 fn test_tenant_delete_appends_filter() {
1233 let mut w = LambdaWrapper::<User>::new("users").with_tenant(TenantConfig {
1235 column: "tenant_id".to_string(),
1236 tenant_id: Value::I64(42),
1237 });
1238 w.eq(UserColumns::Id, Value::I64(1));
1239 let sql = w.build_delete();
1240 assert!(
1241 sql.contains("`tenant_id` = 42"),
1242 "多租户 DELETE 应追加 `tenant_id` = 42: {}",
1243 sql
1244 );
1245 }
1246
1247 #[test]
1248 fn test_soft_delete_and_tenant_combined() {
1249 let mut w = LambdaWrapper::<User>::new("users")
1251 .with_soft_delete(SoftDeleteConfig {
1252 column: "deleted".to_string(),
1253 not_deleted_value: Value::I64(0),
1254 deleted_value: Value::I64(1),
1255 })
1256 .with_tenant(TenantConfig {
1257 column: "tenant_id".to_string(),
1258 tenant_id: Value::I64(99),
1259 });
1260 w.eq(UserColumns::Id, Value::I64(7));
1261
1262 let sql = w.build_select();
1264 assert!(sql.contains("`deleted` = 0"), "应包含软删除条件: {}", sql);
1265 assert!(
1266 sql.contains("`tenant_id` = 99"),
1267 "应包含多租户条件: {}",
1268 sql
1269 );
1270 assert!(sql.contains("`id` = 7"), "应包含用户条件: {}", sql);
1271
1272 let del_sql = w.build_delete();
1274 assert!(del_sql.starts_with("UPDATE `users` SET `deleted` = 1"));
1275 assert!(del_sql.contains("`tenant_id` = 99"));
1276 assert!(del_sql.contains("`deleted` = 0"));
1277 assert!(del_sql.contains("`id` = 7"));
1278 }
1279
1280 #[test]
1281 fn test_no_soft_delete_no_tenant_backward_compat() {
1282 let mut w = LambdaWrapper::<User>::new("users");
1284 w.eq(UserColumns::Id, Value::I64(1));
1285 let sql = w.build_select();
1286 assert!(!sql.contains("deleted"));
1287 assert!(!sql.contains("tenant_id"));
1288
1289 let del_sql = w.build_delete();
1290 assert!(del_sql.starts_with("DELETE FROM"));
1291 }
1292
1293 #[test]
1296 fn test_with_postgres_dialect() {
1297 let dialect: Box<dyn Dialect> = Box::new(PostgreSqlDialect);
1298 let mut w = LambdaWrapper::<User>::with_dialect("users", dialect);
1299 w.eq(UserColumns::Id, Value::I64(1));
1300 let sql = w.build_select();
1301 assert!(sql.contains("\"users\""));
1302 assert!(sql.contains("\"id\" = 1"));
1303 }
1304
1305 #[test]
1306 fn test_postgres_select() {
1307 let dialect: Box<dyn Dialect> = Box::new(PostgreSqlDialect);
1308 let mut w = LambdaWrapper::<User>::with_dialect("users", dialect);
1309 w.select(UserColumns::Id).select(UserColumns::Name);
1310 let sql = w.build_select();
1311 assert!(sql.contains("\"id\", \"name\""));
1312 }
1313
1314 #[test]
1317 fn test_complex_query() {
1318 let mut w = LambdaWrapper::<User>::new("users");
1319 w.select(UserColumns::Id)
1320 .select(UserColumns::Name)
1321 .select(UserColumns::Age)
1322 .gt(UserColumns::Age, Value::I64(18))
1323 .like(UserColumns::Name, Value::String("a%".to_string()))
1324 .is_not_null(UserColumns::Email)
1325 .order_by_desc(UserColumns::Id)
1326 .limit(10)
1327 .offset(20);
1328
1329 let sql = w.build_select();
1330 assert!(sql.contains("SELECT `id`, `name`, `age` FROM `users`"));
1331 assert!(sql.contains("`age` > 18"));
1332 assert!(sql.contains("`name` LIKE 'a%'"));
1333 assert!(sql.contains("`email` IS NOT NULL"));
1334 assert!(sql.contains("ORDER BY `id` DESC"));
1335 assert!(sql.contains("LIMIT 10"));
1336 assert!(sql.contains("OFFSET 20"));
1337 }
1338
1339 #[test]
1340 fn test_reset_clears_all() {
1341 let mut w = LambdaWrapper::<User>::new("users");
1342 w.select(UserColumns::Id)
1343 .eq(UserColumns::Id, Value::I64(1))
1344 .order_by_asc(UserColumns::Name)
1345 .limit(10);
1346
1347 w.reset();
1348 assert_eq!(w.select_count(), 0);
1349 assert_eq!(w.where_count(), 0);
1350 let sql = w.build_select();
1351 assert!(sql.contains("SELECT * FROM `users`"));
1352 assert!(!sql.contains("WHERE"));
1353 assert!(!sql.contains("ORDER BY"));
1354 assert!(!sql.contains("LIMIT"));
1355 }
1356
1357 #[test]
1360 fn test_different_models_dont_share_columns() {
1361 let mut user_w = LambdaWrapper::<User>::new("users");
1362 user_w.eq(UserColumns::Id, Value::I64(1));
1363
1364 let mut order_w = LambdaWrapper::<Order>::new("orders");
1365 order_w.eq(OrderColumns::OrderId, Value::I64(100));
1366
1367 let user_sql = user_w.build_select();
1368 let order_sql = order_w.build_select();
1369
1370 assert!(user_sql.contains("`users`"));
1371 assert!(user_sql.contains("`id` = 1"));
1372 assert!(order_sql.contains("`orders`"));
1373 assert!(order_sql.contains("`order_id` = 100"));
1374 }
1375
1376 #[test]
1379 fn test_type_safety_compile_time_check() {
1380 let mut w = LambdaWrapper::<User>::new("users");
1386 w.eq(UserColumns::Id, Value::I64(1));
1387 assert_eq!(w.where_count(), 1);
1388 }
1389
1390 #[test]
1391 fn test_string_value_escape() {
1392 let mut w = LambdaWrapper::<User>::new("users");
1394 w.eq(UserColumns::Name, Value::String("O'Brien".to_string()));
1395 let sql = w.build_select();
1396 assert!(sql.contains("'O\\'Brien'"));
1398
1399 let pg_dialect: Box<dyn Dialect> = get_dialect(DbType::PostgreSQL).unwrap();
1401 let mut w_pg = LambdaWrapper::<User>::with_dialect("users", pg_dialect);
1402 w_pg.eq(UserColumns::Name, Value::String("O'Brien".to_string()));
1403 let sql_pg = w_pg.build_select();
1404 assert!(sql_pg.contains("'O''Brien'"));
1405 }
1406
1407 #[test]
1410 fn test_with_real_mysql_dialect() {
1411 let dialect: Box<dyn Dialect> = get_dialect(DbType::MySQL).unwrap();
1412 let mut w = LambdaWrapper::<User>::with_dialect("users", dialect);
1413 w.select(UserColumns::Id)
1414 .select(UserColumns::Name)
1415 .eq(UserColumns::Id, Value::I64(42))
1416 .order_by_desc(UserColumns::Id);
1417
1418 let sql = w.build_select();
1419 assert!(sql.contains("SELECT `id`, `name` FROM `users`"));
1420 assert!(sql.contains("`id` = 42"));
1421 assert!(sql.contains("ORDER BY `id` DESC"));
1422 }
1423}