1use crate::context::{ActiveTenant, SharedConnection};
2use crate::page_request::PageRequest;
3use crate::query_alias::AliasedEntityColumn;
4use crate::query_projection::SelectProjections;
5use crate::{
6 IncludeCollection, IncludeNavigation, SoftDeleteEntity, TenantScopedEntity,
7 TrackingRegistryHandle,
8};
9use sql_orm_core::{
10 ColumnMetadata, Entity, EntityColumn, EntityMetadata, FromRow, NavigationKind, OrmError, Row,
11 SqlServerType, SqlTypeMapping, SqlValue,
12};
13use sql_orm_query::{
14 AggregateExpr, AggregateOrderBy, AggregatePredicate, AggregateProjection, AggregateQuery,
15 ColumnRef, ExistsQuery, Expr, Join, JoinType, OrderBy, Pagination, Predicate, SelectProjection,
16 SelectQuery, TableRef,
17};
18use sql_orm_sqlserver::SqlServerCompiler;
19use std::collections::BTreeSet;
20
21#[derive(Clone)]
22pub struct DbSetQuery<E: Entity> {
29 connection: Option<SharedConnection>,
30 active_tenant: Option<ActiveTenant>,
31 tracking_registry: Option<TrackingRegistryHandle>,
32 select_query: SelectQuery,
33 visibility: SoftDeleteVisibility,
34 _entity: core::marker::PhantomData<fn() -> E>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38enum SoftDeleteVisibility {
39 Default,
40 WithDeleted,
41 OnlyDeleted,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CollectionIncludeStrategy {
47 Join,
50 SplitQuery,
55}
56
57const DEFAULT_INCLUDE_MANY_JOIN_ROW_LIMIT: usize = 10_000;
58
59impl<E: Entity> DbSetQuery<E> {
60 pub(crate) fn new(connection: Option<SharedConnection>, select_query: SelectQuery) -> Self {
61 let active_tenant = connection
62 .as_ref()
63 .and_then(SharedConnection::active_tenant);
64 Self {
65 connection,
66 active_tenant,
67 tracking_registry: None,
68 select_query,
69 visibility: SoftDeleteVisibility::Default,
70 _entity: core::marker::PhantomData,
71 }
72 }
73
74 pub(crate) fn with_tracking_registry(
75 mut self,
76 tracking_registry: TrackingRegistryHandle,
77 ) -> Self {
78 self.tracking_registry = Some(tracking_registry);
79 self
80 }
81
82 #[cfg(test)]
83 pub(crate) fn with_active_tenant_for_test(mut self, active_tenant: ActiveTenant) -> Self {
84 self.active_tenant = Some(active_tenant);
85 self
86 }
87
88 pub fn with_select_query(mut self, select_query: SelectQuery) -> Self {
91 self.select_query = select_query;
92 self
93 }
94
95 pub fn filter(mut self, predicate: Predicate) -> Self {
97 self.select_query = self.select_query.filter(predicate);
98 self
99 }
100
101 pub fn join(mut self, join: Join) -> Self {
103 self.select_query = self.select_query.join(join);
104 self
105 }
106
107 pub fn inner_join<J: Entity>(mut self, on: Predicate) -> Self {
109 self.select_query = self.select_query.inner_join::<J>(on);
110 self
111 }
112
113 pub fn left_join<J: Entity>(mut self, on: Predicate) -> Self {
115 self.select_query = self.select_query.left_join::<J>(on);
116 self
117 }
118
119 pub fn try_inner_join_navigation<J: Entity>(
125 self,
126 navigation: &'static str,
127 ) -> Result<Self, OrmError> {
128 self.try_join_navigation::<J>(navigation, JoinType::Inner, None)
129 }
130
131 pub fn try_left_join_navigation<J: Entity>(
137 self,
138 navigation: &'static str,
139 ) -> Result<Self, OrmError> {
140 self.try_join_navigation::<J>(navigation, JoinType::Left, None)
141 }
142
143 pub fn try_inner_join_navigation_as<J: Entity>(
145 self,
146 navigation: &'static str,
147 alias: &'static str,
148 ) -> Result<Self, OrmError> {
149 self.try_join_navigation::<J>(navigation, JoinType::Inner, Some(alias))
150 }
151
152 pub fn try_left_join_navigation_as<J: Entity>(
154 self,
155 navigation: &'static str,
156 alias: &'static str,
157 ) -> Result<Self, OrmError> {
158 self.try_join_navigation::<J>(navigation, JoinType::Left, Some(alias))
159 }
160
161 pub fn include<J: Entity>(
169 self,
170 navigation: &'static str,
171 ) -> Result<DbSetQueryIncludeOne<E, J>, OrmError> {
172 self.include_as::<J>(navigation, navigation)
173 }
174
175 pub fn include_as<J: Entity>(
177 self,
178 navigation: &'static str,
179 alias: &'static str,
180 ) -> Result<DbSetQueryIncludeOne<E, J>, OrmError> {
181 let metadata = E::metadata();
182 let navigation_metadata = metadata.navigation(navigation).ok_or_else(|| {
183 OrmError::new(format!(
184 "entity `{}` does not declare navigation `{}`",
185 metadata.rust_name, navigation
186 ))
187 })?;
188
189 if !matches!(
190 navigation_metadata.kind,
191 NavigationKind::BelongsTo | NavigationKind::HasOne
192 ) {
193 return Err(OrmError::new(format!(
194 "include only supports belongs_to and has_one navigations; `{}` is {:?}",
195 navigation_metadata.rust_field, navigation_metadata.kind
196 )));
197 }
198
199 Ok(DbSetQueryIncludeOne {
200 query: self.try_join_navigation::<J>(navigation, JoinType::Left, Some(alias))?,
201 navigation,
202 alias,
203 _target: core::marker::PhantomData,
204 })
205 }
206
207 pub fn include_many<J: Entity>(
215 self,
216 navigation: &'static str,
217 ) -> Result<DbSetQueryIncludeMany<E, J>, OrmError> {
218 self.include_many_as::<J>(navigation, navigation)
219 }
220
221 pub fn include_many_as<J: Entity>(
223 self,
224 navigation: &'static str,
225 alias: &'static str,
226 ) -> Result<DbSetQueryIncludeMany<E, J>, OrmError> {
227 let metadata = E::metadata();
228 let navigation_metadata = metadata.navigation(navigation).ok_or_else(|| {
229 OrmError::new(format!(
230 "entity `{}` does not declare navigation `{}`",
231 metadata.rust_name, navigation
232 ))
233 })?;
234
235 if !matches!(navigation_metadata.kind, NavigationKind::HasMany) {
236 return Err(OrmError::new(format!(
237 "include_many only supports has_many navigations; `{}` is {:?}",
238 navigation_metadata.rust_field, navigation_metadata.kind
239 )));
240 }
241
242 Ok(DbSetQueryIncludeMany {
243 query: self.try_join_navigation::<J>(navigation, JoinType::Left, Some(alias))?,
244 navigation,
245 alias,
246 strategy: CollectionIncludeStrategy::Join,
247 join_row_limit: Some(DEFAULT_INCLUDE_MANY_JOIN_ROW_LIMIT),
248 _target: core::marker::PhantomData,
249 })
250 }
251
252 pub fn order_by(mut self, order: OrderBy) -> Self {
254 self.select_query = self.select_query.order_by(order);
255 self
256 }
257
258 pub fn limit(mut self, limit: u64) -> Self {
260 self.select_query = self.select_query.paginate(Pagination::new(0, limit));
261 self
262 }
263
264 pub fn take(self, limit: u64) -> Self {
266 self.limit(limit)
267 }
268
269 pub fn paginate(mut self, request: PageRequest) -> Self {
271 self.select_query = self.select_query.paginate(request.to_pagination());
272 self
273 }
274
275 pub fn select<P>(mut self, projection: P) -> Self
280 where
281 P: SelectProjections,
282 {
283 self.select_query = self
284 .select_query
285 .select(projection.into_select_projections());
286 self
287 }
288
289 pub fn group_by<G>(self, group_by: G) -> Result<DbSetGroupedQuery<E>, OrmError>
295 where
296 E: SoftDeleteEntity + TenantScopedEntity,
297 G: GroupByExpressions,
298 {
299 let group_by = group_by.into_group_by_expressions();
300 if group_by.is_empty() {
301 return Err(OrmError::new(
302 "group_by requires at least one group key expression",
303 ));
304 }
305
306 let connection = self.connection.clone();
307 let effective = self.effective_select_query()?;
308 Ok(DbSetGroupedQuery {
309 connection,
310 aggregate_query: AggregateQuery {
311 from: effective.from,
312 joins: effective.joins,
313 projection: Vec::new(),
314 predicate: effective.predicate,
315 group_by,
316 having: None,
317 order_by: Vec::new(),
318 pagination: None,
319 },
320 _entity: core::marker::PhantomData,
321 })
322 }
323
324 #[cfg(test)]
325 pub(crate) fn select_query(&self) -> &SelectQuery {
326 &self.select_query
327 }
328
329 pub fn with_deleted(mut self) -> Self {
333 self.visibility = SoftDeleteVisibility::WithDeleted;
334 self
335 }
336
337 pub fn only_deleted(mut self) -> Self {
341 self.visibility = SoftDeleteVisibility::OnlyDeleted;
342 self
343 }
344
345 #[cfg(test)]
346 pub(crate) fn into_select_query(self) -> SelectQuery {
347 self.select_query
348 }
349
350 pub async fn all(self) -> Result<Vec<E>, OrmError>
352 where
353 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
354 {
355 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
356 let shared_connection = self.require_connection()?;
357 let mut connection = shared_connection.lock().await?;
358 connection.fetch_all(compiled).await
359 }
360
361 pub async fn first(self) -> Result<Option<E>, OrmError>
363 where
364 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
365 {
366 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
367 let shared_connection = self.require_connection()?;
368 let mut connection = shared_connection.lock().await?;
369 connection.fetch_one(compiled).await
370 }
371
372 pub async fn all_as<T>(self) -> Result<Vec<T>, OrmError>
374 where
375 E: SoftDeleteEntity + TenantScopedEntity,
376 T: FromRow + Send,
377 {
378 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
379 let shared_connection = self.require_connection()?;
380 let mut connection = shared_connection.lock().await?;
381 connection.fetch_all(compiled).await
382 }
383
384 pub async fn first_as<T>(self) -> Result<Option<T>, OrmError>
386 where
387 E: SoftDeleteEntity + TenantScopedEntity,
388 T: FromRow + Send,
389 {
390 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
391 let shared_connection = self.require_connection()?;
392 let mut connection = shared_connection.lock().await?;
393 connection.fetch_one(compiled).await
394 }
395
396 pub async fn count(self) -> Result<i64, OrmError>
398 where
399 E: SoftDeleteEntity + TenantScopedEntity,
400 {
401 let compiled = SqlServerCompiler::compile_aggregate(
402 &self.scalar_aggregate_query(AggregateProjection::count_as("count"))?,
403 )?;
404 let shared_connection = self.require_connection()?;
405 let mut connection = shared_connection.lock().await?;
406 let row = connection.fetch_one::<CountRow>(compiled).await?;
407
408 row.map(|row| row.value)
409 .ok_or_else(|| OrmError::new("count query did not return a row"))
410 }
411
412 pub async fn exists(self) -> Result<bool, OrmError>
414 where
415 E: SoftDeleteEntity + TenantScopedEntity,
416 {
417 let compiled = SqlServerCompiler::compile_exists(&self.exists_query()?)?;
418 let shared_connection = self.require_connection()?;
419 let mut connection = shared_connection.lock().await?;
420 let row = connection.fetch_one::<ExistsRow>(compiled).await?;
421
422 row.map(|row| row.value)
423 .ok_or_else(|| OrmError::new("exists query did not return a row"))
424 }
425
426 pub async fn any(self) -> Result<bool, OrmError>
428 where
429 E: SoftDeleteEntity + TenantScopedEntity,
430 {
431 self.exists().await
432 }
433
434 pub async fn sum<T>(self, column: impl Into<Expr>) -> Result<Option<T>, OrmError>
436 where
437 E: SoftDeleteEntity + TenantScopedEntity,
438 T: SqlTypeMapping + Send,
439 {
440 self.scalar_aggregate(AggregateExpr::sum(column)).await
441 }
442
443 pub async fn avg<T>(self, column: impl Into<Expr>) -> Result<Option<T>, OrmError>
445 where
446 E: SoftDeleteEntity + TenantScopedEntity,
447 T: SqlTypeMapping + Send,
448 {
449 self.scalar_aggregate(AggregateExpr::avg(column)).await
450 }
451
452 pub async fn min<T>(self, column: impl Into<Expr>) -> Result<Option<T>, OrmError>
454 where
455 E: SoftDeleteEntity + TenantScopedEntity,
456 T: SqlTypeMapping + Send,
457 {
458 self.scalar_aggregate(AggregateExpr::min(column)).await
459 }
460
461 pub async fn max<T>(self, column: impl Into<Expr>) -> Result<Option<T>, OrmError>
463 where
464 E: SoftDeleteEntity + TenantScopedEntity,
465 T: SqlTypeMapping + Send,
466 {
467 self.scalar_aggregate(AggregateExpr::max(column)).await
468 }
469
470 async fn scalar_aggregate<T>(self, expr: AggregateExpr) -> Result<Option<T>, OrmError>
471 where
472 E: SoftDeleteEntity + TenantScopedEntity,
473 T: SqlTypeMapping + Send,
474 {
475 let compiled = SqlServerCompiler::compile_aggregate(
476 &self.scalar_aggregate_query(AggregateProjection::expr_as(expr, "value"))?,
477 )?;
478 let shared_connection = self.require_connection()?;
479 let mut connection = shared_connection.lock().await?;
480 let row = connection
481 .fetch_one::<ScalarAggregateRow<T>>(compiled)
482 .await?;
483
484 row.map(|row| row.value)
485 .ok_or_else(|| OrmError::new("scalar aggregate query did not return a row"))
486 }
487
488 fn exists_query(&self) -> Result<ExistsQuery, OrmError>
489 where
490 E: SoftDeleteEntity + TenantScopedEntity,
491 {
492 let effective = self.effective_select_query()?;
493 Ok(ExistsQuery {
494 from: effective.from,
495 joins: effective.joins,
496 predicate: effective.predicate,
497 })
498 }
499
500 fn scalar_aggregate_query(
501 &self,
502 projection: AggregateProjection,
503 ) -> Result<AggregateQuery, OrmError>
504 where
505 E: SoftDeleteEntity + TenantScopedEntity,
506 {
507 let effective = self.effective_select_query()?;
508 Ok(AggregateQuery {
509 from: effective.from,
510 joins: effective.joins,
511 projection: vec![projection],
512 predicate: effective.predicate,
513 group_by: Vec::new(),
514 having: None,
515 order_by: Vec::new(),
516 pagination: None,
517 })
518 }
519
520 fn effective_select_query(&self) -> Result<SelectQuery, OrmError>
521 where
522 E: SoftDeleteEntity + TenantScopedEntity,
523 {
524 let mut query = self.select_query.clone();
525
526 if let Some(predicate) =
527 tenant_predicate_for::<E>(self.active_tenant.as_ref(), TableRef::for_entity::<E>())?
528 {
529 query = query.filter(predicate);
530 }
531
532 if let Some(predicate) =
533 soft_delete_visibility_predicate_for::<E>(TableRef::for_entity::<E>(), self.visibility)?
534 {
535 query = query.filter(predicate);
536 }
537
538 Ok(query)
539 }
540
541 fn require_connection(&self) -> Result<SharedConnection, OrmError> {
542 self.connection
543 .as_ref()
544 .cloned()
545 .ok_or_else(|| OrmError::new("DbSetQuery requires an initialized shared connection"))
546 }
547
548 fn try_join_navigation<J: Entity>(
549 mut self,
550 navigation: &'static str,
551 join_type: JoinType,
552 alias: Option<&'static str>,
553 ) -> Result<Self, OrmError> {
554 let join = self.navigation_join::<J>(navigation, join_type, alias)?;
555 self.select_query = self.select_query.join(join);
556 Ok(self)
557 }
558
559 fn navigation_join<J: Entity>(
560 &self,
561 navigation: &'static str,
562 join_type: JoinType,
563 alias: Option<&'static str>,
564 ) -> Result<Join, OrmError> {
565 let root_metadata = E::metadata();
566 let target_metadata = J::metadata();
567 let navigation = root_metadata.navigation(navigation).ok_or_else(|| {
568 OrmError::new(format!(
569 "entity `{}` does not declare navigation `{}`",
570 root_metadata.rust_name, navigation
571 ))
572 })?;
573
574 if navigation.target_schema != target_metadata.schema
575 || navigation.target_table != target_metadata.table
576 {
577 return Err(OrmError::new(format!(
578 "navigation `{}` on `{}` targets `{}.{}`, not entity `{}` (`{}.{}`)",
579 navigation.rust_field,
580 root_metadata.rust_name,
581 navigation.target_schema,
582 navigation.target_table,
583 target_metadata.rust_name,
584 target_metadata.schema,
585 target_metadata.table
586 )));
587 }
588
589 if navigation.local_columns.is_empty()
590 || navigation.local_columns.len() != navigation.target_columns.len()
591 {
592 return Err(OrmError::new(format!(
593 "navigation `{}` on `{}` has invalid join column metadata",
594 navigation.rust_field, root_metadata.rust_name
595 )));
596 }
597
598 let target_table = match alias {
599 Some(alias) => TableRef::for_entity_as::<J>(alias),
600 None => TableRef::for_entity::<J>(),
601 };
602
603 let predicates = navigation
604 .local_columns
605 .iter()
606 .zip(navigation.target_columns.iter())
607 .map(|(local_column, target_column)| {
608 Ok(Predicate::eq(
609 metadata_column_expr(root_metadata, self.select_query.from, local_column)?,
610 metadata_column_expr(target_metadata, target_table, target_column)?,
611 ))
612 })
613 .collect::<Result<Vec<_>, OrmError>>()?;
614
615 let on = if predicates.len() == 1 {
616 predicates[0].clone()
617 } else {
618 Predicate::and(predicates)
619 };
620
621 Ok(Join::new(join_type, target_table, on))
622 }
623}
624
625pub trait GroupByExpressions {
627 fn into_group_by_expressions(self) -> Vec<Expr>;
628}
629
630impl GroupByExpressions for Expr {
631 fn into_group_by_expressions(self) -> Vec<Expr> {
632 vec![self]
633 }
634}
635
636impl<E> GroupByExpressions for EntityColumn<E>
637where
638 E: Entity,
639{
640 fn into_group_by_expressions(self) -> Vec<Expr> {
641 vec![Expr::from(self)]
642 }
643}
644
645impl<E> GroupByExpressions for AliasedEntityColumn<E>
646where
647 E: Entity,
648{
649 fn into_group_by_expressions(self) -> Vec<Expr> {
650 vec![Expr::from(self)]
651 }
652}
653
654impl<P> GroupByExpressions for Vec<P>
655where
656 P: Into<Expr>,
657{
658 fn into_group_by_expressions(self) -> Vec<Expr> {
659 self.into_iter().map(Into::into).collect()
660 }
661}
662
663impl<P, const N: usize> GroupByExpressions for [P; N]
664where
665 P: Into<Expr>,
666{
667 fn into_group_by_expressions(self) -> Vec<Expr> {
668 self.into_iter().map(Into::into).collect()
669 }
670}
671
672macro_rules! impl_group_by_expressions_tuple {
673 ($($name:ident),+ $(,)?) => {
674 impl<$($name),+> GroupByExpressions for ($($name,)+)
675 where
676 $($name: Into<Expr>),+
677 {
678 #[allow(non_snake_case)]
679 fn into_group_by_expressions(self) -> Vec<Expr> {
680 let ($($name,)+) = self;
681 vec![$($name.into()),+]
682 }
683 }
684 };
685}
686
687impl_group_by_expressions_tuple!(A);
688impl_group_by_expressions_tuple!(A, B);
689impl_group_by_expressions_tuple!(A, B, C);
690impl_group_by_expressions_tuple!(A, B, C, D);
691impl_group_by_expressions_tuple!(A, B, C, D, E);
692impl_group_by_expressions_tuple!(A, B, C, D, E, F);
693impl_group_by_expressions_tuple!(A, B, C, D, E, F, G);
694impl_group_by_expressions_tuple!(A, B, C, D, E, F, G, H);
695impl_group_by_expressions_tuple!(A, B, C, D, E, F, G, H, I);
696impl_group_by_expressions_tuple!(A, B, C, D, E, F, G, H, I, J);
697impl_group_by_expressions_tuple!(A, B, C, D, E, F, G, H, I, J, K);
698impl_group_by_expressions_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
699
700pub trait AggregateProjections {
703 fn into_aggregate_projections(self) -> Vec<AggregateProjection>;
704}
705
706impl AggregateProjections for AggregateProjection {
707 fn into_aggregate_projections(self) -> Vec<AggregateProjection> {
708 vec![self]
709 }
710}
711
712impl<P> AggregateProjections for Vec<P>
713where
714 P: Into<AggregateProjection>,
715{
716 fn into_aggregate_projections(self) -> Vec<AggregateProjection> {
717 self.into_iter().map(Into::into).collect()
718 }
719}
720
721impl<P, const N: usize> AggregateProjections for [P; N]
722where
723 P: Into<AggregateProjection>,
724{
725 fn into_aggregate_projections(self) -> Vec<AggregateProjection> {
726 self.into_iter().map(Into::into).collect()
727 }
728}
729
730macro_rules! impl_aggregate_projections_tuple {
731 ($($name:ident),+ $(,)?) => {
732 impl<$($name),+> AggregateProjections for ($($name,)+)
733 where
734 $($name: Into<AggregateProjection>),+
735 {
736 #[allow(non_snake_case)]
737 fn into_aggregate_projections(self) -> Vec<AggregateProjection> {
738 let ($($name,)+) = self;
739 vec![$($name.into()),+]
740 }
741 }
742 };
743}
744
745impl_aggregate_projections_tuple!(A);
746impl_aggregate_projections_tuple!(A, B);
747impl_aggregate_projections_tuple!(A, B, C);
748impl_aggregate_projections_tuple!(A, B, C, D);
749impl_aggregate_projections_tuple!(A, B, C, D, E);
750impl_aggregate_projections_tuple!(A, B, C, D, E, F);
751impl_aggregate_projections_tuple!(A, B, C, D, E, F, G);
752impl_aggregate_projections_tuple!(A, B, C, D, E, F, G, H);
753impl_aggregate_projections_tuple!(A, B, C, D, E, F, G, H, I);
754impl_aggregate_projections_tuple!(A, B, C, D, E, F, G, H, I, J);
755impl_aggregate_projections_tuple!(A, B, C, D, E, F, G, H, I, J, K);
756impl_aggregate_projections_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
757
758pub struct DbSetGroupedQuery<E: Entity> {
760 connection: Option<SharedConnection>,
761 aggregate_query: AggregateQuery,
762 _entity: core::marker::PhantomData<fn() -> E>,
763}
764
765impl<E: Entity> DbSetGroupedQuery<E> {
766 pub fn select_aggregate<P>(mut self, projection: P) -> Self
768 where
769 P: AggregateProjections,
770 {
771 self.aggregate_query.projection = projection.into_aggregate_projections();
772 self
773 }
774
775 pub fn try_select_aggregate<P>(mut self, projection: P) -> Result<Self, OrmError>
777 where
778 P: AggregateProjections,
779 {
780 let projection = projection.into_aggregate_projections();
781 validate_aggregate_projection_aliases(&projection)?;
782 self.aggregate_query.projection = projection;
783 Ok(self)
784 }
785
786 pub fn having(mut self, predicate: AggregatePredicate) -> Self {
788 self.aggregate_query = self.aggregate_query.having(predicate);
789 self
790 }
791
792 pub fn order_by(mut self, order: AggregateOrderBy) -> Self {
794 self.aggregate_query = self.aggregate_query.order_by(order);
795 self
796 }
797
798 pub fn limit(mut self, limit: u64) -> Self {
800 self.aggregate_query = self.aggregate_query.paginate(Pagination::new(0, limit));
801 self
802 }
803
804 pub fn take(self, limit: u64) -> Self {
806 self.limit(limit)
807 }
808
809 pub fn paginate(mut self, request: PageRequest) -> Self {
811 self.aggregate_query = self.aggregate_query.paginate(request.to_pagination());
812 self
813 }
814
815 pub async fn all_as<T>(self) -> Result<Vec<T>, OrmError>
817 where
818 T: FromRow + Send,
819 {
820 let compiled = SqlServerCompiler::compile_aggregate(&self.aggregate_query)?;
821 let shared_connection = self.require_connection()?;
822 let mut connection = shared_connection.lock().await?;
823 connection.fetch_all(compiled).await
824 }
825
826 pub async fn first_as<T>(self) -> Result<Option<T>, OrmError>
828 where
829 T: FromRow + Send,
830 {
831 let compiled = SqlServerCompiler::compile_aggregate(&self.aggregate_query)?;
832 let shared_connection = self.require_connection()?;
833 let mut connection = shared_connection.lock().await?;
834 connection.fetch_one(compiled).await
835 }
836
837 #[cfg(test)]
838 pub(crate) fn aggregate_query(&self) -> &AggregateQuery {
839 &self.aggregate_query
840 }
841
842 fn require_connection(&self) -> Result<SharedConnection, OrmError> {
843 self.connection.as_ref().cloned().ok_or_else(|| {
844 OrmError::new("DbSetGroupedQuery requires an initialized shared connection")
845 })
846 }
847}
848
849fn validate_aggregate_projection_aliases(
850 projection: &[AggregateProjection],
851) -> Result<(), OrmError> {
852 if projection.is_empty() {
853 return Err(OrmError::new(
854 "select_aggregate requires at least one aggregate projection",
855 ));
856 }
857
858 let mut aliases = BTreeSet::new();
859 for projection in projection {
860 if projection.alias.trim().is_empty() {
861 return Err(OrmError::new("aggregate projection alias cannot be empty"));
862 }
863 if !aliases.insert(projection.alias) {
864 return Err(OrmError::new(format!(
865 "aggregate projection alias `{}` is duplicated",
866 projection.alias
867 )));
868 }
869 }
870
871 Ok(())
872}
873
874impl<E: Entity> core::fmt::Debug for DbSetGroupedQuery<E> {
875 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
876 f.debug_struct("DbSetGroupedQuery")
877 .field("entity", &E::metadata().rust_name)
878 .field("table", &E::metadata().table)
879 .field("aggregate_query", &self.aggregate_query)
880 .finish()
881 }
882}
883
884pub struct DbSetQueryIncludeOne<E: Entity, J: Entity> {
887 query: DbSetQuery<E>,
888 navigation: &'static str,
889 alias: &'static str,
890 _target: core::marker::PhantomData<fn() -> J>,
891}
892
893impl<E: Entity, J: Entity> DbSetQueryIncludeOne<E, J> {
894 pub fn filter(mut self, predicate: Predicate) -> Self {
896 self.query = self.query.filter(predicate);
897 self
898 }
899
900 pub fn join(mut self, join: Join) -> Self {
902 self.query = self.query.join(join);
903 self
904 }
905
906 pub fn inner_join<K: Entity>(mut self, on: Predicate) -> Self {
908 self.query = self.query.inner_join::<K>(on);
909 self
910 }
911
912 pub fn left_join<K: Entity>(mut self, on: Predicate) -> Self {
914 self.query = self.query.left_join::<K>(on);
915 self
916 }
917
918 pub fn order_by(mut self, order: OrderBy) -> Self {
920 self.query = self.query.order_by(order);
921 self
922 }
923
924 pub fn limit(mut self, limit: u64) -> Self {
926 self.query = self.query.limit(limit);
927 self
928 }
929
930 pub fn take(self, limit: u64) -> Self {
932 self.limit(limit)
933 }
934
935 pub fn paginate(mut self, request: PageRequest) -> Self {
937 self.query = self.query.paginate(request);
938 self
939 }
940
941 pub fn with_deleted(mut self) -> Self {
946 self.query = self.query.with_deleted();
947 self
948 }
949
950 pub fn only_deleted(mut self) -> Self {
955 self.query = self.query.only_deleted();
956 self
957 }
958
959 pub async fn all(self) -> Result<Vec<E>, OrmError>
962 where
963 E: FromRow + IncludeNavigation<J> + Send + SoftDeleteEntity + TenantScopedEntity,
964 J: Clone + FromRow + Send + SoftDeleteEntity + Sync + TenantScopedEntity + 'static,
965 {
966 let navigation = self.navigation;
967 let alias = self.alias;
968 let tracking_registry = self.query.tracking_registry.clone();
969 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
970 let shared_connection = self.query.require_connection()?;
971 let mut connection = shared_connection.lock().await?;
972 connection
973 .fetch_all_with(compiled, move |row| {
974 materialize_include_one::<E, J>(&row, navigation, alias, tracking_registry.as_ref())
975 })
976 .await
977 }
978
979 pub async fn first(self) -> Result<Option<E>, OrmError>
982 where
983 E: FromRow + IncludeNavigation<J> + Send + SoftDeleteEntity + TenantScopedEntity,
984 J: Clone + FromRow + Send + SoftDeleteEntity + Sync + TenantScopedEntity + 'static,
985 {
986 let navigation = self.navigation;
987 let alias = self.alias;
988 let tracking_registry = self.query.tracking_registry.clone();
989 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
990 let shared_connection = self.query.require_connection()?;
991 let mut connection = shared_connection.lock().await?;
992 connection
993 .fetch_one_with(compiled, move |row| {
994 materialize_include_one::<E, J>(&row, navigation, alias, tracking_registry.as_ref())
995 })
996 .await
997 }
998
999 #[cfg(test)]
1000 pub(crate) fn select_query(&self) -> Result<SelectQuery, OrmError>
1001 where
1002 E: SoftDeleteEntity + TenantScopedEntity,
1003 J: SoftDeleteEntity + TenantScopedEntity,
1004 {
1005 self.effective_select_query()
1006 }
1007
1008 fn effective_select_query(&self) -> Result<SelectQuery, OrmError>
1009 where
1010 E: SoftDeleteEntity + TenantScopedEntity,
1011 J: SoftDeleteEntity + TenantScopedEntity,
1012 {
1013 let query = self.query.effective_select_query()?;
1014 let query = apply_include_policy_filters::<J>(
1015 query,
1016 self.query.active_tenant.as_ref(),
1017 self.alias,
1018 )?;
1019 apply_include_projection::<E, J>(query, self.alias)
1020 }
1021}
1022
1023pub struct DbSetQueryIncludeMany<E: Entity, J: Entity> {
1026 query: DbSetQuery<E>,
1027 navigation: &'static str,
1028 alias: &'static str,
1029 strategy: CollectionIncludeStrategy,
1030 join_row_limit: Option<usize>,
1031 _target: core::marker::PhantomData<fn() -> J>,
1032}
1033
1034impl<E: Entity, J: Entity> DbSetQueryIncludeMany<E, J> {
1035 pub fn filter(mut self, predicate: Predicate) -> Self {
1037 self.query = self.query.filter(predicate);
1038 self
1039 }
1040
1041 pub fn join(mut self, join: Join) -> Self {
1043 self.query = self.query.join(join);
1044 self
1045 }
1046
1047 pub fn inner_join<K: Entity>(mut self, on: Predicate) -> Self {
1049 self.query = self.query.inner_join::<K>(on);
1050 self
1051 }
1052
1053 pub fn left_join<K: Entity>(mut self, on: Predicate) -> Self {
1055 self.query = self.query.left_join::<K>(on);
1056 self
1057 }
1058
1059 pub fn order_by(mut self, order: OrderBy) -> Self {
1061 self.query = self.query.order_by(order);
1062 self
1063 }
1064
1065 pub fn join_strategy(mut self) -> Self {
1070 self.strategy = CollectionIncludeStrategy::Join;
1071 self
1072 }
1073
1074 pub fn split_query(mut self) -> Self {
1079 self.strategy = CollectionIncludeStrategy::SplitQuery;
1080 self
1081 }
1082
1083 pub fn max_joined_rows(mut self, limit: usize) -> Self {
1087 self.join_row_limit = Some(limit);
1088 self
1089 }
1090
1091 pub fn unbounded_join(mut self) -> Self {
1096 self.join_row_limit = None;
1097 self
1098 }
1099
1100 pub fn with_deleted(mut self) -> Self {
1105 self.query = self.query.with_deleted();
1106 self
1107 }
1108
1109 pub fn only_deleted(mut self) -> Self {
1114 self.query = self.query.only_deleted();
1115 self
1116 }
1117
1118 pub async fn all(self) -> Result<Vec<E>, OrmError>
1121 where
1122 E: FromRow + IncludeCollection<J> + Send + SoftDeleteEntity + TenantScopedEntity,
1123 J: Clone + FromRow + Send + SoftDeleteEntity + Sync + TenantScopedEntity + 'static,
1124 {
1125 if self.strategy == CollectionIncludeStrategy::SplitQuery {
1126 return Err(OrmError::new(
1127 "include_many split-query loading is not implemented yet; use join_strategy() with an explicit max_joined_rows(...) limit",
1128 ));
1129 }
1130
1131 let navigation = self.navigation;
1132 let alias = self.alias;
1133 let tracking_registry = self.query.tracking_registry.clone();
1134 let compiled = SqlServerCompiler::compile_select(&self.effective_select_query()?)?;
1135 let shared_connection = self.query.require_connection()?;
1136 let mut connection = shared_connection.lock().await?;
1137 let rows = connection
1138 .fetch_all_with(compiled, move |row| {
1139 materialize_include_many_row::<E, J>(&row, alias)
1140 })
1141 .await?;
1142
1143 enforce_include_many_join_row_limit(rows.len(), self.join_row_limit)?;
1144 group_include_many_rows::<E, J>(rows, navigation, tracking_registry.as_ref())
1145 }
1146
1147 #[cfg(test)]
1148 pub(crate) fn select_query(&self) -> Result<SelectQuery, OrmError>
1149 where
1150 E: SoftDeleteEntity + TenantScopedEntity,
1151 J: SoftDeleteEntity + TenantScopedEntity,
1152 {
1153 self.effective_select_query()
1154 }
1155
1156 fn effective_select_query(&self) -> Result<SelectQuery, OrmError>
1157 where
1158 E: SoftDeleteEntity + TenantScopedEntity,
1159 J: SoftDeleteEntity + TenantScopedEntity,
1160 {
1161 let query = self.query.effective_select_query()?;
1162 if query.pagination.is_some() {
1163 return Err(OrmError::new(
1164 "include_many does not support pagination in the join-based collection loading cut",
1165 ));
1166 }
1167
1168 let query = apply_include_policy_filters::<J>(
1169 query,
1170 self.query.active_tenant.as_ref(),
1171 self.alias,
1172 )?;
1173 apply_include_projection::<E, J>(query, self.alias)
1174 }
1175}
1176
1177fn tenant_predicate_for<E: TenantScopedEntity>(
1178 active_tenant: Option<&ActiveTenant>,
1179 table: TableRef,
1180) -> Result<Option<Predicate>, OrmError> {
1181 let Some(policy) = E::tenant_policy() else {
1182 return Ok(None);
1183 };
1184
1185 if policy.columns.len() != 1 {
1186 return Err(OrmError::new(
1187 "tenant query filter requires exactly one tenant policy column",
1188 ));
1189 }
1190
1191 let tenant_column = &policy.columns[0];
1192 let active_tenant = active_tenant.ok_or_else(|| {
1193 OrmError::new("tenant-scoped query requires an active tenant in the DbContext")
1194 })?;
1195
1196 if active_tenant.column_name != tenant_column.column_name {
1197 return Err(OrmError::new(format!(
1198 "active tenant column `{}` does not match entity tenant column `{}`",
1199 active_tenant.column_name, tenant_column.column_name
1200 )));
1201 }
1202
1203 if !tenant_value_matches_column_type(&active_tenant.value, tenant_column) {
1204 return Err(OrmError::new(format!(
1205 "active tenant value is not compatible with entity tenant column `{}`",
1206 tenant_column.column_name
1207 )));
1208 }
1209
1210 Ok(Some(Predicate::eq(
1211 Expr::Column(ColumnRef::new(
1212 table,
1213 tenant_column.rust_field,
1214 tenant_column.column_name,
1215 )),
1216 Expr::Value(active_tenant.value.clone()),
1217 )))
1218}
1219
1220fn soft_delete_visibility_predicate_for<E: SoftDeleteEntity>(
1221 table: TableRef,
1222 visibility: SoftDeleteVisibility,
1223) -> Result<Option<Predicate>, OrmError> {
1224 let Some(policy) = E::soft_delete_policy() else {
1225 return Ok(None);
1226 };
1227
1228 let visibility = match visibility {
1229 SoftDeleteVisibility::Default => SoftDeleteVisibility::Default,
1230 SoftDeleteVisibility::WithDeleted => return Ok(None),
1231 SoftDeleteVisibility::OnlyDeleted => SoftDeleteVisibility::OnlyDeleted,
1232 };
1233
1234 let indicator = policy.columns.first().ok_or_else(|| {
1235 OrmError::new("soft_delete query visibility requires at least one policy column")
1236 })?;
1237 let column = Expr::Column(ColumnRef::new(
1238 table,
1239 indicator.rust_field,
1240 indicator.column_name,
1241 ));
1242
1243 if indicator.sql_type == SqlServerType::Bit {
1244 return Ok(Some(match visibility {
1245 SoftDeleteVisibility::Default => {
1246 Predicate::eq(column, Expr::Value(SqlValue::Bool(false)))
1247 }
1248 SoftDeleteVisibility::OnlyDeleted => {
1249 Predicate::eq(column, Expr::Value(SqlValue::Bool(true)))
1250 }
1251 SoftDeleteVisibility::WithDeleted => unreachable!(),
1252 }));
1253 }
1254
1255 if indicator.nullable {
1256 return Ok(Some(match visibility {
1257 SoftDeleteVisibility::Default => Predicate::is_null(column),
1258 SoftDeleteVisibility::OnlyDeleted => Predicate::is_not_null(column),
1259 SoftDeleteVisibility::WithDeleted => unreachable!(),
1260 }));
1261 }
1262
1263 Err(OrmError::new(
1264 "soft_delete query visibility requires the first policy column to be nullable or bit",
1265 ))
1266}
1267
1268fn apply_include_policy_filters<J: Entity + SoftDeleteEntity + TenantScopedEntity>(
1269 mut query: SelectQuery,
1270 active_tenant: Option<&ActiveTenant>,
1271 alias: &'static str,
1272) -> Result<SelectQuery, OrmError> {
1273 let target_table = TableRef::for_entity_as::<J>(alias);
1274 let mut predicates = Vec::new();
1275
1276 if let Some(predicate) = tenant_predicate_for::<J>(active_tenant, target_table)? {
1277 predicates.push(predicate);
1278 }
1279
1280 if let Some(predicate) =
1281 soft_delete_visibility_predicate_for::<J>(target_table, SoftDeleteVisibility::Default)?
1282 {
1283 predicates.push(predicate);
1284 }
1285
1286 if predicates.is_empty() {
1287 return Ok(query);
1288 }
1289
1290 let include_join = query
1291 .joins
1292 .iter_mut()
1293 .find(|join| join.table == target_table)
1294 .ok_or_else(|| {
1295 OrmError::new(format!(
1296 "include join for entity `{}` with alias `{}` was not found",
1297 J::metadata().rust_name,
1298 alias
1299 ))
1300 })?;
1301
1302 let policy_predicate = if predicates.len() == 1 {
1303 predicates.remove(0)
1304 } else {
1305 Predicate::and(predicates)
1306 };
1307 include_join.on = Predicate::and(vec![include_join.on.clone(), policy_predicate]);
1308
1309 Ok(query)
1310}
1311
1312fn apply_include_projection<E: Entity, J: Entity>(
1313 mut query: SelectQuery,
1314 alias: &'static str,
1315) -> Result<SelectQuery, OrmError> {
1316 let mut projection = Vec::new();
1317
1318 projection.extend(E::metadata().columns.iter().map(|column| {
1319 SelectProjection::expr_as(
1320 Expr::Column(ColumnRef::new(
1321 query.from,
1322 column.rust_field,
1323 column.column_name,
1324 )),
1325 column.column_name,
1326 )
1327 }));
1328
1329 let target_table = TableRef::for_entity_as::<J>(alias);
1330 for column in J::metadata().columns {
1331 projection.push(SelectProjection::expr_as(
1332 Expr::Column(ColumnRef::new(
1333 target_table,
1334 column.rust_field,
1335 column.column_name,
1336 )),
1337 include_column_alias(alias, column.column_name),
1338 ));
1339 }
1340
1341 query.projection = projection;
1342 Ok(query)
1343}
1344
1345fn materialize_include_one<E, J>(
1346 row: &impl Row,
1347 navigation: &'static str,
1348 alias: &'static str,
1349 tracking_registry: Option<&TrackingRegistryHandle>,
1350) -> Result<E, OrmError>
1351where
1352 E: FromRow + IncludeNavigation<J>,
1353 J: Clone + Entity + FromRow + Send + Sync + 'static,
1354{
1355 let mut entity = E::from_row(row)?;
1356 let related = materialize_prefixed_entity::<J>(row, alias)?;
1357 let related_key = prefixed_primary_key_value::<J>(row, alias)?;
1358 let related =
1359 identity_mapped_optional_navigation_value(tracking_registry, related_key, related);
1360 entity.set_included_navigation(navigation, related)?;
1361 Ok(entity)
1362}
1363
1364fn materialize_prefixed_entity<J: Entity + FromRow>(
1365 row: &impl Row,
1366 alias: &'static str,
1367) -> Result<Option<J>, OrmError> {
1368 let prefix = include_prefix(alias);
1369 let mut saw_value = false;
1370
1371 for column in J::metadata().columns {
1372 let projected = prefixed_column_name(&prefix, column.column_name);
1373 if let Some(value) = row.try_get(&projected)? {
1374 if !value.is_null() {
1375 saw_value = true;
1376 break;
1377 }
1378 }
1379 }
1380
1381 if !saw_value {
1382 return Ok(None);
1383 }
1384
1385 Ok(Some(J::from_row(&PrefixedRow { row, prefix })?))
1386}
1387
1388struct IncludeManyRow<E, J> {
1389 root_key: Vec<SqlValue>,
1390 root: E,
1391 related_key: Option<SqlValue>,
1392 related: Option<J>,
1393}
1394
1395fn materialize_include_many_row<E, J>(
1396 row: &impl Row,
1397 alias: &'static str,
1398) -> Result<IncludeManyRow<E, J>, OrmError>
1399where
1400 E: Entity + FromRow,
1401 J: Entity + FromRow,
1402{
1403 Ok(IncludeManyRow {
1404 root_key: root_primary_key_values::<E>(row)?,
1405 root: E::from_row(row)?,
1406 related_key: prefixed_primary_key_value::<J>(row, alias)?,
1407 related: materialize_prefixed_entity::<J>(row, alias)?,
1408 })
1409}
1410
1411fn prefixed_primary_key_value<J: Entity>(
1412 row: &impl Row,
1413 alias: &'static str,
1414) -> Result<Option<SqlValue>, OrmError> {
1415 let metadata = J::metadata();
1416 if metadata.primary_key.columns.len() != 1 {
1417 return Ok(None);
1418 }
1419
1420 let prefix = include_prefix(alias);
1421 let column = prefixed_column_name(&prefix, metadata.primary_key.columns[0]);
1422 row.try_get(&column)
1423}
1424
1425fn root_primary_key_values<E: Entity>(row: &impl Row) -> Result<Vec<SqlValue>, OrmError> {
1426 let metadata = E::metadata();
1427 if metadata.primary_key.columns.is_empty() {
1428 return Err(OrmError::new(format!(
1429 "include_many requires entity `{}` to declare a primary key for row grouping",
1430 metadata.rust_name
1431 )));
1432 }
1433
1434 metadata
1435 .primary_key
1436 .columns
1437 .iter()
1438 .map(|column_name| row.get_required(column_name))
1439 .collect()
1440}
1441
1442fn group_include_many_rows<E, J>(
1443 rows: Vec<IncludeManyRow<E, J>>,
1444 navigation: &'static str,
1445 tracking_registry: Option<&TrackingRegistryHandle>,
1446) -> Result<Vec<E>, OrmError>
1447where
1448 E: IncludeCollection<J>,
1449 J: Clone + Entity + Send + Sync + 'static,
1450{
1451 let mut grouped: Vec<(Vec<SqlValue>, E, Vec<J>)> = Vec::new();
1452
1453 for row in rows {
1454 let related = identity_mapped_optional_navigation_value(
1455 tracking_registry,
1456 row.related_key,
1457 row.related,
1458 );
1459 if let Some((_, _, related_values)) = grouped
1460 .iter_mut()
1461 .find(|(root_key, _, _)| *root_key == row.root_key)
1462 {
1463 if let Some(related) = related {
1464 related_values.push(related);
1465 }
1466 continue;
1467 }
1468
1469 let related_values = related.into_iter().collect();
1470 grouped.push((row.root_key, row.root, related_values));
1471 }
1472
1473 grouped
1474 .into_iter()
1475 .map(|(_, mut root, related_values)| {
1476 root.set_included_collection(navigation, related_values)?;
1477 Ok(root)
1478 })
1479 .collect()
1480}
1481
1482fn identity_mapped_optional_navigation_value<J>(
1483 tracking_registry: Option<&TrackingRegistryHandle>,
1484 key: Option<SqlValue>,
1485 value: Option<J>,
1486) -> Option<J>
1487where
1488 J: Clone + Entity + Send + Sync + 'static,
1489{
1490 value.map(|value| identity_mapped_navigation_value(tracking_registry, key, value))
1491}
1492
1493fn identity_mapped_navigation_value<J>(
1494 tracking_registry: Option<&TrackingRegistryHandle>,
1495 key: Option<SqlValue>,
1496 value: J,
1497) -> J
1498where
1499 J: Clone + Entity + Send + Sync + 'static,
1500{
1501 let Some(registry) = tracking_registry else {
1502 return value;
1503 };
1504 let Some(key) = key else {
1505 return value;
1506 };
1507
1508 registry.current_snapshot_for_key::<J>(key).unwrap_or(value)
1509}
1510
1511fn enforce_include_many_join_row_limit(
1512 row_count: usize,
1513 limit: Option<usize>,
1514) -> Result<(), OrmError> {
1515 let Some(limit) = limit else {
1516 return Ok(());
1517 };
1518
1519 if row_count > limit {
1520 return Err(OrmError::new(format!(
1521 "include_many join produced {row_count} rows, exceeding the configured limit of {limit}; use max_joined_rows(...), unbounded_join(), or wait for split-query collection loading"
1522 )));
1523 }
1524
1525 Ok(())
1526}
1527
1528struct PrefixedRow<'a, R: Row + ?Sized> {
1529 row: &'a R,
1530 prefix: String,
1531}
1532
1533impl<R: Row + ?Sized> Row for PrefixedRow<'_, R> {
1534 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
1535 self.row
1536 .try_get(&prefixed_column_name(&self.prefix, column))
1537 }
1538}
1539
1540fn include_prefix(alias: &'static str) -> String {
1541 format!("{alias}__")
1542}
1543
1544fn include_column_alias(alias: &'static str, column_name: &'static str) -> &'static str {
1545 Box::leak(format!("{alias}__{column_name}").into_boxed_str())
1546}
1547
1548fn prefixed_column_name(prefix: &str, column_name: &str) -> String {
1549 format!("{prefix}{column_name}")
1550}
1551
1552fn metadata_column_expr(
1553 metadata: &'static EntityMetadata,
1554 table: TableRef,
1555 column_name: &str,
1556) -> Result<Expr, OrmError> {
1557 let column = metadata.column(column_name).ok_or_else(|| {
1558 OrmError::new(format!(
1559 "entity `{}` metadata does not contain column `{}` required by navigation join",
1560 metadata.rust_name, column_name
1561 ))
1562 })?;
1563
1564 Ok(Expr::Column(ColumnRef::new(
1565 table,
1566 column.rust_field,
1567 column.column_name,
1568 )))
1569}
1570
1571pub(crate) fn tenant_value_matches_column_type(value: &SqlValue, column: &ColumnMetadata) -> bool {
1572 if value.is_null() {
1573 return false;
1574 }
1575
1576 match column.sql_type {
1577 SqlServerType::BigInt => matches!(value, SqlValue::I64(_)),
1578 SqlServerType::Int | SqlServerType::SmallInt | SqlServerType::TinyInt => {
1579 matches!(value, SqlValue::I32(_))
1580 }
1581 SqlServerType::Bit => matches!(value, SqlValue::Bool(_)),
1582 SqlServerType::UniqueIdentifier => matches!(value, SqlValue::Uuid(_)),
1583 SqlServerType::Date => matches!(value, SqlValue::Date(_)),
1584 SqlServerType::DateTime2 => matches!(value, SqlValue::DateTime(_)),
1585 SqlServerType::Decimal | SqlServerType::Money => matches!(value, SqlValue::Decimal(_)),
1586 SqlServerType::Float => matches!(value, SqlValue::F64(_)),
1587 SqlServerType::NVarChar | SqlServerType::Custom(_) => {
1588 matches!(value, SqlValue::String(_))
1589 }
1590 SqlServerType::VarBinary | SqlServerType::RowVersion => matches!(value, SqlValue::Bytes(_)),
1591 }
1592}
1593
1594impl<E: Entity> core::fmt::Debug for DbSetQuery<E> {
1595 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1596 f.debug_struct("DbSetQuery")
1597 .field("entity", &E::metadata().rust_name)
1598 .field("table", &E::metadata().table)
1599 .field("select_query", &self.select_query)
1600 .finish()
1601 }
1602}
1603
1604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1605struct CountRow {
1606 value: i64,
1607}
1608
1609impl FromRow for CountRow {
1610 fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
1611 match row.get_required("count")? {
1612 SqlValue::I32(value) => Ok(Self {
1613 value: i64::from(value),
1614 }),
1615 SqlValue::I64(value) => Ok(Self { value }),
1616 _ => Err(OrmError::new(
1617 "expected SQL Server COUNT result as i32 or i64",
1618 )),
1619 }
1620 }
1621}
1622
1623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1624struct ExistsRow {
1625 value: bool,
1626}
1627
1628impl FromRow for ExistsRow {
1629 fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
1630 Ok(Self {
1631 value: row.get_required_typed::<bool>("exists")?,
1632 })
1633 }
1634}
1635
1636#[derive(Debug, Clone, PartialEq)]
1637struct ScalarAggregateRow<T> {
1638 value: Option<T>,
1639}
1640
1641impl<T> FromRow for ScalarAggregateRow<T>
1642where
1643 T: SqlTypeMapping,
1644{
1645 fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
1646 let value = row
1647 .try_get("value")?
1648 .ok_or_else(|| OrmError::new("scalar aggregate result column was not present"))?;
1649
1650 if value.is_null() {
1651 return Ok(Self { value: None });
1652 }
1653
1654 Ok(Self {
1655 value: Some(T::from_sql_value(value)?),
1656 })
1657 }
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::{
1663 DbSetQuery, enforce_include_many_join_row_limit, identity_mapped_navigation_value,
1664 tenant_value_matches_column_type, validate_aggregate_projection_aliases,
1665 };
1666 use crate::context::{ActiveTenant, DbSet};
1667 use crate::page_request::PageRequest;
1668 use crate::{
1669 EntityColumnAliasExt, IncludeCollection, SoftDeleteEntity, TenantScopedEntity, Tracked,
1670 TrackingRegistry,
1671 };
1672 use chrono::{NaiveDate, NaiveDateTime};
1673 use insta::assert_snapshot;
1674 use rust_decimal::Decimal;
1675 use sql_orm_core::{
1676 ColumnMetadata, Entity, EntityColumn, EntityMetadata, EntityPolicyMetadata, FromRow,
1677 NavigationKind, NavigationMetadata, OrmError, PrimaryKeyMetadata, Row, SqlServerType,
1678 SqlValue,
1679 };
1680 use sql_orm_query::{
1681 AggregateExpr, AggregateOrderBy, AggregatePredicate, AggregateProjection, ColumnRef,
1682 CompiledQuery, Expr, Join, JoinType, OrderBy, Pagination, Predicate, SelectProjection,
1683 SelectQuery, SortDirection, TableRef,
1684 };
1685 use sql_orm_sqlserver::SqlServerCompiler;
1686
1687 struct TestEntity;
1688 struct JoinedEntity;
1689 #[derive(Debug)]
1690 struct NavigationRoot;
1691 #[derive(Debug, Clone, PartialEq, Eq)]
1692 struct NavigationTarget {
1693 id: i64,
1694 owner_id: i64,
1695 }
1696 struct TenantNavigationRoot;
1697 struct TenantNavigationTarget;
1698 struct SoftDeleteEntityUnderTest;
1699 struct BoolSoftDeleteEntity;
1700 struct TenantEntity;
1701
1702 static TEST_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1703 rust_name: "TestEntity",
1704 schema: "dbo",
1705 table: "test_entities",
1706 renamed_from: None,
1707 columns: &[],
1708 primary_key: PrimaryKeyMetadata {
1709 name: None,
1710 columns: &[],
1711 },
1712 indexes: &[],
1713 foreign_keys: &[],
1714 navigations: &[],
1715 };
1716
1717 impl Entity for TestEntity {
1718 fn metadata() -> &'static EntityMetadata {
1719 &TEST_ENTITY_METADATA
1720 }
1721 }
1722
1723 #[allow(non_upper_case_globals)]
1724 impl TestEntity {
1725 const id: EntityColumn<TestEntity> = EntityColumn::new("id", "id");
1726 const name: EntityColumn<TestEntity> = EntityColumn::new("name", "name");
1727 }
1728
1729 static JOINED_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1730 rust_name: "JoinedEntity",
1731 schema: "dbo",
1732 table: "joined_entities",
1733 renamed_from: None,
1734 columns: &[],
1735 primary_key: PrimaryKeyMetadata {
1736 name: None,
1737 columns: &[],
1738 },
1739 indexes: &[],
1740 foreign_keys: &[],
1741 navigations: &[],
1742 };
1743
1744 impl Entity for JoinedEntity {
1745 fn metadata() -> &'static EntityMetadata {
1746 &JOINED_ENTITY_METADATA
1747 }
1748 }
1749
1750 static NAVIGATION_ROOT_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
1751 rust_field: "id",
1752 column_name: "id",
1753 renamed_from: None,
1754 sql_type: SqlServerType::BigInt,
1755 nullable: false,
1756 primary_key: true,
1757 identity: None,
1758 default_sql: None,
1759 computed_sql: None,
1760 rowversion: false,
1761 insertable: false,
1762 updatable: false,
1763 max_length: None,
1764 precision: None,
1765 scale: None,
1766 }];
1767
1768 static NAVIGATION_TARGET_COLUMNS: [ColumnMetadata; 2] = [
1769 ColumnMetadata {
1770 rust_field: "id",
1771 column_name: "id",
1772 renamed_from: None,
1773 sql_type: SqlServerType::BigInt,
1774 nullable: false,
1775 primary_key: true,
1776 identity: None,
1777 default_sql: None,
1778 computed_sql: None,
1779 rowversion: false,
1780 insertable: false,
1781 updatable: false,
1782 max_length: None,
1783 precision: None,
1784 scale: None,
1785 },
1786 ColumnMetadata {
1787 rust_field: "owner_id",
1788 column_name: "owner_id",
1789 renamed_from: None,
1790 sql_type: SqlServerType::BigInt,
1791 nullable: false,
1792 primary_key: false,
1793 identity: None,
1794 default_sql: None,
1795 computed_sql: None,
1796 rowversion: false,
1797 insertable: true,
1798 updatable: true,
1799 max_length: None,
1800 precision: None,
1801 scale: None,
1802 },
1803 ];
1804
1805 static NAVIGATION_ROOT_NAVIGATIONS: [NavigationMetadata; 1] = [NavigationMetadata::new(
1806 "orders",
1807 NavigationKind::HasMany,
1808 "NavigationTarget",
1809 "sales",
1810 "navigation_targets",
1811 &["id"],
1812 &["owner_id"],
1813 Some("fk_navigation_targets_owner"),
1814 )];
1815
1816 static NAVIGATION_ROOT_METADATA: EntityMetadata = EntityMetadata {
1817 rust_name: "NavigationRoot",
1818 schema: "dbo",
1819 table: "navigation_roots",
1820 renamed_from: None,
1821 columns: &NAVIGATION_ROOT_COLUMNS,
1822 primary_key: PrimaryKeyMetadata {
1823 name: None,
1824 columns: &["id"],
1825 },
1826 indexes: &[],
1827 foreign_keys: &[],
1828 navigations: &NAVIGATION_ROOT_NAVIGATIONS,
1829 };
1830
1831 static NAVIGATION_TARGET_NAVIGATIONS: [NavigationMetadata; 1] = [NavigationMetadata::new(
1832 "owner",
1833 NavigationKind::BelongsTo,
1834 "NavigationRoot",
1835 "dbo",
1836 "navigation_roots",
1837 &["owner_id"],
1838 &["id"],
1839 Some("fk_navigation_targets_owner"),
1840 )];
1841
1842 static NAVIGATION_TARGET_METADATA: EntityMetadata = EntityMetadata {
1843 rust_name: "NavigationTarget",
1844 schema: "sales",
1845 table: "navigation_targets",
1846 renamed_from: None,
1847 columns: &NAVIGATION_TARGET_COLUMNS,
1848 primary_key: PrimaryKeyMetadata {
1849 name: None,
1850 columns: &["id"],
1851 },
1852 indexes: &[],
1853 foreign_keys: &[],
1854 navigations: &NAVIGATION_TARGET_NAVIGATIONS,
1855 };
1856
1857 impl Entity for NavigationRoot {
1858 fn metadata() -> &'static EntityMetadata {
1859 &NAVIGATION_ROOT_METADATA
1860 }
1861 }
1862
1863 impl Entity for NavigationTarget {
1864 fn metadata() -> &'static EntityMetadata {
1865 &NAVIGATION_TARGET_METADATA
1866 }
1867 }
1868
1869 #[allow(non_upper_case_globals)]
1870 impl NavigationTarget {
1871 const id: EntityColumn<NavigationTarget> = EntityColumn::new("id", "id");
1872 const owner_id: EntityColumn<NavigationTarget> = EntityColumn::new("owner_id", "owner_id");
1873 }
1874
1875 impl FromRow for NavigationRoot {
1876 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
1877 Ok(Self)
1878 }
1879 }
1880
1881 impl FromRow for NavigationTarget {
1882 fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
1883 Ok(Self {
1884 id: required_i64(row, "id")?,
1885 owner_id: required_i64(row, "owner_id")?,
1886 })
1887 }
1888 }
1889
1890 fn required_i64<R: Row>(row: &R, column: &str) -> Result<i64, OrmError> {
1891 match row.get_required(column)? {
1892 SqlValue::I64(value) => Ok(value),
1893 value => Err(OrmError::new(format!(
1894 "expected `{column}` as i64, got {value:?}"
1895 ))),
1896 }
1897 }
1898
1899 static SOFT_DELETE_POLICY_COLUMNS: [ColumnMetadata; 2] = [
1900 ColumnMetadata {
1901 rust_field: "deleted_at",
1902 column_name: "deleted_at",
1903 renamed_from: None,
1904 sql_type: SqlServerType::DateTime2,
1905 nullable: true,
1906 primary_key: false,
1907 identity: None,
1908 default_sql: None,
1909 computed_sql: None,
1910 rowversion: false,
1911 insertable: false,
1912 updatable: true,
1913 max_length: None,
1914 precision: None,
1915 scale: None,
1916 },
1917 ColumnMetadata {
1918 rust_field: "deleted_by",
1919 column_name: "deleted_by",
1920 renamed_from: None,
1921 sql_type: SqlServerType::NVarChar,
1922 nullable: true,
1923 primary_key: false,
1924 identity: None,
1925 default_sql: None,
1926 computed_sql: None,
1927 rowversion: false,
1928 insertable: false,
1929 updatable: true,
1930 max_length: Some(120),
1931 precision: None,
1932 scale: None,
1933 },
1934 ];
1935
1936 static BOOL_SOFT_DELETE_POLICY_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
1937 rust_field: "is_deleted",
1938 column_name: "is_deleted",
1939 renamed_from: None,
1940 sql_type: SqlServerType::Bit,
1941 nullable: false,
1942 primary_key: false,
1943 identity: None,
1944 default_sql: Some("0"),
1945 computed_sql: None,
1946 rowversion: false,
1947 insertable: false,
1948 updatable: true,
1949 max_length: None,
1950 precision: None,
1951 scale: None,
1952 }];
1953
1954 static SOFT_DELETE_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1955 rust_name: "SoftDeleteEntityUnderTest",
1956 schema: "dbo",
1957 table: "soft_delete_entities",
1958 renamed_from: None,
1959 columns: &[],
1960 primary_key: PrimaryKeyMetadata {
1961 name: None,
1962 columns: &[],
1963 },
1964 indexes: &[],
1965 foreign_keys: &[],
1966 navigations: &[],
1967 };
1968
1969 static BOOL_SOFT_DELETE_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1970 rust_name: "BoolSoftDeleteEntity",
1971 schema: "dbo",
1972 table: "bool_soft_delete_entities",
1973 renamed_from: None,
1974 columns: &[],
1975 primary_key: PrimaryKeyMetadata {
1976 name: None,
1977 columns: &[],
1978 },
1979 indexes: &[],
1980 foreign_keys: &[],
1981 navigations: &[],
1982 };
1983
1984 static TENANT_POLICY_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
1985 rust_field: "tenant_id",
1986 column_name: "tenant_id",
1987 renamed_from: None,
1988 sql_type: SqlServerType::BigInt,
1989 nullable: false,
1990 primary_key: false,
1991 identity: None,
1992 default_sql: None,
1993 computed_sql: None,
1994 rowversion: false,
1995 insertable: true,
1996 updatable: false,
1997 max_length: None,
1998 precision: None,
1999 scale: None,
2000 }];
2001
2002 static TENANT_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2003 rust_name: "TenantEntity",
2004 schema: "sales",
2005 table: "tenant_entities",
2006 renamed_from: None,
2007 columns: &TENANT_POLICY_COLUMNS,
2008 primary_key: PrimaryKeyMetadata {
2009 name: None,
2010 columns: &[],
2011 },
2012 indexes: &[],
2013 foreign_keys: &[],
2014 navigations: &[],
2015 };
2016
2017 static TENANT_NAVIGATION_ROOT_COLUMNS: [ColumnMetadata; 2] = [
2018 ColumnMetadata {
2019 rust_field: "id",
2020 column_name: "id",
2021 renamed_from: None,
2022 sql_type: SqlServerType::BigInt,
2023 nullable: false,
2024 primary_key: true,
2025 identity: None,
2026 default_sql: None,
2027 computed_sql: None,
2028 rowversion: false,
2029 insertable: false,
2030 updatable: false,
2031 max_length: None,
2032 precision: None,
2033 scale: None,
2034 },
2035 TENANT_POLICY_COLUMNS[0],
2036 ];
2037
2038 static TENANT_NAVIGATION_TARGET_COLUMNS: [ColumnMetadata; 2] = [
2039 ColumnMetadata {
2040 rust_field: "id",
2041 column_name: "id",
2042 renamed_from: None,
2043 sql_type: SqlServerType::BigInt,
2044 nullable: false,
2045 primary_key: true,
2046 identity: None,
2047 default_sql: None,
2048 computed_sql: None,
2049 rowversion: false,
2050 insertable: false,
2051 updatable: false,
2052 max_length: None,
2053 precision: None,
2054 scale: None,
2055 },
2056 ColumnMetadata {
2057 rust_field: "owner_id",
2058 column_name: "owner_id",
2059 renamed_from: None,
2060 sql_type: SqlServerType::BigInt,
2061 nullable: false,
2062 primary_key: false,
2063 identity: None,
2064 default_sql: None,
2065 computed_sql: None,
2066 rowversion: false,
2067 insertable: true,
2068 updatable: true,
2069 max_length: None,
2070 precision: None,
2071 scale: None,
2072 },
2073 ];
2074
2075 static TENANT_NAVIGATION_TARGET_NAVIGATIONS: [NavigationMetadata; 1] =
2076 [NavigationMetadata::new(
2077 "owner",
2078 NavigationKind::BelongsTo,
2079 "TenantNavigationRoot",
2080 "sales",
2081 "tenant_navigation_roots",
2082 &["owner_id"],
2083 &["id"],
2084 Some("fk_tenant_navigation_targets_owner"),
2085 )];
2086
2087 static TENANT_NAVIGATION_ROOT_METADATA: EntityMetadata = EntityMetadata {
2088 rust_name: "TenantNavigationRoot",
2089 schema: "sales",
2090 table: "tenant_navigation_roots",
2091 renamed_from: None,
2092 columns: &TENANT_NAVIGATION_ROOT_COLUMNS,
2093 primary_key: PrimaryKeyMetadata {
2094 name: None,
2095 columns: &["id"],
2096 },
2097 indexes: &[],
2098 foreign_keys: &[],
2099 navigations: &[],
2100 };
2101
2102 static TENANT_NAVIGATION_TARGET_METADATA: EntityMetadata = EntityMetadata {
2103 rust_name: "TenantNavigationTarget",
2104 schema: "sales",
2105 table: "tenant_navigation_targets",
2106 renamed_from: None,
2107 columns: &TENANT_NAVIGATION_TARGET_COLUMNS,
2108 primary_key: PrimaryKeyMetadata {
2109 name: None,
2110 columns: &["id"],
2111 },
2112 indexes: &[],
2113 foreign_keys: &[],
2114 navigations: &TENANT_NAVIGATION_TARGET_NAVIGATIONS,
2115 };
2116
2117 impl Entity for SoftDeleteEntityUnderTest {
2118 fn metadata() -> &'static EntityMetadata {
2119 &SOFT_DELETE_ENTITY_METADATA
2120 }
2121 }
2122
2123 impl Entity for BoolSoftDeleteEntity {
2124 fn metadata() -> &'static EntityMetadata {
2125 &BOOL_SOFT_DELETE_ENTITY_METADATA
2126 }
2127 }
2128
2129 impl Entity for TenantEntity {
2130 fn metadata() -> &'static EntityMetadata {
2131 &TENANT_ENTITY_METADATA
2132 }
2133 }
2134
2135 #[allow(non_upper_case_globals)]
2136 impl TenantEntity {
2137 const tenant_id: EntityColumn<TenantEntity> = EntityColumn::new("tenant_id", "tenant_id");
2138 }
2139
2140 impl Entity for TenantNavigationRoot {
2141 fn metadata() -> &'static EntityMetadata {
2142 &TENANT_NAVIGATION_ROOT_METADATA
2143 }
2144 }
2145
2146 impl Entity for TenantNavigationTarget {
2147 fn metadata() -> &'static EntityMetadata {
2148 &TENANT_NAVIGATION_TARGET_METADATA
2149 }
2150 }
2151
2152 impl SoftDeleteEntity for TestEntity {
2153 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2154 None
2155 }
2156 }
2157
2158 impl SoftDeleteEntity for JoinedEntity {
2159 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2160 None
2161 }
2162 }
2163
2164 impl SoftDeleteEntity for SoftDeleteEntityUnderTest {
2165 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2166 Some(EntityPolicyMetadata::new(
2167 "soft_delete",
2168 &SOFT_DELETE_POLICY_COLUMNS,
2169 ))
2170 }
2171 }
2172
2173 impl SoftDeleteEntity for BoolSoftDeleteEntity {
2174 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2175 Some(EntityPolicyMetadata::new(
2176 "soft_delete",
2177 &BOOL_SOFT_DELETE_POLICY_COLUMNS,
2178 ))
2179 }
2180 }
2181
2182 impl SoftDeleteEntity for TenantEntity {
2183 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2184 None
2185 }
2186 }
2187
2188 impl TenantScopedEntity for TestEntity {
2189 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2190 None
2191 }
2192 }
2193
2194 impl TenantScopedEntity for JoinedEntity {
2195 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2196 None
2197 }
2198 }
2199
2200 impl TenantScopedEntity for SoftDeleteEntityUnderTest {
2201 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2202 None
2203 }
2204 }
2205
2206 impl TenantScopedEntity for BoolSoftDeleteEntity {
2207 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2208 None
2209 }
2210 }
2211
2212 impl TenantScopedEntity for TenantEntity {
2213 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2214 Some(EntityPolicyMetadata::new("tenant", &TENANT_POLICY_COLUMNS))
2215 }
2216 }
2217
2218 impl SoftDeleteEntity for NavigationRoot {
2219 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2220 Some(EntityPolicyMetadata::new(
2221 "soft_delete",
2222 &SOFT_DELETE_POLICY_COLUMNS,
2223 ))
2224 }
2225 }
2226
2227 impl SoftDeleteEntity for NavigationTarget {
2228 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2229 None
2230 }
2231 }
2232
2233 impl SoftDeleteEntity for TenantNavigationRoot {
2234 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2235 None
2236 }
2237 }
2238
2239 impl SoftDeleteEntity for TenantNavigationTarget {
2240 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2241 None
2242 }
2243 }
2244
2245 impl TenantScopedEntity for NavigationRoot {
2246 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2247 None
2248 }
2249 }
2250
2251 impl TenantScopedEntity for NavigationTarget {
2252 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2253 None
2254 }
2255 }
2256
2257 impl TenantScopedEntity for TenantNavigationRoot {
2258 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2259 Some(EntityPolicyMetadata::new("tenant", &TENANT_POLICY_COLUMNS))
2260 }
2261 }
2262
2263 impl TenantScopedEntity for TenantNavigationTarget {
2264 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2265 None
2266 }
2267 }
2268
2269 impl IncludeCollection<NavigationTarget> for NavigationRoot {
2270 fn set_included_collection(
2271 &mut self,
2272 _navigation: &str,
2273 _values: Vec<NavigationTarget>,
2274 ) -> Result<(), OrmError> {
2275 Ok(())
2276 }
2277 }
2278
2279 #[derive(Debug)]
2280 struct TestProjectionRow;
2281
2282 impl FromRow for TestProjectionRow {
2283 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
2284 Ok(Self)
2285 }
2286 }
2287
2288 #[test]
2289 fn dbset_query_starts_from_entity_select_query() {
2290 let dbset = DbSet::<TestEntity>::disconnected();
2291 let query = dbset.query();
2292
2293 assert_eq!(
2294 query.select_query(),
2295 &SelectQuery::from_entity::<TestEntity>()
2296 );
2297 }
2298
2299 #[test]
2300 fn dbset_query_accepts_replacement_select_query() {
2301 let dbset = DbSet::<TestEntity>::disconnected();
2302 let custom = SelectQuery::from_entity::<TestEntity>().filter(Predicate::eq(
2303 Expr::value(SqlValue::Bool(true)),
2304 Expr::value(SqlValue::Bool(true)),
2305 ));
2306
2307 let query = dbset.query().with_select_query(custom.clone());
2308
2309 assert_eq!(query.select_query(), &custom);
2310 assert_eq!(query.into_select_query(), custom);
2311 }
2312
2313 #[test]
2314 fn dbset_query_filter_builds_on_internal_select_query() {
2315 let dbset = DbSet::<TestEntity>::disconnected();
2316
2317 let query = dbset.query().filter(Predicate::eq(
2318 Expr::value(SqlValue::Bool(true)),
2319 Expr::value(SqlValue::Bool(true)),
2320 ));
2321
2322 assert_eq!(
2323 query.into_select_query(),
2324 SelectQuery::from_entity::<TestEntity>().filter(Predicate::eq(
2325 Expr::value(SqlValue::Bool(true)),
2326 Expr::value(SqlValue::Bool(true)),
2327 ))
2328 );
2329 }
2330
2331 #[test]
2332 fn dbset_query_select_builds_projection_with_aliases() {
2333 let dbset = DbSet::<TestEntity>::disconnected();
2334
2335 let query = dbset
2336 .query()
2337 .select((TestEntity::id, TestEntity::name))
2338 .into_select_query();
2339
2340 assert_eq!(
2341 query.projection,
2342 vec![
2343 SelectProjection::column(TestEntity::id),
2344 SelectProjection::column(TestEntity::name),
2345 ]
2346 );
2347 }
2348
2349 #[tokio::test]
2350 async fn dbset_query_all_as_reuses_projection_compilation_before_connection() {
2351 let dbset = DbSet::<TestEntity>::disconnected();
2352
2353 let error = dbset
2354 .query()
2355 .select(TestEntity::id)
2356 .all_as::<TestProjectionRow>()
2357 .await
2358 .unwrap_err();
2359
2360 assert_eq!(
2361 error.message(),
2362 "DbSetQuery requires an initialized shared connection"
2363 );
2364 }
2365
2366 #[tokio::test]
2367 async fn dbset_query_first_as_rejects_unaliased_expression_projection() {
2368 let dbset = DbSet::<TestEntity>::disconnected();
2369
2370 let error = dbset
2371 .query()
2372 .select(Expr::function("LOWER", vec![Expr::from(TestEntity::name)]))
2373 .first_as::<TestProjectionRow>()
2374 .await
2375 .unwrap_err();
2376
2377 assert_eq!(
2378 error.message(),
2379 "SQL Server projection expressions require an explicit alias"
2380 );
2381 }
2382
2383 #[test]
2384 fn dbset_query_applies_active_only_visibility_for_nullable_indicator() {
2385 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
2386
2387 let query = dbset.query().effective_select_query().unwrap();
2388
2389 assert_eq!(
2390 query,
2391 SelectQuery::from_entity::<SoftDeleteEntityUnderTest>().filter(Predicate::is_null(
2392 Expr::Column(sql_orm_query::ColumnRef::new(
2393 TableRef::new("dbo", "soft_delete_entities"),
2394 "deleted_at",
2395 "deleted_at",
2396 )),
2397 ))
2398 );
2399 }
2400
2401 #[test]
2402 fn dbset_query_with_deleted_removes_soft_delete_filter() {
2403 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
2404
2405 let query = dbset
2406 .query()
2407 .with_deleted()
2408 .effective_select_query()
2409 .unwrap();
2410
2411 assert_eq!(
2412 query,
2413 SelectQuery::from_entity::<SoftDeleteEntityUnderTest>()
2414 );
2415 }
2416
2417 #[test]
2418 fn dbset_query_only_deleted_filters_nullable_indicator() {
2419 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
2420
2421 let query = dbset
2422 .query()
2423 .only_deleted()
2424 .effective_select_query()
2425 .unwrap();
2426
2427 assert_eq!(
2428 query,
2429 SelectQuery::from_entity::<SoftDeleteEntityUnderTest>().filter(Predicate::is_not_null(
2430 Expr::Column(sql_orm_query::ColumnRef::new(
2431 TableRef::new("dbo", "soft_delete_entities"),
2432 "deleted_at",
2433 "deleted_at",
2434 ))
2435 ))
2436 );
2437 }
2438
2439 #[test]
2440 fn dbset_query_uses_bool_indicator_when_soft_delete_column_is_bit() {
2441 let dbset = DbSet::<BoolSoftDeleteEntity>::disconnected();
2442
2443 let active = dbset.query().effective_select_query().unwrap();
2444 let deleted = dbset
2445 .query()
2446 .only_deleted()
2447 .effective_select_query()
2448 .unwrap();
2449
2450 assert_eq!(
2451 active,
2452 SelectQuery::from_entity::<BoolSoftDeleteEntity>().filter(Predicate::eq(
2453 Expr::Column(sql_orm_query::ColumnRef::new(
2454 TableRef::new("dbo", "bool_soft_delete_entities"),
2455 "is_deleted",
2456 "is_deleted",
2457 )),
2458 Expr::Value(SqlValue::Bool(false)),
2459 ))
2460 );
2461 assert_eq!(
2462 deleted,
2463 SelectQuery::from_entity::<BoolSoftDeleteEntity>().filter(Predicate::eq(
2464 Expr::Column(sql_orm_query::ColumnRef::new(
2465 TableRef::new("dbo", "bool_soft_delete_entities"),
2466 "is_deleted",
2467 "is_deleted",
2468 )),
2469 Expr::Value(SqlValue::Bool(true)),
2470 ))
2471 );
2472 }
2473
2474 #[test]
2475 fn dbset_query_applies_active_tenant_filter_for_tenant_scoped_entities() {
2476 let query = DbSetQuery::<TenantEntity>::new(
2477 None,
2478 SelectQuery::from_entity::<TenantEntity>().filter(Predicate::eq(
2479 Expr::value(SqlValue::Bool(true)),
2480 Expr::value(SqlValue::Bool(true)),
2481 )),
2482 )
2483 .with_active_tenant_for_test(ActiveTenant {
2484 column_name: "tenant_id",
2485 value: SqlValue::I64(42),
2486 })
2487 .effective_select_query()
2488 .unwrap();
2489
2490 assert_eq!(
2491 query,
2492 SelectQuery::from_entity::<TenantEntity>()
2493 .filter(Predicate::eq(
2494 Expr::value(SqlValue::Bool(true)),
2495 Expr::value(SqlValue::Bool(true)),
2496 ))
2497 .filter(Predicate::eq(
2498 Expr::Column(sql_orm_query::ColumnRef::new(
2499 TableRef::new("sales", "tenant_entities"),
2500 "tenant_id",
2501 "tenant_id",
2502 )),
2503 Expr::Value(SqlValue::I64(42)),
2504 ))
2505 );
2506 }
2507
2508 #[test]
2509 fn tenant_security_guardrail_keeps_joined_read_sql_tenant_scoped() {
2510 let query = DbSetQuery::<TenantEntity>::new(
2511 None,
2512 SelectQuery::from_entity::<TenantEntity>().inner_join::<JoinedEntity>(Predicate::eq(
2513 Expr::value(SqlValue::Bool(true)),
2514 Expr::value(SqlValue::Bool(true)),
2515 )),
2516 )
2517 .with_active_tenant_for_test(ActiveTenant {
2518 column_name: "tenant_id",
2519 value: SqlValue::I64(42),
2520 })
2521 .effective_select_query()
2522 .unwrap();
2523
2524 let compiled = SqlServerCompiler::compile_select(&query).unwrap();
2525
2526 assert!(
2527 compiled.sql.contains("INNER JOIN [dbo].[joined_entities]"),
2528 "joined tenant read should preserve explicit joins: {}",
2529 compiled.sql
2530 );
2531 assert!(
2532 compiled
2533 .sql
2534 .contains("[sales].[tenant_entities].[tenant_id] = @P"),
2535 "joined tenant read must include tenant predicate on the root entity: {}",
2536 compiled.sql
2537 );
2538 assert!(
2539 compiled.params.contains(&SqlValue::I64(42)),
2540 "joined tenant read params must include active tenant value: {:?}",
2541 compiled.params
2542 );
2543 }
2544
2545 #[test]
2546 fn dbset_query_fails_closed_without_active_tenant_for_tenant_scoped_entities() {
2547 let error =
2548 DbSetQuery::<TenantEntity>::new(None, SelectQuery::from_entity::<TenantEntity>())
2549 .effective_select_query()
2550 .unwrap_err();
2551
2552 assert!(
2553 error
2554 .message()
2555 .contains("requires an active tenant in the DbContext")
2556 );
2557 }
2558
2559 #[test]
2560 fn dbset_query_rejects_mismatched_active_tenant_column() {
2561 let error =
2562 DbSetQuery::<TenantEntity>::new(None, SelectQuery::from_entity::<TenantEntity>())
2563 .with_active_tenant_for_test(ActiveTenant {
2564 column_name: "company_id",
2565 value: SqlValue::I64(42),
2566 })
2567 .effective_select_query()
2568 .unwrap_err();
2569
2570 assert!(error.message().contains("does not match"));
2571 }
2572
2573 #[test]
2574 fn dbset_query_rejects_incompatible_active_tenant_value() {
2575 let error =
2576 DbSetQuery::<TenantEntity>::new(None, SelectQuery::from_entity::<TenantEntity>())
2577 .with_active_tenant_for_test(ActiveTenant {
2578 column_name: "tenant_id",
2579 value: SqlValue::String("not-a-bigint".to_string()),
2580 })
2581 .effective_select_query()
2582 .unwrap_err();
2583
2584 assert!(error.message().contains("not compatible"));
2585 }
2586
2587 #[test]
2588 fn tenant_value_type_matching_rejects_null_even_for_nullable_columns() {
2589 assert!(!tenant_value_matches_column_type(
2590 &SqlValue::Null,
2591 &TENANT_POLICY_COLUMNS[0],
2592 ));
2593 }
2594
2595 #[test]
2596 fn dbset_query_order_by_builds_on_internal_select_query() {
2597 let dbset = DbSet::<TestEntity>::disconnected();
2598
2599 let query = dbset.query().order_by(OrderBy::new(
2600 TableRef::new("dbo", "test_entities"),
2601 "created_at",
2602 SortDirection::Desc,
2603 ));
2604
2605 assert_eq!(
2606 query.into_select_query(),
2607 SelectQuery::from_entity::<TestEntity>().order_by(OrderBy::new(
2608 TableRef::new("dbo", "test_entities"),
2609 "created_at",
2610 SortDirection::Desc,
2611 ))
2612 );
2613 }
2614
2615 #[test]
2616 fn dbset_query_join_builds_on_internal_select_query() {
2617 let dbset = DbSet::<TestEntity>::disconnected();
2618 let join = Join::left(
2619 TableRef::new("dbo", "joined_entities"),
2620 Predicate::eq(
2621 Expr::value(SqlValue::Bool(true)),
2622 Expr::value(SqlValue::Bool(true)),
2623 ),
2624 );
2625
2626 let query = dbset.query().join(join.clone());
2627
2628 assert_eq!(
2629 query.into_select_query(),
2630 SelectQuery::from_entity::<TestEntity>().join(join)
2631 );
2632 }
2633
2634 #[test]
2635 fn dbset_query_exposes_entity_targeted_join_helpers() {
2636 let dbset = DbSet::<TestEntity>::disconnected();
2637
2638 let query = dbset
2639 .query()
2640 .inner_join::<JoinedEntity>(Predicate::eq(
2641 Expr::value(SqlValue::Bool(true)),
2642 Expr::value(SqlValue::Bool(true)),
2643 ))
2644 .left_join::<JoinedEntity>(Predicate::eq(
2645 Expr::value(SqlValue::Bool(false)),
2646 Expr::value(SqlValue::Bool(false)),
2647 ));
2648
2649 let select = query.into_select_query();
2650
2651 assert_eq!(select.joins.len(), 2);
2652 assert_eq!(select.joins[0].join_type, JoinType::Inner);
2653 assert_eq!(
2654 select.joins[0].table,
2655 TableRef::new("dbo", "joined_entities")
2656 );
2657 assert_eq!(select.joins[1].join_type, JoinType::Left);
2658 assert_eq!(
2659 select.joins[1].table,
2660 TableRef::new("dbo", "joined_entities")
2661 );
2662 }
2663
2664 #[test]
2665 fn dbset_query_infers_navigation_join_from_metadata() {
2666 let dbset = DbSet::<NavigationRoot>::disconnected();
2667
2668 let select = dbset
2669 .query()
2670 .try_inner_join_navigation::<NavigationTarget>("orders")
2671 .unwrap()
2672 .into_select_query();
2673
2674 assert_eq!(select.joins.len(), 1);
2675 assert_eq!(select.joins[0].join_type, JoinType::Inner);
2676 assert_eq!(
2677 select.joins[0].table,
2678 TableRef::new("sales", "navigation_targets")
2679 );
2680 assert_eq!(
2681 select.joins[0].on,
2682 Predicate::eq(
2683 Expr::Column(ColumnRef::new(
2684 TableRef::new("dbo", "navigation_roots"),
2685 "id",
2686 "id",
2687 )),
2688 Expr::Column(ColumnRef::new(
2689 TableRef::new("sales", "navigation_targets"),
2690 "owner_id",
2691 "owner_id",
2692 )),
2693 )
2694 );
2695 }
2696
2697 #[test]
2698 fn dbset_query_infers_aliased_navigation_join_from_metadata() {
2699 let dbset = DbSet::<NavigationRoot>::disconnected();
2700
2701 let select = dbset
2702 .query()
2703 .try_left_join_navigation_as::<NavigationTarget>("orders", "orders")
2704 .unwrap()
2705 .into_select_query();
2706
2707 assert_eq!(select.joins.len(), 1);
2708 assert_eq!(select.joins[0].join_type, JoinType::Left);
2709 assert_eq!(
2710 select.joins[0].table,
2711 TableRef::with_alias("sales", "navigation_targets", "orders")
2712 );
2713 assert_eq!(
2714 select.joins[0].on,
2715 Predicate::eq(
2716 Expr::Column(ColumnRef::new(
2717 TableRef::new("dbo", "navigation_roots"),
2718 "id",
2719 "id",
2720 )),
2721 Expr::Column(ColumnRef::new(
2722 TableRef::with_alias("sales", "navigation_targets", "orders"),
2723 "owner_id",
2724 "owner_id",
2725 )),
2726 )
2727 );
2728 }
2729
2730 #[test]
2731 fn dbset_query_rejects_unknown_navigation_join() {
2732 let error = DbSet::<NavigationRoot>::disconnected()
2733 .query()
2734 .try_inner_join_navigation::<NavigationTarget>("missing")
2735 .unwrap_err();
2736
2737 assert!(
2738 error
2739 .message()
2740 .contains("does not declare navigation `missing`")
2741 );
2742 }
2743
2744 #[test]
2745 fn dbset_query_rejects_navigation_join_target_mismatch() {
2746 let error = DbSet::<NavigationRoot>::disconnected()
2747 .query()
2748 .try_inner_join_navigation::<JoinedEntity>("orders")
2749 .unwrap_err();
2750
2751 assert!(
2752 error
2753 .message()
2754 .contains("targets `sales.navigation_targets`")
2755 );
2756 }
2757
2758 #[test]
2759 fn dbset_query_include_projects_root_and_prefixed_related_columns() {
2760 let include = DbSet::<NavigationTarget>::disconnected()
2761 .query()
2762 .include_as::<NavigationRoot>("owner", "owner")
2763 .unwrap();
2764
2765 let select = include.select_query().unwrap();
2766
2767 assert_eq!(select.joins.len(), 1);
2768 assert_eq!(select.joins[0].join_type, JoinType::Left);
2769 assert_eq!(
2770 select.joins[0].table,
2771 TableRef::with_alias("dbo", "navigation_roots", "owner")
2772 );
2773 assert_eq!(select.projection.len(), 3);
2774 assert_eq!(select.projection[0].alias, Some("id"));
2775 assert_eq!(select.projection[1].alias, Some("owner_id"));
2776 assert_eq!(select.projection[2].alias, Some("owner__id"));
2777 }
2778
2779 #[test]
2780 fn compiled_include_sql_preserves_projection_aliases_soft_delete_and_params() {
2781 let include = DbSet::<NavigationTarget>::disconnected()
2782 .query()
2783 .include_as::<NavigationRoot>("owner", "owner")
2784 .unwrap()
2785 .filter(Predicate::gt(
2786 Expr::Column(ColumnRef::new(
2787 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2788 "id",
2789 "id",
2790 )),
2791 Expr::value(SqlValue::I64(7)),
2792 ))
2793 .order_by(OrderBy::new(
2794 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2795 "id",
2796 SortDirection::Desc,
2797 ))
2798 .paginate(PageRequest::new(2, 10));
2799
2800 let compiled = SqlServerCompiler::compile_select(&include.select_query().unwrap()).unwrap();
2801
2802 assert_snapshot!(
2803 "compiled_include_one_with_soft_delete_and_parameters",
2804 render_compiled_query(&compiled)
2805 );
2806 }
2807
2808 #[test]
2809 fn dbset_query_include_applies_included_soft_delete_filter_to_join_on() {
2810 let include = DbSet::<NavigationTarget>::disconnected()
2811 .query()
2812 .include_as::<NavigationRoot>("owner", "owner")
2813 .unwrap();
2814
2815 let select = include.select_query().unwrap();
2816
2817 assert_eq!(
2818 select.joins[0].on,
2819 Predicate::and(vec![
2820 Predicate::eq(
2821 Expr::Column(ColumnRef::new(
2822 TableRef::new("sales", "navigation_targets"),
2823 "owner_id",
2824 "owner_id",
2825 )),
2826 Expr::Column(ColumnRef::new(
2827 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2828 "id",
2829 "id",
2830 )),
2831 ),
2832 Predicate::is_null(Expr::Column(ColumnRef::new(
2833 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2834 "deleted_at",
2835 "deleted_at",
2836 ))),
2837 ])
2838 );
2839 }
2840
2841 #[test]
2842 fn dbset_query_include_applies_included_tenant_filter_to_join_on() {
2843 let include = DbSet::<TenantNavigationTarget>::disconnected()
2844 .query()
2845 .with_active_tenant_for_test(ActiveTenant {
2846 column_name: "tenant_id",
2847 value: SqlValue::I64(42),
2848 })
2849 .include_as::<TenantNavigationRoot>("owner", "owner")
2850 .unwrap();
2851
2852 let select = include.select_query().unwrap();
2853
2854 assert_eq!(
2855 select.joins[0].on,
2856 Predicate::and(vec![
2857 Predicate::eq(
2858 Expr::Column(ColumnRef::new(
2859 TableRef::new("sales", "tenant_navigation_targets"),
2860 "owner_id",
2861 "owner_id",
2862 )),
2863 Expr::Column(ColumnRef::new(
2864 TableRef::with_alias("sales", "tenant_navigation_roots", "owner"),
2865 "id",
2866 "id",
2867 )),
2868 ),
2869 Predicate::eq(
2870 Expr::Column(ColumnRef::new(
2871 TableRef::with_alias("sales", "tenant_navigation_roots", "owner"),
2872 "tenant_id",
2873 "tenant_id",
2874 )),
2875 Expr::Value(SqlValue::I64(42)),
2876 ),
2877 ])
2878 );
2879 }
2880
2881 #[test]
2882 fn compiled_include_sql_preserves_included_tenant_parameter_order() {
2883 let include = DbSet::<TenantNavigationTarget>::disconnected()
2884 .query()
2885 .filter(Predicate::gt(
2886 Expr::Column(ColumnRef::new(
2887 TableRef::new("sales", "tenant_navigation_targets"),
2888 "id",
2889 "id",
2890 )),
2891 Expr::value(SqlValue::I64(100)),
2892 ))
2893 .with_active_tenant_for_test(ActiveTenant {
2894 column_name: "tenant_id",
2895 value: SqlValue::I64(42),
2896 })
2897 .include_as::<TenantNavigationRoot>("owner", "owner")
2898 .unwrap();
2899
2900 let compiled = SqlServerCompiler::compile_select(&include.select_query().unwrap()).unwrap();
2901
2902 assert_snapshot!(
2903 "compiled_include_one_with_included_tenant_parameter_order",
2904 render_compiled_query(&compiled)
2905 );
2906 assert_eq!(compiled.params, vec![SqlValue::I64(42), SqlValue::I64(100)]);
2907 }
2908
2909 #[test]
2910 fn dbset_query_include_fails_closed_for_included_tenant_without_active_tenant() {
2911 let include = DbSet::<TenantNavigationTarget>::disconnected()
2912 .query()
2913 .include_as::<TenantNavigationRoot>("owner", "owner")
2914 .unwrap();
2915
2916 let error = include.select_query().unwrap_err();
2917
2918 assert!(
2919 error
2920 .message()
2921 .contains("requires an active tenant in the DbContext")
2922 );
2923 }
2924
2925 #[test]
2926 fn dbset_query_include_supports_chained_filter_order_and_pagination() {
2927 let include = DbSet::<NavigationTarget>::disconnected()
2928 .query()
2929 .include_as::<NavigationRoot>("owner", "owner")
2930 .unwrap()
2931 .filter(Predicate::gt(
2932 Expr::Column(ColumnRef::new(
2933 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2934 "id",
2935 "id",
2936 )),
2937 Expr::value(SqlValue::I64(0)),
2938 ))
2939 .order_by(OrderBy::new(
2940 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2941 "id",
2942 SortDirection::Desc,
2943 ))
2944 .paginate(PageRequest::new(2, 10));
2945
2946 let select = include.select_query().unwrap();
2947
2948 assert_eq!(
2949 select.predicate,
2950 Some(Predicate::gt(
2951 Expr::Column(ColumnRef::new(
2952 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2953 "id",
2954 "id",
2955 )),
2956 Expr::value(SqlValue::I64(0)),
2957 ))
2958 );
2959 assert_eq!(
2960 select.order_by,
2961 vec![OrderBy::new(
2962 TableRef::with_alias("dbo", "navigation_roots", "owner"),
2963 "id",
2964 SortDirection::Desc,
2965 )]
2966 );
2967 assert_eq!(select.pagination, Some(Pagination::new(10, 10)));
2968 }
2969
2970 #[test]
2971 fn dbset_query_include_rejects_collection_navigation() {
2972 let result = DbSet::<NavigationRoot>::disconnected()
2973 .query()
2974 .include::<NavigationTarget>("orders");
2975 let error = match result {
2976 Ok(_) => panic!("expected collection include to be rejected"),
2977 Err(error) => error,
2978 };
2979
2980 assert!(error.message().contains("belongs_to and has_one"));
2981 }
2982
2983 #[test]
2984 fn dbset_query_include_many_projects_root_and_prefixed_related_columns() {
2985 let include = DbSet::<NavigationRoot>::disconnected()
2986 .query()
2987 .include_many_as::<NavigationTarget>("orders", "orders")
2988 .unwrap();
2989
2990 let select = include.select_query().unwrap();
2991
2992 assert_eq!(select.joins.len(), 1);
2993 assert_eq!(select.joins[0].join_type, JoinType::Left);
2994 assert_eq!(
2995 select.joins[0].table,
2996 TableRef::with_alias("sales", "navigation_targets", "orders")
2997 );
2998 assert_eq!(select.projection.len(), 3);
2999 assert_eq!(select.projection[0].alias, Some("id"));
3000 assert_eq!(select.projection[1].alias, Some("orders__id"));
3001 assert_eq!(select.projection[2].alias, Some("orders__owner_id"));
3002 }
3003
3004 #[test]
3005 fn compiled_include_many_sql_preserves_grouping_projection_and_root_soft_delete() {
3006 let include = DbSet::<NavigationRoot>::disconnected()
3007 .query()
3008 .include_many_as::<NavigationTarget>("orders", "orders")
3009 .unwrap()
3010 .filter(Predicate::gte(
3011 Expr::Column(ColumnRef::new(
3012 TableRef::with_alias("sales", "navigation_targets", "orders"),
3013 "owner_id",
3014 "owner_id",
3015 )),
3016 Expr::value(SqlValue::I64(7)),
3017 ))
3018 .order_by(OrderBy::new(
3019 TableRef::with_alias("sales", "navigation_targets", "orders"),
3020 "id",
3021 SortDirection::Asc,
3022 ));
3023
3024 let compiled = SqlServerCompiler::compile_select(&include.select_query().unwrap()).unwrap();
3025
3026 assert_snapshot!(
3027 "compiled_include_many_with_root_soft_delete_and_parameters",
3028 render_compiled_query(&compiled)
3029 );
3030 assert_eq!(compiled.params, vec![SqlValue::I64(7)]);
3031 }
3032
3033 #[test]
3034 fn dbset_query_include_many_rejects_non_collection_navigation() {
3035 let result = DbSet::<NavigationTarget>::disconnected()
3036 .query()
3037 .include_many::<NavigationRoot>("owner");
3038 let error = match result {
3039 Ok(_) => panic!("expected non-collection include_many to be rejected"),
3040 Err(error) => error,
3041 };
3042
3043 assert!(error.message().contains("has_many"));
3044 }
3045
3046 #[test]
3047 fn dbset_query_include_many_rejects_pagination_for_join_grouping() {
3048 let include = DbSet::<NavigationRoot>::disconnected()
3049 .query()
3050 .take(10)
3051 .include_many_as::<NavigationTarget>("orders", "orders")
3052 .unwrap();
3053
3054 let error = include.select_query().unwrap_err();
3055
3056 assert!(error.message().contains("does not support pagination"));
3057 }
3058
3059 #[tokio::test]
3060 async fn dbset_query_include_many_split_query_reports_explicit_error() {
3061 let error = DbSet::<NavigationRoot>::disconnected()
3062 .query()
3063 .include_many_as::<NavigationTarget>("orders", "orders")
3064 .unwrap()
3065 .split_query()
3066 .all()
3067 .await
3068 .unwrap_err();
3069
3070 assert!(
3071 error
3072 .message()
3073 .contains("split-query loading is not implemented yet")
3074 );
3075 }
3076
3077 #[test]
3078 fn include_many_join_row_limit_reports_clear_error() {
3079 let error = enforce_include_many_join_row_limit(11, Some(10)).unwrap_err();
3080
3081 assert!(error.message().contains("produced 11 rows"));
3082 assert!(error.message().contains("configured limit of 10"));
3083 }
3084
3085 #[test]
3086 fn include_many_join_row_limit_allows_explicit_unbounded_join() {
3087 enforce_include_many_join_row_limit(usize::MAX, None).unwrap();
3088 }
3089
3090 #[test]
3091 fn include_navigation_identity_map_helper_reuses_tracked_snapshot_without_registering() {
3092 let registry = std::sync::Arc::new(TrackingRegistry::default());
3093 let mut tracked = Tracked::from_loaded(NavigationTarget { id: 7, owner_id: 1 });
3094 tracked
3095 .attach_registry_loaded(std::sync::Arc::clone(®istry), SqlValue::I64(7))
3096 .unwrap();
3097 tracked.current_mut().owner_id = 99;
3098
3099 let materialized = NavigationTarget { id: 7, owner_id: 1 };
3100 let mapped =
3101 identity_mapped_navigation_value(Some(®istry), Some(SqlValue::I64(7)), materialized);
3102
3103 assert_eq!(mapped.owner_id, 99);
3104 assert_eq!(registry.tracked_for::<NavigationTarget>().len(), 1);
3105
3106 let ordinary = identity_mapped_navigation_value(
3107 Some(®istry),
3108 Some(SqlValue::I64(8)),
3109 NavigationTarget { id: 8, owner_id: 2 },
3110 );
3111 assert_eq!(ordinary.owner_id, 2);
3112 assert_eq!(registry.tracked_for::<NavigationTarget>().len(), 1);
3113 }
3114
3115 #[test]
3116 fn compiled_self_join_sql_preserves_repeated_aliases_and_parameter_order() {
3117 let query = SelectQuery::from_entity_as::<TestEntity>("root")
3118 .select(vec![
3119 SelectProjection::expr_as(Expr::column_as(TestEntity::id, "root"), "root_id"),
3120 SelectProjection::expr_as(
3121 Expr::column_as(TestEntity::name, "parent"),
3122 "parent_name",
3123 ),
3124 SelectProjection::expr_as(Expr::column_as(TestEntity::name, "child"), "child_name"),
3125 ])
3126 .inner_join_as::<TestEntity>(
3127 "parent",
3128 Predicate::eq(
3129 Expr::column_as(TestEntity::id, "root"),
3130 Expr::column_as(TestEntity::id, "parent"),
3131 ),
3132 )
3133 .left_join_as::<TestEntity>(
3134 "child",
3135 Predicate::eq(
3136 Expr::column_as(TestEntity::id, "root"),
3137 Expr::column_as(TestEntity::id, "child"),
3138 ),
3139 )
3140 .filter(Predicate::like(
3141 Expr::column_as(TestEntity::name, "parent"),
3142 Expr::value(SqlValue::String("%admin%".to_string())),
3143 ))
3144 .filter(Predicate::gte(
3145 Expr::column_as(TestEntity::id, "child"),
3146 Expr::value(SqlValue::I64(10)),
3147 ))
3148 .order_by(OrderBy::new(
3149 TableRef::with_alias("dbo", "test_entities", "child"),
3150 "name",
3151 SortDirection::Asc,
3152 ))
3153 .paginate(Pagination::new(20, 10));
3154
3155 let compiled = SqlServerCompiler::compile_select(&query).unwrap();
3156
3157 assert_snapshot!(
3158 "compiled_self_join_repeated_aliases_and_parameter_order",
3159 render_compiled_query(&compiled)
3160 );
3161 assert_eq!(
3162 compiled.params,
3163 vec![
3164 SqlValue::String("%admin%".to_string()),
3165 SqlValue::I64(10),
3166 SqlValue::I64(20),
3167 SqlValue::I64(10),
3168 ]
3169 );
3170 }
3171
3172 #[test]
3173 fn dbset_query_supports_chaining_filter_and_order_by() {
3174 let dbset = DbSet::<TestEntity>::disconnected();
3175
3176 let query = dbset
3177 .query()
3178 .filter(Predicate::eq(
3179 Expr::value(SqlValue::Bool(true)),
3180 Expr::value(SqlValue::Bool(true)),
3181 ))
3182 .order_by(OrderBy::new(
3183 TableRef::new("dbo", "test_entities"),
3184 "created_at",
3185 SortDirection::Asc,
3186 ));
3187
3188 assert_eq!(
3189 query.into_select_query(),
3190 SelectQuery::from_entity::<TestEntity>()
3191 .filter(Predicate::eq(
3192 Expr::value(SqlValue::Bool(true)),
3193 Expr::value(SqlValue::Bool(true)),
3194 ))
3195 .order_by(OrderBy::new(
3196 TableRef::new("dbo", "test_entities"),
3197 "created_at",
3198 SortDirection::Asc,
3199 ))
3200 );
3201 }
3202
3203 #[test]
3204 fn dbset_query_limit_builds_zero_offset_pagination() {
3205 let dbset = DbSet::<TestEntity>::disconnected();
3206
3207 let query = dbset.query().limit(25);
3208
3209 assert_eq!(
3210 query.into_select_query(),
3211 SelectQuery::from_entity::<TestEntity>().paginate(Pagination::new(0, 25))
3212 );
3213 }
3214
3215 #[test]
3216 fn dbset_query_take_is_alias_for_limit() {
3217 let dbset = DbSet::<TestEntity>::disconnected();
3218
3219 let limited = dbset.query().limit(10).into_select_query();
3220 let taken = dbset.query().take(10).into_select_query();
3221
3222 assert_eq!(limited, taken);
3223 }
3224
3225 #[test]
3226 fn dbset_query_paginate_uses_page_request_contract() {
3227 let dbset = DbSet::<TestEntity>::disconnected();
3228
3229 let query = dbset.query().paginate(PageRequest::new(3, 25));
3230
3231 assert_eq!(
3232 query.into_select_query(),
3233 SelectQuery::from_entity::<TestEntity>().paginate(Pagination::new(50, 25))
3234 );
3235 }
3236
3237 #[test]
3238 fn count_row_accepts_i32_and_i64_results() {
3239 struct CountTestRow {
3240 value: SqlValue,
3241 }
3242
3243 impl Row for CountTestRow {
3244 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
3245 Ok((column == "count").then(|| self.value.clone()))
3246 }
3247 }
3248
3249 let from_i32 = super::CountRow::from_row(&CountTestRow {
3250 value: SqlValue::I32(7),
3251 })
3252 .unwrap();
3253 let from_i64 = super::CountRow::from_row(&CountTestRow {
3254 value: SqlValue::I64(9),
3255 })
3256 .unwrap();
3257
3258 assert_eq!(from_i32.value, 7);
3259 assert_eq!(from_i64.value, 9);
3260 }
3261
3262 #[test]
3263 fn count_row_rejects_non_integer_results() {
3264 struct CountTestRow;
3265
3266 impl Row for CountTestRow {
3267 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
3268 Ok((column == "count").then(|| SqlValue::String("7".to_string())))
3269 }
3270 }
3271
3272 let error = super::CountRow::from_row(&CountTestRow).unwrap_err();
3273
3274 assert_eq!(
3275 error.message(),
3276 "expected SQL Server COUNT result as i32 or i64"
3277 );
3278 }
3279
3280 #[test]
3281 fn exists_query_preserves_joins_and_effective_filters() {
3282 let active_tenant = ActiveTenant {
3283 column_name: "tenant_id",
3284 value: SqlValue::I64(42),
3285 };
3286 let dbset = DbSet::<TenantEntity>::disconnected();
3287 let query = dbset
3288 .query()
3289 .with_active_tenant_for_test(active_tenant)
3290 .inner_join::<JoinedEntity>(Predicate::eq(
3291 Expr::Column(ColumnRef::new(
3292 TableRef::for_entity::<TenantEntity>(),
3293 "tenant_id",
3294 "tenant_id",
3295 )),
3296 Expr::Column(ColumnRef::new(
3297 TableRef::for_entity::<JoinedEntity>(),
3298 "tenant_id",
3299 "tenant_id",
3300 )),
3301 ))
3302 .filter(Predicate::eq(
3303 Expr::Column(ColumnRef::new(
3304 TableRef::for_entity::<TenantEntity>(),
3305 "tenant_id",
3306 "tenant_id",
3307 )),
3308 Expr::value(SqlValue::I64(7)),
3309 ));
3310
3311 let exists = query.exists_query().unwrap();
3312
3313 assert_eq!(exists.joins.len(), 1);
3314 let compiled = SqlServerCompiler::compile_exists(&exists).unwrap();
3315 assert!(compiled.sql.contains("INNER JOIN [dbo].[joined_entities]"));
3316 assert!(
3317 compiled
3318 .sql
3319 .contains("[sales].[tenant_entities].[tenant_id] = @P1")
3320 );
3321 assert!(
3322 compiled
3323 .sql
3324 .contains("[sales].[tenant_entities].[tenant_id] = @P2")
3325 );
3326 assert_eq!(compiled.params, vec![SqlValue::I64(7), SqlValue::I64(42)]);
3327 }
3328
3329 #[test]
3330 fn scalar_aggregate_query_preserves_soft_delete_filter_and_ignores_pagination() {
3331 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
3332 let query = dbset
3333 .query()
3334 .filter(Predicate::eq(
3335 Expr::value(SqlValue::Bool(true)),
3336 Expr::value(SqlValue::Bool(true)),
3337 ))
3338 .order_by(OrderBy::new(
3339 TableRef::for_entity::<SoftDeleteEntityUnderTest>(),
3340 "deleted_at",
3341 SortDirection::Desc,
3342 ))
3343 .limit(5);
3344
3345 let aggregate = query
3346 .scalar_aggregate_query(AggregateProjection::expr_as(
3347 AggregateExpr::max(Expr::Column(ColumnRef::new(
3348 TableRef::for_entity::<SoftDeleteEntityUnderTest>(),
3349 "deleted_at",
3350 "deleted_at",
3351 ))),
3352 "value",
3353 ))
3354 .unwrap();
3355
3356 assert!(aggregate.order_by.is_empty());
3357 assert!(aggregate.pagination.is_none());
3358 let compiled = SqlServerCompiler::compile_aggregate(&aggregate).unwrap();
3359 assert!(
3360 compiled
3361 .sql
3362 .starts_with("SELECT MAX([dbo].[soft_delete_entities].[deleted_at]) AS [value]")
3363 );
3364 assert!(
3365 compiled
3366 .sql
3367 .contains("[dbo].[soft_delete_entities].[deleted_at] IS NULL")
3368 );
3369 assert!(!compiled.sql.contains("ORDER BY"));
3370 assert!(!compiled.sql.contains("OFFSET"));
3371 assert_eq!(
3372 compiled.params,
3373 vec![SqlValue::Bool(true), SqlValue::Bool(true)]
3374 );
3375 }
3376
3377 #[test]
3378 fn scalar_aggregate_query_preserves_explicit_join_and_aliased_column() {
3379 let query = DbSet::<NavigationRoot>::disconnected()
3380 .query()
3381 .try_left_join_navigation_as::<NavigationTarget>("orders", "orders")
3382 .unwrap()
3383 .filter(Predicate::gt(
3384 Expr::from(NavigationTarget::owner_id.aliased("orders")),
3385 Expr::value(SqlValue::I64(10)),
3386 ));
3387
3388 let aggregate = query
3389 .scalar_aggregate_query(AggregateProjection::expr_as(
3390 AggregateExpr::max(NavigationTarget::id.aliased("orders")),
3391 "value",
3392 ))
3393 .unwrap();
3394
3395 assert_eq!(aggregate.joins.len(), 1);
3396 assert_eq!(
3397 aggregate.joins[0].table,
3398 TableRef::with_alias("sales", "navigation_targets", "orders")
3399 );
3400
3401 let compiled = SqlServerCompiler::compile_aggregate(&aggregate).unwrap();
3402 assert!(compiled.sql.contains(
3403 "LEFT JOIN [sales].[navigation_targets] AS [orders] ON ([dbo].[navigation_roots].[id] = [orders].[owner_id])"
3404 ));
3405 assert!(
3406 compiled
3407 .sql
3408 .starts_with("SELECT MAX([orders].[id]) AS [value]")
3409 );
3410 assert!(compiled.sql.contains("WHERE ("));
3411 assert!(compiled.sql.contains("[orders].[owner_id] > @P1"));
3412 assert!(
3413 compiled
3414 .sql
3415 .contains("[dbo].[navigation_roots].[deleted_at] IS NULL")
3416 );
3417 assert_eq!(compiled.params, vec![SqlValue::I64(10)]);
3418 }
3419
3420 #[test]
3421 fn grouped_query_builds_aggregate_ast_with_projection_having_order_and_pagination() {
3422 let dbset = DbSet::<TestEntity>::disconnected();
3423 let grouped = dbset
3424 .query()
3425 .filter(Predicate::eq(
3426 Expr::value(SqlValue::Bool(true)),
3427 Expr::value(SqlValue::Bool(true)),
3428 ))
3429 .group_by(TestEntity::id)
3430 .unwrap()
3431 .select_aggregate((
3432 AggregateProjection::group_key(TestEntity::id),
3433 AggregateProjection::count_as("entity_count"),
3434 ))
3435 .having(AggregatePredicate::gt(
3436 AggregateExpr::count_all(),
3437 Expr::value(SqlValue::I64(1)),
3438 ))
3439 .order_by(AggregateOrderBy::desc(AggregateExpr::count_all()))
3440 .paginate(PageRequest::new(2, 10));
3441
3442 let aggregate = grouped.aggregate_query();
3443
3444 assert_eq!(aggregate.group_by, vec![Expr::from(TestEntity::id)]);
3445 assert_eq!(aggregate.projection.len(), 2);
3446 assert!(matches!(
3447 aggregate.having,
3448 Some(AggregatePredicate::Gt(_, _))
3449 ));
3450 assert_eq!(aggregate.order_by.len(), 1);
3451 assert_eq!(aggregate.pagination, Some(Pagination::new(10, 10)));
3452
3453 let compiled = SqlServerCompiler::compile_aggregate(aggregate).unwrap();
3454 assert!(compiled.sql.contains("COUNT(*) AS [entity_count]"));
3455 assert!(compiled.sql.contains("GROUP BY [dbo].[test_entities].[id]"));
3456 assert!(compiled.sql.contains("HAVING (COUNT(*) > @P3)"));
3457 assert!(compiled.sql.contains("ORDER BY COUNT(*) DESC"));
3458 assert!(
3459 compiled
3460 .sql
3461 .contains("OFFSET @P4 ROWS FETCH NEXT @P5 ROWS ONLY")
3462 );
3463 assert_eq!(
3464 compiled.params,
3465 vec![
3466 SqlValue::Bool(true),
3467 SqlValue::Bool(true),
3468 SqlValue::I64(1),
3469 SqlValue::I64(10),
3470 SqlValue::I64(10),
3471 ]
3472 );
3473 }
3474
3475 #[test]
3476 fn grouped_query_preserves_explicit_navigation_alias_join() {
3477 let grouped = DbSet::<NavigationRoot>::disconnected()
3478 .query()
3479 .try_left_join_navigation_as::<NavigationTarget>("orders", "orders")
3480 .unwrap()
3481 .filter(Predicate::gt(
3482 Expr::from(NavigationTarget::owner_id.aliased("orders")),
3483 Expr::value(SqlValue::I64(10)),
3484 ))
3485 .group_by(NavigationTarget::owner_id.aliased("orders"))
3486 .unwrap()
3487 .select_aggregate((
3488 NavigationTarget::owner_id.aliased("orders"),
3489 AggregateProjection::count_as("order_count"),
3490 ))
3491 .having(AggregatePredicate::gt(
3492 AggregateExpr::count_all(),
3493 Expr::value(SqlValue::I64(1)),
3494 ))
3495 .order_by(AggregateOrderBy::desc(AggregateExpr::count_all()));
3496
3497 let aggregate = grouped.aggregate_query();
3498
3499 assert_eq!(aggregate.joins.len(), 1);
3500 assert_eq!(
3501 aggregate.group_by,
3502 vec![Expr::from(NavigationTarget::owner_id.aliased("orders"))]
3503 );
3504 assert_eq!(aggregate.projection[0].alias, "owner_id");
3505
3506 let compiled = SqlServerCompiler::compile_aggregate(aggregate).unwrap();
3507 assert!(compiled.sql.contains(
3508 "LEFT JOIN [sales].[navigation_targets] AS [orders] ON ([dbo].[navigation_roots].[id] = [orders].[owner_id])"
3509 ));
3510 assert!(compiled.sql.contains("[orders].[owner_id] AS [owner_id]"));
3511 assert!(compiled.sql.contains("COUNT(*) AS [order_count]"));
3512 assert!(compiled.sql.contains("GROUP BY [orders].[owner_id]"));
3513 assert!(compiled.sql.contains("HAVING (COUNT(*) > @P2)"));
3514 assert!(compiled.sql.contains("ORDER BY COUNT(*) DESC"));
3515 assert!(
3516 compiled
3517 .sql
3518 .contains("[dbo].[navigation_roots].[deleted_at] IS NULL")
3519 );
3520 assert_eq!(compiled.params, vec![SqlValue::I64(10), SqlValue::I64(1)]);
3521 }
3522
3523 #[test]
3524 fn grouped_query_does_not_infer_hidden_join_from_aliased_group_key() {
3525 let grouped = DbSet::<NavigationRoot>::disconnected()
3526 .query()
3527 .group_by(NavigationTarget::owner_id.aliased("orders"))
3528 .unwrap()
3529 .select_aggregate((
3530 NavigationTarget::owner_id.aliased("orders"),
3531 AggregateProjection::count_as("order_count"),
3532 ));
3533
3534 let aggregate = grouped.aggregate_query();
3535
3536 assert!(aggregate.joins.is_empty());
3537 assert_eq!(
3538 aggregate.group_by,
3539 vec![Expr::from(NavigationTarget::owner_id.aliased("orders"))]
3540 );
3541
3542 let compiled = SqlServerCompiler::compile_aggregate(aggregate).unwrap();
3543 assert!(!compiled.sql.contains(" JOIN "));
3544 assert!(compiled.sql.contains("GROUP BY [orders].[owner_id]"));
3545 }
3546
3547 #[test]
3548 fn grouped_query_preserves_root_tenant_and_soft_delete_filters() {
3549 let active_tenant = ActiveTenant {
3550 column_name: "tenant_id",
3551 value: SqlValue::I64(42),
3552 };
3553 let dbset = DbSet::<TenantEntity>::disconnected();
3554 let grouped = dbset
3555 .query()
3556 .with_active_tenant_for_test(active_tenant)
3557 .group_by(TenantEntity::tenant_id)
3558 .unwrap()
3559 .select_aggregate((
3560 AggregateProjection::group_key(TenantEntity::tenant_id),
3561 AggregateProjection::count_as("tenant_count"),
3562 ));
3563
3564 let compiled = SqlServerCompiler::compile_aggregate(grouped.aggregate_query()).unwrap();
3565
3566 assert!(
3567 compiled
3568 .sql
3569 .contains("GROUP BY [sales].[tenant_entities].[tenant_id]")
3570 );
3571 assert!(
3572 compiled
3573 .sql
3574 .contains("[sales].[tenant_entities].[tenant_id] = @P1")
3575 );
3576 assert_eq!(compiled.params, vec![SqlValue::I64(42)]);
3577 }
3578
3579 #[test]
3580 fn grouped_query_rejects_empty_group_by_early() {
3581 let dbset = DbSet::<TestEntity>::disconnected();
3582 let error = dbset.query().group_by(Vec::<Expr>::new()).unwrap_err();
3583
3584 assert_eq!(
3585 error.message(),
3586 "group_by requires at least one group key expression"
3587 );
3588 }
3589
3590 #[test]
3591 fn grouped_query_try_select_aggregate_rejects_alias_ambiguity_early() {
3592 let grouped = DbSet::<TestEntity>::disconnected()
3593 .query()
3594 .group_by(TestEntity::id)
3595 .unwrap();
3596
3597 let empty_error = grouped
3598 .try_select_aggregate(AggregateProjection::count_as(" "))
3599 .unwrap_err();
3600 assert_eq!(
3601 empty_error.message(),
3602 "aggregate projection alias cannot be empty"
3603 );
3604
3605 let grouped = DbSet::<TestEntity>::disconnected()
3606 .query()
3607 .group_by(TestEntity::id)
3608 .unwrap();
3609 let duplicate_error = grouped
3610 .try_select_aggregate((
3611 AggregateProjection::count_as("value"),
3612 AggregateProjection::sum_as(TestEntity::id, "value"),
3613 ))
3614 .unwrap_err();
3615 assert_eq!(
3616 duplicate_error.message(),
3617 "aggregate projection alias `value` is duplicated"
3618 );
3619
3620 let empty_projection_error = validate_aggregate_projection_aliases(&[]).unwrap_err();
3621 assert_eq!(
3622 empty_projection_error.message(),
3623 "select_aggregate requires at least one aggregate projection"
3624 );
3625 }
3626
3627 #[test]
3628 fn grouped_query_debug_mentions_entity_type() {
3629 let dbset = DbSet::<TestEntity>::disconnected();
3630 let grouped = dbset
3631 .query()
3632 .group_by(TestEntity::id)
3633 .unwrap()
3634 .select_aggregate(AggregateProjection::count_as("entity_count"));
3635
3636 let rendered = format!("{grouped:?}");
3637
3638 assert!(rendered.contains("DbSetGroupedQuery"));
3639 assert!(rendered.contains("test_entities"));
3640 }
3641
3642 #[test]
3643 fn scalar_aggregate_row_materializes_values_and_nulls() {
3644 struct AggregateTestRow {
3645 value: Option<SqlValue>,
3646 }
3647
3648 impl Row for AggregateTestRow {
3649 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
3650 Ok((column == "value").then(|| self.value.clone()).flatten())
3651 }
3652 }
3653
3654 let from_value = super::ScalarAggregateRow::<i64>::from_row(&AggregateTestRow {
3655 value: Some(SqlValue::I64(12)),
3656 })
3657 .unwrap();
3658 let from_null = super::ScalarAggregateRow::<i64>::from_row(&AggregateTestRow {
3659 value: Some(SqlValue::Null),
3660 })
3661 .unwrap();
3662 let missing = super::ScalarAggregateRow::<i64>::from_row(&AggregateTestRow { value: None })
3663 .unwrap_err();
3664
3665 assert_eq!(from_value.value, Some(12));
3666 assert_eq!(from_null.value, None);
3667 assert_eq!(
3668 missing.message(),
3669 "scalar aggregate result column was not present"
3670 );
3671 }
3672
3673 #[test]
3674 fn scalar_aggregate_row_validates_supported_return_types_strictly() {
3675 struct AggregateTestRow {
3676 value: SqlValue,
3677 }
3678
3679 impl Row for AggregateTestRow {
3680 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
3681 Ok((column == "value").then(|| self.value.clone()))
3682 }
3683 }
3684
3685 let date = NaiveDate::from_ymd_opt(2026, 5, 17).expect("valid date");
3686 let datetime: NaiveDateTime = date.and_hms_opt(9, 30, 0).expect("valid datetime");
3687 let decimal = Decimal::new(12345, 2);
3688
3689 let i32_row = super::ScalarAggregateRow::<i32>::from_row(&AggregateTestRow {
3690 value: SqlValue::I32(7),
3691 })
3692 .unwrap();
3693 let i64_row = super::ScalarAggregateRow::<i64>::from_row(&AggregateTestRow {
3694 value: SqlValue::I64(9),
3695 })
3696 .unwrap();
3697 let f64_row = super::ScalarAggregateRow::<f64>::from_row(&AggregateTestRow {
3698 value: SqlValue::F64(10.5),
3699 })
3700 .unwrap();
3701 let decimal_row = super::ScalarAggregateRow::<Decimal>::from_row(&AggregateTestRow {
3702 value: SqlValue::Decimal(decimal),
3703 })
3704 .unwrap();
3705 let string_row = super::ScalarAggregateRow::<String>::from_row(&AggregateTestRow {
3706 value: SqlValue::String("last".to_string()),
3707 })
3708 .unwrap();
3709 let date_row = super::ScalarAggregateRow::<NaiveDate>::from_row(&AggregateTestRow {
3710 value: SqlValue::Date(date),
3711 })
3712 .unwrap();
3713 let datetime_row =
3714 super::ScalarAggregateRow::<NaiveDateTime>::from_row(&AggregateTestRow {
3715 value: SqlValue::DateTime(datetime),
3716 })
3717 .unwrap();
3718 let mismatch = super::ScalarAggregateRow::<i64>::from_row(&AggregateTestRow {
3719 value: SqlValue::I32(7),
3720 })
3721 .unwrap_err();
3722
3723 assert_eq!(i32_row.value, Some(7));
3724 assert_eq!(i64_row.value, Some(9));
3725 assert_eq!(f64_row.value, Some(10.5));
3726 assert_eq!(decimal_row.value, Some(decimal));
3727 assert_eq!(string_row.value, Some("last".to_string()));
3728 assert_eq!(date_row.value, Some(date));
3729 assert_eq!(datetime_row.value, Some(datetime));
3730 assert_eq!(mismatch.message(), "expected i64 value");
3731 }
3732
3733 #[test]
3734 fn exists_row_materializes_bool_result() {
3735 struct ExistsTestRow;
3736
3737 impl Row for ExistsTestRow {
3738 fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
3739 Ok((column == "exists").then_some(SqlValue::Bool(true)))
3740 }
3741 }
3742
3743 let row = super::ExistsRow::from_row(&ExistsTestRow).unwrap();
3744
3745 assert!(row.value);
3746 }
3747
3748 #[test]
3749 fn debug_mentions_entity_type() {
3750 let query = DbSetQuery::<TestEntity>::new(None, SelectQuery::from_entity::<TestEntity>());
3751
3752 let rendered = format!("{query:?}");
3753
3754 assert!(rendered.contains("DbSetQuery"));
3755 assert!(rendered.contains("test_entities"));
3756 }
3757
3758 fn render_compiled_query(compiled: &CompiledQuery) -> String {
3759 let params = compiled
3760 .params
3761 .iter()
3762 .enumerate()
3763 .map(|(index, value)| format!("{}: {:?}", index + 1, value))
3764 .collect::<Vec<_>>();
3765
3766 if params.is_empty() {
3767 format!("SQL: {}\nParams:\n<none>", compiled.sql)
3768 } else {
3769 format!("SQL: {}\nParams:\n{}", compiled.sql, params.join("\n"))
3770 }
3771 }
3772}