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 mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
2245
2246 for join in &self.joins {
2247 match join {
2248 JoinClause::Inner(t, l, r) => {
2249 let _ = write!(
2250 sql,
2251 " INNER JOIN {} ON {} = {}",
2252 self.dialect.quote(t),
2253 self.dialect.quote(l),
2254 self.dialect.quote(r)
2255 );
2256 }
2257 JoinClause::Left(t, l, r) => {
2258 let _ = write!(
2259 sql,
2260 " LEFT JOIN {} ON {} = {}",
2261 self.dialect.quote(t),
2262 self.dialect.quote(l),
2263 self.dialect.quote(r)
2264 );
2265 }
2266 JoinClause::Right(t, l, r) => {
2267 let _ = write!(
2268 sql,
2269 " RIGHT JOIN {} ON {} = {}",
2270 self.dialect.quote(t),
2271 self.dialect.quote(l),
2272 self.dialect.quote(r)
2273 );
2274 }
2275 JoinClause::Cross(t, on) => {
2276 let _ = write!(
2277 sql,
2278 " CROSS JOIN {} ON {}",
2279 self.dialect.quote(t),
2280 self.dialect.quote(on)
2281 );
2282 }
2283 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2284 let _ = write!(
2285 sql,
2286 " {} {} ON {}.{} = {}.{}",
2287 kind.as_sql(),
2288 self.dialect.quote(tt),
2289 self.dialect.quote(ft),
2290 self.dialect.quote(fk),
2291 self.dialect.quote(tt),
2292 self.dialect.quote(tk)
2293 );
2294 }
2295 }
2296 }
2297
2298 let mut params = Vec::new();
2299 let (where_clause, where_params) = self.build_where_clause_with_params();
2301 if !where_clause.is_empty() {
2302 sql.push_str(&where_clause);
2303 params = where_params;
2304 }
2305
2306 if !self.group_by.is_empty() {
2307 let cols: Vec<String> = self
2308 .group_by
2309 .iter()
2310 .map(|c| self.dialect.quote(c))
2311 .collect();
2312 sql.push_str(" GROUP BY ");
2313 sql.push_str(&cols.join(", "));
2314 }
2315
2316 if !self.having_conditions.is_empty() {
2317 sql.push_str(" HAVING ");
2318 for (i, cond) in self.having_conditions.iter().enumerate() {
2319 if i > 0 {
2320 sql.push_str(" AND ");
2321 }
2322 match cond {
2323 WhereCondition::And(c) => sql.push_str(c),
2324 WhereCondition::Having(agg, op, value) => {
2326 params.push(value.clone());
2327 sql.push_str(&format!("{} {} ?", agg.render(&*self.dialect), op.as_sql()));
2328 }
2329 _ => {}
2330 }
2331 }
2332 }
2333
2334 if !self.order_by.is_empty() {
2335 let order_cols: Vec<String> = self
2336 .order_by
2337 .iter()
2338 .map(|o| {
2339 let dir = match o.direction {
2340 OrderDirection::Asc => " ASC",
2341 OrderDirection::Desc => " DESC",
2342 };
2343 format!("{}{}", self.dialect.quote(&o.field), dir)
2344 })
2345 .collect();
2346 sql.push_str(" ORDER BY ");
2347 sql.push_str(&order_cols.join(", "));
2348 }
2349
2350 if let Some(limit) = self.limit_value {
2351 let _ = write!(sql, " LIMIT {}", limit);
2352 }
2353 if let Some(offset) = self.offset_value {
2354 let _ = write!(sql, " OFFSET {}", offset);
2355 }
2356
2357 if let Some(lock_type) = &self.lock_type {
2359 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2360 sql.push(' ');
2361 sql.push_str(&lock_clause);
2362 }
2363 }
2364
2365 (sql, params)
2366 }
2367
2368 pub fn build_insert_with_params(
2370 &self,
2371 data: &std::collections::HashMap<String, Value>,
2372 ) -> (String, Vec<Value>) {
2373 let table = self
2374 .table
2375 .clone()
2376 .unwrap_or_else(|| M::table_name().to_string());
2377 if data.is_empty() {
2378 return (String::new(), Vec::new());
2379 }
2380
2381 let mut columns = Vec::with_capacity(data.len());
2382 let mut params = Vec::with_capacity(data.len());
2383 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2384 for (k, v) in data.iter() {
2385 columns.push(self.dialect.quote(k));
2386 params.push(v.clone());
2387 }
2388
2389 let insert_clause = if self.insert_or_ignore {
2391 self.dialect.build_insert_or_ignore_prefix(&table)
2392 } else {
2393 format!("INSERT INTO {}", self.dialect.quote(&table))
2394 };
2395
2396 let sql = format!(
2397 "{} ({}) VALUES ({})",
2398 insert_clause,
2399 columns.join(", "),
2400 placeholders.join(", ")
2401 );
2402 (sql, params)
2403 }
2404
2405 pub fn build_batch_insert_with_params(
2412 &self,
2413 rows: &[std::collections::HashMap<String, Value>],
2414 ) -> (String, Vec<Value>) {
2415 let table = self
2416 .table
2417 .clone()
2418 .unwrap_or_else(|| M::table_name().to_string());
2419 if rows.is_empty() {
2420 return (String::new(), Vec::new());
2421 }
2422
2423 let first_row = &rows[0];
2425 let columns: Vec<String> = first_row.keys().cloned().collect();
2426 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2427
2428 let mut params = Vec::with_capacity(rows.len() * columns.len());
2429 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2430 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2431 let mut param_idx = 1usize;
2432 for row in rows {
2433 let placeholders: Vec<String> = columns
2434 .iter()
2435 .map(|col| match row.get(col) {
2436 Some(v) => {
2437 params.push(v.clone());
2438 if is_pg {
2439 let p = format!("${}", param_idx);
2440 param_idx += 1;
2441 p
2442 } else {
2443 "?".to_string()
2444 }
2445 }
2446 None => "NULL".to_string(),
2447 })
2448 .collect();
2449 value_groups.push(format!("({})", placeholders.join(", ")));
2450 }
2451
2452 let sql = format!(
2453 "INSERT INTO {} ({}) VALUES {}",
2454 self.dialect.quote(&table),
2455 quoted_columns.join(", "),
2456 value_groups.join(", ")
2457 );
2458 (sql, params)
2459 }
2460
2461 pub fn build_batch_upsert_with_params(
2482 &self,
2483 rows: &[std::collections::HashMap<String, Value>],
2484 conflict_columns: &[&str],
2485 update_columns: &[&str],
2486 ) -> Result<(String, Vec<Value>), crate::DbError> {
2487 if rows.is_empty() {
2488 return Err(crate::DbError::InvalidInput(
2489 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2490 ));
2491 }
2492
2493 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2495 if insert_sql.is_empty() {
2496 return Err(crate::DbError::InvalidInput(
2497 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2498 ));
2499 }
2500
2501 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2503
2504 let conflict_clause = self
2506 .dialect
2507 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2508 .ok_or_else(|| {
2509 crate::DbError::InvalidInput(format!(
2510 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2511 self.dialect.db_type()
2512 ))
2513 })?;
2514
2515 let sql = format!("{} {}", insert_sql, conflict_clause);
2516 Ok((sql, params))
2517 }
2518
2519 pub fn build_update_with_params(
2522 &self,
2523 data: &std::collections::HashMap<String, Value>,
2524 ) -> (String, Vec<Value>) {
2525 let table = self
2526 .table
2527 .clone()
2528 .unwrap_or_else(|| M::table_name().to_string());
2529 if data.is_empty() {
2530 return (String::new(), Vec::new());
2531 }
2532
2533 let mut set_clauses = Vec::with_capacity(data.len());
2534 let mut params = Vec::with_capacity(data.len());
2535 for (k, v) in data.iter() {
2536 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2537 params.push(v.clone());
2538 }
2539
2540 let mut sql = format!(
2541 "UPDATE {} SET {}",
2542 self.dialect.quote(&table),
2543 set_clauses.join(", ")
2544 );
2545
2546 let (where_clause, where_params) = self.build_where_clause_with_params();
2548 if !where_clause.is_empty() {
2549 sql.push_str(&where_clause);
2550 params.extend(where_params);
2551 }
2552
2553 (sql, params)
2554 }
2555
2556 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2562 let table = self
2563 .table
2564 .clone()
2565 .unwrap_or_else(|| M::table_name().to_string());
2566
2567 if let Some(field) = self.soft_delete_field() {
2569 let (where_clause, where_params) = self.build_where_clause_with_params();
2570 let sql = format!(
2571 "UPDATE {} SET {} = NOW(){}",
2572 self.dialect.quote(&table),
2573 self.dialect.quote(field),
2574 where_clause
2575 );
2576 return (sql, where_params);
2577 }
2578
2579 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2580 let mut params = Vec::new();
2581
2582 let (where_clause, where_params) = self.build_where_clause_with_params();
2583 if !where_clause.is_empty() {
2584 sql.push_str(&where_clause);
2585 params = where_params;
2586 }
2587
2588 (sql, params)
2589 }
2590
2591 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2595 let table = self
2596 .table
2597 .clone()
2598 .unwrap_or_else(|| M::table_name().to_string());
2599
2600 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2601 let mut params = Vec::new();
2602
2603 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2605 if !where_clause.is_empty() {
2606 sql.push_str(&where_clause);
2607 params = where_params;
2608 }
2609
2610 (sql, params)
2611 }
2612
2613 pub fn build_count(&self) -> String {
2615 let table = self
2616 .table
2617 .clone()
2618 .unwrap_or_else(|| M::table_name().to_string());
2619
2620 let mut sql = format!(
2621 "SELECT COUNT(*) as total FROM {}",
2622 self.dialect.quote(&table)
2623 );
2624 sql.push_str(&self.build_where_clause());
2625 sql
2626 }
2627
2628 pub fn build_exists(&self) -> String {
2630 let table = self
2631 .table
2632 .clone()
2633 .unwrap_or_else(|| M::table_name().to_string());
2634
2635 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2636 sql.push_str(&self.build_where_clause());
2637 sql.push_str(" LIMIT 1");
2638 format!("SELECT EXISTS({})", sql)
2639 }
2640
2641 pub fn build_max(&self, field: &str) -> String {
2643 let table = self
2644 .table
2645 .clone()
2646 .unwrap_or_else(|| M::table_name().to_string());
2647
2648 let mut sql = format!(
2649 "SELECT MAX({}) as max_val FROM {}",
2650 self.dialect.quote(field),
2651 self.dialect.quote(&table)
2652 );
2653 sql.push_str(&self.build_where_clause());
2654 sql
2655 }
2656
2657 pub fn build_min(&self, field: &str) -> String {
2659 let table = self
2660 .table
2661 .clone()
2662 .unwrap_or_else(|| M::table_name().to_string());
2663
2664 let mut sql = format!(
2665 "SELECT MIN({}) as min_val FROM {}",
2666 self.dialect.quote(field),
2667 self.dialect.quote(&table)
2668 );
2669 sql.push_str(&self.build_where_clause());
2670 sql
2671 }
2672
2673 pub fn build_sum(&self, field: &str) -> String {
2675 let table = self
2676 .table
2677 .clone()
2678 .unwrap_or_else(|| M::table_name().to_string());
2679
2680 let mut sql = format!(
2681 "SELECT SUM({}) as sum_val FROM {}",
2682 self.dialect.quote(field),
2683 self.dialect.quote(&table)
2684 );
2685 sql.push_str(&self.build_where_clause());
2686 sql
2687 }
2688
2689 pub fn build_avg(&self, field: &str) -> String {
2691 let table = self
2692 .table
2693 .clone()
2694 .unwrap_or_else(|| M::table_name().to_string());
2695
2696 let mut sql = format!(
2697 "SELECT AVG({}) as avg_val FROM {}",
2698 self.dialect.quote(field),
2699 self.dialect.quote(&table)
2700 );
2701 sql.push_str(&self.build_where_clause());
2702 sql
2703 }
2704
2705 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2708 let sql = self.sql();
2709 let mut errors = Vec::new();
2710
2711 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2712 errors.push(e);
2713 }
2714
2715 if !self.joins.is_empty() {
2717 for join in &self.joins {
2718 match join {
2719 JoinClause::Inner(_, left, right)
2720 | JoinClause::Left(_, left, right)
2721 | JoinClause::Right(_, left, right) => {
2722 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2723 errors.push(e);
2724 }
2725 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2726 errors.push(e);
2727 }
2728 }
2729 JoinClause::Relation(_, ft, fk, tt, tk) => {
2730 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2731 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2732 errors.push(e);
2733 }
2734 }
2735 }
2736 _ => {}
2737 }
2738 }
2739 }
2740
2741 let table = self
2743 .table
2744 .clone()
2745 .unwrap_or_else(|| M::table_name().to_string());
2746 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2747 errors.push(e);
2748 }
2749
2750 if errors.is_empty() {
2751 Ok(())
2752 } else {
2753 Err(errors)
2754 }
2755 }
2756
2757 pub fn validate_insert(
2760 &self,
2761 data: &std::collections::HashMap<String, Value>,
2762 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2763 let sql = self.sql_insert(data);
2764 let mut errors = Vec::new();
2765
2766 if sql.is_empty() {
2767 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2768 return Err(errors);
2769 }
2770
2771 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2772 errors.push(e);
2773 }
2774
2775 if errors.is_empty() {
2776 Ok(())
2777 } else {
2778 Err(errors)
2779 }
2780 }
2781
2782 pub fn validate_update(
2785 &self,
2786 data: &std::collections::HashMap<String, Value>,
2787 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2788 let sql = self.sql_update(data);
2789 let mut errors = Vec::new();
2790
2791 if sql.is_empty() {
2792 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2793 return Err(errors);
2794 }
2795
2796 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2797 errors.push(e);
2798 }
2799
2800 if errors.is_empty() {
2801 Ok(())
2802 } else {
2803 Err(errors)
2804 }
2805 }
2806
2807 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2809 let sql = self.sql_delete();
2810 let mut errors = Vec::new();
2811
2812 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2813 errors.push(e);
2814 }
2815
2816 if errors.is_empty() {
2817 Ok(())
2818 } else {
2819 Err(errors)
2820 }
2821 }
2822}
2823
2824impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2826 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2844 let all_columns = M::columns();
2845 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2846
2847 for field in fields {
2848 if !all_columns.contains(field) {
2849 return Err(crate::DbError::InvalidInput(format!(
2850 "排除的字段不存在: {}",
2851 field
2852 )));
2853 }
2854 }
2855
2856 let retained: Vec<String> = all_columns
2857 .into_iter()
2858 .filter(|c| !exclude_set.contains(*c))
2859 .map(|s| s.to_string())
2860 .collect();
2861
2862 if retained.is_empty() {
2863 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2864 }
2865
2866 self.select_mode = crate::partial_model::SelectMode::Partial;
2867 self.select_columns = retained;
2868 Ok(self)
2869 }
2870}
2871
2872impl<M: Model> fmt::Debug for QueryBuilder<M> {
2873 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2874 f.debug_struct("QueryBuilder")
2875 .field("table", &self.table)
2876 .field("select_columns", &self.select_columns)
2877 .field("where_conditions", &self.where_conditions.len())
2878 .field("limit", &self.limit_value)
2879 .finish()
2880 }
2881}
2882
2883#[cfg(test)]
2884#[allow(deprecated)]
2885mod tests {
2886 use super::*;
2887 use crate::db_type::DbType;
2888 use crate::dialect::get_dialect;
2889
2890 struct TestModel;
2891 impl Model for TestModel {
2892 type PrimaryKey = i64;
2893
2894 fn table_name() -> &'static str {
2895 "test_models"
2896 }
2897
2898 fn pk(&self) -> Self::PrimaryKey {
2899 1
2900 }
2901
2902 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2903 }
2904
2905 #[test]
2906 fn test_query_builder_select() -> Result<(), crate::DbError> {
2907 let dialect = get_dialect(DbType::MySQL)?;
2908 let builder = QueryBuilder::<TestModel>::new(dialect);
2909
2910 let sql = builder.table("users").select(vec!["id", "name"])?.sql();
2911 assert!(sql.contains("SELECT `id`, `name` FROM"));
2912 assert!(sql.contains("`users`"));
2913 Ok(())
2914 }
2915
2916 #[test]
2917 fn test_query_builder_where() -> Result<(), crate::DbError> {
2918 let dialect = get_dialect(DbType::MySQL)?;
2919 let builder = QueryBuilder::<TestModel>::new(dialect);
2920
2921 let sql = builder
2922 .table("users")
2923 .where_eq("status", crate::value::Value::String("active".into()))
2924 .where_gt("age", crate::value::Value::I64(18))
2925 .sql();
2926
2927 assert!(sql.contains("WHERE"));
2928 assert!(sql.contains("`status` = 'active'"));
2929 assert!(sql.contains("`age` > 18"));
2930 Ok(())
2931 }
2932
2933 #[test]
2934 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2935 let dialect = get_dialect(DbType::MySQL)?;
2936 let builder = QueryBuilder::<TestModel>::new(dialect);
2937
2938 let sql = builder
2939 .table("users")
2940 .order_by("created_at")
2941 .order_desc("id")
2942 .sql();
2943
2944 assert!(sql.contains("ORDER BY"));
2945 assert!(sql.contains("`created_at` ASC"));
2946 assert!(sql.contains("`id` DESC"));
2947 Ok(())
2948 }
2949
2950 #[test]
2951 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2952 let dialect = get_dialect(DbType::MySQL)?;
2953 let builder = QueryBuilder::<TestModel>::new(dialect);
2954
2955 let sql = builder.table("users").limit(10).offset(20).sql();
2956
2957 assert!(sql.contains("LIMIT 10"));
2958 assert!(sql.contains("OFFSET 20"));
2959 Ok(())
2960 }
2961
2962 #[test]
2963 fn test_query_builder_page() -> Result<(), crate::DbError> {
2964 let dialect = get_dialect(DbType::MySQL)?;
2965 let builder = QueryBuilder::<TestModel>::new(dialect);
2966
2967 let sql = builder.table("users").page(3, 20).sql();
2968
2969 assert!(sql.contains("LIMIT 20"));
2970 assert!(sql.contains("OFFSET 40"));
2971 Ok(())
2972 }
2973
2974 #[test]
2975 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2976 let dialect = get_dialect(DbType::MySQL)?;
2977 let builder = QueryBuilder::<TestModel>::new(dialect);
2978
2979 let mut data = std::collections::HashMap::new();
2980 data.insert("name".to_string(), Value::String("test".to_string()));
2981 data.insert("age".to_string(), Value::I64(25));
2982
2983 let sql = builder.table("users").sql_insert(&data);
2984
2985 assert!(sql.contains("INSERT INTO"));
2986 assert!(sql.contains("`name`"));
2987 assert!(sql.contains("'test'"));
2988 Ok(())
2989 }
2990
2991 #[test]
2992 fn test_query_builder_update() -> Result<(), crate::DbError> {
2993 let dialect = get_dialect(DbType::MySQL)?;
2994 let builder = QueryBuilder::<TestModel>::new(dialect);
2995
2996 let mut data = std::collections::HashMap::new();
2997 data.insert("name".to_string(), Value::String("updated".to_string()));
2998
2999 let sql = builder
3000 .table("users")
3001 .where_eq("id", Value::I64(1))
3002 .sql_update(&data);
3003
3004 assert!(sql.contains("UPDATE"));
3005 assert!(sql.contains("`name` = 'updated'"));
3006 assert!(sql.contains("WHERE"));
3007 Ok(())
3008 }
3009
3010 #[test]
3011 fn test_query_builder_delete() -> Result<(), crate::DbError> {
3012 let dialect = get_dialect(DbType::MySQL)?;
3013 let builder = QueryBuilder::<TestModel>::new(dialect);
3014
3015 let sql = builder
3016 .table("users")
3017 .where_eq("id", Value::I64(1))
3018 .sql_delete();
3019
3020 assert!(sql.contains("DELETE FROM"));
3021 assert!(sql.contains("WHERE"));
3022 Ok(())
3023 }
3024
3025 #[test]
3026 fn test_query_builder_count() -> Result<(), crate::DbError> {
3027 let dialect = get_dialect(DbType::MySQL)?;
3028 let builder = QueryBuilder::<TestModel>::new(dialect);
3029
3030 let sql = builder.table("users").build_count();
3031
3032 assert!(sql.contains("SELECT COUNT(*)"));
3033 assert!(sql.contains("FROM"));
3034 Ok(())
3035 }
3036
3037 #[test]
3038 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
3039 let dialect = get_dialect(DbType::MySQL)?;
3040 let builder = QueryBuilder::<TestModel>::new(dialect);
3041
3042 let sql = builder
3043 .table("users")
3044 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3045 .sql();
3046
3047 assert!(sql.contains("IN ("));
3048 Ok(())
3049 }
3050
3051 #[test]
3052 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
3053 let dialect = get_dialect(DbType::MySQL)?;
3054 let builder = QueryBuilder::<TestModel>::new(dialect);
3055
3056 let sql = builder
3057 .table("users")
3058 .where_between("age", Value::I64(18), Value::I64(30))
3059 .sql();
3060
3061 assert!(sql.contains("BETWEEN"));
3062 Ok(())
3063 }
3064
3065 #[test]
3066 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
3067 let dialect = get_dialect(DbType::MySQL)?;
3068 let builder = QueryBuilder::<TestModel>::new(dialect);
3069
3070 let sql = builder.table("users").where_null("deleted_at").sql();
3071
3072 assert!(sql.contains("IS NULL"));
3073 Ok(())
3074 }
3075
3076 #[test]
3077 fn test_query_builder_join() -> Result<(), crate::DbError> {
3078 let dialect = get_dialect(DbType::MySQL)?;
3079 let builder = QueryBuilder::<TestModel>::new(dialect);
3080
3081 let sql = builder
3082 .table("users")
3083 .join_inner("posts", "users.id", "posts.user_id")
3084 .sql();
3085
3086 assert!(sql.contains("INNER JOIN"));
3087 assert!(sql.contains("`posts`"));
3088 Ok(())
3089 }
3090
3091 #[test]
3092 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3093 let dialect = get_dialect(DbType::MySQL)?;
3094 let builder = QueryBuilder::<TestModel>::new(dialect);
3095
3096 let sql = builder.table("users").group_by("status").sql();
3097
3098 assert!(sql.contains("GROUP BY"));
3099 assert!(sql.contains("`status`"));
3100 Ok(())
3101 }
3102
3103 #[test]
3104 fn test_query_builder_max() -> Result<(), crate::DbError> {
3105 let dialect = get_dialect(DbType::MySQL)?;
3106 let builder = QueryBuilder::<TestModel>::new(dialect);
3107
3108 let sql = builder.table("users").build_max("score");
3109
3110 assert!(sql.contains("MAX("));
3111 assert!(sql.contains("`score`"));
3112 Ok(())
3113 }
3114
3115 #[test]
3116 fn test_query_builder_min() -> Result<(), crate::DbError> {
3117 let dialect = get_dialect(DbType::MySQL)?;
3118 let builder = QueryBuilder::<TestModel>::new(dialect);
3119
3120 let sql = builder.table("users").build_min("price");
3121
3122 assert!(sql.contains("MIN("));
3123 assert!(sql.contains("`price`"));
3124 Ok(())
3125 }
3126
3127 #[test]
3128 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3129 let dialect = get_dialect(DbType::MySQL)?;
3130 let builder = QueryBuilder::<TestModel>::new(dialect);
3131
3132 let sql = builder.table("orders").build_sum("amount");
3133
3134 assert!(sql.contains("SUM("));
3135 assert!(sql.contains("`amount`"));
3136 Ok(())
3137 }
3138
3139 #[test]
3140 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3141 let dialect = get_dialect(DbType::MySQL)?;
3142 let builder = QueryBuilder::<TestModel>::new(dialect);
3143
3144 let sql = builder.table("scores").build_avg("value");
3145
3146 assert!(sql.contains("AVG("));
3147 assert!(sql.contains("`value`"));
3148 Ok(())
3149 }
3150
3151 #[test]
3152 fn test_validator_select() -> Result<(), crate::DbError> {
3153 let dialect = get_dialect(DbType::MySQL)?;
3154 let builder = QueryBuilder::<TestModel>::new(dialect);
3155
3156 let result = builder
3157 .table("users")
3158 .select(vec!["id", "name"])?
3159 .validate();
3160 assert!(result.is_ok());
3161 Ok(())
3162 }
3163
3164 #[test]
3165 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3166 let dialect = get_dialect(DbType::MySQL)?;
3167 let builder = QueryBuilder::<TestModel>::new(dialect);
3168
3169 let result = builder
3170 .table("users")
3171 .join_inner("posts", "users.id", "posts.user_id")
3172 .validate();
3173 assert!(result.is_ok());
3174 Ok(())
3175 }
3176
3177 #[test]
3178 fn test_validator_insert() -> Result<(), crate::DbError> {
3179 let dialect = get_dialect(DbType::MySQL)?;
3180 let builder = QueryBuilder::<TestModel>::new(dialect);
3181
3182 let mut data = std::collections::HashMap::new();
3183 data.insert("name".to_string(), Value::String("test".to_string()));
3184
3185 let result = builder.table("users").validate_insert(&data);
3186 assert!(result.is_ok());
3187 Ok(())
3188 }
3189
3190 #[test]
3191 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3192 let dialect = get_dialect(DbType::MySQL)?;
3193 let builder = QueryBuilder::<TestModel>::new(dialect);
3194
3195 let data = std::collections::HashMap::new();
3196 let result = builder.table("users").validate_insert(&data);
3197 assert!(result.is_err());
3198 Ok(())
3199 }
3200
3201 #[test]
3202 fn test_validator_update() -> Result<(), crate::DbError> {
3203 let dialect = get_dialect(DbType::MySQL)?;
3204 let builder = QueryBuilder::<TestModel>::new(dialect);
3205
3206 let mut data = std::collections::HashMap::new();
3207 data.insert("name".to_string(), Value::String("updated".to_string()));
3208
3209 let result = builder.table("users").validate_update(&data);
3210 assert!(result.is_ok());
3211 Ok(())
3212 }
3213
3214 #[test]
3215 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3216 let dialect = get_dialect(DbType::MySQL)?;
3217 let builder = QueryBuilder::<TestModel>::new(dialect);
3218
3219 let data = std::collections::HashMap::new();
3220 let result = builder.table("users").validate_update(&data);
3221 assert!(result.is_err());
3222 Ok(())
3223 }
3224
3225 #[test]
3226 fn test_validator_delete() -> Result<(), crate::DbError> {
3227 let dialect = get_dialect(DbType::MySQL)?;
3228 let builder = QueryBuilder::<TestModel>::new(dialect);
3229
3230 let result = builder
3231 .table("users")
3232 .where_eq("id", Value::I64(1))
3233 .validate_delete();
3234 assert!(result.is_ok());
3235 Ok(())
3236 }
3237
3238 #[test]
3239 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3240 let dialect = get_dialect(DbType::MySQL)?;
3241 let builder = QueryBuilder::<TestModel>::new(dialect);
3242
3243 let result = builder.table("users").validate_delete();
3245 assert!(result.is_ok());
3246 Ok(())
3247 }
3248
3249 #[test]
3252 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3253 let dialect = get_dialect(DbType::MySQL)?;
3254 let builder = QueryBuilder::<TestModel>::new(dialect);
3255 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3256 let sql = builder.sql();
3257 assert!(sql.contains("SELECT `id`, `name` FROM"));
3259 assert!(sql.contains("`users`"));
3260 Ok(())
3261 }
3262
3263 #[test]
3264 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3265 let dialect = get_dialect(DbType::MySQL)?;
3266 let builder = QueryBuilder::<TestModel>::new(dialect);
3267
3268 let result = builder
3270 .table("users")
3271 .select_quoted(vec!["id; DROP TABLE users"]);
3272 assert!(result.is_err());
3273
3274 let dialect = get_dialect(DbType::MySQL)?;
3276 let builder = QueryBuilder::<TestModel>::new(dialect);
3277 let result = builder.table("users").select_quoted(vec!["name'"]);
3278 assert!(result.is_err());
3279
3280 let dialect = get_dialect(DbType::MySQL)?;
3282 let builder = QueryBuilder::<TestModel>::new(dialect);
3283 let result = builder.table("users").select_quoted(vec!["1col"]);
3284 assert!(result.is_err());
3285
3286 let dialect = get_dialect(DbType::MySQL)?;
3288 let builder = QueryBuilder::<TestModel>::new(dialect);
3289 let result = builder.table("users").select_quoted(vec!["col name"]);
3290 assert!(result.is_err());
3291 Ok(())
3292 }
3293
3294 #[test]
3295 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3296 let dialect = get_dialect(DbType::PostgreSQL)?;
3297 let builder = QueryBuilder::<TestModel>::new(dialect);
3298 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3299 let sql = builder.sql();
3300 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3302 assert!(sql.contains("\"users\""));
3303 Ok(())
3304 }
3305
3306 struct SoftDeleteModel;
3310 impl Model for SoftDeleteModel {
3311 type PrimaryKey = i64;
3312
3313 fn table_name() -> &'static str {
3314 "soft_users"
3315 }
3316
3317 fn pk(&self) -> Self::PrimaryKey {
3318 1
3319 }
3320
3321 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3322
3323 fn soft_delete_field() -> Option<&'static str> {
3324 Some("deleted_at")
3325 }
3326 }
3327
3328 #[test]
3332 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3333 let dialect = get_dialect(DbType::MySQL)?;
3334 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3335 let sql = builder.table("soft_users").sql();
3336 assert!(
3338 sql.contains("`deleted_at` IS NULL"),
3339 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3340 sql
3341 );
3342 Ok(())
3343 }
3344
3345 #[test]
3347 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3348 let dialect = get_dialect(DbType::MySQL)?;
3349 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3350 .table("soft_users")
3351 .where_eq("status", Value::String("active".into()))
3352 .sql();
3353 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3355 assert!(
3356 sql.contains("`deleted_at` IS NULL"),
3357 "软删除条件应自动追加: {}",
3358 sql
3359 );
3360 Ok(())
3361 }
3362
3363 #[test]
3367 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3368 let dialect = get_dialect(DbType::MySQL)?;
3369 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3370 .table("soft_users")
3371 .without_soft_delete()
3372 .sql();
3373 assert!(
3375 !sql.contains("`deleted_at` IS NULL"),
3376 "without_soft_delete 应禁用过滤,实际: {}",
3377 sql
3378 );
3379 assert!(
3381 !sql.contains("WHERE"),
3382 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3383 sql
3384 );
3385 Ok(())
3386 }
3387
3388 #[test]
3392 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3393 let dialect = get_dialect(DbType::MySQL)?;
3394 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3395 .table("soft_users")
3396 .where_eq("id", Value::I64(42))
3397 .sql_delete();
3398 assert!(
3400 sql.starts_with("UPDATE"),
3401 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3402 sql
3403 );
3404 assert!(
3405 !sql.contains("DELETE FROM"),
3406 "不应生成 DELETE FROM: {}",
3407 sql
3408 );
3409 assert!(
3410 sql.contains("`deleted_at` = NOW()"),
3411 "应设置 deleted_at = NOW(): {}",
3412 sql
3413 );
3414 assert!(
3416 sql.contains("`deleted_at` IS NULL"),
3417 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3418 sql
3419 );
3420 Ok(())
3421 }
3422
3423 #[test]
3427 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3428 let dialect = get_dialect(DbType::MySQL)?;
3429 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3430 .table("soft_users")
3431 .where_eq("id", Value::I64(99))
3432 .build_force_delete();
3433 assert!(
3435 sql.starts_with("DELETE FROM"),
3436 "build_force_delete 应生成 DELETE FROM,实际: {}",
3437 sql
3438 );
3439 assert!(
3441 !sql.contains("`deleted_at` IS NULL"),
3442 "物理删除不应追加软删除过滤: {}",
3443 sql
3444 );
3445 Ok(())
3446 }
3447
3448 #[test]
3450 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3451 let dialect = get_dialect(DbType::MySQL)?;
3452 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3453 .table("soft_users")
3454 .where_eq("id", Value::I64(1))
3455 .build_select_with_params();
3456 assert!(
3457 sql.contains("`deleted_at` IS NULL"),
3458 "参数化版本也应自动追加软删除: {}",
3459 sql
3460 );
3461 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3462 assert_eq!(params[0], Value::I64(1));
3463 Ok(())
3464 }
3465
3466 #[test]
3468 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3469 let dialect = get_dialect(DbType::MySQL)?;
3470 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3471 .table("soft_users")
3472 .where_eq("id", Value::I64(7))
3473 .build_delete_with_params();
3474 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3475 assert!(
3476 sql.contains("`deleted_at` = NOW()"),
3477 "应设置 NOW(): {}",
3478 sql
3479 );
3480 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3481 Ok(())
3482 }
3483
3484 #[test]
3486 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3487 let dialect = get_dialect(DbType::MySQL)?;
3488 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3489 .table("soft_users")
3490 .where_eq("id", Value::I64(11))
3491 .build_force_delete_with_params();
3492 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3493 assert!(
3494 !sql.contains("`deleted_at` IS NULL"),
3495 "不应追加软删除过滤: {}",
3496 sql
3497 );
3498 assert_eq!(params.len(), 1);
3499 Ok(())
3500 }
3501
3502 #[test]
3506 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3507 let dialect = get_dialect(DbType::MySQL)?;
3508 let sql = QueryBuilder::<TestModel>::new(dialect)
3509 .table("users")
3510 .where_eq("id", Value::I64(1))
3511 .sql();
3512 assert!(
3513 !sql.contains("deleted_at"),
3514 "非软删除模型不应追加 deleted_at: {}",
3515 sql
3516 );
3517 let dialect = get_dialect(DbType::MySQL)?;
3519 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3520 .table("users")
3521 .where_eq("id", Value::I64(1))
3522 .sql_delete();
3523 assert!(
3524 del_sql.starts_with("DELETE FROM"),
3525 "非软删除模型 build_delete 应生成 DELETE: {}",
3526 del_sql
3527 );
3528 Ok(())
3529 }
3530
3531 #[test]
3533 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3534 let dialect = get_dialect(DbType::MySQL)?;
3535 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3536 .table("soft_users")
3537 .build_count();
3538 assert!(
3539 sql.contains("`deleted_at` IS NULL"),
3540 "build_count 也应追加软删除过滤: {}",
3541 sql
3542 );
3543 Ok(())
3544 }
3545
3546 #[test]
3552 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3553 let dialect = get_dialect(DbType::MySQL)?;
3554 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3555 .table("users")
3556 .where_eq("name", Value::String("alice".into()))
3557 .build_select_with_params();
3558 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3560 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3561 assert_eq!(params.len(), 1);
3562 assert_eq!(params[0], Value::String("alice".into()));
3563 Ok(())
3564 }
3565
3566 #[test]
3568 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3569 let dialect = get_dialect(DbType::MySQL)?;
3570 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3571 .table("users")
3572 .where_like("name", Value::String("%alice%".into()))
3573 .build_select_with_params();
3574 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3575 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3576 assert_eq!(params.len(), 1);
3577 Ok(())
3578 }
3579
3580 #[test]
3584 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3585 let dialect = get_dialect(DbType::MySQL)?;
3586 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3587 .table("users")
3588 .where_ne("status", Value::I64(0))
3589 .build_select_with_params();
3590 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3591 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3592 assert_eq!(params.len(), 1);
3593 assert_eq!(params[0], Value::I64(0));
3594 Ok(())
3595 }
3596
3597 #[test]
3601 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3602 let dialect = get_dialect(DbType::MySQL)?;
3603 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3604 .table("users")
3605 .where_ge("age", Value::I64(18))
3606 .build_select_with_params();
3607 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3608 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3609 assert_eq!(params.len(), 1);
3610 assert_eq!(params[0], Value::I64(18));
3611 Ok(())
3612 }
3613
3614 #[test]
3618 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3619 let dialect = get_dialect(DbType::MySQL)?;
3620 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3621 .table("users")
3622 .where_lt("score", Value::F64(60.0))
3623 .build_select_with_params();
3624 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3625 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3626 assert_eq!(params.len(), 1);
3627 assert_eq!(params[0], Value::F64(60.0));
3628 Ok(())
3629 }
3630
3631 #[test]
3635 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3636 let dialect = get_dialect(DbType::MySQL)?;
3637 let evil_input = "'; DROP TABLE users; --".to_string();
3638 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3639 .table("users")
3640 .where_eq("name", Value::String(evil_input.clone()))
3641 .build_select_with_params();
3642 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3644 assert_eq!(params.len(), 1);
3646 assert_eq!(params[0], Value::String(evil_input));
3647 assert_eq!(sql.matches('?').count(), 1);
3649 Ok(())
3650 }
3651
3652 #[test]
3654 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3655 let dialect = get_dialect(DbType::MySQL)?;
3656 let evil = "' OR '1'='1".to_string();
3657 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3658 .table("users")
3659 .where_eq("name", Value::String(evil.clone()))
3660 .build_select_with_params();
3661 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3662 assert_eq!(params.len(), 1);
3663 assert_eq!(params[0], Value::String(evil));
3664 Ok(())
3665 }
3666
3667 #[test]
3669 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3670 let dialect = get_dialect(DbType::MySQL)?;
3671 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3672 .table("users")
3673 .where_eq("name", Value::String("alice".into()))
3674 .where_gt("age", Value::I64(18))
3675 .where_le("score", Value::F64(99.5))
3676 .build_select_with_params();
3677 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3678 assert_eq!(params.len(), 3);
3679 assert_eq!(params[0], Value::String("alice".into()));
3681 assert_eq!(params[1], Value::I64(18));
3682 assert_eq!(params[2], Value::F64(99.5));
3683 Ok(())
3684 }
3685
3686 #[test]
3688 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3689 let dialect = get_dialect(DbType::MySQL)?;
3690 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3691 .table("users")
3692 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3693 .build_select_with_params();
3694 assert!(
3695 sql.contains("`id` IN (?, ?, ?)"),
3696 "应使用 3 个占位符: {}",
3697 sql
3698 );
3699 assert_eq!(params.len(), 3);
3700 Ok(())
3701 }
3702
3703 #[test]
3705 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3706 let dialect = get_dialect(DbType::MySQL)?;
3707 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3708 .table("users")
3709 .where_between("age", Value::I64(18), Value::I64(65))
3710 .build_select_with_params();
3711 assert!(
3712 sql.contains("`age` BETWEEN ? AND ?"),
3713 "应使用 2 个占位符: {}",
3714 sql
3715 );
3716 assert_eq!(params.len(), 2);
3717 assert_eq!(params[0], Value::I64(18));
3718 assert_eq!(params[1], Value::I64(65));
3719 Ok(())
3720 }
3721
3722 #[test]
3724 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3725 let dialect = get_dialect(DbType::MySQL)?;
3726 let mut data = std::collections::HashMap::new();
3727 data.insert("name".to_string(), Value::String("bob".into()));
3728 data.insert("age".to_string(), Value::I64(30));
3729 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3730 .table("users")
3731 .where_eq("id", Value::I64(99))
3732 .build_update_with_params(&data);
3733 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3735 assert_eq!(params.len(), 3);
3736 assert_eq!(params[2], Value::I64(99));
3739 Ok(())
3740 }
3741
3742 #[test]
3746 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3747 let dialect = get_dialect(DbType::MySQL)?;
3748 let sql = QueryBuilder::<TestModel>::new(dialect)
3749 .table("users")
3750 .where_eq("name", Value::String("alice".into()))
3751 .sql();
3752 assert!(
3754 sql.contains("`name` = "),
3755 "无参数版本应含 WHERE 条件: {}",
3756 sql
3757 );
3758 assert!(
3760 !sql.contains("`name` = ?"),
3761 "无参数版本不应使用 ? 占位符: {}",
3762 sql
3763 );
3764 Ok(())
3765 }
3766
3767 #[test]
3769 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3770 let dialect = get_dialect(DbType::MySQL)?;
3771 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3772 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3773 let builder =
3774 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3775 assert!(
3776 builder.is_soft_delete_disabled(),
3777 "without_soft_delete 后应反映禁用状态"
3778 );
3779 Ok(())
3780 }
3781
3782 struct TenantModel;
3786 impl Model for TenantModel {
3787 type PrimaryKey = i64;
3788
3789 fn table_name() -> &'static str {
3790 "orders"
3791 }
3792
3793 fn pk(&self) -> Self::PrimaryKey {
3794 1
3795 }
3796
3797 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3798
3799 fn tenant_field() -> Option<&'static str> {
3800 Some("tenant_id")
3801 }
3802 }
3803
3804 struct SoftDeleteAndTenantModel;
3806 impl Model for SoftDeleteAndTenantModel {
3807 type PrimaryKey = i64;
3808
3809 fn table_name() -> &'static str {
3810 "documents"
3811 }
3812
3813 fn pk(&self) -> Self::PrimaryKey {
3814 1
3815 }
3816
3817 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3818
3819 fn soft_delete_field() -> Option<&'static str> {
3820 Some("deleted_at")
3821 }
3822
3823 fn tenant_field() -> Option<&'static str> {
3824 Some("tenant_id")
3825 }
3826 }
3827
3828 #[test]
3832 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3833 let dialect = get_dialect(DbType::MySQL)?;
3834 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3835 .table("orders")
3836 .with_tenant_id(42)
3837 .build_select_with_params();
3838 assert!(
3839 sql.contains("`tenant_id` = ?"),
3840 "多租户模型应自动追加 tenant_id = ?: {}",
3841 sql
3842 );
3843 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3844 assert_eq!(params[0], Value::I64(42));
3845 Ok(())
3846 }
3847
3848 #[test]
3850 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3851 let dialect = get_dialect(DbType::MySQL)?;
3852 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3853 .table("orders")
3854 .with_tenant_id(7)
3855 .where_eq("status", Value::String("active".into()))
3856 .build_select_with_params();
3857 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3858 assert!(
3859 sql.contains("`tenant_id` = ?"),
3860 "租户条件应自动追加: {}",
3861 sql
3862 );
3863 assert_eq!(params.len(), 2, "应有 2 个参数");
3864 assert_eq!(params[0], Value::String("active".into()));
3866 assert_eq!(params[1], Value::I64(7));
3867 Ok(())
3868 }
3869
3870 #[test]
3874 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3875 let dialect = get_dialect(DbType::MySQL)?;
3876 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3877 .table("orders")
3878 .with_tenant_id(42)
3879 .without_tenant()
3880 .build_select_with_params();
3881 assert!(
3882 !sql.contains("`tenant_id` = ?"),
3883 "without_tenant 应禁用过滤: {}",
3884 sql
3885 );
3886 assert_eq!(params.len(), 0, "不应有租户参数");
3887 Ok(())
3888 }
3889
3890 #[test]
3894 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3895 let dialect = get_dialect(DbType::MySQL)?;
3896 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3897 .table("orders")
3898 .with_tenant_id(99)
3899 .where_eq("id", Value::I64(1))
3900 .build_delete_with_params();
3901 assert!(
3902 sql.contains("`tenant_id` = ?"),
3903 "删除应自动追加租户条件: {}",
3904 sql
3905 );
3906 assert_eq!(params.len(), 2);
3908 assert_eq!(params[0], Value::I64(1));
3909 assert_eq!(params[1], Value::I64(99));
3910 Ok(())
3911 }
3912
3913 #[test]
3915 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3916 let dialect = get_dialect(DbType::MySQL)?;
3917 let mut data = std::collections::HashMap::new();
3918 data.insert("status".to_string(), Value::String("shipped".into()));
3919 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3920 .table("orders")
3921 .with_tenant_id(5)
3922 .where_eq("id", Value::I64(10))
3923 .build_update_with_params(&data);
3924 assert!(
3925 sql.contains("`tenant_id` = ?"),
3926 "更新应自动追加租户条件: {}",
3927 sql
3928 );
3929 assert_eq!(params.len(), 3);
3931 assert_eq!(params[2], Value::I64(5));
3933 Ok(())
3934 }
3935
3936 #[test]
3938 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3939 let dialect = get_dialect(DbType::MySQL)?;
3940 let sql = QueryBuilder::<TenantModel>::new(dialect)
3941 .table("orders")
3942 .with_tenant_id(42)
3943 .build_count();
3944 assert!(
3945 sql.contains("`tenant_id` = 42"),
3946 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3947 sql
3948 );
3949 Ok(())
3950 }
3951
3952 #[test]
3956 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3957 let dialect = get_dialect(DbType::MySQL)?;
3958 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3960 .table("users")
3961 .with_tenant_id(42)
3962 .build_select_with_params();
3963 assert!(
3964 !sql.contains("tenant_id"),
3965 "非多租户模型不应追加 tenant_id: {}",
3966 sql
3967 );
3968 assert_eq!(params.len(), 0);
3969 Ok(())
3970 }
3971
3972 #[test]
3976 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
3977 let dialect = get_dialect(DbType::MySQL)?;
3978 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3979 .table("orders")
3980 .build_select_with_params();
3981 assert!(
3982 !sql.contains("tenant_id"),
3983 "未设置 tenant_id 时不应追加过滤: {}",
3984 sql
3985 );
3986 assert_eq!(params.len(), 0);
3987 Ok(())
3988 }
3989
3990 #[test]
3994 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
3995 let dialect = get_dialect(DbType::MySQL)?;
3996 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3997 .table("documents")
3998 .with_tenant_id(100)
3999 .where_eq("title", Value::String("report".into()))
4000 .build_select_with_params();
4001 assert!(
4003 sql.contains("`deleted_at` IS NULL"),
4004 "应追加软删除条件: {}",
4005 sql
4006 );
4007 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4009 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
4011 assert_eq!(params.len(), 2);
4013 assert_eq!(params[0], Value::String("report".into()));
4014 assert_eq!(params[1], Value::I64(100));
4015 Ok(())
4016 }
4017
4018 #[test]
4020 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
4021 let dialect = get_dialect(DbType::MySQL)?;
4022 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
4023 .table("documents")
4024 .with_tenant_id(100)
4025 .without_tenant()
4026 .without_soft_delete()
4027 .build_select_with_params();
4028 assert!(
4029 !sql.contains("`deleted_at` IS NULL"),
4030 "应禁用软删除: {}",
4031 sql
4032 );
4033 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
4034 assert_eq!(params.len(), 0);
4035 Ok(())
4036 }
4037
4038 #[test]
4040 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
4041 let dialect = get_dialect(DbType::MySQL)?;
4042 let builder = QueryBuilder::<TenantModel>::new(dialect);
4043 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
4044 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4045 .with_tenant_id(1)
4046 .without_tenant();
4047 assert!(
4048 builder.is_tenant_disabled(),
4049 "without_tenant 后应反映禁用状态"
4050 );
4051 Ok(())
4052 }
4053
4054 #[test]
4058 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
4059 let dialect = get_dialect(DbType::MySQL)?;
4060 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4061 .table("orders")
4062 .with_tenant_id(42)
4063 .where_eq("id", Value::I64(999))
4064 .build_force_delete_with_params();
4065 assert!(
4068 sql.contains("`tenant_id` = ?"),
4069 "物理删除应保留租户条件: {}",
4070 sql
4071 );
4072 assert_eq!(params.len(), 2);
4073 assert_eq!(params[0], Value::I64(999));
4074 assert_eq!(params[1], Value::I64(42));
4075 Ok(())
4076 }
4077
4078 #[cfg(feature = "multi-tenant-enhanced")]
4082 #[tokio::test]
4083 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4084 let ctx = crate::tenant_context::TenantContext::new(
4085 99,
4086 crate::tenant_context::IsolationStrategy::RowLevel,
4087 );
4088 ctx.scope(async {
4089 let (sql, params) =
4091 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4092 .table("orders")
4093 .with_tenant_id(42)
4094 .build_select_with_params();
4095 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4096 assert_eq!(params.len(), 1);
4097 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4098 })
4099 .await;
4100 Ok(())
4101 }
4102
4103 #[cfg(feature = "multi-tenant-enhanced")]
4105 #[tokio::test]
4106 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4107 let ctx = crate::tenant_context::TenantContext::new(
4108 77,
4109 crate::tenant_context::IsolationStrategy::RowLevel,
4110 );
4111 ctx.scope(async {
4112 let (sql, params) =
4113 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4114 .table("orders")
4115 .build_select_with_params();
4116 assert!(
4117 sql.contains("`tenant_id` = ?"),
4118 "应从上下文自动追加租户条件: {}",
4119 sql
4120 );
4121 assert_eq!(params.len(), 1);
4122 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4123 })
4124 .await;
4125 Ok(())
4126 }
4127
4128 #[cfg(feature = "multi-tenant-enhanced")]
4130 #[tokio::test]
4131 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4132 let ctx = crate::tenant_context::TenantContext::new(
4133 42,
4134 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4135 );
4136 ctx.scope(async {
4137 let (sql, _params) =
4138 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4139 .table("orders")
4140 .build_select_with_params();
4141 assert!(
4142 sql.contains("tenant_42_orders"),
4143 "Schema 隔离应重写表名: {}",
4144 sql
4145 );
4146 })
4147 .await;
4148 Ok(())
4149 }
4150
4151 #[cfg(feature = "multi-tenant-enhanced")]
4153 #[test]
4154 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4155 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4156 .table("orders")
4157 .build_select_with_params();
4158 assert!(
4160 !sql.contains("`tenant_id` = ?"),
4161 "未设置上下文不应追加租户条件: {}",
4162 sql
4163 );
4164 assert_eq!(params.len(), 0);
4165 Ok(())
4166 }
4167
4168 struct TcUsersTable;
4171 impl crate::typed::TypedTable for TcUsersTable {
4172 const NAME: &'static str = "users";
4173 }
4174 struct TcColId;
4175 impl crate::typed::TypedColumn for TcColId {
4176 const NAME: &'static str = "id";
4177 type Table = TcUsersTable;
4178 type RustType = i64;
4179 type SqlType = crate::typed_ast::Untyped;
4180 }
4181 struct TcColName;
4182 impl crate::typed::TypedColumn for TcColName {
4183 const NAME: &'static str = "name";
4184 type Table = TcUsersTable;
4185 type RustType = String;
4186 type SqlType = crate::typed_ast::Untyped;
4187 }
4188
4189 #[test]
4190 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4191 let dialect = get_dialect(DbType::MySQL)?;
4192 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4193 .where_eq_typed::<TcColId>(Value::I64(42))
4194 .build_select_with_params();
4195 assert!(sql.contains("`id` = ?"));
4196 assert_eq!(params[0], Value::I64(42));
4197 Ok(())
4198 }
4199
4200 #[test]
4201 fn test_order_by_typed() -> Result<(), crate::DbError> {
4202 let dialect = get_dialect(DbType::MySQL)?;
4203 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4204 .order_by_typed::<TcColName>()
4205 .build_select_with_params();
4206 assert!(sql.contains("ORDER BY"));
4207 assert!(sql.contains("`name`"));
4208 Ok(())
4209 }
4210
4211 #[test]
4212 fn test_select_typed() -> Result<(), crate::DbError> {
4213 let dialect = get_dialect(DbType::MySQL)?;
4214 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4215 .select_typed::<TcColId>()
4216 .select_typed::<TcColName>()
4217 .build_select_with_params();
4218 assert!(sql.contains("SELECT"));
4219 assert!(sql.contains("id"));
4221 assert!(sql.contains("name"));
4222 Ok(())
4223 }
4224
4225 #[test]
4226 fn test_where_null_typed() -> Result<(), crate::DbError> {
4227 let dialect = get_dialect(DbType::MySQL)?;
4228 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4229 .where_null_typed::<TcColName>()
4230 .build_select_with_params();
4231 assert!(sql.contains("`name` IS NULL"));
4232 Ok(())
4233 }
4234
4235 #[test]
4236 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4237 let dialect = get_dialect(DbType::MySQL)?;
4238 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4239 .where_not_null_typed::<TcColName>()
4240 .build_select_with_params();
4241 assert!(sql.contains("`name` IS NOT NULL"));
4242 Ok(())
4243 }
4244
4245 #[test]
4246 fn test_group_by_typed() -> Result<(), crate::DbError> {
4247 let dialect = get_dialect(DbType::MySQL)?;
4248 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4249 .group_by_typed::<TcColName>()
4250 .build_select_with_params();
4251 assert!(sql.contains("GROUP BY"));
4252 assert!(sql.contains("`name`"));
4253 Ok(())
4254 }
4255
4256 #[test]
4257 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4258 let dialect = get_dialect(DbType::MySQL)?;
4259 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4260 .where_gt_typed::<TcColId>(Value::I64(10))
4261 .build_select_with_params();
4262 assert!(sql.contains("`id` > ?"));
4263 assert_eq!(params[0], Value::I64(10));
4264 Ok(())
4265 }
4266
4267 #[test]
4270 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4271 let dialect = get_dialect(DbType::MySQL)?;
4272 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4273 .table("users")
4274 .where_eq("id", Value::I64(1))
4275 .lock_for_update()?
4276 .build_select_with_params();
4277 assert!(sql.contains("SELECT * FROM `users`"));
4278 assert!(sql.contains("WHERE `id` = ?"));
4279 assert!(sql.contains("FOR UPDATE"));
4280 assert_eq!(params.len(), 1);
4281 assert_eq!(params[0], Value::I64(1));
4282 Ok(())
4283 }
4284
4285 #[test]
4286 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4287 let dialect = get_dialect(DbType::MySQL)?;
4288 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4289 .table("users")
4290 .where_eq("id", Value::I64(1))
4291 .lock_shared()?
4292 .build_select_with_params();
4293 assert!(sql.contains("SELECT * FROM `users`"));
4294 assert!(sql.contains("WHERE `id` = ?"));
4295 assert!(sql.contains("LOCK IN SHARE MODE"));
4296 assert_eq!(params.len(), 1);
4297 assert_eq!(params[0], Value::I64(1));
4298 Ok(())
4299 }
4300
4301 #[test]
4302 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4303 let dialect = get_dialect(DbType::PostgreSQL)?;
4304 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4305 .table("users")
4306 .where_eq("id", Value::I64(1))
4307 .lock_for_update()?
4308 .build_select_with_params();
4309 assert!(sql.contains("SELECT * FROM \"users\""));
4310 assert!(sql.contains("WHERE \"id\" = ?"));
4311 assert!(sql.contains("FOR UPDATE"));
4312 assert_eq!(params.len(), 1);
4313 assert_eq!(params[0], Value::I64(1));
4314 Ok(())
4315 }
4316
4317 #[test]
4318 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4319 let dialect = get_dialect(DbType::PostgreSQL)?;
4320 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4321 .table("users")
4322 .where_eq("id", Value::I64(1))
4323 .lock_shared()?
4324 .build_select_with_params();
4325 assert!(sql.contains("SELECT * FROM \"users\""));
4326 assert!(sql.contains("WHERE \"id\" = ?"));
4327 assert!(sql.contains("FOR SHARE"));
4328 assert_eq!(params.len(), 1);
4329 assert_eq!(params[0], Value::I64(1));
4330 Ok(())
4331 }
4332
4333 #[test]
4334 fn test_lock_for_update_sqlite_should_fail() {
4335 let dialect = get_dialect(DbType::Sqlite).unwrap();
4336 let result = QueryBuilder::<TestModel>::new(dialect)
4337 .table("users")
4338 .where_eq("id", Value::I64(1))
4339 .lock_for_update();
4340 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4341 let err = result.err().unwrap();
4342 assert!(
4343 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4344 "错误信息应说明不支持行锁"
4345 );
4346 }
4347
4348 #[test]
4349 fn test_lock_shared_sqlite_should_fail() {
4350 let dialect = get_dialect(DbType::Sqlite).unwrap();
4351 let result = QueryBuilder::<TestModel>::new(dialect)
4352 .table("users")
4353 .where_eq("id", Value::I64(1))
4354 .lock_shared();
4355 assert!(result.is_err(), "SQLite 不应支持共享锁");
4356 let err = result.err().unwrap();
4357 assert!(
4358 format!("{:?}", err).contains("Shared lock is not supported"),
4359 "错误信息应说明不支持共享锁"
4360 );
4361 }
4362
4363 #[test]
4364 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4365 let dialect = get_dialect(DbType::MySQL)?;
4366 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4367 .table("users")
4368 .where_eq("status", Value::String("active".into()))
4369 .limit(10)
4370 .offset(20)
4371 .lock_for_update()?
4372 .build_select_with_params();
4373 assert!(sql.contains("WHERE `status` = ?"));
4374 assert!(sql.contains("LIMIT 10"));
4375 assert!(sql.contains("OFFSET 20"));
4376 assert!(sql.contains("FOR UPDATE"));
4377 assert_eq!(params.len(), 1);
4378 Ok(())
4379 }
4380
4381 #[test]
4384 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4385 let dialect = get_dialect(DbType::MySQL)?;
4386 let mut data = std::collections::HashMap::new();
4387 data.insert("name".to_string(), Value::String("Alice".into()));
4388 data.insert("age".to_string(), Value::I64(30));
4389
4390 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4391 .table("users")
4392 .insert_or_ignore()
4393 .build_insert_with_params(&data);
4394 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4395 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4397 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4398 assert!(sql.contains("VALUES (?, ?)"));
4399 assert_eq!(params.len(), 2);
4400 Ok(())
4401 }
4402
4403 #[test]
4404 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4405 let dialect = get_dialect(DbType::PostgreSQL)?;
4406 let mut data = std::collections::HashMap::new();
4407 data.insert("name".to_string(), Value::String("Bob".into()));
4408
4409 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4410 .table("users")
4411 .insert_or_ignore()
4412 .build_insert_with_params(&data);
4413 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4414 assert!(sql.contains("(\"name\")"));
4415 assert!(sql.contains("VALUES (?)"));
4416 assert_eq!(params.len(), 1);
4417 assert_eq!(params[0], Value::String("Bob".into()));
4418 Ok(())
4419 }
4420
4421 #[test]
4422 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4423 let dialect = get_dialect(DbType::Sqlite)?;
4424 let mut data = std::collections::HashMap::new();
4425 data.insert("name".to_string(), Value::String("Charlie".into()));
4426
4427 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4428 .table("users")
4429 .insert_or_ignore()
4430 .build_insert_with_params(&data);
4431 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4432 assert!(sql.contains("(\"name\")"));
4433 assert!(sql.contains("VALUES (?)"));
4434 assert_eq!(params.len(), 1);
4435 assert_eq!(params[0], Value::String("Charlie".into()));
4436 Ok(())
4437 }
4438
4439 #[test]
4440 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4441 let dialect = get_dialect(DbType::MySQL)?;
4442 let mut data = std::collections::HashMap::new();
4443 data.insert("name".to_string(), Value::String("Dave".into()));
4444
4445 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4446 .table("users")
4447 .build_insert_with_params(&data);
4448 assert!(sql.contains("INSERT INTO `users`"));
4449 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4450 assert_eq!(params.len(), 1);
4451 assert_eq!(params[0], Value::String("Dave".into()));
4452 Ok(())
4453 }
4454
4455 #[test]
4456 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4457 let dialect = get_dialect(DbType::MySQL)?;
4458 let data = std::collections::HashMap::new();
4459
4460 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4461 .table("users")
4462 .insert_or_ignore()
4463 .build_insert_with_params(&data);
4464 assert!(sql.is_empty(), "空数据应返回空 SQL");
4465 assert!(params.is_empty());
4466 Ok(())
4467 }
4468
4469 #[test]
4472 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4473 let mysql = get_dialect(DbType::MySQL)?;
4474 let pg = get_dialect(DbType::PostgreSQL)?;
4475 let sqlite = get_dialect(DbType::Sqlite)?;
4476 let clickhouse = get_dialect(DbType::ClickHouse)?;
4477 let duckdb = get_dialect(DbType::DuckDB)?;
4478
4479 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4480 assert!(
4481 pg.supports_lock_for_update(),
4482 "PostgreSQL 应支持 FOR UPDATE"
4483 );
4484 assert!(
4485 !sqlite.supports_lock_for_update(),
4486 "SQLite 不应支持 FOR UPDATE"
4487 );
4488 assert!(
4489 !clickhouse.supports_lock_for_update(),
4490 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4491 );
4492 assert!(
4493 !duckdb.supports_lock_for_update(),
4494 "DuckDB 不应支持 FOR UPDATE"
4495 );
4496 Ok(())
4497 }
4498
4499 #[test]
4500 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4501 let mysql = get_dialect(DbType::MySQL)?;
4502 let pg = get_dialect(DbType::PostgreSQL)?;
4503 let sqlite = get_dialect(DbType::Sqlite)?;
4504 let clickhouse = get_dialect(DbType::ClickHouse)?;
4505 let duckdb = get_dialect(DbType::DuckDB)?;
4506
4507 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4508 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4509 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4510 assert!(
4511 !clickhouse.supports_lock_shared(),
4512 "ClickHouse 是列式 OLAP,不应支持共享锁"
4513 );
4514 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4515 Ok(())
4516 }
4517
4518 #[test]
4519 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4520 let dialect = get_dialect(DbType::MySQL)?;
4521
4522 let builder = QueryBuilder::<TestModel>::new(dialect);
4524 assert!(builder.get_lock_type().is_none(), "默认无锁");
4525 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4526
4527 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4529 .table("users")
4530 .lock_for_update()?;
4531 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4532
4533 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4535 .table("users")
4536 .insert_or_ignore();
4537 assert!(builder.is_insert_or_ignore());
4538
4539 Ok(())
4540 }
4541
4542 struct TestModelWithColumns;
4543 impl Model for TestModelWithColumns {
4544 type PrimaryKey = i64;
4545 fn table_name() -> &'static str {
4546 "test_with_cols"
4547 }
4548 fn pk(&self) -> Self::PrimaryKey {
4549 0
4550 }
4551 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4552 }
4553 impl crate::model::ModelExt for TestModelWithColumns {
4554 fn columns() -> Vec<&'static str> {
4555 vec!["id", "name", "email", "avatar", "blob_data"]
4556 }
4557 fn fillable() -> Vec<&'static str> {
4558 vec!["name", "email", "avatar", "blob_data"]
4559 }
4560 fn guarded() -> Vec<&'static str> {
4561 vec!["id"]
4562 }
4563 fn hidden() -> Vec<&'static str> {
4564 vec!["blob_data"]
4565 }
4566 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4567 std::collections::HashMap::new()
4568 }
4569 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4570 fn to_json(&self) -> serde_json::Value {
4571 serde_json::Value::Null
4572 }
4573 }
4574
4575 #[test]
4576 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4577 let dialect = get_dialect(DbType::MySQL)?;
4578 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4579 .table("users")
4580 .select_exclude(&["avatar", "blob_data"])?;
4581 let sql = builder.sql();
4582 assert!(sql.contains("id"));
4583 assert!(sql.contains("name"));
4584 assert!(sql.contains("email"));
4585 assert!(!sql.contains("avatar"));
4586 assert!(!sql.contains("blob_data"));
4587 Ok(())
4588 }
4589
4590 #[test]
4591 fn test_select_exclude_nonexistent_field() {
4592 let dialect = get_dialect(DbType::MySQL).unwrap();
4593 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4594 .table("users")
4595 .select_exclude(&["nonexistent"]);
4596 assert!(result.is_err());
4597 }
4598
4599 #[test]
4600 fn test_select_exclude_all_fields() {
4601 let dialect = get_dialect(DbType::MySQL).unwrap();
4602 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4603 .table("users")
4604 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4605 assert!(result.is_err());
4606 let err = result.unwrap_err();
4607 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4608 }
4609
4610 #[tokio::test]
4613 async fn test_execute_with_cache_ttl_hit() {
4614 use crate::l2_cache::L2Cache;
4615 use std::sync::atomic::{AtomicU64, Ordering};
4616 use std::sync::Arc;
4617 use std::time::Duration;
4618
4619 let dialect = get_dialect(DbType::MySQL).unwrap();
4620 let cache = L2Cache::new();
4621 let qb = QueryBuilder::<TestModel>::new(dialect)
4622 .table("users")
4623 .where_eq("status", Value::I64(1))
4624 .cache_ttl(Duration::from_secs(300));
4625
4626 let call_count = Arc::new(AtomicU64::new(0));
4627 let cc1 = Arc::clone(&call_count);
4628 let rows1 = qb
4629 .execute_with_cache(&cache, "users", || async move {
4630 cc1.fetch_add(1, Ordering::SeqCst);
4631 let mut row = std::collections::HashMap::new();
4632 row.insert("id".to_string(), Value::I64(1));
4633 Ok(vec![row])
4634 })
4635 .await
4636 .unwrap();
4637 assert_eq!(rows1.len(), 1);
4638 assert_eq!(call_count.load(Ordering::SeqCst), 1);
4639
4640 let cc2 = Arc::clone(&call_count);
4641 let rows2 = qb
4642 .execute_with_cache(&cache, "users", || async move {
4643 cc2.fetch_add(1, Ordering::SeqCst);
4644 let mut row = std::collections::HashMap::new();
4645 row.insert("id".to_string(), Value::I64(2));
4646 Ok(vec![row])
4647 })
4648 .await
4649 .unwrap();
4650 assert_eq!(rows2.len(), 1);
4651 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
4652 assert_eq!(
4653 call_count.load(Ordering::SeqCst),
4654 1,
4655 "second call should hit cache, not loader"
4656 );
4657 }
4658
4659 #[tokio::test]
4660 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
4661 use crate::l2_cache::L2Cache;
4662 use std::sync::atomic::{AtomicU64, Ordering};
4663 use std::sync::Arc;
4664
4665 let dialect = get_dialect(DbType::MySQL).unwrap();
4666 let cache = L2Cache::new();
4667 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
4668
4669 let call_count = Arc::new(AtomicU64::new(0));
4670 let cc1 = Arc::clone(&call_count);
4671 let rows1 = qb
4672 .execute_with_cache(&cache, "users", || async move {
4673 cc1.fetch_add(1, Ordering::SeqCst);
4674 Ok(vec![])
4675 })
4676 .await
4677 .unwrap();
4678 assert!(rows1.is_empty());
4679
4680 let cc2 = Arc::clone(&call_count);
4681 let rows2 = qb
4682 .execute_with_cache(&cache, "users", || async move {
4683 cc2.fetch_add(1, Ordering::SeqCst);
4684 Ok(vec![])
4685 })
4686 .await
4687 .unwrap();
4688 assert!(rows2.is_empty());
4689 assert_eq!(
4690 call_count.load(Ordering::SeqCst),
4691 2,
4692 "without ttl, loader should be called each time"
4693 );
4694 }
4695}