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::time::Duration;
34
35pub struct QueryBuilder<M: Model> {
37 table: Option<String>,
38 select_columns: Vec<String>,
39 select_mode: crate::partial_model::SelectMode,
40 where_conditions: Vec<WhereCondition>,
41 order_by: Vec<OrderClause>,
42 group_by: Vec<String>,
43 having_conditions: Vec<WhereCondition>,
44 limit_value: Option<usize>,
45 offset_value: Option<usize>,
46 joins: Vec<JoinClause>,
47 dialect: Box<dyn Dialect>,
48 soft_delete_disabled: bool,
50 tenant_id_value: Option<i64>,
52 tenant_disabled: bool,
54 keyset_cursor: Option<KeysetCursor>,
60 cache_ttl: Option<Duration>,
65 lock_type: Option<LockType>,
73 insert_or_ignore: bool,
78 #[cfg(feature = "tenant-quota-rls-enhanced")]
83 rls_enhancer: Option<std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>>,
84 #[allow(dead_code)]
85 model: std::marker::PhantomData<M>,
86}
87
88#[derive(Debug, Clone)]
96struct KeysetCursor {
97 field: String,
99 value: Value,
101 direction: KeysetDirection,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107enum KeysetDirection {
108 After,
110 Before,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum AggExpr {
120 CountStar,
122 Sum(String),
124 Avg(String),
126 Min(String),
128 Max(String),
130}
131
132impl AggExpr {
133 pub fn validate(&self) -> Result<(), crate::DbError> {
135 match self {
136 AggExpr::CountStar => Ok(()),
137 AggExpr::Sum(col) | AggExpr::Avg(col) | AggExpr::Min(col) | AggExpr::Max(col) => {
138 crate::sql_safety::validate_identifier(col, "aggregate column")
139 }
140 }
141 }
142
143 fn render(&self, dialect: &dyn crate::dialect::Dialect) -> String {
145 match self {
146 AggExpr::CountStar => "COUNT(*)".to_string(),
147 AggExpr::Sum(col) => format!("SUM({})", dialect.quote(col)),
148 AggExpr::Avg(col) => format!("AVG({})", dialect.quote(col)),
149 AggExpr::Min(col) => format!("MIN({})", dialect.quote(col)),
150 AggExpr::Max(col) => format!("MAX({})", dialect.quote(col)),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum HavingOp {
158 Eq,
160 Ne,
162 Gt,
164 Ge,
166 Lt,
168 Le,
170}
171
172impl HavingOp {
173 pub fn as_sql(&self) -> &'static str {
175 match self {
176 HavingOp::Eq => "=",
177 HavingOp::Ne => "!=",
178 HavingOp::Gt => ">",
179 HavingOp::Ge => ">=",
180 HavingOp::Lt => "<",
181 HavingOp::Le => "<=",
182 }
183 }
184}
185
186#[derive(Debug, Clone)]
187#[allow(dead_code)]
188enum WhereCondition {
189 And(String),
194 Or(String),
196 Having(AggExpr, HavingOp, Value),
198 Eq(String, Value),
200 Ne(String, Value),
202 Gt(String, Value),
204 Ge(String, Value),
206 Lt(String, Value),
208 Le(String, Value),
210 Like(String, Value),
212 OrEq(String, Value),
214 OrNe(String, Value),
216 OrGt(String, Value),
218 OrGe(String, Value),
220 OrLt(String, Value),
222 OrLe(String, Value),
224 OrLike(String, Value),
226 In(String, Vec<Value>),
227 NotIn(String, Vec<Value>),
228 Between(String, Value, Value),
229 NotBetween(String, Value, Value),
230 Null(String),
231 NotNull(String),
232 Exists(String),
233 NotExists(String),
234 TypedExpr(String, Vec<Value>),
236}
237
238#[derive(Debug, Clone)]
239struct OrderClause {
240 field: String,
241 direction: OrderDirection,
242}
243
244#[derive(Debug, Clone)]
245enum OrderDirection {
246 Asc,
247 Desc,
248}
249
250#[derive(Debug, Clone)]
251#[allow(dead_code)]
252enum JoinClause {
253 Inner(String, String, String),
254 Left(String, String, String),
255 Right(String, String, String),
256 Cross(String, String),
257 Relation(
260 crate::relation_trait::JoinKind,
261 String,
262 String,
263 String,
264 String,
265 ),
266}
267
268impl<M: Model> QueryBuilder<M> {
269 pub fn new(dialect: Box<dyn Dialect>) -> Self {
271 Self {
272 table: None,
273 select_columns: vec!["*".to_string()],
274 select_mode: crate::partial_model::SelectMode::All,
275 where_conditions: Vec::new(),
276 order_by: Vec::new(),
277 group_by: Vec::new(),
278 having_conditions: Vec::new(),
279 limit_value: None,
280 offset_value: None,
281 joins: Vec::new(),
282 dialect,
283 soft_delete_disabled: false,
284 tenant_id_value: None,
285 tenant_disabled: false,
286 keyset_cursor: None,
287 cache_ttl: None,
288 lock_type: None,
289 insert_or_ignore: false,
290 #[cfg(feature = "tenant-quota-rls-enhanced")]
291 rls_enhancer: None,
292 model: std::marker::PhantomData,
293 }
294 }
295
296 pub fn table(mut self, table: impl Into<String>) -> Self {
298 let table_name = table.into();
299 #[cfg(feature = "multi-tenant-enhanced")]
301 {
302 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
303 if ctx.isolation_strategy
304 == crate::tenant_context::IsolationStrategy::SchemaIsolation
305 {
306 self.table = Some(crate::tenant_context::SchemaIsolationRouter::rewrite_table(
307 &table_name,
308 ctx.tenant_id,
309 ));
310 return self;
311 }
312 }
313 }
314 self.table = Some(table_name);
315 self
316 }
317
318 pub fn without_soft_delete(mut self) -> Self {
337 self.soft_delete_disabled = true;
338 self
339 }
340
341 pub fn is_soft_delete_disabled(&self) -> bool {
343 self.soft_delete_disabled
344 }
345
346 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
366 self.cache_ttl = Some(ttl);
367 self
368 }
369
370 pub fn get_cache_ttl(&self) -> Option<Duration> {
372 self.cache_ttl
373 }
374
375 pub async fn execute_with_cache<F, Fut>(
403 &self,
404 cache: &crate::l2_cache::L2Cache,
405 table: &str,
406 loader: F,
407 ) -> Result<crate::pool::QueryRows, crate::DbError>
408 where
409 F: FnOnce() -> Fut,
410 Fut: std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>,
411 {
412 let (sql, params) = self.build_select_with_params();
413 match self.get_cache_ttl() {
414 Some(ttl) => {
415 cache
416 .get_or_load_query(table, &sql, ¶ms, ttl, loader)
417 .await
418 }
419 None => loader().await,
420 }
421 }
422
423 pub fn lock_for_update(mut self) -> Result<Self, crate::error::DbError> {
448 if !self.dialect.supports_lock_for_update() {
449 return Err(crate::error::DbError::QueryError(
450 "FOR UPDATE lock is not supported by this dialect".to_string(),
451 ));
452 }
453 self.lock_type = Some(LockType::ForUpdate);
454 Ok(self)
455 }
456
457 pub fn lock_shared(mut self) -> Result<Self, crate::error::DbError> {
482 if !self.dialect.supports_lock_shared() {
483 return Err(crate::error::DbError::QueryError(
484 "Shared lock is not supported by this dialect".to_string(),
485 ));
486 }
487 self.lock_type = Some(LockType::Shared);
488 Ok(self)
489 }
490
491 pub fn insert_or_ignore(mut self) -> Self {
510 self.insert_or_ignore = true;
511 self
512 }
513
514 pub fn get_lock_type(&self) -> Option<LockType> {
516 self.lock_type
517 }
518
519 pub fn is_insert_or_ignore(&self) -> bool {
521 self.insert_or_ignore
522 }
523
524 pub fn clone_for_count(&self) -> Self {
531 Self {
532 table: self.table.clone(),
533 select_columns: vec!["*".to_string()],
534 select_mode: crate::partial_model::SelectMode::All,
535 where_conditions: self.where_conditions.clone(),
536 order_by: Vec::new(),
537 group_by: self.group_by.clone(),
538 having_conditions: self.having_conditions.clone(),
539 limit_value: None,
540 offset_value: None,
541 joins: self.joins.clone(),
542 dialect: self.dialect.clone_box(),
543 soft_delete_disabled: self.soft_delete_disabled,
544 tenant_id_value: self.tenant_id_value,
545 tenant_disabled: self.tenant_disabled,
546 keyset_cursor: None,
547 cache_ttl: None, lock_type: None, insert_or_ignore: false, #[cfg(feature = "tenant-quota-rls-enhanced")]
551 rls_enhancer: self.rls_enhancer.clone(),
552 model: std::marker::PhantomData,
553 }
554 }
555
556 fn soft_delete_field(&self) -> Option<&'static str> {
560 if self.soft_delete_disabled {
561 return None;
562 }
563 M::soft_delete_field()
564 }
565
566 fn build_soft_delete_condition(&self) -> Option<String> {
570 self.soft_delete_field()
571 .map(|field| format!("{} IS NULL", self.dialect.quote(field)))
572 }
573
574 pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
601 self.tenant_id_value = Some(tenant_id);
602 self
603 }
604
605 pub fn without_tenant(mut self) -> Self {
609 self.tenant_disabled = true;
610 self
611 }
612
613 #[cfg(feature = "tenant-quota-rls-enhanced")]
618 pub fn with_rls_policy_enhancer(
619 mut self,
620 enhancer: std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>,
621 ) -> Self {
622 self.rls_enhancer = Some(enhancer);
623 self
624 }
625
626 pub fn is_tenant_disabled(&self) -> bool {
628 self.tenant_disabled
629 }
630
631 fn tenant_field(&self) -> Option<&'static str> {
635 if self.tenant_disabled {
636 return None;
637 }
638 M::tenant_field()
639 }
640
641 fn tenant_id_value(&self) -> Option<i64> {
647 if self.tenant_disabled {
648 return None;
649 }
650 if let Some(tid) = self.tenant_id_value {
651 return Some(tid);
652 }
653 #[cfg(feature = "multi-tenant-enhanced")]
654 {
655 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
656 return Some(ctx.tenant_id);
657 }
658 }
659 None
660 }
661
662 fn build_tenant_condition(&self) -> Option<(String, Value)> {
667 let field = self.tenant_field()?;
668 let tid = self.tenant_id_value()?;
669 Some((
670 format!("{} = ?", self.dialect.quote(field)),
671 Value::I64(tid),
672 ))
673 }
674
675 #[cfg(feature = "multi-tenant-enhanced")]
681 #[allow(dead_code)]
682 fn require_tenant_condition(&self) -> Result<Option<(String, Value)>, crate::DbError> {
683 if self.tenant_field().is_none() {
684 return Ok(None);
685 }
686 if self.tenant_id_value.is_some() {
687 return Ok(self.build_tenant_condition());
688 }
689 if crate::tenant_context::TenantContext::current().is_some() {
690 return Ok(self.build_tenant_condition());
691 }
692 Err(crate::DbError::TenantError(
693 "TenantContextRequired: multi-tenant-enhanced feature enabled but no tenant context set"
694 .to_string(),
695 ))
696 }
697
698 pub fn select(mut self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
721 let mut quoted = Vec::with_capacity(columns.len());
722 for col in columns {
723 crate::sql_safety::validate_identifier(col, "select column")?;
724 quoted.push(self.dialect.quote(col));
725 }
726 self.select_columns = quoted;
727 Ok(self)
728 }
729
730 pub fn select_expr(mut self, columns: Vec<&str>) -> Self {
736 self.select_columns = columns.into_iter().map(|s| s.to_string()).collect();
737 self
738 }
739
740 pub fn select_quoted(self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
744 self.select(columns)
745 }
746
747 pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
761 self.where_conditions
762 .push(WhereCondition::Eq(field.into(), value));
763 self
764 }
765
766 #[cfg(feature = "type-safe-columns")]
771 pub fn where_eq_col<T: crate::column::Schema>(
772 mut self,
773 col: crate::column::Column<T>,
774 value: Value,
775 ) -> Self {
776 self.where_conditions
777 .push(WhereCondition::Eq(col.name().to_string(), value));
778 self
779 }
780
781 #[cfg(feature = "type-safe-columns")]
786 pub fn where_expr<E: crate::typed_ast::TypedExpression<SqlType = crate::typed_ast::Bool>>(
787 mut self,
788 expr: E,
789 ) -> Self {
790 let (sql, params) = expr.to_sql(&*self.dialect);
791 let values: Vec<Value> = params.into_iter().map(Value::String).collect();
792 self.where_conditions
793 .push(WhereCondition::TypedExpr(sql, values));
794 self
795 }
796
797 pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
799 self.where_conditions
800 .push(WhereCondition::Ne(field.into(), value));
801 self
802 }
803
804 pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
806 self.where_conditions
807 .push(WhereCondition::Gt(field.into(), value));
808 self
809 }
810
811 pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
813 self.where_conditions
814 .push(WhereCondition::Ge(field.into(), value));
815 self
816 }
817
818 pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
820 self.where_conditions
821 .push(WhereCondition::Lt(field.into(), value));
822 self
823 }
824
825 pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
827 self.where_conditions
828 .push(WhereCondition::Le(field.into(), value));
829 self
830 }
831
832 pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
844 self.where_conditions
845 .push(WhereCondition::Like(field.into(), pattern));
846 self
847 }
848
849 pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
853 self.where_conditions
854 .push(WhereCondition::OrEq(field.into(), value));
855 self
856 }
857
858 pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
860 self.where_conditions
861 .push(WhereCondition::OrNe(field.into(), value));
862 self
863 }
864
865 pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
867 self.where_conditions
868 .push(WhereCondition::OrGt(field.into(), value));
869 self
870 }
871
872 pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
874 self.where_conditions
875 .push(WhereCondition::OrGe(field.into(), value));
876 self
877 }
878
879 pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
881 self.where_conditions
882 .push(WhereCondition::OrLt(field.into(), value));
883 self
884 }
885
886 pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
888 self.where_conditions
889 .push(WhereCondition::OrLe(field.into(), value));
890 self
891 }
892
893 pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
895 self.where_conditions
896 .push(WhereCondition::OrLike(field.into(), pattern));
897 self
898 }
899
900 pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
902 self.where_conditions
903 .push(WhereCondition::In(field.into(), values));
904 self
905 }
906
907 pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
909 self.where_conditions
910 .push(WhereCondition::NotIn(field.into(), values));
911 self
912 }
913
914 pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
916 self.where_conditions
917 .push(WhereCondition::Between(field.into(), start, end));
918 self
919 }
920
921 pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
923 self.where_conditions
924 .push(WhereCondition::NotBetween(field.into(), start, end));
925 self
926 }
927
928 pub fn where_null(mut self, field: impl Into<String>) -> Self {
930 self.where_conditions
931 .push(WhereCondition::Null(field.into()));
932 self
933 }
934
935 pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
937 self.where_conditions
938 .push(WhereCondition::NotNull(field.into()));
939 self
940 }
941
942 pub fn order_by(mut self, field: impl Into<String>) -> Self {
944 self.order_by.push(OrderClause {
945 field: field.into(),
946 direction: OrderDirection::Asc,
947 });
948 self
949 }
950
951 pub fn order_desc(mut self, field: impl Into<String>) -> Self {
953 self.order_by.push(OrderClause {
954 field: field.into(),
955 direction: OrderDirection::Desc,
956 });
957 self
958 }
959
960 pub fn group_by(mut self, field: impl Into<String>) -> Self {
962 self.group_by.push(field.into());
963 self
964 }
965
966 pub fn where_eq_typed<C: TypedColumn>(mut self, value: Value) -> Self {
985 self.where_conditions
986 .push(WhereCondition::Eq(C::NAME.to_string(), value));
987 self
988 }
989
990 pub fn where_ne_typed<C: TypedColumn>(mut self, value: Value) -> Self {
992 self.where_conditions
993 .push(WhereCondition::Ne(C::NAME.to_string(), value));
994 self
995 }
996
997 pub fn where_gt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
999 self.where_conditions
1000 .push(WhereCondition::Gt(C::NAME.to_string(), value));
1001 self
1002 }
1003
1004 pub fn where_ge_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1006 self.where_conditions
1007 .push(WhereCondition::Ge(C::NAME.to_string(), value));
1008 self
1009 }
1010
1011 pub fn where_lt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1013 self.where_conditions
1014 .push(WhereCondition::Lt(C::NAME.to_string(), value));
1015 self
1016 }
1017
1018 pub fn where_le_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1020 self.where_conditions
1021 .push(WhereCondition::Le(C::NAME.to_string(), value));
1022 self
1023 }
1024
1025 pub fn where_null_typed<C: TypedColumn>(mut self) -> Self {
1027 self.where_conditions
1028 .push(WhereCondition::Null(C::NAME.to_string()));
1029 self
1030 }
1031
1032 pub fn where_not_null_typed<C: TypedColumn>(mut self) -> Self {
1034 self.where_conditions
1035 .push(WhereCondition::NotNull(C::NAME.to_string()));
1036 self
1037 }
1038
1039 pub fn order_by_typed<C: TypedColumn>(mut self) -> Self {
1041 self.order_by.push(OrderClause {
1042 field: C::NAME.to_string(),
1043 direction: OrderDirection::Asc,
1044 });
1045 self
1046 }
1047
1048 pub fn order_desc_typed<C: TypedColumn>(mut self) -> Self {
1050 self.order_by.push(OrderClause {
1051 field: C::NAME.to_string(),
1052 direction: OrderDirection::Desc,
1053 });
1054 self
1055 }
1056
1057 pub fn group_by_typed<C: TypedColumn>(mut self) -> Self {
1059 self.group_by.push(C::NAME.to_string());
1060 self
1061 }
1062
1063 pub fn select_typed<C: TypedColumn>(mut self) -> Self {
1067 self.select_columns.push(C::NAME.to_string());
1068 self
1069 }
1070
1071 pub fn select_typed_cols<C: TypedColumn, const N: usize>(mut self) -> Self {
1073 self.select_columns.push(C::NAME.to_string());
1076 self
1077 }
1078
1079 pub fn having(
1103 mut self,
1104 agg: AggExpr,
1105 op: HavingOp,
1106 value: Value,
1107 ) -> Result<Self, crate::DbError> {
1108 agg.validate()?;
1109 self.having_conditions
1110 .push(WhereCondition::Having(agg, op, value));
1111 Ok(self)
1112 }
1113
1114 pub fn limit(mut self, limit: usize) -> Self {
1116 self.limit_value = Some(limit);
1117 self
1118 }
1119
1120 pub fn offset(mut self, offset: usize) -> Self {
1122 self.offset_value = Some(offset);
1123 self
1124 }
1125
1126 pub fn page(mut self, page: usize, page_size: usize) -> Self {
1128 self.limit_value = Some(page_size);
1129 self.offset_value = Some((page.saturating_sub(1)) * page_size);
1130 self
1131 }
1132
1133 pub fn keyset_after(
1179 mut self,
1180 field: impl Into<String>,
1181 cursor_value: Value,
1182 page_size: usize,
1183 ) -> Self {
1184 let field_str = field.into();
1185 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1187 existing.direction = OrderDirection::Asc;
1188 } else {
1189 self.order_by.push(OrderClause {
1190 field: field_str.clone(),
1191 direction: OrderDirection::Asc,
1192 });
1193 }
1194 self.limit_value = Some(page_size);
1195 self.offset_value = None;
1197 self.keyset_cursor = Some(KeysetCursor {
1198 field: field_str,
1199 value: cursor_value,
1200 direction: KeysetDirection::After,
1201 });
1202 self
1203 }
1204
1205 pub fn keyset_before(
1251 mut self,
1252 field: impl Into<String>,
1253 cursor_value: Value,
1254 page_size: usize,
1255 ) -> Self {
1256 let field_str = field.into();
1257 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1259 existing.direction = OrderDirection::Desc;
1260 } else {
1261 self.order_by.push(OrderClause {
1262 field: field_str.clone(),
1263 direction: OrderDirection::Desc,
1264 });
1265 }
1266 self.limit_value = Some(page_size);
1267 self.offset_value = None;
1268 self.keyset_cursor = Some(KeysetCursor {
1269 field: field_str,
1270 value: cursor_value,
1271 direction: KeysetDirection::Before,
1272 });
1273 self
1274 }
1275
1276 pub fn join_inner(
1278 mut self,
1279 table: impl Into<String>,
1280 on_left: impl Into<String>,
1281 on_right: impl Into<String>,
1282 ) -> Self {
1283 self.joins.push(JoinClause::Inner(
1284 table.into(),
1285 on_left.into(),
1286 on_right.into(),
1287 ));
1288 self
1289 }
1290
1291 pub fn join_left(
1293 mut self,
1294 table: impl Into<String>,
1295 on_left: impl Into<String>,
1296 on_right: impl Into<String>,
1297 ) -> Self {
1298 self.joins.push(JoinClause::Left(
1299 table.into(),
1300 on_left.into(),
1301 on_right.into(),
1302 ));
1303 self
1304 }
1305
1306 pub fn join_right(
1308 mut self,
1309 table: impl Into<String>,
1310 on_left: impl Into<String>,
1311 on_right: impl Into<String>,
1312 ) -> Self {
1313 self.joins.push(JoinClause::Right(
1314 table.into(),
1315 on_left.into(),
1316 on_right.into(),
1317 ));
1318 self
1319 }
1320
1321 pub fn join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1341 let def = relation.def();
1342 let join_kind = def.kind.default_join_type();
1343 self.joins.push(JoinClause::Relation(
1344 join_kind,
1345 def.from_entity.to_string(),
1346 def.from_key.to_string(),
1347 def.to_entity.to_string(),
1348 def.to_key.to_string(),
1349 ));
1350 self
1351 }
1352
1353 pub fn left_join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1357 let def = relation.def();
1358 self.joins.push(JoinClause::Relation(
1359 crate::relation_trait::JoinKind::Left,
1360 def.from_entity.to_string(),
1361 def.from_key.to_string(),
1362 def.to_entity.to_string(),
1363 def.to_key.to_string(),
1364 ));
1365 self
1366 }
1367
1368 pub fn select_only(mut self) -> Self {
1384 self.select_mode = crate::partial_model::SelectMode::Partial;
1385 self.select_columns.clear();
1386 self
1387 }
1388
1389 pub fn column(mut self, column: impl Into<String>) -> Self {
1393 self.select_columns.push(column.into());
1394 self
1395 }
1396
1397 pub fn columns(mut self, cols: Vec<impl Into<String>>) -> Self {
1399 self.select_columns.extend(cols.into_iter().map(Into::into));
1400 self
1401 }
1402
1403 pub fn column_as(mut self, expr: crate::partial_model::Expr, alias: impl Into<String>) -> Self {
1419 self.select_columns.push(expr.render_as(&alias.into()));
1420 self
1421 }
1422
1423 #[tracing::instrument(skip(self), fields(op = "select"))]
1456 pub fn build_select(&self) -> String {
1457 let table = self
1458 .table
1459 .clone()
1460 .unwrap_or_else(|| M::table_name().to_string());
1461
1462 let columns = if self.select_columns.is_empty() {
1463 "*".to_string()
1464 } else {
1465 self.select_columns.join(", ")
1466 };
1467
1468 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1469 "SELECT {} FROM {}",
1470 columns,
1471 self.dialect.quote(&table)
1472 ));
1473
1474 for join in &self.joins {
1475 match join {
1476 JoinClause::Inner(t, l, r) => {
1477 sql.push_str(&format!(
1478 " INNER JOIN {} ON {} = {}",
1479 self.dialect.quote(t),
1480 self.dialect.quote(l),
1481 self.dialect.quote(r)
1482 ));
1483 }
1484 JoinClause::Left(t, l, r) => {
1485 sql.push_str(&format!(
1486 " LEFT JOIN {} ON {} = {}",
1487 self.dialect.quote(t),
1488 self.dialect.quote(l),
1489 self.dialect.quote(r)
1490 ));
1491 }
1492 JoinClause::Right(t, l, r) => {
1493 sql.push_str(&format!(
1494 " RIGHT JOIN {} ON {} = {}",
1495 self.dialect.quote(t),
1496 self.dialect.quote(l),
1497 self.dialect.quote(r)
1498 ));
1499 }
1500 JoinClause::Cross(t, on) => {
1501 sql.push_str(&format!(
1502 " CROSS JOIN {} ON {}",
1503 self.dialect.quote(t),
1504 self.dialect.quote(on)
1505 ));
1506 }
1507 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1508 sql.push_str(&format!(
1509 " {} {} ON {}.{} = {}.{}",
1510 kind.as_sql(),
1511 self.dialect.quote(tt),
1512 self.dialect.quote(ft),
1513 self.dialect.quote(fk),
1514 self.dialect.quote(tt),
1515 self.dialect.quote(tk)
1516 ));
1517 }
1518 }
1519 }
1520
1521 let where_clause = self.build_where_clause();
1523 if !where_clause.is_empty() {
1524 sql.push_str(&where_clause);
1525 }
1526
1527 if !self.group_by.is_empty() {
1528 let cols: Vec<String> = self
1529 .group_by
1530 .iter()
1531 .map(|c| self.dialect.quote(c))
1532 .collect();
1533 sql.push_str(" GROUP BY ");
1534 sql.push_str(&cols.join(", "));
1535 }
1536
1537 if !self.having_conditions.is_empty() {
1538 sql.push_str(" HAVING ");
1539 for (i, cond) in self.having_conditions.iter().enumerate() {
1540 if i > 0 {
1541 sql.push_str(" AND ");
1542 }
1543 match cond {
1544 WhereCondition::And(c) => sql.push_str(c),
1545 WhereCondition::Having(agg, op, value) => {
1547 sql.push_str(&format!(
1548 "{} {} {}",
1549 agg.render(&*self.dialect),
1550 op.as_sql(),
1551 value.to_param_with_dialect(&*self.dialect)
1552 ));
1553 }
1554 _ => {}
1555 }
1556 }
1557 }
1558
1559 if !self.order_by.is_empty() {
1560 let order_cols: Vec<String> = self
1561 .order_by
1562 .iter()
1563 .map(|o| {
1564 let dir = match o.direction {
1565 OrderDirection::Asc => " ASC",
1566 OrderDirection::Desc => " DESC",
1567 };
1568 format!("{}{}", self.dialect.quote(&o.field), dir)
1569 })
1570 .collect();
1571 sql.push_str(" ORDER BY ");
1572 sql.push_str(&order_cols.join(", "));
1573 }
1574
1575 if let Some(limit) = self.limit_value {
1576 sql.push_str(&format!(" LIMIT {}", limit));
1577 }
1578
1579 if let Some(offset) = self.offset_value {
1580 sql.push_str(&format!(" OFFSET {}", offset));
1581 }
1582
1583 sql.into_string()
1584 }
1585
1586 fn build_where_clause(&self) -> String {
1592 self.build_where_clause_with_options(true)
1593 }
1594
1595 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1603 let soft_delete_cond = if include_soft_delete {
1605 self.build_soft_delete_condition()
1606 } else {
1607 None
1608 };
1609
1610 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1612 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1614 });
1615
1616 if self.where_conditions.is_empty()
1618 && soft_delete_cond.is_none()
1619 && tenant_cond.is_none()
1620 && self.keyset_cursor.is_none()
1621 {
1622 return String::new();
1623 }
1624
1625 let mut conditions: Vec<String> = self
1627 .where_conditions
1628 .iter()
1629 .map(|cond| match cond {
1630 WhereCondition::And(c) => c.clone(),
1631 WhereCondition::Or(c) => format!("OR {}", c),
1632 WhereCondition::Eq(f, v) => format!(
1634 "{} = {}",
1635 self.dialect.quote(f),
1636 v.to_param_with_dialect(&*self.dialect)
1637 ),
1638 WhereCondition::Ne(f, v) => format!(
1639 "{} != {}",
1640 self.dialect.quote(f),
1641 v.to_param_with_dialect(&*self.dialect)
1642 ),
1643 WhereCondition::Gt(f, v) => format!(
1644 "{} > {}",
1645 self.dialect.quote(f),
1646 v.to_param_with_dialect(&*self.dialect)
1647 ),
1648 WhereCondition::Ge(f, v) => format!(
1649 "{} >= {}",
1650 self.dialect.quote(f),
1651 v.to_param_with_dialect(&*self.dialect)
1652 ),
1653 WhereCondition::Lt(f, v) => format!(
1654 "{} < {}",
1655 self.dialect.quote(f),
1656 v.to_param_with_dialect(&*self.dialect)
1657 ),
1658 WhereCondition::Le(f, v) => format!(
1659 "{} <= {}",
1660 self.dialect.quote(f),
1661 v.to_param_with_dialect(&*self.dialect)
1662 ),
1663 WhereCondition::Like(f, v) => format!(
1664 "{} LIKE {}",
1665 self.dialect.quote(f),
1666 v.to_param_with_dialect(&*self.dialect)
1667 ),
1668 WhereCondition::OrEq(f, v) => format!(
1669 "OR {} = {}",
1670 self.dialect.quote(f),
1671 v.to_param_with_dialect(&*self.dialect)
1672 ),
1673 WhereCondition::OrNe(f, v) => format!(
1674 "OR {} != {}",
1675 self.dialect.quote(f),
1676 v.to_param_with_dialect(&*self.dialect)
1677 ),
1678 WhereCondition::OrGt(f, v) => format!(
1679 "OR {} > {}",
1680 self.dialect.quote(f),
1681 v.to_param_with_dialect(&*self.dialect)
1682 ),
1683 WhereCondition::OrGe(f, v) => format!(
1684 "OR {} >= {}",
1685 self.dialect.quote(f),
1686 v.to_param_with_dialect(&*self.dialect)
1687 ),
1688 WhereCondition::OrLt(f, v) => format!(
1689 "OR {} < {}",
1690 self.dialect.quote(f),
1691 v.to_param_with_dialect(&*self.dialect)
1692 ),
1693 WhereCondition::OrLe(f, v) => format!(
1694 "OR {} <= {}",
1695 self.dialect.quote(f),
1696 v.to_param_with_dialect(&*self.dialect)
1697 ),
1698 WhereCondition::OrLike(f, v) => format!(
1699 "OR {} LIKE {}",
1700 self.dialect.quote(f),
1701 v.to_param_with_dialect(&*self.dialect)
1702 ),
1703 WhereCondition::In(f, vals) => {
1704 let vals_str: Vec<String> = vals
1706 .iter()
1707 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1708 .collect();
1709 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1710 }
1711 WhereCondition::NotIn(f, vals) => {
1712 let vals_str: Vec<String> = vals
1713 .iter()
1714 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1715 .collect();
1716 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1717 }
1718 WhereCondition::Between(f, start, end) => {
1719 format!(
1720 "{} BETWEEN {} AND {}",
1721 self.dialect.quote(f),
1722 start.to_param_with_dialect(&*self.dialect),
1723 end.to_param_with_dialect(&*self.dialect)
1724 )
1725 }
1726 WhereCondition::NotBetween(f, start, end) => {
1727 format!(
1728 "{} NOT BETWEEN {} AND {}",
1729 self.dialect.quote(f),
1730 start.to_param_with_dialect(&*self.dialect),
1731 end.to_param_with_dialect(&*self.dialect)
1732 )
1733 }
1734 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1735 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1736 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1737 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1738 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1739 WhereCondition::Having(..) => String::new(),
1741 })
1742 .collect();
1743
1744 if let Some(sd_cond) = soft_delete_cond {
1746 conditions.push(sd_cond);
1747 }
1748
1749 if let Some(t_cond) = tenant_cond {
1751 conditions.push(t_cond);
1752 }
1753
1754 if let Some(ref cursor) = self.keyset_cursor {
1756 let op = match cursor.direction {
1757 KeysetDirection::After => ">",
1758 KeysetDirection::Before => "<",
1759 };
1760 conditions.push(format!(
1761 "{} {} {}",
1762 self.dialect.quote(&cursor.field),
1763 op,
1764 cursor.value.to_param_with_dialect(&*self.dialect)
1765 ));
1766 }
1767
1768 if conditions.is_empty() {
1769 return String::new();
1770 }
1771
1772 let mut groups: Vec<Vec<String>> = Vec::new();
1775 let mut current_group: Vec<String> = Vec::new();
1776 for cond in conditions.iter() {
1777 if let Some(stripped) = cond.strip_prefix("OR ") {
1778 current_group.push(stripped.to_string());
1780 } else {
1781 if !current_group.is_empty() {
1783 groups.push(std::mem::take(&mut current_group));
1784 }
1785 current_group.push(cond.clone());
1786 }
1787 }
1788 if !current_group.is_empty() {
1789 groups.push(current_group);
1790 }
1791
1792 let group_strs: Vec<String> = groups
1793 .iter()
1794 .map(|g| {
1795 if g.len() == 1 {
1796 g[0].clone()
1797 } else {
1798 format!("({})", g.join(" OR "))
1799 }
1800 })
1801 .collect();
1802
1803 format!(" WHERE {}", group_strs.join(" AND "))
1805 }
1806
1807 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1808 pub fn build_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1810 let table = self
1811 .table
1812 .clone()
1813 .unwrap_or_else(|| M::table_name().to_string());
1814
1815 if data.is_empty() {
1816 return String::new();
1817 }
1818
1819 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1820 let values: Vec<String> = data
1822 .values()
1823 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1824 .collect();
1825
1826 crate::sql_buffer::SqlBuffer::from_str(&format!(
1827 "INSERT INTO {} ({}) VALUES ({})",
1828 self.dialect.quote(&table),
1829 columns.join(", "),
1830 values.join(", ")
1831 ))
1832 .into_string()
1833 }
1834
1835 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1836 pub fn build_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1838 let table = self
1839 .table
1840 .clone()
1841 .unwrap_or_else(|| M::table_name().to_string());
1842
1843 if data.is_empty() {
1844 return String::new();
1845 }
1846
1847 let set_clauses: Vec<String> = data
1848 .iter()
1849 .map(|(k, v)| {
1850 format!(
1851 "{} = {}",
1852 self.dialect.quote(k),
1853 v.to_param_with_dialect(&*self.dialect)
1854 )
1855 })
1856 .collect();
1857
1858 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1859 "UPDATE {} SET {}",
1860 self.dialect.quote(&table),
1861 set_clauses.join(", ")
1862 ));
1863
1864 sql.push_str(&self.build_where_clause());
1865 sql.into_string()
1866 }
1867
1868 #[tracing::instrument(skip(self), fields(op = "delete"))]
1879 pub fn build_delete(&self) -> String {
1880 let table = self
1881 .table
1882 .clone()
1883 .unwrap_or_else(|| M::table_name().to_string());
1884
1885 if let Some(field) = self.soft_delete_field() {
1887 let where_clause = self.build_where_clause();
1888 return format!(
1889 "UPDATE {} SET {} = NOW(){}",
1890 self.dialect.quote(&table),
1891 self.dialect.quote(field),
1892 where_clause
1893 );
1894 }
1895
1896 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1897 "DELETE FROM {}",
1898 self.dialect.quote(&table)
1899 ));
1900 sql.push_str(&self.build_where_clause());
1901 sql.into_string()
1902 }
1903
1904 pub fn build_force_delete(&self) -> String {
1914 let table = self
1915 .table
1916 .clone()
1917 .unwrap_or_else(|| M::table_name().to_string());
1918
1919 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1920 sql.push_str(&self.build_where_clause_with_options(false));
1922 sql
1923 }
1924
1925 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
1935 self.build_where_clause_with_params_options(true)
1937 }
1938
1939 fn build_where_clause_with_params_options(
1948 &self,
1949 include_soft_delete: bool,
1950 ) -> (String, Vec<Value>) {
1951 let soft_delete_cond = if include_soft_delete {
1953 self.build_soft_delete_condition()
1954 } else {
1955 None
1956 };
1957
1958 let tenant_cond = self.build_tenant_condition();
1960
1961 #[cfg(feature = "tenant-quota-rls-enhanced")]
1963 let rls_cond: Option<(String, Vec<Value>)> = {
1964 if let Some(ref enhancer) = self.rls_enhancer {
1965 if let Some(ref table) = self.table {
1966 if let Some(tid) = self.tenant_id_value() {
1967 match enhancer.enhance_query(table, &tid.to_string()) {
1968 Ok(Some(c)) => Some((c.sql_fragment, c.params)),
1969 Ok(None) => None,
1970 Err(_) => None,
1971 }
1972 } else {
1973 None
1974 }
1975 } else {
1976 None
1977 }
1978 } else {
1979 None
1980 }
1981 };
1982 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
1983 let rls_cond: Option<(String, Vec<Value>)> = None;
1984
1985 if self.where_conditions.is_empty()
1987 && soft_delete_cond.is_none()
1988 && tenant_cond.is_none()
1989 && self.keyset_cursor.is_none()
1990 && rls_cond.is_none()
1991 {
1992 return (String::new(), Vec::new());
1993 }
1994
1995 let mut params = Vec::new();
1996
1997 let mut conditions: Vec<String> = self
1998 .where_conditions
1999 .iter()
2000 .map(|cond| match cond {
2001 WhereCondition::And(c) => c.clone(),
2002 WhereCondition::Or(c) => format!("OR {}", c),
2003 WhereCondition::Eq(f, v) => {
2005 params.push(v.clone());
2006 format!("{} = ?", self.dialect.quote(f))
2007 }
2008 WhereCondition::Ne(f, v) => {
2009 params.push(v.clone());
2010 format!("{} != ?", self.dialect.quote(f))
2011 }
2012 WhereCondition::Gt(f, v) => {
2013 params.push(v.clone());
2014 format!("{} > ?", self.dialect.quote(f))
2015 }
2016 WhereCondition::Ge(f, v) => {
2017 params.push(v.clone());
2018 format!("{} >= ?", self.dialect.quote(f))
2019 }
2020 WhereCondition::Lt(f, v) => {
2021 params.push(v.clone());
2022 format!("{} < ?", self.dialect.quote(f))
2023 }
2024 WhereCondition::Le(f, v) => {
2025 params.push(v.clone());
2026 format!("{} <= ?", self.dialect.quote(f))
2027 }
2028 WhereCondition::Like(f, v) => {
2029 params.push(v.clone());
2030 format!("{} LIKE ?", self.dialect.quote(f))
2031 }
2032 WhereCondition::OrEq(f, v) => {
2033 params.push(v.clone());
2034 format!("OR {} = ?", self.dialect.quote(f))
2035 }
2036 WhereCondition::OrNe(f, v) => {
2037 params.push(v.clone());
2038 format!("OR {} != ?", self.dialect.quote(f))
2039 }
2040 WhereCondition::OrGt(f, v) => {
2041 params.push(v.clone());
2042 format!("OR {} > ?", self.dialect.quote(f))
2043 }
2044 WhereCondition::OrGe(f, v) => {
2045 params.push(v.clone());
2046 format!("OR {} >= ?", self.dialect.quote(f))
2047 }
2048 WhereCondition::OrLt(f, v) => {
2049 params.push(v.clone());
2050 format!("OR {} < ?", self.dialect.quote(f))
2051 }
2052 WhereCondition::OrLe(f, v) => {
2053 params.push(v.clone());
2054 format!("OR {} <= ?", self.dialect.quote(f))
2055 }
2056 WhereCondition::OrLike(f, v) => {
2057 params.push(v.clone());
2058 format!("OR {} LIKE ?", self.dialect.quote(f))
2059 }
2060 WhereCondition::In(f, vals) => {
2061 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2062 params.extend(vals.iter().cloned());
2063 format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
2064 }
2065 WhereCondition::NotIn(f, vals) => {
2066 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2067 params.extend(vals.iter().cloned());
2068 format!(
2069 "{} NOT IN ({})",
2070 self.dialect.quote(f),
2071 placeholders.join(", ")
2072 )
2073 }
2074 WhereCondition::Between(f, start, end) => {
2075 params.push(start.clone());
2076 params.push(end.clone());
2077 format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
2078 }
2079 WhereCondition::NotBetween(f, start, end) => {
2080 params.push(start.clone());
2081 params.push(end.clone());
2082 format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
2083 }
2084 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
2085 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
2086 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
2087 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
2088 WhereCondition::TypedExpr(sql, expr_params) => {
2089 params.extend(expr_params.iter().cloned());
2090 sql.clone()
2091 }
2092 WhereCondition::Having(..) => String::new(),
2094 })
2095 .collect();
2096
2097 if let Some(sd_cond) = soft_delete_cond {
2099 conditions.push(sd_cond);
2100 }
2101
2102 if let Some((t_sql, t_value)) = tenant_cond {
2104 conditions.push(t_sql);
2105 params.push(t_value);
2106 }
2107
2108 if let Some((rls_sql, rls_params)) = rls_cond {
2110 conditions.push(rls_sql);
2111 params.extend(rls_params);
2112 }
2113
2114 if let Some(ref cursor) = self.keyset_cursor {
2116 let op = match cursor.direction {
2117 KeysetDirection::After => ">",
2118 KeysetDirection::Before => "<",
2119 };
2120 conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
2121 params.push(cursor.value.clone());
2122 }
2123
2124 if conditions.is_empty() {
2125 return (String::new(), params);
2126 }
2127
2128 let mut groups: Vec<Vec<String>> = Vec::new();
2130 let mut current_group: Vec<String> = Vec::new();
2131 for cond in conditions.iter() {
2132 if let Some(stripped) = cond.strip_prefix("OR ") {
2133 current_group.push(stripped.to_string());
2134 } else {
2135 if !current_group.is_empty() {
2136 groups.push(std::mem::take(&mut current_group));
2137 }
2138 current_group.push(cond.clone());
2139 }
2140 }
2141 if !current_group.is_empty() {
2142 groups.push(current_group);
2143 }
2144
2145 let group_strs: Vec<String> = groups
2146 .iter()
2147 .map(|g| {
2148 if g.len() == 1 {
2149 g[0].clone()
2150 } else {
2151 format!("({})", g.join(" OR "))
2152 }
2153 })
2154 .collect();
2155
2156 (format!(" WHERE {}", group_strs.join(" AND ")), params)
2158 }
2159
2160 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
2165 let table = self
2166 .table
2167 .clone()
2168 .unwrap_or_else(|| M::table_name().to_string());
2169 let columns = if self.select_columns.is_empty() {
2170 "*".to_string()
2171 } else {
2172 self.select_columns.join(", ")
2173 };
2174
2175 let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
2176
2177 for join in &self.joins {
2178 match join {
2179 JoinClause::Inner(t, l, r) => {
2180 sql.push_str(&format!(
2181 " INNER JOIN {} ON {} = {}",
2182 self.dialect.quote(t),
2183 self.dialect.quote(l),
2184 self.dialect.quote(r)
2185 ));
2186 }
2187 JoinClause::Left(t, l, r) => {
2188 sql.push_str(&format!(
2189 " LEFT JOIN {} ON {} = {}",
2190 self.dialect.quote(t),
2191 self.dialect.quote(l),
2192 self.dialect.quote(r)
2193 ));
2194 }
2195 JoinClause::Right(t, l, r) => {
2196 sql.push_str(&format!(
2197 " RIGHT JOIN {} ON {} = {}",
2198 self.dialect.quote(t),
2199 self.dialect.quote(l),
2200 self.dialect.quote(r)
2201 ));
2202 }
2203 JoinClause::Cross(t, on) => {
2204 sql.push_str(&format!(
2205 " CROSS JOIN {} ON {}",
2206 self.dialect.quote(t),
2207 self.dialect.quote(on)
2208 ));
2209 }
2210 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2211 sql.push_str(&format!(
2212 " {} {} ON {}.{} = {}.{}",
2213 kind.as_sql(),
2214 self.dialect.quote(tt),
2215 self.dialect.quote(ft),
2216 self.dialect.quote(fk),
2217 self.dialect.quote(tt),
2218 self.dialect.quote(tk)
2219 ));
2220 }
2221 }
2222 }
2223
2224 let mut params = Vec::new();
2225 let (where_clause, where_params) = self.build_where_clause_with_params();
2227 if !where_clause.is_empty() {
2228 sql.push_str(&where_clause);
2229 params = where_params;
2230 }
2231
2232 if !self.group_by.is_empty() {
2233 let cols: Vec<String> = self
2234 .group_by
2235 .iter()
2236 .map(|c| self.dialect.quote(c))
2237 .collect();
2238 sql.push_str(" GROUP BY ");
2239 sql.push_str(&cols.join(", "));
2240 }
2241
2242 if !self.having_conditions.is_empty() {
2243 sql.push_str(" HAVING ");
2244 for (i, cond) in self.having_conditions.iter().enumerate() {
2245 if i > 0 {
2246 sql.push_str(" AND ");
2247 }
2248 match cond {
2249 WhereCondition::And(c) => sql.push_str(c),
2250 WhereCondition::Having(agg, op, value) => {
2252 params.push(value.clone());
2253 sql.push_str(&format!("{} {} ?", agg.render(&*self.dialect), op.as_sql()));
2254 }
2255 _ => {}
2256 }
2257 }
2258 }
2259
2260 if !self.order_by.is_empty() {
2261 let order_cols: Vec<String> = self
2262 .order_by
2263 .iter()
2264 .map(|o| {
2265 let dir = match o.direction {
2266 OrderDirection::Asc => " ASC",
2267 OrderDirection::Desc => " DESC",
2268 };
2269 format!("{}{}", self.dialect.quote(&o.field), dir)
2270 })
2271 .collect();
2272 sql.push_str(" ORDER BY ");
2273 sql.push_str(&order_cols.join(", "));
2274 }
2275
2276 if let Some(limit) = self.limit_value {
2277 sql.push_str(&format!(" LIMIT {}", limit));
2278 }
2279 if let Some(offset) = self.offset_value {
2280 sql.push_str(&format!(" OFFSET {}", offset));
2281 }
2282
2283 if let Some(lock_type) = &self.lock_type {
2285 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2286 sql.push(' ');
2287 sql.push_str(&lock_clause);
2288 }
2289 }
2290
2291 (sql, params)
2292 }
2293
2294 pub fn build_insert_with_params(
2296 &self,
2297 data: &std::collections::HashMap<String, Value>,
2298 ) -> (String, Vec<Value>) {
2299 let table = self
2300 .table
2301 .clone()
2302 .unwrap_or_else(|| M::table_name().to_string());
2303 if data.is_empty() {
2304 return (String::new(), Vec::new());
2305 }
2306
2307 let mut columns = Vec::with_capacity(data.len());
2308 let mut params = Vec::with_capacity(data.len());
2309 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2310 for (k, v) in data.iter() {
2311 columns.push(self.dialect.quote(k));
2312 params.push(v.clone());
2313 }
2314
2315 let insert_clause = if self.insert_or_ignore {
2317 self.dialect.build_insert_or_ignore_prefix(&table)
2318 } else {
2319 format!("INSERT INTO {}", self.dialect.quote(&table))
2320 };
2321
2322 let sql = format!(
2323 "{} ({}) VALUES ({})",
2324 insert_clause,
2325 columns.join(", "),
2326 placeholders.join(", ")
2327 );
2328 (sql, params)
2329 }
2330
2331 pub fn build_batch_insert_with_params(
2338 &self,
2339 rows: &[std::collections::HashMap<String, Value>],
2340 ) -> (String, Vec<Value>) {
2341 let table = self
2342 .table
2343 .clone()
2344 .unwrap_or_else(|| M::table_name().to_string());
2345 if rows.is_empty() {
2346 return (String::new(), Vec::new());
2347 }
2348
2349 let first_row = &rows[0];
2351 let columns: Vec<String> = first_row.keys().cloned().collect();
2352 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2353
2354 let mut params = Vec::with_capacity(rows.len() * columns.len());
2355 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2356 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2357 let mut param_idx = 1usize;
2358 for row in rows {
2359 let placeholders: Vec<String> = columns
2360 .iter()
2361 .map(|col| match row.get(col) {
2362 Some(v) => {
2363 params.push(v.clone());
2364 if is_pg {
2365 let p = format!("${}", param_idx);
2366 param_idx += 1;
2367 p
2368 } else {
2369 "?".to_string()
2370 }
2371 }
2372 None => "NULL".to_string(),
2373 })
2374 .collect();
2375 value_groups.push(format!("({})", placeholders.join(", ")));
2376 }
2377
2378 let sql = format!(
2379 "INSERT INTO {} ({}) VALUES {}",
2380 self.dialect.quote(&table),
2381 quoted_columns.join(", "),
2382 value_groups.join(", ")
2383 );
2384 (sql, params)
2385 }
2386
2387 pub fn build_batch_upsert_with_params(
2408 &self,
2409 rows: &[std::collections::HashMap<String, Value>],
2410 conflict_columns: &[&str],
2411 update_columns: &[&str],
2412 ) -> Result<(String, Vec<Value>), crate::DbError> {
2413 if rows.is_empty() {
2414 return Err(crate::DbError::InvalidInput(
2415 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2416 ));
2417 }
2418
2419 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2421 if insert_sql.is_empty() {
2422 return Err(crate::DbError::InvalidInput(
2423 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2424 ));
2425 }
2426
2427 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2429
2430 let conflict_clause = self
2432 .dialect
2433 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2434 .ok_or_else(|| {
2435 crate::DbError::InvalidInput(format!(
2436 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2437 self.dialect.db_type()
2438 ))
2439 })?;
2440
2441 let sql = format!("{} {}", insert_sql, conflict_clause);
2442 Ok((sql, params))
2443 }
2444
2445 pub fn build_update_with_params(
2448 &self,
2449 data: &std::collections::HashMap<String, Value>,
2450 ) -> (String, Vec<Value>) {
2451 let table = self
2452 .table
2453 .clone()
2454 .unwrap_or_else(|| M::table_name().to_string());
2455 if data.is_empty() {
2456 return (String::new(), Vec::new());
2457 }
2458
2459 let mut set_clauses = Vec::with_capacity(data.len());
2460 let mut params = Vec::with_capacity(data.len());
2461 for (k, v) in data.iter() {
2462 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2463 params.push(v.clone());
2464 }
2465
2466 let mut sql = format!(
2467 "UPDATE {} SET {}",
2468 self.dialect.quote(&table),
2469 set_clauses.join(", ")
2470 );
2471
2472 let (where_clause, where_params) = self.build_where_clause_with_params();
2474 if !where_clause.is_empty() {
2475 sql.push_str(&where_clause);
2476 params.extend(where_params);
2477 }
2478
2479 (sql, params)
2480 }
2481
2482 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2488 let table = self
2489 .table
2490 .clone()
2491 .unwrap_or_else(|| M::table_name().to_string());
2492
2493 if let Some(field) = self.soft_delete_field() {
2495 let (where_clause, where_params) = self.build_where_clause_with_params();
2496 let sql = format!(
2497 "UPDATE {} SET {} = NOW(){}",
2498 self.dialect.quote(&table),
2499 self.dialect.quote(field),
2500 where_clause
2501 );
2502 return (sql, where_params);
2503 }
2504
2505 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2506 let mut params = Vec::new();
2507
2508 let (where_clause, where_params) = self.build_where_clause_with_params();
2509 if !where_clause.is_empty() {
2510 sql.push_str(&where_clause);
2511 params = where_params;
2512 }
2513
2514 (sql, params)
2515 }
2516
2517 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2521 let table = self
2522 .table
2523 .clone()
2524 .unwrap_or_else(|| M::table_name().to_string());
2525
2526 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2527 let mut params = Vec::new();
2528
2529 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2531 if !where_clause.is_empty() {
2532 sql.push_str(&where_clause);
2533 params = where_params;
2534 }
2535
2536 (sql, params)
2537 }
2538
2539 pub fn build_count(&self) -> String {
2541 let table = self
2542 .table
2543 .clone()
2544 .unwrap_or_else(|| M::table_name().to_string());
2545
2546 let mut sql = format!(
2547 "SELECT COUNT(*) as total FROM {}",
2548 self.dialect.quote(&table)
2549 );
2550 sql.push_str(&self.build_where_clause());
2551 sql
2552 }
2553
2554 pub fn build_exists(&self) -> String {
2556 let table = self
2557 .table
2558 .clone()
2559 .unwrap_or_else(|| M::table_name().to_string());
2560
2561 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2562 sql.push_str(&self.build_where_clause());
2563 sql.push_str(" LIMIT 1");
2564 format!("SELECT EXISTS({})", sql)
2565 }
2566
2567 pub fn build_max(&self, field: &str) -> String {
2569 let table = self
2570 .table
2571 .clone()
2572 .unwrap_or_else(|| M::table_name().to_string());
2573
2574 let mut sql = format!(
2575 "SELECT MAX({}) as max_val FROM {}",
2576 self.dialect.quote(field),
2577 self.dialect.quote(&table)
2578 );
2579 sql.push_str(&self.build_where_clause());
2580 sql
2581 }
2582
2583 pub fn build_min(&self, field: &str) -> String {
2585 let table = self
2586 .table
2587 .clone()
2588 .unwrap_or_else(|| M::table_name().to_string());
2589
2590 let mut sql = format!(
2591 "SELECT MIN({}) as min_val FROM {}",
2592 self.dialect.quote(field),
2593 self.dialect.quote(&table)
2594 );
2595 sql.push_str(&self.build_where_clause());
2596 sql
2597 }
2598
2599 pub fn build_sum(&self, field: &str) -> String {
2601 let table = self
2602 .table
2603 .clone()
2604 .unwrap_or_else(|| M::table_name().to_string());
2605
2606 let mut sql = format!(
2607 "SELECT SUM({}) as sum_val FROM {}",
2608 self.dialect.quote(field),
2609 self.dialect.quote(&table)
2610 );
2611 sql.push_str(&self.build_where_clause());
2612 sql
2613 }
2614
2615 pub fn build_avg(&self, field: &str) -> String {
2617 let table = self
2618 .table
2619 .clone()
2620 .unwrap_or_else(|| M::table_name().to_string());
2621
2622 let mut sql = format!(
2623 "SELECT AVG({}) as avg_val FROM {}",
2624 self.dialect.quote(field),
2625 self.dialect.quote(&table)
2626 );
2627 sql.push_str(&self.build_where_clause());
2628 sql
2629 }
2630
2631 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2634 let sql = self.build_select();
2635 let mut errors = Vec::new();
2636
2637 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2638 errors.push(e);
2639 }
2640
2641 if !self.joins.is_empty() {
2643 for join in &self.joins {
2644 match join {
2645 JoinClause::Inner(_, left, right)
2646 | JoinClause::Left(_, left, right)
2647 | JoinClause::Right(_, left, right) => {
2648 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2649 errors.push(e);
2650 }
2651 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2652 errors.push(e);
2653 }
2654 }
2655 JoinClause::Relation(_, ft, fk, tt, tk) => {
2656 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2657 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2658 errors.push(e);
2659 }
2660 }
2661 }
2662 _ => {}
2663 }
2664 }
2665 }
2666
2667 let table = self
2669 .table
2670 .clone()
2671 .unwrap_or_else(|| M::table_name().to_string());
2672 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2673 errors.push(e);
2674 }
2675
2676 if errors.is_empty() {
2677 Ok(())
2678 } else {
2679 Err(errors)
2680 }
2681 }
2682
2683 pub fn validate_insert(
2686 &self,
2687 data: &std::collections::HashMap<String, Value>,
2688 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2689 let sql = self.build_insert(data);
2690 let mut errors = Vec::new();
2691
2692 if sql.is_empty() {
2693 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2694 return Err(errors);
2695 }
2696
2697 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2698 errors.push(e);
2699 }
2700
2701 if errors.is_empty() {
2702 Ok(())
2703 } else {
2704 Err(errors)
2705 }
2706 }
2707
2708 pub fn validate_update(
2711 &self,
2712 data: &std::collections::HashMap<String, Value>,
2713 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2714 let sql = self.build_update(data);
2715 let mut errors = Vec::new();
2716
2717 if sql.is_empty() {
2718 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2719 return Err(errors);
2720 }
2721
2722 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2723 errors.push(e);
2724 }
2725
2726 if errors.is_empty() {
2727 Ok(())
2728 } else {
2729 Err(errors)
2730 }
2731 }
2732
2733 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2735 let sql = self.build_delete();
2736 let mut errors = Vec::new();
2737
2738 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2739 errors.push(e);
2740 }
2741
2742 if errors.is_empty() {
2743 Ok(())
2744 } else {
2745 Err(errors)
2746 }
2747 }
2748}
2749
2750impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2752 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2770 let all_columns = M::columns();
2771 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2772
2773 for field in fields {
2774 if !all_columns.contains(field) {
2775 return Err(crate::DbError::InvalidInput(format!(
2776 "排除的字段不存在: {}",
2777 field
2778 )));
2779 }
2780 }
2781
2782 let retained: Vec<String> = all_columns
2783 .into_iter()
2784 .filter(|c| !exclude_set.contains(*c))
2785 .map(|s| s.to_string())
2786 .collect();
2787
2788 if retained.is_empty() {
2789 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2790 }
2791
2792 self.select_mode = crate::partial_model::SelectMode::Partial;
2793 self.select_columns = retained;
2794 Ok(self)
2795 }
2796}
2797
2798impl<M: Model> fmt::Debug for QueryBuilder<M> {
2799 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2800 f.debug_struct("QueryBuilder")
2801 .field("table", &self.table)
2802 .field("select_columns", &self.select_columns)
2803 .field("where_conditions", &self.where_conditions.len())
2804 .field("limit", &self.limit_value)
2805 .finish()
2806 }
2807}
2808
2809#[cfg(test)]
2810#[allow(deprecated)]
2811mod tests {
2812 use super::*;
2813 use crate::db_type::DbType;
2814 use crate::dialect::get_dialect;
2815
2816 struct TestModel;
2817 impl Model for TestModel {
2818 type PrimaryKey = i64;
2819
2820 fn table_name() -> &'static str {
2821 "test_models"
2822 }
2823
2824 fn pk(&self) -> Self::PrimaryKey {
2825 1
2826 }
2827
2828 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2829 }
2830
2831 #[test]
2832 fn test_query_builder_select() -> Result<(), crate::DbError> {
2833 let dialect = get_dialect(DbType::MySQL)?;
2834 let builder = QueryBuilder::<TestModel>::new(dialect);
2835
2836 let sql = builder
2837 .table("users")
2838 .select(vec!["id", "name"])?
2839 .build_select();
2840 assert!(sql.contains("SELECT `id`, `name` FROM"));
2841 assert!(sql.contains("`users`"));
2842 Ok(())
2843 }
2844
2845 #[test]
2846 fn test_query_builder_where() -> Result<(), crate::DbError> {
2847 let dialect = get_dialect(DbType::MySQL)?;
2848 let builder = QueryBuilder::<TestModel>::new(dialect);
2849
2850 let sql = builder
2851 .table("users")
2852 .where_eq("status", crate::value::Value::String("active".into()))
2853 .where_gt("age", crate::value::Value::I64(18))
2854 .build_select();
2855
2856 assert!(sql.contains("WHERE"));
2857 assert!(sql.contains("`status` = 'active'"));
2858 assert!(sql.contains("`age` > 18"));
2859 Ok(())
2860 }
2861
2862 #[test]
2863 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2864 let dialect = get_dialect(DbType::MySQL)?;
2865 let builder = QueryBuilder::<TestModel>::new(dialect);
2866
2867 let sql = builder
2868 .table("users")
2869 .order_by("created_at")
2870 .order_desc("id")
2871 .build_select();
2872
2873 assert!(sql.contains("ORDER BY"));
2874 assert!(sql.contains("`created_at` ASC"));
2875 assert!(sql.contains("`id` DESC"));
2876 Ok(())
2877 }
2878
2879 #[test]
2880 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2881 let dialect = get_dialect(DbType::MySQL)?;
2882 let builder = QueryBuilder::<TestModel>::new(dialect);
2883
2884 let sql = builder.table("users").limit(10).offset(20).build_select();
2885
2886 assert!(sql.contains("LIMIT 10"));
2887 assert!(sql.contains("OFFSET 20"));
2888 Ok(())
2889 }
2890
2891 #[test]
2892 fn test_query_builder_page() -> Result<(), crate::DbError> {
2893 let dialect = get_dialect(DbType::MySQL)?;
2894 let builder = QueryBuilder::<TestModel>::new(dialect);
2895
2896 let sql = builder.table("users").page(3, 20).build_select();
2897
2898 assert!(sql.contains("LIMIT 20"));
2899 assert!(sql.contains("OFFSET 40"));
2900 Ok(())
2901 }
2902
2903 #[test]
2904 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2905 let dialect = get_dialect(DbType::MySQL)?;
2906 let builder = QueryBuilder::<TestModel>::new(dialect);
2907
2908 let mut data = std::collections::HashMap::new();
2909 data.insert("name".to_string(), Value::String("test".to_string()));
2910 data.insert("age".to_string(), Value::I64(25));
2911
2912 let sql = builder.table("users").build_insert(&data);
2913
2914 assert!(sql.contains("INSERT INTO"));
2915 assert!(sql.contains("`name`"));
2916 assert!(sql.contains("'test'"));
2917 Ok(())
2918 }
2919
2920 #[test]
2921 fn test_query_builder_update() -> Result<(), crate::DbError> {
2922 let dialect = get_dialect(DbType::MySQL)?;
2923 let builder = QueryBuilder::<TestModel>::new(dialect);
2924
2925 let mut data = std::collections::HashMap::new();
2926 data.insert("name".to_string(), Value::String("updated".to_string()));
2927
2928 let sql = builder
2929 .table("users")
2930 .where_eq("id", Value::I64(1))
2931 .build_update(&data);
2932
2933 assert!(sql.contains("UPDATE"));
2934 assert!(sql.contains("`name` = 'updated'"));
2935 assert!(sql.contains("WHERE"));
2936 Ok(())
2937 }
2938
2939 #[test]
2940 fn test_query_builder_delete() -> Result<(), crate::DbError> {
2941 let dialect = get_dialect(DbType::MySQL)?;
2942 let builder = QueryBuilder::<TestModel>::new(dialect);
2943
2944 let sql = builder
2945 .table("users")
2946 .where_eq("id", Value::I64(1))
2947 .build_delete();
2948
2949 assert!(sql.contains("DELETE FROM"));
2950 assert!(sql.contains("WHERE"));
2951 Ok(())
2952 }
2953
2954 #[test]
2955 fn test_query_builder_count() -> Result<(), crate::DbError> {
2956 let dialect = get_dialect(DbType::MySQL)?;
2957 let builder = QueryBuilder::<TestModel>::new(dialect);
2958
2959 let sql = builder.table("users").build_count();
2960
2961 assert!(sql.contains("SELECT COUNT(*)"));
2962 assert!(sql.contains("FROM"));
2963 Ok(())
2964 }
2965
2966 #[test]
2967 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
2968 let dialect = get_dialect(DbType::MySQL)?;
2969 let builder = QueryBuilder::<TestModel>::new(dialect);
2970
2971 let sql = builder
2972 .table("users")
2973 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
2974 .build_select();
2975
2976 assert!(sql.contains("IN ("));
2977 Ok(())
2978 }
2979
2980 #[test]
2981 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
2982 let dialect = get_dialect(DbType::MySQL)?;
2983 let builder = QueryBuilder::<TestModel>::new(dialect);
2984
2985 let sql = builder
2986 .table("users")
2987 .where_between("age", Value::I64(18), Value::I64(30))
2988 .build_select();
2989
2990 assert!(sql.contains("BETWEEN"));
2991 Ok(())
2992 }
2993
2994 #[test]
2995 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
2996 let dialect = get_dialect(DbType::MySQL)?;
2997 let builder = QueryBuilder::<TestModel>::new(dialect);
2998
2999 let sql = builder
3000 .table("users")
3001 .where_null("deleted_at")
3002 .build_select();
3003
3004 assert!(sql.contains("IS NULL"));
3005 Ok(())
3006 }
3007
3008 #[test]
3009 fn test_query_builder_join() -> Result<(), crate::DbError> {
3010 let dialect = get_dialect(DbType::MySQL)?;
3011 let builder = QueryBuilder::<TestModel>::new(dialect);
3012
3013 let sql = builder
3014 .table("users")
3015 .join_inner("posts", "users.id", "posts.user_id")
3016 .build_select();
3017
3018 assert!(sql.contains("INNER JOIN"));
3019 assert!(sql.contains("`posts`"));
3020 Ok(())
3021 }
3022
3023 #[test]
3024 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3025 let dialect = get_dialect(DbType::MySQL)?;
3026 let builder = QueryBuilder::<TestModel>::new(dialect);
3027
3028 let sql = builder.table("users").group_by("status").build_select();
3029
3030 assert!(sql.contains("GROUP BY"));
3031 assert!(sql.contains("`status`"));
3032 Ok(())
3033 }
3034
3035 #[test]
3036 fn test_query_builder_max() -> Result<(), crate::DbError> {
3037 let dialect = get_dialect(DbType::MySQL)?;
3038 let builder = QueryBuilder::<TestModel>::new(dialect);
3039
3040 let sql = builder.table("users").build_max("score");
3041
3042 assert!(sql.contains("MAX("));
3043 assert!(sql.contains("`score`"));
3044 Ok(())
3045 }
3046
3047 #[test]
3048 fn test_query_builder_min() -> Result<(), crate::DbError> {
3049 let dialect = get_dialect(DbType::MySQL)?;
3050 let builder = QueryBuilder::<TestModel>::new(dialect);
3051
3052 let sql = builder.table("users").build_min("price");
3053
3054 assert!(sql.contains("MIN("));
3055 assert!(sql.contains("`price`"));
3056 Ok(())
3057 }
3058
3059 #[test]
3060 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3061 let dialect = get_dialect(DbType::MySQL)?;
3062 let builder = QueryBuilder::<TestModel>::new(dialect);
3063
3064 let sql = builder.table("orders").build_sum("amount");
3065
3066 assert!(sql.contains("SUM("));
3067 assert!(sql.contains("`amount`"));
3068 Ok(())
3069 }
3070
3071 #[test]
3072 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3073 let dialect = get_dialect(DbType::MySQL)?;
3074 let builder = QueryBuilder::<TestModel>::new(dialect);
3075
3076 let sql = builder.table("scores").build_avg("value");
3077
3078 assert!(sql.contains("AVG("));
3079 assert!(sql.contains("`value`"));
3080 Ok(())
3081 }
3082
3083 #[test]
3084 fn test_validator_select() -> Result<(), crate::DbError> {
3085 let dialect = get_dialect(DbType::MySQL)?;
3086 let builder = QueryBuilder::<TestModel>::new(dialect);
3087
3088 let result = builder
3089 .table("users")
3090 .select(vec!["id", "name"])?
3091 .validate();
3092 assert!(result.is_ok());
3093 Ok(())
3094 }
3095
3096 #[test]
3097 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3098 let dialect = get_dialect(DbType::MySQL)?;
3099 let builder = QueryBuilder::<TestModel>::new(dialect);
3100
3101 let result = builder
3102 .table("users")
3103 .join_inner("posts", "users.id", "posts.user_id")
3104 .validate();
3105 assert!(result.is_ok());
3106 Ok(())
3107 }
3108
3109 #[test]
3110 fn test_validator_insert() -> Result<(), crate::DbError> {
3111 let dialect = get_dialect(DbType::MySQL)?;
3112 let builder = QueryBuilder::<TestModel>::new(dialect);
3113
3114 let mut data = std::collections::HashMap::new();
3115 data.insert("name".to_string(), Value::String("test".to_string()));
3116
3117 let result = builder.table("users").validate_insert(&data);
3118 assert!(result.is_ok());
3119 Ok(())
3120 }
3121
3122 #[test]
3123 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3124 let dialect = get_dialect(DbType::MySQL)?;
3125 let builder = QueryBuilder::<TestModel>::new(dialect);
3126
3127 let data = std::collections::HashMap::new();
3128 let result = builder.table("users").validate_insert(&data);
3129 assert!(result.is_err());
3130 Ok(())
3131 }
3132
3133 #[test]
3134 fn test_validator_update() -> Result<(), crate::DbError> {
3135 let dialect = get_dialect(DbType::MySQL)?;
3136 let builder = QueryBuilder::<TestModel>::new(dialect);
3137
3138 let mut data = std::collections::HashMap::new();
3139 data.insert("name".to_string(), Value::String("updated".to_string()));
3140
3141 let result = builder.table("users").validate_update(&data);
3142 assert!(result.is_ok());
3143 Ok(())
3144 }
3145
3146 #[test]
3147 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3148 let dialect = get_dialect(DbType::MySQL)?;
3149 let builder = QueryBuilder::<TestModel>::new(dialect);
3150
3151 let data = std::collections::HashMap::new();
3152 let result = builder.table("users").validate_update(&data);
3153 assert!(result.is_err());
3154 Ok(())
3155 }
3156
3157 #[test]
3158 fn test_validator_delete() -> Result<(), crate::DbError> {
3159 let dialect = get_dialect(DbType::MySQL)?;
3160 let builder = QueryBuilder::<TestModel>::new(dialect);
3161
3162 let result = builder
3163 .table("users")
3164 .where_eq("id", Value::I64(1))
3165 .validate_delete();
3166 assert!(result.is_ok());
3167 Ok(())
3168 }
3169
3170 #[test]
3171 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3172 let dialect = get_dialect(DbType::MySQL)?;
3173 let builder = QueryBuilder::<TestModel>::new(dialect);
3174
3175 let result = builder.table("users").validate_delete();
3177 assert!(result.is_ok());
3178 Ok(())
3179 }
3180
3181 #[test]
3184 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3185 let dialect = get_dialect(DbType::MySQL)?;
3186 let builder = QueryBuilder::<TestModel>::new(dialect);
3187 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3188 let sql = builder.build_select();
3189 assert!(sql.contains("SELECT `id`, `name` FROM"));
3191 assert!(sql.contains("`users`"));
3192 Ok(())
3193 }
3194
3195 #[test]
3196 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3197 let dialect = get_dialect(DbType::MySQL)?;
3198 let builder = QueryBuilder::<TestModel>::new(dialect);
3199
3200 let result = builder
3202 .table("users")
3203 .select_quoted(vec!["id; DROP TABLE users"]);
3204 assert!(result.is_err());
3205
3206 let dialect = get_dialect(DbType::MySQL)?;
3208 let builder = QueryBuilder::<TestModel>::new(dialect);
3209 let result = builder.table("users").select_quoted(vec!["name'"]);
3210 assert!(result.is_err());
3211
3212 let dialect = get_dialect(DbType::MySQL)?;
3214 let builder = QueryBuilder::<TestModel>::new(dialect);
3215 let result = builder.table("users").select_quoted(vec!["1col"]);
3216 assert!(result.is_err());
3217
3218 let dialect = get_dialect(DbType::MySQL)?;
3220 let builder = QueryBuilder::<TestModel>::new(dialect);
3221 let result = builder.table("users").select_quoted(vec!["col name"]);
3222 assert!(result.is_err());
3223 Ok(())
3224 }
3225
3226 #[test]
3227 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3228 let dialect = get_dialect(DbType::PostgreSQL)?;
3229 let builder = QueryBuilder::<TestModel>::new(dialect);
3230 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3231 let sql = builder.build_select();
3232 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3234 assert!(sql.contains("\"users\""));
3235 Ok(())
3236 }
3237
3238 struct SoftDeleteModel;
3242 impl Model for SoftDeleteModel {
3243 type PrimaryKey = i64;
3244
3245 fn table_name() -> &'static str {
3246 "soft_users"
3247 }
3248
3249 fn pk(&self) -> Self::PrimaryKey {
3250 1
3251 }
3252
3253 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3254
3255 fn soft_delete_field() -> Option<&'static str> {
3256 Some("deleted_at")
3257 }
3258 }
3259
3260 #[test]
3264 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3265 let dialect = get_dialect(DbType::MySQL)?;
3266 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3267 let sql = builder.table("soft_users").build_select();
3268 assert!(
3270 sql.contains("`deleted_at` IS NULL"),
3271 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3272 sql
3273 );
3274 Ok(())
3275 }
3276
3277 #[test]
3279 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3280 let dialect = get_dialect(DbType::MySQL)?;
3281 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3282 .table("soft_users")
3283 .where_eq("status", Value::String("active".into()))
3284 .build_select();
3285 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3287 assert!(
3288 sql.contains("`deleted_at` IS NULL"),
3289 "软删除条件应自动追加: {}",
3290 sql
3291 );
3292 Ok(())
3293 }
3294
3295 #[test]
3299 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3300 let dialect = get_dialect(DbType::MySQL)?;
3301 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3302 .table("soft_users")
3303 .without_soft_delete()
3304 .build_select();
3305 assert!(
3307 !sql.contains("`deleted_at` IS NULL"),
3308 "without_soft_delete 应禁用过滤,实际: {}",
3309 sql
3310 );
3311 assert!(
3313 !sql.contains("WHERE"),
3314 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3315 sql
3316 );
3317 Ok(())
3318 }
3319
3320 #[test]
3324 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3325 let dialect = get_dialect(DbType::MySQL)?;
3326 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3327 .table("soft_users")
3328 .where_eq("id", Value::I64(42))
3329 .build_delete();
3330 assert!(
3332 sql.starts_with("UPDATE"),
3333 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3334 sql
3335 );
3336 assert!(
3337 !sql.contains("DELETE FROM"),
3338 "不应生成 DELETE FROM: {}",
3339 sql
3340 );
3341 assert!(
3342 sql.contains("`deleted_at` = NOW()"),
3343 "应设置 deleted_at = NOW(): {}",
3344 sql
3345 );
3346 assert!(
3348 sql.contains("`deleted_at` IS NULL"),
3349 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3350 sql
3351 );
3352 Ok(())
3353 }
3354
3355 #[test]
3359 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3360 let dialect = get_dialect(DbType::MySQL)?;
3361 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3362 .table("soft_users")
3363 .where_eq("id", Value::I64(99))
3364 .build_force_delete();
3365 assert!(
3367 sql.starts_with("DELETE FROM"),
3368 "build_force_delete 应生成 DELETE FROM,实际: {}",
3369 sql
3370 );
3371 assert!(
3373 !sql.contains("`deleted_at` IS NULL"),
3374 "物理删除不应追加软删除过滤: {}",
3375 sql
3376 );
3377 Ok(())
3378 }
3379
3380 #[test]
3382 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3383 let dialect = get_dialect(DbType::MySQL)?;
3384 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3385 .table("soft_users")
3386 .where_eq("id", Value::I64(1))
3387 .build_select_with_params();
3388 assert!(
3389 sql.contains("`deleted_at` IS NULL"),
3390 "参数化版本也应自动追加软删除: {}",
3391 sql
3392 );
3393 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3394 assert_eq!(params[0], Value::I64(1));
3395 Ok(())
3396 }
3397
3398 #[test]
3400 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3401 let dialect = get_dialect(DbType::MySQL)?;
3402 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3403 .table("soft_users")
3404 .where_eq("id", Value::I64(7))
3405 .build_delete_with_params();
3406 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3407 assert!(
3408 sql.contains("`deleted_at` = NOW()"),
3409 "应设置 NOW(): {}",
3410 sql
3411 );
3412 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3413 Ok(())
3414 }
3415
3416 #[test]
3418 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3419 let dialect = get_dialect(DbType::MySQL)?;
3420 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3421 .table("soft_users")
3422 .where_eq("id", Value::I64(11))
3423 .build_force_delete_with_params();
3424 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3425 assert!(
3426 !sql.contains("`deleted_at` IS NULL"),
3427 "不应追加软删除过滤: {}",
3428 sql
3429 );
3430 assert_eq!(params.len(), 1);
3431 Ok(())
3432 }
3433
3434 #[test]
3438 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3439 let dialect = get_dialect(DbType::MySQL)?;
3440 let sql = QueryBuilder::<TestModel>::new(dialect)
3441 .table("users")
3442 .where_eq("id", Value::I64(1))
3443 .build_select();
3444 assert!(
3445 !sql.contains("deleted_at"),
3446 "非软删除模型不应追加 deleted_at: {}",
3447 sql
3448 );
3449 let dialect = get_dialect(DbType::MySQL)?;
3451 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3452 .table("users")
3453 .where_eq("id", Value::I64(1))
3454 .build_delete();
3455 assert!(
3456 del_sql.starts_with("DELETE FROM"),
3457 "非软删除模型 build_delete 应生成 DELETE: {}",
3458 del_sql
3459 );
3460 Ok(())
3461 }
3462
3463 #[test]
3465 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3466 let dialect = get_dialect(DbType::MySQL)?;
3467 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3468 .table("soft_users")
3469 .build_count();
3470 assert!(
3471 sql.contains("`deleted_at` IS NULL"),
3472 "build_count 也应追加软删除过滤: {}",
3473 sql
3474 );
3475 Ok(())
3476 }
3477
3478 #[test]
3484 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3485 let dialect = get_dialect(DbType::MySQL)?;
3486 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3487 .table("users")
3488 .where_eq("name", Value::String("alice".into()))
3489 .build_select_with_params();
3490 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3492 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3493 assert_eq!(params.len(), 1);
3494 assert_eq!(params[0], Value::String("alice".into()));
3495 Ok(())
3496 }
3497
3498 #[test]
3500 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3501 let dialect = get_dialect(DbType::MySQL)?;
3502 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3503 .table("users")
3504 .where_like("name", Value::String("%alice%".into()))
3505 .build_select_with_params();
3506 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3507 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3508 assert_eq!(params.len(), 1);
3509 Ok(())
3510 }
3511
3512 #[test]
3516 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3517 let dialect = get_dialect(DbType::MySQL)?;
3518 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3519 .table("users")
3520 .where_ne("status", Value::I64(0))
3521 .build_select_with_params();
3522 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3523 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3524 assert_eq!(params.len(), 1);
3525 assert_eq!(params[0], Value::I64(0));
3526 Ok(())
3527 }
3528
3529 #[test]
3533 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3534 let dialect = get_dialect(DbType::MySQL)?;
3535 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3536 .table("users")
3537 .where_ge("age", Value::I64(18))
3538 .build_select_with_params();
3539 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3540 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3541 assert_eq!(params.len(), 1);
3542 assert_eq!(params[0], Value::I64(18));
3543 Ok(())
3544 }
3545
3546 #[test]
3550 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3551 let dialect = get_dialect(DbType::MySQL)?;
3552 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3553 .table("users")
3554 .where_lt("score", Value::F64(60.0))
3555 .build_select_with_params();
3556 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3557 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3558 assert_eq!(params.len(), 1);
3559 assert_eq!(params[0], Value::F64(60.0));
3560 Ok(())
3561 }
3562
3563 #[test]
3567 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3568 let dialect = get_dialect(DbType::MySQL)?;
3569 let evil_input = "'; DROP TABLE users; --".to_string();
3570 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3571 .table("users")
3572 .where_eq("name", Value::String(evil_input.clone()))
3573 .build_select_with_params();
3574 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3576 assert_eq!(params.len(), 1);
3578 assert_eq!(params[0], Value::String(evil_input));
3579 assert_eq!(sql.matches('?').count(), 1);
3581 Ok(())
3582 }
3583
3584 #[test]
3586 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3587 let dialect = get_dialect(DbType::MySQL)?;
3588 let evil = "' OR '1'='1".to_string();
3589 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3590 .table("users")
3591 .where_eq("name", Value::String(evil.clone()))
3592 .build_select_with_params();
3593 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3594 assert_eq!(params.len(), 1);
3595 assert_eq!(params[0], Value::String(evil));
3596 Ok(())
3597 }
3598
3599 #[test]
3601 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3602 let dialect = get_dialect(DbType::MySQL)?;
3603 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3604 .table("users")
3605 .where_eq("name", Value::String("alice".into()))
3606 .where_gt("age", Value::I64(18))
3607 .where_le("score", Value::F64(99.5))
3608 .build_select_with_params();
3609 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3610 assert_eq!(params.len(), 3);
3611 assert_eq!(params[0], Value::String("alice".into()));
3613 assert_eq!(params[1], Value::I64(18));
3614 assert_eq!(params[2], Value::F64(99.5));
3615 Ok(())
3616 }
3617
3618 #[test]
3620 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3621 let dialect = get_dialect(DbType::MySQL)?;
3622 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3623 .table("users")
3624 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3625 .build_select_with_params();
3626 assert!(
3627 sql.contains("`id` IN (?, ?, ?)"),
3628 "应使用 3 个占位符: {}",
3629 sql
3630 );
3631 assert_eq!(params.len(), 3);
3632 Ok(())
3633 }
3634
3635 #[test]
3637 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3638 let dialect = get_dialect(DbType::MySQL)?;
3639 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3640 .table("users")
3641 .where_between("age", Value::I64(18), Value::I64(65))
3642 .build_select_with_params();
3643 assert!(
3644 sql.contains("`age` BETWEEN ? AND ?"),
3645 "应使用 2 个占位符: {}",
3646 sql
3647 );
3648 assert_eq!(params.len(), 2);
3649 assert_eq!(params[0], Value::I64(18));
3650 assert_eq!(params[1], Value::I64(65));
3651 Ok(())
3652 }
3653
3654 #[test]
3656 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3657 let dialect = get_dialect(DbType::MySQL)?;
3658 let mut data = std::collections::HashMap::new();
3659 data.insert("name".to_string(), Value::String("bob".into()));
3660 data.insert("age".to_string(), Value::I64(30));
3661 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3662 .table("users")
3663 .where_eq("id", Value::I64(99))
3664 .build_update_with_params(&data);
3665 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3667 assert_eq!(params.len(), 3);
3668 assert_eq!(params[2], Value::I64(99));
3671 Ok(())
3672 }
3673
3674 #[test]
3678 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3679 let dialect = get_dialect(DbType::MySQL)?;
3680 let sql = QueryBuilder::<TestModel>::new(dialect)
3681 .table("users")
3682 .where_eq("name", Value::String("alice".into()))
3683 .build_select();
3684 assert!(
3686 sql.contains("`name` = "),
3687 "无参数版本应含 WHERE 条件: {}",
3688 sql
3689 );
3690 assert!(
3692 !sql.contains("`name` = ?"),
3693 "无参数版本不应使用 ? 占位符: {}",
3694 sql
3695 );
3696 Ok(())
3697 }
3698
3699 #[test]
3701 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3702 let dialect = get_dialect(DbType::MySQL)?;
3703 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3704 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3705 let builder =
3706 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3707 assert!(
3708 builder.is_soft_delete_disabled(),
3709 "without_soft_delete 后应反映禁用状态"
3710 );
3711 Ok(())
3712 }
3713
3714 struct TenantModel;
3718 impl Model for TenantModel {
3719 type PrimaryKey = i64;
3720
3721 fn table_name() -> &'static str {
3722 "orders"
3723 }
3724
3725 fn pk(&self) -> Self::PrimaryKey {
3726 1
3727 }
3728
3729 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3730
3731 fn tenant_field() -> Option<&'static str> {
3732 Some("tenant_id")
3733 }
3734 }
3735
3736 struct SoftDeleteAndTenantModel;
3738 impl Model for SoftDeleteAndTenantModel {
3739 type PrimaryKey = i64;
3740
3741 fn table_name() -> &'static str {
3742 "documents"
3743 }
3744
3745 fn pk(&self) -> Self::PrimaryKey {
3746 1
3747 }
3748
3749 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3750
3751 fn soft_delete_field() -> Option<&'static str> {
3752 Some("deleted_at")
3753 }
3754
3755 fn tenant_field() -> Option<&'static str> {
3756 Some("tenant_id")
3757 }
3758 }
3759
3760 #[test]
3764 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3765 let dialect = get_dialect(DbType::MySQL)?;
3766 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3767 .table("orders")
3768 .with_tenant_id(42)
3769 .build_select_with_params();
3770 assert!(
3771 sql.contains("`tenant_id` = ?"),
3772 "多租户模型应自动追加 tenant_id = ?: {}",
3773 sql
3774 );
3775 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3776 assert_eq!(params[0], Value::I64(42));
3777 Ok(())
3778 }
3779
3780 #[test]
3782 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3783 let dialect = get_dialect(DbType::MySQL)?;
3784 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3785 .table("orders")
3786 .with_tenant_id(7)
3787 .where_eq("status", Value::String("active".into()))
3788 .build_select_with_params();
3789 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3790 assert!(
3791 sql.contains("`tenant_id` = ?"),
3792 "租户条件应自动追加: {}",
3793 sql
3794 );
3795 assert_eq!(params.len(), 2, "应有 2 个参数");
3796 assert_eq!(params[0], Value::String("active".into()));
3798 assert_eq!(params[1], Value::I64(7));
3799 Ok(())
3800 }
3801
3802 #[test]
3806 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3807 let dialect = get_dialect(DbType::MySQL)?;
3808 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3809 .table("orders")
3810 .with_tenant_id(42)
3811 .without_tenant()
3812 .build_select_with_params();
3813 assert!(
3814 !sql.contains("`tenant_id` = ?"),
3815 "without_tenant 应禁用过滤: {}",
3816 sql
3817 );
3818 assert_eq!(params.len(), 0, "不应有租户参数");
3819 Ok(())
3820 }
3821
3822 #[test]
3826 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3827 let dialect = get_dialect(DbType::MySQL)?;
3828 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3829 .table("orders")
3830 .with_tenant_id(99)
3831 .where_eq("id", Value::I64(1))
3832 .build_delete_with_params();
3833 assert!(
3834 sql.contains("`tenant_id` = ?"),
3835 "删除应自动追加租户条件: {}",
3836 sql
3837 );
3838 assert_eq!(params.len(), 2);
3840 assert_eq!(params[0], Value::I64(1));
3841 assert_eq!(params[1], Value::I64(99));
3842 Ok(())
3843 }
3844
3845 #[test]
3847 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3848 let dialect = get_dialect(DbType::MySQL)?;
3849 let mut data = std::collections::HashMap::new();
3850 data.insert("status".to_string(), Value::String("shipped".into()));
3851 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3852 .table("orders")
3853 .with_tenant_id(5)
3854 .where_eq("id", Value::I64(10))
3855 .build_update_with_params(&data);
3856 assert!(
3857 sql.contains("`tenant_id` = ?"),
3858 "更新应自动追加租户条件: {}",
3859 sql
3860 );
3861 assert_eq!(params.len(), 3);
3863 assert_eq!(params[2], Value::I64(5));
3865 Ok(())
3866 }
3867
3868 #[test]
3870 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3871 let dialect = get_dialect(DbType::MySQL)?;
3872 let sql = QueryBuilder::<TenantModel>::new(dialect)
3873 .table("orders")
3874 .with_tenant_id(42)
3875 .build_count();
3876 assert!(
3877 sql.contains("`tenant_id` = 42"),
3878 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3879 sql
3880 );
3881 Ok(())
3882 }
3883
3884 #[test]
3888 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3889 let dialect = get_dialect(DbType::MySQL)?;
3890 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3892 .table("users")
3893 .with_tenant_id(42)
3894 .build_select_with_params();
3895 assert!(
3896 !sql.contains("tenant_id"),
3897 "非多租户模型不应追加 tenant_id: {}",
3898 sql
3899 );
3900 assert_eq!(params.len(), 0);
3901 Ok(())
3902 }
3903
3904 #[test]
3908 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
3909 let dialect = get_dialect(DbType::MySQL)?;
3910 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3911 .table("orders")
3912 .build_select_with_params();
3913 assert!(
3914 !sql.contains("tenant_id"),
3915 "未设置 tenant_id 时不应追加过滤: {}",
3916 sql
3917 );
3918 assert_eq!(params.len(), 0);
3919 Ok(())
3920 }
3921
3922 #[test]
3926 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
3927 let dialect = get_dialect(DbType::MySQL)?;
3928 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3929 .table("documents")
3930 .with_tenant_id(100)
3931 .where_eq("title", Value::String("report".into()))
3932 .build_select_with_params();
3933 assert!(
3935 sql.contains("`deleted_at` IS NULL"),
3936 "应追加软删除条件: {}",
3937 sql
3938 );
3939 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
3941 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
3943 assert_eq!(params.len(), 2);
3945 assert_eq!(params[0], Value::String("report".into()));
3946 assert_eq!(params[1], Value::I64(100));
3947 Ok(())
3948 }
3949
3950 #[test]
3952 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
3953 let dialect = get_dialect(DbType::MySQL)?;
3954 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3955 .table("documents")
3956 .with_tenant_id(100)
3957 .without_tenant()
3958 .without_soft_delete()
3959 .build_select_with_params();
3960 assert!(
3961 !sql.contains("`deleted_at` IS NULL"),
3962 "应禁用软删除: {}",
3963 sql
3964 );
3965 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
3966 assert_eq!(params.len(), 0);
3967 Ok(())
3968 }
3969
3970 #[test]
3972 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
3973 let dialect = get_dialect(DbType::MySQL)?;
3974 let builder = QueryBuilder::<TenantModel>::new(dialect);
3975 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
3976 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
3977 .with_tenant_id(1)
3978 .without_tenant();
3979 assert!(
3980 builder.is_tenant_disabled(),
3981 "without_tenant 后应反映禁用状态"
3982 );
3983 Ok(())
3984 }
3985
3986 #[test]
3990 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
3991 let dialect = get_dialect(DbType::MySQL)?;
3992 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3993 .table("orders")
3994 .with_tenant_id(42)
3995 .where_eq("id", Value::I64(999))
3996 .build_force_delete_with_params();
3997 assert!(
4000 sql.contains("`tenant_id` = ?"),
4001 "物理删除应保留租户条件: {}",
4002 sql
4003 );
4004 assert_eq!(params.len(), 2);
4005 assert_eq!(params[0], Value::I64(999));
4006 assert_eq!(params[1], Value::I64(42));
4007 Ok(())
4008 }
4009
4010 #[cfg(feature = "multi-tenant-enhanced")]
4014 #[tokio::test]
4015 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4016 let ctx = crate::tenant_context::TenantContext::new(
4017 99,
4018 crate::tenant_context::IsolationStrategy::RowLevel,
4019 );
4020 ctx.scope(async {
4021 let (sql, params) =
4023 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4024 .table("orders")
4025 .with_tenant_id(42)
4026 .build_select_with_params();
4027 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4028 assert_eq!(params.len(), 1);
4029 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4030 })
4031 .await;
4032 Ok(())
4033 }
4034
4035 #[cfg(feature = "multi-tenant-enhanced")]
4037 #[tokio::test]
4038 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4039 let ctx = crate::tenant_context::TenantContext::new(
4040 77,
4041 crate::tenant_context::IsolationStrategy::RowLevel,
4042 );
4043 ctx.scope(async {
4044 let (sql, params) =
4045 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4046 .table("orders")
4047 .build_select_with_params();
4048 assert!(
4049 sql.contains("`tenant_id` = ?"),
4050 "应从上下文自动追加租户条件: {}",
4051 sql
4052 );
4053 assert_eq!(params.len(), 1);
4054 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4055 })
4056 .await;
4057 Ok(())
4058 }
4059
4060 #[cfg(feature = "multi-tenant-enhanced")]
4062 #[tokio::test]
4063 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4064 let ctx = crate::tenant_context::TenantContext::new(
4065 42,
4066 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4067 );
4068 ctx.scope(async {
4069 let (sql, _params) =
4070 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4071 .table("orders")
4072 .build_select_with_params();
4073 assert!(
4074 sql.contains("tenant_42_orders"),
4075 "Schema 隔离应重写表名: {}",
4076 sql
4077 );
4078 })
4079 .await;
4080 Ok(())
4081 }
4082
4083 #[cfg(feature = "multi-tenant-enhanced")]
4085 #[test]
4086 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4087 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4088 .table("orders")
4089 .build_select_with_params();
4090 assert!(
4092 !sql.contains("`tenant_id` = ?"),
4093 "未设置上下文不应追加租户条件: {}",
4094 sql
4095 );
4096 assert_eq!(params.len(), 0);
4097 Ok(())
4098 }
4099
4100 struct TcUsersTable;
4103 impl crate::typed::TypedTable for TcUsersTable {
4104 const NAME: &'static str = "users";
4105 }
4106 struct TcColId;
4107 impl crate::typed::TypedColumn for TcColId {
4108 const NAME: &'static str = "id";
4109 type Table = TcUsersTable;
4110 type RustType = i64;
4111 type SqlType = crate::typed_ast::Untyped;
4112 }
4113 struct TcColName;
4114 impl crate::typed::TypedColumn for TcColName {
4115 const NAME: &'static str = "name";
4116 type Table = TcUsersTable;
4117 type RustType = String;
4118 type SqlType = crate::typed_ast::Untyped;
4119 }
4120
4121 #[test]
4122 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4123 let dialect = get_dialect(DbType::MySQL)?;
4124 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4125 .where_eq_typed::<TcColId>(Value::I64(42))
4126 .build_select_with_params();
4127 assert!(sql.contains("`id` = ?"));
4128 assert_eq!(params[0], Value::I64(42));
4129 Ok(())
4130 }
4131
4132 #[test]
4133 fn test_order_by_typed() -> Result<(), crate::DbError> {
4134 let dialect = get_dialect(DbType::MySQL)?;
4135 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4136 .order_by_typed::<TcColName>()
4137 .build_select_with_params();
4138 assert!(sql.contains("ORDER BY"));
4139 assert!(sql.contains("`name`"));
4140 Ok(())
4141 }
4142
4143 #[test]
4144 fn test_select_typed() -> Result<(), crate::DbError> {
4145 let dialect = get_dialect(DbType::MySQL)?;
4146 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4147 .select_typed::<TcColId>()
4148 .select_typed::<TcColName>()
4149 .build_select_with_params();
4150 assert!(sql.contains("SELECT"));
4151 assert!(sql.contains("id"));
4153 assert!(sql.contains("name"));
4154 Ok(())
4155 }
4156
4157 #[test]
4158 fn test_where_null_typed() -> Result<(), crate::DbError> {
4159 let dialect = get_dialect(DbType::MySQL)?;
4160 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4161 .where_null_typed::<TcColName>()
4162 .build_select_with_params();
4163 assert!(sql.contains("`name` IS NULL"));
4164 Ok(())
4165 }
4166
4167 #[test]
4168 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4169 let dialect = get_dialect(DbType::MySQL)?;
4170 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4171 .where_not_null_typed::<TcColName>()
4172 .build_select_with_params();
4173 assert!(sql.contains("`name` IS NOT NULL"));
4174 Ok(())
4175 }
4176
4177 #[test]
4178 fn test_group_by_typed() -> Result<(), crate::DbError> {
4179 let dialect = get_dialect(DbType::MySQL)?;
4180 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4181 .group_by_typed::<TcColName>()
4182 .build_select_with_params();
4183 assert!(sql.contains("GROUP BY"));
4184 assert!(sql.contains("`name`"));
4185 Ok(())
4186 }
4187
4188 #[test]
4189 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4190 let dialect = get_dialect(DbType::MySQL)?;
4191 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4192 .where_gt_typed::<TcColId>(Value::I64(10))
4193 .build_select_with_params();
4194 assert!(sql.contains("`id` > ?"));
4195 assert_eq!(params[0], Value::I64(10));
4196 Ok(())
4197 }
4198
4199 #[test]
4202 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4203 let dialect = get_dialect(DbType::MySQL)?;
4204 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4205 .table("users")
4206 .where_eq("id", Value::I64(1))
4207 .lock_for_update()?
4208 .build_select_with_params();
4209 assert!(sql.contains("SELECT * FROM `users`"));
4210 assert!(sql.contains("WHERE `id` = ?"));
4211 assert!(sql.contains("FOR UPDATE"));
4212 assert_eq!(params.len(), 1);
4213 assert_eq!(params[0], Value::I64(1));
4214 Ok(())
4215 }
4216
4217 #[test]
4218 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4219 let dialect = get_dialect(DbType::MySQL)?;
4220 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4221 .table("users")
4222 .where_eq("id", Value::I64(1))
4223 .lock_shared()?
4224 .build_select_with_params();
4225 assert!(sql.contains("SELECT * FROM `users`"));
4226 assert!(sql.contains("WHERE `id` = ?"));
4227 assert!(sql.contains("LOCK IN SHARE MODE"));
4228 assert_eq!(params.len(), 1);
4229 assert_eq!(params[0], Value::I64(1));
4230 Ok(())
4231 }
4232
4233 #[test]
4234 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4235 let dialect = get_dialect(DbType::PostgreSQL)?;
4236 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4237 .table("users")
4238 .where_eq("id", Value::I64(1))
4239 .lock_for_update()?
4240 .build_select_with_params();
4241 assert!(sql.contains("SELECT * FROM \"users\""));
4242 assert!(sql.contains("WHERE \"id\" = ?"));
4243 assert!(sql.contains("FOR UPDATE"));
4244 assert_eq!(params.len(), 1);
4245 assert_eq!(params[0], Value::I64(1));
4246 Ok(())
4247 }
4248
4249 #[test]
4250 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4251 let dialect = get_dialect(DbType::PostgreSQL)?;
4252 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4253 .table("users")
4254 .where_eq("id", Value::I64(1))
4255 .lock_shared()?
4256 .build_select_with_params();
4257 assert!(sql.contains("SELECT * FROM \"users\""));
4258 assert!(sql.contains("WHERE \"id\" = ?"));
4259 assert!(sql.contains("FOR SHARE"));
4260 assert_eq!(params.len(), 1);
4261 assert_eq!(params[0], Value::I64(1));
4262 Ok(())
4263 }
4264
4265 #[test]
4266 fn test_lock_for_update_sqlite_should_fail() {
4267 let dialect = get_dialect(DbType::Sqlite).unwrap();
4268 let result = QueryBuilder::<TestModel>::new(dialect)
4269 .table("users")
4270 .where_eq("id", Value::I64(1))
4271 .lock_for_update();
4272 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4273 let err = result.err().unwrap();
4274 assert!(
4275 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4276 "错误信息应说明不支持行锁"
4277 );
4278 }
4279
4280 #[test]
4281 fn test_lock_shared_sqlite_should_fail() {
4282 let dialect = get_dialect(DbType::Sqlite).unwrap();
4283 let result = QueryBuilder::<TestModel>::new(dialect)
4284 .table("users")
4285 .where_eq("id", Value::I64(1))
4286 .lock_shared();
4287 assert!(result.is_err(), "SQLite 不应支持共享锁");
4288 let err = result.err().unwrap();
4289 assert!(
4290 format!("{:?}", err).contains("Shared lock is not supported"),
4291 "错误信息应说明不支持共享锁"
4292 );
4293 }
4294
4295 #[test]
4296 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4297 let dialect = get_dialect(DbType::MySQL)?;
4298 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4299 .table("users")
4300 .where_eq("status", Value::String("active".into()))
4301 .limit(10)
4302 .offset(20)
4303 .lock_for_update()?
4304 .build_select_with_params();
4305 assert!(sql.contains("WHERE `status` = ?"));
4306 assert!(sql.contains("LIMIT 10"));
4307 assert!(sql.contains("OFFSET 20"));
4308 assert!(sql.contains("FOR UPDATE"));
4309 assert_eq!(params.len(), 1);
4310 Ok(())
4311 }
4312
4313 #[test]
4316 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4317 let dialect = get_dialect(DbType::MySQL)?;
4318 let mut data = std::collections::HashMap::new();
4319 data.insert("name".to_string(), Value::String("Alice".into()));
4320 data.insert("age".to_string(), Value::I64(30));
4321
4322 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4323 .table("users")
4324 .insert_or_ignore()
4325 .build_insert_with_params(&data);
4326 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4327 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4329 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4330 assert!(sql.contains("VALUES (?, ?)"));
4331 assert_eq!(params.len(), 2);
4332 Ok(())
4333 }
4334
4335 #[test]
4336 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4337 let dialect = get_dialect(DbType::PostgreSQL)?;
4338 let mut data = std::collections::HashMap::new();
4339 data.insert("name".to_string(), Value::String("Bob".into()));
4340
4341 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4342 .table("users")
4343 .insert_or_ignore()
4344 .build_insert_with_params(&data);
4345 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4346 assert!(sql.contains("(\"name\")"));
4347 assert!(sql.contains("VALUES (?)"));
4348 assert_eq!(params.len(), 1);
4349 assert_eq!(params[0], Value::String("Bob".into()));
4350 Ok(())
4351 }
4352
4353 #[test]
4354 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4355 let dialect = get_dialect(DbType::Sqlite)?;
4356 let mut data = std::collections::HashMap::new();
4357 data.insert("name".to_string(), Value::String("Charlie".into()));
4358
4359 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4360 .table("users")
4361 .insert_or_ignore()
4362 .build_insert_with_params(&data);
4363 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4364 assert!(sql.contains("(\"name\")"));
4365 assert!(sql.contains("VALUES (?)"));
4366 assert_eq!(params.len(), 1);
4367 assert_eq!(params[0], Value::String("Charlie".into()));
4368 Ok(())
4369 }
4370
4371 #[test]
4372 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4373 let dialect = get_dialect(DbType::MySQL)?;
4374 let mut data = std::collections::HashMap::new();
4375 data.insert("name".to_string(), Value::String("Dave".into()));
4376
4377 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4378 .table("users")
4379 .build_insert_with_params(&data);
4380 assert!(sql.contains("INSERT INTO `users`"));
4381 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4382 assert_eq!(params.len(), 1);
4383 assert_eq!(params[0], Value::String("Dave".into()));
4384 Ok(())
4385 }
4386
4387 #[test]
4388 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4389 let dialect = get_dialect(DbType::MySQL)?;
4390 let data = std::collections::HashMap::new();
4391
4392 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4393 .table("users")
4394 .insert_or_ignore()
4395 .build_insert_with_params(&data);
4396 assert!(sql.is_empty(), "空数据应返回空 SQL");
4397 assert!(params.is_empty());
4398 Ok(())
4399 }
4400
4401 #[test]
4404 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4405 let mysql = get_dialect(DbType::MySQL)?;
4406 let pg = get_dialect(DbType::PostgreSQL)?;
4407 let sqlite = get_dialect(DbType::Sqlite)?;
4408 let clickhouse = get_dialect(DbType::ClickHouse)?;
4409 let duckdb = get_dialect(DbType::DuckDB)?;
4410
4411 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4412 assert!(
4413 pg.supports_lock_for_update(),
4414 "PostgreSQL 应支持 FOR UPDATE"
4415 );
4416 assert!(
4417 !sqlite.supports_lock_for_update(),
4418 "SQLite 不应支持 FOR UPDATE"
4419 );
4420 assert!(
4421 !clickhouse.supports_lock_for_update(),
4422 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4423 );
4424 assert!(
4425 !duckdb.supports_lock_for_update(),
4426 "DuckDB 不应支持 FOR UPDATE"
4427 );
4428 Ok(())
4429 }
4430
4431 #[test]
4432 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4433 let mysql = get_dialect(DbType::MySQL)?;
4434 let pg = get_dialect(DbType::PostgreSQL)?;
4435 let sqlite = get_dialect(DbType::Sqlite)?;
4436 let clickhouse = get_dialect(DbType::ClickHouse)?;
4437 let duckdb = get_dialect(DbType::DuckDB)?;
4438
4439 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4440 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4441 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4442 assert!(
4443 !clickhouse.supports_lock_shared(),
4444 "ClickHouse 是列式 OLAP,不应支持共享锁"
4445 );
4446 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4447 Ok(())
4448 }
4449
4450 #[test]
4451 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4452 let dialect = get_dialect(DbType::MySQL)?;
4453
4454 let builder = QueryBuilder::<TestModel>::new(dialect);
4456 assert!(builder.get_lock_type().is_none(), "默认无锁");
4457 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4458
4459 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4461 .table("users")
4462 .lock_for_update()?;
4463 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4464
4465 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4467 .table("users")
4468 .insert_or_ignore();
4469 assert!(builder.is_insert_or_ignore());
4470
4471 Ok(())
4472 }
4473
4474 struct TestModelWithColumns;
4475 impl Model for TestModelWithColumns {
4476 type PrimaryKey = i64;
4477 fn table_name() -> &'static str {
4478 "test_with_cols"
4479 }
4480 fn pk(&self) -> Self::PrimaryKey {
4481 0
4482 }
4483 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4484 }
4485 impl crate::model::ModelExt for TestModelWithColumns {
4486 fn columns() -> Vec<&'static str> {
4487 vec!["id", "name", "email", "avatar", "blob_data"]
4488 }
4489 fn fillable() -> Vec<&'static str> {
4490 vec!["name", "email", "avatar", "blob_data"]
4491 }
4492 fn guarded() -> Vec<&'static str> {
4493 vec!["id"]
4494 }
4495 fn hidden() -> Vec<&'static str> {
4496 vec!["blob_data"]
4497 }
4498 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4499 std::collections::HashMap::new()
4500 }
4501 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4502 fn to_json(&self) -> serde_json::Value {
4503 serde_json::Value::Null
4504 }
4505 }
4506
4507 #[test]
4508 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4509 let dialect = get_dialect(DbType::MySQL)?;
4510 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4511 .table("users")
4512 .select_exclude(&["avatar", "blob_data"])?;
4513 let sql = builder.build_select();
4514 assert!(sql.contains("id"));
4515 assert!(sql.contains("name"));
4516 assert!(sql.contains("email"));
4517 assert!(!sql.contains("avatar"));
4518 assert!(!sql.contains("blob_data"));
4519 Ok(())
4520 }
4521
4522 #[test]
4523 fn test_select_exclude_nonexistent_field() {
4524 let dialect = get_dialect(DbType::MySQL).unwrap();
4525 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4526 .table("users")
4527 .select_exclude(&["nonexistent"]);
4528 assert!(result.is_err());
4529 }
4530
4531 #[test]
4532 fn test_select_exclude_all_fields() {
4533 let dialect = get_dialect(DbType::MySQL).unwrap();
4534 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4535 .table("users")
4536 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4537 assert!(result.is_err());
4538 let err = result.unwrap_err();
4539 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4540 }
4541
4542 #[tokio::test]
4545 async fn test_execute_with_cache_ttl_hit() {
4546 use crate::l2_cache::L2Cache;
4547 use std::sync::atomic::{AtomicU64, Ordering};
4548 use std::sync::Arc;
4549 use std::time::Duration;
4550
4551 let dialect = get_dialect(DbType::MySQL).unwrap();
4552 let cache = L2Cache::new();
4553 let qb = QueryBuilder::<TestModel>::new(dialect)
4554 .table("users")
4555 .where_eq("status", Value::I64(1))
4556 .cache_ttl(Duration::from_secs(300));
4557
4558 let call_count = Arc::new(AtomicU64::new(0));
4559 let cc1 = Arc::clone(&call_count);
4560 let rows1 = qb
4561 .execute_with_cache(&cache, "users", || async move {
4562 cc1.fetch_add(1, Ordering::SeqCst);
4563 let mut row = std::collections::HashMap::new();
4564 row.insert("id".to_string(), Value::I64(1));
4565 Ok(vec![row])
4566 })
4567 .await
4568 .unwrap();
4569 assert_eq!(rows1.len(), 1);
4570 assert_eq!(call_count.load(Ordering::SeqCst), 1);
4571
4572 let cc2 = Arc::clone(&call_count);
4573 let rows2 = qb
4574 .execute_with_cache(&cache, "users", || async move {
4575 cc2.fetch_add(1, Ordering::SeqCst);
4576 let mut row = std::collections::HashMap::new();
4577 row.insert("id".to_string(), Value::I64(2));
4578 Ok(vec![row])
4579 })
4580 .await
4581 .unwrap();
4582 assert_eq!(rows2.len(), 1);
4583 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
4584 assert_eq!(
4585 call_count.load(Ordering::SeqCst),
4586 1,
4587 "second call should hit cache, not loader"
4588 );
4589 }
4590
4591 #[tokio::test]
4592 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
4593 use crate::l2_cache::L2Cache;
4594 use std::sync::atomic::{AtomicU64, Ordering};
4595 use std::sync::Arc;
4596
4597 let dialect = get_dialect(DbType::MySQL).unwrap();
4598 let cache = L2Cache::new();
4599 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
4600
4601 let call_count = Arc::new(AtomicU64::new(0));
4602 let cc1 = Arc::clone(&call_count);
4603 let rows1 = qb
4604 .execute_with_cache(&cache, "users", || async move {
4605 cc1.fetch_add(1, Ordering::SeqCst);
4606 Ok(vec![])
4607 })
4608 .await
4609 .unwrap();
4610 assert!(rows1.is_empty());
4611
4612 let cc2 = Arc::clone(&call_count);
4613 let rows2 = qb
4614 .execute_with_cache(&cache, "users", || async move {
4615 cc2.fetch_add(1, Ordering::SeqCst);
4616 Ok(vec![])
4617 })
4618 .await
4619 .unwrap();
4620 assert!(rows2.is_empty());
4621 assert_eq!(
4622 call_count.load(Ordering::SeqCst),
4623 2,
4624 "without ttl, loader should be called each time"
4625 );
4626 }
4627}