1use crate::db_type::DbType;
26use crate::dialect::Dialect;
27use crate::dialect::LockType;
28use crate::model::Model;
29use crate::typed::TypedColumn;
30use crate::value::Value;
31use std::fmt;
32use std::fmt::Write as _;
33use std::time::Duration;
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 sql(&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 let _ = write!(
1478 sql,
1479 " INNER JOIN {} ON {} = {}",
1480 self.dialect.quote(t),
1481 self.dialect.quote(l),
1482 self.dialect.quote(r)
1483 );
1484 }
1485 JoinClause::Left(t, l, r) => {
1486 let _ = write!(
1487 sql,
1488 " LEFT JOIN {} ON {} = {}",
1489 self.dialect.quote(t),
1490 self.dialect.quote(l),
1491 self.dialect.quote(r)
1492 );
1493 }
1494 JoinClause::Right(t, l, r) => {
1495 let _ = write!(
1496 sql,
1497 " RIGHT JOIN {} ON {} = {}",
1498 self.dialect.quote(t),
1499 self.dialect.quote(l),
1500 self.dialect.quote(r)
1501 );
1502 }
1503 JoinClause::Cross(t, on) => {
1504 let _ = write!(
1505 sql,
1506 " CROSS JOIN {} ON {}",
1507 self.dialect.quote(t),
1508 self.dialect.quote(on)
1509 );
1510 }
1511 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1512 let _ = write!(
1513 sql,
1514 " {} {} ON {}.{} = {}.{}",
1515 kind.as_sql(),
1516 self.dialect.quote(tt),
1517 self.dialect.quote(ft),
1518 self.dialect.quote(fk),
1519 self.dialect.quote(tt),
1520 self.dialect.quote(tk)
1521 );
1522 }
1523 }
1524 }
1525
1526 let where_clause = self.build_where_clause();
1528 if !where_clause.is_empty() {
1529 sql.push_str(&where_clause);
1530 }
1531
1532 if !self.group_by.is_empty() {
1533 let cols: Vec<String> = self
1534 .group_by
1535 .iter()
1536 .map(|c| self.dialect.quote(c))
1537 .collect();
1538 sql.push_str(" GROUP BY ");
1539 sql.push_str(&cols.join(", "));
1540 }
1541
1542 if !self.having_conditions.is_empty() {
1543 sql.push_str(" HAVING ");
1544 for (i, cond) in self.having_conditions.iter().enumerate() {
1545 if i > 0 {
1546 sql.push_str(" AND ");
1547 }
1548 match cond {
1549 WhereCondition::And(c) => sql.push_str(c),
1550 WhereCondition::Having(agg, op, value) => {
1552 sql.push_str(&format!(
1553 "{} {} {}",
1554 agg.render(&*self.dialect),
1555 op.as_sql(),
1556 value.to_param_with_dialect(&*self.dialect)
1557 ));
1558 }
1559 _ => {}
1560 }
1561 }
1562 }
1563
1564 if !self.order_by.is_empty() {
1565 let order_cols: Vec<String> = self
1566 .order_by
1567 .iter()
1568 .map(|o| {
1569 let dir = match o.direction {
1570 OrderDirection::Asc => " ASC",
1571 OrderDirection::Desc => " DESC",
1572 };
1573 format!("{}{}", self.dialect.quote(&o.field), dir)
1574 })
1575 .collect();
1576 sql.push_str(" ORDER BY ");
1577 sql.push_str(&order_cols.join(", "));
1578 }
1579
1580 if let Some(limit) = self.limit_value {
1581 let _ = write!(sql, " LIMIT {}", limit);
1582 }
1583
1584 if let Some(offset) = self.offset_value {
1585 let _ = write!(sql, " OFFSET {}", offset);
1586 }
1587
1588 sql.into_string()
1589 }
1590
1591 fn build_where_clause(&self) -> String {
1597 self.build_where_clause_with_options(true)
1598 }
1599
1600 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1608 let soft_delete_cond = if include_soft_delete {
1610 self.build_soft_delete_condition()
1611 } else {
1612 None
1613 };
1614
1615 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1617 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1619 });
1620
1621 if self.where_conditions.is_empty()
1623 && soft_delete_cond.is_none()
1624 && tenant_cond.is_none()
1625 && self.keyset_cursor.is_none()
1626 {
1627 return String::new();
1628 }
1629
1630 let mut conditions: Vec<String> = self
1632 .where_conditions
1633 .iter()
1634 .map(|cond| match cond {
1635 WhereCondition::And(c) => c.clone(),
1636 WhereCondition::Or(c) => format!("OR {}", c),
1637 WhereCondition::Eq(f, v) => format!(
1639 "{} = {}",
1640 self.dialect.quote(f),
1641 v.to_param_with_dialect(&*self.dialect)
1642 ),
1643 WhereCondition::Ne(f, v) => format!(
1644 "{} != {}",
1645 self.dialect.quote(f),
1646 v.to_param_with_dialect(&*self.dialect)
1647 ),
1648 WhereCondition::Gt(f, v) => format!(
1649 "{} > {}",
1650 self.dialect.quote(f),
1651 v.to_param_with_dialect(&*self.dialect)
1652 ),
1653 WhereCondition::Ge(f, v) => format!(
1654 "{} >= {}",
1655 self.dialect.quote(f),
1656 v.to_param_with_dialect(&*self.dialect)
1657 ),
1658 WhereCondition::Lt(f, v) => format!(
1659 "{} < {}",
1660 self.dialect.quote(f),
1661 v.to_param_with_dialect(&*self.dialect)
1662 ),
1663 WhereCondition::Le(f, v) => format!(
1664 "{} <= {}",
1665 self.dialect.quote(f),
1666 v.to_param_with_dialect(&*self.dialect)
1667 ),
1668 WhereCondition::Like(f, v) => format!(
1669 "{} LIKE {}",
1670 self.dialect.quote(f),
1671 v.to_param_with_dialect(&*self.dialect)
1672 ),
1673 WhereCondition::OrEq(f, v) => format!(
1674 "OR {} = {}",
1675 self.dialect.quote(f),
1676 v.to_param_with_dialect(&*self.dialect)
1677 ),
1678 WhereCondition::OrNe(f, v) => format!(
1679 "OR {} != {}",
1680 self.dialect.quote(f),
1681 v.to_param_with_dialect(&*self.dialect)
1682 ),
1683 WhereCondition::OrGt(f, v) => format!(
1684 "OR {} > {}",
1685 self.dialect.quote(f),
1686 v.to_param_with_dialect(&*self.dialect)
1687 ),
1688 WhereCondition::OrGe(f, v) => format!(
1689 "OR {} >= {}",
1690 self.dialect.quote(f),
1691 v.to_param_with_dialect(&*self.dialect)
1692 ),
1693 WhereCondition::OrLt(f, v) => format!(
1694 "OR {} < {}",
1695 self.dialect.quote(f),
1696 v.to_param_with_dialect(&*self.dialect)
1697 ),
1698 WhereCondition::OrLe(f, v) => format!(
1699 "OR {} <= {}",
1700 self.dialect.quote(f),
1701 v.to_param_with_dialect(&*self.dialect)
1702 ),
1703 WhereCondition::OrLike(f, v) => format!(
1704 "OR {} LIKE {}",
1705 self.dialect.quote(f),
1706 v.to_param_with_dialect(&*self.dialect)
1707 ),
1708 WhereCondition::In(f, vals) => {
1709 let vals_str: Vec<String> = vals
1711 .iter()
1712 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1713 .collect();
1714 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1715 }
1716 WhereCondition::NotIn(f, vals) => {
1717 let vals_str: Vec<String> = vals
1718 .iter()
1719 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1720 .collect();
1721 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1722 }
1723 WhereCondition::Between(f, start, end) => {
1724 format!(
1725 "{} BETWEEN {} AND {}",
1726 self.dialect.quote(f),
1727 start.to_param_with_dialect(&*self.dialect),
1728 end.to_param_with_dialect(&*self.dialect)
1729 )
1730 }
1731 WhereCondition::NotBetween(f, start, end) => {
1732 format!(
1733 "{} NOT BETWEEN {} AND {}",
1734 self.dialect.quote(f),
1735 start.to_param_with_dialect(&*self.dialect),
1736 end.to_param_with_dialect(&*self.dialect)
1737 )
1738 }
1739 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1740 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1741 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1742 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1743 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1744 WhereCondition::Having(..) => String::new(),
1746 })
1747 .collect();
1748
1749 if let Some(sd_cond) = soft_delete_cond {
1751 conditions.push(sd_cond);
1752 }
1753
1754 if let Some(t_cond) = tenant_cond {
1756 conditions.push(t_cond);
1757 }
1758
1759 if let Some(ref cursor) = self.keyset_cursor {
1761 let op = match cursor.direction {
1762 KeysetDirection::After => ">",
1763 KeysetDirection::Before => "<",
1764 };
1765 conditions.push(format!(
1766 "{} {} {}",
1767 self.dialect.quote(&cursor.field),
1768 op,
1769 cursor.value.to_param_with_dialect(&*self.dialect)
1770 ));
1771 }
1772
1773 if conditions.is_empty() {
1774 return String::new();
1775 }
1776
1777 let mut groups: Vec<Vec<String>> = Vec::new();
1780 let mut current_group: Vec<String> = Vec::new();
1781 for cond in conditions.iter() {
1782 if let Some(stripped) = cond.strip_prefix("OR ") {
1783 current_group.push(stripped.to_string());
1785 } else {
1786 if !current_group.is_empty() {
1788 groups.push(std::mem::take(&mut current_group));
1789 }
1790 current_group.push(cond.clone());
1791 }
1792 }
1793 if !current_group.is_empty() {
1794 groups.push(current_group);
1795 }
1796
1797 let group_strs: Vec<String> = groups
1798 .iter()
1799 .map(|g| {
1800 if g.len() == 1 {
1801 g[0].clone()
1802 } else {
1803 format!("({})", g.join(" OR "))
1804 }
1805 })
1806 .collect();
1807
1808 format!(" WHERE {}", group_strs.join(" AND "))
1810 }
1811
1812 #[tracing::instrument(skip(self), fields(op = "select"))]
1817 pub fn build_select(&self) -> (String, Vec<Value>) {
1818 self.build_select_with_params()
1819 }
1820
1821 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1823 pub fn build_insert(
1824 &self,
1825 data: &std::collections::HashMap<String, Value>,
1826 ) -> (String, Vec<Value>) {
1827 self.build_insert_with_params(data)
1828 }
1829
1830 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1833 pub fn sql_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1834 let table = self
1835 .table
1836 .clone()
1837 .unwrap_or_else(|| M::table_name().to_string());
1838
1839 if data.is_empty() {
1840 return String::new();
1841 }
1842
1843 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1844 let values: Vec<String> = data
1846 .values()
1847 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1848 .collect();
1849
1850 crate::sql_buffer::SqlBuffer::from_str(&format!(
1851 "INSERT INTO {} ({}) VALUES ({})",
1852 self.dialect.quote(&table),
1853 columns.join(", "),
1854 values.join(", ")
1855 ))
1856 .into_string()
1857 }
1858
1859 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1861 pub fn build_update(
1862 &self,
1863 data: &std::collections::HashMap<String, Value>,
1864 ) -> (String, Vec<Value>) {
1865 self.build_update_with_params(data)
1866 }
1867
1868 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1871 pub fn sql_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1872 let table = self
1873 .table
1874 .clone()
1875 .unwrap_or_else(|| M::table_name().to_string());
1876
1877 if data.is_empty() {
1878 return String::new();
1879 }
1880
1881 let set_clauses: Vec<String> = data
1882 .iter()
1883 .map(|(k, v)| {
1884 format!(
1885 "{} = {}",
1886 self.dialect.quote(k),
1887 v.to_param_with_dialect(&*self.dialect)
1888 )
1889 })
1890 .collect();
1891
1892 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1893 "UPDATE {} SET {}",
1894 self.dialect.quote(&table),
1895 set_clauses.join(", ")
1896 ));
1897
1898 sql.push_str(&self.build_where_clause());
1899 sql.into_string()
1900 }
1901
1902 #[tracing::instrument(skip(self), fields(op = "delete"))]
1914 pub fn build_delete(&self) -> (String, Vec<Value>) {
1915 self.build_delete_with_params()
1916 }
1917
1918 #[tracing::instrument(skip(self), fields(op = "delete"))]
1921 pub fn sql_delete(&self) -> String {
1922 let table = self
1923 .table
1924 .clone()
1925 .unwrap_or_else(|| M::table_name().to_string());
1926
1927 if let Some(field) = self.soft_delete_field() {
1929 let where_clause = self.build_where_clause();
1930 return format!(
1931 "UPDATE {} SET {} = NOW(){}",
1932 self.dialect.quote(&table),
1933 self.dialect.quote(field),
1934 where_clause
1935 );
1936 }
1937
1938 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1939 "DELETE FROM {}",
1940 self.dialect.quote(&table)
1941 ));
1942 sql.push_str(&self.build_where_clause());
1943 sql.into_string()
1944 }
1945
1946 pub fn build_force_delete(&self) -> String {
1956 let table = self
1957 .table
1958 .clone()
1959 .unwrap_or_else(|| M::table_name().to_string());
1960
1961 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1962 sql.push_str(&self.build_where_clause_with_options(false));
1964 sql
1965 }
1966
1967 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
1977 self.build_where_clause_with_params_options(true)
1979 }
1980
1981 fn build_where_clause_with_params_options(
1990 &self,
1991 include_soft_delete: bool,
1992 ) -> (String, Vec<Value>) {
1993 let soft_delete_cond = if include_soft_delete {
1995 self.build_soft_delete_condition()
1996 } else {
1997 None
1998 };
1999
2000 let tenant_cond = self.build_tenant_condition();
2002
2003 #[cfg(feature = "tenant-quota-rls-enhanced")]
2005 let rls_cond: Option<(String, Vec<Value>)> = {
2006 if let Some(ref enhancer) = self.rls_enhancer {
2007 if let Some(ref table) = self.table {
2008 if let Some(tid) = self.tenant_id_value() {
2009 match enhancer.enhance_query(table, &tid.to_string()) {
2010 Ok(Some(c)) => Some((c.sql_fragment, c.params)),
2011 Ok(None) => None,
2012 Err(_) => None,
2013 }
2014 } else {
2015 None
2016 }
2017 } else {
2018 None
2019 }
2020 } else {
2021 None
2022 }
2023 };
2024 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
2025 let rls_cond: Option<(String, Vec<Value>)> = None;
2026
2027 if self.where_conditions.is_empty()
2029 && soft_delete_cond.is_none()
2030 && tenant_cond.is_none()
2031 && self.keyset_cursor.is_none()
2032 && rls_cond.is_none()
2033 {
2034 return (String::new(), Vec::new());
2035 }
2036
2037 let mut params = Vec::new();
2038
2039 let mut conditions: Vec<String> = self
2040 .where_conditions
2041 .iter()
2042 .map(|cond| match cond {
2043 WhereCondition::And(c) => c.clone(),
2044 WhereCondition::Or(c) => format!("OR {}", c),
2045 WhereCondition::Eq(f, v) => {
2047 params.push(v.clone());
2048 format!("{} = ?", self.dialect.quote(f))
2049 }
2050 WhereCondition::Ne(f, v) => {
2051 params.push(v.clone());
2052 format!("{} != ?", self.dialect.quote(f))
2053 }
2054 WhereCondition::Gt(f, v) => {
2055 params.push(v.clone());
2056 format!("{} > ?", self.dialect.quote(f))
2057 }
2058 WhereCondition::Ge(f, v) => {
2059 params.push(v.clone());
2060 format!("{} >= ?", self.dialect.quote(f))
2061 }
2062 WhereCondition::Lt(f, v) => {
2063 params.push(v.clone());
2064 format!("{} < ?", self.dialect.quote(f))
2065 }
2066 WhereCondition::Le(f, v) => {
2067 params.push(v.clone());
2068 format!("{} <= ?", self.dialect.quote(f))
2069 }
2070 WhereCondition::Like(f, v) => {
2071 params.push(v.clone());
2072 format!("{} LIKE ?", self.dialect.quote(f))
2073 }
2074 WhereCondition::OrEq(f, v) => {
2075 params.push(v.clone());
2076 format!("OR {} = ?", self.dialect.quote(f))
2077 }
2078 WhereCondition::OrNe(f, v) => {
2079 params.push(v.clone());
2080 format!("OR {} != ?", self.dialect.quote(f))
2081 }
2082 WhereCondition::OrGt(f, v) => {
2083 params.push(v.clone());
2084 format!("OR {} > ?", self.dialect.quote(f))
2085 }
2086 WhereCondition::OrGe(f, v) => {
2087 params.push(v.clone());
2088 format!("OR {} >= ?", self.dialect.quote(f))
2089 }
2090 WhereCondition::OrLt(f, v) => {
2091 params.push(v.clone());
2092 format!("OR {} < ?", self.dialect.quote(f))
2093 }
2094 WhereCondition::OrLe(f, v) => {
2095 params.push(v.clone());
2096 format!("OR {} <= ?", self.dialect.quote(f))
2097 }
2098 WhereCondition::OrLike(f, v) => {
2099 params.push(v.clone());
2100 format!("OR {} LIKE ?", self.dialect.quote(f))
2101 }
2102 WhereCondition::In(f, vals) => {
2103 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2104 params.extend(vals.iter().cloned());
2105 format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
2106 }
2107 WhereCondition::NotIn(f, vals) => {
2108 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
2109 params.extend(vals.iter().cloned());
2110 format!(
2111 "{} NOT IN ({})",
2112 self.dialect.quote(f),
2113 placeholders.join(", ")
2114 )
2115 }
2116 WhereCondition::Between(f, start, end) => {
2117 params.push(start.clone());
2118 params.push(end.clone());
2119 format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
2120 }
2121 WhereCondition::NotBetween(f, start, end) => {
2122 params.push(start.clone());
2123 params.push(end.clone());
2124 format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
2125 }
2126 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
2127 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
2128 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
2129 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
2130 WhereCondition::TypedExpr(sql, expr_params) => {
2131 params.extend(expr_params.iter().cloned());
2132 sql.clone()
2133 }
2134 WhereCondition::Having(..) => String::new(),
2136 })
2137 .collect();
2138
2139 if let Some(sd_cond) = soft_delete_cond {
2141 conditions.push(sd_cond);
2142 }
2143
2144 if let Some((t_sql, t_value)) = tenant_cond {
2146 conditions.push(t_sql);
2147 params.push(t_value);
2148 }
2149
2150 if let Some((rls_sql, rls_params)) = rls_cond {
2152 conditions.push(rls_sql);
2153 params.extend(rls_params);
2154 }
2155
2156 if let Some(ref cursor) = self.keyset_cursor {
2158 let op = match cursor.direction {
2159 KeysetDirection::After => ">",
2160 KeysetDirection::Before => "<",
2161 };
2162 conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
2163 params.push(cursor.value.clone());
2164 }
2165
2166 if conditions.is_empty() {
2167 return (String::new(), params);
2168 }
2169
2170 let mut groups: Vec<Vec<String>> = Vec::new();
2172 let mut current_group: Vec<String> = Vec::new();
2173 for cond in conditions.iter() {
2174 if let Some(stripped) = cond.strip_prefix("OR ") {
2175 current_group.push(stripped.to_string());
2176 } else {
2177 if !current_group.is_empty() {
2178 groups.push(std::mem::take(&mut current_group));
2179 }
2180 current_group.push(cond.clone());
2181 }
2182 }
2183 if !current_group.is_empty() {
2184 groups.push(current_group);
2185 }
2186
2187 let group_strs: Vec<String> = groups
2188 .iter()
2189 .map(|g| {
2190 if g.len() == 1 {
2191 g[0].clone()
2192 } else {
2193 format!("({})", g.join(" OR "))
2194 }
2195 })
2196 .collect();
2197
2198 (format!(" WHERE {}", group_strs.join(" AND ")), params)
2200 }
2201
2202 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
2207 let table = self
2208 .table
2209 .clone()
2210 .unwrap_or_else(|| M::table_name().to_string());
2211 let columns = if self.select_columns.is_empty() {
2212 "*".to_string()
2213 } else {
2214 self.select_columns.join(", ")
2215 };
2216
2217 let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
2218
2219 for join in &self.joins {
2220 match join {
2221 JoinClause::Inner(t, l, r) => {
2222 let _ = write!(
2223 sql,
2224 " INNER JOIN {} ON {} = {}",
2225 self.dialect.quote(t),
2226 self.dialect.quote(l),
2227 self.dialect.quote(r)
2228 );
2229 }
2230 JoinClause::Left(t, l, r) => {
2231 let _ = write!(
2232 sql,
2233 " LEFT JOIN {} ON {} = {}",
2234 self.dialect.quote(t),
2235 self.dialect.quote(l),
2236 self.dialect.quote(r)
2237 );
2238 }
2239 JoinClause::Right(t, l, r) => {
2240 let _ = write!(
2241 sql,
2242 " RIGHT JOIN {} ON {} = {}",
2243 self.dialect.quote(t),
2244 self.dialect.quote(l),
2245 self.dialect.quote(r)
2246 );
2247 }
2248 JoinClause::Cross(t, on) => {
2249 let _ = write!(
2250 sql,
2251 " CROSS JOIN {} ON {}",
2252 self.dialect.quote(t),
2253 self.dialect.quote(on)
2254 );
2255 }
2256 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2257 let _ = write!(
2258 sql,
2259 " {} {} ON {}.{} = {}.{}",
2260 kind.as_sql(),
2261 self.dialect.quote(tt),
2262 self.dialect.quote(ft),
2263 self.dialect.quote(fk),
2264 self.dialect.quote(tt),
2265 self.dialect.quote(tk)
2266 );
2267 }
2268 }
2269 }
2270
2271 let mut params = Vec::new();
2272 let (where_clause, where_params) = self.build_where_clause_with_params();
2274 if !where_clause.is_empty() {
2275 sql.push_str(&where_clause);
2276 params = where_params;
2277 }
2278
2279 if !self.group_by.is_empty() {
2280 let cols: Vec<String> = self
2281 .group_by
2282 .iter()
2283 .map(|c| self.dialect.quote(c))
2284 .collect();
2285 sql.push_str(" GROUP BY ");
2286 sql.push_str(&cols.join(", "));
2287 }
2288
2289 if !self.having_conditions.is_empty() {
2290 sql.push_str(" HAVING ");
2291 for (i, cond) in self.having_conditions.iter().enumerate() {
2292 if i > 0 {
2293 sql.push_str(" AND ");
2294 }
2295 match cond {
2296 WhereCondition::And(c) => sql.push_str(c),
2297 WhereCondition::Having(agg, op, value) => {
2299 params.push(value.clone());
2300 sql.push_str(&format!("{} {} ?", agg.render(&*self.dialect), op.as_sql()));
2301 }
2302 _ => {}
2303 }
2304 }
2305 }
2306
2307 if !self.order_by.is_empty() {
2308 let order_cols: Vec<String> = self
2309 .order_by
2310 .iter()
2311 .map(|o| {
2312 let dir = match o.direction {
2313 OrderDirection::Asc => " ASC",
2314 OrderDirection::Desc => " DESC",
2315 };
2316 format!("{}{}", self.dialect.quote(&o.field), dir)
2317 })
2318 .collect();
2319 sql.push_str(" ORDER BY ");
2320 sql.push_str(&order_cols.join(", "));
2321 }
2322
2323 if let Some(limit) = self.limit_value {
2324 let _ = write!(sql, " LIMIT {}", limit);
2325 }
2326 if let Some(offset) = self.offset_value {
2327 let _ = write!(sql, " OFFSET {}", offset);
2328 }
2329
2330 if let Some(lock_type) = &self.lock_type {
2332 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2333 sql.push(' ');
2334 sql.push_str(&lock_clause);
2335 }
2336 }
2337
2338 (sql, params)
2339 }
2340
2341 pub fn build_insert_with_params(
2343 &self,
2344 data: &std::collections::HashMap<String, Value>,
2345 ) -> (String, Vec<Value>) {
2346 let table = self
2347 .table
2348 .clone()
2349 .unwrap_or_else(|| M::table_name().to_string());
2350 if data.is_empty() {
2351 return (String::new(), Vec::new());
2352 }
2353
2354 let mut columns = Vec::with_capacity(data.len());
2355 let mut params = Vec::with_capacity(data.len());
2356 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2357 for (k, v) in data.iter() {
2358 columns.push(self.dialect.quote(k));
2359 params.push(v.clone());
2360 }
2361
2362 let insert_clause = if self.insert_or_ignore {
2364 self.dialect.build_insert_or_ignore_prefix(&table)
2365 } else {
2366 format!("INSERT INTO {}", self.dialect.quote(&table))
2367 };
2368
2369 let sql = format!(
2370 "{} ({}) VALUES ({})",
2371 insert_clause,
2372 columns.join(", "),
2373 placeholders.join(", ")
2374 );
2375 (sql, params)
2376 }
2377
2378 pub fn build_batch_insert_with_params(
2385 &self,
2386 rows: &[std::collections::HashMap<String, Value>],
2387 ) -> (String, Vec<Value>) {
2388 let table = self
2389 .table
2390 .clone()
2391 .unwrap_or_else(|| M::table_name().to_string());
2392 if rows.is_empty() {
2393 return (String::new(), Vec::new());
2394 }
2395
2396 let first_row = &rows[0];
2398 let columns: Vec<String> = first_row.keys().cloned().collect();
2399 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2400
2401 let mut params = Vec::with_capacity(rows.len() * columns.len());
2402 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2403 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2404 let mut param_idx = 1usize;
2405 for row in rows {
2406 let placeholders: Vec<String> = columns
2407 .iter()
2408 .map(|col| match row.get(col) {
2409 Some(v) => {
2410 params.push(v.clone());
2411 if is_pg {
2412 let p = format!("${}", param_idx);
2413 param_idx += 1;
2414 p
2415 } else {
2416 "?".to_string()
2417 }
2418 }
2419 None => "NULL".to_string(),
2420 })
2421 .collect();
2422 value_groups.push(format!("({})", placeholders.join(", ")));
2423 }
2424
2425 let sql = format!(
2426 "INSERT INTO {} ({}) VALUES {}",
2427 self.dialect.quote(&table),
2428 quoted_columns.join(", "),
2429 value_groups.join(", ")
2430 );
2431 (sql, params)
2432 }
2433
2434 pub fn build_batch_upsert_with_params(
2455 &self,
2456 rows: &[std::collections::HashMap<String, Value>],
2457 conflict_columns: &[&str],
2458 update_columns: &[&str],
2459 ) -> Result<(String, Vec<Value>), crate::DbError> {
2460 if rows.is_empty() {
2461 return Err(crate::DbError::InvalidInput(
2462 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2463 ));
2464 }
2465
2466 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2468 if insert_sql.is_empty() {
2469 return Err(crate::DbError::InvalidInput(
2470 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2471 ));
2472 }
2473
2474 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2476
2477 let conflict_clause = self
2479 .dialect
2480 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2481 .ok_or_else(|| {
2482 crate::DbError::InvalidInput(format!(
2483 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2484 self.dialect.db_type()
2485 ))
2486 })?;
2487
2488 let sql = format!("{} {}", insert_sql, conflict_clause);
2489 Ok((sql, params))
2490 }
2491
2492 pub fn build_update_with_params(
2495 &self,
2496 data: &std::collections::HashMap<String, Value>,
2497 ) -> (String, Vec<Value>) {
2498 let table = self
2499 .table
2500 .clone()
2501 .unwrap_or_else(|| M::table_name().to_string());
2502 if data.is_empty() {
2503 return (String::new(), Vec::new());
2504 }
2505
2506 let mut set_clauses = Vec::with_capacity(data.len());
2507 let mut params = Vec::with_capacity(data.len());
2508 for (k, v) in data.iter() {
2509 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2510 params.push(v.clone());
2511 }
2512
2513 let mut sql = format!(
2514 "UPDATE {} SET {}",
2515 self.dialect.quote(&table),
2516 set_clauses.join(", ")
2517 );
2518
2519 let (where_clause, where_params) = self.build_where_clause_with_params();
2521 if !where_clause.is_empty() {
2522 sql.push_str(&where_clause);
2523 params.extend(where_params);
2524 }
2525
2526 (sql, params)
2527 }
2528
2529 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2535 let table = self
2536 .table
2537 .clone()
2538 .unwrap_or_else(|| M::table_name().to_string());
2539
2540 if let Some(field) = self.soft_delete_field() {
2542 let (where_clause, where_params) = self.build_where_clause_with_params();
2543 let sql = format!(
2544 "UPDATE {} SET {} = NOW(){}",
2545 self.dialect.quote(&table),
2546 self.dialect.quote(field),
2547 where_clause
2548 );
2549 return (sql, where_params);
2550 }
2551
2552 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2553 let mut params = Vec::new();
2554
2555 let (where_clause, where_params) = self.build_where_clause_with_params();
2556 if !where_clause.is_empty() {
2557 sql.push_str(&where_clause);
2558 params = where_params;
2559 }
2560
2561 (sql, params)
2562 }
2563
2564 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2568 let table = self
2569 .table
2570 .clone()
2571 .unwrap_or_else(|| M::table_name().to_string());
2572
2573 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2574 let mut params = Vec::new();
2575
2576 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2578 if !where_clause.is_empty() {
2579 sql.push_str(&where_clause);
2580 params = where_params;
2581 }
2582
2583 (sql, params)
2584 }
2585
2586 pub fn build_count(&self) -> String {
2588 let table = self
2589 .table
2590 .clone()
2591 .unwrap_or_else(|| M::table_name().to_string());
2592
2593 let mut sql = format!(
2594 "SELECT COUNT(*) as total FROM {}",
2595 self.dialect.quote(&table)
2596 );
2597 sql.push_str(&self.build_where_clause());
2598 sql
2599 }
2600
2601 pub fn build_exists(&self) -> String {
2603 let table = self
2604 .table
2605 .clone()
2606 .unwrap_or_else(|| M::table_name().to_string());
2607
2608 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2609 sql.push_str(&self.build_where_clause());
2610 sql.push_str(" LIMIT 1");
2611 format!("SELECT EXISTS({})", sql)
2612 }
2613
2614 pub fn build_max(&self, field: &str) -> String {
2616 let table = self
2617 .table
2618 .clone()
2619 .unwrap_or_else(|| M::table_name().to_string());
2620
2621 let mut sql = format!(
2622 "SELECT MAX({}) as max_val FROM {}",
2623 self.dialect.quote(field),
2624 self.dialect.quote(&table)
2625 );
2626 sql.push_str(&self.build_where_clause());
2627 sql
2628 }
2629
2630 pub fn build_min(&self, field: &str) -> String {
2632 let table = self
2633 .table
2634 .clone()
2635 .unwrap_or_else(|| M::table_name().to_string());
2636
2637 let mut sql = format!(
2638 "SELECT MIN({}) as min_val FROM {}",
2639 self.dialect.quote(field),
2640 self.dialect.quote(&table)
2641 );
2642 sql.push_str(&self.build_where_clause());
2643 sql
2644 }
2645
2646 pub fn build_sum(&self, field: &str) -> String {
2648 let table = self
2649 .table
2650 .clone()
2651 .unwrap_or_else(|| M::table_name().to_string());
2652
2653 let mut sql = format!(
2654 "SELECT SUM({}) as sum_val FROM {}",
2655 self.dialect.quote(field),
2656 self.dialect.quote(&table)
2657 );
2658 sql.push_str(&self.build_where_clause());
2659 sql
2660 }
2661
2662 pub fn build_avg(&self, field: &str) -> String {
2664 let table = self
2665 .table
2666 .clone()
2667 .unwrap_or_else(|| M::table_name().to_string());
2668
2669 let mut sql = format!(
2670 "SELECT AVG({}) as avg_val FROM {}",
2671 self.dialect.quote(field),
2672 self.dialect.quote(&table)
2673 );
2674 sql.push_str(&self.build_where_clause());
2675 sql
2676 }
2677
2678 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2681 let sql = self.sql();
2682 let mut errors = Vec::new();
2683
2684 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2685 errors.push(e);
2686 }
2687
2688 if !self.joins.is_empty() {
2690 for join in &self.joins {
2691 match join {
2692 JoinClause::Inner(_, left, right)
2693 | JoinClause::Left(_, left, right)
2694 | JoinClause::Right(_, left, right) => {
2695 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2696 errors.push(e);
2697 }
2698 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2699 errors.push(e);
2700 }
2701 }
2702 JoinClause::Relation(_, ft, fk, tt, tk) => {
2703 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2704 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2705 errors.push(e);
2706 }
2707 }
2708 }
2709 _ => {}
2710 }
2711 }
2712 }
2713
2714 let table = self
2716 .table
2717 .clone()
2718 .unwrap_or_else(|| M::table_name().to_string());
2719 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2720 errors.push(e);
2721 }
2722
2723 if errors.is_empty() {
2724 Ok(())
2725 } else {
2726 Err(errors)
2727 }
2728 }
2729
2730 pub fn validate_insert(
2733 &self,
2734 data: &std::collections::HashMap<String, Value>,
2735 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2736 let sql = self.sql_insert(data);
2737 let mut errors = Vec::new();
2738
2739 if sql.is_empty() {
2740 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2741 return Err(errors);
2742 }
2743
2744 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2745 errors.push(e);
2746 }
2747
2748 if errors.is_empty() {
2749 Ok(())
2750 } else {
2751 Err(errors)
2752 }
2753 }
2754
2755 pub fn validate_update(
2758 &self,
2759 data: &std::collections::HashMap<String, Value>,
2760 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2761 let sql = self.sql_update(data);
2762 let mut errors = Vec::new();
2763
2764 if sql.is_empty() {
2765 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2766 return Err(errors);
2767 }
2768
2769 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2770 errors.push(e);
2771 }
2772
2773 if errors.is_empty() {
2774 Ok(())
2775 } else {
2776 Err(errors)
2777 }
2778 }
2779
2780 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2782 let sql = self.sql_delete();
2783 let mut errors = Vec::new();
2784
2785 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2786 errors.push(e);
2787 }
2788
2789 if errors.is_empty() {
2790 Ok(())
2791 } else {
2792 Err(errors)
2793 }
2794 }
2795}
2796
2797impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2799 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2817 let all_columns = M::columns();
2818 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2819
2820 for field in fields {
2821 if !all_columns.contains(field) {
2822 return Err(crate::DbError::InvalidInput(format!(
2823 "排除的字段不存在: {}",
2824 field
2825 )));
2826 }
2827 }
2828
2829 let retained: Vec<String> = all_columns
2830 .into_iter()
2831 .filter(|c| !exclude_set.contains(*c))
2832 .map(|s| s.to_string())
2833 .collect();
2834
2835 if retained.is_empty() {
2836 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2837 }
2838
2839 self.select_mode = crate::partial_model::SelectMode::Partial;
2840 self.select_columns = retained;
2841 Ok(self)
2842 }
2843}
2844
2845impl<M: Model> fmt::Debug for QueryBuilder<M> {
2846 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2847 f.debug_struct("QueryBuilder")
2848 .field("table", &self.table)
2849 .field("select_columns", &self.select_columns)
2850 .field("where_conditions", &self.where_conditions.len())
2851 .field("limit", &self.limit_value)
2852 .finish()
2853 }
2854}
2855
2856#[cfg(test)]
2857#[allow(deprecated)]
2858mod tests {
2859 use super::*;
2860 use crate::db_type::DbType;
2861 use crate::dialect::get_dialect;
2862
2863 struct TestModel;
2864 impl Model for TestModel {
2865 type PrimaryKey = i64;
2866
2867 fn table_name() -> &'static str {
2868 "test_models"
2869 }
2870
2871 fn pk(&self) -> Self::PrimaryKey {
2872 1
2873 }
2874
2875 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2876 }
2877
2878 #[test]
2879 fn test_query_builder_select() -> Result<(), crate::DbError> {
2880 let dialect = get_dialect(DbType::MySQL)?;
2881 let builder = QueryBuilder::<TestModel>::new(dialect);
2882
2883 let sql = builder.table("users").select(vec!["id", "name"])?.sql();
2884 assert!(sql.contains("SELECT `id`, `name` FROM"));
2885 assert!(sql.contains("`users`"));
2886 Ok(())
2887 }
2888
2889 #[test]
2890 fn test_query_builder_where() -> Result<(), crate::DbError> {
2891 let dialect = get_dialect(DbType::MySQL)?;
2892 let builder = QueryBuilder::<TestModel>::new(dialect);
2893
2894 let sql = builder
2895 .table("users")
2896 .where_eq("status", crate::value::Value::String("active".into()))
2897 .where_gt("age", crate::value::Value::I64(18))
2898 .sql();
2899
2900 assert!(sql.contains("WHERE"));
2901 assert!(sql.contains("`status` = 'active'"));
2902 assert!(sql.contains("`age` > 18"));
2903 Ok(())
2904 }
2905
2906 #[test]
2907 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2908 let dialect = get_dialect(DbType::MySQL)?;
2909 let builder = QueryBuilder::<TestModel>::new(dialect);
2910
2911 let sql = builder
2912 .table("users")
2913 .order_by("created_at")
2914 .order_desc("id")
2915 .sql();
2916
2917 assert!(sql.contains("ORDER BY"));
2918 assert!(sql.contains("`created_at` ASC"));
2919 assert!(sql.contains("`id` DESC"));
2920 Ok(())
2921 }
2922
2923 #[test]
2924 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2925 let dialect = get_dialect(DbType::MySQL)?;
2926 let builder = QueryBuilder::<TestModel>::new(dialect);
2927
2928 let sql = builder.table("users").limit(10).offset(20).sql();
2929
2930 assert!(sql.contains("LIMIT 10"));
2931 assert!(sql.contains("OFFSET 20"));
2932 Ok(())
2933 }
2934
2935 #[test]
2936 fn test_query_builder_page() -> Result<(), crate::DbError> {
2937 let dialect = get_dialect(DbType::MySQL)?;
2938 let builder = QueryBuilder::<TestModel>::new(dialect);
2939
2940 let sql = builder.table("users").page(3, 20).sql();
2941
2942 assert!(sql.contains("LIMIT 20"));
2943 assert!(sql.contains("OFFSET 40"));
2944 Ok(())
2945 }
2946
2947 #[test]
2948 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2949 let dialect = get_dialect(DbType::MySQL)?;
2950 let builder = QueryBuilder::<TestModel>::new(dialect);
2951
2952 let mut data = std::collections::HashMap::new();
2953 data.insert("name".to_string(), Value::String("test".to_string()));
2954 data.insert("age".to_string(), Value::I64(25));
2955
2956 let sql = builder.table("users").sql_insert(&data);
2957
2958 assert!(sql.contains("INSERT INTO"));
2959 assert!(sql.contains("`name`"));
2960 assert!(sql.contains("'test'"));
2961 Ok(())
2962 }
2963
2964 #[test]
2965 fn test_query_builder_update() -> Result<(), crate::DbError> {
2966 let dialect = get_dialect(DbType::MySQL)?;
2967 let builder = QueryBuilder::<TestModel>::new(dialect);
2968
2969 let mut data = std::collections::HashMap::new();
2970 data.insert("name".to_string(), Value::String("updated".to_string()));
2971
2972 let sql = builder
2973 .table("users")
2974 .where_eq("id", Value::I64(1))
2975 .sql_update(&data);
2976
2977 assert!(sql.contains("UPDATE"));
2978 assert!(sql.contains("`name` = 'updated'"));
2979 assert!(sql.contains("WHERE"));
2980 Ok(())
2981 }
2982
2983 #[test]
2984 fn test_query_builder_delete() -> Result<(), crate::DbError> {
2985 let dialect = get_dialect(DbType::MySQL)?;
2986 let builder = QueryBuilder::<TestModel>::new(dialect);
2987
2988 let sql = builder
2989 .table("users")
2990 .where_eq("id", Value::I64(1))
2991 .sql_delete();
2992
2993 assert!(sql.contains("DELETE FROM"));
2994 assert!(sql.contains("WHERE"));
2995 Ok(())
2996 }
2997
2998 #[test]
2999 fn test_query_builder_count() -> Result<(), crate::DbError> {
3000 let dialect = get_dialect(DbType::MySQL)?;
3001 let builder = QueryBuilder::<TestModel>::new(dialect);
3002
3003 let sql = builder.table("users").build_count();
3004
3005 assert!(sql.contains("SELECT COUNT(*)"));
3006 assert!(sql.contains("FROM"));
3007 Ok(())
3008 }
3009
3010 #[test]
3011 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
3012 let dialect = get_dialect(DbType::MySQL)?;
3013 let builder = QueryBuilder::<TestModel>::new(dialect);
3014
3015 let sql = builder
3016 .table("users")
3017 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3018 .sql();
3019
3020 assert!(sql.contains("IN ("));
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
3026 let dialect = get_dialect(DbType::MySQL)?;
3027 let builder = QueryBuilder::<TestModel>::new(dialect);
3028
3029 let sql = builder
3030 .table("users")
3031 .where_between("age", Value::I64(18), Value::I64(30))
3032 .sql();
3033
3034 assert!(sql.contains("BETWEEN"));
3035 Ok(())
3036 }
3037
3038 #[test]
3039 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
3040 let dialect = get_dialect(DbType::MySQL)?;
3041 let builder = QueryBuilder::<TestModel>::new(dialect);
3042
3043 let sql = builder.table("users").where_null("deleted_at").sql();
3044
3045 assert!(sql.contains("IS NULL"));
3046 Ok(())
3047 }
3048
3049 #[test]
3050 fn test_query_builder_join() -> Result<(), crate::DbError> {
3051 let dialect = get_dialect(DbType::MySQL)?;
3052 let builder = QueryBuilder::<TestModel>::new(dialect);
3053
3054 let sql = builder
3055 .table("users")
3056 .join_inner("posts", "users.id", "posts.user_id")
3057 .sql();
3058
3059 assert!(sql.contains("INNER JOIN"));
3060 assert!(sql.contains("`posts`"));
3061 Ok(())
3062 }
3063
3064 #[test]
3065 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3066 let dialect = get_dialect(DbType::MySQL)?;
3067 let builder = QueryBuilder::<TestModel>::new(dialect);
3068
3069 let sql = builder.table("users").group_by("status").sql();
3070
3071 assert!(sql.contains("GROUP BY"));
3072 assert!(sql.contains("`status`"));
3073 Ok(())
3074 }
3075
3076 #[test]
3077 fn test_query_builder_max() -> Result<(), crate::DbError> {
3078 let dialect = get_dialect(DbType::MySQL)?;
3079 let builder = QueryBuilder::<TestModel>::new(dialect);
3080
3081 let sql = builder.table("users").build_max("score");
3082
3083 assert!(sql.contains("MAX("));
3084 assert!(sql.contains("`score`"));
3085 Ok(())
3086 }
3087
3088 #[test]
3089 fn test_query_builder_min() -> Result<(), crate::DbError> {
3090 let dialect = get_dialect(DbType::MySQL)?;
3091 let builder = QueryBuilder::<TestModel>::new(dialect);
3092
3093 let sql = builder.table("users").build_min("price");
3094
3095 assert!(sql.contains("MIN("));
3096 assert!(sql.contains("`price`"));
3097 Ok(())
3098 }
3099
3100 #[test]
3101 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3102 let dialect = get_dialect(DbType::MySQL)?;
3103 let builder = QueryBuilder::<TestModel>::new(dialect);
3104
3105 let sql = builder.table("orders").build_sum("amount");
3106
3107 assert!(sql.contains("SUM("));
3108 assert!(sql.contains("`amount`"));
3109 Ok(())
3110 }
3111
3112 #[test]
3113 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3114 let dialect = get_dialect(DbType::MySQL)?;
3115 let builder = QueryBuilder::<TestModel>::new(dialect);
3116
3117 let sql = builder.table("scores").build_avg("value");
3118
3119 assert!(sql.contains("AVG("));
3120 assert!(sql.contains("`value`"));
3121 Ok(())
3122 }
3123
3124 #[test]
3125 fn test_validator_select() -> Result<(), crate::DbError> {
3126 let dialect = get_dialect(DbType::MySQL)?;
3127 let builder = QueryBuilder::<TestModel>::new(dialect);
3128
3129 let result = builder
3130 .table("users")
3131 .select(vec!["id", "name"])?
3132 .validate();
3133 assert!(result.is_ok());
3134 Ok(())
3135 }
3136
3137 #[test]
3138 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3139 let dialect = get_dialect(DbType::MySQL)?;
3140 let builder = QueryBuilder::<TestModel>::new(dialect);
3141
3142 let result = builder
3143 .table("users")
3144 .join_inner("posts", "users.id", "posts.user_id")
3145 .validate();
3146 assert!(result.is_ok());
3147 Ok(())
3148 }
3149
3150 #[test]
3151 fn test_validator_insert() -> Result<(), crate::DbError> {
3152 let dialect = get_dialect(DbType::MySQL)?;
3153 let builder = QueryBuilder::<TestModel>::new(dialect);
3154
3155 let mut data = std::collections::HashMap::new();
3156 data.insert("name".to_string(), Value::String("test".to_string()));
3157
3158 let result = builder.table("users").validate_insert(&data);
3159 assert!(result.is_ok());
3160 Ok(())
3161 }
3162
3163 #[test]
3164 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3165 let dialect = get_dialect(DbType::MySQL)?;
3166 let builder = QueryBuilder::<TestModel>::new(dialect);
3167
3168 let data = std::collections::HashMap::new();
3169 let result = builder.table("users").validate_insert(&data);
3170 assert!(result.is_err());
3171 Ok(())
3172 }
3173
3174 #[test]
3175 fn test_validator_update() -> Result<(), crate::DbError> {
3176 let dialect = get_dialect(DbType::MySQL)?;
3177 let builder = QueryBuilder::<TestModel>::new(dialect);
3178
3179 let mut data = std::collections::HashMap::new();
3180 data.insert("name".to_string(), Value::String("updated".to_string()));
3181
3182 let result = builder.table("users").validate_update(&data);
3183 assert!(result.is_ok());
3184 Ok(())
3185 }
3186
3187 #[test]
3188 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3189 let dialect = get_dialect(DbType::MySQL)?;
3190 let builder = QueryBuilder::<TestModel>::new(dialect);
3191
3192 let data = std::collections::HashMap::new();
3193 let result = builder.table("users").validate_update(&data);
3194 assert!(result.is_err());
3195 Ok(())
3196 }
3197
3198 #[test]
3199 fn test_validator_delete() -> Result<(), crate::DbError> {
3200 let dialect = get_dialect(DbType::MySQL)?;
3201 let builder = QueryBuilder::<TestModel>::new(dialect);
3202
3203 let result = builder
3204 .table("users")
3205 .where_eq("id", Value::I64(1))
3206 .validate_delete();
3207 assert!(result.is_ok());
3208 Ok(())
3209 }
3210
3211 #[test]
3212 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3213 let dialect = get_dialect(DbType::MySQL)?;
3214 let builder = QueryBuilder::<TestModel>::new(dialect);
3215
3216 let result = builder.table("users").validate_delete();
3218 assert!(result.is_ok());
3219 Ok(())
3220 }
3221
3222 #[test]
3225 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3226 let dialect = get_dialect(DbType::MySQL)?;
3227 let builder = QueryBuilder::<TestModel>::new(dialect);
3228 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3229 let sql = builder.sql();
3230 assert!(sql.contains("SELECT `id`, `name` FROM"));
3232 assert!(sql.contains("`users`"));
3233 Ok(())
3234 }
3235
3236 #[test]
3237 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3238 let dialect = get_dialect(DbType::MySQL)?;
3239 let builder = QueryBuilder::<TestModel>::new(dialect);
3240
3241 let result = builder
3243 .table("users")
3244 .select_quoted(vec!["id; DROP TABLE users"]);
3245 assert!(result.is_err());
3246
3247 let dialect = get_dialect(DbType::MySQL)?;
3249 let builder = QueryBuilder::<TestModel>::new(dialect);
3250 let result = builder.table("users").select_quoted(vec!["name'"]);
3251 assert!(result.is_err());
3252
3253 let dialect = get_dialect(DbType::MySQL)?;
3255 let builder = QueryBuilder::<TestModel>::new(dialect);
3256 let result = builder.table("users").select_quoted(vec!["1col"]);
3257 assert!(result.is_err());
3258
3259 let dialect = get_dialect(DbType::MySQL)?;
3261 let builder = QueryBuilder::<TestModel>::new(dialect);
3262 let result = builder.table("users").select_quoted(vec!["col name"]);
3263 assert!(result.is_err());
3264 Ok(())
3265 }
3266
3267 #[test]
3268 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3269 let dialect = get_dialect(DbType::PostgreSQL)?;
3270 let builder = QueryBuilder::<TestModel>::new(dialect);
3271 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3272 let sql = builder.sql();
3273 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3275 assert!(sql.contains("\"users\""));
3276 Ok(())
3277 }
3278
3279 struct SoftDeleteModel;
3283 impl Model for SoftDeleteModel {
3284 type PrimaryKey = i64;
3285
3286 fn table_name() -> &'static str {
3287 "soft_users"
3288 }
3289
3290 fn pk(&self) -> Self::PrimaryKey {
3291 1
3292 }
3293
3294 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3295
3296 fn soft_delete_field() -> Option<&'static str> {
3297 Some("deleted_at")
3298 }
3299 }
3300
3301 #[test]
3305 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3306 let dialect = get_dialect(DbType::MySQL)?;
3307 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3308 let sql = builder.table("soft_users").sql();
3309 assert!(
3311 sql.contains("`deleted_at` IS NULL"),
3312 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3313 sql
3314 );
3315 Ok(())
3316 }
3317
3318 #[test]
3320 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3321 let dialect = get_dialect(DbType::MySQL)?;
3322 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3323 .table("soft_users")
3324 .where_eq("status", Value::String("active".into()))
3325 .sql();
3326 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3328 assert!(
3329 sql.contains("`deleted_at` IS NULL"),
3330 "软删除条件应自动追加: {}",
3331 sql
3332 );
3333 Ok(())
3334 }
3335
3336 #[test]
3340 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3341 let dialect = get_dialect(DbType::MySQL)?;
3342 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3343 .table("soft_users")
3344 .without_soft_delete()
3345 .sql();
3346 assert!(
3348 !sql.contains("`deleted_at` IS NULL"),
3349 "without_soft_delete 应禁用过滤,实际: {}",
3350 sql
3351 );
3352 assert!(
3354 !sql.contains("WHERE"),
3355 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3356 sql
3357 );
3358 Ok(())
3359 }
3360
3361 #[test]
3365 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3366 let dialect = get_dialect(DbType::MySQL)?;
3367 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3368 .table("soft_users")
3369 .where_eq("id", Value::I64(42))
3370 .sql_delete();
3371 assert!(
3373 sql.starts_with("UPDATE"),
3374 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3375 sql
3376 );
3377 assert!(
3378 !sql.contains("DELETE FROM"),
3379 "不应生成 DELETE FROM: {}",
3380 sql
3381 );
3382 assert!(
3383 sql.contains("`deleted_at` = NOW()"),
3384 "应设置 deleted_at = NOW(): {}",
3385 sql
3386 );
3387 assert!(
3389 sql.contains("`deleted_at` IS NULL"),
3390 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3391 sql
3392 );
3393 Ok(())
3394 }
3395
3396 #[test]
3400 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3401 let dialect = get_dialect(DbType::MySQL)?;
3402 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3403 .table("soft_users")
3404 .where_eq("id", Value::I64(99))
3405 .build_force_delete();
3406 assert!(
3408 sql.starts_with("DELETE FROM"),
3409 "build_force_delete 应生成 DELETE FROM,实际: {}",
3410 sql
3411 );
3412 assert!(
3414 !sql.contains("`deleted_at` IS NULL"),
3415 "物理删除不应追加软删除过滤: {}",
3416 sql
3417 );
3418 Ok(())
3419 }
3420
3421 #[test]
3423 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3424 let dialect = get_dialect(DbType::MySQL)?;
3425 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3426 .table("soft_users")
3427 .where_eq("id", Value::I64(1))
3428 .build_select_with_params();
3429 assert!(
3430 sql.contains("`deleted_at` IS NULL"),
3431 "参数化版本也应自动追加软删除: {}",
3432 sql
3433 );
3434 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3435 assert_eq!(params[0], Value::I64(1));
3436 Ok(())
3437 }
3438
3439 #[test]
3441 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3442 let dialect = get_dialect(DbType::MySQL)?;
3443 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3444 .table("soft_users")
3445 .where_eq("id", Value::I64(7))
3446 .build_delete_with_params();
3447 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3448 assert!(
3449 sql.contains("`deleted_at` = NOW()"),
3450 "应设置 NOW(): {}",
3451 sql
3452 );
3453 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3454 Ok(())
3455 }
3456
3457 #[test]
3459 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3460 let dialect = get_dialect(DbType::MySQL)?;
3461 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3462 .table("soft_users")
3463 .where_eq("id", Value::I64(11))
3464 .build_force_delete_with_params();
3465 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3466 assert!(
3467 !sql.contains("`deleted_at` IS NULL"),
3468 "不应追加软删除过滤: {}",
3469 sql
3470 );
3471 assert_eq!(params.len(), 1);
3472 Ok(())
3473 }
3474
3475 #[test]
3479 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3480 let dialect = get_dialect(DbType::MySQL)?;
3481 let sql = QueryBuilder::<TestModel>::new(dialect)
3482 .table("users")
3483 .where_eq("id", Value::I64(1))
3484 .sql();
3485 assert!(
3486 !sql.contains("deleted_at"),
3487 "非软删除模型不应追加 deleted_at: {}",
3488 sql
3489 );
3490 let dialect = get_dialect(DbType::MySQL)?;
3492 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3493 .table("users")
3494 .where_eq("id", Value::I64(1))
3495 .sql_delete();
3496 assert!(
3497 del_sql.starts_with("DELETE FROM"),
3498 "非软删除模型 build_delete 应生成 DELETE: {}",
3499 del_sql
3500 );
3501 Ok(())
3502 }
3503
3504 #[test]
3506 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3507 let dialect = get_dialect(DbType::MySQL)?;
3508 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3509 .table("soft_users")
3510 .build_count();
3511 assert!(
3512 sql.contains("`deleted_at` IS NULL"),
3513 "build_count 也应追加软删除过滤: {}",
3514 sql
3515 );
3516 Ok(())
3517 }
3518
3519 #[test]
3525 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3526 let dialect = get_dialect(DbType::MySQL)?;
3527 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3528 .table("users")
3529 .where_eq("name", Value::String("alice".into()))
3530 .build_select_with_params();
3531 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3533 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3534 assert_eq!(params.len(), 1);
3535 assert_eq!(params[0], Value::String("alice".into()));
3536 Ok(())
3537 }
3538
3539 #[test]
3541 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3542 let dialect = get_dialect(DbType::MySQL)?;
3543 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3544 .table("users")
3545 .where_like("name", Value::String("%alice%".into()))
3546 .build_select_with_params();
3547 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3548 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3549 assert_eq!(params.len(), 1);
3550 Ok(())
3551 }
3552
3553 #[test]
3557 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3558 let dialect = get_dialect(DbType::MySQL)?;
3559 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3560 .table("users")
3561 .where_ne("status", Value::I64(0))
3562 .build_select_with_params();
3563 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3564 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3565 assert_eq!(params.len(), 1);
3566 assert_eq!(params[0], Value::I64(0));
3567 Ok(())
3568 }
3569
3570 #[test]
3574 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3575 let dialect = get_dialect(DbType::MySQL)?;
3576 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3577 .table("users")
3578 .where_ge("age", Value::I64(18))
3579 .build_select_with_params();
3580 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3581 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3582 assert_eq!(params.len(), 1);
3583 assert_eq!(params[0], Value::I64(18));
3584 Ok(())
3585 }
3586
3587 #[test]
3591 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3592 let dialect = get_dialect(DbType::MySQL)?;
3593 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3594 .table("users")
3595 .where_lt("score", Value::F64(60.0))
3596 .build_select_with_params();
3597 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3598 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3599 assert_eq!(params.len(), 1);
3600 assert_eq!(params[0], Value::F64(60.0));
3601 Ok(())
3602 }
3603
3604 #[test]
3608 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3609 let dialect = get_dialect(DbType::MySQL)?;
3610 let evil_input = "'; DROP TABLE users; --".to_string();
3611 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3612 .table("users")
3613 .where_eq("name", Value::String(evil_input.clone()))
3614 .build_select_with_params();
3615 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3617 assert_eq!(params.len(), 1);
3619 assert_eq!(params[0], Value::String(evil_input));
3620 assert_eq!(sql.matches('?').count(), 1);
3622 Ok(())
3623 }
3624
3625 #[test]
3627 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3628 let dialect = get_dialect(DbType::MySQL)?;
3629 let evil = "' OR '1'='1".to_string();
3630 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3631 .table("users")
3632 .where_eq("name", Value::String(evil.clone()))
3633 .build_select_with_params();
3634 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3635 assert_eq!(params.len(), 1);
3636 assert_eq!(params[0], Value::String(evil));
3637 Ok(())
3638 }
3639
3640 #[test]
3642 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3643 let dialect = get_dialect(DbType::MySQL)?;
3644 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3645 .table("users")
3646 .where_eq("name", Value::String("alice".into()))
3647 .where_gt("age", Value::I64(18))
3648 .where_le("score", Value::F64(99.5))
3649 .build_select_with_params();
3650 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3651 assert_eq!(params.len(), 3);
3652 assert_eq!(params[0], Value::String("alice".into()));
3654 assert_eq!(params[1], Value::I64(18));
3655 assert_eq!(params[2], Value::F64(99.5));
3656 Ok(())
3657 }
3658
3659 #[test]
3661 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3662 let dialect = get_dialect(DbType::MySQL)?;
3663 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3664 .table("users")
3665 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3666 .build_select_with_params();
3667 assert!(
3668 sql.contains("`id` IN (?, ?, ?)"),
3669 "应使用 3 个占位符: {}",
3670 sql
3671 );
3672 assert_eq!(params.len(), 3);
3673 Ok(())
3674 }
3675
3676 #[test]
3678 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3679 let dialect = get_dialect(DbType::MySQL)?;
3680 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3681 .table("users")
3682 .where_between("age", Value::I64(18), Value::I64(65))
3683 .build_select_with_params();
3684 assert!(
3685 sql.contains("`age` BETWEEN ? AND ?"),
3686 "应使用 2 个占位符: {}",
3687 sql
3688 );
3689 assert_eq!(params.len(), 2);
3690 assert_eq!(params[0], Value::I64(18));
3691 assert_eq!(params[1], Value::I64(65));
3692 Ok(())
3693 }
3694
3695 #[test]
3697 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3698 let dialect = get_dialect(DbType::MySQL)?;
3699 let mut data = std::collections::HashMap::new();
3700 data.insert("name".to_string(), Value::String("bob".into()));
3701 data.insert("age".to_string(), Value::I64(30));
3702 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3703 .table("users")
3704 .where_eq("id", Value::I64(99))
3705 .build_update_with_params(&data);
3706 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3708 assert_eq!(params.len(), 3);
3709 assert_eq!(params[2], Value::I64(99));
3712 Ok(())
3713 }
3714
3715 #[test]
3719 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3720 let dialect = get_dialect(DbType::MySQL)?;
3721 let sql = QueryBuilder::<TestModel>::new(dialect)
3722 .table("users")
3723 .where_eq("name", Value::String("alice".into()))
3724 .sql();
3725 assert!(
3727 sql.contains("`name` = "),
3728 "无参数版本应含 WHERE 条件: {}",
3729 sql
3730 );
3731 assert!(
3733 !sql.contains("`name` = ?"),
3734 "无参数版本不应使用 ? 占位符: {}",
3735 sql
3736 );
3737 Ok(())
3738 }
3739
3740 #[test]
3742 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3743 let dialect = get_dialect(DbType::MySQL)?;
3744 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3745 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3746 let builder =
3747 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3748 assert!(
3749 builder.is_soft_delete_disabled(),
3750 "without_soft_delete 后应反映禁用状态"
3751 );
3752 Ok(())
3753 }
3754
3755 struct TenantModel;
3759 impl Model for TenantModel {
3760 type PrimaryKey = i64;
3761
3762 fn table_name() -> &'static str {
3763 "orders"
3764 }
3765
3766 fn pk(&self) -> Self::PrimaryKey {
3767 1
3768 }
3769
3770 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3771
3772 fn tenant_field() -> Option<&'static str> {
3773 Some("tenant_id")
3774 }
3775 }
3776
3777 struct SoftDeleteAndTenantModel;
3779 impl Model for SoftDeleteAndTenantModel {
3780 type PrimaryKey = i64;
3781
3782 fn table_name() -> &'static str {
3783 "documents"
3784 }
3785
3786 fn pk(&self) -> Self::PrimaryKey {
3787 1
3788 }
3789
3790 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3791
3792 fn soft_delete_field() -> Option<&'static str> {
3793 Some("deleted_at")
3794 }
3795
3796 fn tenant_field() -> Option<&'static str> {
3797 Some("tenant_id")
3798 }
3799 }
3800
3801 #[test]
3805 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3806 let dialect = get_dialect(DbType::MySQL)?;
3807 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3808 .table("orders")
3809 .with_tenant_id(42)
3810 .build_select_with_params();
3811 assert!(
3812 sql.contains("`tenant_id` = ?"),
3813 "多租户模型应自动追加 tenant_id = ?: {}",
3814 sql
3815 );
3816 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3817 assert_eq!(params[0], Value::I64(42));
3818 Ok(())
3819 }
3820
3821 #[test]
3823 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3824 let dialect = get_dialect(DbType::MySQL)?;
3825 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3826 .table("orders")
3827 .with_tenant_id(7)
3828 .where_eq("status", Value::String("active".into()))
3829 .build_select_with_params();
3830 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3831 assert!(
3832 sql.contains("`tenant_id` = ?"),
3833 "租户条件应自动追加: {}",
3834 sql
3835 );
3836 assert_eq!(params.len(), 2, "应有 2 个参数");
3837 assert_eq!(params[0], Value::String("active".into()));
3839 assert_eq!(params[1], Value::I64(7));
3840 Ok(())
3841 }
3842
3843 #[test]
3847 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3848 let dialect = get_dialect(DbType::MySQL)?;
3849 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3850 .table("orders")
3851 .with_tenant_id(42)
3852 .without_tenant()
3853 .build_select_with_params();
3854 assert!(
3855 !sql.contains("`tenant_id` = ?"),
3856 "without_tenant 应禁用过滤: {}",
3857 sql
3858 );
3859 assert_eq!(params.len(), 0, "不应有租户参数");
3860 Ok(())
3861 }
3862
3863 #[test]
3867 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3868 let dialect = get_dialect(DbType::MySQL)?;
3869 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3870 .table("orders")
3871 .with_tenant_id(99)
3872 .where_eq("id", Value::I64(1))
3873 .build_delete_with_params();
3874 assert!(
3875 sql.contains("`tenant_id` = ?"),
3876 "删除应自动追加租户条件: {}",
3877 sql
3878 );
3879 assert_eq!(params.len(), 2);
3881 assert_eq!(params[0], Value::I64(1));
3882 assert_eq!(params[1], Value::I64(99));
3883 Ok(())
3884 }
3885
3886 #[test]
3888 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3889 let dialect = get_dialect(DbType::MySQL)?;
3890 let mut data = std::collections::HashMap::new();
3891 data.insert("status".to_string(), Value::String("shipped".into()));
3892 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3893 .table("orders")
3894 .with_tenant_id(5)
3895 .where_eq("id", Value::I64(10))
3896 .build_update_with_params(&data);
3897 assert!(
3898 sql.contains("`tenant_id` = ?"),
3899 "更新应自动追加租户条件: {}",
3900 sql
3901 );
3902 assert_eq!(params.len(), 3);
3904 assert_eq!(params[2], Value::I64(5));
3906 Ok(())
3907 }
3908
3909 #[test]
3911 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3912 let dialect = get_dialect(DbType::MySQL)?;
3913 let sql = QueryBuilder::<TenantModel>::new(dialect)
3914 .table("orders")
3915 .with_tenant_id(42)
3916 .build_count();
3917 assert!(
3918 sql.contains("`tenant_id` = 42"),
3919 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3920 sql
3921 );
3922 Ok(())
3923 }
3924
3925 #[test]
3929 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3930 let dialect = get_dialect(DbType::MySQL)?;
3931 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3933 .table("users")
3934 .with_tenant_id(42)
3935 .build_select_with_params();
3936 assert!(
3937 !sql.contains("tenant_id"),
3938 "非多租户模型不应追加 tenant_id: {}",
3939 sql
3940 );
3941 assert_eq!(params.len(), 0);
3942 Ok(())
3943 }
3944
3945 #[test]
3949 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
3950 let dialect = get_dialect(DbType::MySQL)?;
3951 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3952 .table("orders")
3953 .build_select_with_params();
3954 assert!(
3955 !sql.contains("tenant_id"),
3956 "未设置 tenant_id 时不应追加过滤: {}",
3957 sql
3958 );
3959 assert_eq!(params.len(), 0);
3960 Ok(())
3961 }
3962
3963 #[test]
3967 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
3968 let dialect = get_dialect(DbType::MySQL)?;
3969 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3970 .table("documents")
3971 .with_tenant_id(100)
3972 .where_eq("title", Value::String("report".into()))
3973 .build_select_with_params();
3974 assert!(
3976 sql.contains("`deleted_at` IS NULL"),
3977 "应追加软删除条件: {}",
3978 sql
3979 );
3980 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
3982 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
3984 assert_eq!(params.len(), 2);
3986 assert_eq!(params[0], Value::String("report".into()));
3987 assert_eq!(params[1], Value::I64(100));
3988 Ok(())
3989 }
3990
3991 #[test]
3993 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
3994 let dialect = get_dialect(DbType::MySQL)?;
3995 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3996 .table("documents")
3997 .with_tenant_id(100)
3998 .without_tenant()
3999 .without_soft_delete()
4000 .build_select_with_params();
4001 assert!(
4002 !sql.contains("`deleted_at` IS NULL"),
4003 "应禁用软删除: {}",
4004 sql
4005 );
4006 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
4007 assert_eq!(params.len(), 0);
4008 Ok(())
4009 }
4010
4011 #[test]
4013 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
4014 let dialect = get_dialect(DbType::MySQL)?;
4015 let builder = QueryBuilder::<TenantModel>::new(dialect);
4016 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
4017 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4018 .with_tenant_id(1)
4019 .without_tenant();
4020 assert!(
4021 builder.is_tenant_disabled(),
4022 "without_tenant 后应反映禁用状态"
4023 );
4024 Ok(())
4025 }
4026
4027 #[test]
4031 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
4032 let dialect = get_dialect(DbType::MySQL)?;
4033 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4034 .table("orders")
4035 .with_tenant_id(42)
4036 .where_eq("id", Value::I64(999))
4037 .build_force_delete_with_params();
4038 assert!(
4041 sql.contains("`tenant_id` = ?"),
4042 "物理删除应保留租户条件: {}",
4043 sql
4044 );
4045 assert_eq!(params.len(), 2);
4046 assert_eq!(params[0], Value::I64(999));
4047 assert_eq!(params[1], Value::I64(42));
4048 Ok(())
4049 }
4050
4051 #[cfg(feature = "multi-tenant-enhanced")]
4055 #[tokio::test]
4056 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4057 let ctx = crate::tenant_context::TenantContext::new(
4058 99,
4059 crate::tenant_context::IsolationStrategy::RowLevel,
4060 );
4061 ctx.scope(async {
4062 let (sql, params) =
4064 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4065 .table("orders")
4066 .with_tenant_id(42)
4067 .build_select_with_params();
4068 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4069 assert_eq!(params.len(), 1);
4070 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4071 })
4072 .await;
4073 Ok(())
4074 }
4075
4076 #[cfg(feature = "multi-tenant-enhanced")]
4078 #[tokio::test]
4079 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4080 let ctx = crate::tenant_context::TenantContext::new(
4081 77,
4082 crate::tenant_context::IsolationStrategy::RowLevel,
4083 );
4084 ctx.scope(async {
4085 let (sql, params) =
4086 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4087 .table("orders")
4088 .build_select_with_params();
4089 assert!(
4090 sql.contains("`tenant_id` = ?"),
4091 "应从上下文自动追加租户条件: {}",
4092 sql
4093 );
4094 assert_eq!(params.len(), 1);
4095 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4096 })
4097 .await;
4098 Ok(())
4099 }
4100
4101 #[cfg(feature = "multi-tenant-enhanced")]
4103 #[tokio::test]
4104 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4105 let ctx = crate::tenant_context::TenantContext::new(
4106 42,
4107 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4108 );
4109 ctx.scope(async {
4110 let (sql, _params) =
4111 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4112 .table("orders")
4113 .build_select_with_params();
4114 assert!(
4115 sql.contains("tenant_42_orders"),
4116 "Schema 隔离应重写表名: {}",
4117 sql
4118 );
4119 })
4120 .await;
4121 Ok(())
4122 }
4123
4124 #[cfg(feature = "multi-tenant-enhanced")]
4126 #[test]
4127 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4128 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4129 .table("orders")
4130 .build_select_with_params();
4131 assert!(
4133 !sql.contains("`tenant_id` = ?"),
4134 "未设置上下文不应追加租户条件: {}",
4135 sql
4136 );
4137 assert_eq!(params.len(), 0);
4138 Ok(())
4139 }
4140
4141 struct TcUsersTable;
4144 impl crate::typed::TypedTable for TcUsersTable {
4145 const NAME: &'static str = "users";
4146 }
4147 struct TcColId;
4148 impl crate::typed::TypedColumn for TcColId {
4149 const NAME: &'static str = "id";
4150 type Table = TcUsersTable;
4151 type RustType = i64;
4152 type SqlType = crate::typed_ast::Untyped;
4153 }
4154 struct TcColName;
4155 impl crate::typed::TypedColumn for TcColName {
4156 const NAME: &'static str = "name";
4157 type Table = TcUsersTable;
4158 type RustType = String;
4159 type SqlType = crate::typed_ast::Untyped;
4160 }
4161
4162 #[test]
4163 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4164 let dialect = get_dialect(DbType::MySQL)?;
4165 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4166 .where_eq_typed::<TcColId>(Value::I64(42))
4167 .build_select_with_params();
4168 assert!(sql.contains("`id` = ?"));
4169 assert_eq!(params[0], Value::I64(42));
4170 Ok(())
4171 }
4172
4173 #[test]
4174 fn test_order_by_typed() -> Result<(), crate::DbError> {
4175 let dialect = get_dialect(DbType::MySQL)?;
4176 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4177 .order_by_typed::<TcColName>()
4178 .build_select_with_params();
4179 assert!(sql.contains("ORDER BY"));
4180 assert!(sql.contains("`name`"));
4181 Ok(())
4182 }
4183
4184 #[test]
4185 fn test_select_typed() -> Result<(), crate::DbError> {
4186 let dialect = get_dialect(DbType::MySQL)?;
4187 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4188 .select_typed::<TcColId>()
4189 .select_typed::<TcColName>()
4190 .build_select_with_params();
4191 assert!(sql.contains("SELECT"));
4192 assert!(sql.contains("id"));
4194 assert!(sql.contains("name"));
4195 Ok(())
4196 }
4197
4198 #[test]
4199 fn test_where_null_typed() -> Result<(), crate::DbError> {
4200 let dialect = get_dialect(DbType::MySQL)?;
4201 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4202 .where_null_typed::<TcColName>()
4203 .build_select_with_params();
4204 assert!(sql.contains("`name` IS NULL"));
4205 Ok(())
4206 }
4207
4208 #[test]
4209 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4210 let dialect = get_dialect(DbType::MySQL)?;
4211 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4212 .where_not_null_typed::<TcColName>()
4213 .build_select_with_params();
4214 assert!(sql.contains("`name` IS NOT NULL"));
4215 Ok(())
4216 }
4217
4218 #[test]
4219 fn test_group_by_typed() -> Result<(), crate::DbError> {
4220 let dialect = get_dialect(DbType::MySQL)?;
4221 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4222 .group_by_typed::<TcColName>()
4223 .build_select_with_params();
4224 assert!(sql.contains("GROUP BY"));
4225 assert!(sql.contains("`name`"));
4226 Ok(())
4227 }
4228
4229 #[test]
4230 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4231 let dialect = get_dialect(DbType::MySQL)?;
4232 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4233 .where_gt_typed::<TcColId>(Value::I64(10))
4234 .build_select_with_params();
4235 assert!(sql.contains("`id` > ?"));
4236 assert_eq!(params[0], Value::I64(10));
4237 Ok(())
4238 }
4239
4240 #[test]
4243 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4244 let dialect = get_dialect(DbType::MySQL)?;
4245 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4246 .table("users")
4247 .where_eq("id", Value::I64(1))
4248 .lock_for_update()?
4249 .build_select_with_params();
4250 assert!(sql.contains("SELECT * FROM `users`"));
4251 assert!(sql.contains("WHERE `id` = ?"));
4252 assert!(sql.contains("FOR UPDATE"));
4253 assert_eq!(params.len(), 1);
4254 assert_eq!(params[0], Value::I64(1));
4255 Ok(())
4256 }
4257
4258 #[test]
4259 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4260 let dialect = get_dialect(DbType::MySQL)?;
4261 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4262 .table("users")
4263 .where_eq("id", Value::I64(1))
4264 .lock_shared()?
4265 .build_select_with_params();
4266 assert!(sql.contains("SELECT * FROM `users`"));
4267 assert!(sql.contains("WHERE `id` = ?"));
4268 assert!(sql.contains("LOCK IN SHARE MODE"));
4269 assert_eq!(params.len(), 1);
4270 assert_eq!(params[0], Value::I64(1));
4271 Ok(())
4272 }
4273
4274 #[test]
4275 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4276 let dialect = get_dialect(DbType::PostgreSQL)?;
4277 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4278 .table("users")
4279 .where_eq("id", Value::I64(1))
4280 .lock_for_update()?
4281 .build_select_with_params();
4282 assert!(sql.contains("SELECT * FROM \"users\""));
4283 assert!(sql.contains("WHERE \"id\" = ?"));
4284 assert!(sql.contains("FOR UPDATE"));
4285 assert_eq!(params.len(), 1);
4286 assert_eq!(params[0], Value::I64(1));
4287 Ok(())
4288 }
4289
4290 #[test]
4291 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4292 let dialect = get_dialect(DbType::PostgreSQL)?;
4293 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4294 .table("users")
4295 .where_eq("id", Value::I64(1))
4296 .lock_shared()?
4297 .build_select_with_params();
4298 assert!(sql.contains("SELECT * FROM \"users\""));
4299 assert!(sql.contains("WHERE \"id\" = ?"));
4300 assert!(sql.contains("FOR SHARE"));
4301 assert_eq!(params.len(), 1);
4302 assert_eq!(params[0], Value::I64(1));
4303 Ok(())
4304 }
4305
4306 #[test]
4307 fn test_lock_for_update_sqlite_should_fail() {
4308 let dialect = get_dialect(DbType::Sqlite).unwrap();
4309 let result = QueryBuilder::<TestModel>::new(dialect)
4310 .table("users")
4311 .where_eq("id", Value::I64(1))
4312 .lock_for_update();
4313 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4314 let err = result.err().unwrap();
4315 assert!(
4316 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4317 "错误信息应说明不支持行锁"
4318 );
4319 }
4320
4321 #[test]
4322 fn test_lock_shared_sqlite_should_fail() {
4323 let dialect = get_dialect(DbType::Sqlite).unwrap();
4324 let result = QueryBuilder::<TestModel>::new(dialect)
4325 .table("users")
4326 .where_eq("id", Value::I64(1))
4327 .lock_shared();
4328 assert!(result.is_err(), "SQLite 不应支持共享锁");
4329 let err = result.err().unwrap();
4330 assert!(
4331 format!("{:?}", err).contains("Shared lock is not supported"),
4332 "错误信息应说明不支持共享锁"
4333 );
4334 }
4335
4336 #[test]
4337 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4338 let dialect = get_dialect(DbType::MySQL)?;
4339 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4340 .table("users")
4341 .where_eq("status", Value::String("active".into()))
4342 .limit(10)
4343 .offset(20)
4344 .lock_for_update()?
4345 .build_select_with_params();
4346 assert!(sql.contains("WHERE `status` = ?"));
4347 assert!(sql.contains("LIMIT 10"));
4348 assert!(sql.contains("OFFSET 20"));
4349 assert!(sql.contains("FOR UPDATE"));
4350 assert_eq!(params.len(), 1);
4351 Ok(())
4352 }
4353
4354 #[test]
4357 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4358 let dialect = get_dialect(DbType::MySQL)?;
4359 let mut data = std::collections::HashMap::new();
4360 data.insert("name".to_string(), Value::String("Alice".into()));
4361 data.insert("age".to_string(), Value::I64(30));
4362
4363 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4364 .table("users")
4365 .insert_or_ignore()
4366 .build_insert_with_params(&data);
4367 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4368 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4370 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4371 assert!(sql.contains("VALUES (?, ?)"));
4372 assert_eq!(params.len(), 2);
4373 Ok(())
4374 }
4375
4376 #[test]
4377 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4378 let dialect = get_dialect(DbType::PostgreSQL)?;
4379 let mut data = std::collections::HashMap::new();
4380 data.insert("name".to_string(), Value::String("Bob".into()));
4381
4382 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4383 .table("users")
4384 .insert_or_ignore()
4385 .build_insert_with_params(&data);
4386 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4387 assert!(sql.contains("(\"name\")"));
4388 assert!(sql.contains("VALUES (?)"));
4389 assert_eq!(params.len(), 1);
4390 assert_eq!(params[0], Value::String("Bob".into()));
4391 Ok(())
4392 }
4393
4394 #[test]
4395 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4396 let dialect = get_dialect(DbType::Sqlite)?;
4397 let mut data = std::collections::HashMap::new();
4398 data.insert("name".to_string(), Value::String("Charlie".into()));
4399
4400 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4401 .table("users")
4402 .insert_or_ignore()
4403 .build_insert_with_params(&data);
4404 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4405 assert!(sql.contains("(\"name\")"));
4406 assert!(sql.contains("VALUES (?)"));
4407 assert_eq!(params.len(), 1);
4408 assert_eq!(params[0], Value::String("Charlie".into()));
4409 Ok(())
4410 }
4411
4412 #[test]
4413 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4414 let dialect = get_dialect(DbType::MySQL)?;
4415 let mut data = std::collections::HashMap::new();
4416 data.insert("name".to_string(), Value::String("Dave".into()));
4417
4418 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4419 .table("users")
4420 .build_insert_with_params(&data);
4421 assert!(sql.contains("INSERT INTO `users`"));
4422 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4423 assert_eq!(params.len(), 1);
4424 assert_eq!(params[0], Value::String("Dave".into()));
4425 Ok(())
4426 }
4427
4428 #[test]
4429 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4430 let dialect = get_dialect(DbType::MySQL)?;
4431 let data = std::collections::HashMap::new();
4432
4433 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4434 .table("users")
4435 .insert_or_ignore()
4436 .build_insert_with_params(&data);
4437 assert!(sql.is_empty(), "空数据应返回空 SQL");
4438 assert!(params.is_empty());
4439 Ok(())
4440 }
4441
4442 #[test]
4445 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4446 let mysql = get_dialect(DbType::MySQL)?;
4447 let pg = get_dialect(DbType::PostgreSQL)?;
4448 let sqlite = get_dialect(DbType::Sqlite)?;
4449 let clickhouse = get_dialect(DbType::ClickHouse)?;
4450 let duckdb = get_dialect(DbType::DuckDB)?;
4451
4452 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4453 assert!(
4454 pg.supports_lock_for_update(),
4455 "PostgreSQL 应支持 FOR UPDATE"
4456 );
4457 assert!(
4458 !sqlite.supports_lock_for_update(),
4459 "SQLite 不应支持 FOR UPDATE"
4460 );
4461 assert!(
4462 !clickhouse.supports_lock_for_update(),
4463 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4464 );
4465 assert!(
4466 !duckdb.supports_lock_for_update(),
4467 "DuckDB 不应支持 FOR UPDATE"
4468 );
4469 Ok(())
4470 }
4471
4472 #[test]
4473 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4474 let mysql = get_dialect(DbType::MySQL)?;
4475 let pg = get_dialect(DbType::PostgreSQL)?;
4476 let sqlite = get_dialect(DbType::Sqlite)?;
4477 let clickhouse = get_dialect(DbType::ClickHouse)?;
4478 let duckdb = get_dialect(DbType::DuckDB)?;
4479
4480 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4481 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4482 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4483 assert!(
4484 !clickhouse.supports_lock_shared(),
4485 "ClickHouse 是列式 OLAP,不应支持共享锁"
4486 );
4487 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4488 Ok(())
4489 }
4490
4491 #[test]
4492 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4493 let dialect = get_dialect(DbType::MySQL)?;
4494
4495 let builder = QueryBuilder::<TestModel>::new(dialect);
4497 assert!(builder.get_lock_type().is_none(), "默认无锁");
4498 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4499
4500 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4502 .table("users")
4503 .lock_for_update()?;
4504 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4505
4506 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4508 .table("users")
4509 .insert_or_ignore();
4510 assert!(builder.is_insert_or_ignore());
4511
4512 Ok(())
4513 }
4514
4515 struct TestModelWithColumns;
4516 impl Model for TestModelWithColumns {
4517 type PrimaryKey = i64;
4518 fn table_name() -> &'static str {
4519 "test_with_cols"
4520 }
4521 fn pk(&self) -> Self::PrimaryKey {
4522 0
4523 }
4524 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4525 }
4526 impl crate::model::ModelExt for TestModelWithColumns {
4527 fn columns() -> Vec<&'static str> {
4528 vec!["id", "name", "email", "avatar", "blob_data"]
4529 }
4530 fn fillable() -> Vec<&'static str> {
4531 vec!["name", "email", "avatar", "blob_data"]
4532 }
4533 fn guarded() -> Vec<&'static str> {
4534 vec!["id"]
4535 }
4536 fn hidden() -> Vec<&'static str> {
4537 vec!["blob_data"]
4538 }
4539 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4540 std::collections::HashMap::new()
4541 }
4542 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4543 fn to_json(&self) -> serde_json::Value {
4544 serde_json::Value::Null
4545 }
4546 }
4547
4548 #[test]
4549 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4550 let dialect = get_dialect(DbType::MySQL)?;
4551 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4552 .table("users")
4553 .select_exclude(&["avatar", "blob_data"])?;
4554 let sql = builder.sql();
4555 assert!(sql.contains("id"));
4556 assert!(sql.contains("name"));
4557 assert!(sql.contains("email"));
4558 assert!(!sql.contains("avatar"));
4559 assert!(!sql.contains("blob_data"));
4560 Ok(())
4561 }
4562
4563 #[test]
4564 fn test_select_exclude_nonexistent_field() {
4565 let dialect = get_dialect(DbType::MySQL).unwrap();
4566 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4567 .table("users")
4568 .select_exclude(&["nonexistent"]);
4569 assert!(result.is_err());
4570 }
4571
4572 #[test]
4573 fn test_select_exclude_all_fields() {
4574 let dialect = get_dialect(DbType::MySQL).unwrap();
4575 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4576 .table("users")
4577 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4578 assert!(result.is_err());
4579 let err = result.unwrap_err();
4580 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4581 }
4582
4583 #[tokio::test]
4586 async fn test_execute_with_cache_ttl_hit() {
4587 use crate::l2_cache::L2Cache;
4588 use std::sync::atomic::{AtomicU64, Ordering};
4589 use std::sync::Arc;
4590 use std::time::Duration;
4591
4592 let dialect = get_dialect(DbType::MySQL).unwrap();
4593 let cache = L2Cache::new();
4594 let qb = QueryBuilder::<TestModel>::new(dialect)
4595 .table("users")
4596 .where_eq("status", Value::I64(1))
4597 .cache_ttl(Duration::from_secs(300));
4598
4599 let call_count = Arc::new(AtomicU64::new(0));
4600 let cc1 = Arc::clone(&call_count);
4601 let rows1 = qb
4602 .execute_with_cache(&cache, "users", || async move {
4603 cc1.fetch_add(1, Ordering::SeqCst);
4604 let mut row = std::collections::HashMap::new();
4605 row.insert("id".to_string(), Value::I64(1));
4606 Ok(vec![row])
4607 })
4608 .await
4609 .unwrap();
4610 assert_eq!(rows1.len(), 1);
4611 assert_eq!(call_count.load(Ordering::SeqCst), 1);
4612
4613 let cc2 = Arc::clone(&call_count);
4614 let rows2 = qb
4615 .execute_with_cache(&cache, "users", || async move {
4616 cc2.fetch_add(1, Ordering::SeqCst);
4617 let mut row = std::collections::HashMap::new();
4618 row.insert("id".to_string(), Value::I64(2));
4619 Ok(vec![row])
4620 })
4621 .await
4622 .unwrap();
4623 assert_eq!(rows2.len(), 1);
4624 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
4625 assert_eq!(
4626 call_count.load(Ordering::SeqCst),
4627 1,
4628 "second call should hit cache, not loader"
4629 );
4630 }
4631
4632 #[tokio::test]
4633 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
4634 use crate::l2_cache::L2Cache;
4635 use std::sync::atomic::{AtomicU64, Ordering};
4636 use std::sync::Arc;
4637
4638 let dialect = get_dialect(DbType::MySQL).unwrap();
4639 let cache = L2Cache::new();
4640 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
4641
4642 let call_count = Arc::new(AtomicU64::new(0));
4643 let cc1 = Arc::clone(&call_count);
4644 let rows1 = qb
4645 .execute_with_cache(&cache, "users", || async move {
4646 cc1.fetch_add(1, Ordering::SeqCst);
4647 Ok(vec![])
4648 })
4649 .await
4650 .unwrap();
4651 assert!(rows1.is_empty());
4652
4653 let cc2 = Arc::clone(&call_count);
4654 let rows2 = qb
4655 .execute_with_cache(&cache, "users", || async move {
4656 cc2.fetch_add(1, Ordering::SeqCst);
4657 Ok(vec![])
4658 })
4659 .await
4660 .unwrap();
4661 assert!(rows2.is_empty());
4662 assert_eq!(
4663 call_count.load(Ordering::SeqCst),
4664 2,
4665 "without ttl, loader should be called each time"
4666 );
4667 }
4668}