Skip to main content

sql_orm/
dbset_query.rs

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