1use crate::db_type::DbType;
27use crate::dialect::Dialect;
28use crate::dialect::LockType;
29use crate::model::Model;
30use crate::typed::TypedColumn;
31use crate::value::Value;
32use std::fmt;
33use std::time::Duration;
34
35pub struct QueryBuilder<M: Model> {
37 table: Option<String>,
38 select_columns: Vec<String>,
39 select_mode: crate::partial_model::SelectMode,
40 where_conditions: Vec<WhereCondition>,
41 order_by: Vec<OrderClause>,
42 group_by: Vec<String>,
43 having_conditions: Vec<WhereCondition>,
44 limit_value: Option<usize>,
45 offset_value: Option<usize>,
46 joins: Vec<JoinClause>,
47 dialect: Box<dyn Dialect>,
48 soft_delete_disabled: bool,
50 tenant_id_value: Option<i64>,
52 tenant_disabled: bool,
54 keyset_cursor: Option<KeysetCursor>,
60 cache_ttl: Option<Duration>,
65 lock_type: Option<LockType>,
73 insert_or_ignore: bool,
78 #[allow(dead_code)]
79 model: std::marker::PhantomData<M>,
80}
81
82#[derive(Debug, Clone)]
90struct KeysetCursor {
91 field: String,
93 value: Value,
95 direction: KeysetDirection,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101enum KeysetDirection {
102 After,
104 Before,
106}
107
108#[derive(Debug, Clone)]
109#[allow(dead_code)]
110enum WhereCondition {
111 And(String),
116 Or(String),
118 Eq(String, Value),
120 Ne(String, Value),
122 Gt(String, Value),
124 Ge(String, Value),
126 Lt(String, Value),
128 Le(String, Value),
130 Like(String, Value),
132 OrEq(String, Value),
134 OrNe(String, Value),
136 OrGt(String, Value),
138 OrGe(String, Value),
140 OrLt(String, Value),
142 OrLe(String, Value),
144 OrLike(String, Value),
146 In(String, Vec<Value>),
147 NotIn(String, Vec<Value>),
148 Between(String, Value, Value),
149 NotBetween(String, Value, Value),
150 Null(String),
151 NotNull(String),
152 Exists(String),
153 NotExists(String),
154 TypedExpr(String, Vec<Value>),
156}
157
158#[derive(Debug, Clone)]
159struct OrderClause {
160 field: String,
161 direction: OrderDirection,
162}
163
164#[derive(Debug, Clone)]
165enum OrderDirection {
166 Asc,
167 Desc,
168}
169
170#[derive(Debug, Clone)]
171#[allow(dead_code)]
172enum JoinClause {
173 Inner(String, String, String),
174 Left(String, String, String),
175 Right(String, String, String),
176 Cross(String, String),
177 Relation(
180 crate::relation_trait::JoinKind,
181 String,
182 String,
183 String,
184 String,
185 ),
186}
187
188impl<M: Model> QueryBuilder<M> {
189 pub fn new(dialect: Box<dyn Dialect>) -> Self {
191 Self {
192 table: None,
193 select_columns: vec!["*".to_string()],
194 select_mode: crate::partial_model::SelectMode::All,
195 where_conditions: Vec::new(),
196 order_by: Vec::new(),
197 group_by: Vec::new(),
198 having_conditions: Vec::new(),
199 limit_value: None,
200 offset_value: None,
201 joins: Vec::new(),
202 dialect,
203 soft_delete_disabled: false,
204 tenant_id_value: None,
205 tenant_disabled: false,
206 keyset_cursor: None,
207 cache_ttl: None,
208 lock_type: None,
209 insert_or_ignore: false,
210 model: std::marker::PhantomData,
211 }
212 }
213
214 pub fn table(mut self, table: impl Into<String>) -> Self {
216 let table_name = table.into();
217 #[cfg(feature = "multi-tenant-enhanced")]
219 {
220 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
221 if ctx.isolation_strategy
222 == crate::tenant_context::IsolationStrategy::SchemaIsolation
223 {
224 self.table = Some(crate::tenant_context::SchemaIsolationRouter::rewrite_table(
225 &table_name,
226 ctx.tenant_id,
227 ));
228 return self;
229 }
230 }
231 }
232 self.table = Some(table_name);
233 self
234 }
235
236 pub fn without_soft_delete(mut self) -> Self {
255 self.soft_delete_disabled = true;
256 self
257 }
258
259 pub fn is_soft_delete_disabled(&self) -> bool {
261 self.soft_delete_disabled
262 }
263
264 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
284 self.cache_ttl = Some(ttl);
285 self
286 }
287
288 pub fn get_cache_ttl(&self) -> Option<Duration> {
290 self.cache_ttl
291 }
292
293 pub fn lock_for_update(mut self) -> Result<Self, crate::error::DbError> {
318 if !self.dialect.supports_lock_for_update() {
319 return Err(crate::error::DbError::QueryError(
320 "FOR UPDATE lock is not supported by this dialect".to_string(),
321 ));
322 }
323 self.lock_type = Some(LockType::ForUpdate);
324 Ok(self)
325 }
326
327 pub fn lock_shared(mut self) -> Result<Self, crate::error::DbError> {
352 if !self.dialect.supports_lock_shared() {
353 return Err(crate::error::DbError::QueryError(
354 "Shared lock is not supported by this dialect".to_string(),
355 ));
356 }
357 self.lock_type = Some(LockType::Shared);
358 Ok(self)
359 }
360
361 pub fn insert_or_ignore(mut self) -> Self {
380 self.insert_or_ignore = true;
381 self
382 }
383
384 pub fn get_lock_type(&self) -> Option<LockType> {
386 self.lock_type
387 }
388
389 pub fn is_insert_or_ignore(&self) -> bool {
391 self.insert_or_ignore
392 }
393
394 pub fn clone_for_count(&self) -> Self {
401 Self {
402 table: self.table.clone(),
403 select_columns: vec!["*".to_string()],
404 select_mode: crate::partial_model::SelectMode::All,
405 where_conditions: self.where_conditions.clone(),
406 order_by: Vec::new(),
407 group_by: self.group_by.clone(),
408 having_conditions: self.having_conditions.clone(),
409 limit_value: None,
410 offset_value: None,
411 joins: self.joins.clone(),
412 dialect: self.dialect.clone_box(),
413 soft_delete_disabled: self.soft_delete_disabled,
414 tenant_id_value: self.tenant_id_value,
415 tenant_disabled: self.tenant_disabled,
416 keyset_cursor: None,
417 cache_ttl: None, lock_type: None, insert_or_ignore: false, model: std::marker::PhantomData,
421 }
422 }
423
424 fn soft_delete_field(&self) -> Option<&'static str> {
428 if self.soft_delete_disabled {
429 return None;
430 }
431 M::soft_delete_field()
432 }
433
434 fn build_soft_delete_condition(&self) -> Option<String> {
438 self.soft_delete_field()
439 .map(|field| format!("{} IS NULL", self.dialect.quote(field)))
440 }
441
442 pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
469 self.tenant_id_value = Some(tenant_id);
470 self
471 }
472
473 pub fn without_tenant(mut self) -> Self {
477 self.tenant_disabled = true;
478 self
479 }
480
481 pub fn is_tenant_disabled(&self) -> bool {
483 self.tenant_disabled
484 }
485
486 fn tenant_field(&self) -> Option<&'static str> {
490 if self.tenant_disabled {
491 return None;
492 }
493 M::tenant_field()
494 }
495
496 fn tenant_id_value(&self) -> Option<i64> {
502 if self.tenant_disabled {
503 return None;
504 }
505 if let Some(tid) = self.tenant_id_value {
506 return Some(tid);
507 }
508 #[cfg(feature = "multi-tenant-enhanced")]
509 {
510 if let Some(ctx) = crate::tenant_context::TenantContext::current() {
511 return Some(ctx.tenant_id);
512 }
513 }
514 None
515 }
516
517 fn build_tenant_condition(&self) -> Option<(String, Value)> {
522 let field = self.tenant_field()?;
523 let tid = self.tenant_id_value()?;
524 Some((
525 format!("{} = ?", self.dialect.quote(field)),
526 Value::I64(tid),
527 ))
528 }
529
530 #[cfg(feature = "multi-tenant-enhanced")]
536 #[allow(dead_code)]
537 fn require_tenant_condition(&self) -> Result<Option<(String, Value)>, crate::DbError> {
538 if self.tenant_field().is_none() {
539 return Ok(None);
540 }
541 if self.tenant_id_value.is_some() {
542 return Ok(self.build_tenant_condition());
543 }
544 if crate::tenant_context::TenantContext::current().is_some() {
545 return Ok(self.build_tenant_condition());
546 }
547 Err(crate::DbError::TenantError(
548 "TenantContextRequired: multi-tenant-enhanced feature enabled but no tenant context set"
549 .to_string(),
550 ))
551 }
552
553 pub fn select(mut self, columns: Vec<&str>) -> Self {
561 self.select_columns = columns.into_iter().map(|s| s.to_string()).collect();
562 self
563 }
564
565 pub fn select_quoted(mut self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
574 let mut quoted = Vec::with_capacity(columns.len());
575 for col in columns {
576 crate::sql_safety::validate_identifier(col, "select column")?;
577 quoted.push(self.dialect.quote(col));
578 }
579 self.select_columns = quoted;
580 Ok(self)
581 }
582
583 pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
597 self.where_conditions
598 .push(WhereCondition::Eq(field.into(), value));
599 self
600 }
601
602 #[cfg(feature = "type-safe-columns")]
607 pub fn where_eq_col<T: crate::column::Schema>(
608 mut self,
609 col: crate::column::Column<T>,
610 value: Value,
611 ) -> Self {
612 self.where_conditions
613 .push(WhereCondition::Eq(col.name().to_string(), value));
614 self
615 }
616
617 #[cfg(feature = "type-safe-columns")]
622 pub fn where_expr<E: crate::typed_ast::TypedExpression<SqlType = crate::typed_ast::Bool>>(
623 mut self,
624 expr: E,
625 ) -> Self {
626 let (sql, params) = expr.to_sql(&*self.dialect);
627 let values: Vec<Value> = params.into_iter().map(Value::String).collect();
628 self.where_conditions
629 .push(WhereCondition::TypedExpr(sql, values));
630 self
631 }
632
633 pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
635 self.where_conditions
636 .push(WhereCondition::Ne(field.into(), value));
637 self
638 }
639
640 pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
642 self.where_conditions
643 .push(WhereCondition::Gt(field.into(), value));
644 self
645 }
646
647 pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
649 self.where_conditions
650 .push(WhereCondition::Ge(field.into(), value));
651 self
652 }
653
654 pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
656 self.where_conditions
657 .push(WhereCondition::Lt(field.into(), value));
658 self
659 }
660
661 pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
663 self.where_conditions
664 .push(WhereCondition::Le(field.into(), value));
665 self
666 }
667
668 pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
680 self.where_conditions
681 .push(WhereCondition::Like(field.into(), pattern));
682 self
683 }
684
685 pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
689 self.where_conditions
690 .push(WhereCondition::OrEq(field.into(), value));
691 self
692 }
693
694 pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
696 self.where_conditions
697 .push(WhereCondition::OrNe(field.into(), value));
698 self
699 }
700
701 pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
703 self.where_conditions
704 .push(WhereCondition::OrGt(field.into(), value));
705 self
706 }
707
708 pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
710 self.where_conditions
711 .push(WhereCondition::OrGe(field.into(), value));
712 self
713 }
714
715 pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
717 self.where_conditions
718 .push(WhereCondition::OrLt(field.into(), value));
719 self
720 }
721
722 pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
724 self.where_conditions
725 .push(WhereCondition::OrLe(field.into(), value));
726 self
727 }
728
729 pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
731 self.where_conditions
732 .push(WhereCondition::OrLike(field.into(), pattern));
733 self
734 }
735
736 pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
738 self.where_conditions
739 .push(WhereCondition::In(field.into(), values));
740 self
741 }
742
743 pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
745 self.where_conditions
746 .push(WhereCondition::NotIn(field.into(), values));
747 self
748 }
749
750 pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
752 self.where_conditions
753 .push(WhereCondition::Between(field.into(), start, end));
754 self
755 }
756
757 pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
759 self.where_conditions
760 .push(WhereCondition::NotBetween(field.into(), start, end));
761 self
762 }
763
764 pub fn where_null(mut self, field: impl Into<String>) -> Self {
766 self.where_conditions
767 .push(WhereCondition::Null(field.into()));
768 self
769 }
770
771 pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
773 self.where_conditions
774 .push(WhereCondition::NotNull(field.into()));
775 self
776 }
777
778 pub fn order_by(mut self, field: impl Into<String>) -> Self {
780 self.order_by.push(OrderClause {
781 field: field.into(),
782 direction: OrderDirection::Asc,
783 });
784 self
785 }
786
787 pub fn order_desc(mut self, field: impl Into<String>) -> Self {
789 self.order_by.push(OrderClause {
790 field: field.into(),
791 direction: OrderDirection::Desc,
792 });
793 self
794 }
795
796 pub fn group_by(mut self, field: impl Into<String>) -> Self {
798 self.group_by.push(field.into());
799 self
800 }
801
802 pub fn where_eq_typed<C: TypedColumn>(mut self, value: Value) -> Self {
821 self.where_conditions
822 .push(WhereCondition::Eq(C::NAME.to_string(), value));
823 self
824 }
825
826 pub fn where_ne_typed<C: TypedColumn>(mut self, value: Value) -> Self {
828 self.where_conditions
829 .push(WhereCondition::Ne(C::NAME.to_string(), value));
830 self
831 }
832
833 pub fn where_gt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
835 self.where_conditions
836 .push(WhereCondition::Gt(C::NAME.to_string(), value));
837 self
838 }
839
840 pub fn where_ge_typed<C: TypedColumn>(mut self, value: Value) -> Self {
842 self.where_conditions
843 .push(WhereCondition::Ge(C::NAME.to_string(), value));
844 self
845 }
846
847 pub fn where_lt_typed<C: TypedColumn>(mut self, value: Value) -> Self {
849 self.where_conditions
850 .push(WhereCondition::Lt(C::NAME.to_string(), value));
851 self
852 }
853
854 pub fn where_le_typed<C: TypedColumn>(mut self, value: Value) -> Self {
856 self.where_conditions
857 .push(WhereCondition::Le(C::NAME.to_string(), value));
858 self
859 }
860
861 pub fn where_null_typed<C: TypedColumn>(mut self) -> Self {
863 self.where_conditions
864 .push(WhereCondition::Null(C::NAME.to_string()));
865 self
866 }
867
868 pub fn where_not_null_typed<C: TypedColumn>(mut self) -> Self {
870 self.where_conditions
871 .push(WhereCondition::NotNull(C::NAME.to_string()));
872 self
873 }
874
875 pub fn order_by_typed<C: TypedColumn>(mut self) -> Self {
877 self.order_by.push(OrderClause {
878 field: C::NAME.to_string(),
879 direction: OrderDirection::Asc,
880 });
881 self
882 }
883
884 pub fn order_desc_typed<C: TypedColumn>(mut self) -> Self {
886 self.order_by.push(OrderClause {
887 field: C::NAME.to_string(),
888 direction: OrderDirection::Desc,
889 });
890 self
891 }
892
893 pub fn group_by_typed<C: TypedColumn>(mut self) -> Self {
895 self.group_by.push(C::NAME.to_string());
896 self
897 }
898
899 pub fn select_typed<C: TypedColumn>(mut self) -> Self {
903 self.select_columns.push(C::NAME.to_string());
904 self
905 }
906
907 pub fn select_typed_cols<C: TypedColumn, const N: usize>(mut self) -> Self {
909 self.select_columns.push(C::NAME.to_string());
912 self
913 }
914
915 pub fn having(mut self, condition: impl Into<String>) -> Self {
917 self.having_conditions
918 .push(WhereCondition::And(condition.into()));
919 self
920 }
921
922 pub fn limit(mut self, limit: usize) -> Self {
924 self.limit_value = Some(limit);
925 self
926 }
927
928 pub fn offset(mut self, offset: usize) -> Self {
930 self.offset_value = Some(offset);
931 self
932 }
933
934 pub fn page(mut self, page: usize, page_size: usize) -> Self {
936 self.limit_value = Some(page_size);
937 self.offset_value = Some((page.saturating_sub(1)) * page_size);
938 self
939 }
940
941 pub fn keyset_after(
987 mut self,
988 field: impl Into<String>,
989 cursor_value: Value,
990 page_size: usize,
991 ) -> Self {
992 let field_str = field.into();
993 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
995 existing.direction = OrderDirection::Asc;
996 } else {
997 self.order_by.push(OrderClause {
998 field: field_str.clone(),
999 direction: OrderDirection::Asc,
1000 });
1001 }
1002 self.limit_value = Some(page_size);
1003 self.offset_value = None;
1005 self.keyset_cursor = Some(KeysetCursor {
1006 field: field_str,
1007 value: cursor_value,
1008 direction: KeysetDirection::After,
1009 });
1010 self
1011 }
1012
1013 pub fn keyset_before(
1059 mut self,
1060 field: impl Into<String>,
1061 cursor_value: Value,
1062 page_size: usize,
1063 ) -> Self {
1064 let field_str = field.into();
1065 if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
1067 existing.direction = OrderDirection::Desc;
1068 } else {
1069 self.order_by.push(OrderClause {
1070 field: field_str.clone(),
1071 direction: OrderDirection::Desc,
1072 });
1073 }
1074 self.limit_value = Some(page_size);
1075 self.offset_value = None;
1076 self.keyset_cursor = Some(KeysetCursor {
1077 field: field_str,
1078 value: cursor_value,
1079 direction: KeysetDirection::Before,
1080 });
1081 self
1082 }
1083
1084 pub fn join_inner(
1086 mut self,
1087 table: impl Into<String>,
1088 on_left: impl Into<String>,
1089 on_right: impl Into<String>,
1090 ) -> Self {
1091 self.joins.push(JoinClause::Inner(
1092 table.into(),
1093 on_left.into(),
1094 on_right.into(),
1095 ));
1096 self
1097 }
1098
1099 pub fn join_left(
1101 mut self,
1102 table: impl Into<String>,
1103 on_left: impl Into<String>,
1104 on_right: impl Into<String>,
1105 ) -> Self {
1106 self.joins.push(JoinClause::Left(
1107 table.into(),
1108 on_left.into(),
1109 on_right.into(),
1110 ));
1111 self
1112 }
1113
1114 pub fn join_right(
1116 mut self,
1117 table: impl Into<String>,
1118 on_left: impl Into<String>,
1119 on_right: impl Into<String>,
1120 ) -> Self {
1121 self.joins.push(JoinClause::Right(
1122 table.into(),
1123 on_left.into(),
1124 on_right.into(),
1125 ));
1126 self
1127 }
1128
1129 pub fn join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1149 let def = relation.def();
1150 let join_kind = def.kind.default_join_type();
1151 self.joins.push(JoinClause::Relation(
1152 join_kind,
1153 def.from_entity.to_string(),
1154 def.from_key.to_string(),
1155 def.to_entity.to_string(),
1156 def.to_key.to_string(),
1157 ));
1158 self
1159 }
1160
1161 pub fn left_join(mut self, relation: &dyn crate::relation_trait::RelationTrait) -> Self {
1165 let def = relation.def();
1166 self.joins.push(JoinClause::Relation(
1167 crate::relation_trait::JoinKind::Left,
1168 def.from_entity.to_string(),
1169 def.from_key.to_string(),
1170 def.to_entity.to_string(),
1171 def.to_key.to_string(),
1172 ));
1173 self
1174 }
1175
1176 pub fn select_only(mut self) -> Self {
1192 self.select_mode = crate::partial_model::SelectMode::Partial;
1193 self.select_columns.clear();
1194 self
1195 }
1196
1197 pub fn column(mut self, column: impl Into<String>) -> Self {
1201 self.select_columns.push(column.into());
1202 self
1203 }
1204
1205 pub fn columns(mut self, cols: Vec<impl Into<String>>) -> Self {
1207 self.select_columns.extend(cols.into_iter().map(Into::into));
1208 self
1209 }
1210
1211 pub fn column_as(mut self, expr: crate::partial_model::Expr, alias: impl Into<String>) -> Self {
1227 self.select_columns.push(expr.render_as(&alias.into()));
1228 self
1229 }
1230
1231 #[tracing::instrument(skip(self), fields(op = "select"))]
1264 pub fn build_select(&self) -> String {
1265 let table = self
1266 .table
1267 .clone()
1268 .unwrap_or_else(|| M::table_name().to_string());
1269
1270 let columns = if self.select_columns.is_empty() {
1271 "*".to_string()
1272 } else {
1273 self.select_columns.join(", ")
1274 };
1275
1276 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1277 "SELECT {} FROM {}",
1278 columns,
1279 self.dialect.quote(&table)
1280 ));
1281
1282 for join in &self.joins {
1283 match join {
1284 JoinClause::Inner(t, l, r) => {
1285 sql.push_str(&format!(
1286 " INNER JOIN {} ON {} = {}",
1287 self.dialect.quote(t),
1288 self.dialect.quote(l),
1289 self.dialect.quote(r)
1290 ));
1291 }
1292 JoinClause::Left(t, l, r) => {
1293 sql.push_str(&format!(
1294 " LEFT JOIN {} ON {} = {}",
1295 self.dialect.quote(t),
1296 self.dialect.quote(l),
1297 self.dialect.quote(r)
1298 ));
1299 }
1300 JoinClause::Right(t, l, r) => {
1301 sql.push_str(&format!(
1302 " RIGHT JOIN {} ON {} = {}",
1303 self.dialect.quote(t),
1304 self.dialect.quote(l),
1305 self.dialect.quote(r)
1306 ));
1307 }
1308 JoinClause::Cross(t, on) => {
1309 sql.push_str(&format!(
1310 " CROSS JOIN {} ON {}",
1311 self.dialect.quote(t),
1312 self.dialect.quote(on)
1313 ));
1314 }
1315 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1316 sql.push_str(&format!(
1317 " {} {} ON {}.{} = {}.{}",
1318 kind.as_sql(),
1319 self.dialect.quote(tt),
1320 self.dialect.quote(ft),
1321 self.dialect.quote(fk),
1322 self.dialect.quote(tt),
1323 self.dialect.quote(tk)
1324 ));
1325 }
1326 }
1327 }
1328
1329 let where_clause = self.build_where_clause();
1331 if !where_clause.is_empty() {
1332 sql.push_str(&where_clause);
1333 }
1334
1335 if !self.group_by.is_empty() {
1336 let cols: Vec<String> = self
1337 .group_by
1338 .iter()
1339 .map(|c| self.dialect.quote(c))
1340 .collect();
1341 sql.push_str(" GROUP BY ");
1342 sql.push_str(&cols.join(", "));
1343 }
1344
1345 if !self.having_conditions.is_empty() {
1346 sql.push_str(" HAVING ");
1347 for (i, cond) in self.having_conditions.iter().enumerate() {
1348 if i > 0 {
1349 sql.push_str(" AND ");
1350 }
1351 if let WhereCondition::And(c) = cond {
1352 sql.push_str(c);
1353 }
1354 }
1355 }
1356
1357 if !self.order_by.is_empty() {
1358 let order_cols: Vec<String> = self
1359 .order_by
1360 .iter()
1361 .map(|o| {
1362 let dir = match o.direction {
1363 OrderDirection::Asc => " ASC",
1364 OrderDirection::Desc => " DESC",
1365 };
1366 format!("{}{}", self.dialect.quote(&o.field), dir)
1367 })
1368 .collect();
1369 sql.push_str(" ORDER BY ");
1370 sql.push_str(&order_cols.join(", "));
1371 }
1372
1373 if let Some(limit) = self.limit_value {
1374 sql.push_str(&format!(" LIMIT {}", limit));
1375 }
1376
1377 if let Some(offset) = self.offset_value {
1378 sql.push_str(&format!(" OFFSET {}", offset));
1379 }
1380
1381 sql.into_string()
1382 }
1383
1384 fn build_where_clause(&self) -> String {
1390 self.build_where_clause_with_options(true)
1391 }
1392
1393 fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
1401 let soft_delete_cond = if include_soft_delete {
1403 self.build_soft_delete_condition()
1404 } else {
1405 None
1406 };
1407
1408 let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
1410 sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
1412 });
1413
1414 if self.where_conditions.is_empty()
1416 && soft_delete_cond.is_none()
1417 && tenant_cond.is_none()
1418 && self.keyset_cursor.is_none()
1419 {
1420 return String::new();
1421 }
1422
1423 let mut conditions: Vec<String> = self
1425 .where_conditions
1426 .iter()
1427 .map(|cond| match cond {
1428 WhereCondition::And(c) => c.clone(),
1429 WhereCondition::Or(c) => format!("OR {}", c),
1430 WhereCondition::Eq(f, v) => format!(
1432 "{} = {}",
1433 self.dialect.quote(f),
1434 v.to_param_with_dialect(&*self.dialect)
1435 ),
1436 WhereCondition::Ne(f, v) => format!(
1437 "{} != {}",
1438 self.dialect.quote(f),
1439 v.to_param_with_dialect(&*self.dialect)
1440 ),
1441 WhereCondition::Gt(f, v) => format!(
1442 "{} > {}",
1443 self.dialect.quote(f),
1444 v.to_param_with_dialect(&*self.dialect)
1445 ),
1446 WhereCondition::Ge(f, v) => format!(
1447 "{} >= {}",
1448 self.dialect.quote(f),
1449 v.to_param_with_dialect(&*self.dialect)
1450 ),
1451 WhereCondition::Lt(f, v) => format!(
1452 "{} < {}",
1453 self.dialect.quote(f),
1454 v.to_param_with_dialect(&*self.dialect)
1455 ),
1456 WhereCondition::Le(f, v) => format!(
1457 "{} <= {}",
1458 self.dialect.quote(f),
1459 v.to_param_with_dialect(&*self.dialect)
1460 ),
1461 WhereCondition::Like(f, v) => format!(
1462 "{} LIKE {}",
1463 self.dialect.quote(f),
1464 v.to_param_with_dialect(&*self.dialect)
1465 ),
1466 WhereCondition::OrEq(f, v) => format!(
1467 "OR {} = {}",
1468 self.dialect.quote(f),
1469 v.to_param_with_dialect(&*self.dialect)
1470 ),
1471 WhereCondition::OrNe(f, v) => format!(
1472 "OR {} != {}",
1473 self.dialect.quote(f),
1474 v.to_param_with_dialect(&*self.dialect)
1475 ),
1476 WhereCondition::OrGt(f, v) => format!(
1477 "OR {} > {}",
1478 self.dialect.quote(f),
1479 v.to_param_with_dialect(&*self.dialect)
1480 ),
1481 WhereCondition::OrGe(f, v) => format!(
1482 "OR {} >= {}",
1483 self.dialect.quote(f),
1484 v.to_param_with_dialect(&*self.dialect)
1485 ),
1486 WhereCondition::OrLt(f, v) => format!(
1487 "OR {} < {}",
1488 self.dialect.quote(f),
1489 v.to_param_with_dialect(&*self.dialect)
1490 ),
1491 WhereCondition::OrLe(f, v) => format!(
1492 "OR {} <= {}",
1493 self.dialect.quote(f),
1494 v.to_param_with_dialect(&*self.dialect)
1495 ),
1496 WhereCondition::OrLike(f, v) => format!(
1497 "OR {} LIKE {}",
1498 self.dialect.quote(f),
1499 v.to_param_with_dialect(&*self.dialect)
1500 ),
1501 WhereCondition::In(f, vals) => {
1502 let vals_str: Vec<String> = vals
1504 .iter()
1505 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1506 .collect();
1507 format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1508 }
1509 WhereCondition::NotIn(f, vals) => {
1510 let vals_str: Vec<String> = vals
1511 .iter()
1512 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1513 .collect();
1514 format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1515 }
1516 WhereCondition::Between(f, start, end) => {
1517 format!(
1518 "{} BETWEEN {} AND {}",
1519 self.dialect.quote(f),
1520 start.to_param_with_dialect(&*self.dialect),
1521 end.to_param_with_dialect(&*self.dialect)
1522 )
1523 }
1524 WhereCondition::NotBetween(f, start, end) => {
1525 format!(
1526 "{} NOT BETWEEN {} AND {}",
1527 self.dialect.quote(f),
1528 start.to_param_with_dialect(&*self.dialect),
1529 end.to_param_with_dialect(&*self.dialect)
1530 )
1531 }
1532 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1533 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1534 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1535 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1536 WhereCondition::TypedExpr(sql, _) => sql.clone(),
1537 })
1538 .collect();
1539
1540 if let Some(sd_cond) = soft_delete_cond {
1542 conditions.push(sd_cond);
1543 }
1544
1545 if let Some(t_cond) = tenant_cond {
1547 conditions.push(t_cond);
1548 }
1549
1550 if let Some(ref cursor) = self.keyset_cursor {
1552 let op = match cursor.direction {
1553 KeysetDirection::After => ">",
1554 KeysetDirection::Before => "<",
1555 };
1556 conditions.push(format!(
1557 "{} {} {}",
1558 self.dialect.quote(&cursor.field),
1559 op,
1560 cursor.value.to_param_with_dialect(&*self.dialect)
1561 ));
1562 }
1563
1564 if conditions.is_empty() {
1565 return String::new();
1566 }
1567
1568 let mut groups: Vec<Vec<String>> = Vec::new();
1571 let mut current_group: Vec<String> = Vec::new();
1572 for cond in conditions.iter() {
1573 if let Some(stripped) = cond.strip_prefix("OR ") {
1574 current_group.push(stripped.to_string());
1576 } else {
1577 if !current_group.is_empty() {
1579 groups.push(std::mem::take(&mut current_group));
1580 }
1581 current_group.push(cond.clone());
1582 }
1583 }
1584 if !current_group.is_empty() {
1585 groups.push(current_group);
1586 }
1587
1588 let group_strs: Vec<String> = groups
1589 .iter()
1590 .map(|g| {
1591 if g.len() == 1 {
1592 g[0].clone()
1593 } else {
1594 format!("({})", g.join(" OR "))
1595 }
1596 })
1597 .collect();
1598
1599 format!(" WHERE {}", group_strs.join(" AND "))
1601 }
1602
1603 #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1604 pub fn build_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1606 let table = self
1607 .table
1608 .clone()
1609 .unwrap_or_else(|| M::table_name().to_string());
1610
1611 if data.is_empty() {
1612 return String::new();
1613 }
1614
1615 let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1616 let values: Vec<String> = data
1618 .values()
1619 .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1620 .collect();
1621
1622 crate::sql_buffer::SqlBuffer::from_str(&format!(
1623 "INSERT INTO {} ({}) VALUES ({})",
1624 self.dialect.quote(&table),
1625 columns.join(", "),
1626 values.join(", ")
1627 ))
1628 .into_string()
1629 }
1630
1631 #[tracing::instrument(skip(self, data), fields(op = "update"))]
1632 pub fn build_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1634 let table = self
1635 .table
1636 .clone()
1637 .unwrap_or_else(|| M::table_name().to_string());
1638
1639 if data.is_empty() {
1640 return String::new();
1641 }
1642
1643 let set_clauses: Vec<String> = data
1644 .iter()
1645 .map(|(k, v)| {
1646 format!(
1647 "{} = {}",
1648 self.dialect.quote(k),
1649 v.to_param_with_dialect(&*self.dialect)
1650 )
1651 })
1652 .collect();
1653
1654 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1655 "UPDATE {} SET {}",
1656 self.dialect.quote(&table),
1657 set_clauses.join(", ")
1658 ));
1659
1660 sql.push_str(&self.build_where_clause());
1661 sql.into_string()
1662 }
1663
1664 #[tracing::instrument(skip(self), fields(op = "delete"))]
1675 pub fn build_delete(&self) -> String {
1676 let table = self
1677 .table
1678 .clone()
1679 .unwrap_or_else(|| M::table_name().to_string());
1680
1681 if let Some(field) = self.soft_delete_field() {
1683 let where_clause = self.build_where_clause();
1684 return format!(
1685 "UPDATE {} SET {} = NOW(){}",
1686 self.dialect.quote(&table),
1687 self.dialect.quote(field),
1688 where_clause
1689 );
1690 }
1691
1692 let mut sql = crate::sql_buffer::SqlBuffer::from_str(&format!(
1693 "DELETE FROM {}",
1694 self.dialect.quote(&table)
1695 ));
1696 sql.push_str(&self.build_where_clause());
1697 sql.into_string()
1698 }
1699
1700 pub fn build_force_delete(&self) -> String {
1710 let table = self
1711 .table
1712 .clone()
1713 .unwrap_or_else(|| M::table_name().to_string());
1714
1715 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1716 sql.push_str(&self.build_where_clause_with_options(false));
1718 sql
1719 }
1720
1721 fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
1731 self.build_where_clause_with_params_options(true)
1733 }
1734
1735 fn build_where_clause_with_params_options(
1744 &self,
1745 include_soft_delete: bool,
1746 ) -> (String, Vec<Value>) {
1747 let soft_delete_cond = if include_soft_delete {
1749 self.build_soft_delete_condition()
1750 } else {
1751 None
1752 };
1753
1754 let tenant_cond = self.build_tenant_condition();
1756
1757 if self.where_conditions.is_empty()
1759 && soft_delete_cond.is_none()
1760 && tenant_cond.is_none()
1761 && self.keyset_cursor.is_none()
1762 {
1763 return (String::new(), Vec::new());
1764 }
1765
1766 let mut params = Vec::new();
1767
1768 let mut conditions: Vec<String> = self
1769 .where_conditions
1770 .iter()
1771 .map(|cond| match cond {
1772 WhereCondition::And(c) => c.clone(),
1773 WhereCondition::Or(c) => format!("OR {}", c),
1774 WhereCondition::Eq(f, v) => {
1776 params.push(v.clone());
1777 format!("{} = ?", self.dialect.quote(f))
1778 }
1779 WhereCondition::Ne(f, v) => {
1780 params.push(v.clone());
1781 format!("{} != ?", self.dialect.quote(f))
1782 }
1783 WhereCondition::Gt(f, v) => {
1784 params.push(v.clone());
1785 format!("{} > ?", self.dialect.quote(f))
1786 }
1787 WhereCondition::Ge(f, v) => {
1788 params.push(v.clone());
1789 format!("{} >= ?", self.dialect.quote(f))
1790 }
1791 WhereCondition::Lt(f, v) => {
1792 params.push(v.clone());
1793 format!("{} < ?", self.dialect.quote(f))
1794 }
1795 WhereCondition::Le(f, v) => {
1796 params.push(v.clone());
1797 format!("{} <= ?", self.dialect.quote(f))
1798 }
1799 WhereCondition::Like(f, v) => {
1800 params.push(v.clone());
1801 format!("{} LIKE ?", self.dialect.quote(f))
1802 }
1803 WhereCondition::OrEq(f, v) => {
1804 params.push(v.clone());
1805 format!("OR {} = ?", self.dialect.quote(f))
1806 }
1807 WhereCondition::OrNe(f, v) => {
1808 params.push(v.clone());
1809 format!("OR {} != ?", self.dialect.quote(f))
1810 }
1811 WhereCondition::OrGt(f, v) => {
1812 params.push(v.clone());
1813 format!("OR {} > ?", self.dialect.quote(f))
1814 }
1815 WhereCondition::OrGe(f, v) => {
1816 params.push(v.clone());
1817 format!("OR {} >= ?", self.dialect.quote(f))
1818 }
1819 WhereCondition::OrLt(f, v) => {
1820 params.push(v.clone());
1821 format!("OR {} < ?", self.dialect.quote(f))
1822 }
1823 WhereCondition::OrLe(f, v) => {
1824 params.push(v.clone());
1825 format!("OR {} <= ?", self.dialect.quote(f))
1826 }
1827 WhereCondition::OrLike(f, v) => {
1828 params.push(v.clone());
1829 format!("OR {} LIKE ?", self.dialect.quote(f))
1830 }
1831 WhereCondition::In(f, vals) => {
1832 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
1833 params.extend(vals.iter().cloned());
1834 format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
1835 }
1836 WhereCondition::NotIn(f, vals) => {
1837 let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
1838 params.extend(vals.iter().cloned());
1839 format!(
1840 "{} NOT IN ({})",
1841 self.dialect.quote(f),
1842 placeholders.join(", ")
1843 )
1844 }
1845 WhereCondition::Between(f, start, end) => {
1846 params.push(start.clone());
1847 params.push(end.clone());
1848 format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
1849 }
1850 WhereCondition::NotBetween(f, start, end) => {
1851 params.push(start.clone());
1852 params.push(end.clone());
1853 format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
1854 }
1855 WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1856 WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1857 WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1858 WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1859 WhereCondition::TypedExpr(sql, expr_params) => {
1860 params.extend(expr_params.iter().cloned());
1861 sql.clone()
1862 }
1863 })
1864 .collect();
1865
1866 if let Some(sd_cond) = soft_delete_cond {
1868 conditions.push(sd_cond);
1869 }
1870
1871 if let Some((t_sql, t_value)) = tenant_cond {
1873 conditions.push(t_sql);
1874 params.push(t_value);
1875 }
1876
1877 if let Some(ref cursor) = self.keyset_cursor {
1879 let op = match cursor.direction {
1880 KeysetDirection::After => ">",
1881 KeysetDirection::Before => "<",
1882 };
1883 conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
1884 params.push(cursor.value.clone());
1885 }
1886
1887 if conditions.is_empty() {
1888 return (String::new(), params);
1889 }
1890
1891 let mut groups: Vec<Vec<String>> = Vec::new();
1893 let mut current_group: Vec<String> = Vec::new();
1894 for cond in conditions.iter() {
1895 if let Some(stripped) = cond.strip_prefix("OR ") {
1896 current_group.push(stripped.to_string());
1897 } else {
1898 if !current_group.is_empty() {
1899 groups.push(std::mem::take(&mut current_group));
1900 }
1901 current_group.push(cond.clone());
1902 }
1903 }
1904 if !current_group.is_empty() {
1905 groups.push(current_group);
1906 }
1907
1908 let group_strs: Vec<String> = groups
1909 .iter()
1910 .map(|g| {
1911 if g.len() == 1 {
1912 g[0].clone()
1913 } else {
1914 format!("({})", g.join(" OR "))
1915 }
1916 })
1917 .collect();
1918
1919 (format!(" WHERE {}", group_strs.join(" AND ")), params)
1921 }
1922
1923 pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
1928 let table = self
1929 .table
1930 .clone()
1931 .unwrap_or_else(|| M::table_name().to_string());
1932 let columns = if self.select_columns.is_empty() {
1933 "*".to_string()
1934 } else {
1935 self.select_columns.join(", ")
1936 };
1937
1938 let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
1939
1940 for join in &self.joins {
1941 match join {
1942 JoinClause::Inner(t, l, r) => {
1943 sql.push_str(&format!(
1944 " INNER JOIN {} ON {} = {}",
1945 self.dialect.quote(t),
1946 self.dialect.quote(l),
1947 self.dialect.quote(r)
1948 ));
1949 }
1950 JoinClause::Left(t, l, r) => {
1951 sql.push_str(&format!(
1952 " LEFT JOIN {} ON {} = {}",
1953 self.dialect.quote(t),
1954 self.dialect.quote(l),
1955 self.dialect.quote(r)
1956 ));
1957 }
1958 JoinClause::Right(t, l, r) => {
1959 sql.push_str(&format!(
1960 " RIGHT JOIN {} ON {} = {}",
1961 self.dialect.quote(t),
1962 self.dialect.quote(l),
1963 self.dialect.quote(r)
1964 ));
1965 }
1966 JoinClause::Cross(t, on) => {
1967 sql.push_str(&format!(
1968 " CROSS JOIN {} ON {}",
1969 self.dialect.quote(t),
1970 self.dialect.quote(on)
1971 ));
1972 }
1973 JoinClause::Relation(kind, ft, fk, tt, tk) => {
1974 sql.push_str(&format!(
1975 " {} {} ON {}.{} = {}.{}",
1976 kind.as_sql(),
1977 self.dialect.quote(tt),
1978 self.dialect.quote(ft),
1979 self.dialect.quote(fk),
1980 self.dialect.quote(tt),
1981 self.dialect.quote(tk)
1982 ));
1983 }
1984 }
1985 }
1986
1987 let mut params = Vec::new();
1988 let (where_clause, where_params) = self.build_where_clause_with_params();
1990 if !where_clause.is_empty() {
1991 sql.push_str(&where_clause);
1992 params = where_params;
1993 }
1994
1995 if !self.group_by.is_empty() {
1996 let cols: Vec<String> = self
1997 .group_by
1998 .iter()
1999 .map(|c| self.dialect.quote(c))
2000 .collect();
2001 sql.push_str(" GROUP BY ");
2002 sql.push_str(&cols.join(", "));
2003 }
2004
2005 if !self.having_conditions.is_empty() {
2006 sql.push_str(" HAVING ");
2007 for (i, cond) in self.having_conditions.iter().enumerate() {
2008 if i > 0 {
2009 sql.push_str(" AND ");
2010 }
2011 if let WhereCondition::And(c) = cond {
2012 sql.push_str(c);
2013 }
2014 }
2015 }
2016
2017 if !self.order_by.is_empty() {
2018 let order_cols: Vec<String> = self
2019 .order_by
2020 .iter()
2021 .map(|o| {
2022 let dir = match o.direction {
2023 OrderDirection::Asc => " ASC",
2024 OrderDirection::Desc => " DESC",
2025 };
2026 format!("{}{}", self.dialect.quote(&o.field), dir)
2027 })
2028 .collect();
2029 sql.push_str(" ORDER BY ");
2030 sql.push_str(&order_cols.join(", "));
2031 }
2032
2033 if let Some(limit) = self.limit_value {
2034 sql.push_str(&format!(" LIMIT {}", limit));
2035 }
2036 if let Some(offset) = self.offset_value {
2037 sql.push_str(&format!(" OFFSET {}", offset));
2038 }
2039
2040 if let Some(lock_type) = &self.lock_type {
2042 if let Some(lock_clause) = self.dialect.build_lock_clause(*lock_type) {
2043 sql.push(' ');
2044 sql.push_str(&lock_clause);
2045 }
2046 }
2047
2048 (sql, params)
2049 }
2050
2051 pub fn build_insert_with_params(
2053 &self,
2054 data: &std::collections::HashMap<String, Value>,
2055 ) -> (String, Vec<Value>) {
2056 let table = self
2057 .table
2058 .clone()
2059 .unwrap_or_else(|| M::table_name().to_string());
2060 if data.is_empty() {
2061 return (String::new(), Vec::new());
2062 }
2063
2064 let mut columns = Vec::with_capacity(data.len());
2065 let mut params = Vec::with_capacity(data.len());
2066 let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
2067 for (k, v) in data.iter() {
2068 columns.push(self.dialect.quote(k));
2069 params.push(v.clone());
2070 }
2071
2072 let insert_clause = if self.insert_or_ignore {
2074 self.dialect.build_insert_or_ignore_prefix(&table)
2075 } else {
2076 format!("INSERT INTO {}", self.dialect.quote(&table))
2077 };
2078
2079 let sql = format!(
2080 "{} ({}) VALUES ({})",
2081 insert_clause,
2082 columns.join(", "),
2083 placeholders.join(", ")
2084 );
2085 (sql, params)
2086 }
2087
2088 pub fn build_batch_insert_with_params(
2095 &self,
2096 rows: &[std::collections::HashMap<String, Value>],
2097 ) -> (String, Vec<Value>) {
2098 let table = self
2099 .table
2100 .clone()
2101 .unwrap_or_else(|| M::table_name().to_string());
2102 if rows.is_empty() {
2103 return (String::new(), Vec::new());
2104 }
2105
2106 let first_row = &rows[0];
2108 let columns: Vec<String> = first_row.keys().cloned().collect();
2109 let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
2110
2111 let mut params = Vec::with_capacity(rows.len() * columns.len());
2112 let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
2113 let is_pg = self.dialect.db_type() == DbType::PostgreSQL;
2114 let mut param_idx = 1usize;
2115 for row in rows {
2116 let placeholders: Vec<String> = columns
2117 .iter()
2118 .map(|col| match row.get(col) {
2119 Some(v) => {
2120 params.push(v.clone());
2121 if is_pg {
2122 let p = format!("${}", param_idx);
2123 param_idx += 1;
2124 p
2125 } else {
2126 "?".to_string()
2127 }
2128 }
2129 None => "NULL".to_string(),
2130 })
2131 .collect();
2132 value_groups.push(format!("({})", placeholders.join(", ")));
2133 }
2134
2135 let sql = format!(
2136 "INSERT INTO {} ({}) VALUES {}",
2137 self.dialect.quote(&table),
2138 quoted_columns.join(", "),
2139 value_groups.join(", ")
2140 );
2141 (sql, params)
2142 }
2143
2144 pub fn build_batch_upsert_with_params(
2165 &self,
2166 rows: &[std::collections::HashMap<String, Value>],
2167 conflict_columns: &[&str],
2168 update_columns: &[&str],
2169 ) -> Result<(String, Vec<Value>), crate::DbError> {
2170 if rows.is_empty() {
2171 return Err(crate::DbError::InvalidInput(
2172 "build_batch_upsert_with_params: rows cannot be empty".to_string(),
2173 ));
2174 }
2175
2176 let (insert_sql, params) = self.build_batch_insert_with_params(rows);
2178 if insert_sql.is_empty() {
2179 return Err(crate::DbError::InvalidInput(
2180 "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
2181 ));
2182 }
2183
2184 let all_columns: Vec<String> = rows[0].keys().cloned().collect();
2186
2187 let conflict_clause = self
2189 .dialect
2190 .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
2191 .ok_or_else(|| {
2192 crate::DbError::InvalidInput(format!(
2193 "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
2194 self.dialect.db_type()
2195 ))
2196 })?;
2197
2198 let sql = format!("{} {}", insert_sql, conflict_clause);
2199 Ok((sql, params))
2200 }
2201
2202 pub fn build_update_with_params(
2205 &self,
2206 data: &std::collections::HashMap<String, Value>,
2207 ) -> (String, Vec<Value>) {
2208 let table = self
2209 .table
2210 .clone()
2211 .unwrap_or_else(|| M::table_name().to_string());
2212 if data.is_empty() {
2213 return (String::new(), Vec::new());
2214 }
2215
2216 let mut set_clauses = Vec::with_capacity(data.len());
2217 let mut params = Vec::with_capacity(data.len());
2218 for (k, v) in data.iter() {
2219 set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
2220 params.push(v.clone());
2221 }
2222
2223 let mut sql = format!(
2224 "UPDATE {} SET {}",
2225 self.dialect.quote(&table),
2226 set_clauses.join(", ")
2227 );
2228
2229 let (where_clause, where_params) = self.build_where_clause_with_params();
2231 if !where_clause.is_empty() {
2232 sql.push_str(&where_clause);
2233 params.extend(where_params);
2234 }
2235
2236 (sql, params)
2237 }
2238
2239 pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
2245 let table = self
2246 .table
2247 .clone()
2248 .unwrap_or_else(|| M::table_name().to_string());
2249
2250 if let Some(field) = self.soft_delete_field() {
2252 let (where_clause, where_params) = self.build_where_clause_with_params();
2253 let sql = format!(
2254 "UPDATE {} SET {} = NOW(){}",
2255 self.dialect.quote(&table),
2256 self.dialect.quote(field),
2257 where_clause
2258 );
2259 return (sql, where_params);
2260 }
2261
2262 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2263 let mut params = Vec::new();
2264
2265 let (where_clause, where_params) = self.build_where_clause_with_params();
2266 if !where_clause.is_empty() {
2267 sql.push_str(&where_clause);
2268 params = where_params;
2269 }
2270
2271 (sql, params)
2272 }
2273
2274 pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
2278 let table = self
2279 .table
2280 .clone()
2281 .unwrap_or_else(|| M::table_name().to_string());
2282
2283 let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
2284 let mut params = Vec::new();
2285
2286 let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
2288 if !where_clause.is_empty() {
2289 sql.push_str(&where_clause);
2290 params = where_params;
2291 }
2292
2293 (sql, params)
2294 }
2295
2296 pub fn build_count(&self) -> String {
2298 let table = self
2299 .table
2300 .clone()
2301 .unwrap_or_else(|| M::table_name().to_string());
2302
2303 let mut sql = format!(
2304 "SELECT COUNT(*) as total FROM {}",
2305 self.dialect.quote(&table)
2306 );
2307 sql.push_str(&self.build_where_clause());
2308 sql
2309 }
2310
2311 pub fn build_exists(&self) -> String {
2313 let table = self
2314 .table
2315 .clone()
2316 .unwrap_or_else(|| M::table_name().to_string());
2317
2318 let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
2319 sql.push_str(&self.build_where_clause());
2320 sql.push_str(" LIMIT 1");
2321 format!("SELECT EXISTS({})", sql)
2322 }
2323
2324 pub fn build_max(&self, field: &str) -> String {
2326 let table = self
2327 .table
2328 .clone()
2329 .unwrap_or_else(|| M::table_name().to_string());
2330
2331 let mut sql = format!(
2332 "SELECT MAX({}) as max_val FROM {}",
2333 self.dialect.quote(field),
2334 self.dialect.quote(&table)
2335 );
2336 sql.push_str(&self.build_where_clause());
2337 sql
2338 }
2339
2340 pub fn build_min(&self, field: &str) -> String {
2342 let table = self
2343 .table
2344 .clone()
2345 .unwrap_or_else(|| M::table_name().to_string());
2346
2347 let mut sql = format!(
2348 "SELECT MIN({}) as min_val FROM {}",
2349 self.dialect.quote(field),
2350 self.dialect.quote(&table)
2351 );
2352 sql.push_str(&self.build_where_clause());
2353 sql
2354 }
2355
2356 pub fn build_sum(&self, field: &str) -> String {
2358 let table = self
2359 .table
2360 .clone()
2361 .unwrap_or_else(|| M::table_name().to_string());
2362
2363 let mut sql = format!(
2364 "SELECT SUM({}) as sum_val FROM {}",
2365 self.dialect.quote(field),
2366 self.dialect.quote(&table)
2367 );
2368 sql.push_str(&self.build_where_clause());
2369 sql
2370 }
2371
2372 pub fn build_avg(&self, field: &str) -> String {
2374 let table = self
2375 .table
2376 .clone()
2377 .unwrap_or_else(|| M::table_name().to_string());
2378
2379 let mut sql = format!(
2380 "SELECT AVG({}) as avg_val FROM {}",
2381 self.dialect.quote(field),
2382 self.dialect.quote(&table)
2383 );
2384 sql.push_str(&self.build_where_clause());
2385 sql
2386 }
2387
2388 pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2391 let sql = self.build_select();
2392 let mut errors = Vec::new();
2393
2394 if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
2395 errors.push(e);
2396 }
2397
2398 if !self.joins.is_empty() {
2400 for join in &self.joins {
2401 match join {
2402 JoinClause::Inner(_, left, right)
2403 | JoinClause::Left(_, left, right)
2404 | JoinClause::Right(_, left, right) => {
2405 if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
2406 errors.push(e);
2407 }
2408 if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
2409 errors.push(e);
2410 }
2411 }
2412 JoinClause::Relation(_, ft, fk, tt, tk) => {
2413 for ident in [ft.as_str(), fk.as_str(), tt.as_str(), tk.as_str()] {
2414 if let Err(e) = sz_orm_sql_validator::validate_column_name(ident) {
2415 errors.push(e);
2416 }
2417 }
2418 }
2419 _ => {}
2420 }
2421 }
2422 }
2423
2424 let table = self
2426 .table
2427 .clone()
2428 .unwrap_or_else(|| M::table_name().to_string());
2429 if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
2430 errors.push(e);
2431 }
2432
2433 if errors.is_empty() {
2434 Ok(())
2435 } else {
2436 Err(errors)
2437 }
2438 }
2439
2440 pub fn validate_insert(
2443 &self,
2444 data: &std::collections::HashMap<String, Value>,
2445 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2446 let sql = self.build_insert(data);
2447 let mut errors = Vec::new();
2448
2449 if sql.is_empty() {
2450 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
2451 return Err(errors);
2452 }
2453
2454 if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
2455 errors.push(e);
2456 }
2457
2458 if errors.is_empty() {
2459 Ok(())
2460 } else {
2461 Err(errors)
2462 }
2463 }
2464
2465 pub fn validate_update(
2468 &self,
2469 data: &std::collections::HashMap<String, Value>,
2470 ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2471 let sql = self.build_update(data);
2472 let mut errors = Vec::new();
2473
2474 if sql.is_empty() {
2475 errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
2476 return Err(errors);
2477 }
2478
2479 if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
2480 errors.push(e);
2481 }
2482
2483 if errors.is_empty() {
2484 Ok(())
2485 } else {
2486 Err(errors)
2487 }
2488 }
2489
2490 pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
2492 let sql = self.build_delete();
2493 let mut errors = Vec::new();
2494
2495 if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
2496 errors.push(e);
2497 }
2498
2499 if errors.is_empty() {
2500 Ok(())
2501 } else {
2502 Err(errors)
2503 }
2504 }
2505}
2506
2507impl<M: Model + crate::model::ModelExt> QueryBuilder<M> {
2509 pub fn select_exclude(mut self, fields: &[&str]) -> Result<Self, crate::DbError> {
2527 let all_columns = M::columns();
2528 let exclude_set: std::collections::HashSet<&str> = fields.iter().copied().collect();
2529
2530 for field in fields {
2531 if !all_columns.contains(field) {
2532 return Err(crate::DbError::InvalidInput(format!(
2533 "排除的字段不存在: {}",
2534 field
2535 )));
2536 }
2537 }
2538
2539 let retained: Vec<String> = all_columns
2540 .into_iter()
2541 .filter(|c| !exclude_set.contains(*c))
2542 .map(|s| s.to_string())
2543 .collect();
2544
2545 if retained.is_empty() {
2546 return Err(crate::DbError::InvalidInput("不能排除所有字段".to_string()));
2547 }
2548
2549 self.select_mode = crate::partial_model::SelectMode::Partial;
2550 self.select_columns = retained;
2551 Ok(self)
2552 }
2553}
2554
2555impl<M: Model> fmt::Debug for QueryBuilder<M> {
2556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2557 f.debug_struct("QueryBuilder")
2558 .field("table", &self.table)
2559 .field("select_columns", &self.select_columns)
2560 .field("where_conditions", &self.where_conditions.len())
2561 .field("limit", &self.limit_value)
2562 .finish()
2563 }
2564}
2565
2566#[cfg(test)]
2567#[allow(deprecated)]
2568mod tests {
2569 use super::*;
2570 use crate::db_type::DbType;
2571 use crate::dialect::get_dialect;
2572
2573 struct TestModel;
2574 impl Model for TestModel {
2575 type PrimaryKey = i64;
2576
2577 fn table_name() -> &'static str {
2578 "test_models"
2579 }
2580
2581 fn pk(&self) -> Self::PrimaryKey {
2582 1
2583 }
2584
2585 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2586 }
2587
2588 #[test]
2589 fn test_query_builder_select() -> Result<(), crate::DbError> {
2590 let dialect = get_dialect(DbType::MySQL)?;
2591 let builder = QueryBuilder::<TestModel>::new(dialect);
2592
2593 let sql = builder
2594 .table("users")
2595 .select(vec!["id", "name"])
2596 .build_select();
2597 assert!(sql.contains("SELECT id, name FROM"));
2598 assert!(sql.contains("`users`"));
2599 Ok(())
2600 }
2601
2602 #[test]
2603 fn test_query_builder_where() -> Result<(), crate::DbError> {
2604 let dialect = get_dialect(DbType::MySQL)?;
2605 let builder = QueryBuilder::<TestModel>::new(dialect);
2606
2607 let sql = builder
2608 .table("users")
2609 .where_eq("status", crate::value::Value::String("active".into()))
2610 .where_gt("age", crate::value::Value::I64(18))
2611 .build_select();
2612
2613 assert!(sql.contains("WHERE"));
2614 assert!(sql.contains("`status` = 'active'"));
2615 assert!(sql.contains("`age` > 18"));
2616 Ok(())
2617 }
2618
2619 #[test]
2620 fn test_query_builder_order_by() -> Result<(), crate::DbError> {
2621 let dialect = get_dialect(DbType::MySQL)?;
2622 let builder = QueryBuilder::<TestModel>::new(dialect);
2623
2624 let sql = builder
2625 .table("users")
2626 .order_by("created_at")
2627 .order_desc("id")
2628 .build_select();
2629
2630 assert!(sql.contains("ORDER BY"));
2631 assert!(sql.contains("`created_at` ASC"));
2632 assert!(sql.contains("`id` DESC"));
2633 Ok(())
2634 }
2635
2636 #[test]
2637 fn test_query_builder_limit_offset() -> Result<(), crate::DbError> {
2638 let dialect = get_dialect(DbType::MySQL)?;
2639 let builder = QueryBuilder::<TestModel>::new(dialect);
2640
2641 let sql = builder.table("users").limit(10).offset(20).build_select();
2642
2643 assert!(sql.contains("LIMIT 10"));
2644 assert!(sql.contains("OFFSET 20"));
2645 Ok(())
2646 }
2647
2648 #[test]
2649 fn test_query_builder_page() -> Result<(), crate::DbError> {
2650 let dialect = get_dialect(DbType::MySQL)?;
2651 let builder = QueryBuilder::<TestModel>::new(dialect);
2652
2653 let sql = builder.table("users").page(3, 20).build_select();
2654
2655 assert!(sql.contains("LIMIT 20"));
2656 assert!(sql.contains("OFFSET 40"));
2657 Ok(())
2658 }
2659
2660 #[test]
2661 fn test_query_builder_insert() -> Result<(), crate::DbError> {
2662 let dialect = get_dialect(DbType::MySQL)?;
2663 let builder = QueryBuilder::<TestModel>::new(dialect);
2664
2665 let mut data = std::collections::HashMap::new();
2666 data.insert("name".to_string(), Value::String("test".to_string()));
2667 data.insert("age".to_string(), Value::I64(25));
2668
2669 let sql = builder.table("users").build_insert(&data);
2670
2671 assert!(sql.contains("INSERT INTO"));
2672 assert!(sql.contains("`name`"));
2673 assert!(sql.contains("'test'"));
2674 Ok(())
2675 }
2676
2677 #[test]
2678 fn test_query_builder_update() -> Result<(), crate::DbError> {
2679 let dialect = get_dialect(DbType::MySQL)?;
2680 let builder = QueryBuilder::<TestModel>::new(dialect);
2681
2682 let mut data = std::collections::HashMap::new();
2683 data.insert("name".to_string(), Value::String("updated".to_string()));
2684
2685 let sql = builder
2686 .table("users")
2687 .where_eq("id", Value::I64(1))
2688 .build_update(&data);
2689
2690 assert!(sql.contains("UPDATE"));
2691 assert!(sql.contains("`name` = 'updated'"));
2692 assert!(sql.contains("WHERE"));
2693 Ok(())
2694 }
2695
2696 #[test]
2697 fn test_query_builder_delete() -> Result<(), crate::DbError> {
2698 let dialect = get_dialect(DbType::MySQL)?;
2699 let builder = QueryBuilder::<TestModel>::new(dialect);
2700
2701 let sql = builder
2702 .table("users")
2703 .where_eq("id", Value::I64(1))
2704 .build_delete();
2705
2706 assert!(sql.contains("DELETE FROM"));
2707 assert!(sql.contains("WHERE"));
2708 Ok(())
2709 }
2710
2711 #[test]
2712 fn test_query_builder_count() -> Result<(), crate::DbError> {
2713 let dialect = get_dialect(DbType::MySQL)?;
2714 let builder = QueryBuilder::<TestModel>::new(dialect);
2715
2716 let sql = builder.table("users").build_count();
2717
2718 assert!(sql.contains("SELECT COUNT(*)"));
2719 assert!(sql.contains("FROM"));
2720 Ok(())
2721 }
2722
2723 #[test]
2724 fn test_query_builder_where_in() -> Result<(), crate::DbError> {
2725 let dialect = get_dialect(DbType::MySQL)?;
2726 let builder = QueryBuilder::<TestModel>::new(dialect);
2727
2728 let sql = builder
2729 .table("users")
2730 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
2731 .build_select();
2732
2733 assert!(sql.contains("IN ("));
2734 Ok(())
2735 }
2736
2737 #[test]
2738 fn test_query_builder_where_between() -> Result<(), crate::DbError> {
2739 let dialect = get_dialect(DbType::MySQL)?;
2740 let builder = QueryBuilder::<TestModel>::new(dialect);
2741
2742 let sql = builder
2743 .table("users")
2744 .where_between("age", Value::I64(18), Value::I64(30))
2745 .build_select();
2746
2747 assert!(sql.contains("BETWEEN"));
2748 Ok(())
2749 }
2750
2751 #[test]
2752 fn test_query_builder_where_null() -> Result<(), crate::DbError> {
2753 let dialect = get_dialect(DbType::MySQL)?;
2754 let builder = QueryBuilder::<TestModel>::new(dialect);
2755
2756 let sql = builder
2757 .table("users")
2758 .where_null("deleted_at")
2759 .build_select();
2760
2761 assert!(sql.contains("IS NULL"));
2762 Ok(())
2763 }
2764
2765 #[test]
2766 fn test_query_builder_join() -> Result<(), crate::DbError> {
2767 let dialect = get_dialect(DbType::MySQL)?;
2768 let builder = QueryBuilder::<TestModel>::new(dialect);
2769
2770 let sql = builder
2771 .table("users")
2772 .join_inner("posts", "users.id", "posts.user_id")
2773 .build_select();
2774
2775 assert!(sql.contains("INNER JOIN"));
2776 assert!(sql.contains("`posts`"));
2777 Ok(())
2778 }
2779
2780 #[test]
2781 fn test_query_builder_group_by() -> Result<(), crate::DbError> {
2782 let dialect = get_dialect(DbType::MySQL)?;
2783 let builder = QueryBuilder::<TestModel>::new(dialect);
2784
2785 let sql = builder.table("users").group_by("status").build_select();
2786
2787 assert!(sql.contains("GROUP BY"));
2788 assert!(sql.contains("`status`"));
2789 Ok(())
2790 }
2791
2792 #[test]
2793 fn test_query_builder_max() -> Result<(), crate::DbError> {
2794 let dialect = get_dialect(DbType::MySQL)?;
2795 let builder = QueryBuilder::<TestModel>::new(dialect);
2796
2797 let sql = builder.table("users").build_max("score");
2798
2799 assert!(sql.contains("MAX("));
2800 assert!(sql.contains("`score`"));
2801 Ok(())
2802 }
2803
2804 #[test]
2805 fn test_query_builder_min() -> Result<(), crate::DbError> {
2806 let dialect = get_dialect(DbType::MySQL)?;
2807 let builder = QueryBuilder::<TestModel>::new(dialect);
2808
2809 let sql = builder.table("users").build_min("price");
2810
2811 assert!(sql.contains("MIN("));
2812 assert!(sql.contains("`price`"));
2813 Ok(())
2814 }
2815
2816 #[test]
2817 fn test_query_builder_sum() -> Result<(), crate::DbError> {
2818 let dialect = get_dialect(DbType::MySQL)?;
2819 let builder = QueryBuilder::<TestModel>::new(dialect);
2820
2821 let sql = builder.table("orders").build_sum("amount");
2822
2823 assert!(sql.contains("SUM("));
2824 assert!(sql.contains("`amount`"));
2825 Ok(())
2826 }
2827
2828 #[test]
2829 fn test_query_builder_avg() -> Result<(), crate::DbError> {
2830 let dialect = get_dialect(DbType::MySQL)?;
2831 let builder = QueryBuilder::<TestModel>::new(dialect);
2832
2833 let sql = builder.table("scores").build_avg("value");
2834
2835 assert!(sql.contains("AVG("));
2836 assert!(sql.contains("`value`"));
2837 Ok(())
2838 }
2839
2840 #[test]
2841 fn test_validator_select() -> Result<(), crate::DbError> {
2842 let dialect = get_dialect(DbType::MySQL)?;
2843 let builder = QueryBuilder::<TestModel>::new(dialect);
2844
2845 let result = builder.table("users").select(vec!["id", "name"]).validate();
2846 assert!(result.is_ok());
2847 Ok(())
2848 }
2849
2850 #[test]
2851 fn test_validator_select_with_join() -> Result<(), crate::DbError> {
2852 let dialect = get_dialect(DbType::MySQL)?;
2853 let builder = QueryBuilder::<TestModel>::new(dialect);
2854
2855 let result = builder
2856 .table("users")
2857 .join_inner("posts", "users.id", "posts.user_id")
2858 .validate();
2859 assert!(result.is_ok());
2860 Ok(())
2861 }
2862
2863 #[test]
2864 fn test_validator_insert() -> Result<(), crate::DbError> {
2865 let dialect = get_dialect(DbType::MySQL)?;
2866 let builder = QueryBuilder::<TestModel>::new(dialect);
2867
2868 let mut data = std::collections::HashMap::new();
2869 data.insert("name".to_string(), Value::String("test".to_string()));
2870
2871 let result = builder.table("users").validate_insert(&data);
2872 assert!(result.is_ok());
2873 Ok(())
2874 }
2875
2876 #[test]
2877 fn test_validator_insert_empty_data() -> Result<(), crate::DbError> {
2878 let dialect = get_dialect(DbType::MySQL)?;
2879 let builder = QueryBuilder::<TestModel>::new(dialect);
2880
2881 let data = std::collections::HashMap::new();
2882 let result = builder.table("users").validate_insert(&data);
2883 assert!(result.is_err());
2884 Ok(())
2885 }
2886
2887 #[test]
2888 fn test_validator_update() -> Result<(), crate::DbError> {
2889 let dialect = get_dialect(DbType::MySQL)?;
2890 let builder = QueryBuilder::<TestModel>::new(dialect);
2891
2892 let mut data = std::collections::HashMap::new();
2893 data.insert("name".to_string(), Value::String("updated".to_string()));
2894
2895 let result = builder.table("users").validate_update(&data);
2896 assert!(result.is_ok());
2897 Ok(())
2898 }
2899
2900 #[test]
2901 fn test_validator_update_empty_data() -> Result<(), crate::DbError> {
2902 let dialect = get_dialect(DbType::MySQL)?;
2903 let builder = QueryBuilder::<TestModel>::new(dialect);
2904
2905 let data = std::collections::HashMap::new();
2906 let result = builder.table("users").validate_update(&data);
2907 assert!(result.is_err());
2908 Ok(())
2909 }
2910
2911 #[test]
2912 fn test_validator_delete() -> Result<(), crate::DbError> {
2913 let dialect = get_dialect(DbType::MySQL)?;
2914 let builder = QueryBuilder::<TestModel>::new(dialect);
2915
2916 let result = builder
2917 .table("users")
2918 .where_eq("id", Value::I64(1))
2919 .validate_delete();
2920 assert!(result.is_ok());
2921 Ok(())
2922 }
2923
2924 #[test]
2925 fn test_validator_delete_no_where() -> Result<(), crate::DbError> {
2926 let dialect = get_dialect(DbType::MySQL)?;
2927 let builder = QueryBuilder::<TestModel>::new(dialect);
2928
2929 let result = builder.table("users").validate_delete();
2931 assert!(result.is_ok());
2932 Ok(())
2933 }
2934
2935 #[test]
2938 fn test_m3_select_quoted_valid_columns() -> Result<(), crate::DbError> {
2939 let dialect = get_dialect(DbType::MySQL)?;
2940 let builder = QueryBuilder::<TestModel>::new(dialect);
2941 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
2942 let sql = builder.build_select();
2943 assert!(sql.contains("SELECT `id`, `name` FROM"));
2945 assert!(sql.contains("`users`"));
2946 Ok(())
2947 }
2948
2949 #[test]
2950 fn test_m3_select_quoted_rejects_sql_injection() -> Result<(), crate::DbError> {
2951 let dialect = get_dialect(DbType::MySQL)?;
2952 let builder = QueryBuilder::<TestModel>::new(dialect);
2953
2954 let result = builder
2956 .table("users")
2957 .select_quoted(vec!["id; DROP TABLE users"]);
2958 assert!(result.is_err());
2959
2960 let dialect = get_dialect(DbType::MySQL)?;
2962 let builder = QueryBuilder::<TestModel>::new(dialect);
2963 let result = builder.table("users").select_quoted(vec!["name'"]);
2964 assert!(result.is_err());
2965
2966 let dialect = get_dialect(DbType::MySQL)?;
2968 let builder = QueryBuilder::<TestModel>::new(dialect);
2969 let result = builder.table("users").select_quoted(vec!["1col"]);
2970 assert!(result.is_err());
2971
2972 let dialect = get_dialect(DbType::MySQL)?;
2974 let builder = QueryBuilder::<TestModel>::new(dialect);
2975 let result = builder.table("users").select_quoted(vec!["col name"]);
2976 assert!(result.is_err());
2977 Ok(())
2978 }
2979
2980 #[test]
2981 fn test_m3_select_quoted_postgresql_dialect() -> Result<(), crate::DbError> {
2982 let dialect = get_dialect(DbType::PostgreSQL)?;
2983 let builder = QueryBuilder::<TestModel>::new(dialect);
2984 let builder = builder.table("users").select_quoted(vec!["id", "name"])?;
2985 let sql = builder.build_select();
2986 assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
2988 assert!(sql.contains("\"users\""));
2989 Ok(())
2990 }
2991
2992 struct SoftDeleteModel;
2996 impl Model for SoftDeleteModel {
2997 type PrimaryKey = i64;
2998
2999 fn table_name() -> &'static str {
3000 "soft_users"
3001 }
3002
3003 fn pk(&self) -> Self::PrimaryKey {
3004 1
3005 }
3006
3007 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3008
3009 fn soft_delete_field() -> Option<&'static str> {
3010 Some("deleted_at")
3011 }
3012 }
3013
3014 #[test]
3018 fn test_p01_soft_delete_select_auto_filter() -> Result<(), crate::DbError> {
3019 let dialect = get_dialect(DbType::MySQL)?;
3020 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3021 let sql = builder.table("soft_users").build_select();
3022 assert!(
3024 sql.contains("`deleted_at` IS NULL"),
3025 "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
3026 sql
3027 );
3028 Ok(())
3029 }
3030
3031 #[test]
3033 fn test_p01_soft_delete_select_with_user_where() -> Result<(), crate::DbError> {
3034 let dialect = get_dialect(DbType::MySQL)?;
3035 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3036 .table("soft_users")
3037 .where_eq("status", Value::String("active".into()))
3038 .build_select();
3039 assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
3041 assert!(
3042 sql.contains("`deleted_at` IS NULL"),
3043 "软删除条件应自动追加: {}",
3044 sql
3045 );
3046 Ok(())
3047 }
3048
3049 #[test]
3053 fn test_p01_soft_delete_without_soft_delete() -> Result<(), crate::DbError> {
3054 let dialect = get_dialect(DbType::MySQL)?;
3055 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3056 .table("soft_users")
3057 .without_soft_delete()
3058 .build_select();
3059 assert!(
3061 !sql.contains("`deleted_at` IS NULL"),
3062 "without_soft_delete 应禁用过滤,实际: {}",
3063 sql
3064 );
3065 assert!(
3067 !sql.contains("WHERE"),
3068 "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
3069 sql
3070 );
3071 Ok(())
3072 }
3073
3074 #[test]
3078 fn test_p01_soft_delete_delete_becomes_update() -> Result<(), crate::DbError> {
3079 let dialect = get_dialect(DbType::MySQL)?;
3080 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3081 .table("soft_users")
3082 .where_eq("id", Value::I64(42))
3083 .build_delete();
3084 assert!(
3086 sql.starts_with("UPDATE"),
3087 "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
3088 sql
3089 );
3090 assert!(
3091 !sql.contains("DELETE FROM"),
3092 "不应生成 DELETE FROM: {}",
3093 sql
3094 );
3095 assert!(
3096 sql.contains("`deleted_at` = NOW()"),
3097 "应设置 deleted_at = NOW(): {}",
3098 sql
3099 );
3100 assert!(
3102 sql.contains("`deleted_at` IS NULL"),
3103 "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
3104 sql
3105 );
3106 Ok(())
3107 }
3108
3109 #[test]
3113 fn test_p01_soft_delete_force_delete() -> Result<(), crate::DbError> {
3114 let dialect = get_dialect(DbType::MySQL)?;
3115 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3116 .table("soft_users")
3117 .where_eq("id", Value::I64(99))
3118 .build_force_delete();
3119 assert!(
3121 sql.starts_with("DELETE FROM"),
3122 "build_force_delete 应生成 DELETE FROM,实际: {}",
3123 sql
3124 );
3125 assert!(
3127 !sql.contains("`deleted_at` IS NULL"),
3128 "物理删除不应追加软删除过滤: {}",
3129 sql
3130 );
3131 Ok(())
3132 }
3133
3134 #[test]
3136 fn test_p01_soft_delete_select_with_params() -> Result<(), crate::DbError> {
3137 let dialect = get_dialect(DbType::MySQL)?;
3138 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3139 .table("soft_users")
3140 .where_eq("id", Value::I64(1))
3141 .build_select_with_params();
3142 assert!(
3143 sql.contains("`deleted_at` IS NULL"),
3144 "参数化版本也应自动追加软删除: {}",
3145 sql
3146 );
3147 assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
3148 assert_eq!(params[0], Value::I64(1));
3149 Ok(())
3150 }
3151
3152 #[test]
3154 fn test_p01_soft_delete_delete_with_params_becomes_update() -> Result<(), crate::DbError> {
3155 let dialect = get_dialect(DbType::MySQL)?;
3156 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3157 .table("soft_users")
3158 .where_eq("id", Value::I64(7))
3159 .build_delete_with_params();
3160 assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
3161 assert!(
3162 sql.contains("`deleted_at` = NOW()"),
3163 "应设置 NOW(): {}",
3164 sql
3165 );
3166 assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
3167 Ok(())
3168 }
3169
3170 #[test]
3172 fn test_p01_soft_delete_force_delete_with_params() -> Result<(), crate::DbError> {
3173 let dialect = get_dialect(DbType::MySQL)?;
3174 let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
3175 .table("soft_users")
3176 .where_eq("id", Value::I64(11))
3177 .build_force_delete_with_params();
3178 assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
3179 assert!(
3180 !sql.contains("`deleted_at` IS NULL"),
3181 "不应追加软删除过滤: {}",
3182 sql
3183 );
3184 assert_eq!(params.len(), 1);
3185 Ok(())
3186 }
3187
3188 #[test]
3192 fn test_p01_non_soft_delete_model_unchanged() -> Result<(), crate::DbError> {
3193 let dialect = get_dialect(DbType::MySQL)?;
3194 let sql = QueryBuilder::<TestModel>::new(dialect)
3195 .table("users")
3196 .where_eq("id", Value::I64(1))
3197 .build_select();
3198 assert!(
3199 !sql.contains("deleted_at"),
3200 "非软删除模型不应追加 deleted_at: {}",
3201 sql
3202 );
3203 let dialect = get_dialect(DbType::MySQL)?;
3205 let del_sql = QueryBuilder::<TestModel>::new(dialect)
3206 .table("users")
3207 .where_eq("id", Value::I64(1))
3208 .build_delete();
3209 assert!(
3210 del_sql.starts_with("DELETE FROM"),
3211 "非软删除模型 build_delete 应生成 DELETE: {}",
3212 del_sql
3213 );
3214 Ok(())
3215 }
3216
3217 #[test]
3219 fn test_p01_soft_delete_count_auto_filter() -> Result<(), crate::DbError> {
3220 let dialect = get_dialect(DbType::MySQL)?;
3221 let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
3222 .table("soft_users")
3223 .build_count();
3224 assert!(
3225 sql.contains("`deleted_at` IS NULL"),
3226 "build_count 也应追加软删除过滤: {}",
3227 sql
3228 );
3229 Ok(())
3230 }
3231
3232 #[test]
3238 fn test_p02_where_eq_uses_placeholder() -> Result<(), crate::DbError> {
3239 let dialect = get_dialect(DbType::MySQL)?;
3240 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3241 .table("users")
3242 .where_eq("name", Value::String("alice".into()))
3243 .build_select_with_params();
3244 assert!(sql.contains("`name` = ?"), "应使用 ? 占位符: {}", sql);
3246 assert!(!sql.contains("'alice'"), "不应内嵌值到 SQL: {}", sql);
3247 assert_eq!(params.len(), 1);
3248 assert_eq!(params[0], Value::String("alice".into()));
3249 Ok(())
3250 }
3251
3252 #[test]
3254 fn test_p02_where_like_uses_placeholder() -> Result<(), crate::DbError> {
3255 let dialect = get_dialect(DbType::MySQL)?;
3256 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3257 .table("users")
3258 .where_like("name", Value::String("%alice%".into()))
3259 .build_select_with_params();
3260 assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
3261 assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
3262 assert_eq!(params.len(), 1);
3263 Ok(())
3264 }
3265
3266 #[test]
3270 fn test_p02_where_ne_uses_placeholder() -> Result<(), crate::DbError> {
3271 let dialect = get_dialect(DbType::MySQL)?;
3272 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3273 .table("users")
3274 .where_ne("status", Value::I64(0))
3275 .build_select_with_params();
3276 assert!(sql.contains("`status` != ?"), "应使用 != ?: {}", sql);
3277 assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
3278 assert_eq!(params.len(), 1);
3279 assert_eq!(params[0], Value::I64(0));
3280 Ok(())
3281 }
3282
3283 #[test]
3287 fn test_p02_where_ge_uses_placeholder() -> Result<(), crate::DbError> {
3288 let dialect = get_dialect(DbType::MySQL)?;
3289 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3290 .table("users")
3291 .where_ge("age", Value::I64(18))
3292 .build_select_with_params();
3293 assert!(sql.contains("`age` >= ?"), "应使用 >= ?: {}", sql);
3294 assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
3295 assert_eq!(params.len(), 1);
3296 assert_eq!(params[0], Value::I64(18));
3297 Ok(())
3298 }
3299
3300 #[test]
3304 fn test_p02_where_lt_uses_placeholder() -> Result<(), crate::DbError> {
3305 let dialect = get_dialect(DbType::MySQL)?;
3306 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3307 .table("users")
3308 .where_lt("score", Value::F64(60.0))
3309 .build_select_with_params();
3310 assert!(sql.contains("`score` < ?"), "应使用 < ?: {}", sql);
3311 assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
3312 assert_eq!(params.len(), 1);
3313 assert_eq!(params[0], Value::F64(60.0));
3314 Ok(())
3315 }
3316
3317 #[test]
3321 fn test_p02_injection_protection_drop_table() -> Result<(), crate::DbError> {
3322 let dialect = get_dialect(DbType::MySQL)?;
3323 let evil_input = "'; DROP TABLE users; --".to_string();
3324 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3325 .table("users")
3326 .where_eq("name", Value::String(evil_input.clone()))
3327 .build_select_with_params();
3328 assert!(!sql.contains("DROP TABLE"), "SQL 注入未防护: {}", sql);
3330 assert_eq!(params.len(), 1);
3332 assert_eq!(params[0], Value::String(evil_input));
3333 assert_eq!(sql.matches('?').count(), 1);
3335 Ok(())
3336 }
3337
3338 #[test]
3340 fn test_p02_injection_protection_or_one_equals_one() -> Result<(), crate::DbError> {
3341 let dialect = get_dialect(DbType::MySQL)?;
3342 let evil = "' OR '1'='1".to_string();
3343 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3344 .table("users")
3345 .where_eq("name", Value::String(evil.clone()))
3346 .build_select_with_params();
3347 assert!(!sql.contains("OR '1'='1'"), "OR 1=1 注入未防护: {}", sql);
3348 assert_eq!(params.len(), 1);
3349 assert_eq!(params[0], Value::String(evil));
3350 Ok(())
3351 }
3352
3353 #[test]
3355 fn test_p02_multiple_params_order() -> Result<(), crate::DbError> {
3356 let dialect = get_dialect(DbType::MySQL)?;
3357 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3358 .table("users")
3359 .where_eq("name", Value::String("alice".into()))
3360 .where_gt("age", Value::I64(18))
3361 .where_le("score", Value::F64(99.5))
3362 .build_select_with_params();
3363 assert_eq!(sql.matches('?').count(), 3, "应有 3 个占位符: {}", sql);
3364 assert_eq!(params.len(), 3);
3365 assert_eq!(params[0], Value::String("alice".into()));
3367 assert_eq!(params[1], Value::I64(18));
3368 assert_eq!(params[2], Value::F64(99.5));
3369 Ok(())
3370 }
3371
3372 #[test]
3374 fn test_p02_where_in_uses_placeholders() -> Result<(), crate::DbError> {
3375 let dialect = get_dialect(DbType::MySQL)?;
3376 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3377 .table("users")
3378 .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
3379 .build_select_with_params();
3380 assert!(
3381 sql.contains("`id` IN (?, ?, ?)"),
3382 "应使用 3 个占位符: {}",
3383 sql
3384 );
3385 assert_eq!(params.len(), 3);
3386 Ok(())
3387 }
3388
3389 #[test]
3391 fn test_p02_where_between_uses_placeholders() -> Result<(), crate::DbError> {
3392 let dialect = get_dialect(DbType::MySQL)?;
3393 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3394 .table("users")
3395 .where_between("age", Value::I64(18), Value::I64(65))
3396 .build_select_with_params();
3397 assert!(
3398 sql.contains("`age` BETWEEN ? AND ?"),
3399 "应使用 2 个占位符: {}",
3400 sql
3401 );
3402 assert_eq!(params.len(), 2);
3403 assert_eq!(params[0], Value::I64(18));
3404 assert_eq!(params[1], Value::I64(65));
3405 Ok(())
3406 }
3407
3408 #[test]
3410 fn test_p02_update_params_order_set_before_where() -> Result<(), crate::DbError> {
3411 let dialect = get_dialect(DbType::MySQL)?;
3412 let mut data = std::collections::HashMap::new();
3413 data.insert("name".to_string(), Value::String("bob".into()));
3414 data.insert("age".to_string(), Value::I64(30));
3415 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3416 .table("users")
3417 .where_eq("id", Value::I64(99))
3418 .build_update_with_params(&data);
3419 assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
3421 assert_eq!(params.len(), 3);
3422 assert_eq!(params[2], Value::I64(99));
3425 Ok(())
3426 }
3427
3428 #[test]
3432 fn test_p02_build_where_clause_inlines_value() -> Result<(), crate::DbError> {
3433 let dialect = get_dialect(DbType::MySQL)?;
3434 let sql = QueryBuilder::<TestModel>::new(dialect)
3435 .table("users")
3436 .where_eq("name", Value::String("alice".into()))
3437 .build_select();
3438 assert!(
3440 sql.contains("`name` = "),
3441 "无参数版本应含 WHERE 条件: {}",
3442 sql
3443 );
3444 assert!(
3446 !sql.contains("`name` = ?"),
3447 "无参数版本不应使用 ? 占位符: {}",
3448 sql
3449 );
3450 Ok(())
3451 }
3452
3453 #[test]
3455 fn test_p01_is_soft_delete_disabled_flag() -> Result<(), crate::DbError> {
3456 let dialect = get_dialect(DbType::MySQL)?;
3457 let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
3458 assert!(!builder.is_soft_delete_disabled(), "默认应启用软删除过滤");
3459 let builder =
3460 QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL)?).without_soft_delete();
3461 assert!(
3462 builder.is_soft_delete_disabled(),
3463 "without_soft_delete 后应反映禁用状态"
3464 );
3465 Ok(())
3466 }
3467
3468 struct TenantModel;
3472 impl Model for TenantModel {
3473 type PrimaryKey = i64;
3474
3475 fn table_name() -> &'static str {
3476 "orders"
3477 }
3478
3479 fn pk(&self) -> Self::PrimaryKey {
3480 1
3481 }
3482
3483 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3484
3485 fn tenant_field() -> Option<&'static str> {
3486 Some("tenant_id")
3487 }
3488 }
3489
3490 struct SoftDeleteAndTenantModel;
3492 impl Model for SoftDeleteAndTenantModel {
3493 type PrimaryKey = i64;
3494
3495 fn table_name() -> &'static str {
3496 "documents"
3497 }
3498
3499 fn pk(&self) -> Self::PrimaryKey {
3500 1
3501 }
3502
3503 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
3504
3505 fn soft_delete_field() -> Option<&'static str> {
3506 Some("deleted_at")
3507 }
3508
3509 fn tenant_field() -> Option<&'static str> {
3510 Some("tenant_id")
3511 }
3512 }
3513
3514 #[test]
3518 fn test_p03_tenant_select_auto_filter() -> Result<(), crate::DbError> {
3519 let dialect = get_dialect(DbType::MySQL)?;
3520 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3521 .table("orders")
3522 .with_tenant_id(42)
3523 .build_select_with_params();
3524 assert!(
3525 sql.contains("`tenant_id` = ?"),
3526 "多租户模型应自动追加 tenant_id = ?: {}",
3527 sql
3528 );
3529 assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
3530 assert_eq!(params[0], Value::I64(42));
3531 Ok(())
3532 }
3533
3534 #[test]
3536 fn test_p03_tenant_select_with_user_where() -> Result<(), crate::DbError> {
3537 let dialect = get_dialect(DbType::MySQL)?;
3538 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3539 .table("orders")
3540 .with_tenant_id(7)
3541 .where_eq("status", Value::String("active".into()))
3542 .build_select_with_params();
3543 assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
3544 assert!(
3545 sql.contains("`tenant_id` = ?"),
3546 "租户条件应自动追加: {}",
3547 sql
3548 );
3549 assert_eq!(params.len(), 2, "应有 2 个参数");
3550 assert_eq!(params[0], Value::String("active".into()));
3552 assert_eq!(params[1], Value::I64(7));
3553 Ok(())
3554 }
3555
3556 #[test]
3560 fn test_p03_tenant_without_tenant() -> Result<(), crate::DbError> {
3561 let dialect = get_dialect(DbType::MySQL)?;
3562 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3563 .table("orders")
3564 .with_tenant_id(42)
3565 .without_tenant()
3566 .build_select_with_params();
3567 assert!(
3568 !sql.contains("`tenant_id` = ?"),
3569 "without_tenant 应禁用过滤: {}",
3570 sql
3571 );
3572 assert_eq!(params.len(), 0, "不应有租户参数");
3573 Ok(())
3574 }
3575
3576 #[test]
3580 fn test_p03_tenant_delete_auto_filter() -> Result<(), crate::DbError> {
3581 let dialect = get_dialect(DbType::MySQL)?;
3582 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3583 .table("orders")
3584 .with_tenant_id(99)
3585 .where_eq("id", Value::I64(1))
3586 .build_delete_with_params();
3587 assert!(
3588 sql.contains("`tenant_id` = ?"),
3589 "删除应自动追加租户条件: {}",
3590 sql
3591 );
3592 assert_eq!(params.len(), 2);
3594 assert_eq!(params[0], Value::I64(1));
3595 assert_eq!(params[1], Value::I64(99));
3596 Ok(())
3597 }
3598
3599 #[test]
3601 fn test_p03_tenant_update_auto_filter() -> Result<(), crate::DbError> {
3602 let dialect = get_dialect(DbType::MySQL)?;
3603 let mut data = std::collections::HashMap::new();
3604 data.insert("status".to_string(), Value::String("shipped".into()));
3605 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3606 .table("orders")
3607 .with_tenant_id(5)
3608 .where_eq("id", Value::I64(10))
3609 .build_update_with_params(&data);
3610 assert!(
3611 sql.contains("`tenant_id` = ?"),
3612 "更新应自动追加租户条件: {}",
3613 sql
3614 );
3615 assert_eq!(params.len(), 3);
3617 assert_eq!(params[2], Value::I64(5));
3619 Ok(())
3620 }
3621
3622 #[test]
3624 fn test_p03_tenant_count_auto_filter() -> Result<(), crate::DbError> {
3625 let dialect = get_dialect(DbType::MySQL)?;
3626 let sql = QueryBuilder::<TenantModel>::new(dialect)
3627 .table("orders")
3628 .with_tenant_id(42)
3629 .build_count();
3630 assert!(
3631 sql.contains("`tenant_id` = 42"),
3632 "build_count 应追加租户条件(无参数版本内嵌值): {}",
3633 sql
3634 );
3635 Ok(())
3636 }
3637
3638 #[test]
3642 fn test_p03_non_tenant_model_unchanged() -> Result<(), crate::DbError> {
3643 let dialect = get_dialect(DbType::MySQL)?;
3644 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3646 .table("users")
3647 .with_tenant_id(42)
3648 .build_select_with_params();
3649 assert!(
3650 !sql.contains("tenant_id"),
3651 "非多租户模型不应追加 tenant_id: {}",
3652 sql
3653 );
3654 assert_eq!(params.len(), 0);
3655 Ok(())
3656 }
3657
3658 #[test]
3662 fn test_p03_tenant_no_id_no_filter() -> Result<(), crate::DbError> {
3663 let dialect = get_dialect(DbType::MySQL)?;
3664 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3665 .table("orders")
3666 .build_select_with_params();
3667 assert!(
3668 !sql.contains("tenant_id"),
3669 "未设置 tenant_id 时不应追加过滤: {}",
3670 sql
3671 );
3672 assert_eq!(params.len(), 0);
3673 Ok(())
3674 }
3675
3676 #[test]
3680 fn test_p03_soft_delete_and_tenant_combined() -> Result<(), crate::DbError> {
3681 let dialect = get_dialect(DbType::MySQL)?;
3682 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3683 .table("documents")
3684 .with_tenant_id(100)
3685 .where_eq("title", Value::String("report".into()))
3686 .build_select_with_params();
3687 assert!(
3689 sql.contains("`deleted_at` IS NULL"),
3690 "应追加软删除条件: {}",
3691 sql
3692 );
3693 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
3695 assert!(sql.contains("`title` = ?"), "用户条件应保留: {}", sql);
3697 assert_eq!(params.len(), 2);
3699 assert_eq!(params[0], Value::String("report".into()));
3700 assert_eq!(params[1], Value::I64(100));
3701 Ok(())
3702 }
3703
3704 #[test]
3706 fn test_p03_without_tenant_and_soft_delete() -> Result<(), crate::DbError> {
3707 let dialect = get_dialect(DbType::MySQL)?;
3708 let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3709 .table("documents")
3710 .with_tenant_id(100)
3711 .without_tenant()
3712 .without_soft_delete()
3713 .build_select_with_params();
3714 assert!(
3715 !sql.contains("`deleted_at` IS NULL"),
3716 "应禁用软删除: {}",
3717 sql
3718 );
3719 assert!(!sql.contains("`tenant_id` = ?"), "应禁用租户: {}", sql);
3720 assert_eq!(params.len(), 0);
3721 Ok(())
3722 }
3723
3724 #[test]
3726 fn test_p03_is_tenant_disabled_flag() -> Result<(), crate::DbError> {
3727 let dialect = get_dialect(DbType::MySQL)?;
3728 let builder = QueryBuilder::<TenantModel>::new(dialect);
3729 assert!(!builder.is_tenant_disabled(), "默认应启用租户过滤");
3730 let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
3731 .with_tenant_id(1)
3732 .without_tenant();
3733 assert!(
3734 builder.is_tenant_disabled(),
3735 "without_tenant 后应反映禁用状态"
3736 );
3737 Ok(())
3738 }
3739
3740 #[test]
3744 fn test_p03_tenant_force_delete_keeps_tenant_filter() -> Result<(), crate::DbError> {
3745 let dialect = get_dialect(DbType::MySQL)?;
3746 let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3747 .table("orders")
3748 .with_tenant_id(42)
3749 .where_eq("id", Value::I64(999))
3750 .build_force_delete_with_params();
3751 assert!(
3754 sql.contains("`tenant_id` = ?"),
3755 "物理删除应保留租户条件: {}",
3756 sql
3757 );
3758 assert_eq!(params.len(), 2);
3759 assert_eq!(params[0], Value::I64(999));
3760 assert_eq!(params[1], Value::I64(42));
3761 Ok(())
3762 }
3763
3764 #[cfg(feature = "multi-tenant-enhanced")]
3768 #[tokio::test]
3769 async fn test_mt_explicit_tenant_id_takes_priority() -> Result<(), crate::DbError> {
3770 let ctx = crate::tenant_context::TenantContext::new(
3771 99,
3772 crate::tenant_context::IsolationStrategy::RowLevel,
3773 );
3774 ctx.scope(async {
3775 let (sql, params) =
3777 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
3778 .table("orders")
3779 .with_tenant_id(42)
3780 .build_select_with_params();
3781 assert!(sql.contains("`tenant_id` = ?"), "应追加租户条件: {}", sql);
3782 assert_eq!(params.len(), 1);
3783 assert_eq!(params[0], Value::I64(42), "显式 tenant_id 应优先");
3784 })
3785 .await;
3786 Ok(())
3787 }
3788
3789 #[cfg(feature = "multi-tenant-enhanced")]
3791 #[tokio::test]
3792 async fn test_mt_context_auto_inject() -> Result<(), crate::DbError> {
3793 let ctx = crate::tenant_context::TenantContext::new(
3794 77,
3795 crate::tenant_context::IsolationStrategy::RowLevel,
3796 );
3797 ctx.scope(async {
3798 let (sql, params) =
3799 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
3800 .table("orders")
3801 .build_select_with_params();
3802 assert!(
3803 sql.contains("`tenant_id` = ?"),
3804 "应从上下文自动追加租户条件: {}",
3805 sql
3806 );
3807 assert_eq!(params.len(), 1);
3808 assert_eq!(params[0], Value::I64(77), "应从上下文注入 tenant_id");
3809 })
3810 .await;
3811 Ok(())
3812 }
3813
3814 #[cfg(feature = "multi-tenant-enhanced")]
3816 #[tokio::test]
3817 async fn test_mt_schema_isolation_table_rewrite() -> Result<(), crate::DbError> {
3818 let ctx = crate::tenant_context::TenantContext::new(
3819 42,
3820 crate::tenant_context::IsolationStrategy::SchemaIsolation,
3821 );
3822 ctx.scope(async {
3823 let (sql, _params) =
3824 QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
3825 .table("orders")
3826 .build_select_with_params();
3827 assert!(
3828 sql.contains("tenant_42_orders"),
3829 "Schema 隔离应重写表名: {}",
3830 sql
3831 );
3832 })
3833 .await;
3834 Ok(())
3835 }
3836
3837 #[cfg(feature = "multi-tenant-enhanced")]
3839 #[test]
3840 fn test_mt_no_context_no_change() -> Result<(), crate::DbError> {
3841 let (sql, params) = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL)?)
3842 .table("orders")
3843 .build_select_with_params();
3844 assert!(
3846 !sql.contains("`tenant_id` = ?"),
3847 "未设置上下文不应追加租户条件: {}",
3848 sql
3849 );
3850 assert_eq!(params.len(), 0);
3851 Ok(())
3852 }
3853
3854 struct TcUsersTable;
3857 impl crate::typed::TypedTable for TcUsersTable {
3858 const NAME: &'static str = "users";
3859 }
3860 struct TcColId;
3861 impl crate::typed::TypedColumn for TcColId {
3862 const NAME: &'static str = "id";
3863 type Table = TcUsersTable;
3864 type RustType = i64;
3865 type SqlType = crate::typed_ast::Untyped;
3866 }
3867 struct TcColName;
3868 impl crate::typed::TypedColumn for TcColName {
3869 const NAME: &'static str = "name";
3870 type Table = TcUsersTable;
3871 type RustType = String;
3872 type SqlType = crate::typed_ast::Untyped;
3873 }
3874
3875 #[test]
3876 fn test_where_eq_typed() -> Result<(), crate::DbError> {
3877 let dialect = get_dialect(DbType::MySQL)?;
3878 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3879 .where_eq_typed::<TcColId>(Value::I64(42))
3880 .build_select_with_params();
3881 assert!(sql.contains("`id` = ?"));
3882 assert_eq!(params[0], Value::I64(42));
3883 Ok(())
3884 }
3885
3886 #[test]
3887 fn test_order_by_typed() -> Result<(), crate::DbError> {
3888 let dialect = get_dialect(DbType::MySQL)?;
3889 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
3890 .order_by_typed::<TcColName>()
3891 .build_select_with_params();
3892 assert!(sql.contains("ORDER BY"));
3893 assert!(sql.contains("`name`"));
3894 Ok(())
3895 }
3896
3897 #[test]
3898 fn test_select_typed() -> Result<(), crate::DbError> {
3899 let dialect = get_dialect(DbType::MySQL)?;
3900 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
3901 .select_typed::<TcColId>()
3902 .select_typed::<TcColName>()
3903 .build_select_with_params();
3904 assert!(sql.contains("SELECT"));
3905 assert!(sql.contains("id"));
3907 assert!(sql.contains("name"));
3908 Ok(())
3909 }
3910
3911 #[test]
3912 fn test_where_null_typed() -> Result<(), crate::DbError> {
3913 let dialect = get_dialect(DbType::MySQL)?;
3914 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
3915 .where_null_typed::<TcColName>()
3916 .build_select_with_params();
3917 assert!(sql.contains("`name` IS NULL"));
3918 Ok(())
3919 }
3920
3921 #[test]
3922 fn test_where_not_null_typed() -> Result<(), crate::DbError> {
3923 let dialect = get_dialect(DbType::MySQL)?;
3924 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
3925 .where_not_null_typed::<TcColName>()
3926 .build_select_with_params();
3927 assert!(sql.contains("`name` IS NOT NULL"));
3928 Ok(())
3929 }
3930
3931 #[test]
3932 fn test_group_by_typed() -> Result<(), crate::DbError> {
3933 let dialect = get_dialect(DbType::MySQL)?;
3934 let (sql, _) = QueryBuilder::<TestModel>::new(dialect)
3935 .group_by_typed::<TcColName>()
3936 .build_select_with_params();
3937 assert!(sql.contains("GROUP BY"));
3938 assert!(sql.contains("`name`"));
3939 Ok(())
3940 }
3941
3942 #[test]
3943 fn test_where_gt_typed() -> Result<(), crate::DbError> {
3944 let dialect = get_dialect(DbType::MySQL)?;
3945 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3946 .where_gt_typed::<TcColId>(Value::I64(10))
3947 .build_select_with_params();
3948 assert!(sql.contains("`id` > ?"));
3949 assert_eq!(params[0], Value::I64(10));
3950 Ok(())
3951 }
3952
3953 #[test]
3956 fn test_lock_for_update_mysql() -> Result<(), crate::DbError> {
3957 let dialect = get_dialect(DbType::MySQL)?;
3958 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3959 .table("users")
3960 .where_eq("id", Value::I64(1))
3961 .lock_for_update()?
3962 .build_select_with_params();
3963 assert!(sql.contains("SELECT * FROM `users`"));
3964 assert!(sql.contains("WHERE `id` = ?"));
3965 assert!(sql.contains("FOR UPDATE"));
3966 assert_eq!(params.len(), 1);
3967 assert_eq!(params[0], Value::I64(1));
3968 Ok(())
3969 }
3970
3971 #[test]
3972 fn test_lock_shared_mysql() -> Result<(), crate::DbError> {
3973 let dialect = get_dialect(DbType::MySQL)?;
3974 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3975 .table("users")
3976 .where_eq("id", Value::I64(1))
3977 .lock_shared()?
3978 .build_select_with_params();
3979 assert!(sql.contains("SELECT * FROM `users`"));
3980 assert!(sql.contains("WHERE `id` = ?"));
3981 assert!(sql.contains("LOCK IN SHARE MODE"));
3982 assert_eq!(params.len(), 1);
3983 assert_eq!(params[0], Value::I64(1));
3984 Ok(())
3985 }
3986
3987 #[test]
3988 fn test_lock_for_update_postgresql() -> Result<(), crate::DbError> {
3989 let dialect = get_dialect(DbType::PostgreSQL)?;
3990 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3991 .table("users")
3992 .where_eq("id", Value::I64(1))
3993 .lock_for_update()?
3994 .build_select_with_params();
3995 assert!(sql.contains("SELECT * FROM \"users\""));
3996 assert!(sql.contains("WHERE \"id\" = ?"));
3997 assert!(sql.contains("FOR UPDATE"));
3998 assert_eq!(params.len(), 1);
3999 assert_eq!(params[0], Value::I64(1));
4000 Ok(())
4001 }
4002
4003 #[test]
4004 fn test_lock_shared_postgresql() -> Result<(), crate::DbError> {
4005 let dialect = get_dialect(DbType::PostgreSQL)?;
4006 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4007 .table("users")
4008 .where_eq("id", Value::I64(1))
4009 .lock_shared()?
4010 .build_select_with_params();
4011 assert!(sql.contains("SELECT * FROM \"users\""));
4012 assert!(sql.contains("WHERE \"id\" = ?"));
4013 assert!(sql.contains("FOR SHARE"));
4014 assert_eq!(params.len(), 1);
4015 assert_eq!(params[0], Value::I64(1));
4016 Ok(())
4017 }
4018
4019 #[test]
4020 fn test_lock_for_update_sqlite_should_fail() {
4021 let dialect = get_dialect(DbType::Sqlite).unwrap();
4022 let result = QueryBuilder::<TestModel>::new(dialect)
4023 .table("users")
4024 .where_eq("id", Value::I64(1))
4025 .lock_for_update();
4026 assert!(result.is_err(), "SQLite 不应支持 FOR UPDATE 锁");
4027 let err = result.err().unwrap();
4028 assert!(
4029 format!("{:?}", err).contains("FOR UPDATE lock is not supported"),
4030 "错误信息应说明不支持行锁"
4031 );
4032 }
4033
4034 #[test]
4035 fn test_lock_shared_sqlite_should_fail() {
4036 let dialect = get_dialect(DbType::Sqlite).unwrap();
4037 let result = QueryBuilder::<TestModel>::new(dialect)
4038 .table("users")
4039 .where_eq("id", Value::I64(1))
4040 .lock_shared();
4041 assert!(result.is_err(), "SQLite 不应支持共享锁");
4042 let err = result.err().unwrap();
4043 assert!(
4044 format!("{:?}", err).contains("Shared lock is not supported"),
4045 "错误信息应说明不支持共享锁"
4046 );
4047 }
4048
4049 #[test]
4050 fn test_lock_with_limit_and_offset() -> Result<(), crate::DbError> {
4051 let dialect = get_dialect(DbType::MySQL)?;
4052 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4053 .table("users")
4054 .where_eq("status", Value::String("active".into()))
4055 .limit(10)
4056 .offset(20)
4057 .lock_for_update()?
4058 .build_select_with_params();
4059 assert!(sql.contains("WHERE `status` = ?"));
4060 assert!(sql.contains("LIMIT 10"));
4061 assert!(sql.contains("OFFSET 20"));
4062 assert!(sql.contains("FOR UPDATE"));
4063 assert_eq!(params.len(), 1);
4064 Ok(())
4065 }
4066
4067 #[test]
4070 fn test_insert_or_ignore_mysql() -> Result<(), crate::DbError> {
4071 let dialect = get_dialect(DbType::MySQL)?;
4072 let mut data = std::collections::HashMap::new();
4073 data.insert("name".to_string(), Value::String("Alice".into()));
4074 data.insert("age".to_string(), Value::I64(30));
4075
4076 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4077 .table("users")
4078 .insert_or_ignore()
4079 .build_insert_with_params(&data);
4080 assert!(sql.contains("INSERT IGNORE INTO `users`"));
4081 assert!(sql.contains("`name`"), "SQL 应包含 name 列: {}", sql);
4083 assert!(sql.contains("`age`"), "SQL 应包含 age 列: {}", sql);
4084 assert!(sql.contains("VALUES (?, ?)"));
4085 assert_eq!(params.len(), 2);
4086 Ok(())
4087 }
4088
4089 #[test]
4090 fn test_insert_or_ignore_postgresql() -> Result<(), crate::DbError> {
4091 let dialect = get_dialect(DbType::PostgreSQL)?;
4092 let mut data = std::collections::HashMap::new();
4093 data.insert("name".to_string(), Value::String("Bob".into()));
4094
4095 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4096 .table("users")
4097 .insert_or_ignore()
4098 .build_insert_with_params(&data);
4099 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4100 assert!(sql.contains("(\"name\")"));
4101 assert!(sql.contains("VALUES (?)"));
4102 assert_eq!(params.len(), 1);
4103 assert_eq!(params[0], Value::String("Bob".into()));
4104 Ok(())
4105 }
4106
4107 #[test]
4108 fn test_insert_or_ignore_sqlite() -> Result<(), crate::DbError> {
4109 let dialect = get_dialect(DbType::Sqlite)?;
4110 let mut data = std::collections::HashMap::new();
4111 data.insert("name".to_string(), Value::String("Charlie".into()));
4112
4113 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4114 .table("users")
4115 .insert_or_ignore()
4116 .build_insert_with_params(&data);
4117 assert!(sql.contains("INSERT OR IGNORE INTO \"users\""));
4118 assert!(sql.contains("(\"name\")"));
4119 assert!(sql.contains("VALUES (?)"));
4120 assert_eq!(params.len(), 1);
4121 assert_eq!(params[0], Value::String("Charlie".into()));
4122 Ok(())
4123 }
4124
4125 #[test]
4126 fn test_insert_normal_without_ignore() -> Result<(), crate::DbError> {
4127 let dialect = get_dialect(DbType::MySQL)?;
4128 let mut data = std::collections::HashMap::new();
4129 data.insert("name".to_string(), Value::String("Dave".into()));
4130
4131 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4132 .table("users")
4133 .build_insert_with_params(&data);
4134 assert!(sql.contains("INSERT INTO `users`"));
4135 assert!(!sql.contains("IGNORE"), "普通插入不应包含 IGNORE");
4136 assert_eq!(params.len(), 1);
4137 assert_eq!(params[0], Value::String("Dave".into()));
4138 Ok(())
4139 }
4140
4141 #[test]
4142 fn test_insert_or_ignore_empty_data() -> Result<(), crate::DbError> {
4143 let dialect = get_dialect(DbType::MySQL)?;
4144 let data = std::collections::HashMap::new();
4145
4146 let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
4147 .table("users")
4148 .insert_or_ignore()
4149 .build_insert_with_params(&data);
4150 assert!(sql.is_empty(), "空数据应返回空 SQL");
4151 assert!(params.is_empty());
4152 Ok(())
4153 }
4154
4155 #[test]
4158 fn test_dialect_supports_lock_for_update() -> Result<(), crate::DbError> {
4159 let mysql = get_dialect(DbType::MySQL)?;
4160 let pg = get_dialect(DbType::PostgreSQL)?;
4161 let sqlite = get_dialect(DbType::Sqlite)?;
4162 let clickhouse = get_dialect(DbType::ClickHouse)?;
4163 let duckdb = get_dialect(DbType::DuckDB)?;
4164
4165 assert!(mysql.supports_lock_for_update(), "MySQL 应支持 FOR UPDATE");
4166 assert!(
4167 pg.supports_lock_for_update(),
4168 "PostgreSQL 应支持 FOR UPDATE"
4169 );
4170 assert!(
4171 !sqlite.supports_lock_for_update(),
4172 "SQLite 不应支持 FOR UPDATE"
4173 );
4174 assert!(
4175 !clickhouse.supports_lock_for_update(),
4176 "ClickHouse 是列式 OLAP,不应支持 FOR UPDATE"
4177 );
4178 assert!(
4179 !duckdb.supports_lock_for_update(),
4180 "DuckDB 不应支持 FOR UPDATE"
4181 );
4182 Ok(())
4183 }
4184
4185 #[test]
4186 fn test_dialect_supports_lock_shared() -> Result<(), crate::DbError> {
4187 let mysql = get_dialect(DbType::MySQL)?;
4188 let pg = get_dialect(DbType::PostgreSQL)?;
4189 let sqlite = get_dialect(DbType::Sqlite)?;
4190 let clickhouse = get_dialect(DbType::ClickHouse)?;
4191 let duckdb = get_dialect(DbType::DuckDB)?;
4192
4193 assert!(mysql.supports_lock_shared(), "MySQL 应支持共享锁");
4194 assert!(pg.supports_lock_shared(), "PostgreSQL 应支持共享锁");
4195 assert!(!sqlite.supports_lock_shared(), "SQLite 不应支持共享锁");
4196 assert!(
4197 !clickhouse.supports_lock_shared(),
4198 "ClickHouse 是列式 OLAP,不应支持共享锁"
4199 );
4200 assert!(!duckdb.supports_lock_shared(), "DuckDB 不应支持共享锁");
4201 Ok(())
4202 }
4203
4204 #[test]
4205 fn test_get_lock_type_and_is_insert_or_ignore() -> Result<(), crate::DbError> {
4206 let dialect = get_dialect(DbType::MySQL)?;
4207
4208 let builder = QueryBuilder::<TestModel>::new(dialect);
4210 assert!(builder.get_lock_type().is_none(), "默认无锁");
4211 assert!(!builder.is_insert_or_ignore(), "默认不忽略插入");
4212
4213 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4215 .table("users")
4216 .lock_for_update()?;
4217 assert_eq!(builder.get_lock_type(), Some(LockType::ForUpdate));
4218
4219 let builder = QueryBuilder::<TestModel>::new(get_dialect(DbType::MySQL)?)
4221 .table("users")
4222 .insert_or_ignore();
4223 assert!(builder.is_insert_or_ignore());
4224
4225 Ok(())
4226 }
4227
4228 struct TestModelWithColumns;
4229 impl Model for TestModelWithColumns {
4230 type PrimaryKey = i64;
4231 fn table_name() -> &'static str {
4232 "test_with_cols"
4233 }
4234 fn pk(&self) -> Self::PrimaryKey {
4235 0
4236 }
4237 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
4238 }
4239 impl crate::model::ModelExt for TestModelWithColumns {
4240 fn columns() -> Vec<&'static str> {
4241 vec!["id", "name", "email", "avatar", "blob_data"]
4242 }
4243 fn fillable() -> Vec<&'static str> {
4244 vec!["name", "email", "avatar", "blob_data"]
4245 }
4246 fn guarded() -> Vec<&'static str> {
4247 vec!["id"]
4248 }
4249 fn hidden() -> Vec<&'static str> {
4250 vec!["blob_data"]
4251 }
4252 fn relations() -> std::collections::HashMap<&'static str, crate::model::Relation> {
4253 std::collections::HashMap::new()
4254 }
4255 fn fill(&mut self, _data: std::collections::HashMap<String, crate::value::Value>) {}
4256 fn to_json(&self) -> serde_json::Value {
4257 serde_json::Value::Null
4258 }
4259 }
4260
4261 #[test]
4262 fn test_select_exclude_basic() -> Result<(), crate::DbError> {
4263 let dialect = get_dialect(DbType::MySQL)?;
4264 let builder = QueryBuilder::<TestModelWithColumns>::new(dialect)
4265 .table("users")
4266 .select_exclude(&["avatar", "blob_data"])?;
4267 let sql = builder.build_select();
4268 assert!(sql.contains("id"));
4269 assert!(sql.contains("name"));
4270 assert!(sql.contains("email"));
4271 assert!(!sql.contains("avatar"));
4272 assert!(!sql.contains("blob_data"));
4273 Ok(())
4274 }
4275
4276 #[test]
4277 fn test_select_exclude_nonexistent_field() {
4278 let dialect = get_dialect(DbType::MySQL).unwrap();
4279 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4280 .table("users")
4281 .select_exclude(&["nonexistent"]);
4282 assert!(result.is_err());
4283 }
4284
4285 #[test]
4286 fn test_select_exclude_all_fields() {
4287 let dialect = get_dialect(DbType::MySQL).unwrap();
4288 let result = QueryBuilder::<TestModelWithColumns>::new(dialect)
4289 .table("users")
4290 .select_exclude(&["id", "name", "email", "avatar", "blob_data"]);
4291 assert!(result.is_err());
4292 let err = result.unwrap_err();
4293 assert!(matches!(err, crate::DbError::InvalidInput(_)));
4294 }
4295}