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