1use crate::clause::{Limit, Offset, OrderBy, Where};
4use crate::eager::{EagerLoader, IncludePath, build_join_clause, find_relationship};
5use crate::expr::{Dialect, Expr};
6use crate::join::Join;
7use asupersync::{Cx, Outcome};
8use sqlmodel_core::{Connection, Model, RelationshipKind, Value};
9use std::marker::PhantomData;
10
11#[derive(Debug, Clone)]
16#[allow(dead_code)] struct EagerJoinInfo {
18 relationship_name: &'static str,
20 related_table: &'static str,
22 kind: RelationshipKind,
24 nested: Vec<IncludePath>,
26}
27
28#[derive(Debug, Clone)]
33pub struct Select<M: Model> {
34 columns: Vec<String>,
36 where_clause: Option<Where>,
38 order_by: Vec<OrderBy>,
40 joins: Vec<Join>,
42 limit: Option<Limit>,
44 offset: Option<Offset>,
46 group_by: Vec<String>,
48 having: Option<Where>,
50 distinct: bool,
52 for_update: bool,
54 eager_loader: Option<EagerLoader<M>>,
56 _marker: PhantomData<M>,
58}
59
60impl<M: Model> Select<M> {
61 pub fn new() -> Self {
63 Self {
64 columns: Vec::new(),
65 where_clause: None,
66 order_by: Vec::new(),
67 joins: Vec::new(),
68 limit: None,
69 offset: None,
70 group_by: Vec::new(),
71 having: None,
72 distinct: false,
73 for_update: false,
74 eager_loader: None,
75 _marker: PhantomData,
76 }
77 }
78
79 pub fn columns(mut self, cols: &[&str]) -> Self {
81 self.columns = cols.iter().map(|&s| s.to_string()).collect();
82 self
83 }
84
85 pub fn filter(mut self, expr: Expr) -> Self {
87 self.where_clause = Some(match self.where_clause {
88 Some(existing) => existing.and(expr),
89 None => Where::new(expr),
90 });
91 self
92 }
93
94 pub fn or_filter(mut self, expr: Expr) -> Self {
96 self.where_clause = Some(match self.where_clause {
97 Some(existing) => existing.or(expr),
98 None => Where::new(expr),
99 });
100 self
101 }
102
103 pub fn order_by(mut self, order: OrderBy) -> Self {
105 self.order_by.push(order);
106 self
107 }
108
109 pub fn join(mut self, join: Join) -> Self {
111 self.joins.push(join);
112 self
113 }
114
115 pub fn limit(mut self, n: u64) -> Self {
117 self.limit = Some(Limit(n));
118 self
119 }
120
121 pub fn offset(mut self, n: u64) -> Self {
123 self.offset = Some(Offset(n));
124 self
125 }
126
127 pub fn group_by(mut self, cols: &[&str]) -> Self {
129 self.group_by.extend(cols.iter().map(|&s| s.to_string()));
130 self
131 }
132
133 pub fn having(mut self, expr: Expr) -> Self {
135 self.having = Some(match self.having {
136 Some(existing) => existing.and(expr),
137 None => Where::new(expr),
138 });
139 self
140 }
141
142 pub fn distinct(mut self) -> Self {
144 self.distinct = true;
145 self
146 }
147
148 pub fn for_update(mut self) -> Self {
150 self.for_update = true;
151 self
152 }
153
154 pub fn eager(mut self, loader: EagerLoader<M>) -> Self {
165 self.eager_loader = Some(loader);
166 self
167 }
168
169 #[tracing::instrument(level = "trace", skip(self))]
173 fn build_eager(&self) -> (String, Vec<Value>, Vec<EagerJoinInfo>) {
174 let mut sql = String::new();
175 let mut params = Vec::new();
176 let mut join_info = Vec::new();
177
178 let parent_cols: Vec<&str> = M::fields().iter().map(|f| f.name).collect();
180
181 sql.push_str("SELECT ");
183 if self.distinct {
184 sql.push_str("DISTINCT ");
185 }
186
187 let mut col_parts = Vec::new();
189 for col in &parent_cols {
190 col_parts.push(format!(
191 "{}.{} AS {}__{}",
192 M::TABLE_NAME,
193 col,
194 M::TABLE_NAME,
195 col
196 ));
197 }
198
199 if let Some(loader) = &self.eager_loader {
201 for include in loader.includes() {
202 if let Some(rel) = find_relationship::<M>(include.relationship) {
203 join_info.push(EagerJoinInfo {
206 relationship_name: include.relationship,
207 related_table: rel.related_table,
208 kind: rel.kind,
209 nested: include.nested.clone(),
210 });
211
212 col_parts.push(format!("{}.*", rel.related_table));
215 }
216 }
217 }
218
219 sql.push_str(&col_parts.join(", "));
220
221 sql.push_str(" FROM ");
223 sql.push_str(M::TABLE_NAME);
224
225 if let Some(loader) = &self.eager_loader {
227 for include in loader.includes() {
228 if let Some(rel) = find_relationship::<M>(include.relationship) {
229 let (join_sql, join_params) =
230 build_join_clause(M::TABLE_NAME, rel, params.len());
231 sql.push_str(&join_sql);
232 params.extend(join_params);
233 }
234 }
235 }
236
237 for join in &self.joins {
239 sql.push_str(&join.build(&mut params, 0));
240 }
241
242 if let Some(where_clause) = &self.where_clause {
244 let (where_sql, where_params) = where_clause.build_with_offset(params.len());
245 sql.push_str(" WHERE ");
246 sql.push_str(&where_sql);
247 params.extend(where_params);
248 }
249
250 if !self.group_by.is_empty() {
252 sql.push_str(" GROUP BY ");
253 sql.push_str(&self.group_by.join(", "));
254 }
255
256 if let Some(having) = &self.having {
258 let (having_sql, having_params) = having.build_with_offset(params.len());
259 sql.push_str(" HAVING ");
260 sql.push_str(&having_sql);
261 params.extend(having_params);
262 }
263
264 if !self.order_by.is_empty() {
266 sql.push_str(" ORDER BY ");
267 let order_strs: Vec<_> = self
268 .order_by
269 .iter()
270 .map(|o| o.build(Dialect::default(), &mut params, 0))
271 .collect();
272 sql.push_str(&order_strs.join(", "));
273 }
274
275 if let Some(Limit(n)) = self.limit {
277 sql.push_str(&format!(" LIMIT {}", n));
278 }
279
280 if let Some(Offset(n)) = self.offset {
282 sql.push_str(&format!(" OFFSET {}", n));
283 }
284
285 (sql, params, join_info)
286 }
287
288 #[tracing::instrument(level = "debug", skip(self, cx, conn))]
312 pub async fn all_eager<C: Connection>(
313 self,
314 cx: &Cx,
315 conn: &C,
316 ) -> Outcome<Vec<M>, sqlmodel_core::Error> {
317 if !self.eager_loader.as_ref().is_some_and(|e| e.has_includes()) {
319 tracing::trace!("No eager loading configured, falling back to regular all()");
320 return self.all(cx, conn).await;
321 }
322
323 let (sql, params, join_info) = self.build_eager();
324
325 tracing::debug!(
326 table = M::TABLE_NAME,
327 includes = join_info.len(),
328 "Executing eager loading query"
329 );
330 tracing::trace!(sql = %sql, "Eager SQL");
331
332 let rows = conn.query(cx, &sql, ¶ms).await;
333
334 rows.and_then(|rows| {
335 tracing::debug!(row_count = rows.len(), "Processing eager query results");
336
337 let mut seen_pks = std::collections::HashSet::new();
339 let mut models = Vec::with_capacity(rows.len());
340
341 for row in &rows {
342 let parent_row = row.subset_by_prefix(M::TABLE_NAME);
344
345 if parent_row.is_empty() {
347 tracing::warn!(
348 table = M::TABLE_NAME,
349 "Row has no columns with parent table prefix"
350 );
351 match M::from_row(row) {
353 Ok(model) => {
354 models.push(model);
355 }
356 Err(e) => {
357 tracing::debug!(error = %e, "Failed to parse model from row");
358 return Outcome::Err(e);
359 }
360 }
361 continue;
362 }
363
364 match M::from_row(&parent_row) {
366 Ok(model) => {
367 let pk = model.primary_key_value();
369 let pk_hash = {
370 use std::hash::{Hash, Hasher};
371 let mut hasher = std::collections::hash_map::DefaultHasher::new();
372 format!("{:?}", pk).hash(&mut hasher);
374 hasher.finish()
375 };
376
377 if seen_pks.insert(pk_hash) {
378 models.push(model);
379 }
380 }
381 Err(e) => {
382 tracing::debug!(error = %e, "Failed to parse model from prefixed row");
383 return Outcome::Err(e);
384 }
385 }
386 }
387
388 tracing::debug!(
389 unique_models = models.len(),
390 "Eager loading complete (deduplicated)"
391 );
392 Outcome::Ok(models)
393 })
394 }
395
396 pub fn build(&self) -> (String, Vec<Value>) {
398 let mut sql = String::new();
399 let mut params = Vec::new();
400
401 sql.push_str("SELECT ");
403 if self.distinct {
404 sql.push_str("DISTINCT ");
405 }
406
407 if self.columns.is_empty() {
408 sql.push('*');
409 } else {
410 sql.push_str(&self.columns.join(", "));
411 }
412
413 sql.push_str(" FROM ");
415 sql.push_str(M::TABLE_NAME);
416
417 for join in &self.joins {
419 sql.push_str(&join.build(&mut params, 0));
420 }
421
422 if let Some(where_clause) = &self.where_clause {
424 let (where_sql, where_params) = where_clause.build_with_offset(params.len());
425 sql.push_str(" WHERE ");
426 sql.push_str(&where_sql);
427 params.extend(where_params);
428 }
429
430 if !self.group_by.is_empty() {
432 sql.push_str(" GROUP BY ");
433 sql.push_str(&self.group_by.join(", "));
434 }
435
436 if let Some(having) = &self.having {
438 let (having_sql, having_params) = having.build_with_offset(params.len());
439 sql.push_str(" HAVING ");
440 sql.push_str(&having_sql);
441 params.extend(having_params);
442 }
443
444 if !self.order_by.is_empty() {
446 sql.push_str(" ORDER BY ");
447 let order_strs: Vec<_> = self
448 .order_by
449 .iter()
450 .map(|o| o.build(Dialect::default(), &mut params, 0))
451 .collect();
452 sql.push_str(&order_strs.join(", "));
453 }
454
455 if let Some(Limit(n)) = self.limit {
457 sql.push_str(&format!(" LIMIT {}", n));
458 }
459
460 if let Some(Offset(n)) = self.offset {
462 sql.push_str(&format!(" OFFSET {}", n));
463 }
464
465 if self.for_update {
467 sql.push_str(" FOR UPDATE");
468 }
469
470 (sql, params)
471 }
472
473 pub fn into_exists(self) -> Expr {
495 let (sql, params) = self.build_exists_subquery();
497 Expr::exists(sql, params)
498 }
499
500 pub fn into_not_exists(self) -> Expr {
521 let (sql, params) = self.build_exists_subquery();
522 Expr::not_exists(sql, params)
523 }
524
525 pub fn into_lateral_join(
551 self,
552 alias: impl Into<String>,
553 join_type: crate::JoinType,
554 on: Expr,
555 ) -> crate::Join {
556 let (sql, params) = self.build();
557 crate::Join::lateral(join_type, sql, alias, on, params)
558 }
559
560 fn build_exists_subquery(&self) -> (String, Vec<Value>) {
562 let mut sql = String::new();
563 let mut params = Vec::new();
564
565 sql.push_str("SELECT 1 FROM ");
567 sql.push_str(M::TABLE_NAME);
568
569 for join in &self.joins {
571 sql.push_str(&join.build(&mut params, 0));
572 }
573
574 if let Some(where_clause) = &self.where_clause {
576 let (where_sql, where_params) = where_clause.build_with_offset(params.len());
577 sql.push_str(" WHERE ");
578 sql.push_str(&where_sql);
579 params.extend(where_params);
580 }
581
582 if !self.group_by.is_empty() {
584 sql.push_str(" GROUP BY ");
585 sql.push_str(&self.group_by.join(", "));
586 }
587
588 if let Some(having) = &self.having {
590 let (having_sql, having_params) = having.build_with_offset(params.len());
591 sql.push_str(" HAVING ");
592 sql.push_str(&having_sql);
593 params.extend(having_params);
594 }
595
596 (sql, params)
599 }
600
601 pub async fn all<C: Connection>(
603 self,
604 cx: &Cx,
605 conn: &C,
606 ) -> Outcome<Vec<M>, sqlmodel_core::Error> {
607 let (sql, params) = self.build();
608 let rows = conn.query(cx, &sql, ¶ms).await;
609
610 rows.and_then(|rows| {
611 let mut models = Vec::with_capacity(rows.len());
612 for row in &rows {
613 match M::from_row(row) {
614 Ok(model) => models.push(model),
615 Err(e) => return Outcome::Err(e),
616 }
617 }
618 Outcome::Ok(models)
619 })
620 }
621
622 pub async fn first<C: Connection>(
624 self,
625 cx: &Cx,
626 conn: &C,
627 ) -> Outcome<Option<M>, sqlmodel_core::Error> {
628 let query = self.limit(1);
629 let (sql, params) = query.build();
630 let row = conn.query_one(cx, &sql, ¶ms).await;
631
632 row.and_then(|opt_row| match opt_row {
633 Some(row) => match M::from_row(&row) {
634 Ok(model) => Outcome::Ok(Some(model)),
635 Err(e) => Outcome::Err(e),
636 },
637 None => Outcome::Ok(None),
638 })
639 }
640
641 pub async fn one<C: Connection>(self, cx: &Cx, conn: &C) -> Outcome<M, sqlmodel_core::Error> {
643 match self.first(cx, conn).await {
644 Outcome::Ok(Some(model)) => Outcome::Ok(model),
645 Outcome::Ok(None) => Outcome::Err(sqlmodel_core::Error::Custom(
646 "Expected one row, found none".to_string(),
647 )),
648 Outcome::Err(e) => Outcome::Err(e),
649 Outcome::Cancelled(r) => Outcome::Cancelled(r),
650 Outcome::Panicked(p) => Outcome::Panicked(p),
651 }
652 }
653
654 pub async fn count<C: Connection>(
656 self,
657 cx: &Cx,
658 conn: &C,
659 ) -> Outcome<u64, sqlmodel_core::Error> {
660 let mut count_query = self;
661 count_query.columns = vec!["COUNT(*) as count".to_string()];
662 count_query.order_by.clear();
663 count_query.limit = None;
664 count_query.offset = None;
665
666 let (sql, params) = count_query.build();
667 let row = conn.query_one(cx, &sql, ¶ms).await;
668
669 row.and_then(|opt_row| match opt_row {
670 Some(row) => match row.get_named::<i64>("count") {
671 Ok(count) => Outcome::Ok(count as u64),
672 Err(e) => Outcome::Err(e),
673 },
674 None => Outcome::Ok(0),
675 })
676 }
677
678 pub async fn exists<C: Connection>(
680 self,
681 cx: &Cx,
682 conn: &C,
683 ) -> Outcome<bool, sqlmodel_core::Error> {
684 let count = self.count(cx, conn).await;
685 count.map(|n| n > 0)
686 }
687}
688
689impl<M: Model> Default for Select<M> {
690 fn default() -> Self {
691 Self::new()
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use sqlmodel_core::{Error, FieldInfo, Result, Row, Value};
699
700 #[derive(Debug, Clone)]
701 struct Hero;
702
703 impl Model for Hero {
704 const TABLE_NAME: &'static str = "heroes";
705 const PRIMARY_KEY: &'static [&'static str] = &["id"];
706
707 fn fields() -> &'static [FieldInfo] {
708 &[]
709 }
710
711 fn to_row(&self) -> Vec<(&'static str, Value)> {
712 Vec::new()
713 }
714
715 fn from_row(_row: &Row) -> Result<Self> {
716 Err(Error::Custom("not used in tests".to_string()))
717 }
718
719 fn primary_key_value(&self) -> Vec<Value> {
720 Vec::new()
721 }
722
723 fn is_new(&self) -> bool {
724 true
725 }
726 }
727
728 #[test]
729 fn build_collects_params_across_joins_where_having() {
730 let query = Select::<Hero>::new()
731 .join(Join::inner(
732 "teams",
733 Expr::qualified("teams", "active").eq(true),
734 ))
735 .filter(Expr::col("age").gt(18))
736 .group_by(&["team_id"])
737 .having(Expr::col("count").gt(1));
738
739 let (sql, params) = query.build();
740
741 assert_eq!(
742 sql,
743 "SELECT * FROM heroes INNER JOIN teams ON \"teams\".\"active\" = $1 WHERE \"age\" > $2 GROUP BY team_id HAVING \"count\" > $3"
744 );
745 assert_eq!(
746 params,
747 vec![Value::Bool(true), Value::Int(18), Value::Int(1)]
748 );
749 }
750
751 #[test]
752 fn test_select_all_columns() {
753 let query = Select::<Hero>::new();
754 let (sql, params) = query.build();
755
756 assert_eq!(sql, "SELECT * FROM heroes");
757 assert!(params.is_empty());
758 }
759
760 #[test]
761 fn test_select_specific_columns() {
762 let query = Select::<Hero>::new().columns(&["id", "name", "power"]);
763 let (sql, params) = query.build();
764
765 assert_eq!(sql, "SELECT id, name, power FROM heroes");
766 assert!(params.is_empty());
767 }
768
769 #[test]
770 fn test_select_distinct() {
771 let query = Select::<Hero>::new().columns(&["team_id"]).distinct();
772 let (sql, params) = query.build();
773
774 assert_eq!(sql, "SELECT DISTINCT team_id FROM heroes");
775 assert!(params.is_empty());
776 }
777
778 #[test]
779 fn test_select_with_simple_filter() {
780 let query = Select::<Hero>::new().filter(Expr::col("active").eq(true));
781 let (sql, params) = query.build();
782
783 assert_eq!(sql, "SELECT * FROM heroes WHERE \"active\" = $1");
784 assert_eq!(params, vec![Value::Bool(true)]);
785 }
786
787 #[test]
788 fn test_select_with_multiple_and_filters() {
789 let query = Select::<Hero>::new()
790 .filter(Expr::col("active").eq(true))
791 .filter(Expr::col("age").gt(18));
792 let (sql, params) = query.build();
793
794 assert_eq!(
795 sql,
796 "SELECT * FROM heroes WHERE \"active\" = $1 AND \"age\" > $2"
797 );
798 assert_eq!(params, vec![Value::Bool(true), Value::Int(18)]);
799 }
800
801 #[test]
802 fn test_select_with_or_filter() {
803 let query = Select::<Hero>::new()
804 .filter(Expr::col("role").eq("warrior"))
805 .or_filter(Expr::col("role").eq("mage"));
806 let (sql, params) = query.build();
807
808 assert_eq!(
809 sql,
810 "SELECT * FROM heroes WHERE \"role\" = $1 OR \"role\" = $2"
811 );
812 assert_eq!(
813 params,
814 vec![
815 Value::Text("warrior".to_string()),
816 Value::Text("mage".to_string())
817 ]
818 );
819 }
820
821 #[test]
822 fn test_select_with_order_by_asc() {
823 let query = Select::<Hero>::new().order_by(OrderBy::asc(Expr::col("name")));
824 let (sql, params) = query.build();
825
826 assert_eq!(sql, "SELECT * FROM heroes ORDER BY \"name\" ASC");
827 assert!(params.is_empty());
828 }
829
830 #[test]
831 fn test_select_with_order_by_desc() {
832 let query = Select::<Hero>::new().order_by(OrderBy::desc(Expr::col("created_at")));
833 let (sql, params) = query.build();
834
835 assert_eq!(sql, "SELECT * FROM heroes ORDER BY \"created_at\" DESC");
836 assert!(params.is_empty());
837 }
838
839 #[test]
840 fn test_select_with_multiple_order_by() {
841 let query = Select::<Hero>::new()
842 .order_by(OrderBy::asc(Expr::col("team_id")))
843 .order_by(OrderBy::asc(Expr::col("name")));
844 let (sql, params) = query.build();
845
846 assert_eq!(
847 sql,
848 "SELECT * FROM heroes ORDER BY \"team_id\" ASC, \"name\" ASC"
849 );
850 assert!(params.is_empty());
851 }
852
853 #[test]
854 fn test_select_with_limit() {
855 let query = Select::<Hero>::new().limit(10);
856 let (sql, params) = query.build();
857
858 assert_eq!(sql, "SELECT * FROM heroes LIMIT 10");
859 assert!(params.is_empty());
860 }
861
862 #[test]
863 fn test_select_with_offset() {
864 let query = Select::<Hero>::new().offset(20);
865 let (sql, params) = query.build();
866
867 assert_eq!(sql, "SELECT * FROM heroes OFFSET 20");
868 assert!(params.is_empty());
869 }
870
871 #[test]
872 fn test_select_with_limit_and_offset() {
873 let query = Select::<Hero>::new().limit(10).offset(20);
874 let (sql, params) = query.build();
875
876 assert_eq!(sql, "SELECT * FROM heroes LIMIT 10 OFFSET 20");
877 assert!(params.is_empty());
878 }
879
880 #[test]
881 fn test_select_with_group_by() {
882 let query = Select::<Hero>::new()
883 .columns(&["team_id", "COUNT(*) as count"])
884 .group_by(&["team_id"]);
885 let (sql, params) = query.build();
886
887 assert_eq!(
888 sql,
889 "SELECT team_id, COUNT(*) as count FROM heroes GROUP BY team_id"
890 );
891 assert!(params.is_empty());
892 }
893
894 #[test]
895 fn test_select_with_multiple_group_by() {
896 let query = Select::<Hero>::new()
897 .columns(&["team_id", "role", "COUNT(*) as count"])
898 .group_by(&["team_id", "role"]);
899 let (sql, params) = query.build();
900
901 assert_eq!(
902 sql,
903 "SELECT team_id, role, COUNT(*) as count FROM heroes GROUP BY team_id, role"
904 );
905 assert!(params.is_empty());
906 }
907
908 #[test]
909 fn test_select_with_for_update() {
910 let query = Select::<Hero>::new()
911 .filter(Expr::col("id").eq(1))
912 .for_update();
913 let (sql, params) = query.build();
914
915 assert_eq!(sql, "SELECT * FROM heroes WHERE \"id\" = $1 FOR UPDATE");
916 assert_eq!(params, vec![Value::Int(1)]);
917 }
918
919 #[test]
920 fn test_select_inner_join() {
921 let query = Select::<Hero>::new().join(Join::inner(
922 "teams",
923 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
924 ));
925 let (sql, _) = query.build();
926
927 assert!(sql.contains("INNER JOIN teams ON"));
928 }
929
930 #[test]
931 fn test_select_left_join() {
932 let query = Select::<Hero>::new().join(Join::left(
933 "teams",
934 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
935 ));
936 let (sql, _) = query.build();
937
938 assert!(sql.contains("LEFT JOIN teams ON"));
939 }
940
941 #[test]
942 fn test_select_right_join() {
943 let query = Select::<Hero>::new().join(Join::right(
944 "teams",
945 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
946 ));
947 let (sql, _) = query.build();
948
949 assert!(sql.contains("RIGHT JOIN teams ON"));
950 }
951
952 #[test]
953 fn test_select_multiple_joins() {
954 let query = Select::<Hero>::new()
955 .join(Join::inner(
956 "teams",
957 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
958 ))
959 .join(Join::left(
960 "powers",
961 Expr::qualified("heroes", "id").eq(Expr::qualified("powers", "hero_id")),
962 ));
963 let (sql, _) = query.build();
964
965 assert!(sql.contains("INNER JOIN teams ON"));
966 assert!(sql.contains("LEFT JOIN powers ON"));
967 }
968
969 #[test]
970 fn test_select_complex_query() {
971 let query = Select::<Hero>::new()
972 .columns(&["heroes.id", "heroes.name", "teams.name as team_name"])
973 .distinct()
974 .join(Join::inner(
975 "teams",
976 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
977 ))
978 .filter(Expr::col("active").eq(true))
979 .filter(Expr::col("level").gt(10))
980 .group_by(&["heroes.id", "heroes.name", "teams.name"])
981 .having(Expr::col("score").gt(100))
982 .order_by(OrderBy::desc(Expr::col("level")))
983 .limit(50)
984 .offset(0);
985 let (sql, params) = query.build();
986
987 assert!(sql.starts_with(
988 "SELECT DISTINCT heroes.id, heroes.name, teams.name as team_name FROM heroes"
989 ));
990 assert!(sql.contains("INNER JOIN teams ON"));
991 assert!(sql.contains("WHERE"));
992 assert!(sql.contains("GROUP BY"));
993 assert!(sql.contains("HAVING"));
994 assert!(sql.contains("ORDER BY"));
995 assert!(sql.contains("LIMIT 50"));
996 assert!(sql.contains("OFFSET 0"));
997
998 assert_eq!(params.len(), 3);
1001 }
1002
1003 #[test]
1004 fn test_select_default() {
1005 let query = Select::<Hero>::default();
1006 let (sql, _) = query.build();
1007 assert_eq!(sql, "SELECT * FROM heroes");
1008 }
1009
1010 #[test]
1011 fn test_select_clone() {
1012 let query = Select::<Hero>::new()
1013 .filter(Expr::col("id").eq(1))
1014 .limit(10);
1015 let cloned = query.clone();
1016
1017 let (sql1, params1) = query.build();
1018 let (sql2, params2) = cloned.build();
1019
1020 assert_eq!(sql1, sql2);
1021 assert_eq!(params1, params2);
1022 }
1023
1024 use sqlmodel_core::RelationshipInfo;
1029
1030 #[derive(Debug, Clone)]
1032 struct EagerHero;
1033
1034 impl Model for EagerHero {
1035 const TABLE_NAME: &'static str = "heroes";
1036 const PRIMARY_KEY: &'static [&'static str] = &["id"];
1037 const RELATIONSHIPS: &'static [RelationshipInfo] =
1038 &[
1039 RelationshipInfo::new("team", "teams", RelationshipKind::ManyToOne)
1040 .local_key("team_id"),
1041 ];
1042
1043 fn fields() -> &'static [FieldInfo] {
1044 static FIELDS: &[FieldInfo] = &[
1045 FieldInfo::new("id", "id", sqlmodel_core::SqlType::BigInt),
1046 FieldInfo::new("name", "name", sqlmodel_core::SqlType::Text),
1047 FieldInfo::new("team_id", "team_id", sqlmodel_core::SqlType::BigInt),
1048 ];
1049 FIELDS
1050 }
1051
1052 fn to_row(&self) -> Vec<(&'static str, Value)> {
1053 Vec::new()
1054 }
1055
1056 fn from_row(_row: &Row) -> Result<Self> {
1057 Err(Error::Custom("not used in tests".to_string()))
1058 }
1059
1060 fn primary_key_value(&self) -> Vec<Value> {
1061 Vec::new()
1062 }
1063
1064 fn is_new(&self) -> bool {
1065 true
1066 }
1067 }
1068
1069 #[test]
1070 fn test_select_with_eager_loader() {
1071 let loader = EagerLoader::<EagerHero>::new().include("team");
1072 let query = Select::<EagerHero>::new().eager(loader);
1073
1074 assert!(query.eager_loader.is_some());
1076 assert!(query.eager_loader.as_ref().unwrap().has_includes());
1077 }
1078
1079 #[test]
1080 fn test_select_eager_generates_join() {
1081 let loader = EagerLoader::<EagerHero>::new().include("team");
1082 let query = Select::<EagerHero>::new().eager(loader);
1083
1084 let (sql, params, join_info) = query.build_eager();
1085
1086 assert!(sql.contains("LEFT JOIN teams"));
1088 assert!(sql.contains("heroes.team_id = teams.id"));
1089
1090 assert!(sql.contains("heroes.id AS heroes__id"));
1092 assert!(sql.contains("heroes.name AS heroes__name"));
1093 assert!(sql.contains("heroes.team_id AS heroes__team_id"));
1094
1095 assert_eq!(join_info.len(), 1);
1097 assert!(params.is_empty());
1098 }
1099
1100 #[test]
1101 fn test_select_eager_with_filter() {
1102 let loader = EagerLoader::<EagerHero>::new().include("team");
1103 let query = Select::<EagerHero>::new()
1104 .eager(loader)
1105 .filter(Expr::col("active").eq(true));
1106
1107 let (sql, params, _) = query.build_eager();
1108
1109 assert!(sql.contains("LEFT JOIN teams"));
1110 assert!(sql.contains("WHERE"));
1111 assert!(sql.contains("\"active\" = $1"));
1112 assert_eq!(params, vec![Value::Bool(true)]);
1113 }
1114
1115 #[test]
1116 fn test_select_eager_with_order_and_limit() {
1117 let loader = EagerLoader::<EagerHero>::new().include("team");
1118 let query = Select::<EagerHero>::new()
1119 .eager(loader)
1120 .order_by(OrderBy::asc(Expr::col("name")))
1121 .limit(10)
1122 .offset(5);
1123
1124 let (sql, _, _) = query.build_eager();
1125
1126 assert!(sql.contains("LEFT JOIN teams"));
1127 assert!(sql.contains("ORDER BY"));
1128 assert!(sql.contains("LIMIT 10"));
1129 assert!(sql.contains("OFFSET 5"));
1130 }
1131
1132 #[test]
1133 fn test_select_eager_no_includes_fallback() {
1134 let loader = EagerLoader::<EagerHero>::new();
1136 let query = Select::<EagerHero>::new().eager(loader);
1137
1138 assert!(query.eager_loader.is_some());
1141 assert!(!query.eager_loader.as_ref().unwrap().has_includes());
1142 }
1143
1144 #[test]
1145 fn test_select_eager_distinct() {
1146 let loader = EagerLoader::<EagerHero>::new().include("team");
1147 let query = Select::<EagerHero>::new().eager(loader).distinct();
1148
1149 let (sql, _, _) = query.build_eager();
1150
1151 assert!(sql.starts_with("SELECT DISTINCT"));
1152 }
1153
1154 #[test]
1157 fn test_select_into_exists() {
1158 let exists_expr = Select::<Hero>::new()
1160 .filter(Expr::raw("orders.customer_id = customers.id"))
1161 .into_exists();
1162
1163 let mut params = Vec::new();
1164 let sql = exists_expr.build(&mut params, 0);
1165
1166 assert_eq!(
1168 sql,
1169 "EXISTS (SELECT 1 FROM heroes WHERE orders.customer_id = customers.id)"
1170 );
1171 }
1172
1173 #[test]
1174 fn test_select_into_not_exists() {
1175 let not_exists_expr = Select::<Hero>::new()
1177 .filter(Expr::raw("orders.customer_id = customers.id"))
1178 .into_not_exists();
1179
1180 let mut params = Vec::new();
1181 let sql = not_exists_expr.build(&mut params, 0);
1182
1183 assert_eq!(
1184 sql,
1185 "NOT EXISTS (SELECT 1 FROM heroes WHERE orders.customer_id = customers.id)"
1186 );
1187 }
1188
1189 #[test]
1190 fn test_select_into_exists_with_params() {
1191 let exists_expr = Select::<Hero>::new()
1193 .filter(Expr::col("status").eq("active"))
1194 .into_exists();
1195
1196 let mut params = Vec::new();
1197 let sql = exists_expr.build(&mut params, 0);
1198
1199 assert_eq!(sql, "EXISTS (SELECT 1 FROM heroes WHERE \"status\" = $1)");
1200 assert_eq!(params.len(), 1);
1201 assert_eq!(params[0], Value::Text("active".to_string()));
1202 }
1203
1204 #[test]
1205 fn test_select_into_exists_with_join() {
1206 let exists_expr = Select::<Hero>::new()
1208 .join(Join::inner(
1209 "teams",
1210 Expr::qualified("heroes", "team_id").eq(Expr::qualified("teams", "id")),
1211 ))
1212 .filter(Expr::col("active").eq(true))
1213 .into_exists();
1214
1215 let mut params = Vec::new();
1216 let sql = exists_expr.build(&mut params, 0);
1217
1218 assert!(sql.starts_with("EXISTS (SELECT 1 FROM heroes"));
1219 assert!(sql.contains("INNER JOIN teams ON"));
1220 assert!(sql.contains("WHERE"));
1221 }
1222
1223 #[test]
1224 fn test_select_into_exists_omits_order_by_limit() {
1225 let exists_expr = Select::<Hero>::new()
1228 .filter(Expr::col("active").eq(true))
1229 .order_by(OrderBy::asc(Expr::col("name")))
1230 .limit(10)
1231 .offset(5)
1232 .into_exists();
1233
1234 let mut params = Vec::new();
1235 let sql = exists_expr.build(&mut params, 0);
1236
1237 assert!(!sql.contains("ORDER BY"));
1239 assert!(!sql.contains("LIMIT"));
1240 assert!(!sql.contains("OFFSET"));
1241 assert_eq!(sql, "EXISTS (SELECT 1 FROM heroes WHERE \"active\" = $1)");
1242 }
1243
1244 #[test]
1245 fn test_exists_in_outer_query() {
1246 let has_heroes = Select::<Hero>::new()
1248 .filter(Expr::raw("heroes.team_id = teams.id"))
1249 .into_exists();
1250
1251 let outer_expr = Expr::col("active").eq(true).and(has_heroes);
1254
1255 let mut params = Vec::new();
1256 let sql = outer_expr.build(&mut params, 0);
1257
1258 assert_eq!(
1259 sql,
1260 "\"active\" = $1 AND EXISTS (SELECT 1 FROM heroes WHERE heroes.team_id = teams.id)"
1261 );
1262 }
1263}