1use crate::db_type::DbType;
26use crate::dialect::Dialect;
27use crate::dialect::LockType;
28use crate::model::Model;
29use crate::typed::TypedColumn;
30use crate::value::Value;
31use std::fmt;
32use std::fmt::Write as _;
33use std::time::Duration;
34use sz_orm_macros::{api_beta, api_stable};
35
36pub struct QueryBuilder<M: Model> {
38 table: Option<String>,
39 select_columns: Vec<String>,
40 select_mode: crate::partial_model::SelectMode,
41 where_conditions: Vec<WhereCondition>,
42 order_by: Vec<OrderClause>,
43 group_by: Vec<String>,
44 having_conditions: Vec<WhereCondition>,
45 limit_value: Option<usize>,
46 offset_value: Option<usize>,
47 joins: Vec<JoinClause>,
48 dialect: Box<dyn Dialect>,
49 soft_delete_disabled: bool,
51 tenant_id_value: Option<i64>,
53 tenant_disabled: bool,
55 keyset_cursor: Option<KeysetCursor>,
61 cache_ttl: Option<Duration>,
66 lock_type: Option<LockType>,
74 insert_or_ignore: bool,
79 #[cfg(feature = "tenant-quota-rls-enhanced")]
84 rls_enhancer: Option<std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>>,
85 #[allow(dead_code)]
86 model: std::marker::PhantomData<M>,
87}
88
89#[derive(Debug, Clone)]
97struct KeysetCursor {
98 field: String,
100 value: Value,
102 direction: KeysetDirection,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108enum KeysetDirection {
109 After,
111 Before,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum AggExpr {
121 CountStar,
123 Sum(String),
125 Avg(String),
127 Min(String),
129 Max(String),
131}
132
133impl AggExpr {
134 pub fn validate(&self) -> Result<(), crate::DbError> {
136 match self {
137 AggExpr::CountStar => Ok(()),
138 AggExpr::Sum(col) | AggExpr::Avg(col) | AggExpr::Min(col) | AggExpr::Max(col) => {
139 crate::sql_safety::validate_identifier(col, "aggregate column")
140 }
141 }
142 }
143
144 fn render(&self, dialect: &dyn crate::dialect::Dialect) -> String {
146 match self {
147 AggExpr::CountStar => "COUNT(*)".to_string(),
148 AggExpr::Sum(col) => format!("SUM({})", dialect.quote(col)),
149 AggExpr::Avg(col) => format!("AVG({})", dialect.quote(col)),
150 AggExpr::Min(col) => format!("MIN({})", dialect.quote(col)),
151 AggExpr::Max(col) => format!("MAX({})", dialect.quote(col)),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum HavingOp {
159 Eq,
161 Ne,
163 Gt,
165 Ge,
167 Lt,
169 Le,
171}
172
173impl HavingOp {
174 pub fn as_sql(&self) -> &'static str {
176 match self {
177 HavingOp::Eq => "=",
178 HavingOp::Ne => "!=",
179 HavingOp::Gt => ">",
180 HavingOp::Ge => ">=",
181 HavingOp::Lt => "<",
182 HavingOp::Le => "<=",
183 }
184 }
185}
186
187#[derive(Debug, Clone)]
188#[allow(dead_code)]
189enum WhereCondition {
190 And(String),
195 Or(String),
197 Having(AggExpr, HavingOp, Value),
199 Eq(String, Value),
201 Ne(String, Value),
203 Gt(String, Value),
205 Ge(String, Value),
207 Lt(String, Value),
209 Le(String, Value),
211 Like(String, Value),
213 OrEq(String, Value),
215 OrNe(String, Value),
217 OrGt(String, Value),
219 OrGe(String, Value),
221 OrLt(String, Value),
223 OrLe(String, Value),
225 OrLike(String, Value),
227 In(String, Vec<Value>),
228 NotIn(String, Vec<Value>),
229 Between(String, Value, Value),
230 NotBetween(String, Value, Value),
231 Null(String),
232 NotNull(String),
233 Exists(String),
234 NotExists(String),
235 TypedExpr(String, Vec<Value>),
237}
238
239#[derive(Debug, Clone)]
240struct OrderClause {
241 field: String,
242 direction: OrderDirection,
243}
244
245#[derive(Debug, Clone)]
246enum OrderDirection {
247 Asc,
248 Desc,
249}
250
251#[derive(Debug, Clone)]
252#[allow(dead_code)]
253enum JoinClause {
254 Inner(String, String, String),
255 Left(String, String, String),
256 Right(String, String, String),
257 Cross(String, String),
258 Relation(
261 crate::relation_trait::JoinKind,
262 String,
263 String,
264 String,
265 String,
266 ),
267}
268
269impl<M: Model> QueryBuilder<M> {
270 pub fn new(dialect: Box<dyn Dialect>) -> Self {
272 Self {
273 table: None,
274 select_columns: vec!["*".to_string()],
275 select_mode: crate::partial_model::SelectMode::All,
276 where_conditions: Vec::new(),
277 order_by: Vec::new(),
278 group_by: Vec::new(),
279 having_conditions: Vec::new(),
280 limit_value: None,
281 offset_value: None,
282 joins: Vec::new(),
283 dialect,
284 soft_delete_disabled: false,
285 tenant_id_value: None,
286 tenant_disabled: false,
287 keyset_cursor: None,
288 cache_ttl: None,
289 lock_type: None,
290 insert_or_ignore: false,
291 #[cfg(feature = "tenant-quota-rls-enhanced")]
292 rls_enhancer: None,
293 model: std::marker::PhantomData,
294 }
295 }
296
297 pub fn table(mut self, table: impl Into<String>) -> Self {
299 let table_name = table.into();
300 #[cfg(feature = "multi-tenant-enhanced")]
302 {
303 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
304 if ctx.isolation_strategy
305 == crate::tenant_context::IsolationStrategy::SchemaIsolation
306 {
307 self.table = Some(crate::tenant_context::SchemaIsolationRouter::rewrite_table(
308 &table_name,
309 ctx.tenant_id,
310 ));
311 return self;
312 }
313 }
314 }
315 self.table = Some(table_name);
316 self
317 }
318
319 pub fn without_soft_delete(mut self) -> Self {
338 self.soft_delete_disabled = true;
339 self
340 }
341
342 pub fn is_soft_delete_disabled(&self) -> bool {
344 self.soft_delete_disabled
345 }
346
347 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
367 self.cache_ttl = Some(ttl);
368 self
369 }
370
371 pub fn get_cache_ttl(&self) -> Option<Duration> {
373 self.cache_ttl
374 }
375
376 pub async fn execute_with_cache<F, Fut>(
404 &self,
405 cache: &crate::l2_cache::L2Cache,
406 table: &str,
407 loader: F,
408 ) -> Result<crate::pool::QueryRows, crate::DbError>
409 where
410 F: FnOnce() -> Fut,
411 Fut: std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>,
412 {
413 let (sql, params) = self.build_select_with_params();
414 match self.get_cache_ttl() {
415 Some(ttl) => {
416 cache
417 .get_or_load_query(table, &sql, ¶ms, ttl, loader)
418 .await
419 }
420 None => loader().await,
421 }
422 }
423
424 pub fn lock_for_update(mut self) -> Result<Self, crate::error::DbError> {
449 if !self.dialect.supports_lock_for_update() {
450 return Err(crate::error::DbError::QueryError(
451 "FOR UPDATE lock is not supported by this dialect".to_string(),
452 ));
453 }
454 self.lock_type = Some(LockType::ForUpdate);
455 Ok(self)
456 }
457
458 pub fn lock_shared(mut self) -> Result<Self, crate::error::DbError> {
483 if !self.dialect.supports_lock_shared() {
484 return Err(crate::error::DbError::QueryError(
485 "Shared lock is not supported by this dialect".to_string(),
486 ));
487 }
488 self.lock_type = Some(LockType::Shared);
489 Ok(self)
490 }
491
492 pub fn insert_or_ignore(mut self) -> Self {
511 self.insert_or_ignore = true;
512 self
513 }
514
515 pub fn get_lock_type(&self) -> Option<LockType> {
517 self.lock_type
518 }
519
520 pub fn is_insert_or_ignore(&self) -> bool {
522 self.insert_or_ignore
523 }
524
525 pub fn clone_for_count(&self) -> Self {
532 Self {
533 table: self.table.clone(),
534 select_columns: vec!["*".to_string()],
535 select_mode: crate::partial_model::SelectMode::All,
536 where_conditions: self.where_conditions.clone(),
537 order_by: Vec::new(),
538 group_by: self.group_by.clone(),
539 having_conditions: self.having_conditions.clone(),
540 limit_value: None,
541 offset_value: None,
542 joins: self.joins.clone(),
543 dialect: self.dialect.clone_box(),
544 soft_delete_disabled: self.soft_delete_disabled,
545 tenant_id_value: self.tenant_id_value,
546 tenant_disabled: self.tenant_disabled,
547 keyset_cursor: None,
548 cache_ttl: None, lock_type: None, insert_or_ignore: false, #[cfg(feature = "tenant-quota-rls-enhanced")]
552 rls_enhancer: self.rls_enhancer.clone(),
553 model: std::marker::PhantomData,
554 }
555 }
556
557 fn soft_delete_field(&self) -> Option<&'static str> {
561 if self.soft_delete_disabled {
562 return None;
563 }
564 M::soft_delete_field()
565 }
566
567 fn build_soft_delete_condition(&self) -> Option<String> {
571 self.soft_delete_field()
572 .map(|field| format!("{} IS NULL", self.dialect.quote(field)))
573 }
574
575 pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
602 self.tenant_id_value = Some(tenant_id);
603 self
604 }
605
606 pub fn without_tenant(mut self) -> Self {
610 self.tenant_disabled = true;
611 self
612 }
613
614 #[cfg(feature = "tenant-quota-rls-enhanced")]
619 pub fn with_rls_policy_enhancer(
620 mut self,
621 enhancer: std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>,
622 ) -> Self {
623 self.rls_enhancer = Some(enhancer);
624 self
625 }
626
627 pub fn is_tenant_disabled(&self) -> bool {
629 self.tenant_disabled
630 }
631
632 fn tenant_field(&self) -> Option<&'static str> {
636 if self.tenant_disabled {
637 return None;
638 }
639 M::tenant_field()
640 }
641
642 fn tenant_id_value(&self) -> Option<i64> {
648 if self.tenant_disabled {
649 return None;
650 }
651 if let Some(tid) = self.tenant_id_value {
652 return Some(tid);
653 }
654 #[cfg(feature = "multi-tenant-enhanced")]
655 {
656 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
657 return Some(ctx.tenant_id);
658 }
659 }
660 None
661 }
662
663 fn build_tenant_condition(&self) -> Option<(String, Value)> {
668 let field = self.tenant_field()?;
669 let tid = self.tenant_id_value()?;
670 Some((
671 format!("{} = ?", self.dialect.quote(field)),
672 Value::I64(tid),
673 ))
674 }
675
676 #[cfg(feature = "multi-tenant-enhanced")]
682 #[allow(dead_code)]
683 fn require_tenant_condition(&self) -> Result<Option<(String, Value)>, crate::DbError> {
684 if self.tenant_field().is_none() {
685 return Ok(None);
686 }
687 if self.tenant_id_value.is_some() {
688 return Ok(self.build_tenant_condition());
689 }
690 if crate::tenant_context::TenantContext::current().is_some() {
691 return Ok(self.build_tenant_condition());
692 }
693 Err(crate::DbError::TenantError(
694 "TenantContextRequired: multi-tenant-enhanced feature enabled but no tenant context set"
695 .to_string(),
696 ))
697 }
698
699 pub fn select(mut self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
722 let mut quoted = Vec::with_capacity(columns.len());
723 for col in columns {
724 crate::sql_safety::validate_identifier(col, "select column")?;
725 quoted.push(self.dialect.quote(col));
726 }
727 self.select_columns = quoted;
728 Ok(self)
729 }
730
731 pub fn select_expr(mut self, columns: Vec<&str>) -> Self {
737 self.select_columns = columns.into_iter().map(|s| s.to_string()).collect();
738 self
739 }
740
741 pub fn select_quoted(self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
745 self.select(columns)
746 }
747 #[api_stable]
748
749 pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
763 self.where_conditions
764 .push(WhereCondition::Eq(field.into(), value));
765 self
766 }
767
768 #[cfg(feature = "type-safe-columns")]
773 pub fn where_eq_col<T: crate::column::Schema>(
774 mut self,
775 col: crate::column::Column<T>,
776 value: Value,
777 ) -> Self {
778 self.where_conditions
779 .push(WhereCondition::Eq(col.name().to_string(), value));
780 self
781 }
782
783 #[cfg(feature = "type-safe-columns")]
788 pub fn where_expr<E: crate::typed_ast::TypedExpression<SqlType = crate::typed_ast::Bool>>(
789 mut self,
790 expr: E,
791 ) -> Self {
792 let (sql, params) = expr.to_sql(&*self.dialect);
793 let values: Vec<Value> = params.into_iter().map(Value::String).collect();
794 self.where_conditions
795 .push(WhereCondition::TypedExpr(sql, values));
796 self
797 }
798 #[api_stable]
799
800 pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
802 self.where_conditions
803 .push(WhereCondition::Ne(field.into(), value));
804 self
805 }
806 #[api_stable]
807
808 pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
810 self.where_conditions
811 .push(WhereCondition::Gt(field.into(), value));
812 self
813 }
814 #[api_stable]
815
816 pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
818 self.where_conditions
819 .push(WhereCondition::Ge(field.into(), value));
820 self
821 }
822 #[api_stable]
823
824 pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
826 self.where_conditions
827 .push(WhereCondition::Lt(field.into(), value));
828 self
829 }
830 #[api_stable]
831
832 pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
834 self.where_conditions
835 .push(WhereCondition::Le(field.into(), value));
836 self
837 }
838 #[api_stable]
839
840 pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
852 self.where_conditions
853 .push(WhereCondition::Like(field.into(), pattern));
854 self
855 }
856 #[api_stable]
857
858 pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
862 self.where_conditions
863 .push(WhereCondition::OrEq(field.into(), value));
864 self
865 }
866 #[api_stable]
867
868 pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
870 self.where_conditions
871 .push(WhereCondition::OrNe(field.into(), value));
872 self
873 }
874 #[api_stable]
875
876 pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
878 self.where_conditions
879 .push(WhereCondition::OrGt(field.into(), value));
880 self
881 }
882 #[api_stable]
883
884 pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
886 self.where_conditions
887 .push(WhereCondition::OrGe(field.into(), value));
888 self
889 }
890 #[api_stable]
891
892 pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
894 self.where_conditions
895 .push(WhereCondition::OrLt(field.into(), value));
896 self
897 }
898 #[api_stable]
899
900 pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
902 self.where_conditions
903 .push(WhereCondition::OrLe(field.into(), value));
904 self
905 }
906 #[api_stable]
907
908 pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
910 self.where_conditions
911 .push(WhereCondition::OrLike(field.into(), pattern));
912 self
913 }
914 #[api_stable]
915
916 pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
918 self.where_conditions
919 .push(WhereCondition::In(field.into(), values));
920 self
921 }
922
923 pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
925 self.where_conditions
926 .push(WhereCondition::NotIn(field.into(), values));
927 self
928 }
929 #[api_stable]
930
931 pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
933 self.where_conditions
934 .push(WhereCondition::Between(field.into(), start, end));
935 self
936 }
937
938 pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
940 self.where_conditions
941 .push(WhereCondition::NotBetween(field.into(), start, end));
942 self
943 }
944 #[api_stable]
945
946 pub fn where_null(mut self, field: impl Into<String>) -> Self {
948 self.where_conditions
949 .push(WhereCondition::Null(field.into()));
950 self
951 }
952
953 pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
955 self.where_conditions
956 .push(WhereCondition::NotNull(field.into()));
957 self
958 }
959
960 pub fn order_by(mut self, field: impl Into<String>) -> Self {
962 self.order_by.push(OrderClause {
963 field: field.into(),
964 direction: OrderDirection::Asc,
965 });
966 self
967 }
968
969 pub fn order_desc(mut self, field: impl Into<String>) -> Self {
971 self.order_by.push(OrderClause {
972 field: field.into(),
973 direction: OrderDirection::Desc,
974 });
975 self
976 }
977
978 pub fn group_by(mut self, field: impl Into<String>) -> Self {
980 self.group_by.push(field.into());
981 self
982 }
983
984 #[api_beta]
990
991 pub fn where_eq_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1004 self.where_conditions
1005 .push(WhereCondition::Eq(C::NAME.to_string(), value));
1006 self
1007 }
1008
1009 pub fn where_ne_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1011 self.where_conditions
1012 .push(WhereCondition::Ne(C::NAME.to_string(), value));
1013 self
1014 }
1015
1016 pub fn where_gt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1018 self.where_conditions
1019 .push(WhereCondition::Gt(C::NAME.to_string(), value));
1020 self
1021 }
1022
1023 pub fn where_ge_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1025 self.where_conditions
1026 .push(WhereCondition::Ge(C::NAME.to_string(), value));
1027 self
1028 }
1029
1030 pub fn where_lt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1032 self.where_conditions
1033 .push(WhereCondition::Lt(C::NAME.to_string(), value));
1034 self
1035 }
1036
1037 pub fn where_le_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1039 self.where_conditions
1040 .push(WhereCondition::Le(C::NAME.to_string(), value));
1041 self
1042 }
1043
1044 pub fn where_null_typed<C: TypedColumn>(mut self) -> Self {
1046 self.where_conditions
1047 .push(WhereCondition::Null(C::NAME.to_string()));
1048 self
1049 }
1050
1051 pub fn where_not_null_typed<C: TypedColumn>(mut self) -> Self {
1053 self.where_conditions
1054 .push(WhereCondition::NotNull(C::NAME.to_string()));
1055 self
1056 }
1057
1058 pub fn order_by_typed<C: TypedColumn>(mut self) -> Self {
1060 self.order_by.push(OrderClause {
1061 field: C::NAME.to_string(),
1062 direction: OrderDirection::Asc,
1063 });
1064 self
1065 }
1066
1067 pub fn order_desc_typed<C: TypedColumn>(mut self) -> Self {
1069 self.order_by.push(OrderClause {
1070 field: C::NAME.to_string(),
1071 direction: OrderDirection::Desc,
1072 });
1073 self
1074 }
1075
1076 pub fn group_by_typed<C: TypedColumn>(mut self) -> Self {
1078 self.group_by.push(C::NAME.to_string());
1079 self
1080 }
1081
1082 pub fn select_typed<C: TypedColumn>(mut self) -> Self {
1086 self.select_columns.push(C::NAME.to_string());
1087 self
1088 }
1089
1090 pub fn select_typed_cols<C: TypedColumn, const N: usize>(mut self) -> Self {
1092 self.select_columns.push(C::NAME.to_string());
1095 self
1096 }
1097
1098 pub fn having(
1122 mut self,
1123 agg: AggExpr,
1124 op: HavingOp,
1125 value: Value,
1126 ) -> Result<Self, crate::DbError> {
1127 agg.validate()?;
1128 self.having_conditions
1129 .push(WhereCondition::Having(agg, op, value));
1130 Ok(self)
1131 }
1132
1133 pub fn limit(mut self, limit: usize) -> Self {
1135 self.limit_value = Some(limit);
1136 self
1137 }
1138
1139 pub fn offset(mut self, offset: usize) -> Self {
1141 self.offset_value = Some(offset);
1142 self
1143 }
1144
1145 pub fn page(mut self, page: usize, page_size: usize) -> Self {
1147 self.limit_value = Some(page_size);
1148 self.offset_value = Some((page.saturating_sub(1)) * page_size);
1149 self
1150 }
1151
1152 pub fn keyset_after(
1198 mut self,
1199 field: impl Into<String>,
1200 cursor_value: Value,
1201 page_size: usize,
1202 ) -> Self {
1203 let field_str = field.into();
1204 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1206 existing.direction = OrderDirection::Asc;
1207 } else {
1208 self.order_by.push(OrderClause {
1209 field: field_str.clone(),
1210 direction: OrderDirection::Asc,
1211 });
1212 }
1213 self.limit_value = Some(page_size);
1214 self.offset_value = None;
1216 self.keyset_cursor = Some(KeysetCursor {
1217 field: field_str,
1218 value: cursor_value,
1219 direction: KeysetDirection::After,
1220 });
1221 self
1222 }
1223
1224 pub fn keyset_before(
1270 mut self,
1271 field: impl Into<String>,
1272 cursor_value: Value,
1273 page_size: usize,
1274 ) -> Self {
1275 let field_str = field.into();
1276 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1278 existing.direction = OrderDirection::Desc;
1279 } else {
1280 self.order_by.push(OrderClause {
1281 field: field_str.clone(),
1282 direction: OrderDirection::Desc,
1283 });
1284 }
1285 self.limit_value = Some(page_size);
1286 self.offset_value = None;
1287 self.keyset_cursor = Some(KeysetCursor {
1288 field: field_str,
1289 value: cursor_value,
1290 direction: KeysetDirection::Before,
1291 });
1292 self
1293 }
1294
1295 pub fn join_inner(
1297 mut self,
1298 table: impl Into<String>,
1299 on_left: impl Into<String>,
1300 on_right: impl Into<String>,
1301 ) -> Self {
1302 self.joins.push(JoinClause::Inner(
1303 table.into(),
1304 on_left.into(),
1305 on_right.into(),
1306 ));
1307 self
1308 }
1309
1310 pub fn join_left(
1312 mut self,
1313 table: impl Into<String>,
1314 on_left: impl Into<String>,
1315 on_right: impl Into<String>,
1316 ) -> Self {
1317 self.joins.push(JoinClause::Left(
1318 table.into(),
1319 on_left.into(),
1320 on_right.into(),
1321 ));
1322 self
1323 }
1324
1325 pub fn join_right(
1327 mut self,
1328 table: impl Into<String>,
1329 on_left: impl Into<String>,
1330 on_right: impl Into<String>,
1331 ) -> Self {
1332 self.joins.push(JoinClause::Right(
1333 table.into(),
1334 on_left.into(),
1335 on_right.into(),
1336 ));
1337 self
1338 }
1339
1340 pub fn join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1360 let def = relation.def();
1361 let join_kind = def.kind.default_join_type();
1362 self.joins.push(JoinClause::Relation(
1363 join_kind,
1364 def.from_entity.to_string(),
1365 def.from_key.to_string(),
1366 def.to_entity.to_string(),
1367 def.to_key.to_string(),
1368 ));
1369 self
1370 }
1371
1372 pub fn left_join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1376 let def = relation.def();
1377 self.joins.push(JoinClause::Relation(
1378 crate::relation_trait::JoinKind::Left,
1379 def.from_entity.to_string(),
1380 def.from_key.to_string(),
1381 def.to_entity.to_string(),
1382 def.to_key.to_string(),
1383 ));
1384 self
1385 }
1386
1387 pub fn select_only(mut self) -> Self {
1403 self.select_mode = crate::partial_model::SelectMode::Partial;
1404 self.select_columns.clear();
1405 self
1406 }
1407
1408 pub fn column(mut self, column: impl Into<String>) -> Self {
1412 self.select_columns.push(column.into());
1413 self
1414 }
1415
1416 pub fn columns(mut self, cols: Vec<impl Into<String>>) -> Self {
1418 self.select_columns.extend(cols.into_iter().map(Into::into));
1419 self
1420 }
1421
1422 pub fn column_as(mut self, expr: crate::partial_model::Expr, alias: impl Into<String>) -> Self {
1438 self.select_columns.push(expr.render_as(&alias.into()));
1439 self
1440 }
1441 #[api_stable]
1442
1443 #[tracing::instrument(skip(self), fields(op = "select"))]
1476 pub fn sql(&self) -> String {
1477 let table = self
1478 .table
1479 .clone()
1480 .unwrap_or_else(|| M::table_name().to_string());
1481
1482 let columns = if self.select_columns.is_empty() {
1483 "*".to_string()
1484 } else {
1485 self.select_columns.join(", ")
1486 };
1487
1488 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1489 "SELECT {} FROM {}",
1490 columns,
1491 self.dialect.quote(&table)
1492 ));
1493
1494 for join in &self.joins {
1495 match join {
1496 JoinClause::Inner(t, l, r) => {
1497 let _ = write!(
1498 sql,
1499 " INNER JOIN {} ON {} = {}",
1500 self.dialect.quote(t),
1501 self.dialect.quote(l),
1502 self.dialect.quote(r)
1503 );
1504 }
1505 JoinClause::Left(t, l, r) => {
1506 let _ = write!(
1507 sql,
1508 " LEFT JOIN {} ON {} = {}",
1509 self.dialect.quote(t),
1510 self.dialect.quote(l),
1511 self.dialect.quote(r)
1512 );
1513 }
1514 JoinClause::Right(t, l, r) => {
1515 let _ = write!(
1516 sql,
1517 " RIGHT JOIN {} ON {} = {}",
1518 self.dialect.quote(t),
1519 self.dialect.quote(l),
1520 self.dialect.quote(r)
1521 );
1522 }
1523 JoinClause::Cross(t, on) => {
1524 let _ = write!(
1525 sql,
1526 " CROSS JOIN {} ON {}",
1527 self.dialect.quote(t),
1528 self.dialect.quote(on)
1529 );
1530 }
1531 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1532 let _ = write!(
1533 sql,
1534 " {} {} ON {}.{} = {}.{}",
1535 kind.as_sql(),
1536 self.dialect.quote(tt),
1537 self.dialect.quote(ft),
1538 self.dialect.quote(fk),
1539 self.dialect.quote(tt),
1540 self.dialect.quote(tk)
1541 );
1542 }
1543 }
1544 }
1545
1546 let where_clause = self.build_where_clause();
1548 if !where_clause.is_empty() {
1549 sql.push_str(&where_clause);
1550 }
1551
1552 if !self.group_by.is_empty() {
1553 let cols: Vec<String> = self
1554 .group_by
1555 .iter()
1556 .map(|c| self.dialect.quote(c))
1557 .collect();
1558 sql.push_str(" GROUP BY ");
1559 sql.push_str(&cols.join(", "));
1560 }
1561
1562 if !self.having_conditions.is_empty() {
1563 sql.push_str(" HAVING ");
1564 for (i, cond) in self.having_conditions.iter().enumerate() {
1565 if i > 0 {
1566 sql.push_str(" AND ");
1567 }
1568 match cond {
1569 WhereCondition::And(c) => sql.push_str(c),
1570 WhereCondition::Having(agg, op, value) => {
1572 sql.push_str(&format!(
1573 "{} {} {}",
1574 agg.render(&*self.dialect),
1575 op.as_sql(),
1576 value.to_param_with_dialect(&*self.dialect)
1577 ));
1578 }
1579 _ => {}
1580 }
1581 }
1582 }
1583
1584 if !self.order_by.is_empty() {
1585 let order_cols: Vec<String> = self
1586 .order_by
1587 .iter()
1588 .map(|o| {
1589 let dir = match o.direction {
1590 OrderDirection::Asc => " ASC",
1591 OrderDirection::Desc => " DESC",
1592 };
1593 format!("{}{}", self.dialect.quote(&o.field), dir)
1594 })
1595 .collect();
1596 sql.push_str(" ORDER BY ");
1597 sql.push_str(&order_cols.join(", "));
1598 }
1599
1600 if let Some(limit) = self.limit_value {
1601 let _ = write!(sql, " LIMIT {}", limit);
1602 }
1603
1604 if let Some(offset) = self.offset_value {
1605 let _ = write!(sql, " OFFSET {}", offset);
1606 }
1607
1608 sql.into_string()
1609 }
1610
1611 fn build_where_clause(&self) -> String {
1617 self.build_where_clause_with_options(true)
1618 }
1619
1620 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1628 let soft_delete_cond = if include_soft_delete {
1630 self.build_soft_delete_condition()
1631 } else {
1632 None
1633 };
1634
1635 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1637 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1639 });
1640
1641 if self.where_conditions.is_empty()
1643 && soft_delete_cond.is_none()
1644 && tenant_cond.is_none()
1645 && self.keyset_cursor.is_none()
1646 {
1647 return String::new();
1648 }
1649
1650 let mut conditions: Vec<String> = self
1652 .where_conditions
1653 .iter()
1654 .map(|cond| match cond {
1655 WhereCondition::And(c) => c.clone(),
1656 WhereCondition::Or(c) => format!("OR {}", c),
1657 WhereCondition::Eq(f, v) => format!(
1659 "{} = {}",
1660 self.dialect.quote(f),
1661 v.to_param_with_dialect(&*self.dialect)
1662 ),
1663 WhereCondition::Ne(f, v) => format!(
1664 "{} != {}",
1665 self.dialect.quote(f),
1666 v.to_param_with_dialect(&*self.dialect)
1667 ),
1668 WhereCondition::Gt(f, v) => format!(
1669 "{} > {}",
1670 self.dialect.quote(f),
1671 v.to_param_with_dialect(&*self.dialect)
1672 ),
1673 WhereCondition::Ge(f, v) => format!(
1674 "{} >= {}",
1675 self.dialect.quote(f),
1676 v.to_param_with_dialect(&*self.dialect)
1677 ),
1678 WhereCondition::Lt(f, v) => format!(
1679 "{} < {}",
1680 self.dialect.quote(f),
1681 v.to_param_with_dialect(&*self.dialect)
1682 ),
1683 WhereCondition::Le(f, v) => format!(
1684 "{} <= {}",
1685 self.dialect.quote(f),
1686 v.to_param_with_dialect(&*self.dialect)
1687 ),
1688 WhereCondition::Like(f, v) => format!(
1689 "{} LIKE {}",
1690 self.dialect.quote(f),
1691 v.to_param_with_dialect(&*self.dialect)
1692 ),
1693 WhereCondition::OrEq(f, v) => format!(
1694 "OR {} = {}",
1695 self.dialect.quote(f),
1696 v.to_param_with_dialect(&*self.dialect)
1697 ),
1698 WhereCondition::OrNe(f, v) => format!(
1699 "OR {} != {}",
1700 self.dialect.quote(f),
1701 v.to_param_with_dialect(&*self.dialect)
1702 ),
1703 WhereCondition::OrGt(f, v) => format!(
1704 "OR {} > {}",
1705 self.dialect.quote(f),
1706 v.to_param_with_dialect(&*self.dialect)
1707 ),
1708 WhereCondition::OrGe(f, v) => format!(
1709 "OR {} >= {}",
1710 self.dialect.quote(f),
1711 v.to_param_with_dialect(&*self.dialect)
1712 ),
1713 WhereCondition::OrLt(f, v) => format!(
1714 "OR {} < {}",
1715 self.dialect.quote(f),
1716 v.to_param_with_dialect(&*self.dialect)
1717 ),
1718 WhereCondition::OrLe(f, v) => format!(
1719 "OR {} <= {}",
1720 self.dialect.quote(f),
1721 v.to_param_with_dialect(&*self.dialect)
1722 ),
1723 WhereCondition::OrLike(f, v) => format!(
1724 "OR {} LIKE {}",
1725 self.dialect.quote(f),
1726 v.to_param_with_dialect(&*self.dialect)
1727 ),
1728 WhereCondition::In(f, vals) => {
1729 let vals_str: Vec<String> = vals
1731 .iter()
1732 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1733 .collect();
1734 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1735 }
1736 WhereCondition::NotIn(f, vals) => {
1737 let vals_str: Vec<String> = vals
1738 .iter()
1739 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1740 .collect();
1741 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1742 }
1743 WhereCondition::Between(f, start, end) => {
1744 format!(
1745 "{} BETWEEN {} AND {}",
1746 self.dialect.quote(f),
1747 start.to_param_with_dialect(&*self.dialect),
1748 end.to_param_with_dialect(&*self.dialect)
1749 )
1750 }
1751 WhereCondition::NotBetween(f, start, end) => {
1752 format!(
1753 "{} NOT BETWEEN {} AND {}",
1754 self.dialect.quote(f),
1755 start.to_param_with_dialect(&*self.dialect),
1756 end.to_param_with_dialect(&*self.dialect)
1757 )
1758 }
1759 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1760 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1761 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1762 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1763 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1764 WhereCondition::Having(..) => String::new(),
1766 })
1767 .collect();
1768
1769 if let Some(sd_cond) = soft_delete_cond {
1771 conditions.push(sd_cond);
1772 }
1773
1774 if let Some(t_cond) = tenant_cond {
1776 conditions.push(t_cond);
1777 }
1778
1779 if let Some(ref cursor) = self.keyset_cursor {
1781 let op = match cursor.direction {
1782 KeysetDirection::After => ">",
1783 KeysetDirection::Before => "<",
1784 };
1785 conditions.push(format!(
1786 "{} {} {}",
1787 self.dialect.quote(&cursor.field),
1788 op,
1789 cursor.value.to_param_with_dialect(&*self.dialect)
1790 ));
1791 }
1792
1793 if conditions.is_empty() {
1794 return String::new();
1795 }
1796
1797 let mut groups: Vec<Vec<String>> = Vec::new();
1800 let mut current_group: Vec<String> = Vec::new();
1801 for cond in conditions.iter() {
1802 if let Some(stripped) = cond.strip_prefix("OR ") {
1803 current_group.push(stripped.to_string());
1805 } else {
1806 if !current_group.is_empty() {
1808 groups.push(std::mem::take(&mut current_group));
1809 }
1810 current_group.push(cond.clone());
1811 }
1812 }
1813 if !current_group.is_empty() {
1814 groups.push(current_group);
1815 }
1816
1817 let group_strs: Vec<String> = groups
1818 .iter()
1819 .map(|g| {
1820 if g.len() == 1 {
1821 g[0].clone()
1822 } else {
1823 format!("({})", g.join(" OR "))
1824 }
1825 })
1826 .collect();
1827
1828 format!(" WHERE {}", group_strs.join(" AND "))
1830 }
1831 #[api_stable]
1832
1833 #[tracing::instrument(skip(self), fields(op = "select"))]
1838 pub fn build_select(&self) -> (String, Vec<Value>) {
1839 self.build_select_with_params()
1840 }
1841 #[api_stable]
1842
1843 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1845 pub fn build_insert(
1846 &self,
1847 data: &std::collections::HashMap<String, Value>,
1848 ) -> (String, Vec<Value>) {
1849 self.build_insert_with_params(data)
1850 }
1851
1852 #[api_stable]
1855 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1856 pub fn sql_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1857 let table = self
1858 .table
1859 .clone()
1860 .unwrap_or_else(|| M::table_name().to_string());
1861
1862 if data.is_empty() {
1863 return String::new();
1864 }
1865
1866 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1867 let values: Vec<String> = data
1869 .values()
1870 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1871 .collect();
1872
1873 crate::sql_buffer::SqlBuffer::from_str(&format!(
1874 "INSERT INTO {} ({}) VALUES ({})",
1875 self.dialect.quote(&table),
1876 columns.join(", "),
1877 values.join(", ")
1878 ))
1879 .into_string()
1880 }
1881 #[api_stable]
1882
1883 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1885 pub fn build_update(
1886 &self,
1887 data: &std::collections::HashMap<String, Value>,
1888 ) -> (String, Vec<Value>) {
1889 self.build_update_with_params(data)
1890 }
1891
1892 #[api_stable]
1895 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1896 pub fn sql_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1897 let table = self
1898 .table
1899 .clone()
1900 .unwrap_or_else(|| M::table_name().to_string());
1901
1902 if data.is_empty() {
1903 return String::new();
1904 }
1905
1906 let set_clauses: Vec<String> = data
1907 .iter()
1908 .map(|(k, v)| {
1909 format!(
1910 "{} = {}",
1911 self.dialect.quote(k),
1912 v.to_param_with_dialect(&*self.dialect)
1913 )
1914 })
1915 .collect();
1916
1917 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1918 "UPDATE {} SET {}",
1919 self.dialect.quote(&table),
1920 set_clauses.join(", ")
1921 ));
1922
1923 sql.push_str(&self.build_where_clause());
1924 sql.into_string()
1925 }
1926 #[api_stable]
1927
1928 #[tracing::instrument(skip(self), fields(op = "delete"))]
1940 pub fn build_delete(&self) -> (String, Vec<Value>) {
1941 self.build_delete_with_params()
1942 }
1943
1944 #[api_stable]
1947 #[tracing::instrument(skip(self), fields(op = "delete"))]
1948 pub fn sql_delete(&self) -> String {
1949 let table = self
1950 .table
1951 .clone()
1952 .unwrap_or_else(|| M::table_name().to_string());
1953
1954 if let Some(field) = self.soft_delete_field() {
1956 let where_clause = self.build_where_clause();
1957 return format!(
1958 "UPDATE {} SET {} = NOW(){}",
1959 self.dialect.quote(&table),
1960 self.dialect.quote(field),
1961 where_clause
1962 );
1963 }
1964
1965 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1966 "DELETE FROM {}",
1967 self.dialect.quote(&table)
1968 ));
1969 sql.push_str(&self.build_where_clause());
1970 sql.into_string()
1971 }
1972
1973 pub fn build_force_delete(&self) -> String {
1983 let table = self
1984 .table
1985 .clone()
1986 .unwrap_or_else(|| M::table_name().to_string());
1987
1988 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1989 sql.push_str(&self.build_where_clause_with_options(false));
1991 sql
1992 }
1993
1994 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
2004 self.build_where_clause_with_params_options(true)
2006 }
2007
2008 fn build_where_clause_with_params_options(
2017 &self,
2018 include_soft_delete: bool,
2019 ) -> (String, Vec<Value>) {
2020 let soft_delete_cond = if include_soft_delete {
2022 self.build_soft_delete_condition()
2023 } else {
2024 None
2025 };
2026
2027 let tenant_cond = self.build_tenant_condition();
2029
2030 #[cfg(feature = "tenant-quota-rls-enhanced")]
2032 let rls_cond: Option<(String, Vec<Value>)> = {
2033 if let Some(ref enhancer) = self.rls_enhancer {
2034 if let Some(ref table) = self.table {
2035 if let Some(tid) = self.tenant_id_value() {
2036 match enhancer.enhance_query(table, &tid.to_string()) {
2037 Ok(Some(c)) => Some((c.sql_fragment, c.params)),
2038 Ok(None) => None,
2039 Err(_) => None,
2040 }
2041 } else {
2042 None
2043 }
2044 } else {
2045 None
2046 }
2047 } else {
2048 None
2049 }
2050 };
2051 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
2052 let rls_cond: Option<(String, Vec<Value>)> = None;
2053
2054 if self.where_conditions.is_empty()
2056 && soft_delete_cond.is_none()
2057 && tenant_cond.is_none()
2058 && self.keyset_cursor.is_none()
2059 && rls_cond.is_none()
2060 {
2061 return (String::new(), Vec::new());
2062 }
2063
2064 let mut params = Vec::new();
2065
2066 let mut conditions: Vec<String> = self
2067 .where_conditions
2068 .iter()
2069 .map(|cond| match cond {
2070 WhereCondition::And(c) => c.clone(),
2071 WhereCondition::Or(c) => format!("OR {}", c),
2072 WhereCondition::Eq(f, v) => {
2074 params.push(v.clone());
2075 format!("{} = ?", self.dialect.quote(f))
2076 }
2077 WhereCondition::Ne(f, v) => {
2078 params.push(v.clone());
2079 format!("{} != ?", self.dialect.quote(f))
2080 }
2081 WhereCondition::Gt(f, v) => {
2082 params.push(v.clone());
2083 format!("{} > ?", self.dialect.quote(f))
2084 }
2085 WhereCondition::Ge(f, v) => {
2086 params.push(v.clone());
2087 format!("{} >= ?", self.dialect.quote(f))
2088 }
2089 WhereCondition::Lt(f, v) => {
2090 params.push(v.clone());
2091 format!("{} < ?", self.dialect.quote(f))
2092 }
2093 WhereCondition::Le(f, v) => {
2094 params.push(v.clone());
2095 format!("{} <= ?", self.dialect.quote(f))
2096 }
2097 WhereCondition::Like(f, v) => {
2098 params.push(v.clone());
2099 format!("{} LIKE ?", self.dialect.quote(f))
2100 }
2101 WhereCondition::OrEq(f, v) => {
2102 params.push(v.clone());
2103 format!("OR {} = ?", self.dialect.quote(f))
2104 }
2105 WhereCondition::OrNe(f, v) => {
2106 params.push(v.clone());
2107 format!("OR {} != ?", self.dialect.quote(f))
2108 }
2109 WhereCondition::OrGt(f, v) => {
2110 params.push(v.clone());
2111 format!("OR {} > ?", self.dialect.quote(f))
2112 }
2113 WhereCondition::OrGe(f, v) => {
2114 params.push(v.clone());
2115 format!("OR {} >= ?", self.dialect.quote(f))
2116 }
2117 WhereCondition::OrLt(f, v) => {
2118 params.push(v.clone());
2119 format!("OR {} < ?", self.dialect.quote(f))
2120 }
2121 WhereCondition::OrLe(f, v) => {
2122 params.push(v.clone());
2123 format!("OR {} <= ?", self.dialect.quote(f))
2124 }
2125 WhereCondition::OrLike(f, v) => {
2126 params.push(v.clone());
2127 format!("OR {} LIKE ?", self.dialect.quote(f))
2128 }
2129 WhereCondition::In(f, vals) => {
2130 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2131 params.extend(vals.iter().cloned());
2132 format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
2133 }
2134 WhereCondition::NotIn(f, vals) => {
2135 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2136 params.extend(vals.iter().cloned());
2137 format!(
2138 "{} NOT IN ({})",
2139 self.dialect.quote(f),
2140 placeholders.join(", ")
2141 )
2142 }
2143 WhereCondition::Between(f, start, end) => {
2144 params.push(start.clone());
2145 params.push(end.clone());
2146 format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
2147 }
2148 WhereCondition::NotBetween(f, start, end) => {
2149 params.push(start.clone());
2150 params.push(end.clone());
2151 format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
2152 }
2153 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
2154 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
2155 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
2156 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
2157 WhereCondition::TypedExpr(sql, expr_params) => {
2158 params.extend(expr_params.iter().cloned());
2159 sql.clone()
2160 }
2161 WhereCondition::Having(..) => String::new(),
2163 })
2164 .collect();
2165
2166 if let Some(sd_cond) = soft_delete_cond {
2168 conditions.push(sd_cond);
2169 }
2170
2171 if let Some((t_sql, t_value)) = tenant_cond {
2173 conditions.push(t_sql);
2174 params.push(t_value);
2175 }
2176
2177 if let Some((rls_sql, rls_params)) = rls_cond {
2179 conditions.push(rls_sql);
2180 params.extend(rls_params);
2181 }
2182
2183 if let Some(ref cursor) = self.keyset_cursor {
2185 let op = match cursor.direction {
2186 KeysetDirection::After => ">",
2187 KeysetDirection::Before => "<",
2188 };
2189 conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
2190 params.push(cursor.value.clone());
2191 }
2192
2193 if conditions.is_empty() {
2194 return (String::new(), params);
2195 }
2196
2197 let mut groups: Vec<Vec<String>> = Vec::new();
2199 let mut current_group: Vec<String> = Vec::new();
2200 for cond in conditions.iter() {
2201 if let Some(stripped) = cond.strip_prefix("OR ") {
2202 current_group.push(stripped.to_string());
2203 } else {
2204 if !current_group.is_empty() {
2205 groups.push(std::mem::take(&mut current_group));
2206 }
2207 current_group.push(cond.clone());
2208 }
2209 }
2210 if !current_group.is_empty() {
2211 groups.push(current_group);
2212 }
2213
2214 let group_strs: Vec<String> = groups
2215 .iter()
2216 .map(|g| {
2217 if g.len() == 1 {
2218 g[0].clone()
2219 } else {
2220 format!("({})", g.join(" OR "))
2221 }
2222 })
2223 .collect();
2224
2225 (format!(" WHERE {}", group_strs.join(" AND ")), params)
2227 }
2228
2229 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
2234 let table = self
2235 .table
2236 .clone()
2237 .unwrap_or_else(|| M::table_name().to_string());
2238 let columns = if self.select_columns.is_empty() {
2239 "*".to_string()
2240 } else {
2241 self.select_columns.join(", ")
2242 };
2243
2244 let quoted_table = self.dialect.quote(&table);
2245 let capacity = 16 + quoted_table.len() + columns.len() + 64;
2246 let mut sql = String::with_capacity(capacity);
2247 let _ = write!(sql, "SELECT {} FROM {}", columns, quoted_table);
2248
2249 for join in &self.joins {
2250 match join {
2251 JoinClause::Inner(t, l, r) => {
2252 let _ = write!(
2253 sql,
2254 " INNER JOIN {} ON {} = {}",
2255 self.dialect.quote(t),
2256 self.dialect.quote(l),
2257 self.dialect.quote(r)
2258 );
2259 }
2260 JoinClause::Left(t, l, r) => {
2261 let _ = write!(
2262 sql,
2263 " LEFT JOIN {} ON {} = {}",
2264 self.dialect.quote(t),
2265 self.dialect.quote(l),
2266 self.dialect.quote(r)
2267 );
2268 }
2269 JoinClause::Right(t, l, r) => {
2270 let _ = write!(
2271 sql,
2272 " RIGHT JOIN {} ON {} = {}",
2273 self.dialect.quote(t),
2274 self.dialect.quote(l),
2275 self.dialect.quote(r)
2276 );
2277 }
2278 JoinClause::Cross(t, on) => {
2279 let _ = write!(
2280 sql,
2281 " CROSS JOIN {} ON {}",
2282 self.dialect.quote(t),
2283 self.dialect.quote(on)
2284 );
2285 }
2286 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2287 let _ = write!(
2288 sql,
2289 " {} {} ON {}.{} = {}.{}",
2290 kind.as_sql(),
2291 self.dialect.quote(tt),
2292 self.dialect.quote(ft),
2293 self.dialect.quote(fk),
2294 self.dialect.quote(tt),
2295 self.dialect.quote(tk)
2296 );
2297 }
2298 }
2299 }
2300
2301 let mut params = Vec::new();
2302 let (where_clause, where_params) = self.build_where_clause_with_params();
2304 if !where_clause.is_empty() {
2305 sql.push_str(&where_clause);
2306 params = where_params;
2307 }
2308
2309 if !self.group_by.is_empty() {
2310 let cols: Vec<String> = self
2311 .group_by
2312 .iter()
2313 .map(|c| self.dialect.quote(c))
2314 .collect();
2315 sql.push_str(" GROUP BY ");
2316 sql.push_str(&cols.join(", "));
2317 }
2318
2319 if !self.having_conditions.is_empty() {
2320 sql.push_str(" HAVING ");
2321 for (i, cond) in self.having_conditions.iter().enumerate() {
2322 if i > 0 {
2323 sql.push_str(" AND ");
2324 }
2325 match cond {
2326 WhereCondition::And(c) => sql.push_str(c),
2327 WhereCondition::Having(agg, op, value) => {
2329 params.push(value.clone());
2330 sql.push_str(&format!("{} {} ?", agg.render(&*self.dialect), op.as_sql()));
2331 }
2332 _ => {}
2333 }
2334 }
2335 }
2336
2337 if !self.order_by.is_empty() {
2338 let order_cols: Vec<String> = self
2339 .order_by
2340 .iter()
2341 .map(|o| {
2342 let dir = match o.direction {
2343 OrderDirection::Asc => " ASC",
2344 OrderDirection::Desc => " DESC",
2345 };
2346 format!("{}{}", self.dialect.quote(&o.field), dir)
2347 })
2348 .collect();
2349 sql.push_str(" ORDER BY ");
2350 sql.push_str(&order_cols.join(", "));
2351 }
2352
2353 if let Some(limit) = self.limit_value {
2354 let _ = write!(sql, " LIMIT {}", limit);
2355 }
2356 if let Some(offset) = self.offset_value {
2357 let _ = write!(sql, " OFFSET {}", offset);
2358 }
2359
2360 if let Some(lock_type) = &self.lock_type {
2362 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2363 sql.push(' ');
2364 sql.push_str(&lock_clause);
2365 }
2366 }
2367
2368 (sql, params)
2369 }
2370
2371 pub fn build_insert_with_params(
2373 &self,
2374 data: &std::collections::HashMap<String, Value>,
2375 ) -> (String, Vec<Value>) {
2376 let table = self
2377 .table
2378 .clone()
2379 .unwrap_or_else(|| M::table_name().to_string());
2380 if data.is_empty() {
2381 return (String::new(), Vec::new());
2382 }
2383
2384 let mut columns = Vec::with_capacity(data.len());
2385 let mut params = Vec::with_capacity(data.len());
2386 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2387 for (k, v) in data.iter() {
2388 columns.push(self.dialect.quote(k));
2389 params.push(v.clone());
2390 }
2391
2392 let insert_clause = if self.insert_or_ignore {
2394 self.dialect.build_insert_or_ignore_prefix(&table)
2395 } else {
2396 format!("INSERT INTO {}", self.dialect.quote(&table))
2397 };
2398
2399 let sql = format!(
2400 "{} ({}) VALUES ({})",
2401 insert_clause,
2402 columns.join(", "),
2403 placeholders.join(", ")
2404 );
2405 (sql, params)
2406 }
2407
2408 pub fn build_batch_insert_with_params(
2415 &self,
2416 rows: &[std::collections::HashMap<String, Value>],
2417 ) -> (String, Vec<Value>) {
2418 let table = self
2419 .table
2420 .clone()
2421 .unwrap_or_else(|| M::table_name().to_string());
2422 if rows.is_empty() {
2423 return (String::new(), Vec::new());
2424 }
2425
2426 let first_row = &rows[0];
2428 let columns: Vec<String> = first_row.keys().cloned().collect();
2429 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2430
2431 let mut params = Vec::with_capacity(rows.len() * columns.len());
2432 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2433 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2434 let mut param_idx = 1usize;
2435 for row in rows {
2436 let placeholders: Vec<String> = columns
2437 .iter()
2438 .map(|col| match row.get(col) {
2439 Some(v) => {
2440 params.push(v.clone());
2441 if is_pg {
2442 let p = format!("${}", param_idx);
2443 param_idx += 1;
2444 p
2445 } else {
2446 "?".to_string()
2447 }
2448 }
2449 None => "NULL".to_string(),
2450 })
2451 .collect();
2452 value_groups.push(format!("({})", placeholders.join(", ")));
2453 }
2454
2455 let sql = format!(
2456 "INSERT INTO {} ({}) VALUES {}",
2457 self.dialect.quote(&table),
2458 quoted_columns.join(", "),
2459 value_groups.join(", ")
2460 );
2461 (sql, params)
2462 }
2463
2464 pub fn build_batch_upsert_with_params(
2485 &self,
2486 rows: &[std::collections::HashMap<String, Value>],
2487 conflict_columns: &[&str],
2488 update_columns: &[&str],
2489 ) -> Result<(String, Vec<Value>), crate::DbError> {
2490 if rows.is_empty() {
2491 return Err(crate::DbError::InvalidInput(
2492 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2493 ));
2494 }
2495
2496 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2498 if insert_sql.is_empty() {
2499 return Err(crate::DbError::InvalidInput(
2500 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2501 ));
2502 }
2503
2504 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2506
2507 let conflict_clause = self
2509 .dialect
2510 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2511 .ok_or_else(|| {
2512 crate::DbError::InvalidInput(format!(
2513 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2514 self.dialect.db_type()
2515 ))
2516 })?;
2517
2518 let sql = format!("{} {}", insert_sql, conflict_clause);
2519 Ok((sql, params))
2520 }
2521
2522 pub fn build_update_with_params(
2525 &self,
2526 data: &std::collections::HashMap<String, Value>,
2527 ) -> (String, Vec<Value>) {
2528 let table = self
2529 .table
2530 .clone()
2531 .unwrap_or_else(|| M::table_name().to_string());
2532 if data.is_empty() {
2533 return (String::new(), Vec::new());
2534 }
2535
2536 let mut set_clauses = Vec::with_capacity(data.len());
2537 let mut params = Vec::with_capacity(data.len());
2538 for (k, v) in data.iter() {
2539 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2540 params.push(v.clone());
2541 }
2542
2543 let mut sql = format!(
2544 "UPDATE {} SET {}",
2545 self.dialect.quote(&table),
2546 set_clauses.join(", ")
2547 );
2548
2549 let (where_clause, where_params) = self.build_where_clause_with_params();
2551 if !where_clause.is_empty() {
2552 sql.push_str(&where_clause);
2553 params.extend(where_params);
2554 }
2555
2556 (sql, params)
2557 }
2558
2559 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2565 let table = self
2566 .table
2567 .clone()
2568 .unwrap_or_else(|| M::table_name().to_string());
2569
2570 if let Some(field) = self.soft_delete_field() {
2572 let (where_clause, where_params) = self.build_where_clause_with_params();
2573 let sql = format!(
2574 "UPDATE {} SET {} = NOW(){}",
2575 self.dialect.quote(&table),
2576 self.dialect.quote(field),
2577 where_clause
2578 );
2579 return (sql, where_params);
2580 }
2581
2582 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2583 let mut params = Vec::new();
2584
2585 let (where_clause, where_params) = self.build_where_clause_with_params();
2586 if !where_clause.is_empty() {
2587 sql.push_str(&where_clause);
2588 params = where_params;
2589 }
2590
2591 (sql, params)
2592 }
2593
2594 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2598 let table = self
2599 .table
2600 .clone()
2601 .unwrap_or_else(|| M::table_name().to_string());
2602
2603 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2604 let mut params = Vec::new();
2605
2606 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2608 if !where_clause.is_empty() {
2609 sql.push_str(&where_clause);
2610 params = where_params;
2611 }
2612
2613 (sql, params)
2614 }
2615
2616 pub fn build_count(&self) -> String {
2618 let table = self
2619 .table
2620 .clone()
2621 .unwrap_or_else(|| M::table_name().to_string());
2622
2623 let mut sql = format!(
2624 "SELECT COUNT(*) as total FROM {}",
2625 self.dialect.quote(&table)
2626 );
2627 sql.push_str(&self.build_where_clause());
2628 sql
2629 }
2630
2631 pub fn build_exists(&self) -> String {
2633 let table = self
2634 .table
2635 .clone()
2636 .unwrap_or_else(|| M::table_name().to_string());
2637
2638 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2639 sql.push_str(&self.build_where_clause());
2640 sql.push_str(" LIMIT 1");
2641 format!("SELECT EXISTS({})", sql)
2642 }
2643
2644 pub fn build_max(&self, field: &str) -> String {
2646 let table = self
2647 .table
2648 .clone()
2649 .unwrap_or_else(|| M::table_name().to_string());
2650
2651 let mut sql = format!(
2652 "SELECT MAX({}) as max_val FROM {}",
2653 self.dialect.quote(field),
2654 self.dialect.quote(&table)
2655 );
2656 sql.push_str(&self.build_where_clause());
2657 sql
2658 }
2659
2660 pub fn build_min(&self, field: &str) -> String {
2662 let table = self
2663 .table
2664 .clone()
2665 .unwrap_or_else(|| M::table_name().to_string());
2666
2667 let mut sql = format!(
2668 "SELECT MIN({}) as min_val FROM {}",
2669 self.dialect.quote(field),
2670 self.dialect.quote(&table)
2671 );
2672 sql.push_str(&self.build_where_clause());
2673 sql
2674 }
2675
2676 pub fn build_sum(&self, field: &str) -> String {
2678 let table = self
2679 .table
2680 .clone()
2681 .unwrap_or_else(|| M::table_name().to_string());
2682
2683 let mut sql = format!(
2684 "SELECT SUM({}) as sum_val FROM {}",
2685 self.dialect.quote(field),
2686 self.dialect.quote(&table)
2687 );
2688 sql.push_str(&self.build_where_clause());
2689 sql
2690 }
2691
2692 pub fn build_avg(&self, field: &str) -> String {
2694 let table = self
2695 .table
2696 .clone()
2697 .unwrap_or_else(|| M::table_name().to_string());
2698
2699 let mut sql = format!(
2700 "SELECT AVG({}) as avg_val FROM {}",
2701 self.dialect.quote(field),
2702 self.dialect.quote(&table)
2703 );
2704 sql.push_str(&self.build_where_clause());
2705 sql
2706 }
2707
2708 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2711 let sql = self.sql();
2712 let mut errors = Vec::new();
2713
2714 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2715 errors.push(e);
2716 }
2717
2718 if !self.joins.is_empty() {
2720 for join in &self.joins {
2721 match join {
2722 JoinClause::Inner(_, left, right)
2723 | JoinClause::Left(_, left, right)
2724 | JoinClause::Right(_, left, right) => {
2725 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2726 errors.push(e);
2727 }
2728 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2729 errors.push(e);
2730 }
2731 }
2732 JoinClause::Relation(_, ft, fk, tt, tk) => {
2733 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2734 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2735 errors.push(e);
2736 }
2737 }
2738 }
2739 _ => {}
2740 }
2741 }
2742 }
2743
2744 let table = self
2746 .table
2747 .clone()
2748 .unwrap_or_else(|| M::table_name().to_string());
2749 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2750 errors.push(e);
2751 }
2752
2753 if errors.is_empty() {
2754 Ok(())
2755 } else {
2756 Err(errors)
2757 }
2758 }
2759
2760 pub fn validate_insert(
2763 &self,
2764 data: &std::collections::HashMap<String, Value>,
2765 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2766 let sql = self.sql_insert(data);
2767 let mut errors = Vec::new();
2768
2769 if sql.is_empty() {
2770 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2771 return Err(errors);
2772 }
2773
2774 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2775 errors.push(e);
2776 }
2777
2778 if errors.is_empty() {
2779 Ok(())
2780 } else {
2781 Err(errors)
2782 }
2783 }
2784
2785 pub fn validate_update(
2788 &self,
2789 data: &std::collections::HashMap<String, Value>,
2790 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2791 let sql = self.sql_update(data);
2792 let mut errors = Vec::new();
2793
2794 if sql.is_empty() {
2795 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2796 return Err(errors);
2797 }
2798
2799 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2800 errors.push(e);
2801 }
2802
2803 if errors.is_empty() {
2804 Ok(())
2805 } else {
2806 Err(errors)
2807 }
2808 }
2809
2810 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2812 let sql = self.sql_delete();
2813 let mut errors = Vec::new();
2814
2815 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2816 errors.push(e);
2817 }
2818
2819 if errors.is_empty() {
2820 Ok(())
2821 } else {
2822 Err(errors)
2823 }
2824 }
2825}
2826
2827impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2829 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2847 let all_columns = M::columns();
2848 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2849
2850 for field in fields {
2851 if !all_columns.contains(field) {
2852 return Err(crate::DbError::InvalidInput(format!(
2853 "排除的字段不存在: {}",
2854 field
2855 )));
2856 }
2857 }
2858
2859 let retained: Vec<String> = all_columns
2860 .into_iter()
2861 .filter(|c| !exclude_set.contains(*c))
2862 .map(|s| s.to_string())
2863 .collect();
2864
2865 if retained.is_empty() {
2866 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2867 }
2868
2869 self.select_mode = crate::partial_model::SelectMode::Partial;
2870 self.select_columns = retained;
2871 Ok(self)
2872 }
2873}
2874
2875impl<M: Model> fmt::Debug for QueryBuilder<M> {
2876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2877 f.debug_struct("QueryBuilder")
2878 .field("table", &self.table)
2879 .field("select_columns", &self.select_columns)
2880 .field("where_conditions", &self.where_conditions.len())
2881 .field("limit", &self.limit_value)
2882 .finish()
2883 }
2884}
2885
2886#[cfg(test)]
2887#[allow(deprecated)]
2888mod tests {
2889 use super::*;
2890 use crate::db_type::DbType;
2891 use crate::dialect::get_dialect;
2892
2893 struct TestModel;
2894 impl Model for TestModel {
2895 type PrimaryKey = i64;
2896
2897 fn table_name() -> &'static str {
2898 "test_models"
2899 }
2900
2901 fn pk(&self) -> Self::PrimaryKey {
2902 1
2903 }
2904
2905 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2906 }
2907
2908 #[test]
2909 fn test_query_builder_select() -> Result<(), crate::DbError> {
2910 let dialect = get_dialect(DbType::MySQL)?;
2911 let builder = QueryBuilder::<TestModel>::new(dialect);
2912
2913 let sql = builder.table("users").select(vec!["id", "name"])?.sql();
2914 assert!(sql.contains("SELECT `id`, `name` FROM"));
2915 assert!(sql.contains("`users`"));
2916 Ok(())
2917 }
2918
2919 #[test]
2920 fn test_query_builder_where() -> Result<(), crate::DbError> {
2921 let dialect = get_dialect(DbType::MySQL)?;
2922 let builder = QueryBuilder::<TestModel>::new(dialect);
2923
2924 let sql = builder
2925 .table("users")
2926 .where_eq("status", crate::value::Value::String("active".into()))
2927 .where_gt("age", crate::value::Value::I64(18))
2928 .sql();
2929
2930 assert!(sql.contains("WHERE"));
2931 assert!(sql.contains("`status` = 'active'"));
2932 assert!(sql.contains("`age` > 18"));
2933 Ok(())
2934 }
2935
2936 #[test]
2937 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2938 let dialect = get_dialect(DbType::MySQL)?;
2939 let builder = QueryBuilder::<TestModel>::new(dialect);
2940
2941 let sql = builder
2942 .table("users")
2943 .order_by("created_at")
2944 .order_desc("id")
2945 .sql();
2946
2947 assert!(sql.contains("ORDER BY"));
2948 assert!(sql.contains("`created_at` ASC"));
2949 assert!(sql.contains("`id` DESC"));
2950 Ok(())
2951 }
2952
2953 #[test]
2954 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2955 let dialect = get_dialect(DbType::MySQL)?;
2956 let builder = QueryBuilder::<TestModel>::new(dialect);
2957
2958 let sql = builder.table("users").limit(10).offset(20).sql();
2959
2960 assert!(sql.contains("LIMIT 10"));
2961 assert!(sql.contains("OFFSET 20"));
2962 Ok(())
2963 }
2964
2965 #[test]
2966 fn test_query_builder_page() -> Result<(), crate::DbError> {
2967 let dialect = get_dialect(DbType::MySQL)?;
2968 let builder = QueryBuilder::<TestModel>::new(dialect);
2969
2970 let sql = builder.table("users").page(3, 20).sql();
2971
2972 assert!(sql.contains("LIMIT 20"));
2973 assert!(sql.contains("OFFSET 40"));
2974 Ok(())
2975 }
2976
2977 #[test]
2978 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2979 let dialect = get_dialect(DbType::MySQL)?;
2980 let builder = QueryBuilder::<TestModel>::new(dialect);
2981
2982 let mut data = std::collections::HashMap::new();
2983 data.insert("name".to_string(), Value::String("test".to_string()));
2984 data.insert("age".to_string(), Value::I64(25));
2985
2986 let sql = builder.table("users").sql_insert(&data);
2987
2988 assert!(sql.contains("INSERT INTO"));
2989 assert!(sql.contains("`name`"));
2990 assert!(sql.contains("'test'"));
2991 Ok(())
2992 }
2993
2994 #[test]
2995 fn test_query_builder_update() -> Result<(), crate::DbError> {
2996 let dialect = get_dialect(DbType::MySQL)?;
2997 let builder = QueryBuilder::<TestModel>::new(dialect);
2998
2999 let mut data = std::collections::HashMap::new();
3000 data.insert("name".to_string(), Value::String("updated".to_string()));
3001
3002 let sql = builder
3003 .table("users")
3004 .where_eq("id", Value::I64(1))
3005 .sql_update(&data);
3006
3007 assert!(sql.contains("UPDATE"));
3008 assert!(sql.contains("`name` = 'updated'"));
3009 assert!(sql.contains("WHERE"));
3010 Ok(())
3011 }
3012
3013 #[test]
3014 fn test_query_builder_delete() -> Result<(), crate::DbError> {
3015 let dialect = get_dialect(DbType::MySQL)?;
3016 let builder = QueryBuilder::<TestModel>::new(dialect);
3017
3018 let sql = builder
3019 .table("users")
3020 .where_eq("id", Value::I64(1))
3021 .sql_delete();
3022
3023 assert!(sql.contains("DELETE FROM"));
3024 assert!(sql.contains("WHERE"));
3025 Ok(())
3026 }
3027
3028 #[test]
3029 fn test_query_builder_count() -> Result<(), crate::DbError> {
3030 let dialect = get_dialect(DbType::MySQL)?;
3031 let builder = QueryBuilder::<TestModel>::new(dialect);
3032
3033 let sql = builder.table("users").build_count();
3034
3035 assert!(sql.contains("SELECT COUNT(*)"));
3036 assert!(sql.contains("FROM"));
3037 Ok(())
3038 }
3039
3040 #[test]
3041 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
3042 let dialect = get_dialect(DbType::MySQL)?;
3043 let builder = QueryBuilder::<TestModel>::new(dialect);
3044
3045 let sql = builder
3046 .table("users")
3047 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3048 .sql();
3049
3050 assert!(sql.contains("IN ("));
3051 Ok(())
3052 }
3053
3054 #[test]
3055 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
3056 let dialect = get_dialect(DbType::MySQL)?;
3057 let builder = QueryBuilder::<TestModel>::new(dialect);
3058
3059 let sql = builder
3060 .table("users")
3061 .where_between("age", Value::I64(18), Value::I64(30))
3062 .sql();
3063
3064 assert!(sql.contains("BETWEEN"));
3065 Ok(())
3066 }
3067
3068 #[test]
3069 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
3070 let dialect = get_dialect(DbType::MySQL)?;
3071 let builder = QueryBuilder::<TestModel>::new(dialect);
3072
3073 let sql = builder.table("users").where_null("deleted_at").sql();
3074
3075 assert!(sql.contains("IS NULL"));
3076 Ok(())
3077 }
3078
3079 #[test]
3080 fn test_query_builder_join() -> Result<(), crate::DbError> {
3081 let dialect = get_dialect(DbType::MySQL)?;
3082 let builder = QueryBuilder::<TestModel>::new(dialect);
3083
3084 let sql = builder
3085 .table("users")
3086 .join_inner("posts", "users.id", "posts.user_id")
3087 .sql();
3088
3089 assert!(sql.contains("INNER JOIN"));
3090 assert!(sql.contains("`posts`"));
3091 Ok(())
3092 }
3093
3094 #[test]
3095 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3096 let dialect = get_dialect(DbType::MySQL)?;
3097 let builder = QueryBuilder::<TestModel>::new(dialect);
3098
3099 let sql = builder.table("users").group_by("status").sql();
3100
3101 assert!(sql.contains("GROUP BY"));
3102 assert!(sql.contains("`status`"));
3103 Ok(())
3104 }
3105
3106 #[test]
3107 fn test_query_builder_max() -> Result<(), crate::DbError> {
3108 let dialect = get_dialect(DbType::MySQL)?;
3109 let builder = QueryBuilder::<TestModel>::new(dialect);
3110
3111 let sql = builder.table("users").build_max("score");
3112
3113 assert!(sql.contains("MAX("));
3114 assert!(sql.contains("`score`"));
3115 Ok(())
3116 }
3117
3118 #[test]
3119 fn test_query_builder_min() -> Result<(), crate::DbError> {
3120 let dialect = get_dialect(DbType::MySQL)?;
3121 let builder = QueryBuilder::<TestModel>::new(dialect);
3122
3123 let sql = builder.table("users").build_min("price");
3124
3125 assert!(sql.contains("MIN("));
3126 assert!(sql.contains("`price`"));
3127 Ok(())
3128 }
3129
3130 #[test]
3131 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3132 let dialect = get_dialect(DbType::MySQL)?;
3133 let builder = QueryBuilder::<TestModel>::new(dialect);
3134
3135 let sql = builder.table("orders").build_sum("amount");
3136
3137 assert!(sql.contains("SUM("));
3138 assert!(sql.contains("`amount`"));
3139 Ok(())
3140 }
3141
3142 #[test]
3143 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3144 let dialect = get_dialect(DbType::MySQL)?;
3145 let builder = QueryBuilder::<TestModel>::new(dialect);
3146
3147 let sql = builder.table("scores").build_avg("value");
3148
3149 assert!(sql.contains("AVG("));
3150 assert!(sql.contains("`value`"));
3151 Ok(())
3152 }
3153
3154 #[test]
3155 fn test_validator_select() -> Result<(), crate::DbError> {
3156 let dialect = get_dialect(DbType::MySQL)?;
3157 let builder = QueryBuilder::<TestModel>::new(dialect);
3158
3159 let result = builder
3160 .table("users")
3161 .select(vec!["id", "name"])?
3162 .validate();
3163 assert!(result.is_ok());
3164 Ok(())
3165 }
3166
3167 #[test]
3168 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3169 let dialect = get_dialect(DbType::MySQL)?;
3170 let builder = QueryBuilder::<TestModel>::new(dialect);
3171
3172 let result = builder
3173 .table("users")
3174 .join_inner("posts", "users.id", "posts.user_id")
3175 .validate();
3176 assert!(result.is_ok());
3177 Ok(())
3178 }
3179
3180 #[test]
3181 fn test_validator_insert() -> Result<(), crate::DbError> {
3182 let dialect = get_dialect(DbType::MySQL)?;
3183 let builder = QueryBuilder::<TestModel>::new(dialect);
3184
3185 let mut data = std::collections::HashMap::new();
3186 data.insert("name".to_string(), Value::String("test".to_string()));
3187
3188 let result = builder.table("users").validate_insert(&data);
3189 assert!(result.is_ok());
3190 Ok(())
3191 }
3192
3193 #[test]
3194 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3195 let dialect = get_dialect(DbType::MySQL)?;
3196 let builder = QueryBuilder::<TestModel>::new(dialect);
3197
3198 let data = std::collections::HashMap::new();
3199 let result = builder.table("users").validate_insert(&data);
3200 assert!(result.is_err());
3201 Ok(())
3202 }
3203
3204 #[test]
3205 fn test_validator_update() -> Result<(), crate::DbError> {
3206 let dialect = get_dialect(DbType::MySQL)?;
3207 let builder = QueryBuilder::<TestModel>::new(dialect);
3208
3209 let mut data = std::collections::HashMap::new();
3210 data.insert("name".to_string(), Value::String("updated".to_string()));
3211
3212 let result = builder.table("users").validate_update(&data);
3213 assert!(result.is_ok());
3214 Ok(())
3215 }
3216
3217 #[test]
3218 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3219 let dialect = get_dialect(DbType::MySQL)?;
3220 let builder = QueryBuilder::<TestModel>::new(dialect);
3221
3222 let data = std::collections::HashMap::new();
3223 let result = builder.table("users").validate_update(&data);
3224 assert!(result.is_err());
3225 Ok(())
3226 }
3227
3228 #[test]
3229 fn test_validator_delete() -> Result<(), crate::DbError> {
3230 let dialect = get_dialect(DbType::MySQL)?;
3231 let builder = QueryBuilder::<TestModel>::new(dialect);
3232
3233 let result = builder
3234 .table("users")
3235 .where_eq("id", Value::I64(1))
3236 .validate_delete();
3237 assert!(result.is_ok());
3238 Ok(())
3239 }
3240
3241 #[test]
3242 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3243 let dialect = get_dialect(DbType::MySQL)?;
3244 let builder = QueryBuilder::<TestModel>::new(dialect);
3245
3246 let result = builder.table("users").validate_delete();
3248 assert!(result.is_ok());
3249 Ok(())
3250 }
3251
3252 #[test]
3255 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3256 let dialect = get_dialect(DbType::MySQL)?;
3257 let builder = QueryBuilder::<TestModel>::new(dialect);
3258 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3259 let sql = builder.sql();
3260 assert!(sql.contains("SELECT `id`, `name` FROM"));
3262 assert!(sql.contains("`users`"));
3263 Ok(())
3264 }
3265
3266 #[test]
3267 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3268 let dialect = get_dialect(DbType::MySQL)?;
3269 let builder = QueryBuilder::<TestModel>::new(dialect);
3270
3271 let result = builder
3273 .table("users")
3274 .select_quoted(vec!["id; DROP TABLE users"]);
3275 assert!(result.is_err());
3276
3277 let dialect = get_dialect(DbType::MySQL)?;
3279 let builder = QueryBuilder::<TestModel>::new(dialect);
3280 let result = builder.table("users").select_quoted(vec!["name'"]);
3281 assert!(result.is_err());
3282
3283 let dialect = get_dialect(DbType::MySQL)?;
3285 let builder = QueryBuilder::<TestModel>::new(dialect);
3286 let result = builder.table("users").select_quoted(vec!["1col"]);
3287 assert!(result.is_err());
3288
3289 let dialect = get_dialect(DbType::MySQL)?;
3291 let builder = QueryBuilder::<TestModel>::new(dialect);
3292 let result = builder.table("users").select_quoted(vec!["col name"]);
3293 assert!(result.is_err());
3294 Ok(())
3295 }
3296
3297 #[test]
3298 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3299 let dialect = get_dialect(DbType::PostgreSQL)?;
3300 let builder = QueryBuilder::<TestModel>::new(dialect);
3301 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3302 let sql = builder.sql();
3303 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3305 assert!(sql.contains("\"users\""));
3306 Ok(())
3307 }
3308
3309 struct SoftDeleteModel;
3313 impl Model for SoftDeleteModel {
3314 type PrimaryKey = i64;
3315
3316 fn table_name() -> &'static str {
3317 "soft_users"
3318 }
3319
3320 fn pk(&self) -> Self::PrimaryKey {
3321 1
3322 }
3323
3324 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3325
3326 fn soft_delete_field() -> Option<&'static str> {
3327 Some("deleted_at")
3328 }
3329 }
3330
3331 #[test]
3335 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3336 let dialect = get_dialect(DbType::MySQL)?;
3337 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3338 let sql = builder.table("soft_users").sql();
3339 assert!(
3341 sql.contains("`deleted_at` IS NULL"),
3342 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3343 sql
3344 );
3345 Ok(())
3346 }
3347
3348 #[test]
3350 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3351 let dialect = get_dialect(DbType::MySQL)?;
3352 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3353 .table("soft_users")
3354 .where_eq("status", Value::String("active".into()))
3355 .sql();
3356 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3358 assert!(
3359 sql.contains("`deleted_at` IS NULL"),
3360 "软删除条件应自动追加: {}",
3361 sql
3362 );
3363 Ok(())
3364 }
3365
3366 #[test]
3370 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3371 let dialect = get_dialect(DbType::MySQL)?;
3372 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3373 .table("soft_users")
3374 .without_soft_delete()
3375 .sql();
3376 assert!(
3378 !sql.contains("`deleted_at` IS NULL"),
3379 "without_soft_delete 应禁用过滤,实际: {}",
3380 sql
3381 );
3382 assert!(
3384 !sql.contains("WHERE"),
3385 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3386 sql
3387 );
3388 Ok(())
3389 }
3390
3391 #[test]
3395 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3396 let dialect = get_dialect(DbType::MySQL)?;
3397 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3398 .table("soft_users")
3399 .where_eq("id", Value::I64(42))
3400 .sql_delete();
3401 assert!(
3403 sql.starts_with("UPDATE"),
3404 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3405 sql
3406 );
3407 assert!(
3408 !sql.contains("DELETE FROM"),
3409 "不应生成 DELETE FROM: {}",
3410 sql
3411 );
3412 assert!(
3413 sql.contains("`deleted_at` = NOW()"),
3414 "应设置 deleted_at = NOW(): {}",
3415 sql
3416 );
3417 assert!(
3419 sql.contains("`deleted_at` IS NULL"),
3420 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3421 sql
3422 );
3423 Ok(())
3424 }
3425
3426 #[test]
3430 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3431 let dialect = get_dialect(DbType::MySQL)?;
3432 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3433 .table("soft_users")
3434 .where_eq("id", Value::I64(99))
3435 .build_force_delete();
3436 assert!(
3438 sql.starts_with("DELETE FROM"),
3439 "build_force_delete 应生成 DELETE FROM,实际: {}",
3440 sql
3441 );
3442 assert!(
3444 !sql.contains("`deleted_at` IS NULL"),
3445 "物理删除不应追加软删除过滤: {}",
3446 sql
3447 );
3448 Ok(())
3449 }
3450
3451 #[test]
3453 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3454 let dialect = get_dialect(DbType::MySQL)?;
3455 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3456 .table("soft_users")
3457 .where_eq("id", Value::I64(1))
3458 .build_select_with_params();
3459 assert!(
3460 sql.contains("`deleted_at` IS NULL"),
3461 "参数化版本也应自动追加软删除: {}",
3462 sql
3463 );
3464 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3465 assert_eq!(params[0], Value::I64(1));
3466 Ok(())
3467 }
3468
3469 #[test]
3471 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3472 let dialect = get_dialect(DbType::MySQL)?;
3473 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3474 .table("soft_users")
3475 .where_eq("id", Value::I64(7))
3476 .build_delete_with_params();
3477 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3478 assert!(
3479 sql.contains("`deleted_at` = NOW()"),
3480 "应设置 NOW(): {}",
3481 sql
3482 );
3483 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3484 Ok(())
3485 }
3486
3487 #[test]
3489 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3490 let dialect = get_dialect(DbType::MySQL)?;
3491 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3492 .table("soft_users")
3493 .where_eq("id", Value::I64(11))
3494 .build_force_delete_with_params();
3495 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3496 assert!(
3497 !sql.contains("`deleted_at` IS NULL"),
3498 "不应追加软删除过滤: {}",
3499 sql
3500 );
3501 assert_eq!(params.len(), 1);
3502 Ok(())
3503 }
3504
3505 #[test]
3509 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3510 let dialect = get_dialect(DbType::MySQL)?;
3511 let sql = QueryBuilder::<TestModel>::new(dialect)
3512 .table("users")
3513 .where_eq("id", Value::I64(1))
3514 .sql();
3515 assert!(
3516 !sql.contains("deleted_at"),
3517 "非软删除模型不应追加 deleted_at: {}",
3518 sql
3519 );
3520 let dialect = get_dialect(DbType::MySQL)?;
3522 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3523 .table("users")
3524 .where_eq("id", Value::I64(1))
3525 .sql_delete();
3526 assert!(
3527 del_sql.starts_with("DELETE FROM"),
3528 "非软删除模型 build_delete 应生成 DELETE: {}",
3529 del_sql
3530 );
3531 Ok(())
3532 }
3533
3534 #[test]
3536 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3537 let dialect = get_dialect(DbType::MySQL)?;
3538 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3539 .table("soft_users")
3540 .build_count();
3541 assert!(
3542 sql.contains("`deleted_at` IS NULL"),
3543 "build_count 也应追加软删除过滤: {}",
3544 sql
3545 );
3546 Ok(())
3547 }
3548
3549 #[test]
3555 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3556 let dialect = get_dialect(DbType::MySQL)?;
3557 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3558 .table("users")
3559 .where_eq("name", Value::String("alice".into()))
3560 .build_select_with_params();
3561 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3563 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3564 assert_eq!(params.len(), 1);
3565 assert_eq!(params[0], Value::String("alice".into()));
3566 Ok(())
3567 }
3568
3569 #[test]
3574 fn build_select_with_params_no_string_concat() -> Result<(), crate::DbError> {
3575 let dialect = get_dialect(DbType::MySQL)?;
3576 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3577 .table("users")
3578 .where_eq("name", Value::String("O'Brien".into()))
3579 .build_select_with_params();
3580 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3581 assert!(
3582 !sql.contains("O'Brien"),
3583 "不应将值内嵌到 SQL(字符串拼接): {}",
3584 sql
3585 );
3586 assert!(
3587 !sql.contains("O\\'Brien"),
3588 "不应将转义值内嵌到 SQL: {}",
3589 sql
3590 );
3591 assert_eq!(params.len(), 1, "应有 1 个参数: {:?}", params);
3592 assert_eq!(params[0], Value::String("O'Brien".into()));
3593 Ok(())
3594 }
3595
3596 #[test]
3598 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3599 let dialect = get_dialect(DbType::MySQL)?;
3600 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3601 .table("users")
3602 .where_like("name", Value::String("%alice%".into()))
3603 .build_select_with_params();
3604 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3605 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3606 assert_eq!(params.len(), 1);
3607 Ok(())
3608 }
3609
3610 #[test]
3614 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3615 let dialect = get_dialect(DbType::MySQL)?;
3616 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3617 .table("users")
3618 .where_ne("status", Value::I64(0))
3619 .build_select_with_params();
3620 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3621 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3622 assert_eq!(params.len(), 1);
3623 assert_eq!(params[0], Value::I64(0));
3624 Ok(())
3625 }
3626
3627 #[test]
3631 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3632 let dialect = get_dialect(DbType::MySQL)?;
3633 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3634 .table("users")
3635 .where_ge("age", Value::I64(18))
3636 .build_select_with_params();
3637 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3638 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3639 assert_eq!(params.len(), 1);
3640 assert_eq!(params[0], Value::I64(18));
3641 Ok(())
3642 }
3643
3644 #[test]
3648 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3649 let dialect = get_dialect(DbType::MySQL)?;
3650 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3651 .table("users")
3652 .where_lt("score", Value::F64(60.0))
3653 .build_select_with_params();
3654 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3655 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3656 assert_eq!(params.len(), 1);
3657 assert_eq!(params[0], Value::F64(60.0));
3658 Ok(())
3659 }
3660
3661 #[test]
3665 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3666 let dialect = get_dialect(DbType::MySQL)?;
3667 let evil_input = "'; DROP TABLE users; --".to_string();
3668 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3669 .table("users")
3670 .where_eq("name", Value::String(evil_input.clone()))
3671 .build_select_with_params();
3672 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3674 assert_eq!(params.len(), 1);
3676 assert_eq!(params[0], Value::String(evil_input));
3677 assert_eq!(sql.matches('?').count(), 1);
3679 Ok(())
3680 }
3681
3682 #[test]
3684 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3685 let dialect = get_dialect(DbType::MySQL)?;
3686 let evil = "' OR '1'='1".to_string();
3687 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3688 .table("users")
3689 .where_eq("name", Value::String(evil.clone()))
3690 .build_select_with_params();
3691 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3692 assert_eq!(params.len(), 1);
3693 assert_eq!(params[0], Value::String(evil));
3694 Ok(())
3695 }
3696
3697 #[test]
3699 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3700 let dialect = get_dialect(DbType::MySQL)?;
3701 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3702 .table("users")
3703 .where_eq("name", Value::String("alice".into()))
3704 .where_gt("age", Value::I64(18))
3705 .where_le("score", Value::F64(99.5))
3706 .build_select_with_params();
3707 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3708 assert_eq!(params.len(), 3);
3709 assert_eq!(params[0], Value::String("alice".into()));
3711 assert_eq!(params[1], Value::I64(18));
3712 assert_eq!(params[2], Value::F64(99.5));
3713 Ok(())
3714 }
3715
3716 #[test]
3718 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3719 let dialect = get_dialect(DbType::MySQL)?;
3720 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3721 .table("users")
3722 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3723 .build_select_with_params();
3724 assert!(
3725 sql.contains("`id` IN (?, ?, ?)"),
3726 "应使用 3 个占位符: {}",
3727 sql
3728 );
3729 assert_eq!(params.len(), 3);
3730 Ok(())
3731 }
3732
3733 #[test]
3735 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3736 let dialect = get_dialect(DbType::MySQL)?;
3737 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3738 .table("users")
3739 .where_between("age", Value::I64(18), Value::I64(65))
3740 .build_select_with_params();
3741 assert!(
3742 sql.contains("`age` BETWEEN ? AND ?"),
3743 "应使用 2 个占位符: {}",
3744 sql
3745 );
3746 assert_eq!(params.len(), 2);
3747 assert_eq!(params[0], Value::I64(18));
3748 assert_eq!(params[1], Value::I64(65));
3749 Ok(())
3750 }
3751
3752 #[test]
3754 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3755 let dialect = get_dialect(DbType::MySQL)?;
3756 let mut data = std::collections::HashMap::new();
3757 data.insert("name".to_string(), Value::String("bob".into()));
3758 data.insert("age".to_string(), Value::I64(30));
3759 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3760 .table("users")
3761 .where_eq("id", Value::I64(99))
3762 .build_update_with_params(&data);
3763 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3765 assert_eq!(params.len(), 3);
3766 assert_eq!(params[2], Value::I64(99));
3769 Ok(())
3770 }
3771
3772 #[test]
3776 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3777 let dialect = get_dialect(DbType::MySQL)?;
3778 let sql = QueryBuilder::<TestModel>::new(dialect)
3779 .table("users")
3780 .where_eq("name", Value::String("alice".into()))
3781 .sql();
3782 assert!(
3784 sql.contains("`name` = "),
3785 "无参数版本应含 WHERE 条件: {}",
3786 sql
3787 );
3788 assert!(
3790 !sql.contains("`name` = ?"),
3791 "无参数版本不应使用 ? 占位符: {}",
3792 sql
3793 );
3794 Ok(())
3795 }
3796
3797 #[test]
3799 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3800 let dialect = get_dialect(DbType::MySQL)?;
3801 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3802 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3803 let builder =
3804 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3805 assert!(
3806 builder.is_soft_delete_disabled(),
3807 "without_soft_delete 后应反映禁用状态"
3808 );
3809 Ok(())
3810 }
3811
3812 struct TenantModel;
3816 impl Model for TenantModel {
3817 type PrimaryKey = i64;
3818
3819 fn table_name() -> &'static str {
3820 "orders"
3821 }
3822
3823 fn pk(&self) -> Self::PrimaryKey {
3824 1
3825 }
3826
3827 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3828
3829 fn tenant_field() -> Option<&'static str> {
3830 Some("tenant_id")
3831 }
3832 }
3833
3834 struct SoftDeleteAndTenantModel;
3836 impl Model for SoftDeleteAndTenantModel {
3837 type PrimaryKey = i64;
3838
3839 fn table_name() -> &'static str {
3840 "documents"
3841 }
3842
3843 fn pk(&self) -> Self::PrimaryKey {
3844 1
3845 }
3846
3847 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3848
3849 fn soft_delete_field() -> Option<&'static str> {
3850 Some("deleted_at")
3851 }
3852
3853 fn tenant_field() -> Option<&'static str> {
3854 Some("tenant_id")
3855 }
3856 }
3857
3858 #[test]
3862 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3863 let dialect = get_dialect(DbType::MySQL)?;
3864 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3865 .table("orders")
3866 .with_tenant_id(42)
3867 .build_select_with_params();
3868 assert!(
3869 sql.contains("`tenant_id` = ?"),
3870 "多租户模型应自动追加 tenant_id = ?: {}",
3871 sql
3872 );
3873 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3874 assert_eq!(params[0], Value::I64(42));
3875 Ok(())
3876 }
3877
3878 #[test]
3880 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3881 let dialect = get_dialect(DbType::MySQL)?;
3882 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3883 .table("orders")
3884 .with_tenant_id(7)
3885 .where_eq("status", Value::String("active".into()))
3886 .build_select_with_params();
3887 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3888 assert!(
3889 sql.contains("`tenant_id` = ?"),
3890 "租户条件应自动追加: {}",
3891 sql
3892 );
3893 assert_eq!(params.len(), 2, "应有 2 个参数");
3894 assert_eq!(params[0], Value::String("active".into()));
3896 assert_eq!(params[1], Value::I64(7));
3897 Ok(())
3898 }
3899
3900 #[test]
3904 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3905 let dialect = get_dialect(DbType::MySQL)?;
3906 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3907 .table("orders")
3908 .with_tenant_id(42)
3909 .without_tenant()
3910 .build_select_with_params();
3911 assert!(
3912 !sql.contains("`tenant_id` = ?"),
3913 "without_tenant 应禁用过滤: {}",
3914 sql
3915 );
3916 assert_eq!(params.len(), 0, "不应有租户参数");
3917 Ok(())
3918 }
3919
3920 #[test]
3924 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3925 let dialect = get_dialect(DbType::MySQL)?;
3926 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3927 .table("orders")
3928 .with_tenant_id(99)
3929 .where_eq("id", Value::I64(1))
3930 .build_delete_with_params();
3931 assert!(
3932 sql.contains("`tenant_id` = ?"),
3933 "删除应自动追加租户条件: {}",
3934 sql
3935 );
3936 assert_eq!(params.len(), 2);
3938 assert_eq!(params[0], Value::I64(1));
3939 assert_eq!(params[1], Value::I64(99));
3940 Ok(())
3941 }
3942
3943 #[test]
3945 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3946 let dialect = get_dialect(DbType::MySQL)?;
3947 let mut data = std::collections::HashMap::new();
3948 data.insert("status".to_string(), Value::String("shipped".into()));
3949 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3950 .table("orders")
3951 .with_tenant_id(5)
3952 .where_eq("id", Value::I64(10))
3953 .build_update_with_params(&data);
3954 assert!(
3955 sql.contains("`tenant_id` = ?"),
3956 "更新应自动追加租户条件: {}",
3957 sql
3958 );
3959 assert_eq!(params.len(), 3);
3961 assert_eq!(params[2], Value::I64(5));
3963 Ok(())
3964 }
3965
3966 #[test]
3968 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3969 let dialect = get_dialect(DbType::MySQL)?;
3970 let sql = QueryBuilder::<TenantModel>::new(dialect)
3971 .table("orders")
3972 .with_tenant_id(42)
3973 .build_count();
3974 assert!(
3975 sql.contains("`tenant_id` = 42"),
3976 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3977 sql
3978 );
3979 Ok(())
3980 }
3981
3982 #[test]
3986 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3987 let dialect = get_dialect(DbType::MySQL)?;
3988 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3990 .table("users")
3991 .with_tenant_id(42)
3992 .build_select_with_params();
3993 assert!(
3994 !sql.contains("tenant_id"),
3995 "非多租户模型不应追加 tenant_id: {}",
3996 sql
3997 );
3998 assert_eq!(params.len(), 0);
3999 Ok(())
4000 }
4001
4002 #[test]
4006 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
4007 let dialect = get_dialect(DbType::MySQL)?;
4008 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4009 .table("orders")
4010 .build_select_with_params();
4011 assert!(
4012 !sql.contains("tenant_id"),
4013 "未设置 tenant_id 时不应追加过滤: {}",
4014 sql
4015 );
4016 assert_eq!(params.len(), 0);
4017 Ok(())
4018 }
4019
4020 #[test]
4024 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
4025 let dialect = get_dialect(DbType::MySQL)?;
4026 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
4027 .table("documents")
4028 .with_tenant_id(100)
4029 .where_eq("title", Value::String("report".into()))
4030 .build_select_with_params();
4031 assert!(
4033 sql.contains("`deleted_at` IS NULL"),
4034 "应追加软删除条件: {}",
4035 sql
4036 );
4037 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4039 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
4041 assert_eq!(params.len(), 2);
4043 assert_eq!(params[0], Value::String("report".into()));
4044 assert_eq!(params[1], Value::I64(100));
4045 Ok(())
4046 }
4047
4048 #[test]
4050 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
4051 let dialect = get_dialect(DbType::MySQL)?;
4052 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
4053 .table("documents")
4054 .with_tenant_id(100)
4055 .without_tenant()
4056 .without_soft_delete()
4057 .build_select_with_params();
4058 assert!(
4059 !sql.contains("`deleted_at` IS NULL"),
4060 "应禁用软删除: {}",
4061 sql
4062 );
4063 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
4064 assert_eq!(params.len(), 0);
4065 Ok(())
4066 }
4067
4068 #[test]
4070 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
4071 let dialect = get_dialect(DbType::MySQL)?;
4072 let builder = QueryBuilder::<TenantModel>::new(dialect);
4073 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
4074 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4075 .with_tenant_id(1)
4076 .without_tenant();
4077 assert!(
4078 builder.is_tenant_disabled(),
4079 "without_tenant 后应反映禁用状态"
4080 );
4081 Ok(())
4082 }
4083
4084 #[test]
4088 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
4089 let dialect = get_dialect(DbType::MySQL)?;
4090 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4091 .table("orders")
4092 .with_tenant_id(42)
4093 .where_eq("id", Value::I64(999))
4094 .build_force_delete_with_params();
4095 assert!(
4098 sql.contains("`tenant_id` = ?"),
4099 "物理删除应保留租户条件: {}",
4100 sql
4101 );
4102 assert_eq!(params.len(), 2);
4103 assert_eq!(params[0], Value::I64(999));
4104 assert_eq!(params[1], Value::I64(42));
4105 Ok(())
4106 }
4107
4108 #[cfg(feature = "multi-tenant-enhanced")]
4112 #[tokio::test]
4113 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4114 let ctx = crate::tenant_context::TenantContext::new(
4115 99,
4116 crate::tenant_context::IsolationStrategy::RowLevel,
4117 );
4118 ctx.scope(async {
4119 let (sql, params) =
4121 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4122 .table("orders")
4123 .with_tenant_id(42)
4124 .build_select_with_params();
4125 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4126 assert_eq!(params.len(), 1);
4127 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4128 })
4129 .await;
4130 Ok(())
4131 }
4132
4133 #[cfg(feature = "multi-tenant-enhanced")]
4135 #[tokio::test]
4136 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4137 let ctx = crate::tenant_context::TenantContext::new(
4138 77,
4139 crate::tenant_context::IsolationStrategy::RowLevel,
4140 );
4141 ctx.scope(async {
4142 let (sql, params) =
4143 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4144 .table("orders")
4145 .build_select_with_params();
4146 assert!(
4147 sql.contains("`tenant_id` = ?"),
4148 "应从上下文自动追加租户条件: {}",
4149 sql
4150 );
4151 assert_eq!(params.len(), 1);
4152 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4153 })
4154 .await;
4155 Ok(())
4156 }
4157
4158 #[cfg(feature = "multi-tenant-enhanced")]
4160 #[tokio::test]
4161 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4162 let ctx = crate::tenant_context::TenantContext::new(
4163 42,
4164 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4165 );
4166 ctx.scope(async {
4167 let (sql, _params) =
4168 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4169 .table("orders")
4170 .build_select_with_params();
4171 assert!(
4172 sql.contains("tenant_42_orders"),
4173 "Schema 隔离应重写表名: {}",
4174 sql
4175 );
4176 })
4177 .await;
4178 Ok(())
4179 }
4180
4181 #[cfg(feature = "multi-tenant-enhanced")]
4183 #[test]
4184 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4185 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4186 .table("orders")
4187 .build_select_with_params();
4188 assert!(
4190 !sql.contains("`tenant_id` = ?"),
4191 "未设置上下文不应追加租户条件: {}",
4192 sql
4193 );
4194 assert_eq!(params.len(), 0);
4195 Ok(())
4196 }
4197
4198 struct TcUsersTable;
4201 impl crate::typed::TypedTable for TcUsersTable {
4202 const NAME: &'static str = "users";
4203 }
4204 struct TcColId;
4205 impl crate::typed::TypedColumn for TcColId {
4206 const NAME: &'static str = "id";
4207 type Table = TcUsersTable;
4208 type RustType = i64;
4209 type SqlType = crate::typed_ast::Untyped;
4210 }
4211 struct TcColName;
4212 impl crate::typed::TypedColumn for TcColName {
4213 const NAME: &'static str = "name";
4214 type Table = TcUsersTable;
4215 type RustType = String;
4216 type SqlType = crate::typed_ast::Untyped;
4217 }
4218
4219 #[test]
4220 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4221 let dialect = get_dialect(DbType::MySQL)?;
4222 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4223 .where_eq_typed::<TcColId>(Value::I64(42))
4224 .build_select_with_params();
4225 assert!(sql.contains("`id` = ?"));
4226 assert_eq!(params[0], Value::I64(42));
4227 Ok(())
4228 }
4229
4230 #[test]
4231 fn test_order_by_typed() -> Result<(), crate::DbError> {
4232 let dialect = get_dialect(DbType::MySQL)?;
4233 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4234 .order_by_typed::<TcColName>()
4235 .build_select_with_params();
4236 assert!(sql.contains("ORDER BY"));
4237 assert!(sql.contains("`name`"));
4238 Ok(())
4239 }
4240
4241 #[test]
4242 fn test_select_typed() -> Result<(), crate::DbError> {
4243 let dialect = get_dialect(DbType::MySQL)?;
4244 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4245 .select_typed::<TcColId>()
4246 .select_typed::<TcColName>()
4247 .build_select_with_params();
4248 assert!(sql.contains("SELECT"));
4249 assert!(sql.contains("id"));
4251 assert!(sql.contains("name"));
4252 Ok(())
4253 }
4254
4255 #[test]
4256 fn test_where_null_typed() -> Result<(), crate::DbError> {
4257 let dialect = get_dialect(DbType::MySQL)?;
4258 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4259 .where_null_typed::<TcColName>()
4260 .build_select_with_params();
4261 assert!(sql.contains("`name` IS NULL"));
4262 Ok(())
4263 }
4264
4265 #[test]
4266 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4267 let dialect = get_dialect(DbType::MySQL)?;
4268 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4269 .where_not_null_typed::<TcColName>()
4270 .build_select_with_params();
4271 assert!(sql.contains("`name` IS NOT NULL"));
4272 Ok(())
4273 }
4274
4275 #[test]
4276 fn test_group_by_typed() -> Result<(), crate::DbError> {
4277 let dialect = get_dialect(DbType::MySQL)?;
4278 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4279 .group_by_typed::<TcColName>()
4280 .build_select_with_params();
4281 assert!(sql.contains("GROUP BY"));
4282 assert!(sql.contains("`name`"));
4283 Ok(())
4284 }
4285
4286 #[test]
4287 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4288 let dialect = get_dialect(DbType::MySQL)?;
4289 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4290 .where_gt_typed::<TcColId>(Value::I64(10))
4291 .build_select_with_params();
4292 assert!(sql.contains("`id` > ?"));
4293 assert_eq!(params[0], Value::I64(10));
4294 Ok(())
4295 }
4296
4297 #[test]
4300 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4301 let dialect = get_dialect(DbType::MySQL)?;
4302 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4303 .table("users")
4304 .where_eq("id", Value::I64(1))
4305 .lock_for_update()?
4306 .build_select_with_params();
4307 assert!(sql.contains("SELECT * FROM `users`"));
4308 assert!(sql.contains("WHERE `id` = ?"));
4309 assert!(sql.contains("FOR UPDATE"));
4310 assert_eq!(params.len(), 1);
4311 assert_eq!(params[0], Value::I64(1));
4312 Ok(())
4313 }
4314
4315 #[test]
4316 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4317 let dialect = get_dialect(DbType::MySQL)?;
4318 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4319 .table("users")
4320 .where_eq("id", Value::I64(1))
4321 .lock_shared()?
4322 .build_select_with_params();
4323 assert!(sql.contains("SELECT * FROM `users`"));
4324 assert!(sql.contains("WHERE `id` = ?"));
4325 assert!(sql.contains("LOCK IN SHARE MODE"));
4326 assert_eq!(params.len(), 1);
4327 assert_eq!(params[0], Value::I64(1));
4328 Ok(())
4329 }
4330
4331 #[test]
4332 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4333 let dialect = get_dialect(DbType::PostgreSQL)?;
4334 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4335 .table("users")
4336 .where_eq("id", Value::I64(1))
4337 .lock_for_update()?
4338 .build_select_with_params();
4339 assert!(sql.contains("SELECT * FROM \"users\""));
4340 assert!(sql.contains("WHERE \"id\" = ?"));
4341 assert!(sql.contains("FOR UPDATE"));
4342 assert_eq!(params.len(), 1);
4343 assert_eq!(params[0], Value::I64(1));
4344 Ok(())
4345 }
4346
4347 #[test]
4348 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4349 let dialect = get_dialect(DbType::PostgreSQL)?;
4350 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4351 .table("users")
4352 .where_eq("id", Value::I64(1))
4353 .lock_shared()?
4354 .build_select_with_params();
4355 assert!(sql.contains("SELECT * FROM \"users\""));
4356 assert!(sql.contains("WHERE \"id\" = ?"));
4357 assert!(sql.contains("FOR SHARE"));
4358 assert_eq!(params.len(), 1);
4359 assert_eq!(params[0], Value::I64(1));
4360 Ok(())
4361 }
4362
4363 #[test]
4364 fn test_lock_for_update_sqlite_should_fail() {
4365 let dialect = get_dialect(DbType::Sqlite).unwrap();
4366 let result = QueryBuilder::<TestModel>::new(dialect)
4367 .table("users")
4368 .where_eq("id", Value::I64(1))
4369 .lock_for_update();
4370 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4371 let err = result.err().unwrap();
4372 assert!(
4373 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4374 "错误信息应说明不支持行锁"
4375 );
4376 }
4377
4378 #[test]
4379 fn test_lock_shared_sqlite_should_fail() {
4380 let dialect = get_dialect(DbType::Sqlite).unwrap();
4381 let result = QueryBuilder::<TestModel>::new(dialect)
4382 .table("users")
4383 .where_eq("id", Value::I64(1))
4384 .lock_shared();
4385 assert!(result.is_err(), "SQLite 不应支持共享锁");
4386 let err = result.err().unwrap();
4387 assert!(
4388 format!("{:?}", err).contains("Shared lock is not supported"),
4389 "错误信息应说明不支持共享锁"
4390 );
4391 }
4392
4393 #[test]
4394 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4395 let dialect = get_dialect(DbType::MySQL)?;
4396 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4397 .table("users")
4398 .where_eq("status", Value::String("active".into()))
4399 .limit(10)
4400 .offset(20)
4401 .lock_for_update()?
4402 .build_select_with_params();
4403 assert!(sql.contains("WHERE `status` = ?"));
4404 assert!(sql.contains("LIMIT 10"));
4405 assert!(sql.contains("OFFSET 20"));
4406 assert!(sql.contains("FOR UPDATE"));
4407 assert_eq!(params.len(), 1);
4408 Ok(())
4409 }
4410
4411 #[test]
4414 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4415 let dialect = get_dialect(DbType::MySQL)?;
4416 let mut data = std::collections::HashMap::new();
4417 data.insert("name".to_string(), Value::String("Alice".into()));
4418 data.insert("age".to_string(), Value::I64(30));
4419
4420 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4421 .table("users")
4422 .insert_or_ignore()
4423 .build_insert_with_params(&data);
4424 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4425 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4427 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4428 assert!(sql.contains("VALUES (?, ?)"));
4429 assert_eq!(params.len(), 2);
4430 Ok(())
4431 }
4432
4433 #[test]
4434 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4435 let dialect = get_dialect(DbType::PostgreSQL)?;
4436 let mut data = std::collections::HashMap::new();
4437 data.insert("name".to_string(), Value::String("Bob".into()));
4438
4439 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4440 .table("users")
4441 .insert_or_ignore()
4442 .build_insert_with_params(&data);
4443 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4444 assert!(sql.contains("(\"name\")"));
4445 assert!(sql.contains("VALUES (?)"));
4446 assert_eq!(params.len(), 1);
4447 assert_eq!(params[0], Value::String("Bob".into()));
4448 Ok(())
4449 }
4450
4451 #[test]
4452 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4453 let dialect = get_dialect(DbType::Sqlite)?;
4454 let mut data = std::collections::HashMap::new();
4455 data.insert("name".to_string(), Value::String("Charlie".into()));
4456
4457 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4458 .table("users")
4459 .insert_or_ignore()
4460 .build_insert_with_params(&data);
4461 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4462 assert!(sql.contains("(\"name\")"));
4463 assert!(sql.contains("VALUES (?)"));
4464 assert_eq!(params.len(), 1);
4465 assert_eq!(params[0], Value::String("Charlie".into()));
4466 Ok(())
4467 }
4468
4469 #[test]
4470 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4471 let dialect = get_dialect(DbType::MySQL)?;
4472 let mut data = std::collections::HashMap::new();
4473 data.insert("name".to_string(), Value::String("Dave".into()));
4474
4475 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4476 .table("users")
4477 .build_insert_with_params(&data);
4478 assert!(sql.contains("INSERT INTO `users`"));
4479 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4480 assert_eq!(params.len(), 1);
4481 assert_eq!(params[0], Value::String("Dave".into()));
4482 Ok(())
4483 }
4484
4485 #[test]
4486 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4487 let dialect = get_dialect(DbType::MySQL)?;
4488 let data = std::collections::HashMap::new();
4489
4490 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4491 .table("users")
4492 .insert_or_ignore()
4493 .build_insert_with_params(&data);
4494 assert!(sql.is_empty(), "空数据应返回空 SQL");
4495 assert!(params.is_empty());
4496 Ok(())
4497 }
4498
4499 #[test]
4502 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4503 let mysql = get_dialect(DbType::MySQL)?;
4504 let pg = get_dialect(DbType::PostgreSQL)?;
4505 let sqlite = get_dialect(DbType::Sqlite)?;
4506 let clickhouse = get_dialect(DbType::ClickHouse)?;
4507 let duckdb = get_dialect(DbType::DuckDB)?;
4508
4509 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4510 assert!(
4511 pg.supports_lock_for_update(),
4512 "PostgreSQL 应支持 FOR UPDATE"
4513 );
4514 assert!(
4515 !sqlite.supports_lock_for_update(),
4516 "SQLite 不应支持 FOR UPDATE"
4517 );
4518 assert!(
4519 !clickhouse.supports_lock_for_update(),
4520 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4521 );
4522 assert!(
4523 !duckdb.supports_lock_for_update(),
4524 "DuckDB 不应支持 FOR UPDATE"
4525 );
4526 Ok(())
4527 }
4528
4529 #[test]
4530 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4531 let mysql = get_dialect(DbType::MySQL)?;
4532 let pg = get_dialect(DbType::PostgreSQL)?;
4533 let sqlite = get_dialect(DbType::Sqlite)?;
4534 let clickhouse = get_dialect(DbType::ClickHouse)?;
4535 let duckdb = get_dialect(DbType::DuckDB)?;
4536
4537 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4538 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4539 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4540 assert!(
4541 !clickhouse.supports_lock_shared(),
4542 "ClickHouse 是列式 OLAP,不应支持共享锁"
4543 );
4544 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4545 Ok(())
4546 }
4547
4548 #[test]
4549 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4550 let dialect = get_dialect(DbType::MySQL)?;
4551
4552 let builder = QueryBuilder::<TestModel>::new(dialect);
4554 assert!(builder.get_lock_type().is_none(), "默认无锁");
4555 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4556
4557 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4559 .table("users")
4560 .lock_for_update()?;
4561 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4562
4563 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4565 .table("users")
4566 .insert_or_ignore();
4567 assert!(builder.is_insert_or_ignore());
4568
4569 Ok(())
4570 }
4571
4572 struct TestModelWithColumns;
4573 impl Model for TestModelWithColumns {
4574 type PrimaryKey = i64;
4575 fn table_name() -> &'static str {
4576 "test_with_cols"
4577 }
4578 fn pk(&self) -> Self::PrimaryKey {
4579 0
4580 }
4581 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4582 }
4583 impl crate::model::ModelExt for TestModelWithColumns {
4584 fn columns() -> Vec<&'static str> {
4585 vec!["id", "name", "email", "avatar", "blob_data"]
4586 }
4587 fn fillable() -> Vec<&'static str> {
4588 vec!["name", "email", "avatar", "blob_data"]
4589 }
4590 fn guarded() -> Vec<&'static str> {
4591 vec!["id"]
4592 }
4593 fn hidden() -> Vec<&'static str> {
4594 vec!["blob_data"]
4595 }
4596 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4597 std::collections::HashMap::new()
4598 }
4599 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4600 fn to_json(&self) -> serde_json::Value {
4601 serde_json::Value::Null
4602 }
4603 }
4604
4605 #[test]
4606 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4607 let dialect = get_dialect(DbType::MySQL)?;
4608 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4609 .table("users")
4610 .select_exclude(&["avatar", "blob_data"])?;
4611 let sql = builder.sql();
4612 assert!(sql.contains("id"));
4613 assert!(sql.contains("name"));
4614 assert!(sql.contains("email"));
4615 assert!(!sql.contains("avatar"));
4616 assert!(!sql.contains("blob_data"));
4617 Ok(())
4618 }
4619
4620 #[test]
4621 fn test_select_exclude_nonexistent_field() {
4622 let dialect = get_dialect(DbType::MySQL).unwrap();
4623 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4624 .table("users")
4625 .select_exclude(&["nonexistent"]);
4626 assert!(result.is_err());
4627 }
4628
4629 #[test]
4630 fn test_select_exclude_all_fields() {
4631 let dialect = get_dialect(DbType::MySQL).unwrap();
4632 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4633 .table("users")
4634 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4635 assert!(result.is_err());
4636 let err = result.unwrap_err();
4637 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4638 }
4639
4640 #[tokio::test]
4643 async fn test_execute_with_cache_ttl_hit() {
4644 use crate::l2_cache::L2Cache;
4645 use std::sync::atomic::{AtomicU64, Ordering};
4646 use std::sync::Arc;
4647 use std::time::Duration;
4648
4649 let dialect = get_dialect(DbType::MySQL).unwrap();
4650 let cache = L2Cache::new();
4651 let qb = QueryBuilder::<TestModel>::new(dialect)
4652 .table("users")
4653 .where_eq("status", Value::I64(1))
4654 .cache_ttl(Duration::from_secs(300));
4655
4656 let call_count = Arc::new(AtomicU64::new(0));
4657 let cc1 = Arc::clone(&call_count);
4658 let rows1 = qb
4659 .execute_with_cache(&cache, "users", || async move {
4660 cc1.fetch_add(1, Ordering::SeqCst);
4661 let mut row = std::collections::HashMap::new();
4662 row.insert("id".to_string(), Value::I64(1));
4663 Ok(vec![row])
4664 })
4665 .await
4666 .unwrap();
4667 assert_eq!(rows1.len(), 1);
4668 assert_eq!(call_count.load(Ordering::SeqCst), 1);
4669
4670 let cc2 = Arc::clone(&call_count);
4671 let rows2 = qb
4672 .execute_with_cache(&cache, "users", || async move {
4673 cc2.fetch_add(1, Ordering::SeqCst);
4674 let mut row = std::collections::HashMap::new();
4675 row.insert("id".to_string(), Value::I64(2));
4676 Ok(vec![row])
4677 })
4678 .await
4679 .unwrap();
4680 assert_eq!(rows2.len(), 1);
4681 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
4682 assert_eq!(
4683 call_count.load(Ordering::SeqCst),
4684 1,
4685 "second call should hit cache, not loader"
4686 );
4687 }
4688
4689 #[tokio::test]
4690 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
4691 use crate::l2_cache::L2Cache;
4692 use std::sync::atomic::{AtomicU64, Ordering};
4693 use std::sync::Arc;
4694
4695 let dialect = get_dialect(DbType::MySQL).unwrap();
4696 let cache = L2Cache::new();
4697 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
4698
4699 let call_count = Arc::new(AtomicU64::new(0));
4700 let cc1 = Arc::clone(&call_count);
4701 let rows1 = qb
4702 .execute_with_cache(&cache, "users", || async move {
4703 cc1.fetch_add(1, Ordering::SeqCst);
4704 Ok(vec![])
4705 })
4706 .await
4707 .unwrap();
4708 assert!(rows1.is_empty());
4709
4710 let cc2 = Arc::clone(&call_count);
4711 let rows2 = qb
4712 .execute_with_cache(&cache, "users", || async move {
4713 cc2.fetch_add(1, Ordering::SeqCst);
4714 Ok(vec![])
4715 })
4716 .await
4717 .unwrap();
4718 assert!(rows2.is_empty());
4719 assert_eq!(
4720 call_count.load(Ordering::SeqCst),
4721 2,
4722 "without ttl, loader should be called each time"
4723 );
4724 }
4725}