1use crate::db_type::DbType;
26use crate::dialect::Dialect;
27use crate::dialect::LockType;
28use crate::model::Model;
29use crate::typed::TypedColumn;
30use crate::value::Value;
31use std::fmt;
32use std::fmt::Write as _;
33use std::time::Duration;
34use sz_orm_macros::{api_beta, api_stable};
35
36pub struct QueryBuilder<M: Model> {
38 table: Option<String>,
39 select_columns: Vec<String>,
40 select_mode: crate::partial_model::SelectMode,
41 where_conditions: Vec<WhereCondition>,
42 order_by: Vec<OrderClause>,
43 group_by: Vec<String>,
44 having_conditions: Vec<WhereCondition>,
45 limit_value: Option<usize>,
46 offset_value: Option<usize>,
47 joins: Vec<JoinClause>,
48 dialect: Box<dyn Dialect>,
49 #[cfg(feature = "perf-enum-dispatch")]
55 dialect_kind: Option<crate::dialect::DialectKind>,
56 soft_delete_disabled: bool,
58 tenant_id_value: Option<i64>,
60 tenant_disabled: bool,
62 keyset_cursor: Option<KeysetCursor>,
68 cache_ttl: Option<Duration>,
73 lock_type: Option<LockType>,
81 insert_or_ignore: bool,
86 #[cfg(feature = "tenant-quota-rls-enhanced")]
91 rls_enhancer: Option<std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>>,
92 #[allow(dead_code)]
93 model: std::marker::PhantomData<M>,
94}
95
96#[derive(Debug, Clone)]
104struct KeysetCursor {
105 field: String,
107 value: Value,
109 direction: KeysetDirection,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115enum KeysetDirection {
116 After,
118 Before,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum AggExpr {
128 CountStar,
130 Sum(String),
132 Avg(String),
134 Min(String),
136 Max(String),
138}
139
140impl AggExpr {
141 pub fn validate(&self) -> Result<(), crate::DbError> {
143 match self {
144 AggExpr::CountStar => Ok(()),
145 AggExpr::Sum(col) | AggExpr::Avg(col) | AggExpr::Min(col) | AggExpr::Max(col) => {
146 crate::sql_safety::validate_identifier(col, "aggregate column")
147 }
148 }
149 }
150
151 fn render(&self, dialect: &dyn crate::dialect::Dialect) -> String {
153 match self {
154 AggExpr::CountStar => "COUNT(*)".to_string(),
155 AggExpr::Sum(col) => format!("SUM({})", dialect.quote(col)),
156 AggExpr::Avg(col) => format!("AVG({})", dialect.quote(col)),
157 AggExpr::Min(col) => format!("MIN({})", dialect.quote(col)),
158 AggExpr::Max(col) => format!("MAX({})", dialect.quote(col)),
159 }
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum HavingOp {
166 Eq,
168 Ne,
170 Gt,
172 Ge,
174 Lt,
176 Le,
178}
179
180impl HavingOp {
181 pub fn as_sql(&self) -> &'static str {
183 match self {
184 HavingOp::Eq => "=",
185 HavingOp::Ne => "!=",
186 HavingOp::Gt => ">",
187 HavingOp::Ge => ">=",
188 HavingOp::Lt => "<",
189 HavingOp::Le => "<=",
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
195#[allow(dead_code)]
196enum WhereCondition {
197 And(String),
202 Or(String),
204 Having(AggExpr, HavingOp, Value),
206 Eq(String, Value),
208 Ne(String, Value),
210 Gt(String, Value),
212 Ge(String, Value),
214 Lt(String, Value),
216 Le(String, Value),
218 Like(String, Value),
220 OrEq(String, Value),
222 OrNe(String, Value),
224 OrGt(String, Value),
226 OrGe(String, Value),
228 OrLt(String, Value),
230 OrLe(String, Value),
232 OrLike(String, Value),
234 In(String, Vec<Value>),
235 NotIn(String, Vec<Value>),
236 Between(String, Value, Value),
237 NotBetween(String, Value, Value),
238 Null(String),
239 NotNull(String),
240 Exists(String),
241 NotExists(String),
242 TypedExpr(String, Vec<Value>),
244}
245
246#[derive(Debug, Clone)]
247struct OrderClause {
248 field: String,
249 direction: OrderDirection,
250}
251
252#[derive(Debug, Clone)]
253enum OrderDirection {
254 Asc,
255 Desc,
256}
257
258#[derive(Debug, Clone)]
259#[allow(dead_code)]
260enum JoinClause {
261 Inner(String, String, String),
262 Left(String, String, String),
263 Right(String, String, String),
264 Cross(String, String),
265 Relation(
268 crate::relation_trait::JoinKind,
269 String,
270 String,
271 String,
272 String,
273 ),
274}
275
276fn push_usize_to_string<W: fmt::Write>(n: usize, buf: &mut W) {
278 if n == 0 {
279 let _ = buf.write_char('0');
280 return;
281 }
282 let mut digits = [0u8; 20];
283 let mut len = 0;
284 let mut val = n;
285 while val > 0 {
286 digits[len] = b'0' + (val % 10) as u8;
287 val /= 10;
288 len += 1;
289 }
290 for i in (0..len).rev() {
291 let _ = buf.write_char(digits[i] as char);
292 }
293}
294
295impl<M: Model> QueryBuilder<M> {
296 pub fn new(dialect: Box<dyn Dialect>) -> Self {
298 #[cfg(feature = "perf-enum-dispatch")]
299 let dialect_kind = crate::dialect::DialectKind::from_db_type(dialect.db_type());
300 Self {
301 table: None,
302 select_columns: Vec::new(),
303 select_mode: crate::partial_model::SelectMode::All,
304 where_conditions: Vec::new(),
305 order_by: Vec::new(),
306 group_by: Vec::new(),
307 having_conditions: Vec::new(),
308 limit_value: None,
309 offset_value: None,
310 joins: Vec::new(),
311 dialect,
312 #[cfg(feature = "perf-enum-dispatch")]
313 dialect_kind,
314 soft_delete_disabled: false,
315 tenant_id_value: None,
316 tenant_disabled: false,
317 keyset_cursor: None,
318 cache_ttl: None,
319 lock_type: None,
320 insert_or_ignore: false,
321 #[cfg(feature = "tenant-quota-rls-enhanced")]
322 rls_enhancer: None,
323 model: std::marker::PhantomData,
324 }
325 }
326
327 #[inline]
329 fn quote_into(&self, identifier: &str, buf: &mut String) {
330 #[cfg(feature = "perf-enum-dispatch")]
331 if let Some(kind) = self.dialect_kind {
332 kind.quote_into(identifier, buf);
333 return;
334 }
335 self.quote_into(identifier, buf);
336 }
337
338 pub fn table(mut self, table: impl Into<String>) -> Self {
340 let table_name = table.into(); #[cfg(feature = "multi-tenant-enhanced")]
342 {
343 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
344 if ctx.isolation_strategy
345 == crate::tenant_context::IsolationStrategy::SchemaIsolation
346 {
347 self.table = Some(crate::tenant_context::SchemaIsolationRouter::rewrite_table(
348 &table_name,
349 ctx.tenant_id,
350 ));
351 return self;
352 }
353 }
354 }
355 self.table = Some(table_name);
356 self
357 }
358
359 pub fn without_soft_delete(mut self) -> Self {
378 self.soft_delete_disabled = true;
379 self
380 }
381
382 pub fn is_soft_delete_disabled(&self) -> bool {
384 self.soft_delete_disabled
385 }
386
387 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
407 self.cache_ttl = Some(ttl);
408 self
409 }
410
411 pub fn get_cache_ttl(&self) -> Option<Duration> {
413 self.cache_ttl
414 }
415
416 pub async fn execute_with_cache<F, Fut>(
444 &self,
445 cache: &crate::l2_cache::L2Cache,
446 table: &str,
447 loader: F,
448 ) -> Result<crate::pool::QueryRows, crate::DbError>
449 where
450 F: FnOnce() -> Fut,
451 Fut: std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>,
452 {
453 let (sql, params) = self.build_select_with_params();
454 match self.get_cache_ttl() {
455 Some(ttl) => {
456 cache
457 .get_or_load_query(table, &sql, ¶ms, ttl, loader)
458 .await
459 }
460 None => loader().await,
461 }
462 }
463
464 pub fn lock_for_update(mut self) -> Result<Self, crate::error::DbError> {
489 if !self.dialect.supports_lock_for_update() {
490 return Err(crate::error::DbError::QueryError(
491 "FOR UPDATE lock is not supported by this dialect".to_string(),
492 ));
493 }
494 self.lock_type = Some(LockType::ForUpdate);
495 Ok(self)
496 }
497
498 pub fn lock_shared(mut self) -> Result<Self, crate::error::DbError> {
523 if !self.dialect.supports_lock_shared() {
524 return Err(crate::error::DbError::QueryError(
525 "Shared lock is not supported by this dialect".to_string(),
526 ));
527 }
528 self.lock_type = Some(LockType::Shared);
529 Ok(self)
530 }
531
532 pub fn insert_or_ignore(mut self) -> Self {
551 self.insert_or_ignore = true;
552 self
553 }
554
555 pub fn get_lock_type(&self) -> Option<LockType> {
557 self.lock_type
558 }
559
560 pub fn is_insert_or_ignore(&self) -> bool {
562 self.insert_or_ignore
563 }
564
565 pub fn clone_for_count(&self) -> Self {
572 Self {
573 table: self.table.clone(),
574 select_columns: Vec::new(),
575 select_mode: crate::partial_model::SelectMode::All,
576 where_conditions: self.where_conditions.clone(),
577 order_by: Vec::new(),
578 group_by: self.group_by.clone(),
579 having_conditions: self.having_conditions.clone(),
580 limit_value: None,
581 offset_value: None,
582 joins: self.joins.clone(),
583 dialect: self.dialect.clone_box(),
584 #[cfg(feature = "perf-enum-dispatch")]
585 dialect_kind: self.dialect_kind,
586 soft_delete_disabled: self.soft_delete_disabled,
587 tenant_id_value: self.tenant_id_value,
588 tenant_disabled: self.tenant_disabled,
589 keyset_cursor: None,
590 cache_ttl: None, lock_type: None, insert_or_ignore: false, #[cfg(feature = "tenant-quota-rls-enhanced")]
594 rls_enhancer: self.rls_enhancer.clone(),
595 model: std::marker::PhantomData,
596 }
597 }
598
599 fn soft_delete_field(&self) -> Option<&'static str> {
603 if self.soft_delete_disabled {
604 return None;
605 }
606 M::soft_delete_field()
607 }
608
609 fn build_soft_delete_condition(&self) -> Option<String> {
613 self.soft_delete_field()
614 .map(|field| format!("{} IS NULL", self.dialect.quote(field)))
615 }
616
617 pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
644 self.tenant_id_value = Some(tenant_id);
645 self
646 }
647
648 pub fn without_tenant(mut self) -> Self {
652 self.tenant_disabled = true;
653 self
654 }
655
656 #[cfg(feature = "tenant-quota-rls-enhanced")]
661 pub fn with_rls_policy_enhancer(
662 mut self,
663 enhancer: std::sync::Arc<crate::tenant_quota_rls::RlsPolicyEnhancer>,
664 ) -> Self {
665 self.rls_enhancer = Some(enhancer);
666 self
667 }
668
669 pub fn is_tenant_disabled(&self) -> bool {
671 self.tenant_disabled
672 }
673
674 fn tenant_field(&self) -> Option<&'static str> {
678 if self.tenant_disabled {
679 return None;
680 }
681 M::tenant_field()
682 }
683
684 fn tenant_id_value(&self) -> Option<i64> {
690 if self.tenant_disabled {
691 return None;
692 }
693 if let Some(tid) = self.tenant_id_value {
694 return Some(tid);
695 }
696 #[cfg(feature = "multi-tenant-enhanced")]
697 {
698 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
699 return Some(ctx.tenant_id);
700 }
701 }
702 None
703 }
704
705 fn build_tenant_condition(&self) -> Option<(String, Value)> {
710 let field = self.tenant_field()?;
711 let tid = self.tenant_id_value()?;
712 Some((
713 format!("{} = ?", self.dialect.quote(field)),
714 Value::I64(tid),
715 ))
716 }
717
718 #[cfg(feature = "multi-tenant-enhanced")]
724 #[allow(dead_code)]
725 fn require_tenant_condition(&self) -> Result<Option<(String, Value)>, crate::DbError> {
726 if self.tenant_field().is_none() {
727 return Ok(None);
728 }
729 if self.tenant_id_value.is_some() {
730 return Ok(self.build_tenant_condition());
731 }
732 if crate::tenant_context::TenantContext::current().is_some() {
733 return Ok(self.build_tenant_condition());
734 }
735 Err(crate::DbError::TenantError(
736 "TenantContextRequired: multi-tenant-enhanced feature enabled but no tenant context set"
737 .to_string(),
738 ))
739 }
740
741 pub fn select(mut self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
764 let mut quoted = Vec::with_capacity(columns.len());
765 for col in columns {
766 crate::sql_safety::validate_identifier(col, "select column")?;
767 quoted.push(self.dialect.quote(col));
768 }
769 self.select_columns = quoted;
770 Ok(self)
771 }
772
773 pub fn select_expr(mut self, columns: Vec<&str>) -> Self {
779 self.select_columns = columns.into_iter().map(|s| s.to_string()).collect();
780 self
781 }
782
783 pub fn select_quoted(self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
787 self.select(columns)
788 }
789 #[api_stable]
790
791 pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
805 self.where_conditions
806 .push(WhereCondition::Eq(field.into(), value));
807 self
808 }
809
810 #[cfg(feature = "type-safe-columns")]
815 pub fn where_eq_col<T: crate::column::Schema>(
816 mut self,
817 col: crate::column::Column<T>,
818 value: Value,
819 ) -> Self {
820 self.where_conditions
821 .push(WhereCondition::Eq(col.name().to_string(), value));
822 self
823 }
824
825 #[cfg(feature = "type-safe-columns")]
830 pub fn where_expr<E: crate::typed_ast::TypedExpression<SqlType = crate::typed_ast::Bool>>(
831 mut self,
832 expr: E,
833 ) -> Self {
834 let (sql, params) = expr.to_sql(&*self.dialect);
835 let values: Vec<Value> = params.into_iter().map(Value::String).collect();
836 self.where_conditions
837 .push(WhereCondition::TypedExpr(sql, values));
838 self
839 }
840 #[api_stable]
841
842 pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
844 self.where_conditions
845 .push(WhereCondition::Ne(field.into(), value));
846 self
847 }
848 #[api_stable]
849
850 pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
852 self.where_conditions
853 .push(WhereCondition::Gt(field.into(), value));
854 self
855 }
856 #[api_stable]
857
858 pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
860 self.where_conditions
861 .push(WhereCondition::Ge(field.into(), value));
862 self
863 }
864 #[api_stable]
865
866 pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
868 self.where_conditions
869 .push(WhereCondition::Lt(field.into(), value));
870 self
871 }
872 #[api_stable]
873
874 pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
876 self.where_conditions
877 .push(WhereCondition::Le(field.into(), value));
878 self
879 }
880 #[api_stable]
881
882 pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
894 self.where_conditions
895 .push(WhereCondition::Like(field.into(), pattern));
896 self
897 }
898 #[api_stable]
899
900 pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
904 self.where_conditions
905 .push(WhereCondition::OrEq(field.into(), value));
906 self
907 }
908 #[api_stable]
909
910 pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
912 self.where_conditions
913 .push(WhereCondition::OrNe(field.into(), value));
914 self
915 }
916 #[api_stable]
917
918 pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
920 self.where_conditions
921 .push(WhereCondition::OrGt(field.into(), value));
922 self
923 }
924 #[api_stable]
925
926 pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
928 self.where_conditions
929 .push(WhereCondition::OrGe(field.into(), value));
930 self
931 }
932 #[api_stable]
933
934 pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
936 self.where_conditions
937 .push(WhereCondition::OrLt(field.into(), value));
938 self
939 }
940 #[api_stable]
941
942 pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
944 self.where_conditions
945 .push(WhereCondition::OrLe(field.into(), value));
946 self
947 }
948 #[api_stable]
949
950 pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
952 self.where_conditions
953 .push(WhereCondition::OrLike(field.into(), pattern));
954 self
955 }
956 #[api_stable]
957
958 pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
960 self.where_conditions
961 .push(WhereCondition::In(field.into(), values));
962 self
963 }
964
965 pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
967 self.where_conditions
968 .push(WhereCondition::NotIn(field.into(), values));
969 self
970 }
971 #[api_stable]
972
973 pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
975 self.where_conditions
976 .push(WhereCondition::Between(field.into(), start, end));
977 self
978 }
979
980 pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
982 self.where_conditions
983 .push(WhereCondition::NotBetween(field.into(), start, end));
984 self
985 }
986 #[api_stable]
987
988 pub fn where_null(mut self, field: impl Into<String>) -> Self {
990 self.where_conditions
991 .push(WhereCondition::Null(field.into()));
992 self
993 }
994
995 pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
997 self.where_conditions
998 .push(WhereCondition::NotNull(field.into()));
999 self
1000 }
1001
1002 pub fn order_by(mut self, field: impl Into<String>) -> Self {
1004 self.order_by.push(OrderClause {
1005 field: field.into(),
1006 direction: OrderDirection::Asc,
1007 });
1008 self
1009 }
1010
1011 pub fn order_desc(mut self, field: impl Into<String>) -> Self {
1013 self.order_by.push(OrderClause {
1014 field: field.into(),
1015 direction: OrderDirection::Desc,
1016 });
1017 self
1018 }
1019
1020 pub fn group_by(mut self, field: impl Into<String>) -> Self {
1022 self.group_by.push(field.into());
1023 self
1024 }
1025
1026 #[api_beta]
1032
1033 pub fn where_eq_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1046 self.where_conditions
1047 .push(WhereCondition::Eq(C::NAME.to_string(), value));
1048 self
1049 }
1050
1051 pub fn where_ne_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1053 self.where_conditions
1054 .push(WhereCondition::Ne(C::NAME.to_string(), value));
1055 self
1056 }
1057
1058 pub fn where_gt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1060 self.where_conditions
1061 .push(WhereCondition::Gt(C::NAME.to_string(), value));
1062 self
1063 }
1064
1065 pub fn where_ge_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1067 self.where_conditions
1068 .push(WhereCondition::Ge(C::NAME.to_string(), value));
1069 self
1070 }
1071
1072 pub fn where_lt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1074 self.where_conditions
1075 .push(WhereCondition::Lt(C::NAME.to_string(), value));
1076 self
1077 }
1078
1079 pub fn where_le_typed<C: TypedColumn>(mut self, value: Value) -> Self {
1081 self.where_conditions
1082 .push(WhereCondition::Le(C::NAME.to_string(), value));
1083 self
1084 }
1085
1086 pub fn where_null_typed<C: TypedColumn>(mut self) -> Self {
1088 self.where_conditions
1089 .push(WhereCondition::Null(C::NAME.to_string()));
1090 self
1091 }
1092
1093 pub fn where_not_null_typed<C: TypedColumn>(mut self) -> Self {
1095 self.where_conditions
1096 .push(WhereCondition::NotNull(C::NAME.to_string()));
1097 self
1098 }
1099
1100 pub fn order_by_typed<C: TypedColumn>(mut self) -> Self {
1102 self.order_by.push(OrderClause {
1103 field: C::NAME.to_string(),
1104 direction: OrderDirection::Asc,
1105 });
1106 self
1107 }
1108
1109 pub fn order_desc_typed<C: TypedColumn>(mut self) -> Self {
1111 self.order_by.push(OrderClause {
1112 field: C::NAME.to_string(),
1113 direction: OrderDirection::Desc,
1114 });
1115 self
1116 }
1117
1118 pub fn group_by_typed<C: TypedColumn>(mut self) -> Self {
1120 self.group_by.push(C::NAME.to_string());
1121 self
1122 }
1123
1124 pub fn select_typed<C: TypedColumn>(mut self) -> Self {
1128 self.select_columns.push(C::NAME.to_string());
1129 self
1130 }
1131
1132 pub fn select_typed_cols<C: TypedColumn, const N: usize>(mut self) -> Self {
1134 self.select_columns.push(C::NAME.to_string());
1137 self
1138 }
1139
1140 pub fn having(
1164 mut self,
1165 agg: AggExpr,
1166 op: HavingOp,
1167 value: Value,
1168 ) -> Result<Self, crate::DbError> {
1169 agg.validate()?;
1170 self.having_conditions
1171 .push(WhereCondition::Having(agg, op, value));
1172 Ok(self)
1173 }
1174
1175 pub fn limit(mut self, limit: usize) -> Self {
1177 self.limit_value = Some(limit);
1178 self
1179 }
1180
1181 pub fn offset(mut self, offset: usize) -> Self {
1183 self.offset_value = Some(offset);
1184 self
1185 }
1186
1187 pub fn page(mut self, page: usize, page_size: usize) -> Self {
1189 self.limit_value = Some(page_size);
1190 self.offset_value = Some((page.saturating_sub(1)) * page_size);
1191 self
1192 }
1193
1194 pub fn keyset_after(
1240 mut self,
1241 field: impl Into<String>,
1242 cursor_value: Value,
1243 page_size: usize,
1244 ) -> Self {
1245 let field_str = field.into();
1246 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1248 existing.direction = OrderDirection::Asc;
1249 } else {
1250 self.order_by.push(OrderClause {
1251 field: field_str.clone(),
1252 direction: OrderDirection::Asc,
1253 });
1254 }
1255 self.limit_value = Some(page_size);
1256 self.offset_value = None;
1258 self.keyset_cursor = Some(KeysetCursor {
1259 field: field_str,
1260 value: cursor_value,
1261 direction: KeysetDirection::After,
1262 });
1263 self
1264 }
1265
1266 pub fn keyset_before(
1312 mut self,
1313 field: impl Into<String>,
1314 cursor_value: Value,
1315 page_size: usize,
1316 ) -> Self {
1317 let field_str = field.into();
1318 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1320 existing.direction = OrderDirection::Desc;
1321 } else {
1322 self.order_by.push(OrderClause {
1323 field: field_str.clone(),
1324 direction: OrderDirection::Desc,
1325 });
1326 }
1327 self.limit_value = Some(page_size);
1328 self.offset_value = None;
1329 self.keyset_cursor = Some(KeysetCursor {
1330 field: field_str,
1331 value: cursor_value,
1332 direction: KeysetDirection::Before,
1333 });
1334 self
1335 }
1336
1337 pub fn join_inner(
1339 mut self,
1340 table: impl Into<String>,
1341 on_left: impl Into<String>,
1342 on_right: impl Into<String>,
1343 ) -> Self {
1344 self.joins.push(JoinClause::Inner(
1345 table.into(),
1346 on_left.into(),
1347 on_right.into(),
1348 ));
1349 self
1350 }
1351
1352 pub fn join_left(
1354 mut self,
1355 table: impl Into<String>,
1356 on_left: impl Into<String>,
1357 on_right: impl Into<String>,
1358 ) -> Self {
1359 self.joins.push(JoinClause::Left(
1360 table.into(),
1361 on_left.into(),
1362 on_right.into(),
1363 ));
1364 self
1365 }
1366
1367 pub fn join_right(
1369 mut self,
1370 table: impl Into<String>,
1371 on_left: impl Into<String>,
1372 on_right: impl Into<String>,
1373 ) -> Self {
1374 self.joins.push(JoinClause::Right(
1375 table.into(),
1376 on_left.into(),
1377 on_right.into(),
1378 ));
1379 self
1380 }
1381
1382 pub fn join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1402 let def = relation.def();
1403 let join_kind = def.kind.default_join_type();
1404 self.joins.push(JoinClause::Relation(
1405 join_kind,
1406 def.from_entity.to_string(),
1407 def.from_key.to_string(),
1408 def.to_entity.to_string(),
1409 def.to_key.to_string(),
1410 ));
1411 self
1412 }
1413
1414 pub fn left_join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1418 let def = relation.def();
1419 self.joins.push(JoinClause::Relation(
1420 crate::relation_trait::JoinKind::Left,
1421 def.from_entity.to_string(),
1422 def.from_key.to_string(),
1423 def.to_entity.to_string(),
1424 def.to_key.to_string(),
1425 ));
1426 self
1427 }
1428
1429 pub fn select_only(mut self) -> Self {
1445 self.select_mode = crate::partial_model::SelectMode::Partial;
1446 self.select_columns.clear();
1447 self
1448 }
1449
1450 pub fn column(mut self, column: impl Into<String>) -> Self {
1454 self.select_columns.push(column.into());
1455 self
1456 }
1457
1458 pub fn columns(mut self, cols: Vec<impl Into<String>>) -> Self {
1460 self.select_columns.extend(cols.into_iter().map(Into::into));
1461 self
1462 }
1463
1464 pub fn column_as(mut self, expr: crate::partial_model::Expr, alias: impl Into<String>) -> Self {
1480 self.select_columns.push(expr.render_as(&alias.into()));
1481 self
1482 }
1483 #[api_stable]
1484
1485 #[tracing::instrument(skip(self), fields(op = "select"))]
1518 pub fn sql(&self) -> String {
1519 let table = self
1520 .table
1521 .clone()
1522 .unwrap_or_else(|| M::table_name().to_string());
1523
1524 let columns = if self.select_columns.is_empty() {
1525 "*".to_string()
1526 } else {
1527 self.select_columns.join(", ")
1528 };
1529
1530 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1531 "SELECT {} FROM {}",
1532 columns,
1533 self.dialect.quote(&table)
1534 ));
1535
1536 for join in &self.joins {
1537 match join {
1538 JoinClause::Inner(t, l, r) => {
1539 let _ = write!(
1540 sql,
1541 " INNER JOIN {} ON {} = {}",
1542 self.dialect.quote(t),
1543 self.dialect.quote(l),
1544 self.dialect.quote(r)
1545 );
1546 }
1547 JoinClause::Left(t, l, r) => {
1548 let _ = write!(
1549 sql,
1550 " LEFT JOIN {} ON {} = {}",
1551 self.dialect.quote(t),
1552 self.dialect.quote(l),
1553 self.dialect.quote(r)
1554 );
1555 }
1556 JoinClause::Right(t, l, r) => {
1557 let _ = write!(
1558 sql,
1559 " RIGHT JOIN {} ON {} = {}",
1560 self.dialect.quote(t),
1561 self.dialect.quote(l),
1562 self.dialect.quote(r)
1563 );
1564 }
1565 JoinClause::Cross(t, on) => {
1566 let _ = write!(
1567 sql,
1568 " CROSS JOIN {} ON {}",
1569 self.dialect.quote(t),
1570 self.dialect.quote(on)
1571 );
1572 }
1573 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1574 let _ = write!(
1575 sql,
1576 " {} {} ON {}.{} = {}.{}",
1577 kind.as_sql(),
1578 self.dialect.quote(tt),
1579 self.dialect.quote(ft),
1580 self.dialect.quote(fk),
1581 self.dialect.quote(tt),
1582 self.dialect.quote(tk)
1583 );
1584 }
1585 }
1586 }
1587
1588 let where_clause = self.build_where_clause();
1590 if !where_clause.is_empty() {
1591 sql.push_str(&where_clause);
1592 }
1593
1594 if !self.group_by.is_empty() {
1595 let cols: Vec<String> = self
1596 .group_by
1597 .iter()
1598 .map(|c| self.dialect.quote(c))
1599 .collect();
1600 sql.push_str(" GROUP BY ");
1601 sql.push_str(&cols.join(", "));
1602 }
1603
1604 if !self.having_conditions.is_empty() {
1605 sql.push_str(" HAVING ");
1606 for (i, cond) in self.having_conditions.iter().enumerate() {
1607 if i > 0 {
1608 sql.push_str(" AND ");
1609 }
1610 match cond {
1611 WhereCondition::And(c) => sql.push_str(c),
1612 WhereCondition::Having(agg, op, value) => {
1614 sql.push_str(&format!(
1615 "{} {} {}",
1616 agg.render(&*self.dialect),
1617 op.as_sql(),
1618 value.to_param_with_dialect(&*self.dialect)
1619 ));
1620 }
1621 _ => {}
1622 }
1623 }
1624 }
1625
1626 if !self.order_by.is_empty() {
1627 let order_cols: Vec<String> = self
1628 .order_by
1629 .iter()
1630 .map(|o| {
1631 let dir = match o.direction {
1632 OrderDirection::Asc => " ASC",
1633 OrderDirection::Desc => " DESC",
1634 };
1635 format!("{}{}", self.dialect.quote(&o.field), dir)
1636 })
1637 .collect();
1638 sql.push_str(" ORDER BY ");
1639 sql.push_str(&order_cols.join(", "));
1640 }
1641
1642 if let Some(limit) = self.limit_value {
1643 sql.push_str(" LIMIT ");
1644 push_usize_to_string(limit, &mut sql);
1645 }
1646
1647 if let Some(offset) = self.offset_value {
1648 sql.push_str(" OFFSET ");
1649 push_usize_to_string(offset, &mut sql);
1650 }
1651
1652 sql.into_string()
1653 }
1654
1655 fn build_where_clause(&self) -> String {
1661 self.build_where_clause_with_options(true)
1662 }
1663
1664 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1672 let soft_delete_cond = if include_soft_delete {
1674 self.build_soft_delete_condition()
1675 } else {
1676 None
1677 };
1678
1679 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1681 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1683 });
1684
1685 if self.where_conditions.is_empty()
1687 && soft_delete_cond.is_none()
1688 && tenant_cond.is_none()
1689 && self.keyset_cursor.is_none()
1690 {
1691 return String::new();
1692 }
1693
1694 let mut conditions: Vec<String> = self
1696 .where_conditions
1697 .iter()
1698 .map(|cond| match cond {
1699 WhereCondition::And(c) => c.clone(),
1700 WhereCondition::Or(c) => format!("OR {}", c),
1701 WhereCondition::Eq(f, v) => format!(
1703 "{} = {}",
1704 self.dialect.quote(f),
1705 v.to_param_with_dialect(&*self.dialect)
1706 ),
1707 WhereCondition::Ne(f, v) => format!(
1708 "{} != {}",
1709 self.dialect.quote(f),
1710 v.to_param_with_dialect(&*self.dialect)
1711 ),
1712 WhereCondition::Gt(f, v) => format!(
1713 "{} > {}",
1714 self.dialect.quote(f),
1715 v.to_param_with_dialect(&*self.dialect)
1716 ),
1717 WhereCondition::Ge(f, v) => format!(
1718 "{} >= {}",
1719 self.dialect.quote(f),
1720 v.to_param_with_dialect(&*self.dialect)
1721 ),
1722 WhereCondition::Lt(f, v) => format!(
1723 "{} < {}",
1724 self.dialect.quote(f),
1725 v.to_param_with_dialect(&*self.dialect)
1726 ),
1727 WhereCondition::Le(f, v) => format!(
1728 "{} <= {}",
1729 self.dialect.quote(f),
1730 v.to_param_with_dialect(&*self.dialect)
1731 ),
1732 WhereCondition::Like(f, v) => format!(
1733 "{} LIKE {}",
1734 self.dialect.quote(f),
1735 v.to_param_with_dialect(&*self.dialect)
1736 ),
1737 WhereCondition::OrEq(f, v) => format!(
1738 "OR {} = {}",
1739 self.dialect.quote(f),
1740 v.to_param_with_dialect(&*self.dialect)
1741 ),
1742 WhereCondition::OrNe(f, v) => format!(
1743 "OR {} != {}",
1744 self.dialect.quote(f),
1745 v.to_param_with_dialect(&*self.dialect)
1746 ),
1747 WhereCondition::OrGt(f, v) => format!(
1748 "OR {} > {}",
1749 self.dialect.quote(f),
1750 v.to_param_with_dialect(&*self.dialect)
1751 ),
1752 WhereCondition::OrGe(f, v) => format!(
1753 "OR {} >= {}",
1754 self.dialect.quote(f),
1755 v.to_param_with_dialect(&*self.dialect)
1756 ),
1757 WhereCondition::OrLt(f, v) => format!(
1758 "OR {} < {}",
1759 self.dialect.quote(f),
1760 v.to_param_with_dialect(&*self.dialect)
1761 ),
1762 WhereCondition::OrLe(f, v) => format!(
1763 "OR {} <= {}",
1764 self.dialect.quote(f),
1765 v.to_param_with_dialect(&*self.dialect)
1766 ),
1767 WhereCondition::OrLike(f, v) => format!(
1768 "OR {} LIKE {}",
1769 self.dialect.quote(f),
1770 v.to_param_with_dialect(&*self.dialect)
1771 ),
1772 WhereCondition::In(f, vals) => {
1773 let vals_str: Vec<String> = vals
1775 .iter()
1776 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1777 .collect();
1778 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1779 }
1780 WhereCondition::NotIn(f, vals) => {
1781 let vals_str: Vec<String> = vals
1782 .iter()
1783 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1784 .collect();
1785 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1786 }
1787 WhereCondition::Between(f, start, end) => {
1788 format!(
1789 "{} BETWEEN {} AND {}",
1790 self.dialect.quote(f),
1791 start.to_param_with_dialect(&*self.dialect),
1792 end.to_param_with_dialect(&*self.dialect)
1793 )
1794 }
1795 WhereCondition::NotBetween(f, start, end) => {
1796 format!(
1797 "{} NOT BETWEEN {} AND {}",
1798 self.dialect.quote(f),
1799 start.to_param_with_dialect(&*self.dialect),
1800 end.to_param_with_dialect(&*self.dialect)
1801 )
1802 }
1803 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1804 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1805 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1806 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1807 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1808 WhereCondition::Having(..) => String::new(),
1810 })
1811 .collect();
1812
1813 if let Some(sd_cond) = soft_delete_cond {
1815 conditions.push(sd_cond);
1816 }
1817
1818 if let Some(t_cond) = tenant_cond {
1820 conditions.push(t_cond);
1821 }
1822
1823 if let Some(ref cursor) = self.keyset_cursor {
1825 let op = match cursor.direction {
1826 KeysetDirection::After => ">",
1827 KeysetDirection::Before => "<",
1828 };
1829 conditions.push(format!(
1830 "{} {} {}",
1831 self.dialect.quote(&cursor.field),
1832 op,
1833 cursor.value.to_param_with_dialect(&*self.dialect)
1834 ));
1835 }
1836
1837 if conditions.is_empty() {
1838 return String::new();
1839 }
1840
1841 let mut groups: Vec<Vec<String>> = Vec::new();
1844 let mut current_group: Vec<String> = Vec::new();
1845 for cond in conditions.iter() {
1846 if let Some(stripped) = cond.strip_prefix("OR ") {
1847 current_group.push(stripped.to_string());
1849 } else {
1850 if !current_group.is_empty() {
1852 groups.push(std::mem::take(&mut current_group));
1853 }
1854 current_group.push(cond.clone());
1855 }
1856 }
1857 if !current_group.is_empty() {
1858 groups.push(current_group);
1859 }
1860
1861 let group_strs: Vec<String> = groups
1862 .iter()
1863 .map(|g| {
1864 if g.len() == 1 {
1865 g[0].clone()
1866 } else {
1867 format!("({})", g.join(" OR "))
1868 }
1869 })
1870 .collect();
1871
1872 format!(" WHERE {}", group_strs.join(" AND "))
1874 }
1875 #[api_stable]
1876
1877 #[tracing::instrument(skip(self), fields(op = "select"))]
1882 pub fn build_select(&self) -> (String, Vec<Value>) {
1883 self.build_select_with_params()
1884 }
1885 #[api_stable]
1886
1887 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1889 pub fn build_insert(
1890 &self,
1891 data: &std::collections::HashMap<String, Value>,
1892 ) -> (String, Vec<Value>) {
1893 self.build_insert_with_params(data)
1894 }
1895
1896 #[api_stable]
1899 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1900 pub fn sql_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1901 let table = self
1902 .table
1903 .clone()
1904 .unwrap_or_else(|| M::table_name().to_string());
1905
1906 if data.is_empty() {
1907 return String::new();
1908 }
1909
1910 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1911 let values: Vec<String> = data
1913 .values()
1914 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1915 .collect();
1916
1917 crate::sql_buffer::SqlBuffer::from_str(&format!(
1918 "INSERT INTO {} ({}) VALUES ({})",
1919 self.dialect.quote(&table),
1920 columns.join(", "),
1921 values.join(", ")
1922 ))
1923 .into_string()
1924 }
1925 #[api_stable]
1926
1927 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1929 pub fn build_update(
1930 &self,
1931 data: &std::collections::HashMap<String, Value>,
1932 ) -> (String, Vec<Value>) {
1933 self.build_update_with_params(data)
1934 }
1935
1936 #[api_stable]
1939 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1940 pub fn sql_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1941 let table = self
1942 .table
1943 .clone()
1944 .unwrap_or_else(|| M::table_name().to_string());
1945
1946 if data.is_empty() {
1947 return String::new();
1948 }
1949
1950 let set_clauses: Vec<String> = data
1951 .iter()
1952 .map(|(k, v)| {
1953 format!(
1954 "{} = {}",
1955 self.dialect.quote(k),
1956 v.to_param_with_dialect(&*self.dialect)
1957 )
1958 })
1959 .collect();
1960
1961 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1962 "UPDATE {} SET {}",
1963 self.dialect.quote(&table),
1964 set_clauses.join(", ")
1965 ));
1966
1967 sql.push_str(&self.build_where_clause());
1968 sql.into_string()
1969 }
1970 #[api_stable]
1971
1972 #[tracing::instrument(skip(self), fields(op = "delete"))]
1984 pub fn build_delete(&self) -> (String, Vec<Value>) {
1985 self.build_delete_with_params()
1986 }
1987
1988 #[api_stable]
1991 #[tracing::instrument(skip(self), fields(op = "delete"))]
1992 pub fn sql_delete(&self) -> String {
1993 let table = self
1994 .table
1995 .clone()
1996 .unwrap_or_else(|| M::table_name().to_string());
1997
1998 if let Some(field) = self.soft_delete_field() {
2000 let where_clause = self.build_where_clause();
2001 return format!(
2002 "UPDATE {} SET {} = NOW(){}",
2003 self.dialect.quote(&table),
2004 self.dialect.quote(field),
2005 where_clause
2006 );
2007 }
2008
2009 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
2010 "DELETE FROM {}",
2011 self.dialect.quote(&table)
2012 ));
2013 sql.push_str(&self.build_where_clause());
2014 sql.into_string()
2015 }
2016
2017 pub fn build_force_delete(&self) -> String {
2027 let table = self
2028 .table
2029 .clone()
2030 .unwrap_or_else(|| M::table_name().to_string());
2031
2032 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2033 sql.push_str(&self.build_where_clause_with_options(false));
2035 sql
2036 }
2037
2038 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
2048 self.build_where_clause_with_params_options(true)
2050 }
2051
2052 fn build_where_clause_with_params_options(
2061 &self,
2062 include_soft_delete: bool,
2063 ) -> (String, Vec<Value>) {
2064 let soft_delete_cond = if include_soft_delete {
2066 self.build_soft_delete_condition()
2067 } else {
2068 None
2069 };
2070
2071 let tenant_cond = self.build_tenant_condition();
2073
2074 #[cfg(feature = "tenant-quota-rls-enhanced")]
2076 let rls_cond: Option<(String, Vec<Value>)> = {
2077 if let Some(ref enhancer) = self.rls_enhancer {
2078 if let Some(ref table) = self.table {
2079 if let Some(tid) = self.tenant_id_value() {
2080 match enhancer.enhance_query(table, &tid.to_string()) {
2081 Ok(Some(c)) => Some((c.sql_fragment, c.params)),
2082 Ok(None) => None,
2083 Err(_) => None,
2084 }
2085 } else {
2086 None
2087 }
2088 } else {
2089 None
2090 }
2091 } else {
2092 None
2093 }
2094 };
2095 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
2096 let rls_cond: Option<(String, Vec<Value>)> = None;
2097
2098 if self.where_conditions.is_empty()
2100 && soft_delete_cond.is_none()
2101 && tenant_cond.is_none()
2102 && self.keyset_cursor.is_none()
2103 && rls_cond.is_none()
2104 {
2105 return (String::new(), Vec::new());
2106 }
2107
2108 let mut params = Vec::new();
2109
2110 let has_extra = soft_delete_cond.is_some()
2112 || tenant_cond.is_some()
2113 || rls_cond.is_some()
2114 || self.keyset_cursor.is_some();
2115 if !has_extra {
2116 let has_or = self.where_conditions.iter().any(|c| {
2117 matches!(
2118 c,
2119 WhereCondition::Or(_)
2120 | WhereCondition::OrEq(..)
2121 | WhereCondition::OrNe(..)
2122 | WhereCondition::OrGt(..)
2123 | WhereCondition::OrGe(..)
2124 | WhereCondition::OrLt(..)
2125 | WhereCondition::OrLe(..)
2126 | WhereCondition::OrLike(..)
2127 )
2128 });
2129 if !has_or {
2130 let mut result = String::with_capacity(7 + self.where_conditions.len() * 32);
2131 result.push_str(" WHERE ");
2132 for (i, cond) in self.where_conditions.iter().enumerate() {
2133 if i > 0 {
2134 result.push_str(" AND ");
2135 }
2136 match cond {
2137 WhereCondition::And(c) => result.push_str(c),
2138 WhereCondition::Eq(f, v) => {
2139 params.push(v.clone());
2140 self.quote_into(f, &mut result);
2141 result.push_str(" = ?");
2142 }
2143 WhereCondition::Ne(f, v) => {
2144 params.push(v.clone());
2145 self.quote_into(f, &mut result);
2146 result.push_str(" != ?");
2147 }
2148 WhereCondition::Gt(f, v) => {
2149 params.push(v.clone());
2150 self.quote_into(f, &mut result);
2151 result.push_str(" > ?");
2152 }
2153 WhereCondition::Ge(f, v) => {
2154 params.push(v.clone());
2155 self.quote_into(f, &mut result);
2156 result.push_str(" >= ?");
2157 }
2158 WhereCondition::Lt(f, v) => {
2159 params.push(v.clone());
2160 self.quote_into(f, &mut result);
2161 result.push_str(" < ?");
2162 }
2163 WhereCondition::Le(f, v) => {
2164 params.push(v.clone());
2165 self.quote_into(f, &mut result);
2166 result.push_str(" <= ?");
2167 }
2168 WhereCondition::Like(f, v) => {
2169 params.push(v.clone());
2170 self.quote_into(f, &mut result);
2171 result.push_str(" LIKE ?");
2172 }
2173 WhereCondition::In(f, vals) => {
2174 params.extend(vals.iter().cloned());
2175 self.quote_into(f, &mut result);
2176 result.push_str(" IN (");
2177 for (j, _) in vals.iter().enumerate() {
2178 if j > 0 {
2179 result.push_str(", ");
2180 }
2181 result.push('?');
2182 }
2183 result.push(')');
2184 }
2185 WhereCondition::NotIn(f, vals) => {
2186 params.extend(vals.iter().cloned());
2187 self.quote_into(f, &mut result);
2188 result.push_str(" NOT IN (");
2189 for (j, _) in vals.iter().enumerate() {
2190 if j > 0 {
2191 result.push_str(", ");
2192 }
2193 result.push('?');
2194 }
2195 result.push(')');
2196 }
2197 WhereCondition::Between(f, start, end) => {
2198 params.push(start.clone());
2199 params.push(end.clone());
2200 self.quote_into(f, &mut result);
2201 result.push_str(" BETWEEN ? AND ?");
2202 }
2203 WhereCondition::NotBetween(f, start, end) => {
2204 params.push(start.clone());
2205 params.push(end.clone());
2206 self.quote_into(f, &mut result);
2207 result.push_str(" NOT BETWEEN ? AND ?");
2208 }
2209 WhereCondition::Null(f) => {
2210 self.quote_into(f, &mut result);
2211 result.push_str(" IS NULL");
2212 }
2213 WhereCondition::NotNull(f) => {
2214 self.quote_into(f, &mut result);
2215 result.push_str(" IS NOT NULL");
2216 }
2217 WhereCondition::Exists(s) => {
2218 result.push_str("EXISTS (");
2219 result.push_str(s);
2220 result.push(')');
2221 }
2222 WhereCondition::NotExists(s) => {
2223 result.push_str("NOT EXISTS (");
2224 result.push_str(s);
2225 result.push(')');
2226 }
2227 WhereCondition::TypedExpr(sql, expr_params) => {
2228 params.extend(expr_params.iter().cloned());
2229 result.push_str(sql);
2230 }
2231 WhereCondition::Having(..) => {}
2232 _ => {}
2233 }
2234 }
2235 return (result, params);
2236 }
2237 }
2238
2239 let mut conditions: Vec<String> = Vec::with_capacity(self.where_conditions.len() + 4);
2240 for cond in self.where_conditions.iter() {
2241 match cond {
2242 WhereCondition::And(c) => conditions.push(c.clone()),
2243 WhereCondition::Or(c) => {
2244 let mut s = String::with_capacity(c.len() + 4);
2245 s.push_str("OR ");
2246 s.push_str(c);
2247 conditions.push(s);
2248 }
2249 WhereCondition::Eq(f, v) => {
2250 params.push(v.clone());
2251 let mut s = String::with_capacity(f.len() + 5);
2252 self.quote_into(f, &mut s);
2253 s.push_str(" = ?");
2254 conditions.push(s);
2255 }
2256 WhereCondition::Ne(f, v) => {
2257 params.push(v.clone());
2258 let mut s = String::with_capacity(f.len() + 6);
2259 self.quote_into(f, &mut s);
2260 s.push_str(" != ?");
2261 conditions.push(s);
2262 }
2263 WhereCondition::Gt(f, v) => {
2264 params.push(v.clone());
2265 let mut s = String::with_capacity(f.len() + 5);
2266 self.quote_into(f, &mut s);
2267 s.push_str(" > ?");
2268 conditions.push(s);
2269 }
2270 WhereCondition::Ge(f, v) => {
2271 params.push(v.clone());
2272 let mut s = String::with_capacity(f.len() + 6);
2273 self.quote_into(f, &mut s);
2274 s.push_str(" >= ?");
2275 conditions.push(s);
2276 }
2277 WhereCondition::Lt(f, v) => {
2278 params.push(v.clone());
2279 let mut s = String::with_capacity(f.len() + 5);
2280 self.quote_into(f, &mut s);
2281 s.push_str(" < ?");
2282 conditions.push(s);
2283 }
2284 WhereCondition::Le(f, v) => {
2285 params.push(v.clone());
2286 let mut s = String::with_capacity(f.len() + 6);
2287 self.quote_into(f, &mut s);
2288 s.push_str(" <= ?");
2289 conditions.push(s);
2290 }
2291 WhereCondition::Like(f, v) => {
2292 params.push(v.clone());
2293 let mut s = String::with_capacity(f.len() + 8);
2294 self.quote_into(f, &mut s);
2295 s.push_str(" LIKE ?");
2296 conditions.push(s);
2297 }
2298 WhereCondition::OrEq(f, v) => {
2299 params.push(v.clone());
2300 let mut s = String::with_capacity(f.len() + 8);
2301 s.push_str("OR ");
2302 self.quote_into(f, &mut s);
2303 s.push_str(" = ?");
2304 conditions.push(s);
2305 }
2306 WhereCondition::OrNe(f, v) => {
2307 params.push(v.clone());
2308 let mut s = String::with_capacity(f.len() + 9);
2309 s.push_str("OR ");
2310 self.quote_into(f, &mut s);
2311 s.push_str(" != ?");
2312 conditions.push(s);
2313 }
2314 WhereCondition::OrGt(f, v) => {
2315 params.push(v.clone());
2316 let mut s = String::with_capacity(f.len() + 8);
2317 s.push_str("OR ");
2318 self.quote_into(f, &mut s);
2319 s.push_str(" > ?");
2320 conditions.push(s);
2321 }
2322 WhereCondition::OrGe(f, v) => {
2323 params.push(v.clone());
2324 let mut s = String::with_capacity(f.len() + 9);
2325 s.push_str("OR ");
2326 self.quote_into(f, &mut s);
2327 s.push_str(" >= ?");
2328 conditions.push(s);
2329 }
2330 WhereCondition::OrLt(f, v) => {
2331 params.push(v.clone());
2332 let mut s = String::with_capacity(f.len() + 8);
2333 s.push_str("OR ");
2334 self.quote_into(f, &mut s);
2335 s.push_str(" < ?");
2336 conditions.push(s);
2337 }
2338 WhereCondition::OrLe(f, v) => {
2339 params.push(v.clone());
2340 let mut s = String::with_capacity(f.len() + 9);
2341 s.push_str("OR ");
2342 self.quote_into(f, &mut s);
2343 s.push_str(" <= ?");
2344 conditions.push(s);
2345 }
2346 WhereCondition::OrLike(f, v) => {
2347 params.push(v.clone());
2348 let mut s = String::with_capacity(f.len() + 11);
2349 s.push_str("OR ");
2350 self.quote_into(f, &mut s);
2351 s.push_str(" LIKE ?");
2352 conditions.push(s);
2353 }
2354 WhereCondition::In(f, vals) => {
2355 params.extend(vals.iter().cloned());
2356 let mut s = String::with_capacity(f.len() + vals.len() * 2 + 7);
2357 self.quote_into(f, &mut s);
2358 s.push_str(" IN (");
2359 for (i, _) in vals.iter().enumerate() {
2360 if i > 0 {
2361 s.push_str(", ");
2362 }
2363 s.push('?');
2364 }
2365 s.push(')');
2366 conditions.push(s);
2367 }
2368 WhereCondition::NotIn(f, vals) => {
2369 params.extend(vals.iter().cloned());
2370 let mut s = String::with_capacity(f.len() + vals.len() * 2 + 11);
2371 self.quote_into(f, &mut s);
2372 s.push_str(" NOT IN (");
2373 for (i, _) in vals.iter().enumerate() {
2374 if i > 0 {
2375 s.push_str(", ");
2376 }
2377 s.push('?');
2378 }
2379 s.push(')');
2380 conditions.push(s);
2381 }
2382 WhereCondition::Between(f, start, end) => {
2383 params.push(start.clone());
2384 params.push(end.clone());
2385 let mut s = String::with_capacity(f.len() + 18);
2386 self.quote_into(f, &mut s);
2387 s.push_str(" BETWEEN ? AND ?");
2388 conditions.push(s);
2389 }
2390 WhereCondition::NotBetween(f, start, end) => {
2391 params.push(start.clone());
2392 params.push(end.clone());
2393 let mut s = String::with_capacity(f.len() + 22);
2394 self.quote_into(f, &mut s);
2395 s.push_str(" NOT BETWEEN ? AND ?");
2396 conditions.push(s);
2397 }
2398 WhereCondition::Null(f) => {
2399 let mut s = String::with_capacity(f.len() + 9);
2400 self.quote_into(f, &mut s);
2401 s.push_str(" IS NULL");
2402 conditions.push(s);
2403 }
2404 WhereCondition::NotNull(f) => {
2405 let mut s = String::with_capacity(f.len() + 13);
2406 self.quote_into(f, &mut s);
2407 s.push_str(" IS NOT NULL");
2408 conditions.push(s);
2409 }
2410 WhereCondition::Exists(s) => {
2411 let mut out = String::with_capacity(s.len() + 9);
2412 out.push_str("EXISTS (");
2413 out.push_str(s);
2414 out.push(')');
2415 conditions.push(out);
2416 }
2417 WhereCondition::NotExists(s) => {
2418 let mut out = String::with_capacity(s.len() + 13);
2419 out.push_str("NOT EXISTS (");
2420 out.push_str(s);
2421 out.push(')');
2422 conditions.push(out);
2423 }
2424 WhereCondition::TypedExpr(sql, expr_params) => {
2425 params.extend(expr_params.iter().cloned());
2426 conditions.push(sql.clone());
2427 }
2428 WhereCondition::Having(..) => conditions.push(String::new()),
2429 }
2430 }
2431
2432 if let Some(sd_cond) = soft_delete_cond {
2434 conditions.push(sd_cond);
2435 }
2436
2437 if let Some((t_sql, t_value)) = tenant_cond {
2439 conditions.push(t_sql);
2440 params.push(t_value);
2441 }
2442
2443 if let Some((rls_sql, rls_params)) = rls_cond {
2445 conditions.push(rls_sql);
2446 params.extend(rls_params);
2447 }
2448
2449 if let Some(ref cursor) = self.keyset_cursor {
2451 let op = match cursor.direction {
2452 KeysetDirection::After => " > ?",
2453 KeysetDirection::Before => " < ?",
2454 };
2455 let mut s = String::with_capacity(cursor.field.len() + 5);
2456 self.quote_into(&cursor.field, &mut s);
2457 s.push_str(op);
2458 conditions.push(s);
2459 params.push(cursor.value.clone());
2460 }
2461
2462 if conditions.is_empty() {
2463 return (String::new(), params);
2464 }
2465
2466 let has_or = conditions.iter().any(|c| c.starts_with("OR "));
2467 if !has_or {
2468 let mut result =
2469 String::with_capacity(7 + conditions.iter().map(|s| s.len() + 5).sum::<usize>());
2470 result.push_str(" WHERE ");
2471 for (i, c) in conditions.iter().enumerate() {
2472 if i > 0 {
2473 result.push_str(" AND ");
2474 }
2475 result.push_str(c);
2476 }
2477 return (result, params);
2478 }
2479
2480 let mut groups: Vec<Vec<String>> = Vec::new();
2482 let mut current_group: Vec<String> = Vec::new();
2483 for cond in conditions.iter() {
2484 if let Some(stripped) = cond.strip_prefix("OR ") {
2485 current_group.push(stripped.to_string());
2486 } else {
2487 if !current_group.is_empty() {
2488 groups.push(std::mem::take(&mut current_group));
2489 }
2490 current_group.push(cond.clone());
2491 }
2492 }
2493 if !current_group.is_empty() {
2494 groups.push(current_group);
2495 }
2496
2497 let group_strs: Vec<String> = groups
2498 .iter()
2499 .map(|g| {
2500 if g.len() == 1 {
2501 g[0].clone()
2502 } else {
2503 format!("({})", g.join(" OR "))
2504 }
2505 })
2506 .collect();
2507
2508 let mut result =
2510 String::with_capacity(7 + group_strs.iter().map(|s| s.len() + 5).sum::<usize>());
2511 result.push_str(" WHERE ");
2512 for (i, g) in group_strs.iter().enumerate() {
2513 if i > 0 {
2514 result.push_str(" AND ");
2515 }
2516 result.push_str(g);
2517 }
2518 (result, params)
2519 }
2520
2521 pub async fn find_by_ids(
2527 &self,
2528 conn: &mut dyn crate::pool::Connection,
2529 ids: &[i64],
2530 ) -> Result<Vec<std::collections::HashMap<String, Value>>, crate::DbError> {
2531 if ids.is_empty() {
2532 return Ok(Vec::new());
2533 }
2534 let unique_ids: Vec<i64> = {
2535 let s: std::collections::HashSet<i64> = ids.iter().copied().collect();
2536 s.into_iter().collect()
2537 };
2538 const CHUNK: usize = 999;
2539 let mut all_rows = Vec::with_capacity(unique_ids.len());
2540 for chunk in unique_ids.chunks(CHUNK) {
2541 let id_values: Vec<Value> = chunk.iter().map(|&id| Value::I64(id)).collect();
2542 let (sql, params) = QueryBuilder::<M>::new(self.dialect.clone_box())
2543 .table(M::table_name())
2544 .where_in(M::pk_name(), id_values)
2545 .build_select_with_params();
2546 let rows = conn.query_with_params(&sql, ¶ms).await?;
2547 all_rows.extend(rows);
2548 }
2549 Ok(all_rows)
2550 }
2551
2552 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
2557 let table = self.table.as_deref().unwrap_or_else(|| M::table_name());
2558
2559 let columns_len = if self.select_columns.is_empty() {
2560 1
2561 } else {
2562 self.select_columns
2563 .iter()
2564 .map(|c| c.len() + 2)
2565 .sum::<usize>()
2566 };
2567
2568 let capacity = 16
2569 + columns_len
2570 + table.len()
2571 + 2
2572 + self.joins.len() * 48
2573 + self.where_conditions.len() * 32
2574 + self.group_by.len() * 24
2575 + self.having_conditions.len() * 32
2576 + self.order_by.len() * 24
2577 + 32;
2578 let mut sql = String::with_capacity(capacity);
2579 sql.push_str("SELECT ");
2580 if self.select_columns.is_empty() {
2581 sql.push('*');
2582 } else {
2583 for (i, col) in self.select_columns.iter().enumerate() {
2584 if i > 0 {
2585 sql.push_str(", ");
2586 }
2587 sql.push_str(col);
2588 }
2589 }
2590 sql.push_str(" FROM ");
2591 self.quote_into(table, &mut sql);
2592
2593 for join in &self.joins {
2594 match join {
2595 JoinClause::Inner(t, l, r) => {
2596 sql.push_str(" INNER JOIN ");
2597 self.quote_into(t, &mut sql);
2598 sql.push_str(" ON ");
2599 self.quote_into(l, &mut sql);
2600 sql.push_str(" = ");
2601 self.quote_into(r, &mut sql);
2602 }
2603 JoinClause::Left(t, l, r) => {
2604 sql.push_str(" LEFT JOIN ");
2605 self.quote_into(t, &mut sql);
2606 sql.push_str(" ON ");
2607 self.quote_into(l, &mut sql);
2608 sql.push_str(" = ");
2609 self.quote_into(r, &mut sql);
2610 }
2611 JoinClause::Right(t, l, r) => {
2612 sql.push_str(" RIGHT JOIN ");
2613 self.quote_into(t, &mut sql);
2614 sql.push_str(" ON ");
2615 self.quote_into(l, &mut sql);
2616 sql.push_str(" = ");
2617 self.quote_into(r, &mut sql);
2618 }
2619 JoinClause::Cross(t, on) => {
2620 sql.push_str(" CROSS JOIN ");
2621 self.quote_into(t, &mut sql);
2622 sql.push_str(" ON ");
2623 self.quote_into(on, &mut sql);
2624 }
2625 JoinClause::Relation(kind, ft, fk, tt, tk) => {
2626 sql.push(' ');
2627 sql.push_str(kind.as_sql());
2628 sql.push(' ');
2629 self.quote_into(tt, &mut sql);
2630 sql.push_str(" ON ");
2631 self.quote_into(ft, &mut sql);
2632 sql.push('.');
2633 self.quote_into(fk, &mut sql);
2634 sql.push_str(" = ");
2635 self.quote_into(tt, &mut sql);
2636 sql.push('.');
2637 self.quote_into(tk, &mut sql);
2638 }
2639 }
2640 }
2641
2642 let mut params = Vec::new();
2643
2644 let no_soft_delete = self.soft_delete_disabled || M::soft_delete_field().is_none();
2646 let no_tenant =
2647 self.tenant_disabled || M::tenant_field().is_none() || self.tenant_id_value.is_none();
2648 #[cfg(feature = "tenant-quota-rls-enhanced")]
2649 let no_rls = self.rls_enhancer.is_none();
2650 #[cfg(not(feature = "tenant-quota-rls-enhanced"))]
2651 let no_rls = true;
2652 let no_keyset = self.keyset_cursor.is_none();
2653
2654 if no_soft_delete && no_tenant && no_rls && no_keyset && !self.where_conditions.is_empty() {
2655 let has_or = self.where_conditions.iter().any(|c| {
2656 matches!(
2657 c,
2658 WhereCondition::Or(_)
2659 | WhereCondition::OrEq(..)
2660 | WhereCondition::OrNe(..)
2661 | WhereCondition::OrGt(..)
2662 | WhereCondition::OrGe(..)
2663 | WhereCondition::OrLt(..)
2664 | WhereCondition::OrLe(..)
2665 | WhereCondition::OrLike(..)
2666 )
2667 });
2668 if !has_or {
2669 sql.push_str(" WHERE ");
2670 for (i, cond) in self.where_conditions.iter().enumerate() {
2671 if i > 0 {
2672 sql.push_str(" AND ");
2673 }
2674 match cond {
2675 WhereCondition::And(c) => sql.push_str(c),
2676 WhereCondition::Eq(f, v) => {
2677 params.push(v.clone());
2678 self.quote_into(f, &mut sql);
2679 sql.push_str(" = ?");
2680 }
2681 WhereCondition::Ne(f, v) => {
2682 params.push(v.clone());
2683 self.quote_into(f, &mut sql);
2684 sql.push_str(" != ?");
2685 }
2686 WhereCondition::Gt(f, v) => {
2687 params.push(v.clone());
2688 self.quote_into(f, &mut sql);
2689 sql.push_str(" > ?");
2690 }
2691 WhereCondition::Ge(f, v) => {
2692 params.push(v.clone());
2693 self.quote_into(f, &mut sql);
2694 sql.push_str(" >= ?");
2695 }
2696 WhereCondition::Lt(f, v) => {
2697 params.push(v.clone());
2698 self.quote_into(f, &mut sql);
2699 sql.push_str(" < ?");
2700 }
2701 WhereCondition::Le(f, v) => {
2702 params.push(v.clone());
2703 self.quote_into(f, &mut sql);
2704 sql.push_str(" <= ?");
2705 }
2706 WhereCondition::Like(f, v) => {
2707 params.push(v.clone());
2708 self.quote_into(f, &mut sql);
2709 sql.push_str(" LIKE ?");
2710 }
2711 WhereCondition::In(f, vals) => {
2712 params.extend(vals.iter().cloned());
2713 self.quote_into(f, &mut sql);
2714 sql.push_str(" IN (");
2715 for (j, _) in vals.iter().enumerate() {
2716 if j > 0 {
2717 sql.push_str(", ");
2718 }
2719 sql.push('?');
2720 }
2721 sql.push(')');
2722 }
2723 WhereCondition::NotIn(f, vals) => {
2724 params.extend(vals.iter().cloned());
2725 self.quote_into(f, &mut sql);
2726 sql.push_str(" NOT IN (");
2727 for (j, _) in vals.iter().enumerate() {
2728 if j > 0 {
2729 sql.push_str(", ");
2730 }
2731 sql.push('?');
2732 }
2733 sql.push(')');
2734 }
2735 WhereCondition::Between(f, start, end) => {
2736 params.push(start.clone());
2737 params.push(end.clone());
2738 self.quote_into(f, &mut sql);
2739 sql.push_str(" BETWEEN ? AND ?");
2740 }
2741 WhereCondition::NotBetween(f, start, end) => {
2742 params.push(start.clone());
2743 params.push(end.clone());
2744 self.quote_into(f, &mut sql);
2745 sql.push_str(" NOT BETWEEN ? AND ?");
2746 }
2747 WhereCondition::Null(f) => {
2748 self.quote_into(f, &mut sql);
2749 sql.push_str(" IS NULL");
2750 }
2751 WhereCondition::NotNull(f) => {
2752 self.quote_into(f, &mut sql);
2753 sql.push_str(" IS NOT NULL");
2754 }
2755 WhereCondition::Exists(s) => {
2756 sql.push_str("EXISTS (");
2757 sql.push_str(s);
2758 sql.push(')');
2759 }
2760 WhereCondition::NotExists(s) => {
2761 sql.push_str("NOT EXISTS (");
2762 sql.push_str(s);
2763 sql.push(')');
2764 }
2765 WhereCondition::TypedExpr(sql_expr, expr_params) => {
2766 params.extend(expr_params.iter().cloned());
2767 sql.push_str(sql_expr);
2768 }
2769 WhereCondition::Having(..) => {}
2770 _ => {}
2771 }
2772 }
2773 } else {
2774 let (where_clause, where_params) = self.build_where_clause_with_params();
2775 if !where_clause.is_empty() {
2776 sql.push_str(&where_clause);
2777 params = where_params;
2778 }
2779 }
2780 } else {
2781 let (where_clause, where_params) = self.build_where_clause_with_params();
2783 if !where_clause.is_empty() {
2784 sql.push_str(&where_clause);
2785 params = where_params;
2786 }
2787 }
2788
2789 if !self.group_by.is_empty() {
2790 sql.push_str(" GROUP BY ");
2791 for (i, c) in self.group_by.iter().enumerate() {
2792 if i > 0 {
2793 sql.push_str(", ");
2794 }
2795 self.quote_into(c, &mut sql);
2796 }
2797 }
2798
2799 if !self.having_conditions.is_empty() {
2800 sql.push_str(" HAVING ");
2801 for (i, cond) in self.having_conditions.iter().enumerate() {
2802 if i > 0 {
2803 sql.push_str(" AND ");
2804 }
2805 match cond {
2806 WhereCondition::And(c) => sql.push_str(c),
2807 WhereCondition::Having(agg, op, value) => {
2809 params.push(value.clone());
2810 let _ = write!(sql, "{} {} ?", agg.render(&*self.dialect), op.as_sql());
2811 }
2812 _ => {}
2813 }
2814 }
2815 }
2816
2817 if !self.order_by.is_empty() {
2818 sql.push_str(" ORDER BY ");
2819 for (i, o) in self.order_by.iter().enumerate() {
2820 if i > 0 {
2821 sql.push_str(", ");
2822 }
2823 self.quote_into(&o.field, &mut sql);
2824 match o.direction {
2825 OrderDirection::Asc => sql.push_str(" ASC"),
2826 OrderDirection::Desc => sql.push_str(" DESC"),
2827 }
2828 }
2829 }
2830
2831 if let Some(limit) = self.limit_value {
2832 sql.push_str(" LIMIT ");
2833 push_usize_to_string(limit, &mut sql);
2834 }
2835 if let Some(offset) = self.offset_value {
2836 sql.push_str(" OFFSET ");
2837 push_usize_to_string(offset, &mut sql);
2838 }
2839
2840 if let Some(lock_type) = &self.lock_type {
2842 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2843 sql.push(' ');
2844 sql.push_str(&lock_clause);
2845 }
2846 }
2847
2848 (sql, params)
2849 }
2850
2851 pub fn build_insert_with_params(
2853 &self,
2854 data: &std::collections::HashMap<String, Value>,
2855 ) -> (String, Vec<Value>) {
2856 let table = self.table.as_deref().unwrap_or_else(|| M::table_name());
2857 if data.is_empty() {
2858 return (String::new(), Vec::new());
2859 }
2860
2861 let mut params = Vec::with_capacity(data.len());
2862 let capacity = 32 + table.len() + data.len() * 6;
2863 let mut sql = String::with_capacity(capacity);
2864
2865 if self.insert_or_ignore {
2866 sql.push_str(&self.dialect.build_insert_or_ignore_prefix(table));
2867 } else {
2868 sql.push_str("INSERT INTO ");
2869 self.quote_into(table, &mut sql);
2870 }
2871 sql.push_str(" (");
2872
2873 for (i, (k, v)) in data.iter().enumerate() {
2874 if i > 0 {
2875 sql.push_str(", ");
2876 }
2877 self.quote_into(k, &mut sql);
2878 params.push(v.clone());
2879 }
2880
2881 sql.push_str(") VALUES (");
2882 for i in 0..data.len() {
2883 if i > 0 {
2884 sql.push_str(", ");
2885 }
2886 sql.push('?');
2887 }
2888 sql.push(')');
2889 (sql, params)
2890 }
2891
2892 pub fn build_batch_insert_with_params(
2899 &self,
2900 rows: &[std::collections::HashMap<String, Value>],
2901 ) -> (String, Vec<Value>) {
2902 let table = self.table.as_deref().unwrap_or_else(|| M::table_name());
2903 if rows.is_empty() {
2904 return (String::new(), Vec::new());
2905 }
2906
2907 let first_row = &rows[0];
2908 let columns: Vec<&String> = first_row.keys().collect();
2909 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2910
2911 let capacity = 32
2912 + table.len()
2913 + columns.iter().map(|c| c.len() + 2).sum::<usize>()
2914 + rows.len() * (columns.len() * 3 + 4);
2915 let mut sql = String::with_capacity(capacity);
2916 let mut params = Vec::with_capacity(rows.len() * columns.len());
2917
2918 sql.push_str("INSERT INTO ");
2919 self.quote_into(table, &mut sql);
2920 sql.push_str(" (");
2921 for (i, col) in columns.iter().enumerate() {
2922 if i > 0 {
2923 sql.push_str(", ");
2924 }
2925 self.quote_into(col, &mut sql);
2926 }
2927 sql.push_str(") VALUES ");
2928
2929 let mut param_idx = 1usize;
2930 for (row_i, row) in rows.iter().enumerate() {
2931 if row_i > 0 {
2932 sql.push_str(", ");
2933 }
2934 sql.push('(');
2935 for (col_i, col) in columns.iter().enumerate() {
2936 if col_i > 0 {
2937 sql.push_str(", ");
2938 }
2939 match row.get(*col) {
2940 Some(v) => {
2941 params.push(v.clone());
2942 if is_pg {
2943 sql.push('$');
2944 push_usize_to_string(param_idx, &mut sql);
2945 param_idx += 1;
2946 } else {
2947 sql.push('?');
2948 }
2949 }
2950 None => {
2951 sql.push_str("NULL");
2952 }
2953 }
2954 }
2955 sql.push(')');
2956 }
2957 (sql, params)
2958 }
2959
2960 pub fn build_batch_upsert_with_params(
2981 &self,
2982 rows: &[std::collections::HashMap<String, Value>],
2983 conflict_columns: &[&str],
2984 update_columns: &[&str],
2985 ) -> Result<(String, Vec<Value>), crate::DbError> {
2986 if rows.is_empty() {
2987 return Err(crate::DbError::InvalidInput(
2988 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2989 ));
2990 }
2991
2992 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2994 if insert_sql.is_empty() {
2995 return Err(crate::DbError::InvalidInput(
2996 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2997 ));
2998 }
2999
3000 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
3002
3003 let conflict_clause = self
3005 .dialect
3006 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
3007 .ok_or_else(|| {
3008 crate::DbError::InvalidInput(format!(
3009 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
3010 self.dialect.db_type()
3011 ))
3012 })?;
3013
3014 let sql = format!("{} {}", insert_sql, conflict_clause);
3015 Ok((sql, params))
3016 }
3017
3018 pub fn build_update_with_params(
3021 &self,
3022 data: &std::collections::HashMap<String, Value>,
3023 ) -> (String, Vec<Value>) {
3024 let table = self
3025 .table
3026 .clone()
3027 .unwrap_or_else(|| M::table_name().to_string());
3028 if data.is_empty() {
3029 return (String::new(), Vec::new());
3030 }
3031
3032 let mut set_clauses = Vec::with_capacity(data.len());
3033 let mut params = Vec::with_capacity(data.len());
3034 for (k, v) in data.iter() {
3035 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
3036 params.push(v.clone());
3037 }
3038
3039 let mut sql = format!(
3040 "UPDATE {} SET {}",
3041 self.dialect.quote(&table),
3042 set_clauses.join(", ")
3043 );
3044
3045 let (where_clause, where_params) = self.build_where_clause_with_params();
3047 if !where_clause.is_empty() {
3048 sql.push_str(&where_clause);
3049 params.extend(where_params);
3050 }
3051
3052 (sql, params)
3053 }
3054
3055 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
3061 let table = self
3062 .table
3063 .clone()
3064 .unwrap_or_else(|| M::table_name().to_string());
3065
3066 if let Some(field) = self.soft_delete_field() {
3068 let (where_clause, where_params) = self.build_where_clause_with_params();
3069 let sql = format!(
3070 "UPDATE {} SET {} = NOW(){}",
3071 self.dialect.quote(&table),
3072 self.dialect.quote(field),
3073 where_clause
3074 );
3075 return (sql, where_params);
3076 }
3077
3078 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
3079 let mut params = Vec::new();
3080
3081 let (where_clause, where_params) = self.build_where_clause_with_params();
3082 if !where_clause.is_empty() {
3083 sql.push_str(&where_clause);
3084 params = where_params;
3085 }
3086
3087 (sql, params)
3088 }
3089
3090 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
3094 let table = self
3095 .table
3096 .clone()
3097 .unwrap_or_else(|| M::table_name().to_string());
3098
3099 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
3100 let mut params = Vec::new();
3101
3102 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
3104 if !where_clause.is_empty() {
3105 sql.push_str(&where_clause);
3106 params = where_params;
3107 }
3108
3109 (sql, params)
3110 }
3111
3112 pub fn build_count(&self) -> String {
3114 let table = self
3115 .table
3116 .clone()
3117 .unwrap_or_else(|| M::table_name().to_string());
3118
3119 let mut sql = format!(
3120 "SELECT COUNT(*) as total FROM {}",
3121 self.dialect.quote(&table)
3122 );
3123 sql.push_str(&self.build_where_clause());
3124 sql
3125 }
3126
3127 pub fn build_exists(&self) -> String {
3129 let table = self
3130 .table
3131 .clone()
3132 .unwrap_or_else(|| M::table_name().to_string());
3133
3134 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
3135 sql.push_str(&self.build_where_clause());
3136 sql.push_str(" LIMIT 1");
3137 format!("SELECT EXISTS({})", sql)
3138 }
3139
3140 pub fn build_max(&self, field: &str) -> String {
3142 let table = self
3143 .table
3144 .clone()
3145 .unwrap_or_else(|| M::table_name().to_string());
3146
3147 let mut sql = format!(
3148 "SELECT MAX({}) as max_val FROM {}",
3149 self.dialect.quote(field),
3150 self.dialect.quote(&table)
3151 );
3152 sql.push_str(&self.build_where_clause());
3153 sql
3154 }
3155
3156 pub fn build_min(&self, field: &str) -> String {
3158 let table = self
3159 .table
3160 .clone()
3161 .unwrap_or_else(|| M::table_name().to_string());
3162
3163 let mut sql = format!(
3164 "SELECT MIN({}) as min_val FROM {}",
3165 self.dialect.quote(field),
3166 self.dialect.quote(&table)
3167 );
3168 sql.push_str(&self.build_where_clause());
3169 sql
3170 }
3171
3172 pub fn build_sum(&self, field: &str) -> String {
3174 let table = self
3175 .table
3176 .clone()
3177 .unwrap_or_else(|| M::table_name().to_string());
3178
3179 let mut sql = format!(
3180 "SELECT SUM({}) as sum_val FROM {}",
3181 self.dialect.quote(field),
3182 self.dialect.quote(&table)
3183 );
3184 sql.push_str(&self.build_where_clause());
3185 sql
3186 }
3187
3188 pub fn build_avg(&self, field: &str) -> String {
3190 let table = self
3191 .table
3192 .clone()
3193 .unwrap_or_else(|| M::table_name().to_string());
3194
3195 let mut sql = format!(
3196 "SELECT AVG({}) as avg_val FROM {}",
3197 self.dialect.quote(field),
3198 self.dialect.quote(&table)
3199 );
3200 sql.push_str(&self.build_where_clause());
3201 sql
3202 }
3203
3204 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
3207 let sql = self.sql();
3208 let mut errors = Vec::new();
3209
3210 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
3211 errors.push(e);
3212 }
3213
3214 if !self.joins.is_empty() {
3216 for join in &self.joins {
3217 match join {
3218 JoinClause::Inner(_, left, right)
3219 | JoinClause::Left(_, left, right)
3220 | JoinClause::Right(_, left, right) => {
3221 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
3222 errors.push(e);
3223 }
3224 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
3225 errors.push(e);
3226 }
3227 }
3228 JoinClause::Relation(_, ft, fk, tt, tk) => {
3229 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
3230 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
3231 errors.push(e);
3232 }
3233 }
3234 }
3235 _ => {}
3236 }
3237 }
3238 }
3239
3240 let table = self
3242 .table
3243 .clone()
3244 .unwrap_or_else(|| M::table_name().to_string());
3245 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
3246 errors.push(e);
3247 }
3248
3249 if errors.is_empty() {
3250 Ok(())
3251 } else {
3252 Err(errors)
3253 }
3254 }
3255
3256 pub fn validate_insert(
3259 &self,
3260 data: &std::collections::HashMap<String, Value>,
3261 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
3262 let sql = self.sql_insert(data);
3263 let mut errors = Vec::new();
3264
3265 if sql.is_empty() {
3266 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
3267 return Err(errors);
3268 }
3269
3270 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
3271 errors.push(e);
3272 }
3273
3274 if errors.is_empty() {
3275 Ok(())
3276 } else {
3277 Err(errors)
3278 }
3279 }
3280
3281 pub fn validate_update(
3284 &self,
3285 data: &std::collections::HashMap<String, Value>,
3286 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
3287 let sql = self.sql_update(data);
3288 let mut errors = Vec::new();
3289
3290 if sql.is_empty() {
3291 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
3292 return Err(errors);
3293 }
3294
3295 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
3296 errors.push(e);
3297 }
3298
3299 if errors.is_empty() {
3300 Ok(())
3301 } else {
3302 Err(errors)
3303 }
3304 }
3305
3306 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
3308 let sql = self.sql_delete();
3309 let mut errors = Vec::new();
3310
3311 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
3312 errors.push(e);
3313 }
3314
3315 if errors.is_empty() {
3316 Ok(())
3317 } else {
3318 Err(errors)
3319 }
3320 }
3321}
3322
3323impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
3325 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
3343 let all_columns = M::columns();
3344 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
3345
3346 for field in fields {
3347 if !all_columns.contains(field) {
3348 return Err(crate::DbError::InvalidInput(format!(
3349 "排除的字段不存在: {}",
3350 field
3351 )));
3352 }
3353 }
3354
3355 let retained: Vec<String> = all_columns
3356 .into_iter()
3357 .filter(|c| !exclude_set.contains(*c))
3358 .map(|s| s.to_string())
3359 .collect();
3360
3361 if retained.is_empty() {
3362 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
3363 }
3364
3365 self.select_mode = crate::partial_model::SelectMode::Partial;
3366 self.select_columns = retained;
3367 Ok(self)
3368 }
3369}
3370
3371impl<M: Model> fmt::Debug for QueryBuilder<M> {
3372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3373 f.debug_struct("QueryBuilder")
3374 .field("table", &self.table)
3375 .field("select_columns", &self.select_columns)
3376 .field("where_conditions", &self.where_conditions.len())
3377 .field("limit", &self.limit_value)
3378 .finish()
3379 }
3380}
3381
3382#[cfg(test)]
3383#[allow(deprecated)]
3384mod tests {
3385 use super::*;
3386 use crate::db_type::DbType;
3387 use crate::dialect::get_dialect;
3388
3389 struct TestModel;
3390 impl Model for TestModel {
3391 type PrimaryKey = i64;
3392
3393 fn table_name() -> &'static str {
3394 "test_models"
3395 }
3396
3397 fn pk(&self) -> Self::PrimaryKey {
3398 1
3399 }
3400
3401 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3402 }
3403
3404 #[test]
3405 fn test_query_builder_select() -> Result<(), crate::DbError> {
3406 let dialect = get_dialect(DbType::MySQL)?;
3407 let builder = QueryBuilder::<TestModel>::new(dialect);
3408
3409 let sql = builder.table("users").select(vec!["id", "name"])?.sql();
3410 assert!(sql.contains("SELECT `id`, `name` FROM"));
3411 assert!(sql.contains("`users`"));
3412 Ok(())
3413 }
3414
3415 #[test]
3416 fn test_query_builder_where() -> Result<(), crate::DbError> {
3417 let dialect = get_dialect(DbType::MySQL)?;
3418 let builder = QueryBuilder::<TestModel>::new(dialect);
3419
3420 let sql = builder
3421 .table("users")
3422 .where_eq("status", crate::value::Value::String("active".into()))
3423 .where_gt("age", crate::value::Value::I64(18))
3424 .sql();
3425
3426 assert!(sql.contains("WHERE"));
3427 assert!(sql.contains("`status` = 'active'"));
3428 assert!(sql.contains("`age` > 18"));
3429 Ok(())
3430 }
3431
3432 #[test]
3433 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
3434 let dialect = get_dialect(DbType::MySQL)?;
3435 let builder = QueryBuilder::<TestModel>::new(dialect);
3436
3437 let sql = builder
3438 .table("users")
3439 .order_by("created_at")
3440 .order_desc("id")
3441 .sql();
3442
3443 assert!(sql.contains("ORDER BY"));
3444 assert!(sql.contains("`created_at` ASC"));
3445 assert!(sql.contains("`id` DESC"));
3446 Ok(())
3447 }
3448
3449 #[test]
3450 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
3451 let dialect = get_dialect(DbType::MySQL)?;
3452 let builder = QueryBuilder::<TestModel>::new(dialect);
3453
3454 let sql = builder.table("users").limit(10).offset(20).sql();
3455
3456 assert!(sql.contains("LIMIT 10"));
3457 assert!(sql.contains("OFFSET 20"));
3458 Ok(())
3459 }
3460
3461 #[test]
3462 fn test_query_builder_page() -> Result<(), crate::DbError> {
3463 let dialect = get_dialect(DbType::MySQL)?;
3464 let builder = QueryBuilder::<TestModel>::new(dialect);
3465
3466 let sql = builder.table("users").page(3, 20).sql();
3467
3468 assert!(sql.contains("LIMIT 20"));
3469 assert!(sql.contains("OFFSET 40"));
3470 Ok(())
3471 }
3472
3473 #[test]
3474 fn test_query_builder_insert() -> Result<(), crate::DbError> {
3475 let dialect = get_dialect(DbType::MySQL)?;
3476 let builder = QueryBuilder::<TestModel>::new(dialect);
3477
3478 let mut data = std::collections::HashMap::new();
3479 data.insert("name".to_string(), Value::String("test".to_string()));
3480 data.insert("age".to_string(), Value::I64(25));
3481
3482 let sql = builder.table("users").sql_insert(&data);
3483
3484 assert!(sql.contains("INSERT INTO"));
3485 assert!(sql.contains("`name`"));
3486 assert!(sql.contains("'test'"));
3487 Ok(())
3488 }
3489
3490 #[test]
3491 fn test_query_builder_update() -> Result<(), crate::DbError> {
3492 let dialect = get_dialect(DbType::MySQL)?;
3493 let builder = QueryBuilder::<TestModel>::new(dialect);
3494
3495 let mut data = std::collections::HashMap::new();
3496 data.insert("name".to_string(), Value::String("updated".to_string()));
3497
3498 let sql = builder
3499 .table("users")
3500 .where_eq("id", Value::I64(1))
3501 .sql_update(&data);
3502
3503 assert!(sql.contains("UPDATE"));
3504 assert!(sql.contains("`name` = 'updated'"));
3505 assert!(sql.contains("WHERE"));
3506 Ok(())
3507 }
3508
3509 #[test]
3510 fn test_query_builder_delete() -> Result<(), crate::DbError> {
3511 let dialect = get_dialect(DbType::MySQL)?;
3512 let builder = QueryBuilder::<TestModel>::new(dialect);
3513
3514 let sql = builder
3515 .table("users")
3516 .where_eq("id", Value::I64(1))
3517 .sql_delete();
3518
3519 assert!(sql.contains("DELETE FROM"));
3520 assert!(sql.contains("WHERE"));
3521 Ok(())
3522 }
3523
3524 #[test]
3525 fn test_query_builder_count() -> Result<(), crate::DbError> {
3526 let dialect = get_dialect(DbType::MySQL)?;
3527 let builder = QueryBuilder::<TestModel>::new(dialect);
3528
3529 let sql = builder.table("users").build_count();
3530
3531 assert!(sql.contains("SELECT COUNT(*)"));
3532 assert!(sql.contains("FROM"));
3533 Ok(())
3534 }
3535
3536 #[test]
3537 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
3538 let dialect = get_dialect(DbType::MySQL)?;
3539 let builder = QueryBuilder::<TestModel>::new(dialect);
3540
3541 let sql = builder
3542 .table("users")
3543 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3544 .sql();
3545
3546 assert!(sql.contains("IN ("));
3547 Ok(())
3548 }
3549
3550 #[test]
3551 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
3552 let dialect = get_dialect(DbType::MySQL)?;
3553 let builder = QueryBuilder::<TestModel>::new(dialect);
3554
3555 let sql = builder
3556 .table("users")
3557 .where_between("age", Value::I64(18), Value::I64(30))
3558 .sql();
3559
3560 assert!(sql.contains("BETWEEN"));
3561 Ok(())
3562 }
3563
3564 #[test]
3565 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
3566 let dialect = get_dialect(DbType::MySQL)?;
3567 let builder = QueryBuilder::<TestModel>::new(dialect);
3568
3569 let sql = builder.table("users").where_null("deleted_at").sql();
3570
3571 assert!(sql.contains("IS NULL"));
3572 Ok(())
3573 }
3574
3575 #[test]
3576 fn test_query_builder_join() -> Result<(), crate::DbError> {
3577 let dialect = get_dialect(DbType::MySQL)?;
3578 let builder = QueryBuilder::<TestModel>::new(dialect);
3579
3580 let sql = builder
3581 .table("users")
3582 .join_inner("posts", "users.id", "posts.user_id")
3583 .sql();
3584
3585 assert!(sql.contains("INNER JOIN"));
3586 assert!(sql.contains("`posts`"));
3587 Ok(())
3588 }
3589
3590 #[test]
3591 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
3592 let dialect = get_dialect(DbType::MySQL)?;
3593 let builder = QueryBuilder::<TestModel>::new(dialect);
3594
3595 let sql = builder.table("users").group_by("status").sql();
3596
3597 assert!(sql.contains("GROUP BY"));
3598 assert!(sql.contains("`status`"));
3599 Ok(())
3600 }
3601
3602 #[test]
3603 fn test_query_builder_max() -> Result<(), crate::DbError> {
3604 let dialect = get_dialect(DbType::MySQL)?;
3605 let builder = QueryBuilder::<TestModel>::new(dialect);
3606
3607 let sql = builder.table("users").build_max("score");
3608
3609 assert!(sql.contains("MAX("));
3610 assert!(sql.contains("`score`"));
3611 Ok(())
3612 }
3613
3614 #[test]
3615 fn test_query_builder_min() -> Result<(), crate::DbError> {
3616 let dialect = get_dialect(DbType::MySQL)?;
3617 let builder = QueryBuilder::<TestModel>::new(dialect);
3618
3619 let sql = builder.table("users").build_min("price");
3620
3621 assert!(sql.contains("MIN("));
3622 assert!(sql.contains("`price`"));
3623 Ok(())
3624 }
3625
3626 #[test]
3627 fn test_query_builder_sum() -> Result<(), crate::DbError> {
3628 let dialect = get_dialect(DbType::MySQL)?;
3629 let builder = QueryBuilder::<TestModel>::new(dialect);
3630
3631 let sql = builder.table("orders").build_sum("amount");
3632
3633 assert!(sql.contains("SUM("));
3634 assert!(sql.contains("`amount`"));
3635 Ok(())
3636 }
3637
3638 #[test]
3639 fn test_query_builder_avg() -> Result<(), crate::DbError> {
3640 let dialect = get_dialect(DbType::MySQL)?;
3641 let builder = QueryBuilder::<TestModel>::new(dialect);
3642
3643 let sql = builder.table("scores").build_avg("value");
3644
3645 assert!(sql.contains("AVG("));
3646 assert!(sql.contains("`value`"));
3647 Ok(())
3648 }
3649
3650 #[test]
3651 fn test_validator_select() -> Result<(), crate::DbError> {
3652 let dialect = get_dialect(DbType::MySQL)?;
3653 let builder = QueryBuilder::<TestModel>::new(dialect);
3654
3655 let result = builder
3656 .table("users")
3657 .select(vec!["id", "name"])?
3658 .validate();
3659 assert!(result.is_ok());
3660 Ok(())
3661 }
3662
3663 #[test]
3664 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
3665 let dialect = get_dialect(DbType::MySQL)?;
3666 let builder = QueryBuilder::<TestModel>::new(dialect);
3667
3668 let result = builder
3669 .table("users")
3670 .join_inner("posts", "users.id", "posts.user_id")
3671 .validate();
3672 assert!(result.is_ok());
3673 Ok(())
3674 }
3675
3676 #[test]
3677 fn test_validator_insert() -> Result<(), crate::DbError> {
3678 let dialect = get_dialect(DbType::MySQL)?;
3679 let builder = QueryBuilder::<TestModel>::new(dialect);
3680
3681 let mut data = std::collections::HashMap::new();
3682 data.insert("name".to_string(), Value::String("test".to_string()));
3683
3684 let result = builder.table("users").validate_insert(&data);
3685 assert!(result.is_ok());
3686 Ok(())
3687 }
3688
3689 #[test]
3690 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
3691 let dialect = get_dialect(DbType::MySQL)?;
3692 let builder = QueryBuilder::<TestModel>::new(dialect);
3693
3694 let data = std::collections::HashMap::new();
3695 let result = builder.table("users").validate_insert(&data);
3696 assert!(result.is_err());
3697 Ok(())
3698 }
3699
3700 #[test]
3701 fn test_validator_update() -> Result<(), crate::DbError> {
3702 let dialect = get_dialect(DbType::MySQL)?;
3703 let builder = QueryBuilder::<TestModel>::new(dialect);
3704
3705 let mut data = std::collections::HashMap::new();
3706 data.insert("name".to_string(), Value::String("updated".to_string()));
3707
3708 let result = builder.table("users").validate_update(&data);
3709 assert!(result.is_ok());
3710 Ok(())
3711 }
3712
3713 #[test]
3714 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
3715 let dialect = get_dialect(DbType::MySQL)?;
3716 let builder = QueryBuilder::<TestModel>::new(dialect);
3717
3718 let data = std::collections::HashMap::new();
3719 let result = builder.table("users").validate_update(&data);
3720 assert!(result.is_err());
3721 Ok(())
3722 }
3723
3724 #[test]
3725 fn test_validator_delete() -> Result<(), crate::DbError> {
3726 let dialect = get_dialect(DbType::MySQL)?;
3727 let builder = QueryBuilder::<TestModel>::new(dialect);
3728
3729 let result = builder
3730 .table("users")
3731 .where_eq("id", Value::I64(1))
3732 .validate_delete();
3733 assert!(result.is_ok());
3734 Ok(())
3735 }
3736
3737 #[test]
3738 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
3739 let dialect = get_dialect(DbType::MySQL)?;
3740 let builder = QueryBuilder::<TestModel>::new(dialect);
3741
3742 let result = builder.table("users").validate_delete();
3744 assert!(result.is_ok());
3745 Ok(())
3746 }
3747
3748 #[test]
3751 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
3752 let dialect = get_dialect(DbType::MySQL)?;
3753 let builder = QueryBuilder::<TestModel>::new(dialect);
3754 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3755 let sql = builder.sql();
3756 assert!(sql.contains("SELECT `id`, `name` FROM"));
3758 assert!(sql.contains("`users`"));
3759 Ok(())
3760 }
3761
3762 #[test]
3763 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
3764 let dialect = get_dialect(DbType::MySQL)?;
3765 let builder = QueryBuilder::<TestModel>::new(dialect);
3766
3767 let result = builder
3769 .table("users")
3770 .select_quoted(vec!["id; DROP TABLE users"]);
3771 assert!(result.is_err());
3772
3773 let dialect = get_dialect(DbType::MySQL)?;
3775 let builder = QueryBuilder::<TestModel>::new(dialect);
3776 let result = builder.table("users").select_quoted(vec!["name'"]);
3777 assert!(result.is_err());
3778
3779 let dialect = get_dialect(DbType::MySQL)?;
3781 let builder = QueryBuilder::<TestModel>::new(dialect);
3782 let result = builder.table("users").select_quoted(vec!["1col"]);
3783 assert!(result.is_err());
3784
3785 let dialect = get_dialect(DbType::MySQL)?;
3787 let builder = QueryBuilder::<TestModel>::new(dialect);
3788 let result = builder.table("users").select_quoted(vec!["col name"]);
3789 assert!(result.is_err());
3790 Ok(())
3791 }
3792
3793 #[test]
3794 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
3795 let dialect = get_dialect(DbType::PostgreSQL)?;
3796 let builder = QueryBuilder::<TestModel>::new(dialect);
3797 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
3798 let sql = builder.sql();
3799 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
3801 assert!(sql.contains("\"users\""));
3802 Ok(())
3803 }
3804
3805 struct SoftDeleteModel;
3809 impl Model for SoftDeleteModel {
3810 type PrimaryKey = i64;
3811
3812 fn table_name() -> &'static str {
3813 "soft_users"
3814 }
3815
3816 fn pk(&self) -> Self::PrimaryKey {
3817 1
3818 }
3819
3820 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3821
3822 fn soft_delete_field() -> Option<&'static str> {
3823 Some("deleted_at")
3824 }
3825 }
3826
3827 #[test]
3831 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3832 let dialect = get_dialect(DbType::MySQL)?;
3833 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3834 let sql = builder.table("soft_users").sql();
3835 assert!(
3837 sql.contains("`deleted_at` IS NULL"),
3838 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3839 sql
3840 );
3841 Ok(())
3842 }
3843
3844 #[test]
3846 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3847 let dialect = get_dialect(DbType::MySQL)?;
3848 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3849 .table("soft_users")
3850 .where_eq("status", Value::String("active".into()))
3851 .sql();
3852 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3854 assert!(
3855 sql.contains("`deleted_at` IS NULL"),
3856 "软删除条件应自动追加: {}",
3857 sql
3858 );
3859 Ok(())
3860 }
3861
3862 #[test]
3866 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3867 let dialect = get_dialect(DbType::MySQL)?;
3868 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3869 .table("soft_users")
3870 .without_soft_delete()
3871 .sql();
3872 assert!(
3874 !sql.contains("`deleted_at` IS NULL"),
3875 "without_soft_delete 应禁用过滤,实际: {}",
3876 sql
3877 );
3878 assert!(
3880 !sql.contains("WHERE"),
3881 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3882 sql
3883 );
3884 Ok(())
3885 }
3886
3887 #[test]
3891 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3892 let dialect = get_dialect(DbType::MySQL)?;
3893 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3894 .table("soft_users")
3895 .where_eq("id", Value::I64(42))
3896 .sql_delete();
3897 assert!(
3899 sql.starts_with("UPDATE"),
3900 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3901 sql
3902 );
3903 assert!(
3904 !sql.contains("DELETE FROM"),
3905 "不应生成 DELETE FROM: {}",
3906 sql
3907 );
3908 assert!(
3909 sql.contains("`deleted_at` = NOW()"),
3910 "应设置 deleted_at = NOW(): {}",
3911 sql
3912 );
3913 assert!(
3915 sql.contains("`deleted_at` IS NULL"),
3916 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3917 sql
3918 );
3919 Ok(())
3920 }
3921
3922 #[test]
3926 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3927 let dialect = get_dialect(DbType::MySQL)?;
3928 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3929 .table("soft_users")
3930 .where_eq("id", Value::I64(99))
3931 .build_force_delete();
3932 assert!(
3934 sql.starts_with("DELETE FROM"),
3935 "build_force_delete 应生成 DELETE FROM,实际: {}",
3936 sql
3937 );
3938 assert!(
3940 !sql.contains("`deleted_at` IS NULL"),
3941 "物理删除不应追加软删除过滤: {}",
3942 sql
3943 );
3944 Ok(())
3945 }
3946
3947 #[test]
3949 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3950 let dialect = get_dialect(DbType::MySQL)?;
3951 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3952 .table("soft_users")
3953 .where_eq("id", Value::I64(1))
3954 .build_select_with_params();
3955 assert!(
3956 sql.contains("`deleted_at` IS NULL"),
3957 "参数化版本也应自动追加软删除: {}",
3958 sql
3959 );
3960 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3961 assert_eq!(params[0], Value::I64(1));
3962 Ok(())
3963 }
3964
3965 #[test]
3967 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3968 let dialect = get_dialect(DbType::MySQL)?;
3969 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3970 .table("soft_users")
3971 .where_eq("id", Value::I64(7))
3972 .build_delete_with_params();
3973 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3974 assert!(
3975 sql.contains("`deleted_at` = NOW()"),
3976 "应设置 NOW(): {}",
3977 sql
3978 );
3979 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3980 Ok(())
3981 }
3982
3983 #[test]
3985 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3986 let dialect = get_dialect(DbType::MySQL)?;
3987 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3988 .table("soft_users")
3989 .where_eq("id", Value::I64(11))
3990 .build_force_delete_with_params();
3991 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3992 assert!(
3993 !sql.contains("`deleted_at` IS NULL"),
3994 "不应追加软删除过滤: {}",
3995 sql
3996 );
3997 assert_eq!(params.len(), 1);
3998 Ok(())
3999 }
4000
4001 #[test]
4005 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
4006 let dialect = get_dialect(DbType::MySQL)?;
4007 let sql = QueryBuilder::<TestModel>::new(dialect)
4008 .table("users")
4009 .where_eq("id", Value::I64(1))
4010 .sql();
4011 assert!(
4012 !sql.contains("deleted_at"),
4013 "非软删除模型不应追加 deleted_at: {}",
4014 sql
4015 );
4016 let dialect = get_dialect(DbType::MySQL)?;
4018 let del_sql = QueryBuilder::<TestModel>::new(dialect)
4019 .table("users")
4020 .where_eq("id", Value::I64(1))
4021 .sql_delete();
4022 assert!(
4023 del_sql.starts_with("DELETE FROM"),
4024 "非软删除模型 build_delete 应生成 DELETE: {}",
4025 del_sql
4026 );
4027 Ok(())
4028 }
4029
4030 #[test]
4032 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
4033 let dialect = get_dialect(DbType::MySQL)?;
4034 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
4035 .table("soft_users")
4036 .build_count();
4037 assert!(
4038 sql.contains("`deleted_at` IS NULL"),
4039 "build_count 也应追加软删除过滤: {}",
4040 sql
4041 );
4042 Ok(())
4043 }
4044
4045 #[test]
4051 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
4052 let dialect = get_dialect(DbType::MySQL)?;
4053 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4054 .table("users")
4055 .where_eq("name", Value::String("alice".into()))
4056 .build_select_with_params();
4057 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
4059 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
4060 assert_eq!(params.len(), 1);
4061 assert_eq!(params[0], Value::String("alice".into()));
4062 Ok(())
4063 }
4064
4065 #[test]
4070 fn build_select_with_params_no_string_concat() -> Result<(), crate::DbError> {
4071 let dialect = get_dialect(DbType::MySQL)?;
4072 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4073 .table("users")
4074 .where_eq("name", Value::String("O'Brien".into()))
4075 .build_select_with_params();
4076 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
4077 assert!(
4078 !sql.contains("O'Brien"),
4079 "不应将值内嵌到 SQL(字符串拼接): {}",
4080 sql
4081 );
4082 assert!(
4083 !sql.contains("O\\'Brien"),
4084 "不应将转义值内嵌到 SQL: {}",
4085 sql
4086 );
4087 assert_eq!(params.len(), 1, "应有 1 个参数: {:?}", params);
4088 assert_eq!(params[0], Value::String("O'Brien".into()));
4089 Ok(())
4090 }
4091
4092 #[test]
4094 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
4095 let dialect = get_dialect(DbType::MySQL)?;
4096 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4097 .table("users")
4098 .where_like("name", Value::String("%alice%".into()))
4099 .build_select_with_params();
4100 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
4101 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
4102 assert_eq!(params.len(), 1);
4103 Ok(())
4104 }
4105
4106 #[test]
4110 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
4111 let dialect = get_dialect(DbType::MySQL)?;
4112 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4113 .table("users")
4114 .where_ne("status", Value::I64(0))
4115 .build_select_with_params();
4116 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
4117 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
4118 assert_eq!(params.len(), 1);
4119 assert_eq!(params[0], Value::I64(0));
4120 Ok(())
4121 }
4122
4123 #[test]
4127 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
4128 let dialect = get_dialect(DbType::MySQL)?;
4129 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4130 .table("users")
4131 .where_ge("age", Value::I64(18))
4132 .build_select_with_params();
4133 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
4134 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
4135 assert_eq!(params.len(), 1);
4136 assert_eq!(params[0], Value::I64(18));
4137 Ok(())
4138 }
4139
4140 #[test]
4144 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
4145 let dialect = get_dialect(DbType::MySQL)?;
4146 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4147 .table("users")
4148 .where_lt("score", Value::F64(60.0))
4149 .build_select_with_params();
4150 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
4151 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
4152 assert_eq!(params.len(), 1);
4153 assert_eq!(params[0], Value::F64(60.0));
4154 Ok(())
4155 }
4156
4157 #[test]
4161 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
4162 let dialect = get_dialect(DbType::MySQL)?;
4163 let evil_input = "'; DROP TABLE users; --".to_string();
4164 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4165 .table("users")
4166 .where_eq("name", Value::String(evil_input.clone()))
4167 .build_select_with_params();
4168 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
4170 assert_eq!(params.len(), 1);
4172 assert_eq!(params[0], Value::String(evil_input));
4173 assert_eq!(sql.matches('?').count(), 1);
4175 Ok(())
4176 }
4177
4178 #[test]
4180 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
4181 let dialect = get_dialect(DbType::MySQL)?;
4182 let evil = "' OR '1'='1".to_string();
4183 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4184 .table("users")
4185 .where_eq("name", Value::String(evil.clone()))
4186 .build_select_with_params();
4187 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
4188 assert_eq!(params.len(), 1);
4189 assert_eq!(params[0], Value::String(evil));
4190 Ok(())
4191 }
4192
4193 #[test]
4195 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
4196 let dialect = get_dialect(DbType::MySQL)?;
4197 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4198 .table("users")
4199 .where_eq("name", Value::String("alice".into()))
4200 .where_gt("age", Value::I64(18))
4201 .where_le("score", Value::F64(99.5))
4202 .build_select_with_params();
4203 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
4204 assert_eq!(params.len(), 3);
4205 assert_eq!(params[0], Value::String("alice".into()));
4207 assert_eq!(params[1], Value::I64(18));
4208 assert_eq!(params[2], Value::F64(99.5));
4209 Ok(())
4210 }
4211
4212 #[test]
4214 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
4215 let dialect = get_dialect(DbType::MySQL)?;
4216 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4217 .table("users")
4218 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
4219 .build_select_with_params();
4220 assert!(
4221 sql.contains("`id` IN (?, ?, ?)"),
4222 "应使用 3 个占位符: {}",
4223 sql
4224 );
4225 assert_eq!(params.len(), 3);
4226 Ok(())
4227 }
4228
4229 #[test]
4231 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
4232 let dialect = get_dialect(DbType::MySQL)?;
4233 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4234 .table("users")
4235 .where_between("age", Value::I64(18), Value::I64(65))
4236 .build_select_with_params();
4237 assert!(
4238 sql.contains("`age` BETWEEN ? AND ?"),
4239 "应使用 2 个占位符: {}",
4240 sql
4241 );
4242 assert_eq!(params.len(), 2);
4243 assert_eq!(params[0], Value::I64(18));
4244 assert_eq!(params[1], Value::I64(65));
4245 Ok(())
4246 }
4247
4248 #[test]
4250 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
4251 let dialect = get_dialect(DbType::MySQL)?;
4252 let mut data = std::collections::HashMap::new();
4253 data.insert("name".to_string(), Value::String("bob".into()));
4254 data.insert("age".to_string(), Value::I64(30));
4255 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4256 .table("users")
4257 .where_eq("id", Value::I64(99))
4258 .build_update_with_params(&data);
4259 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
4261 assert_eq!(params.len(), 3);
4262 assert_eq!(params[2], Value::I64(99));
4265 Ok(())
4266 }
4267
4268 #[test]
4272 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
4273 let dialect = get_dialect(DbType::MySQL)?;
4274 let sql = QueryBuilder::<TestModel>::new(dialect)
4275 .table("users")
4276 .where_eq("name", Value::String("alice".into()))
4277 .sql();
4278 assert!(
4280 sql.contains("`name` = "),
4281 "无参数版本应含 WHERE 条件: {}",
4282 sql
4283 );
4284 assert!(
4286 !sql.contains("`name` = ?"),
4287 "无参数版本不应使用 ? 占位符: {}",
4288 sql
4289 );
4290 Ok(())
4291 }
4292
4293 #[test]
4295 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
4296 let dialect = get_dialect(DbType::MySQL)?;
4297 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
4298 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
4299 let builder =
4300 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
4301 assert!(
4302 builder.is_soft_delete_disabled(),
4303 "without_soft_delete 后应反映禁用状态"
4304 );
4305 Ok(())
4306 }
4307
4308 struct TenantModel;
4312 impl Model for TenantModel {
4313 type PrimaryKey = i64;
4314
4315 fn table_name() -> &'static str {
4316 "orders"
4317 }
4318
4319 fn pk(&self) -> Self::PrimaryKey {
4320 1
4321 }
4322
4323 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4324
4325 fn tenant_field() -> Option<&'static str> {
4326 Some("tenant_id")
4327 }
4328 }
4329
4330 struct SoftDeleteAndTenantModel;
4332 impl Model for SoftDeleteAndTenantModel {
4333 type PrimaryKey = i64;
4334
4335 fn table_name() -> &'static str {
4336 "documents"
4337 }
4338
4339 fn pk(&self) -> Self::PrimaryKey {
4340 1
4341 }
4342
4343 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4344
4345 fn soft_delete_field() -> Option<&'static str> {
4346 Some("deleted_at")
4347 }
4348
4349 fn tenant_field() -> Option<&'static str> {
4350 Some("tenant_id")
4351 }
4352 }
4353
4354 #[test]
4358 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
4359 let dialect = get_dialect(DbType::MySQL)?;
4360 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4361 .table("orders")
4362 .with_tenant_id(42)
4363 .build_select_with_params();
4364 assert!(
4365 sql.contains("`tenant_id` = ?"),
4366 "多租户模型应自动追加 tenant_id = ?: {}",
4367 sql
4368 );
4369 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
4370 assert_eq!(params[0], Value::I64(42));
4371 Ok(())
4372 }
4373
4374 #[test]
4376 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
4377 let dialect = get_dialect(DbType::MySQL)?;
4378 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4379 .table("orders")
4380 .with_tenant_id(7)
4381 .where_eq("status", Value::String("active".into()))
4382 .build_select_with_params();
4383 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
4384 assert!(
4385 sql.contains("`tenant_id` = ?"),
4386 "租户条件应自动追加: {}",
4387 sql
4388 );
4389 assert_eq!(params.len(), 2, "应有 2 个参数");
4390 assert_eq!(params[0], Value::String("active".into()));
4392 assert_eq!(params[1], Value::I64(7));
4393 Ok(())
4394 }
4395
4396 #[test]
4400 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
4401 let dialect = get_dialect(DbType::MySQL)?;
4402 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4403 .table("orders")
4404 .with_tenant_id(42)
4405 .without_tenant()
4406 .build_select_with_params();
4407 assert!(
4408 !sql.contains("`tenant_id` = ?"),
4409 "without_tenant 应禁用过滤: {}",
4410 sql
4411 );
4412 assert_eq!(params.len(), 0, "不应有租户参数");
4413 Ok(())
4414 }
4415
4416 #[test]
4420 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
4421 let dialect = get_dialect(DbType::MySQL)?;
4422 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4423 .table("orders")
4424 .with_tenant_id(99)
4425 .where_eq("id", Value::I64(1))
4426 .build_delete_with_params();
4427 assert!(
4428 sql.contains("`tenant_id` = ?"),
4429 "删除应自动追加租户条件: {}",
4430 sql
4431 );
4432 assert_eq!(params.len(), 2);
4434 assert_eq!(params[0], Value::I64(1));
4435 assert_eq!(params[1], Value::I64(99));
4436 Ok(())
4437 }
4438
4439 #[test]
4441 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
4442 let dialect = get_dialect(DbType::MySQL)?;
4443 let mut data = std::collections::HashMap::new();
4444 data.insert("status".to_string(), Value::String("shipped".into()));
4445 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4446 .table("orders")
4447 .with_tenant_id(5)
4448 .where_eq("id", Value::I64(10))
4449 .build_update_with_params(&data);
4450 assert!(
4451 sql.contains("`tenant_id` = ?"),
4452 "更新应自动追加租户条件: {}",
4453 sql
4454 );
4455 assert_eq!(params.len(), 3);
4457 assert_eq!(params[2], Value::I64(5));
4459 Ok(())
4460 }
4461
4462 #[test]
4464 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
4465 let dialect = get_dialect(DbType::MySQL)?;
4466 let sql = QueryBuilder::<TenantModel>::new(dialect)
4467 .table("orders")
4468 .with_tenant_id(42)
4469 .build_count();
4470 assert!(
4471 sql.contains("`tenant_id` = 42"),
4472 "build_count 应追加租户条件(无参数版本内嵌值): {}",
4473 sql
4474 );
4475 Ok(())
4476 }
4477
4478 #[test]
4482 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
4483 let dialect = get_dialect(DbType::MySQL)?;
4484 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4486 .table("users")
4487 .with_tenant_id(42)
4488 .build_select_with_params();
4489 assert!(
4490 !sql.contains("tenant_id"),
4491 "非多租户模型不应追加 tenant_id: {}",
4492 sql
4493 );
4494 assert_eq!(params.len(), 0);
4495 Ok(())
4496 }
4497
4498 #[test]
4502 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
4503 let dialect = get_dialect(DbType::MySQL)?;
4504 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4505 .table("orders")
4506 .build_select_with_params();
4507 assert!(
4508 !sql.contains("tenant_id"),
4509 "未设置 tenant_id 时不应追加过滤: {}",
4510 sql
4511 );
4512 assert_eq!(params.len(), 0);
4513 Ok(())
4514 }
4515
4516 #[test]
4520 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
4521 let dialect = get_dialect(DbType::MySQL)?;
4522 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
4523 .table("documents")
4524 .with_tenant_id(100)
4525 .where_eq("title", Value::String("report".into()))
4526 .build_select_with_params();
4527 assert!(
4529 sql.contains("`deleted_at` IS NULL"),
4530 "应追加软删除条件: {}",
4531 sql
4532 );
4533 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4535 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
4537 assert_eq!(params.len(), 2);
4539 assert_eq!(params[0], Value::String("report".into()));
4540 assert_eq!(params[1], Value::I64(100));
4541 Ok(())
4542 }
4543
4544 #[test]
4546 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
4547 let dialect = get_dialect(DbType::MySQL)?;
4548 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
4549 .table("documents")
4550 .with_tenant_id(100)
4551 .without_tenant()
4552 .without_soft_delete()
4553 .build_select_with_params();
4554 assert!(
4555 !sql.contains("`deleted_at` IS NULL"),
4556 "应禁用软删除: {}",
4557 sql
4558 );
4559 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
4560 assert_eq!(params.len(), 0);
4561 Ok(())
4562 }
4563
4564 #[test]
4566 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
4567 let dialect = get_dialect(DbType::MySQL)?;
4568 let builder = QueryBuilder::<TenantModel>::new(dialect);
4569 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
4570 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4571 .with_tenant_id(1)
4572 .without_tenant();
4573 assert!(
4574 builder.is_tenant_disabled(),
4575 "without_tenant 后应反映禁用状态"
4576 );
4577 Ok(())
4578 }
4579
4580 #[test]
4584 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
4585 let dialect = get_dialect(DbType::MySQL)?;
4586 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
4587 .table("orders")
4588 .with_tenant_id(42)
4589 .where_eq("id", Value::I64(999))
4590 .build_force_delete_with_params();
4591 assert!(
4594 sql.contains("`tenant_id` = ?"),
4595 "物理删除应保留租户条件: {}",
4596 sql
4597 );
4598 assert_eq!(params.len(), 2);
4599 assert_eq!(params[0], Value::I64(999));
4600 assert_eq!(params[1], Value::I64(42));
4601 Ok(())
4602 }
4603
4604 #[cfg(feature = "multi-tenant-enhanced")]
4608 #[tokio::test]
4609 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
4610 let ctx = crate::tenant_context::TenantContext::new(
4611 99,
4612 crate::tenant_context::IsolationStrategy::RowLevel,
4613 );
4614 ctx.scope(async {
4615 let (sql, params) =
4617 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4618 .table("orders")
4619 .with_tenant_id(42)
4620 .build_select_with_params();
4621 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
4622 assert_eq!(params.len(), 1);
4623 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
4624 })
4625 .await;
4626 Ok(())
4627 }
4628
4629 #[cfg(feature = "multi-tenant-enhanced")]
4631 #[tokio::test]
4632 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
4633 let ctx = crate::tenant_context::TenantContext::new(
4634 77,
4635 crate::tenant_context::IsolationStrategy::RowLevel,
4636 );
4637 ctx.scope(async {
4638 let (sql, params) =
4639 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4640 .table("orders")
4641 .build_select_with_params();
4642 assert!(
4643 sql.contains("`tenant_id` = ?"),
4644 "应从上下文自动追加租户条件: {}",
4645 sql
4646 );
4647 assert_eq!(params.len(), 1);
4648 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
4649 })
4650 .await;
4651 Ok(())
4652 }
4653
4654 #[cfg(feature = "multi-tenant-enhanced")]
4656 #[tokio::test]
4657 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
4658 let ctx = crate::tenant_context::TenantContext::new(
4659 42,
4660 crate::tenant_context::IsolationStrategy::SchemaIsolation,
4661 );
4662 ctx.scope(async {
4663 let (sql, _params) =
4664 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
4665 .table("orders")
4666 .build_select_with_params();
4667 assert!(
4668 sql.contains("tenant_42_orders"),
4669 "Schema 隔离应重写表名: {}",
4670 sql
4671 );
4672 })
4673 .await;
4674 Ok(())
4675 }
4676
4677 #[cfg(feature = "multi-tenant-enhanced")]
4679 #[test]
4680 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
4681 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
4682 .table("orders")
4683 .build_select_with_params();
4684 assert!(
4686 !sql.contains("`tenant_id` = ?"),
4687 "未设置上下文不应追加租户条件: {}",
4688 sql
4689 );
4690 assert_eq!(params.len(), 0);
4691 Ok(())
4692 }
4693
4694 struct TcUsersTable;
4697 impl crate::typed::TypedTable for TcUsersTable {
4698 const NAME: &'static str = "users";
4699 }
4700 struct TcColId;
4701 impl crate::typed::TypedColumn for TcColId {
4702 const NAME: &'static str = "id";
4703 type Table = TcUsersTable;
4704 type RustType = i64;
4705 type SqlType = crate::typed_ast::Untyped;
4706 }
4707 struct TcColName;
4708 impl crate::typed::TypedColumn for TcColName {
4709 const NAME: &'static str = "name";
4710 type Table = TcUsersTable;
4711 type RustType = String;
4712 type SqlType = crate::typed_ast::Untyped;
4713 }
4714
4715 #[test]
4716 fn test_where_eq_typed() -> Result<(), crate::DbError> {
4717 let dialect = get_dialect(DbType::MySQL)?;
4718 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4719 .where_eq_typed::<TcColId>(Value::I64(42))
4720 .build_select_with_params();
4721 assert!(sql.contains("`id` = ?"));
4722 assert_eq!(params[0], Value::I64(42));
4723 Ok(())
4724 }
4725
4726 #[test]
4727 fn test_order_by_typed() -> Result<(), crate::DbError> {
4728 let dialect = get_dialect(DbType::MySQL)?;
4729 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4730 .order_by_typed::<TcColName>()
4731 .build_select_with_params();
4732 assert!(sql.contains("ORDER BY"));
4733 assert!(sql.contains("`name`"));
4734 Ok(())
4735 }
4736
4737 #[test]
4738 fn test_select_typed() -> Result<(), crate::DbError> {
4739 let dialect = get_dialect(DbType::MySQL)?;
4740 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4741 .select_typed::<TcColId>()
4742 .select_typed::<TcColName>()
4743 .build_select_with_params();
4744 assert!(sql.contains("SELECT"));
4745 assert!(sql.contains("id"));
4747 assert!(sql.contains("name"));
4748 Ok(())
4749 }
4750
4751 #[test]
4752 fn test_where_null_typed() -> Result<(), crate::DbError> {
4753 let dialect = get_dialect(DbType::MySQL)?;
4754 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4755 .where_null_typed::<TcColName>()
4756 .build_select_with_params();
4757 assert!(sql.contains("`name` IS NULL"));
4758 Ok(())
4759 }
4760
4761 #[test]
4762 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
4763 let dialect = get_dialect(DbType::MySQL)?;
4764 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4765 .where_not_null_typed::<TcColName>()
4766 .build_select_with_params();
4767 assert!(sql.contains("`name` IS NOT NULL"));
4768 Ok(())
4769 }
4770
4771 #[test]
4772 fn test_group_by_typed() -> Result<(), crate::DbError> {
4773 let dialect = get_dialect(DbType::MySQL)?;
4774 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
4775 .group_by_typed::<TcColName>()
4776 .build_select_with_params();
4777 assert!(sql.contains("GROUP BY"));
4778 assert!(sql.contains("`name`"));
4779 Ok(())
4780 }
4781
4782 #[test]
4783 fn test_where_gt_typed() -> Result<(), crate::DbError> {
4784 let dialect = get_dialect(DbType::MySQL)?;
4785 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4786 .where_gt_typed::<TcColId>(Value::I64(10))
4787 .build_select_with_params();
4788 assert!(sql.contains("`id` > ?"));
4789 assert_eq!(params[0], Value::I64(10));
4790 Ok(())
4791 }
4792
4793 #[test]
4796 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
4797 let dialect = get_dialect(DbType::MySQL)?;
4798 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4799 .table("users")
4800 .where_eq("id", Value::I64(1))
4801 .lock_for_update()?
4802 .build_select_with_params();
4803 assert!(sql.contains("SELECT * FROM `users`"));
4804 assert!(sql.contains("WHERE `id` = ?"));
4805 assert!(sql.contains("FOR UPDATE"));
4806 assert_eq!(params.len(), 1);
4807 assert_eq!(params[0], Value::I64(1));
4808 Ok(())
4809 }
4810
4811 #[test]
4812 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
4813 let dialect = get_dialect(DbType::MySQL)?;
4814 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4815 .table("users")
4816 .where_eq("id", Value::I64(1))
4817 .lock_shared()?
4818 .build_select_with_params();
4819 assert!(sql.contains("SELECT * FROM `users`"));
4820 assert!(sql.contains("WHERE `id` = ?"));
4821 assert!(sql.contains("LOCK IN SHARE MODE"));
4822 assert_eq!(params.len(), 1);
4823 assert_eq!(params[0], Value::I64(1));
4824 Ok(())
4825 }
4826
4827 #[test]
4828 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
4829 let dialect = get_dialect(DbType::PostgreSQL)?;
4830 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4831 .table("users")
4832 .where_eq("id", Value::I64(1))
4833 .lock_for_update()?
4834 .build_select_with_params();
4835 assert!(sql.contains("SELECT * FROM \"users\""));
4836 assert!(sql.contains("WHERE \"id\" = ?"));
4837 assert!(sql.contains("FOR UPDATE"));
4838 assert_eq!(params.len(), 1);
4839 assert_eq!(params[0], Value::I64(1));
4840 Ok(())
4841 }
4842
4843 #[test]
4844 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4845 let dialect = get_dialect(DbType::PostgreSQL)?;
4846 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4847 .table("users")
4848 .where_eq("id", Value::I64(1))
4849 .lock_shared()?
4850 .build_select_with_params();
4851 assert!(sql.contains("SELECT * FROM \"users\""));
4852 assert!(sql.contains("WHERE \"id\" = ?"));
4853 assert!(sql.contains("FOR SHARE"));
4854 assert_eq!(params.len(), 1);
4855 assert_eq!(params[0], Value::I64(1));
4856 Ok(())
4857 }
4858
4859 #[test]
4860 fn test_lock_for_update_sqlite_should_fail() {
4861 let dialect = get_dialect(DbType::Sqlite).unwrap();
4862 let result = QueryBuilder::<TestModel>::new(dialect)
4863 .table("users")
4864 .where_eq("id", Value::I64(1))
4865 .lock_for_update();
4866 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4867 let err = result.err().unwrap();
4868 assert!(
4869 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4870 "错误信息应说明不支持行锁"
4871 );
4872 }
4873
4874 #[test]
4875 fn test_lock_shared_sqlite_should_fail() {
4876 let dialect = get_dialect(DbType::Sqlite).unwrap();
4877 let result = QueryBuilder::<TestModel>::new(dialect)
4878 .table("users")
4879 .where_eq("id", Value::I64(1))
4880 .lock_shared();
4881 assert!(result.is_err(), "SQLite 不应支持共享锁");
4882 let err = result.err().unwrap();
4883 assert!(
4884 format!("{:?}", err).contains("Shared lock is not supported"),
4885 "错误信息应说明不支持共享锁"
4886 );
4887 }
4888
4889 #[test]
4890 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4891 let dialect = get_dialect(DbType::MySQL)?;
4892 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4893 .table("users")
4894 .where_eq("status", Value::String("active".into()))
4895 .limit(10)
4896 .offset(20)
4897 .lock_for_update()?
4898 .build_select_with_params();
4899 assert!(sql.contains("WHERE `status` = ?"));
4900 assert!(sql.contains("LIMIT 10"));
4901 assert!(sql.contains("OFFSET 20"));
4902 assert!(sql.contains("FOR UPDATE"));
4903 assert_eq!(params.len(), 1);
4904 Ok(())
4905 }
4906
4907 #[test]
4910 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4911 let dialect = get_dialect(DbType::MySQL)?;
4912 let mut data = std::collections::HashMap::new();
4913 data.insert("name".to_string(), Value::String("Alice".into()));
4914 data.insert("age".to_string(), Value::I64(30));
4915
4916 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4917 .table("users")
4918 .insert_or_ignore()
4919 .build_insert_with_params(&data);
4920 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4921 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4923 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4924 assert!(sql.contains("VALUES (?, ?)"));
4925 assert_eq!(params.len(), 2);
4926 Ok(())
4927 }
4928
4929 #[test]
4930 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4931 let dialect = get_dialect(DbType::PostgreSQL)?;
4932 let mut data = std::collections::HashMap::new();
4933 data.insert("name".to_string(), Value::String("Bob".into()));
4934
4935 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4936 .table("users")
4937 .insert_or_ignore()
4938 .build_insert_with_params(&data);
4939 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4940 assert!(sql.contains("(\"name\")"));
4941 assert!(sql.contains("VALUES (?)"));
4942 assert_eq!(params.len(), 1);
4943 assert_eq!(params[0], Value::String("Bob".into()));
4944 Ok(())
4945 }
4946
4947 #[test]
4948 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4949 let dialect = get_dialect(DbType::Sqlite)?;
4950 let mut data = std::collections::HashMap::new();
4951 data.insert("name".to_string(), Value::String("Charlie".into()));
4952
4953 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4954 .table("users")
4955 .insert_or_ignore()
4956 .build_insert_with_params(&data);
4957 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4958 assert!(sql.contains("(\"name\")"));
4959 assert!(sql.contains("VALUES (?)"));
4960 assert_eq!(params.len(), 1);
4961 assert_eq!(params[0], Value::String("Charlie".into()));
4962 Ok(())
4963 }
4964
4965 #[test]
4966 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4967 let dialect = get_dialect(DbType::MySQL)?;
4968 let mut data = std::collections::HashMap::new();
4969 data.insert("name".to_string(), Value::String("Dave".into()));
4970
4971 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4972 .table("users")
4973 .build_insert_with_params(&data);
4974 assert!(sql.contains("INSERT INTO `users`"));
4975 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4976 assert_eq!(params.len(), 1);
4977 assert_eq!(params[0], Value::String("Dave".into()));
4978 Ok(())
4979 }
4980
4981 #[test]
4982 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4983 let dialect = get_dialect(DbType::MySQL)?;
4984 let data = std::collections::HashMap::new();
4985
4986 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4987 .table("users")
4988 .insert_or_ignore()
4989 .build_insert_with_params(&data);
4990 assert!(sql.is_empty(), "空数据应返回空 SQL");
4991 assert!(params.is_empty());
4992 Ok(())
4993 }
4994
4995 #[test]
4998 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4999 let mysql = get_dialect(DbType::MySQL)?;
5000 let pg = get_dialect(DbType::PostgreSQL)?;
5001 let sqlite = get_dialect(DbType::Sqlite)?;
5002 let clickhouse = get_dialect(DbType::ClickHouse)?;
5003 let duckdb = get_dialect(DbType::DuckDB)?;
5004
5005 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
5006 assert!(
5007 pg.supports_lock_for_update(),
5008 "PostgreSQL 应支持 FOR UPDATE"
5009 );
5010 assert!(
5011 !sqlite.supports_lock_for_update(),
5012 "SQLite 不应支持 FOR UPDATE"
5013 );
5014 assert!(
5015 !clickhouse.supports_lock_for_update(),
5016 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
5017 );
5018 assert!(
5019 !duckdb.supports_lock_for_update(),
5020 "DuckDB 不应支持 FOR UPDATE"
5021 );
5022 Ok(())
5023 }
5024
5025 #[test]
5026 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
5027 let mysql = get_dialect(DbType::MySQL)?;
5028 let pg = get_dialect(DbType::PostgreSQL)?;
5029 let sqlite = get_dialect(DbType::Sqlite)?;
5030 let clickhouse = get_dialect(DbType::ClickHouse)?;
5031 let duckdb = get_dialect(DbType::DuckDB)?;
5032
5033 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
5034 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
5035 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
5036 assert!(
5037 !clickhouse.supports_lock_shared(),
5038 "ClickHouse 是列式 OLAP,不应支持共享锁"
5039 );
5040 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
5041 Ok(())
5042 }
5043
5044 #[test]
5045 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
5046 let dialect = get_dialect(DbType::MySQL)?;
5047
5048 let builder = QueryBuilder::<TestModel>::new(dialect);
5050 assert!(builder.get_lock_type().is_none(), "默认无锁");
5051 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
5052
5053 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
5055 .table("users")
5056 .lock_for_update()?;
5057 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
5058
5059 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
5061 .table("users")
5062 .insert_or_ignore();
5063 assert!(builder.is_insert_or_ignore());
5064
5065 Ok(())
5066 }
5067
5068 struct TestModelWithColumns;
5069 impl Model for TestModelWithColumns {
5070 type PrimaryKey = i64;
5071 fn table_name() -> &'static str {
5072 "test_with_cols"
5073 }
5074 fn pk(&self) -> Self::PrimaryKey {
5075 0
5076 }
5077 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
5078 }
5079 impl crate::model::ModelExt for TestModelWithColumns {
5080 fn columns() -> Vec<&'static str> {
5081 vec!["id", "name", "email", "avatar", "blob_data"]
5082 }
5083 fn fillable() -> Vec<&'static str> {
5084 vec!["name", "email", "avatar", "blob_data"]
5085 }
5086 fn guarded() -> Vec<&'static str> {
5087 vec!["id"]
5088 }
5089 fn hidden() -> Vec<&'static str> {
5090 vec!["blob_data"]
5091 }
5092 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
5093 std::collections::HashMap::new()
5094 }
5095 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
5096 fn to_json(&self) -> serde_json::Value {
5097 serde_json::Value::Null
5098 }
5099 }
5100
5101 #[test]
5102 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
5103 let dialect = get_dialect(DbType::MySQL)?;
5104 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
5105 .table("users")
5106 .select_exclude(&["avatar", "blob_data"])?;
5107 let sql = builder.sql();
5108 assert!(sql.contains("id"));
5109 assert!(sql.contains("name"));
5110 assert!(sql.contains("email"));
5111 assert!(!sql.contains("avatar"));
5112 assert!(!sql.contains("blob_data"));
5113 Ok(())
5114 }
5115
5116 #[test]
5117 fn test_select_exclude_nonexistent_field() {
5118 let dialect = get_dialect(DbType::MySQL).unwrap();
5119 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
5120 .table("users")
5121 .select_exclude(&["nonexistent"]);
5122 assert!(result.is_err());
5123 }
5124
5125 #[test]
5126 fn test_select_exclude_all_fields() {
5127 let dialect = get_dialect(DbType::MySQL).unwrap();
5128 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
5129 .table("users")
5130 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
5131 assert!(result.is_err());
5132 let err = result.unwrap_err();
5133 assert!(matches!(err, crate::DbError::InvalidInput(_)));
5134 }
5135
5136 #[tokio::test]
5139 async fn test_execute_with_cache_ttl_hit() {
5140 use crate::l2_cache::L2Cache;
5141 use std::sync::atomic::{AtomicU64, Ordering};
5142 use std::sync::Arc;
5143 use std::time::Duration;
5144
5145 let dialect = get_dialect(DbType::MySQL).unwrap();
5146 let cache = L2Cache::new();
5147 let qb = QueryBuilder::<TestModel>::new(dialect)
5148 .table("users")
5149 .where_eq("status", Value::I64(1))
5150 .cache_ttl(Duration::from_secs(300));
5151
5152 let call_count = Arc::new(AtomicU64::new(0));
5153 let cc1 = Arc::clone(&call_count);
5154 let rows1 = qb
5155 .execute_with_cache(&cache, "users", || async move {
5156 cc1.fetch_add(1, Ordering::SeqCst);
5157 let mut row = std::collections::HashMap::new();
5158 row.insert("id".to_string(), Value::I64(1));
5159 Ok(vec![row])
5160 })
5161 .await
5162 .unwrap();
5163 assert_eq!(rows1.len(), 1);
5164 assert_eq!(call_count.load(Ordering::SeqCst), 1);
5165
5166 let cc2 = Arc::clone(&call_count);
5167 let rows2 = qb
5168 .execute_with_cache(&cache, "users", || async move {
5169 cc2.fetch_add(1, Ordering::SeqCst);
5170 let mut row = std::collections::HashMap::new();
5171 row.insert("id".to_string(), Value::I64(2));
5172 Ok(vec![row])
5173 })
5174 .await
5175 .unwrap();
5176 assert_eq!(rows2.len(), 1);
5177 assert_eq!(rows2[0].get("id"), Some(&Value::I64(1)));
5178 assert_eq!(
5179 call_count.load(Ordering::SeqCst),
5180 1,
5181 "second call should hit cache, not loader"
5182 );
5183 }
5184
5185 #[tokio::test]
5186 async fn test_execute_with_cache_no_ttl_bypasses_cache() {
5187 use crate::l2_cache::L2Cache;
5188 use std::sync::atomic::{AtomicU64, Ordering};
5189 use std::sync::Arc;
5190
5191 let dialect = get_dialect(DbType::MySQL).unwrap();
5192 let cache = L2Cache::new();
5193 let qb = QueryBuilder::<TestModel>::new(dialect).table("users");
5194
5195 let call_count = Arc::new(AtomicU64::new(0));
5196 let cc1 = Arc::clone(&call_count);
5197 let rows1 = qb
5198 .execute_with_cache(&cache, "users", || async move {
5199 cc1.fetch_add(1, Ordering::SeqCst);
5200 Ok(vec![])
5201 })
5202 .await
5203 .unwrap();
5204 assert!(rows1.is_empty());
5205
5206 let cc2 = Arc::clone(&call_count);
5207 let rows2 = qb
5208 .execute_with_cache(&cache, "users", || async move {
5209 cc2.fetch_add(1, Ordering::SeqCst);
5210 Ok(vec![])
5211 })
5212 .await
5213 .unwrap();
5214 assert!(rows2.is_empty());
5215 assert_eq!(
5216 call_count.load(Ordering::SeqCst),
5217 2,
5218 "without ttl, loader should be called each time"
5219 );
5220 }
5221
5222 #[test]
5223 fn test_push_usize_to_string() {
5224 let cases = [
5225 0usize,
5226 1,
5227 9,
5228 10,
5229 99,
5230 100,
5231 999,
5232 1000,
5233 9999,
5234 10000,
5235 99999999,
5236 usize::MAX,
5237 ];
5238 for n in cases {
5239 let mut buf = String::new();
5240 push_usize_to_string(n, &mut buf);
5241 assert_eq!(buf, n.to_string(), "failed for n={}", n);
5242 }
5243 }
5244
5245 struct MockConnection {
5246 data: Vec<std::collections::HashMap<String, Value>>,
5247 query_call_count: std::sync::atomic::AtomicUsize,
5248 }
5249
5250 impl MockConnection {
5251 fn new(data: Vec<std::collections::HashMap<String, Value>>) -> Self {
5252 Self {
5253 data,
5254 query_call_count: std::sync::atomic::AtomicUsize::new(0),
5255 }
5256 }
5257
5258 fn call_count(&self) -> usize {
5259 self.query_call_count
5260 .load(std::sync::atomic::Ordering::SeqCst)
5261 }
5262 }
5263
5264 impl crate::pool::Connection for MockConnection {
5265 fn execute<'a>(
5266 &'a mut self,
5267 sql: &'a str,
5268 ) -> std::pin::Pin<
5269 Box<dyn std::future::Future<Output = Result<u64, crate::DbError>> + Send + 'a>,
5270 > {
5271 let _ = sql;
5272 Box::pin(async move { Ok(0) })
5273 }
5274 fn query<'a>(
5275 &'a mut self,
5276 sql: &'a str,
5277 ) -> std::pin::Pin<
5278 Box<
5279 dyn std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>
5280 + Send
5281 + 'a,
5282 >,
5283 > {
5284 let _ = sql;
5285 Box::pin(async move { Ok(Vec::new()) })
5286 }
5287 fn begin_transaction<'a>(
5288 &'a mut self,
5289 ) -> std::pin::Pin<
5290 Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
5291 > {
5292 Box::pin(async move { Ok(()) })
5293 }
5294 fn commit<'a>(
5295 &'a mut self,
5296 ) -> std::pin::Pin<
5297 Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
5298 > {
5299 Box::pin(async move { Ok(()) })
5300 }
5301 fn rollback<'a>(
5302 &'a mut self,
5303 ) -> std::pin::Pin<
5304 Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
5305 > {
5306 Box::pin(async move { Ok(()) })
5307 }
5308 fn is_connected(&self) -> bool {
5309 true
5310 }
5311 fn ping<'a>(
5312 &'a mut self,
5313 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
5314 Box::pin(async move { true })
5315 }
5316 fn close<'a>(
5317 &'a mut self,
5318 ) -> std::pin::Pin<
5319 Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
5320 > {
5321 Box::pin(async move { Ok(()) })
5322 }
5323 fn query_with_params<'a>(
5324 &'a mut self,
5325 sql: &'a str,
5326 params: &'a [Value],
5327 ) -> std::pin::Pin<
5328 Box<
5329 dyn std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>
5330 + Send
5331 + 'a,
5332 >,
5333 > {
5334 self.query_call_count
5335 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5336 let _ = sql;
5337 let id_set: std::collections::HashSet<i64> =
5338 params.iter().filter_map(|v| v.as_i64()).collect();
5339 let rows: Vec<std::collections::HashMap<String, Value>> = self
5340 .data
5341 .iter()
5342 .filter(|row| {
5343 row.get("id")
5344 .and_then(|v| v.as_i64())
5345 .is_some_and(|id| id_set.contains(&id))
5346 })
5347 .cloned()
5348 .collect();
5349 Box::pin(async move { Ok(rows) })
5350 }
5351 }
5352
5353 fn make_test_rows(n: usize) -> Vec<std::collections::HashMap<String, Value>> {
5354 (1..=n)
5355 .map(|i| {
5356 let mut row = std::collections::HashMap::new();
5357 row.insert("id".to_string(), Value::I64(i as i64));
5358 row.insert("name".to_string(), Value::String(format!("user{}", i)));
5359 row
5360 })
5361 .collect()
5362 }
5363
5364 #[tokio::test]
5365 async fn test_find_by_ids_empty() {
5366 let mut conn = MockConnection::new(make_test_rows(10));
5367 let qb = QueryBuilder::<TestModel>::new(Box::new(crate::dialect::SqliteDialect));
5368 let rows = qb.find_by_ids(&mut conn, &[]).await.unwrap();
5369 assert!(rows.is_empty());
5370 }
5371
5372 #[tokio::test]
5373 async fn test_find_by_ids_dedup() {
5374 let mut conn = MockConnection::new(make_test_rows(10));
5375 let qb = QueryBuilder::<TestModel>::new(Box::new(crate::dialect::SqliteDialect));
5376 let rows = qb.find_by_ids(&mut conn, &[1, 1, 2, 2, 3]).await.unwrap();
5377 assert_eq!(rows.len(), 3);
5378 assert_eq!(conn.call_count(), 1);
5379 }
5380
5381 #[tokio::test]
5382 async fn test_find_by_ids_chunk_999() {
5383 let mut conn = MockConnection::new(make_test_rows(999));
5384 let qb = QueryBuilder::<TestModel>::new(Box::new(crate::dialect::SqliteDialect));
5385 let ids: Vec<i64> = (1..=999).collect();
5386 let rows = qb.find_by_ids(&mut conn, &ids).await.unwrap();
5387 assert_eq!(rows.len(), 999);
5388 assert_eq!(conn.call_count(), 1);
5389 }
5390
5391 #[tokio::test]
5392 async fn test_find_by_ids_chunk_1000() {
5393 let mut conn = MockConnection::new(make_test_rows(1000));
5394 let qb = QueryBuilder::<TestModel>::new(Box::new(crate::dialect::SqliteDialect));
5395 let ids: Vec<i64> = (1..=1000).collect();
5396 let rows = qb.find_by_ids(&mut conn, &ids).await.unwrap();
5397 assert_eq!(rows.len(), 1000);
5398 assert_eq!(conn.call_count(), 2);
5399 }
5400
5401 #[tokio::test]
5402 async fn test_find_by_ids_chunk_2000() {
5403 let mut conn = MockConnection::new(make_test_rows(2000));
5404 let qb = QueryBuilder::<TestModel>::new(Box::new(crate::dialect::SqliteDialect));
5405 let ids: Vec<i64> = (1..=2000).collect();
5406 let rows = qb.find_by_ids(&mut conn, &ids).await.unwrap();
5407 assert_eq!(rows.len(), 2000);
5408 assert_eq!(conn.call_count(), 3);
5409 }
5410}