1use crate::db_type::DbType;
27use crate::dialect::Dialect;
28use crate::dialect::LockType;
29use crate::model::Model;
30use crate::typed::TypedColumn;
31use crate::value::Value;
32use std::fmt;
33use std::fmt::Write as _;
34use std::time::Duration;
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
748 pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
762 self.where_conditions
763 .push(WhereCondition::Eq(field.into(), value));
764 self
765 }
766
767 #[cfg(feature = "type-safe-columns")]
772 pub fn where_eq_col<T: crate::column::Schema>(
773 mut self,
774 col: crate::column::Column<T>,
775 value: Value,
776 ) -> Self {
777 self.where_conditions
778 .push(WhereCondition::Eq(col.name().to_string(), value));
779 self
780 }
781
782 #[cfg(feature = "type-safe-columns")]
787 pub fn where_expr<E: crate::typed_ast::TypedExpression<SqlType = crate::typed_ast::Bool>>(
788 mut self,
789 expr: E,
790 ) -> Self {
791 let (sql, params) = expr.to_sql(&*self.dialect);
792 let values: Vec<Value> = params.into_iter().map(Value::String).collect();
793 self.where_conditions
794 .push(WhereCondition::TypedExpr(sql, values));
795 self
796 }
797
798 pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
800 self.where_conditions
801 .push(WhereCondition::Ne(field.into(), value));
802 self
803 }
804
805 pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
807 self.where_conditions
808 .push(WhereCondition::Gt(field.into(), value));
809 self
810 }
811
812 pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
814 self.where_conditions
815 .push(WhereCondition::Ge(field.into(), value));
816 self
817 }
818
819 pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
821 self.where_conditions
822 .push(WhereCondition::Lt(field.into(), value));
823 self
824 }
825
826 pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
828 self.where_conditions
829 .push(WhereCondition::Le(field.into(), value));
830 self
831 }
832
833 pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
845 self.where_conditions
846 .push(WhereCondition::Like(field.into(), pattern));
847 self
848 }
849
850 pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
854 self.where_conditions
855 .push(WhereCondition::OrEq(field.into(), value));
856 self
857 }
858
859 pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
861 self.where_conditions
862 .push(WhereCondition::OrNe(field.into(), value));
863 self
864 }
865
866 pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
868 self.where_conditions
869 .push(WhereCondition::OrGt(field.into(), value));
870 self
871 }
872
873 pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
875 self.where_conditions
876 .push(WhereCondition::OrGe(field.into(), value));
877 self
878 }
879
880 pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
882 self.where_conditions
883 .push(WhereCondition::OrLt(field.into(), value));
884 self
885 }
886
887 pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
889 self.where_conditions
890 .push(WhereCondition::OrLe(field.into(), value));
891 self
892 }
893
894 pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
896 self.where_conditions
897 .push(WhereCondition::OrLike(field.into(), pattern));
898 self
899 }
900
901 pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
903 self.where_conditions
904 .push(WhereCondition::In(field.into(), values));
905 self
906 }
907
908 pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
910 self.where_conditions
911 .push(WhereCondition::NotIn(field.into(), values));
912 self
913 }
914
915 pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
917 self.where_conditions
918 .push(WhereCondition::Between(field.into(), start, end));
919 self
920 }
921
922 pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
924 self.where_conditions
925 .push(WhereCondition::NotBetween(field.into(), start, end));
926 self
927 }
928
929 pub fn where_null(mut self, field: impl Into<String>) -> Self {
931 self.where_conditions
932 .push(WhereCondition::Null(field.into()));
933 self
934 }
935
936 pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
938 self.where_conditions
939 .push(WhereCondition::NotNull(field.into()));
940 self
941 }
942
943 pub fn order_by(mut self, field: impl Into<String>) -> Self {
945 self.order_by.push(OrderClause {
946 field: field.into(),
947 direction: OrderDirection::Asc,
948 });
949 self
950 }
951
952 pub fn order_desc(mut self, field: impl Into<String>) -> Self {
954 self.order_by.push(OrderClause {
955 field: field.into(),
956 direction: OrderDirection::Desc,
957 });
958 self
959 }
960
961 pub fn group_by(mut self, field: impl Into<String>) -> Self {
963 self.group_by.push(field.into());
964 self
965 }
966
967 pub fn where_eq_typed<C: TypedColumn>(mut self, value: Value) -> Self {
986 self.where_conditions
987 .push(WhereCondition::Eq(C::NAME.to_string(), value));
988 self
989 }
990
991 pub fn where_ne_typed<C: TypedColumn>(mut self, value: Value) -> Self {
993 self.where_conditions
994 .push(WhereCondition::Ne(C::NAME.to_string(), value));
995 self
996 }
997
998 pub fn where_gt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1000 self.where_conditions
1001 .push(WhereCondition::Gt(C::NAME.to_string(), value));
1002 self
1003 }
1004
1005 pub fn where_ge_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1007 self.where_conditions
1008 .push(WhereCondition::Ge(C::NAME.to_string(), value));
1009 self
1010 }
1011
1012 pub fn where_lt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1014 self.where_conditions
1015 .push(WhereCondition::Lt(C::NAME.to_string(), value));
1016 self
1017 }
1018
1019 pub fn where_le_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1021 self.where_conditions
1022 .push(WhereCondition::Le(C::NAME.to_string(), value));
1023 self
1024 }
1025
1026 pub fn where_null_typed<C: TypedColumn>(mut self) -> Self {
1028 self.where_conditions
1029 .push(WhereCondition::Null(C::NAME.to_string()));
1030 self
1031 }
1032
1033 pub fn where_not_null_typed<C: TypedColumn>(mut self) -> Self {
1035 self.where_conditions
1036 .push(WhereCondition::NotNull(C::NAME.to_string()));
1037 self
1038 }
1039
1040 pub fn order_by_typed<C: TypedColumn>(mut self) -> Self {
1042 self.order_by.push(OrderClause {
1043 field: C::NAME.to_string(),
1044 direction: OrderDirection::Asc,
1045 });
1046 self
1047 }
1048
1049 pub fn order_desc_typed<C: TypedColumn>(mut self) -> Self {
1051 self.order_by.push(OrderClause {
1052 field: C::NAME.to_string(),
1053 direction: OrderDirection::Desc,
1054 });
1055 self
1056 }
1057
1058 pub fn group_by_typed<C: TypedColumn>(mut self) -> Self {
1060 self.group_by.push(C::NAME.to_string());
1061 self
1062 }
1063
1064 pub fn select_typed<C: TypedColumn>(mut self) -> Self {
1068 self.select_columns.push(C::NAME.to_string());
1069 self
1070 }
1071
1072 pub fn select_typed_cols<C: TypedColumn, const N: usize>(mut self) -> Self {
1074 self.select_columns.push(C::NAME.to_string());
1077 self
1078 }
1079
1080 pub fn having(
1104 mut self,
1105 agg: AggExpr,
1106 op: HavingOp,
1107 value: Value,
1108 ) -> Result<Self, crate::DbError> {
1109 agg.validate()?;
1110 self.having_conditions
1111 .push(WhereCondition::Having(agg, op, value));
1112 Ok(self)
1113 }
1114
1115 pub fn limit(mut self, limit: usize) -> Self {
1117 self.limit_value = Some(limit);
1118 self
1119 }
1120
1121 pub fn offset(mut self, offset: usize) -> Self {
1123 self.offset_value = Some(offset);
1124 self
1125 }
1126
1127 pub fn page(mut self, page: usize, page_size: usize) -> Self {
1129 self.limit_value = Some(page_size);
1130 self.offset_value = Some((page.saturating_sub(1)) * page_size);
1131 self
1132 }
1133
1134 pub fn keyset_after(
1180 mut self,
1181 field: impl Into<String>,
1182 cursor_value: Value,
1183 page_size: usize,
1184 ) -> Self {
1185 let field_str = field.into();
1186 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1188 existing.direction = OrderDirection::Asc;
1189 } else {
1190 self.order_by.push(OrderClause {
1191 field: field_str.clone(),
1192 direction: OrderDirection::Asc,
1193 });
1194 }
1195 self.limit_value = Some(page_size);
1196 self.offset_value = None;
1198 self.keyset_cursor = Some(KeysetCursor {
1199 field: field_str,
1200 value: cursor_value,
1201 direction: KeysetDirection::After,
1202 });
1203 self
1204 }
1205
1206 pub fn keyset_before(
1252 mut self,
1253 field: impl Into<String>,
1254 cursor_value: Value,
1255 page_size: usize,
1256 ) -> Self {
1257 let field_str = field.into();
1258 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1260 existing.direction = OrderDirection::Desc;
1261 } else {
1262 self.order_by.push(OrderClause {
1263 field: field_str.clone(),
1264 direction: OrderDirection::Desc,
1265 });
1266 }
1267 self.limit_value = Some(page_size);
1268 self.offset_value = None;
1269 self.keyset_cursor = Some(KeysetCursor {
1270 field: field_str,
1271 value: cursor_value,
1272 direction: KeysetDirection::Before,
1273 });
1274 self
1275 }
1276
1277 pub fn join_inner(
1279 mut self,
1280 table: impl Into<String>,
1281 on_left: impl Into<String>,
1282 on_right: impl Into<String>,
1283 ) -> Self {
1284 self.joins.push(JoinClause::Inner(
1285 table.into(),
1286 on_left.into(),
1287 on_right.into(),
1288 ));
1289 self
1290 }
1291
1292 pub fn join_left(
1294 mut self,
1295 table: impl Into<String>,
1296 on_left: impl Into<String>,
1297 on_right: impl Into<String>,
1298 ) -> Self {
1299 self.joins.push(JoinClause::Left(
1300 table.into(),
1301 on_left.into(),
1302 on_right.into(),
1303 ));
1304 self
1305 }
1306
1307 pub fn join_right(
1309 mut self,
1310 table: impl Into<String>,
1311 on_left: impl Into<String>,
1312 on_right: impl Into<String>,
1313 ) -> Self {
1314 self.joins.push(JoinClause::Right(
1315 table.into(),
1316 on_left.into(),
1317 on_right.into(),
1318 ));
1319 self
1320 }
1321
1322 pub fn join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1342 let def = relation.def();
1343 let join_kind = def.kind.default_join_type();
1344 self.joins.push(JoinClause::Relation(
1345 join_kind,
1346 def.from_entity.to_string(),
1347 def.from_key.to_string(),
1348 def.to_entity.to_string(),
1349 def.to_key.to_string(),
1350 ));
1351 self
1352 }
1353
1354 pub fn left_join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1358 let def = relation.def();
1359 self.joins.push(JoinClause::Relation(
1360 crate::relation_trait::JoinKind::Left,
1361 def.from_entity.to_string(),
1362 def.from_key.to_string(),
1363 def.to_entity.to_string(),
1364 def.to_key.to_string(),
1365 ));
1366 self
1367 }
1368
1369 pub fn select_only(mut self) -> Self {
1385 self.select_mode = crate::partial_model::SelectMode::Partial;
1386 self.select_columns.clear();
1387 self
1388 }
1389
1390 pub fn column(mut self, column: impl Into<String>) -> Self {
1394 self.select_columns.push(column.into());
1395 self
1396 }
1397
1398 pub fn columns(mut self, cols: Vec<impl Into<String>>) -> Self {
1400 self.select_columns.extend(cols.into_iter().map(Into::into));
1401 self
1402 }
1403
1404 pub fn column_as(mut self, expr: crate::partial_model::Expr, alias: impl Into<String>) -> Self {
1420 self.select_columns.push(expr.render_as(&alias.into()));
1421 self
1422 }
1423
1424 #[tracing::instrument(skip(self), fields(op = "select"))]
1457 pub fn build_select(&self) -> String {
1458 let table = self
1459 .table
1460 .clone()
1461 .unwrap_or_else(|| M::table_name().to_string());
1462
1463 let columns = if self.select_columns.is_empty() {
1464 "*".to_string()
1465 } else {
1466 self.select_columns.join(", ")
1467 };
1468
1469 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1470 "SELECT {} FROM {}",
1471 columns,
1472 self.dialect.quote(&table)
1473 ));
1474
1475 for join in &self.joins {
1476 match join {
1477 JoinClause::Inner(t, l, r) => {
1478 let _ = write!(
1479 sql,
1480 " INNER JOIN {} ON {} = {}",
1481 self.dialect.quote(t),
1482 self.dialect.quote(l),
1483 self.dialect.quote(r)
1484 );
1485 }
1486 JoinClause::Left(t, l, r) => {
1487 let _ = write!(
1488 sql,
1489 " LEFT JOIN {} ON {} = {}",
1490 self.dialect.quote(t),
1491 self.dialect.quote(l),
1492 self.dialect.quote(r)
1493 );
1494 }
1495 JoinClause::Right(t, l, r) => {
1496 let _ = write!(
1497 sql,
1498 " RIGHT JOIN {} ON {} = {}",
1499 self.dialect.quote(t),
1500 self.dialect.quote(l),
1501 self.dialect.quote(r)
1502 );
1503 }
1504 JoinClause::Cross(t, on) => {
1505 let _ = write!(
1506 sql,
1507 " CROSS JOIN {} ON {}",
1508 self.dialect.quote(t),
1509 self.dialect.quote(on)
1510 );
1511 }
1512 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1513 let _ = write!(
1514 sql,
1515 " {} {} ON {}.{} = {}.{}",
1516 kind.as_sql(),
1517 self.dialect.quote(tt),
1518 self.dialect.quote(ft),
1519 self.dialect.quote(fk),
1520 self.dialect.quote(tt),
1521 self.dialect.quote(tk)
1522 );
1523 }
1524 }
1525 }
1526
1527 let where_clause = self.build_where_clause();
1529 if !where_clause.is_empty() {
1530 sql.push_str(&where_clause);
1531 }
1532
1533 if !self.group_by.is_empty() {
1534 let cols: Vec<String> = self
1535 .group_by
1536 .iter()
1537 .map(|c| self.dialect.quote(c))
1538 .collect();
1539 sql.push_str(" GROUP BY ");
1540 sql.push_str(&cols.join(", "));
1541 }
1542
1543 if !self.having_conditions.is_empty() {
1544 sql.push_str(" HAVING ");
1545 for (i, cond) in self.having_conditions.iter().enumerate() {
1546 if i > 0 {
1547 sql.push_str(" AND ");
1548 }
1549 match cond {
1550 WhereCondition::And(c) => sql.push_str(c),
1551 WhereCondition::Having(agg, op, value) => {
1553 sql.push_str(&format!(
1554 "{} {} {}",
1555 agg.render(&*self.dialect),
1556 op.as_sql(),
1557 value.to_param_with_dialect(&*self.dialect)
1558 ));
1559 }
1560 _ => {}
1561 }
1562 }
1563 }
1564
1565 if !self.order_by.is_empty() {
1566 let order_cols: Vec<String> = self
1567 .order_by
1568 .iter()
1569 .map(|o| {
1570 let dir = match o.direction {
1571 OrderDirection::Asc => " ASC",
1572 OrderDirection::Desc => " DESC",
1573 };
1574 format!("{}{}", self.dialect.quote(&o.field), dir)
1575 })
1576 .collect();
1577 sql.push_str(" ORDER BY ");
1578 sql.push_str(&order_cols.join(", "));
1579 }
1580
1581 if let Some(limit) = self.limit_value {
1582 let _ = write!(sql, " LIMIT {}", limit);
1583 }
1584
1585 if let Some(offset) = self.offset_value {
1586 let _ = write!(sql, " OFFSET {}", offset);
1587 }
1588
1589 sql.into_string()
1590 }
1591
1592 fn build_where_clause(&self) -> String {
1598 self.build_where_clause_with_options(true)
1599 }
1600
1601 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1609 let soft_delete_cond = if include_soft_delete {
1611 self.build_soft_delete_condition()
1612 } else {
1613 None
1614 };
1615
1616 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1618 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1620 });
1621
1622 if self.where_conditions.is_empty()
1624 && soft_delete_cond.is_none()
1625 && tenant_cond.is_none()
1626 && self.keyset_cursor.is_none()
1627 {
1628 return String::new();
1629 }
1630
1631 let mut conditions: Vec<String> = self
1633 .where_conditions
1634 .iter()
1635 .map(|cond| match cond {
1636 WhereCondition::And(c) => c.clone(),
1637 WhereCondition::Or(c) => format!("OR {}", c),
1638 WhereCondition::Eq(f, v) => format!(
1640 "{} = {}",
1641 self.dialect.quote(f),
1642 v.to_param_with_dialect(&*self.dialect)
1643 ),
1644 WhereCondition::Ne(f, v) => format!(
1645 "{} != {}",
1646 self.dialect.quote(f),
1647 v.to_param_with_dialect(&*self.dialect)
1648 ),
1649 WhereCondition::Gt(f, v) => format!(
1650 "{} > {}",
1651 self.dialect.quote(f),
1652 v.to_param_with_dialect(&*self.dialect)
1653 ),
1654 WhereCondition::Ge(f, v) => format!(
1655 "{} >= {}",
1656 self.dialect.quote(f),
1657 v.to_param_with_dialect(&*self.dialect)
1658 ),
1659 WhereCondition::Lt(f, v) => format!(
1660 "{} < {}",
1661 self.dialect.quote(f),
1662 v.to_param_with_dialect(&*self.dialect)
1663 ),
1664 WhereCondition::Le(f, v) => format!(
1665 "{} <= {}",
1666 self.dialect.quote(f),
1667 v.to_param_with_dialect(&*self.dialect)
1668 ),
1669 WhereCondition::Like(f, v) => format!(
1670 "{} LIKE {}",
1671 self.dialect.quote(f),
1672 v.to_param_with_dialect(&*self.dialect)
1673 ),
1674 WhereCondition::OrEq(f, v) => format!(
1675 "OR {} = {}",
1676 self.dialect.quote(f),
1677 v.to_param_with_dialect(&*self.dialect)
1678 ),
1679 WhereCondition::OrNe(f, v) => format!(
1680 "OR {} != {}",
1681 self.dialect.quote(f),
1682 v.to_param_with_dialect(&*self.dialect)
1683 ),
1684 WhereCondition::OrGt(f, v) => format!(
1685 "OR {} > {}",
1686 self.dialect.quote(f),
1687 v.to_param_with_dialect(&*self.dialect)
1688 ),
1689 WhereCondition::OrGe(f, v) => format!(
1690 "OR {} >= {}",
1691 self.dialect.quote(f),
1692 v.to_param_with_dialect(&*self.dialect)
1693 ),
1694 WhereCondition::OrLt(f, v) => format!(
1695 "OR {} < {}",
1696 self.dialect.quote(f),
1697 v.to_param_with_dialect(&*self.dialect)
1698 ),
1699 WhereCondition::OrLe(f, v) => format!(
1700 "OR {} <= {}",
1701 self.dialect.quote(f),
1702 v.to_param_with_dialect(&*self.dialect)
1703 ),
1704 WhereCondition::OrLike(f, v) => format!(
1705 "OR {} LIKE {}",
1706 self.dialect.quote(f),
1707 v.to_param_with_dialect(&*self.dialect)
1708 ),
1709 WhereCondition::In(f, vals) => {
1710 let vals_str: Vec<String> = vals
1712 .iter()
1713 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1714 .collect();
1715 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1716 }
1717 WhereCondition::NotIn(f, vals) => {
1718 let vals_str: Vec<String> = vals
1719 .iter()
1720 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1721 .collect();
1722 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1723 }
1724 WhereCondition::Between(f, start, end) => {
1725 format!(
1726 "{} BETWEEN {} AND {}",
1727 self.dialect.quote(f),
1728 start.to_param_with_dialect(&*self.dialect),
1729 end.to_param_with_dialect(&*self.dialect)
1730 )
1731 }
1732 WhereCondition::NotBetween(f, start, end) => {
1733 format!(
1734 "{} NOT BETWEEN {} AND {}",
1735 self.dialect.quote(f),
1736 start.to_param_with_dialect(&*self.dialect),
1737 end.to_param_with_dialect(&*self.dialect)
1738 )
1739 }
1740 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1741 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1742 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1743 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1744 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1745 WhereCondition::Having(..) => String::new(),
1747 })
1748 .collect();
1749
1750 if let Some(sd_cond) = soft_delete_cond {
1752 conditions.push(sd_cond);
1753 }
1754
1755 if let Some(t_cond) = tenant_cond {
1757 conditions.push(t_cond);
1758 }
1759
1760 if let Some(ref cursor) = self.keyset_cursor {
1762 let op = match cursor.direction {
1763 KeysetDirection::After => ">",
1764 KeysetDirection::Before => "<",
1765 };
1766 conditions.push(format!(
1767 "{} {} {}",
1768 self.dialect.quote(&cursor.field),
1769 op,
1770 cursor.value.to_param_with_dialect(&*self.dialect)
1771 ));
1772 }
1773
1774 if conditions.is_empty() {
1775 return String::new();
1776 }
1777
1778 let mut groups: Vec<Vec<String>> = Vec::new();
1781 let mut current_group: Vec<String> = Vec::new();
1782 for cond in conditions.iter() {
1783 if let Some(stripped) = cond.strip_prefix("OR ") {
1784 current_group.push(stripped.to_string());
1786 } else {
1787 if !current_group.is_empty() {
1789 groups.push(std::mem::take(&mut current_group));
1790 }
1791 current_group.push(cond.clone());
1792 }
1793 }
1794 if !current_group.is_empty() {
1795 groups.push(current_group);
1796 }
1797
1798 let group_strs: Vec<String> = groups
1799 .iter()
1800 .map(|g| {
1801 if g.len() == 1 {
1802 g[0].clone()
1803 } else {
1804 format!("({})", g.join(" OR "))
1805 }
1806 })
1807 .collect();
1808
1809 format!(" WHERE {}", group_strs.join(" AND "))
1811 }
1812
1813 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1814 pub fn build_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1816 let table = self
1817 .table
1818 .clone()
1819 .unwrap_or_else(|| M::table_name().to_string());
1820
1821 if data.is_empty() {
1822 return String::new();
1823 }
1824
1825 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1826 let values: Vec<String> = data
1828 .values()
1829 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1830 .collect();
1831
1832 crate::sql_buffer::SqlBuffer::from_str(&format!(
1833 "INSERT INTO {} ({}) VALUES ({})",
1834 self.dialect.quote(&table),
1835 columns.join(", "),
1836 values.join(", ")
1837 ))
1838 .into_string()
1839 }
1840
1841 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1842 pub fn build_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1844 let table = self
1845 .table
1846 .clone()
1847 .unwrap_or_else(|| M::table_name().to_string());
1848
1849 if data.is_empty() {
1850 return String::new();
1851 }
1852
1853 let set_clauses: Vec<String> = data
1854 .iter()
1855 .map(|(k, v)| {
1856 format!(
1857 "{} = {}",
1858 self.dialect.quote(k),
1859 v.to_param_with_dialect(&*self.dialect)
1860 )
1861 })
1862 .collect();
1863
1864 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1865 "UPDATE {} SET {}",
1866 self.dialect.quote(&table),
1867 set_clauses.join(", ")
1868 ));
1869
1870 sql.push_str(&self.build_where_clause());
1871 sql.into_string()
1872 }
1873
1874 #[tracing::instrument(skip(self), fields(op = "delete"))]
1885 pub fn build_delete(&self) -> String {
1886 let table = self
1887 .table
1888 .clone()
1889 .unwrap_or_else(|| M::table_name().to_string());
1890
1891 if let Some(field) = self.soft_delete_field() {
1893 let where_clause = self.build_where_clause();
1894 return format!(
1895 "UPDATE {} SET {} = NOW(){}",
1896 self.dialect.quote(&table),
1897 self.dialect.quote(field),
1898 where_clause
1899 );
1900 }
1901
1902 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1903 "DELETE FROM {}",
1904 self.dialect.quote(&table)
1905 ));
1906 sql.push_str(&self.build_where_clause());
1907 sql.into_string()
1908 }
1909
1910 pub fn build_force_delete(&self) -> String {
1920 let table = self
1921 .table
1922 .clone()
1923 .unwrap_or_else(|| M::table_name().to_string());
1924
1925 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1926 sql.push_str(&self.build_where_clause_with_options(false));
1928 sql
1929 }
1930
1931 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
1941 self.build_where_clause_with_params_options(true)
1943 }
1944
1945 fn build_where_clause_with_params_options(
1954 &self,
1955 include_soft_delete: bool,
1956 ) -> (String, Vec<Value>) {
1957 let soft_delete_cond = if include_soft_delete {
1959 self.build_soft_delete_condition()
1960 } else {
1961 None
1962 };
1963
1964 let tenant_cond = self.build_tenant_condition();
1966
1967 #[cfg(feature = "tenant-quota-rls-enhanced")]
1969 let rls_cond: Option<(String, Vec<Value>)> = {
1970 if let Some(ref enhancer) = self.rls_enhancer {
1971 if let Some(ref table) = self.table {
1972 if let Some(tid) = self.tenant_id_value() {
1973 match enhancer.enhance_query(table, &tid.to_string()) {
1974 Ok(Some(c)) => Some((c.sql_fragment, c.params)),
1975 Ok(None) => None,
1976 Err(_) => None,
1977 }
1978 } else {
1979 None
1980 }
1981 } else {
1982 None
1983 }
1984 } else {
1985 None
1986 }
1987 };
1988 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
1989 let rls_cond: Option<(String, Vec<Value>)> = None;
1990
1991 if self.where_conditions.is_empty()
1993 && soft_delete_cond.is_none()
1994 && tenant_cond.is_none()
1995 && self.keyset_cursor.is_none()
1996 && rls_cond.is_none()
1997 {
1998 return (String::new(), Vec::new());
1999 }
2000
2001 let mut params = Vec::new();
2002
2003 let mut conditions: Vec<String> = self
2004 .where_conditions
2005 .iter()
2006 .map(|cond| match cond {
2007 WhereCondition::And(c) => c.clone(),
2008 WhereCondition::Or(c) => format!("OR {}", c),
2009 WhereCondition::Eq(f, v) => {
2011 params.push(v.clone());
2012 format!("{} = ?", self.dialect.quote(f))
2013 }
2014 WhereCondition::Ne(f, v) => {
2015 params.push(v.clone());
2016 format!("{} != ?", self.dialect.quote(f))
2017 }
2018 WhereCondition::Gt(f, v) => {
2019 params.push(v.clone());
2020 format!("{} > ?", self.dialect.quote(f))
2021 }
2022 WhereCondition::Ge(f, v) => {
2023 params.push(v.clone());
2024 format!("{} >= ?", self.dialect.quote(f))
2025 }
2026 WhereCondition::Lt(f, v) => {
2027 params.push(v.clone());
2028 format!("{} < ?", self.dialect.quote(f))
2029 }
2030 WhereCondition::Le(f, v) => {
2031 params.push(v.clone());
2032 format!("{} <= ?", self.dialect.quote(f))
2033 }
2034 WhereCondition::Like(f, v) => {
2035 params.push(v.clone());
2036 format!("{} LIKE ?", self.dialect.quote(f))
2037 }
2038 WhereCondition::OrEq(f, v) => {
2039 params.push(v.clone());
2040 format!("OR {} = ?", self.dialect.quote(f))
2041 }
2042 WhereCondition::OrNe(f, v) => {
2043 params.push(v.clone());
2044 format!("OR {} != ?", self.dialect.quote(f))
2045 }
2046 WhereCondition::OrGt(f, v) => {
2047 params.push(v.clone());
2048 format!("OR {} > ?", self.dialect.quote(f))
2049 }
2050 WhereCondition::OrGe(f, v) => {
2051 params.push(v.clone());
2052 format!("OR {} >= ?", self.dialect.quote(f))
2053 }
2054 WhereCondition::OrLt(f, v) => {
2055 params.push(v.clone());
2056 format!("OR {} < ?", self.dialect.quote(f))
2057 }
2058 WhereCondition::OrLe(f, v) => {
2059 params.push(v.clone());
2060 format!("OR {} <= ?", self.dialect.quote(f))
2061 }
2062 WhereCondition::OrLike(f, v) => {
2063 params.push(v.clone());
2064 format!("OR {} LIKE ?", self.dialect.quote(f))
2065 }
2066 WhereCondition::In(f, vals) => {
2067 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2068 params.extend(vals.iter().cloned());
2069 format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
2070 }
2071 WhereCondition::NotIn(f, vals) => {
2072 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2073 params.extend(vals.iter().cloned());
2074 format!(
2075 "{} NOT IN ({})",
2076 self.dialect.quote(f),
2077 placeholders.join(", ")
2078 )
2079 }
2080 WhereCondition::Between(f, start, end) => {
2081 params.push(start.clone());
2082 params.push(end.clone());
2083 format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
2084 }
2085 WhereCondition::NotBetween(f, start, end) => {
2086 params.push(start.clone());
2087 params.push(end.clone());
2088 format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
2089 }
2090 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
2091 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
2092 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
2093 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
2094 WhereCondition::TypedExpr(sql, expr_params) => {
2095 params.extend(expr_params.iter().cloned());
2096 sql.clone()
2097 }
2098 WhereCondition::Having(..) => String::new(),
2100 })
2101 .collect();
2102
2103 if let Some(sd_cond) = soft_delete_cond {
2105 conditions.push(sd_cond);
2106 }
2107
2108 if let Some((t_sql, t_value)) = tenant_cond {
2110 conditions.push(t_sql);
2111 params.push(t_value);
2112 }
2113
2114 if let Some((rls_sql, rls_params)) = rls_cond {
2116 conditions.push(rls_sql);
2117 params.extend(rls_params);
2118 }
2119
2120 if let Some(ref cursor) = self.keyset_cursor {
2122 let op = match cursor.direction {
2123 KeysetDirection::After => ">",
2124 KeysetDirection::Before => "<",
2125 };
2126 conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
2127 params.push(cursor.value.clone());
2128 }
2129
2130 if conditions.is_empty() {
2131 return (String::new(), params);
2132 }
2133
2134 let mut groups: Vec<Vec<String>> = Vec::new();
2136 let mut current_group: Vec<String> = Vec::new();
2137 for cond in conditions.iter() {
2138 if let Some(stripped) = cond.strip_prefix("OR ") {
2139 current_group.push(stripped.to_string());
2140 } else {
2141 if !current_group.is_empty() {
2142 groups.push(std::mem::take(&mut current_group));
2143 }
2144 current_group.push(cond.clone());
2145 }
2146 }
2147 if !current_group.is_empty() {
2148 groups.push(current_group);
2149 }
2150
2151 let group_strs: Vec<String> = groups
2152 .iter()
2153 .map(|g| {
2154 if g.len() == 1 {
2155 g[0].clone()
2156 } else {
2157 format!("({})", g.join(" OR "))
2158 }
2159 })
2160 .collect();
2161
2162 (format!(" WHERE {}", group_strs.join(" AND ")), params)
2164 }
2165
2166 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
2171 let table = self
2172 .table
2173 .clone()
2174 .unwrap_or_else(|| M::table_name().to_string());
2175 let columns = if self.select_columns.is_empty() {
2176 "*".to_string()
2177 } else {
2178 self.select_columns.join(", ")
2179 };
2180
2181 let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
2182
2183 for join in &self.joins {
2184 match join {
2185 JoinClause::Inner(t, l, r) => {
2186 let _ = write!(
2187 sql,
2188 " INNER JOIN {} ON {} = {}",
2189 self.dialect.quote(t),
2190 self.dialect.quote(l),
2191 self.dialect.quote(r)
2192 );
2193 }
2194 JoinClause::Left(t, l, r) => {
2195 let _ = write!(
2196 sql,
2197 " LEFT JOIN {} ON {} = {}",
2198 self.dialect.quote(t),
2199 self.dialect.quote(l),
2200 self.dialect.quote(r)
2201 );
2202 }
2203 JoinClause::Right(t, l, r) => {
2204 let _ = write!(
2205 sql,
2206 " RIGHT JOIN {} ON {} = {}",
2207 self.dialect.quote(t),
2208 self.dialect.quote(l),
2209 self.dialect.quote(r)
2210 );
2211 }
2212 JoinClause::Cross(t, on) => {
2213 let _ = write!(
2214 sql,
2215 " CROSS JOIN {} ON {}",
2216 self.dialect.quote(t),
2217 self.dialect.quote(on)
2218 );
2219 }
2220 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2221 let _ = write!(
2222 sql,
2223 " {} {} ON {}.{} = {}.{}",
2224 kind.as_sql(),
2225 self.dialect.quote(tt),
2226 self.dialect.quote(ft),
2227 self.dialect.quote(fk),
2228 self.dialect.quote(tt),
2229 self.dialect.quote(tk)
2230 );
2231 }
2232 }
2233 }
2234
2235 let mut params = Vec::new();
2236 let (where_clause, where_params) = self.build_where_clause_with_params();
2238 if !where_clause.is_empty() {
2239 sql.push_str(&where_clause);
2240 params = where_params;
2241 }
2242
2243 if !self.group_by.is_empty() {
2244 let cols: Vec<String> = self
2245 .group_by
2246 .iter()
2247 .map(|c| self.dialect.quote(c))
2248 .collect();
2249 sql.push_str(" GROUP BY ");
2250 sql.push_str(&cols.join(", "));
2251 }
2252
2253 if !self.having_conditions.is_empty() {
2254 sql.push_str(" HAVING ");
2255 for (i, cond) in self.having_conditions.iter().enumerate() {
2256 if i > 0 {
2257 sql.push_str(" AND ");
2258 }
2259 match cond {
2260 WhereCondition::And(c) => sql.push_str(c),
2261 WhereCondition::Having(agg, op, value) => {
2263 params.push(value.clone());
2264 sql.push_str(&format!("{} {} ?", agg.render(&*self.dialect), op.as_sql()));
2265 }
2266 _ => {}
2267 }
2268 }
2269 }
2270
2271 if !self.order_by.is_empty() {
2272 let order_cols: Vec<String> = self
2273 .order_by
2274 .iter()
2275 .map(|o| {
2276 let dir = match o.direction {
2277 OrderDirection::Asc => " ASC",
2278 OrderDirection::Desc => " DESC",
2279 };
2280 format!("{}{}", self.dialect.quote(&o.field), dir)
2281 })
2282 .collect();
2283 sql.push_str(" ORDER BY ");
2284 sql.push_str(&order_cols.join(", "));
2285 }
2286
2287 if let Some(limit) = self.limit_value {
2288 let _ = write!(sql, " LIMIT {}", limit);
2289 }
2290 if let Some(offset) = self.offset_value {
2291 let _ = write!(sql, " OFFSET {}", offset);
2292 }
2293
2294 if let Some(lock_type) = &self.lock_type {
2296 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2297 sql.push(' ');
2298 sql.push_str(&lock_clause);
2299 }
2300 }
2301
2302 (sql, params)
2303 }
2304
2305 pub fn build_insert_with_params(
2307 &self,
2308 data: &std::collections::HashMap<String, Value>,
2309 ) -> (String, Vec<Value>) {
2310 let table = self
2311 .table
2312 .clone()
2313 .unwrap_or_else(|| M::table_name().to_string());
2314 if data.is_empty() {
2315 return (String::new(), Vec::new());
2316 }
2317
2318 let mut columns = Vec::with_capacity(data.len());
2319 let mut params = Vec::with_capacity(data.len());
2320 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2321 for (k, v) in data.iter() {
2322 columns.push(self.dialect.quote(k));
2323 params.push(v.clone());
2324 }
2325
2326 let insert_clause = if self.insert_or_ignore {
2328 self.dialect.build_insert_or_ignore_prefix(&table)
2329 } else {
2330 format!("INSERT INTO {}", self.dialect.quote(&table))
2331 };
2332
2333 let sql = format!(
2334 "{} ({}) VALUES ({})",
2335 insert_clause,
2336 columns.join(", "),
2337 placeholders.join(", ")
2338 );
2339 (sql, params)
2340 }
2341
2342 pub fn build_batch_insert_with_params(
2349 &self,
2350 rows: &[std::collections::HashMap<String, Value>],
2351 ) -> (String, Vec<Value>) {
2352 let table = self
2353 .table
2354 .clone()
2355 .unwrap_or_else(|| M::table_name().to_string());
2356 if rows.is_empty() {
2357 return (String::new(), Vec::new());
2358 }
2359
2360 let first_row = &rows[0];
2362 let columns: Vec<String> = first_row.keys().cloned().collect();
2363 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2364
2365 let mut params = Vec::with_capacity(rows.len() * columns.len());
2366 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2367 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2368 let mut param_idx = 1usize;
2369 for row in rows {
2370 let placeholders: Vec<String> = columns
2371 .iter()
2372 .map(|col| match row.get(col) {
2373 Some(v) => {
2374 params.push(v.clone());
2375 if is_pg {
2376 let p = format!("${}", param_idx);
2377 param_idx += 1;
2378 p
2379 } else {
2380 "?".to_string()
2381 }
2382 }
2383 None => "NULL".to_string(),
2384 })
2385 .collect();
2386 value_groups.push(format!("({})", placeholders.join(", ")));
2387 }
2388
2389 let sql = format!(
2390 "INSERT INTO {} ({}) VALUES {}",
2391 self.dialect.quote(&table),
2392 quoted_columns.join(", "),
2393 value_groups.join(", ")
2394 );
2395 (sql, params)
2396 }
2397
2398 pub fn build_batch_upsert_with_params(
2419 &self,
2420 rows: &[std::collections::HashMap<String, Value>],
2421 conflict_columns: &[&str],
2422 update_columns: &[&str],
2423 ) -> Result<(String, Vec<Value>), crate::DbError> {
2424 if rows.is_empty() {
2425 return Err(crate::DbError::InvalidInput(
2426 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2427 ));
2428 }
2429
2430 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2432 if insert_sql.is_empty() {
2433 return Err(crate::DbError::InvalidInput(
2434 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2435 ));
2436 }
2437
2438 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2440
2441 let conflict_clause = self
2443 .dialect
2444 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2445 .ok_or_else(|| {
2446 crate::DbError::InvalidInput(format!(
2447 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2448 self.dialect.db_type()
2449 ))
2450 })?;
2451
2452 let sql = format!("{} {}", insert_sql, conflict_clause);
2453 Ok((sql, params))
2454 }
2455
2456 pub fn build_update_with_params(
2459 &self,
2460 data: &std::collections::HashMap<String, Value>,
2461 ) -> (String, Vec<Value>) {
2462 let table = self
2463 .table
2464 .clone()
2465 .unwrap_or_else(|| M::table_name().to_string());
2466 if data.is_empty() {
2467 return (String::new(), Vec::new());
2468 }
2469
2470 let mut set_clauses = Vec::with_capacity(data.len());
2471 let mut params = Vec::with_capacity(data.len());
2472 for (k, v) in data.iter() {
2473 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2474 params.push(v.clone());
2475 }
2476
2477 let mut sql = format!(
2478 "UPDATE {} SET {}",
2479 self.dialect.quote(&table),
2480 set_clauses.join(", ")
2481 );
2482
2483 let (where_clause, where_params) = self.build_where_clause_with_params();
2485 if !where_clause.is_empty() {
2486 sql.push_str(&where_clause);
2487 params.extend(where_params);
2488 }
2489
2490 (sql, params)
2491 }
2492
2493 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2499 let table = self
2500 .table
2501 .clone()
2502 .unwrap_or_else(|| M::table_name().to_string());
2503
2504 if let Some(field) = self.soft_delete_field() {
2506 let (where_clause, where_params) = self.build_where_clause_with_params();
2507 let sql = format!(
2508 "UPDATE {} SET {} = NOW(){}",
2509 self.dialect.quote(&table),
2510 self.dialect.quote(field),
2511 where_clause
2512 );
2513 return (sql, where_params);
2514 }
2515
2516 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2517 let mut params = Vec::new();
2518
2519 let (where_clause, where_params) = self.build_where_clause_with_params();
2520 if !where_clause.is_empty() {
2521 sql.push_str(&where_clause);
2522 params = where_params;
2523 }
2524
2525 (sql, params)
2526 }
2527
2528 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2532 let table = self
2533 .table
2534 .clone()
2535 .unwrap_or_else(|| M::table_name().to_string());
2536
2537 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2538 let mut params = Vec::new();
2539
2540 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2542 if !where_clause.is_empty() {
2543 sql.push_str(&where_clause);
2544 params = where_params;
2545 }
2546
2547 (sql, params)
2548 }
2549
2550 pub fn build_count(&self) -> String {
2552 let table = self
2553 .table
2554 .clone()
2555 .unwrap_or_else(|| M::table_name().to_string());
2556
2557 let mut sql = format!(
2558 "SELECT COUNT(*) as total FROM {}",
2559 self.dialect.quote(&table)
2560 );
2561 sql.push_str(&self.build_where_clause());
2562 sql
2563 }
2564
2565 pub fn build_exists(&self) -> String {
2567 let table = self
2568 .table
2569 .clone()
2570 .unwrap_or_else(|| M::table_name().to_string());
2571
2572 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2573 sql.push_str(&self.build_where_clause());
2574 sql.push_str(" LIMIT 1");
2575 format!("SELECT EXISTS({})", sql)
2576 }
2577
2578 pub fn build_max(&self, field: &str) -> String {
2580 let table = self
2581 .table
2582 .clone()
2583 .unwrap_or_else(|| M::table_name().to_string());
2584
2585 let mut sql = format!(
2586 "SELECT MAX({}) as max_val FROM {}",
2587 self.dialect.quote(field),
2588 self.dialect.quote(&table)
2589 );
2590 sql.push_str(&self.build_where_clause());
2591 sql
2592 }
2593
2594 pub fn build_min(&self, field: &str) -> String {
2596 let table = self
2597 .table
2598 .clone()
2599 .unwrap_or_else(|| M::table_name().to_string());
2600
2601 let mut sql = format!(
2602 "SELECT MIN({}) as min_val FROM {}",
2603 self.dialect.quote(field),
2604 self.dialect.quote(&table)
2605 );
2606 sql.push_str(&self.build_where_clause());
2607 sql
2608 }
2609
2610 pub fn build_sum(&self, field: &str) -> String {
2612 let table = self
2613 .table
2614 .clone()
2615 .unwrap_or_else(|| M::table_name().to_string());
2616
2617 let mut sql = format!(
2618 "SELECT SUM({}) as sum_val FROM {}",
2619 self.dialect.quote(field),
2620 self.dialect.quote(&table)
2621 );
2622 sql.push_str(&self.build_where_clause());
2623 sql
2624 }
2625
2626 pub fn build_avg(&self, field: &str) -> String {
2628 let table = self
2629 .table
2630 .clone()
2631 .unwrap_or_else(|| M::table_name().to_string());
2632
2633 let mut sql = format!(
2634 "SELECT AVG({}) as avg_val FROM {}",
2635 self.dialect.quote(field),
2636 self.dialect.quote(&table)
2637 );
2638 sql.push_str(&self.build_where_clause());
2639 sql
2640 }
2641
2642 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2645 let sql = self.build_select();
2646 let mut errors = Vec::new();
2647
2648 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2649 errors.push(e);
2650 }
2651
2652 if !self.joins.is_empty() {
2654 for join in &self.joins {
2655 match join {
2656 JoinClause::Inner(_, left, right)
2657 | JoinClause::Left(_, left, right)
2658 | JoinClause::Right(_, left, right) => {
2659 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2660 errors.push(e);
2661 }
2662 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2663 errors.push(e);
2664 }
2665 }
2666 JoinClause::Relation(_, ft, fk, tt, tk) => {
2667 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2668 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2669 errors.push(e);
2670 }
2671 }
2672 }
2673 _ => {}
2674 }
2675 }
2676 }
2677
2678 let table = self
2680 .table
2681 .clone()
2682 .unwrap_or_else(|| M::table_name().to_string());
2683 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2684 errors.push(e);
2685 }
2686
2687 if errors.is_empty() {
2688 Ok(())
2689 } else {
2690 Err(errors)
2691 }
2692 }
2693
2694 pub fn validate_insert(
2697 &self,
2698 data: &std::collections::HashMap<String, Value>,
2699 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2700 let sql = self.build_insert(data);
2701 let mut errors = Vec::new();
2702
2703 if sql.is_empty() {
2704 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2705 return Err(errors);
2706 }
2707
2708 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2709 errors.push(e);
2710 }
2711
2712 if errors.is_empty() {
2713 Ok(())
2714 } else {
2715 Err(errors)
2716 }
2717 }
2718
2719 pub fn validate_update(
2722 &self,
2723 data: &std::collections::HashMap<String, Value>,
2724 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2725 let sql = self.build_update(data);
2726 let mut errors = Vec::new();
2727
2728 if sql.is_empty() {
2729 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2730 return Err(errors);
2731 }
2732
2733 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2734 errors.push(e);
2735 }
2736
2737 if errors.is_empty() {
2738 Ok(())
2739 } else {
2740 Err(errors)
2741 }
2742 }
2743
2744 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2746 let sql = self.build_delete();
2747 let mut errors = Vec::new();
2748
2749 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2750 errors.push(e);
2751 }
2752
2753 if errors.is_empty() {
2754 Ok(())
2755 } else {
2756 Err(errors)
2757 }
2758 }
2759}
2760
2761impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2763 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2781 let all_columns = M::columns();
2782 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2783
2784 for field in fields {
2785 if !all_columns.contains(field) {
2786 return Err(crate::DbError::InvalidInput(format!(
2787 "排除的字段不存在: {}",
2788 field
2789 )));
2790 }
2791 }
2792
2793 let retained: Vec<String> = all_columns
2794 .into_iter()
2795 .filter(|c| !exclude_set.contains(*c))
2796 .map(|s| s.to_string())
2797 .collect();
2798
2799 if retained.is_empty() {
2800 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2801 }
2802
2803 self.select_mode = crate::partial_model::SelectMode::Partial;
2804 self.select_columns = retained;
2805 Ok(self)
2806 }
2807}
2808
2809impl<M: Model> fmt::Debug for QueryBuilder<M> {
2810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2811 f.debug_struct("QueryBuilder")
2812 .field("table", &self.table)
2813 .field("select_columns", &self.select_columns)
2814 .field("where_conditions", &self.where_conditions.len())
2815 .field("limit", &self.limit_value)
2816 .finish()
2817 }
2818}
2819
2820#[cfg(test)]
2821#[allow(deprecated)]
2822mod tests {
2823 use super::*;
2824 use crate::db_type::DbType;
2825 use crate::dialect::get_dialect;
2826
2827 struct TestModel;
2828 impl Model for TestModel {
2829 type PrimaryKey = i64;
2830
2831 fn table_name() -> &'static str {
2832 "test_models"
2833 }
2834
2835 fn pk(&self) -> Self::PrimaryKey {
2836 1
2837 }
2838
2839 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2840 }
2841
2842 #[test]
2843 fn test_query_builder_select() -> Result<(), crate::DbError> {
2844 let dialect = get_dialect(DbType::MySQL)?;
2845 let builder = QueryBuilder::<TestModel>::new(dialect);
2846
2847 let sql = builder
2848 .table("users")
2849 .select(vec!["id", "name"])?
2850 .build_select();
2851 assert!(sql.contains("SELECT `id`, `name` FROM"));
2852 assert!(sql.contains("`users`"));
2853 Ok(())
2854 }
2855
2856 #[test]
2857 fn test_query_builder_where() -> Result<(), crate::DbError> {
2858 let dialect = get_dialect(DbType::MySQL)?;
2859 let builder = QueryBuilder::<TestModel>::new(dialect);
2860
2861 let sql = builder
2862 .table("users")
2863 .where_eq("status", crate::value::Value::String("active".into()))
2864 .where_gt("age", crate::value::Value::I64(18))
2865 .build_select();
2866
2867 assert!(sql.contains("WHERE"));
2868 assert!(sql.contains("`status` = 'active'"));
2869 assert!(sql.contains("`age` > 18"));
2870 Ok(())
2871 }
2872
2873 #[test]
2874 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2875 let dialect = get_dialect(DbType::MySQL)?;
2876 let builder = QueryBuilder::<TestModel>::new(dialect);
2877
2878 let sql = builder
2879 .table("users")
2880 .order_by("created_at")
2881 .order_desc("id")
2882 .build_select();
2883
2884 assert!(sql.contains("ORDER BY"));
2885 assert!(sql.contains("`created_at` ASC"));
2886 assert!(sql.contains("`id` DESC"));
2887 Ok(())
2888 }
2889
2890 #[test]
2891 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2892 let dialect = get_dialect(DbType::MySQL)?;
2893 let builder = QueryBuilder::<TestModel>::new(dialect);
2894
2895 let sql = builder.table("users").limit(10).offset(20).build_select();
2896
2897 assert!(sql.contains("LIMIT 10"));
2898 assert!(sql.contains("OFFSET 20"));
2899 Ok(())
2900 }
2901
2902 #[test]
2903 fn test_query_builder_page() -> Result<(), crate::DbError> {
2904 let dialect = get_dialect(DbType::MySQL)?;
2905 let builder = QueryBuilder::<TestModel>::new(dialect);
2906
2907 let sql = builder.table("users").page(3, 20).build_select();
2908
2909 assert!(sql.contains("LIMIT 20"));
2910 assert!(sql.contains("OFFSET 40"));
2911 Ok(())
2912 }
2913
2914 #[test]
2915 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2916 let dialect = get_dialect(DbType::MySQL)?;
2917 let builder = QueryBuilder::<TestModel>::new(dialect);
2918
2919 let mut data = std::collections::HashMap::new();
2920 data.insert("name".to_string(), Value::String("test".to_string()));
2921 data.insert("age".to_string(), Value::I64(25));
2922
2923 let sql = builder.table("users").build_insert(&data);
2924
2925 assert!(sql.contains("INSERT INTO"));
2926 assert!(sql.contains("`name`"));
2927 assert!(sql.contains("'test'"));
2928 Ok(())
2929 }
2930
2931 #[test]
2932 fn test_query_builder_update() -> Result<(), crate::DbError> {
2933 let dialect = get_dialect(DbType::MySQL)?;
2934 let builder = QueryBuilder::<TestModel>::new(dialect);
2935
2936 let mut data = std::collections::HashMap::new();
2937 data.insert("name".to_string(), Value::String("updated".to_string()));
2938
2939 let sql = builder
2940 .table("users")
2941 .where_eq("id", Value::I64(1))
2942 .build_update(&data);
2943
2944 assert!(sql.contains("UPDATE"));
2945 assert!(sql.contains("`name` = 'updated'"));
2946 assert!(sql.contains("WHERE"));
2947 Ok(())
2948 }
2949
2950 #[test]
2951 fn test_query_builder_delete() -> Result<(), crate::DbError> {
2952 let dialect = get_dialect(DbType::MySQL)?;
2953 let builder = QueryBuilder::<TestModel>::new(dialect);
2954
2955 let sql = builder
2956 .table("users")
2957 .where_eq("id", Value::I64(1))
2958 .build_delete();
2959
2960 assert!(sql.contains("DELETE FROM"));
2961 assert!(sql.contains("WHERE"));
2962 Ok(())
2963 }
2964
2965 #[test]
2966 fn test_query_builder_count() -> Result<(), crate::DbError> {
2967 let dialect = get_dialect(DbType::MySQL)?;
2968 let builder = QueryBuilder::<TestModel>::new(dialect);
2969
2970 let sql = builder.table("users").build_count();
2971
2972 assert!(sql.contains("SELECT COUNT(*)"));
2973 assert!(sql.contains("FROM"));
2974 Ok(())
2975 }
2976
2977 #[test]
2978 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
2979 let dialect = get_dialect(DbType::MySQL)?;
2980 let builder = QueryBuilder::<TestModel>::new(dialect);
2981
2982 let sql = builder
2983 .table("users")
2984 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
2985 .build_select();
2986
2987 assert!(sql.contains("IN ("));
2988 Ok(())
2989 }
2990
2991 #[test]
2992 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
2993 let dialect = get_dialect(DbType::MySQL)?;
2994 let builder = QueryBuilder::<TestModel>::new(dialect);
2995
2996 let sql = builder
2997 .table("users")
2998 .where_between("age", Value::I64(18), Value::I64(30))
2999 .build_select();
3000
3001 assert!(sql.contains("BETWEEN"));
3002 Ok(())
3003 }
3004
3005 #[test]
3006 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
3007 let dialect = get_dialect(DbType::MySQL)?;
3008 let builder = QueryBuilder::<TestModel>::new(dialect);
3009
3010 let sql = builder
3011 .table("users")
3012 .where_null("deleted_at")
3013 .build_select();
3014
3015 assert!(sql.contains("IS NULL"));
3016 Ok(())
3017 }
3018
3019 #[test]
3020 fn test_query_builder_join() -> Result<(), crate::DbError> {
3021 let dialect = get_dialect(DbType::MySQL)?;
3022 let builder = QueryBuilder::<TestModel>::new(dialect);
3023
3024 let sql = builder
3025 .table("users")
3026 .join_inner("posts", "users.id", "posts.user_id")
3027 .build_select();
3028
3029 assert!(sql.contains("INNER JOIN"));
3030 assert!(sql.contains("`posts`"));
3031 Ok(())
3032 }
3033
3034 #[test]
3035 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3036 let dialect = get_dialect(DbType::MySQL)?;
3037 let builder = QueryBuilder::<TestModel>::new(dialect);
3038
3039 let sql = builder.table("users").group_by("status").build_select();
3040
3041 assert!(sql.contains("GROUP BY"));
3042 assert!(sql.contains("`status`"));
3043 Ok(())
3044 }
3045
3046 #[test]
3047 fn test_query_builder_max() -> Result<(), crate::DbError> {
3048 let dialect = get_dialect(DbType::MySQL)?;
3049 let builder = QueryBuilder::<TestModel>::new(dialect);
3050
3051 let sql = builder.table("users").build_max("score");
3052
3053 assert!(sql.contains("MAX("));
3054 assert!(sql.contains("`score`"));
3055 Ok(())
3056 }
3057
3058 #[test]
3059 fn test_query_builder_min() -> Result<(), crate::DbError> {
3060 let dialect = get_dialect(DbType::MySQL)?;
3061 let builder = QueryBuilder::<TestModel>::new(dialect);
3062
3063 let sql = builder.table("users").build_min("price");
3064
3065 assert!(sql.contains("MIN("));
3066 assert!(sql.contains("`price`"));
3067 Ok(())
3068 }
3069
3070 #[test]
3071 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3072 let dialect = get_dialect(DbType::MySQL)?;
3073 let builder = QueryBuilder::<TestModel>::new(dialect);
3074
3075 let sql = builder.table("orders").build_sum("amount");
3076
3077 assert!(sql.contains("SUM("));
3078 assert!(sql.contains("`amount`"));
3079 Ok(())
3080 }
3081
3082 #[test]
3083 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3084 let dialect = get_dialect(DbType::MySQL)?;
3085 let builder = QueryBuilder::<TestModel>::new(dialect);
3086
3087 let sql = builder.table("scores").build_avg("value");
3088
3089 assert!(sql.contains("AVG("));
3090 assert!(sql.contains("`value`"));
3091 Ok(())
3092 }
3093
3094 #[test]
3095 fn test_validator_select() -> Result<(), crate::DbError> {
3096 let dialect = get_dialect(DbType::MySQL)?;
3097 let builder = QueryBuilder::<TestModel>::new(dialect);
3098
3099 let result = builder
3100 .table("users")
3101 .select(vec!["id", "name"])?
3102 .validate();
3103 assert!(result.is_ok());
3104 Ok(())
3105 }
3106
3107 #[test]
3108 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3109 let dialect = get_dialect(DbType::MySQL)?;
3110 let builder = QueryBuilder::<TestModel>::new(dialect);
3111
3112 let result = builder
3113 .table("users")
3114 .join_inner("posts", "users.id", "posts.user_id")
3115 .validate();
3116 assert!(result.is_ok());
3117 Ok(())
3118 }
3119
3120 #[test]
3121 fn test_validator_insert() -> Result<(), crate::DbError> {
3122 let dialect = get_dialect(DbType::MySQL)?;
3123 let builder = QueryBuilder::<TestModel>::new(dialect);
3124
3125 let mut data = std::collections::HashMap::new();
3126 data.insert("name".to_string(), Value::String("test".to_string()));
3127
3128 let result = builder.table("users").validate_insert(&data);
3129 assert!(result.is_ok());
3130 Ok(())
3131 }
3132
3133 #[test]
3134 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3135 let dialect = get_dialect(DbType::MySQL)?;
3136 let builder = QueryBuilder::<TestModel>::new(dialect);
3137
3138 let data = std::collections::HashMap::new();
3139 let result = builder.table("users").validate_insert(&data);
3140 assert!(result.is_err());
3141 Ok(())
3142 }
3143
3144 #[test]
3145 fn test_validator_update() -> Result<(), crate::DbError> {
3146 let dialect = get_dialect(DbType::MySQL)?;
3147 let builder = QueryBuilder::<TestModel>::new(dialect);
3148
3149 let mut data = std::collections::HashMap::new();
3150 data.insert("name".to_string(), Value::String("updated".to_string()));
3151
3152 let result = builder.table("users").validate_update(&data);
3153 assert!(result.is_ok());
3154 Ok(())
3155 }
3156
3157 #[test]
3158 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3159 let dialect = get_dialect(DbType::MySQL)?;
3160 let builder = QueryBuilder::<TestModel>::new(dialect);
3161
3162 let data = std::collections::HashMap::new();
3163 let result = builder.table("users").validate_update(&data);
3164 assert!(result.is_err());
3165 Ok(())
3166 }
3167
3168 #[test]
3169 fn test_validator_delete() -> Result<(), crate::DbError> {
3170 let dialect = get_dialect(DbType::MySQL)?;
3171 let builder = QueryBuilder::<TestModel>::new(dialect);
3172
3173 let result = builder
3174 .table("users")
3175 .where_eq("id", Value::I64(1))
3176 .validate_delete();
3177 assert!(result.is_ok());
3178 Ok(())
3179 }
3180
3181 #[test]
3182 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3183 let dialect = get_dialect(DbType::MySQL)?;
3184 let builder = QueryBuilder::<TestModel>::new(dialect);
3185
3186 let result = builder.table("users").validate_delete();
3188 assert!(result.is_ok());
3189 Ok(())
3190 }
3191
3192 #[test]
3195 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3196 let dialect = get_dialect(DbType::MySQL)?;
3197 let builder = QueryBuilder::<TestModel>::new(dialect);
3198 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3199 let sql = builder.build_select();
3200 assert!(sql.contains("SELECT `id`, `name` FROM"));
3202 assert!(sql.contains("`users`"));
3203 Ok(())
3204 }
3205
3206 #[test]
3207 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3208 let dialect = get_dialect(DbType::MySQL)?;
3209 let builder = QueryBuilder::<TestModel>::new(dialect);
3210
3211 let result = builder
3213 .table("users")
3214 .select_quoted(vec!["id; DROP TABLE users"]);
3215 assert!(result.is_err());
3216
3217 let dialect = get_dialect(DbType::MySQL)?;
3219 let builder = QueryBuilder::<TestModel>::new(dialect);
3220 let result = builder.table("users").select_quoted(vec!["name'"]);
3221 assert!(result.is_err());
3222
3223 let dialect = get_dialect(DbType::MySQL)?;
3225 let builder = QueryBuilder::<TestModel>::new(dialect);
3226 let result = builder.table("users").select_quoted(vec!["1col"]);
3227 assert!(result.is_err());
3228
3229 let dialect = get_dialect(DbType::MySQL)?;
3231 let builder = QueryBuilder::<TestModel>::new(dialect);
3232 let result = builder.table("users").select_quoted(vec!["col name"]);
3233 assert!(result.is_err());
3234 Ok(())
3235 }
3236
3237 #[test]
3238 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3239 let dialect = get_dialect(DbType::PostgreSQL)?;
3240 let builder = QueryBuilder::<TestModel>::new(dialect);
3241 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3242 let sql = builder.build_select();
3243 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3245 assert!(sql.contains("\"users\""));
3246 Ok(())
3247 }
3248
3249 struct SoftDeleteModel;
3253 impl Model for SoftDeleteModel {
3254 type PrimaryKey = i64;
3255
3256 fn table_name() -> &'static str {
3257 "soft_users"
3258 }
3259
3260 fn pk(&self) -> Self::PrimaryKey {
3261 1
3262 }
3263
3264 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3265
3266 fn soft_delete_field() -> Option<&'static str> {
3267 Some("deleted_at")
3268 }
3269 }
3270
3271 #[test]
3275 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3276 let dialect = get_dialect(DbType::MySQL)?;
3277 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3278 let sql = builder.table("soft_users").build_select();
3279 assert!(
3281 sql.contains("`deleted_at` IS NULL"),
3282 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3283 sql
3284 );
3285 Ok(())
3286 }
3287
3288 #[test]
3290 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3291 let dialect = get_dialect(DbType::MySQL)?;
3292 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3293 .table("soft_users")
3294 .where_eq("status", Value::String("active".into()))
3295 .build_select();
3296 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3298 assert!(
3299 sql.contains("`deleted_at` IS NULL"),
3300 "软删除条件应自动追加: {}",
3301 sql
3302 );
3303 Ok(())
3304 }
3305
3306 #[test]
3310 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3311 let dialect = get_dialect(DbType::MySQL)?;
3312 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3313 .table("soft_users")
3314 .without_soft_delete()
3315 .build_select();
3316 assert!(
3318 !sql.contains("`deleted_at` IS NULL"),
3319 "without_soft_delete 应禁用过滤,实际: {}",
3320 sql
3321 );
3322 assert!(
3324 !sql.contains("WHERE"),
3325 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3326 sql
3327 );
3328 Ok(())
3329 }
3330
3331 #[test]
3335 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3336 let dialect = get_dialect(DbType::MySQL)?;
3337 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3338 .table("soft_users")
3339 .where_eq("id", Value::I64(42))
3340 .build_delete();
3341 assert!(
3343 sql.starts_with("UPDATE"),
3344 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3345 sql
3346 );
3347 assert!(
3348 !sql.contains("DELETE FROM"),
3349 "不应生成 DELETE FROM: {}",
3350 sql
3351 );
3352 assert!(
3353 sql.contains("`deleted_at` = NOW()"),
3354 "应设置 deleted_at = NOW(): {}",
3355 sql
3356 );
3357 assert!(
3359 sql.contains("`deleted_at` IS NULL"),
3360 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3361 sql
3362 );
3363 Ok(())
3364 }
3365
3366 #[test]
3370 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3371 let dialect = get_dialect(DbType::MySQL)?;
3372 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3373 .table("soft_users")
3374 .where_eq("id", Value::I64(99))
3375 .build_force_delete();
3376 assert!(
3378 sql.starts_with("DELETE FROM"),
3379 "build_force_delete 应生成 DELETE FROM,实际: {}",
3380 sql
3381 );
3382 assert!(
3384 !sql.contains("`deleted_at` IS NULL"),
3385 "物理删除不应追加软删除过滤: {}",
3386 sql
3387 );
3388 Ok(())
3389 }
3390
3391 #[test]
3393 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3394 let dialect = get_dialect(DbType::MySQL)?;
3395 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3396 .table("soft_users")
3397 .where_eq("id", Value::I64(1))
3398 .build_select_with_params();
3399 assert!(
3400 sql.contains("`deleted_at` IS NULL"),
3401 "参数化版本也应自动追加软删除: {}",
3402 sql
3403 );
3404 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3405 assert_eq!(params[0], Value::I64(1));
3406 Ok(())
3407 }
3408
3409 #[test]
3411 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3412 let dialect = get_dialect(DbType::MySQL)?;
3413 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3414 .table("soft_users")
3415 .where_eq("id", Value::I64(7))
3416 .build_delete_with_params();
3417 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3418 assert!(
3419 sql.contains("`deleted_at` = NOW()"),
3420 "应设置 NOW(): {}",
3421 sql
3422 );
3423 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3424 Ok(())
3425 }
3426
3427 #[test]
3429 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3430 let dialect = get_dialect(DbType::MySQL)?;
3431 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3432 .table("soft_users")
3433 .where_eq("id", Value::I64(11))
3434 .build_force_delete_with_params();
3435 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3436 assert!(
3437 !sql.contains("`deleted_at` IS NULL"),
3438 "不应追加软删除过滤: {}",
3439 sql
3440 );
3441 assert_eq!(params.len(), 1);
3442 Ok(())
3443 }
3444
3445 #[test]
3449 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3450 let dialect = get_dialect(DbType::MySQL)?;
3451 let sql = QueryBuilder::<TestModel>::new(dialect)
3452 .table("users")
3453 .where_eq("id", Value::I64(1))
3454 .build_select();
3455 assert!(
3456 !sql.contains("deleted_at"),
3457 "非软删除模型不应追加 deleted_at: {}",
3458 sql
3459 );
3460 let dialect = get_dialect(DbType::MySQL)?;
3462 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3463 .table("users")
3464 .where_eq("id", Value::I64(1))
3465 .build_delete();
3466 assert!(
3467 del_sql.starts_with("DELETE FROM"),
3468 "非软删除模型 build_delete 应生成 DELETE: {}",
3469 del_sql
3470 );
3471 Ok(())
3472 }
3473
3474 #[test]
3476 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3477 let dialect = get_dialect(DbType::MySQL)?;
3478 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3479 .table("soft_users")
3480 .build_count();
3481 assert!(
3482 sql.contains("`deleted_at` IS NULL"),
3483 "build_count 也应追加软删除过滤: {}",
3484 sql
3485 );
3486 Ok(())
3487 }
3488
3489 #[test]
3495 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3496 let dialect = get_dialect(DbType::MySQL)?;
3497 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3498 .table("users")
3499 .where_eq("name", Value::String("alice".into()))
3500 .build_select_with_params();
3501 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3503 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3504 assert_eq!(params.len(), 1);
3505 assert_eq!(params[0], Value::String("alice".into()));
3506 Ok(())
3507 }
3508
3509 #[test]
3511 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3512 let dialect = get_dialect(DbType::MySQL)?;
3513 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3514 .table("users")
3515 .where_like("name", Value::String("%alice%".into()))
3516 .build_select_with_params();
3517 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3518 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3519 assert_eq!(params.len(), 1);
3520 Ok(())
3521 }
3522
3523 #[test]
3527 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3528 let dialect = get_dialect(DbType::MySQL)?;
3529 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3530 .table("users")
3531 .where_ne("status", Value::I64(0))
3532 .build_select_with_params();
3533 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3534 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3535 assert_eq!(params.len(), 1);
3536 assert_eq!(params[0], Value::I64(0));
3537 Ok(())
3538 }
3539
3540 #[test]
3544 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3545 let dialect = get_dialect(DbType::MySQL)?;
3546 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3547 .table("users")
3548 .where_ge("age", Value::I64(18))
3549 .build_select_with_params();
3550 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3551 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3552 assert_eq!(params.len(), 1);
3553 assert_eq!(params[0], Value::I64(18));
3554 Ok(())
3555 }
3556
3557 #[test]
3561 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3562 let dialect = get_dialect(DbType::MySQL)?;
3563 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3564 .table("users")
3565 .where_lt("score", Value::F64(60.0))
3566 .build_select_with_params();
3567 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3568 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3569 assert_eq!(params.len(), 1);
3570 assert_eq!(params[0], Value::F64(60.0));
3571 Ok(())
3572 }
3573
3574 #[test]
3578 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3579 let dialect = get_dialect(DbType::MySQL)?;
3580 let evil_input = "'; DROP TABLE users; --".to_string();
3581 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3582 .table("users")
3583 .where_eq("name", Value::String(evil_input.clone()))
3584 .build_select_with_params();
3585 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3587 assert_eq!(params.len(), 1);
3589 assert_eq!(params[0], Value::String(evil_input));
3590 assert_eq!(sql.matches('?').count(), 1);
3592 Ok(())
3593 }
3594
3595 #[test]
3597 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3598 let dialect = get_dialect(DbType::MySQL)?;
3599 let evil = "' OR '1'='1".to_string();
3600 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3601 .table("users")
3602 .where_eq("name", Value::String(evil.clone()))
3603 .build_select_with_params();
3604 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3605 assert_eq!(params.len(), 1);
3606 assert_eq!(params[0], Value::String(evil));
3607 Ok(())
3608 }
3609
3610 #[test]
3612 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3613 let dialect = get_dialect(DbType::MySQL)?;
3614 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3615 .table("users")
3616 .where_eq("name", Value::String("alice".into()))
3617 .where_gt("age", Value::I64(18))
3618 .where_le("score", Value::F64(99.5))
3619 .build_select_with_params();
3620 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3621 assert_eq!(params.len(), 3);
3622 assert_eq!(params[0], Value::String("alice".into()));
3624 assert_eq!(params[1], Value::I64(18));
3625 assert_eq!(params[2], Value::F64(99.5));
3626 Ok(())
3627 }
3628
3629 #[test]
3631 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3632 let dialect = get_dialect(DbType::MySQL)?;
3633 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3634 .table("users")
3635 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3636 .build_select_with_params();
3637 assert!(
3638 sql.contains("`id` IN (?, ?, ?)"),
3639 "应使用 3 个占位符: {}",
3640 sql
3641 );
3642 assert_eq!(params.len(), 3);
3643 Ok(())
3644 }
3645
3646 #[test]
3648 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3649 let dialect = get_dialect(DbType::MySQL)?;
3650 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3651 .table("users")
3652 .where_between("age", Value::I64(18), Value::I64(65))
3653 .build_select_with_params();
3654 assert!(
3655 sql.contains("`age` BETWEEN ? AND ?"),
3656 "应使用 2 个占位符: {}",
3657 sql
3658 );
3659 assert_eq!(params.len(), 2);
3660 assert_eq!(params[0], Value::I64(18));
3661 assert_eq!(params[1], Value::I64(65));
3662 Ok(())
3663 }
3664
3665 #[test]
3667 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3668 let dialect = get_dialect(DbType::MySQL)?;
3669 let mut data = std::collections::HashMap::new();
3670 data.insert("name".to_string(), Value::String("bob".into()));
3671 data.insert("age".to_string(), Value::I64(30));
3672 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3673 .table("users")
3674 .where_eq("id", Value::I64(99))
3675 .build_update_with_params(&data);
3676 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3678 assert_eq!(params.len(), 3);
3679 assert_eq!(params[2], Value::I64(99));
3682 Ok(())
3683 }
3684
3685 #[test]
3689 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3690 let dialect = get_dialect(DbType::MySQL)?;
3691 let sql = QueryBuilder::<TestModel>::new(dialect)
3692 .table("users")
3693 .where_eq("name", Value::String("alice".into()))
3694 .build_select();
3695 assert!(
3697 sql.contains("`name` = "),
3698 "无参数版本应含 WHERE 条件: {}",
3699 sql
3700 );
3701 assert!(
3703 !sql.contains("`name` = ?"),
3704 "无参数版本不应使用 ? 占位符: {}",
3705 sql
3706 );
3707 Ok(())
3708 }
3709
3710 #[test]
3712 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3713 let dialect = get_dialect(DbType::MySQL)?;
3714 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3715 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3716 let builder =
3717 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3718 assert!(
3719 builder.is_soft_delete_disabled(),
3720 "without_soft_delete 后应反映禁用状态"
3721 );
3722 Ok(())
3723 }
3724
3725 struct TenantModel;
3729 impl Model for TenantModel {
3730 type PrimaryKey = i64;
3731
3732 fn table_name() -> &'static str {
3733 "orders"
3734 }
3735
3736 fn pk(&self) -> Self::PrimaryKey {
3737 1
3738 }
3739
3740 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3741
3742 fn tenant_field() -> Option<&'static str> {
3743 Some("tenant_id")
3744 }
3745 }
3746
3747 struct SoftDeleteAndTenantModel;
3749 impl Model for SoftDeleteAndTenantModel {
3750 type PrimaryKey = i64;
3751
3752 fn table_name() -> &'static str {
3753 "documents"
3754 }
3755
3756 fn pk(&self) -> Self::PrimaryKey {
3757 1
3758 }
3759
3760 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3761
3762 fn soft_delete_field() -> Option<&'static str> {
3763 Some("deleted_at")
3764 }
3765
3766 fn tenant_field() -> Option<&'static str> {
3767 Some("tenant_id")
3768 }
3769 }
3770
3771 #[test]
3775 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3776 let dialect = get_dialect(DbType::MySQL)?;
3777 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3778 .table("orders")
3779 .with_tenant_id(42)
3780 .build_select_with_params();
3781 assert!(
3782 sql.contains("`tenant_id` = ?"),
3783 "多租户模型应自动追加 tenant_id = ?: {}",
3784 sql
3785 );
3786 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3787 assert_eq!(params[0], Value::I64(42));
3788 Ok(())
3789 }
3790
3791 #[test]
3793 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3794 let dialect = get_dialect(DbType::MySQL)?;
3795 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3796 .table("orders")
3797 .with_tenant_id(7)
3798 .where_eq("status", Value::String("active".into()))
3799 .build_select_with_params();
3800 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3801 assert!(
3802 sql.contains("`tenant_id` = ?"),
3803 "租户条件应自动追加: {}",
3804 sql
3805 );
3806 assert_eq!(params.len(), 2, "应有 2 个参数");
3807 assert_eq!(params[0], Value::String("active".into()));
3809 assert_eq!(params[1], Value::I64(7));
3810 Ok(())
3811 }
3812
3813 #[test]
3817 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3818 let dialect = get_dialect(DbType::MySQL)?;
3819 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3820 .table("orders")
3821 .with_tenant_id(42)
3822 .without_tenant()
3823 .build_select_with_params();
3824 assert!(
3825 !sql.contains("`tenant_id` = ?"),
3826 "without_tenant 应禁用过滤: {}",
3827 sql
3828 );
3829 assert_eq!(params.len(), 0, "不应有租户参数");
3830 Ok(())
3831 }
3832
3833 #[test]
3837 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3838 let dialect = get_dialect(DbType::MySQL)?;
3839 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3840 .table("orders")
3841 .with_tenant_id(99)
3842 .where_eq("id", Value::I64(1))
3843 .build_delete_with_params();
3844 assert!(
3845 sql.contains("`tenant_id` = ?"),
3846 "删除应自动追加租户条件: {}",
3847 sql
3848 );
3849 assert_eq!(params.len(), 2);
3851 assert_eq!(params[0], Value::I64(1));
3852 assert_eq!(params[1], Value::I64(99));
3853 Ok(())
3854 }
3855
3856 #[test]
3858 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3859 let dialect = get_dialect(DbType::MySQL)?;
3860 let mut data = std::collections::HashMap::new();
3861 data.insert("status".to_string(), Value::String("shipped".into()));
3862 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3863 .table("orders")
3864 .with_tenant_id(5)
3865 .where_eq("id", Value::I64(10))
3866 .build_update_with_params(&data);
3867 assert!(
3868 sql.contains("`tenant_id` = ?"),
3869 "更新应自动追加租户条件: {}",
3870 sql
3871 );
3872 assert_eq!(params.len(), 3);
3874 assert_eq!(params[2], Value::I64(5));
3876 Ok(())
3877 }
3878
3879 #[test]
3881 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3882 let dialect = get_dialect(DbType::MySQL)?;
3883 let sql = QueryBuilder::<TenantModel>::new(dialect)
3884 .table("orders")
3885 .with_tenant_id(42)
3886 .build_count();
3887 assert!(
3888 sql.contains("`tenant_id` = 42"),
3889 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3890 sql
3891 );
3892 Ok(())
3893 }
3894
3895 #[test]
3899 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3900 let dialect = get_dialect(DbType::MySQL)?;
3901 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3903 .table("users")
3904 .with_tenant_id(42)
3905 .build_select_with_params();
3906 assert!(
3907 !sql.contains("tenant_id"),
3908 "非多租户模型不应追加 tenant_id: {}",
3909 sql
3910 );
3911 assert_eq!(params.len(), 0);
3912 Ok(())
3913 }
3914
3915 #[test]
3919 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
3920 let dialect = get_dialect(DbType::MySQL)?;
3921 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3922 .table("orders")
3923 .build_select_with_params();
3924 assert!(
3925 !sql.contains("tenant_id"),
3926 "未设置 tenant_id 时不应追加过滤: {}",
3927 sql
3928 );
3929 assert_eq!(params.len(), 0);
3930 Ok(())
3931 }
3932
3933 #[test]
3937 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
3938 let dialect = get_dialect(DbType::MySQL)?;
3939 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3940 .table("documents")
3941 .with_tenant_id(100)
3942 .where_eq("title", Value::String("report".into()))
3943 .build_select_with_params();
3944 assert!(
3946 sql.contains("`deleted_at` IS NULL"),
3947 "应追加软删除条件: {}",
3948 sql
3949 );
3950 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
3952 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
3954 assert_eq!(params.len(), 2);
3956 assert_eq!(params[0], Value::String("report".into()));
3957 assert_eq!(params[1], Value::I64(100));
3958 Ok(())
3959 }
3960
3961 #[test]
3963 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
3964 let dialect = get_dialect(DbType::MySQL)?;
3965 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3966 .table("documents")
3967 .with_tenant_id(100)
3968 .without_tenant()
3969 .without_soft_delete()
3970 .build_select_with_params();
3971 assert!(
3972 !sql.contains("`deleted_at` IS NULL"),
3973 "应禁用软删除: {}",
3974 sql
3975 );
3976 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
3977 assert_eq!(params.len(), 0);
3978 Ok(())
3979 }
3980
3981 #[test]
3983 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
3984 let dialect = get_dialect(DbType::MySQL)?;
3985 let builder = QueryBuilder::<TenantModel>::new(dialect);
3986 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
3987 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
3988 .with_tenant_id(1)
3989 .without_tenant();
3990 assert!(
3991 builder.is_tenant_disabled(),
3992 "without_tenant 后应反映禁用状态"
3993 );
3994 Ok(())
3995 }
3996
3997 #[test]
4001 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
4002 let dialect = get_dialect(DbType::MySQL)?;
4003 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4004 .table("orders")
4005 .with_tenant_id(42)
4006 .where_eq("id", Value::I64(999))
4007 .build_force_delete_with_params();
4008 assert!(
4011 sql.contains("`tenant_id` = ?"),
4012 "物理删除应保留租户条件: {}",
4013 sql
4014 );
4015 assert_eq!(params.len(), 2);
4016 assert_eq!(params[0], Value::I64(999));
4017 assert_eq!(params[1], Value::I64(42));
4018 Ok(())
4019 }
4020
4021 #[cfg(feature = "multi-tenant-enhanced")]
4025 #[tokio::test]
4026 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4027 let ctx = crate::tenant_context::TenantContext::new(
4028 99,
4029 crate::tenant_context::IsolationStrategy::RowLevel,
4030 );
4031 ctx.scope(async {
4032 let (sql, params) =
4034 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4035 .table("orders")
4036 .with_tenant_id(42)
4037 .build_select_with_params();
4038 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4039 assert_eq!(params.len(), 1);
4040 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4041 })
4042 .await;
4043 Ok(())
4044 }
4045
4046 #[cfg(feature = "multi-tenant-enhanced")]
4048 #[tokio::test]
4049 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4050 let ctx = crate::tenant_context::TenantContext::new(
4051 77,
4052 crate::tenant_context::IsolationStrategy::RowLevel,
4053 );
4054 ctx.scope(async {
4055 let (sql, params) =
4056 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4057 .table("orders")
4058 .build_select_with_params();
4059 assert!(
4060 sql.contains("`tenant_id` = ?"),
4061 "应从上下文自动追加租户条件: {}",
4062 sql
4063 );
4064 assert_eq!(params.len(), 1);
4065 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4066 })
4067 .await;
4068 Ok(())
4069 }
4070
4071 #[cfg(feature = "multi-tenant-enhanced")]
4073 #[tokio::test]
4074 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4075 let ctx = crate::tenant_context::TenantContext::new(
4076 42,
4077 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4078 );
4079 ctx.scope(async {
4080 let (sql, _params) =
4081 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4082 .table("orders")
4083 .build_select_with_params();
4084 assert!(
4085 sql.contains("tenant_42_orders"),
4086 "Schema 隔离应重写表名: {}",
4087 sql
4088 );
4089 })
4090 .await;
4091 Ok(())
4092 }
4093
4094 #[cfg(feature = "multi-tenant-enhanced")]
4096 #[test]
4097 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4098 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4099 .table("orders")
4100 .build_select_with_params();
4101 assert!(
4103 !sql.contains("`tenant_id` = ?"),
4104 "未设置上下文不应追加租户条件: {}",
4105 sql
4106 );
4107 assert_eq!(params.len(), 0);
4108 Ok(())
4109 }
4110
4111 struct TcUsersTable;
4114 impl crate::typed::TypedTable for TcUsersTable {
4115 const NAME: &'static str = "users";
4116 }
4117 struct TcColId;
4118 impl crate::typed::TypedColumn for TcColId {
4119 const NAME: &'static str = "id";
4120 type Table = TcUsersTable;
4121 type RustType = i64;
4122 type SqlType = crate::typed_ast::Untyped;
4123 }
4124 struct TcColName;
4125 impl crate::typed::TypedColumn for TcColName {
4126 const NAME: &'static str = "name";
4127 type Table = TcUsersTable;
4128 type RustType = String;
4129 type SqlType = crate::typed_ast::Untyped;
4130 }
4131
4132 #[test]
4133 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4134 let dialect = get_dialect(DbType::MySQL)?;
4135 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4136 .where_eq_typed::<TcColId>(Value::I64(42))
4137 .build_select_with_params();
4138 assert!(sql.contains("`id` = ?"));
4139 assert_eq!(params[0], Value::I64(42));
4140 Ok(())
4141 }
4142
4143 #[test]
4144 fn test_order_by_typed() -> Result<(), crate::DbError> {
4145 let dialect = get_dialect(DbType::MySQL)?;
4146 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4147 .order_by_typed::<TcColName>()
4148 .build_select_with_params();
4149 assert!(sql.contains("ORDER BY"));
4150 assert!(sql.contains("`name`"));
4151 Ok(())
4152 }
4153
4154 #[test]
4155 fn test_select_typed() -> Result<(), crate::DbError> {
4156 let dialect = get_dialect(DbType::MySQL)?;
4157 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4158 .select_typed::<TcColId>()
4159 .select_typed::<TcColName>()
4160 .build_select_with_params();
4161 assert!(sql.contains("SELECT"));
4162 assert!(sql.contains("id"));
4164 assert!(sql.contains("name"));
4165 Ok(())
4166 }
4167
4168 #[test]
4169 fn test_where_null_typed() -> Result<(), crate::DbError> {
4170 let dialect = get_dialect(DbType::MySQL)?;
4171 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4172 .where_null_typed::<TcColName>()
4173 .build_select_with_params();
4174 assert!(sql.contains("`name` IS NULL"));
4175 Ok(())
4176 }
4177
4178 #[test]
4179 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4180 let dialect = get_dialect(DbType::MySQL)?;
4181 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4182 .where_not_null_typed::<TcColName>()
4183 .build_select_with_params();
4184 assert!(sql.contains("`name` IS NOT NULL"));
4185 Ok(())
4186 }
4187
4188 #[test]
4189 fn test_group_by_typed() -> Result<(), crate::DbError> {
4190 let dialect = get_dialect(DbType::MySQL)?;
4191 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4192 .group_by_typed::<TcColName>()
4193 .build_select_with_params();
4194 assert!(sql.contains("GROUP BY"));
4195 assert!(sql.contains("`name`"));
4196 Ok(())
4197 }
4198
4199 #[test]
4200 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4201 let dialect = get_dialect(DbType::MySQL)?;
4202 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4203 .where_gt_typed::<TcColId>(Value::I64(10))
4204 .build_select_with_params();
4205 assert!(sql.contains("`id` > ?"));
4206 assert_eq!(params[0], Value::I64(10));
4207 Ok(())
4208 }
4209
4210 #[test]
4213 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4214 let dialect = get_dialect(DbType::MySQL)?;
4215 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4216 .table("users")
4217 .where_eq("id", Value::I64(1))
4218 .lock_for_update()?
4219 .build_select_with_params();
4220 assert!(sql.contains("SELECT * FROM `users`"));
4221 assert!(sql.contains("WHERE `id` = ?"));
4222 assert!(sql.contains("FOR UPDATE"));
4223 assert_eq!(params.len(), 1);
4224 assert_eq!(params[0], Value::I64(1));
4225 Ok(())
4226 }
4227
4228 #[test]
4229 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4230 let dialect = get_dialect(DbType::MySQL)?;
4231 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4232 .table("users")
4233 .where_eq("id", Value::I64(1))
4234 .lock_shared()?
4235 .build_select_with_params();
4236 assert!(sql.contains("SELECT * FROM `users`"));
4237 assert!(sql.contains("WHERE `id` = ?"));
4238 assert!(sql.contains("LOCK IN SHARE MODE"));
4239 assert_eq!(params.len(), 1);
4240 assert_eq!(params[0], Value::I64(1));
4241 Ok(())
4242 }
4243
4244 #[test]
4245 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4246 let dialect = get_dialect(DbType::PostgreSQL)?;
4247 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4248 .table("users")
4249 .where_eq("id", Value::I64(1))
4250 .lock_for_update()?
4251 .build_select_with_params();
4252 assert!(sql.contains("SELECT * FROM \"users\""));
4253 assert!(sql.contains("WHERE \"id\" = ?"));
4254 assert!(sql.contains("FOR UPDATE"));
4255 assert_eq!(params.len(), 1);
4256 assert_eq!(params[0], Value::I64(1));
4257 Ok(())
4258 }
4259
4260 #[test]
4261 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4262 let dialect = get_dialect(DbType::PostgreSQL)?;
4263 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4264 .table("users")
4265 .where_eq("id", Value::I64(1))
4266 .lock_shared()?
4267 .build_select_with_params();
4268 assert!(sql.contains("SELECT * FROM \"users\""));
4269 assert!(sql.contains("WHERE \"id\" = ?"));
4270 assert!(sql.contains("FOR SHARE"));
4271 assert_eq!(params.len(), 1);
4272 assert_eq!(params[0], Value::I64(1));
4273 Ok(())
4274 }
4275
4276 #[test]
4277 fn test_lock_for_update_sqlite_should_fail() {
4278 let dialect = get_dialect(DbType::Sqlite).unwrap();
4279 let result = QueryBuilder::<TestModel>::new(dialect)
4280 .table("users")
4281 .where_eq("id", Value::I64(1))
4282 .lock_for_update();
4283 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4284 let err = result.err().unwrap();
4285 assert!(
4286 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4287 "错误信息应说明不支持行锁"
4288 );
4289 }
4290
4291 #[test]
4292 fn test_lock_shared_sqlite_should_fail() {
4293 let dialect = get_dialect(DbType::Sqlite).unwrap();
4294 let result = QueryBuilder::<TestModel>::new(dialect)
4295 .table("users")
4296 .where_eq("id", Value::I64(1))
4297 .lock_shared();
4298 assert!(result.is_err(), "SQLite 不应支持共享锁");
4299 let err = result.err().unwrap();
4300 assert!(
4301 format!("{:?}", err).contains("Shared lock is not supported"),
4302 "错误信息应说明不支持共享锁"
4303 );
4304 }
4305
4306 #[test]
4307 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4308 let dialect = get_dialect(DbType::MySQL)?;
4309 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4310 .table("users")
4311 .where_eq("status", Value::String("active".into()))
4312 .limit(10)
4313 .offset(20)
4314 .lock_for_update()?
4315 .build_select_with_params();
4316 assert!(sql.contains("WHERE `status` = ?"));
4317 assert!(sql.contains("LIMIT 10"));
4318 assert!(sql.contains("OFFSET 20"));
4319 assert!(sql.contains("FOR UPDATE"));
4320 assert_eq!(params.len(), 1);
4321 Ok(())
4322 }
4323
4324 #[test]
4327 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4328 let dialect = get_dialect(DbType::MySQL)?;
4329 let mut data = std::collections::HashMap::new();
4330 data.insert("name".to_string(), Value::String("Alice".into()));
4331 data.insert("age".to_string(), Value::I64(30));
4332
4333 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4334 .table("users")
4335 .insert_or_ignore()
4336 .build_insert_with_params(&data);
4337 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4338 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4340 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4341 assert!(sql.contains("VALUES (?, ?)"));
4342 assert_eq!(params.len(), 2);
4343 Ok(())
4344 }
4345
4346 #[test]
4347 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4348 let dialect = get_dialect(DbType::PostgreSQL)?;
4349 let mut data = std::collections::HashMap::new();
4350 data.insert("name".to_string(), Value::String("Bob".into()));
4351
4352 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4353 .table("users")
4354 .insert_or_ignore()
4355 .build_insert_with_params(&data);
4356 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4357 assert!(sql.contains("(\"name\")"));
4358 assert!(sql.contains("VALUES (?)"));
4359 assert_eq!(params.len(), 1);
4360 assert_eq!(params[0], Value::String("Bob".into()));
4361 Ok(())
4362 }
4363
4364 #[test]
4365 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4366 let dialect = get_dialect(DbType::Sqlite)?;
4367 let mut data = std::collections::HashMap::new();
4368 data.insert("name".to_string(), Value::String("Charlie".into()));
4369
4370 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4371 .table("users")
4372 .insert_or_ignore()
4373 .build_insert_with_params(&data);
4374 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4375 assert!(sql.contains("(\"name\")"));
4376 assert!(sql.contains("VALUES (?)"));
4377 assert_eq!(params.len(), 1);
4378 assert_eq!(params[0], Value::String("Charlie".into()));
4379 Ok(())
4380 }
4381
4382 #[test]
4383 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4384 let dialect = get_dialect(DbType::MySQL)?;
4385 let mut data = std::collections::HashMap::new();
4386 data.insert("name".to_string(), Value::String("Dave".into()));
4387
4388 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4389 .table("users")
4390 .build_insert_with_params(&data);
4391 assert!(sql.contains("INSERT INTO `users`"));
4392 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4393 assert_eq!(params.len(), 1);
4394 assert_eq!(params[0], Value::String("Dave".into()));
4395 Ok(())
4396 }
4397
4398 #[test]
4399 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4400 let dialect = get_dialect(DbType::MySQL)?;
4401 let data = std::collections::HashMap::new();
4402
4403 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4404 .table("users")
4405 .insert_or_ignore()
4406 .build_insert_with_params(&data);
4407 assert!(sql.is_empty(), "空数据应返回空 SQL");
4408 assert!(params.is_empty());
4409 Ok(())
4410 }
4411
4412 #[test]
4415 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4416 let mysql = get_dialect(DbType::MySQL)?;
4417 let pg = get_dialect(DbType::PostgreSQL)?;
4418 let sqlite = get_dialect(DbType::Sqlite)?;
4419 let clickhouse = get_dialect(DbType::ClickHouse)?;
4420 let duckdb = get_dialect(DbType::DuckDB)?;
4421
4422 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4423 assert!(
4424 pg.supports_lock_for_update(),
4425 "PostgreSQL 应支持 FOR UPDATE"
4426 );
4427 assert!(
4428 !sqlite.supports_lock_for_update(),
4429 "SQLite 不应支持 FOR UPDATE"
4430 );
4431 assert!(
4432 !clickhouse.supports_lock_for_update(),
4433 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4434 );
4435 assert!(
4436 !duckdb.supports_lock_for_update(),
4437 "DuckDB 不应支持 FOR UPDATE"
4438 );
4439 Ok(())
4440 }
4441
4442 #[test]
4443 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4444 let mysql = get_dialect(DbType::MySQL)?;
4445 let pg = get_dialect(DbType::PostgreSQL)?;
4446 let sqlite = get_dialect(DbType::Sqlite)?;
4447 let clickhouse = get_dialect(DbType::ClickHouse)?;
4448 let duckdb = get_dialect(DbType::DuckDB)?;
4449
4450 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4451 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4452 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4453 assert!(
4454 !clickhouse.supports_lock_shared(),
4455 "ClickHouse 是列式 OLAP,不应支持共享锁"
4456 );
4457 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4458 Ok(())
4459 }
4460
4461 #[test]
4462 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4463 let dialect = get_dialect(DbType::MySQL)?;
4464
4465 let builder = QueryBuilder::<TestModel>::new(dialect);
4467 assert!(builder.get_lock_type().is_none(), "默认无锁");
4468 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4469
4470 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4472 .table("users")
4473 .lock_for_update()?;
4474 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4475
4476 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4478 .table("users")
4479 .insert_or_ignore();
4480 assert!(builder.is_insert_or_ignore());
4481
4482 Ok(())
4483 }
4484
4485 struct TestModelWithColumns;
4486 impl Model for TestModelWithColumns {
4487 type PrimaryKey = i64;
4488 fn table_name() -> &'static str {
4489 "test_with_cols"
4490 }
4491 fn pk(&self) -> Self::PrimaryKey {
4492 0
4493 }
4494 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4495 }
4496 impl crate::model::ModelExt for TestModelWithColumns {
4497 fn columns() -> Vec<&'static str> {
4498 vec!["id", "name", "email", "avatar", "blob_data"]
4499 }
4500 fn fillable() -> Vec<&'static str> {
4501 vec!["name", "email", "avatar", "blob_data"]
4502 }
4503 fn guarded() -> Vec<&'static str> {
4504 vec!["id"]
4505 }
4506 fn hidden() -> Vec<&'static str> {
4507 vec!["blob_data"]
4508 }
4509 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4510 std::collections::HashMap::new()
4511 }
4512 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4513 fn to_json(&self) -> serde_json::Value {
4514 serde_json::Value::Null
4515 }
4516 }
4517
4518 #[test]
4519 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4520 let dialect = get_dialect(DbType::MySQL)?;
4521 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4522 .table("users")
4523 .select_exclude(&["avatar", "blob_data"])?;
4524 let sql = builder.build_select();
4525 assert!(sql.contains("id"));
4526 assert!(sql.contains("name"));
4527 assert!(sql.contains("email"));
4528 assert!(!sql.contains("avatar"));
4529 assert!(!sql.contains("blob_data"));
4530 Ok(())
4531 }
4532
4533 #[test]
4534 fn test_select_exclude_nonexistent_field() {
4535 let dialect = get_dialect(DbType::MySQL).unwrap();
4536 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4537 .table("users")
4538 .select_exclude(&["nonexistent"]);
4539 assert!(result.is_err());
4540 }
4541
4542 #[test]
4543 fn test_select_exclude_all_fields() {
4544 let dialect = get_dialect(DbType::MySQL).unwrap();
4545 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4546 .table("users")
4547 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4548 assert!(result.is_err());
4549 let err = result.unwrap_err();
4550 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4551 }
4552
4553 #[tokio::test]
4556 async fn test_execute_with_cache_ttl_hit() {
4557 use crate::l2_cache::L2Cache;
4558 use std::sync::atomic::{AtomicU64, Ordering};
4559 use std::sync::Arc;
4560 use std::time::Duration;
4561
4562 let dialect = get_dialect(DbType::MySQL).unwrap();
4563 let cache = L2Cache::new();
4564 let qb = QueryBuilder::<TestModel>::new(dialect)
4565 .table("users")
4566 .where_eq("status", Value::I64(1))
4567 .cache_ttl(Duration::from_secs(300));
4568
4569 let call_count = Arc::new(AtomicU64::new(0));
4570 let cc1 = Arc::clone(&call_count);
4571 let rows1 = qb
4572 .execute_with_cache(&cache, "users", || async move {
4573 cc1.fetch_add(1, Ordering::SeqCst);
4574 let mut row = std::collections::HashMap::new();
4575 row.insert("id".to_string(), Value::I64(1));
4576 Ok(vec![row])
4577 })
4578 .await
4579 .unwrap();
4580 assert_eq!(rows1.len(), 1);
4581 assert_eq!(call_count.load(Ordering::SeqCst), 1);
4582
4583 let cc2 = Arc::clone(&call_count);
4584 let rows2 = qb
4585 .execute_with_cache(&cache, "users", || async move {
4586 cc2.fetch_add(1, Ordering::SeqCst);
4587 let mut row = std::collections::HashMap::new();
4588 row.insert("id".to_string(), Value::I64(2));
4589 Ok(vec![row])
4590 })
4591 .await
4592 .unwrap();
4593 assert_eq!(rows2.len(), 1);
4594 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
4595 assert_eq!(
4596 call_count.load(Ordering::SeqCst),
4597 1,
4598 "second call should hit cache, not loader"
4599 );
4600 }
4601
4602 #[tokio::test]
4603 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
4604 use crate::l2_cache::L2Cache;
4605 use std::sync::atomic::{AtomicU64, Ordering};
4606 use std::sync::Arc;
4607
4608 let dialect = get_dialect(DbType::MySQL).unwrap();
4609 let cache = L2Cache::new();
4610 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
4611
4612 let call_count = Arc::new(AtomicU64::new(0));
4613 let cc1 = Arc::clone(&call_count);
4614 let rows1 = qb
4615 .execute_with_cache(&cache, "users", || async move {
4616 cc1.fetch_add(1, Ordering::SeqCst);
4617 Ok(vec![])
4618 })
4619 .await
4620 .unwrap();
4621 assert!(rows1.is_empty());
4622
4623 let cc2 = Arc::clone(&call_count);
4624 let rows2 = qb
4625 .execute_with_cache(&cache, "users", || async move {
4626 cc2.fetch_add(1, Ordering::SeqCst);
4627 Ok(vec![])
4628 })
4629 .await
4630 .unwrap();
4631 assert!(rows2.is_empty());
4632 assert_eq!(
4633 call_count.load(Ordering::SeqCst),
4634 2,
4635 "without ttl, loader should be called each time"
4636 );
4637 }
4638}