Skip to main content

sql_orm/
lib.rs

1//! Public API surface for the SQL Server code-first ORM.
2//!
3//! Most applications should import [`prelude`] and define entities with the
4//! derive macros re-exported there. The crate root also exposes advanced
5//! modules for users who need direct access to metadata, query ASTs,
6//! migrations, SQL Server compilation, or the Tiberius adapter.
7
8extern crate self as sql_orm;
9
10mod active_record;
11mod audit_runtime;
12mod context;
13mod dbset_query;
14mod page_request;
15mod predicate_composition;
16mod query_alias;
17mod query_order;
18mod query_predicates;
19mod query_projection;
20mod raw_sql;
21mod soft_delete_runtime;
22mod tracking;
23
24pub use sql_orm_core as core;
25pub use sql_orm_macros as macros;
26pub use sql_orm_migrate as migrate;
27pub use sql_orm_query as query;
28pub use sql_orm_sqlserver as sqlserver;
29pub use sql_orm_tiberius as tiberius;
30pub use tokio;
31
32pub use active_record::{ActiveRecord, EntityPersist, EntityPersistMode, EntityPrimaryKey};
33pub use audit_runtime::{
34    AuditContext, AuditOperation, AuditProvider, AuditRequestValues, AuditValues,
35    resolve_audit_values,
36};
37#[cfg(feature = "pool-bb8")]
38pub use context::connect_shared_from_pool;
39pub use context::{
40    ActiveTenant, DbContext, DbContextEntitySet, DbSet, SharedConnection, connect_shared,
41    connect_shared_with_config, connect_shared_with_options,
42};
43pub use dbset_query::{
44    AggregateProjections, CollectionIncludeStrategy, DbSetGroupedQuery, DbSetQuery,
45    DbSetQueryIncludeMany, DbSetQueryIncludeOne, GroupByExpressions,
46};
47pub use page_request::PageRequest;
48pub use predicate_composition::PredicateCompositionExt;
49pub use query_alias::{AliasedEntityColumn, EntityColumnAliasExt};
50pub use query_order::EntityColumnOrderExt;
51pub use query_predicates::EntityColumnPredicateExt;
52pub use query_projection::SelectProjections;
53pub use raw_sql::{QueryHint, RawCommand, RawParam, RawParams, RawQuery};
54pub use soft_delete_runtime::{
55    SoftDeleteContext, SoftDeleteOperation, SoftDeleteProvider, SoftDeleteRequestValues,
56    SoftDeleteValues,
57};
58pub use sql_orm_core::{EntityMetadata, NavigationKind, NavigationMetadata};
59pub use sql_orm_query::{AggregateExpr, AggregateOrderBy, AggregatePredicate, AggregateProjection};
60pub use sql_orm_tiberius::{
61    MssqlConnectionConfig, MssqlHealthCheckOptions, MssqlHealthCheckQuery, MssqlOperationalOptions,
62    MssqlParameterLogMode, MssqlPoolBackend, MssqlPoolOptions, MssqlRetryOptions,
63    MssqlSlowQueryOptions, MssqlTimeoutOptions, MssqlTracingOptions,
64};
65#[cfg(feature = "pool-bb8")]
66pub use sql_orm_tiberius::{MssqlPool, MssqlPoolBuilder, MssqlPooledConnection};
67pub use tracking::{EntityState, Tracked};
68#[doc(hidden)]
69pub use tracking::{
70    SaveChangesOperationPlan, TrackedEntityRegistration, TrackingRegistry, TrackingRegistryHandle,
71    save_changes_operation_plan,
72};
73
74/// Provides entity metadata for code-first migration snapshot generation.
75///
76/// `#[derive(DbContext)]` implements this trait for application contexts by
77/// returning the metadata for every `DbSet<T>` declared on the context.
78pub trait MigrationModelSource {
79    /// Returns the static metadata for all entities owned by the context.
80    fn entity_metadata() -> &'static [&'static EntityMetadata];
81}
82
83/// Runtime metadata hook for entities that declare `#[orm(audit = Audit)]`.
84///
85/// The derive macro implements this for every entity. Entities without audit
86/// policy return `None`; audited entities return the audit-owned columns as an
87/// `EntityPolicyMetadata` view without changing the normal entity metadata
88/// shape used by snapshots, diffs, and DDL.
89pub trait AuditEntity: core::Entity {
90    /// Returns audit-owned columns for this entity when audit is enabled.
91    fn audit_policy() -> Option<core::EntityPolicyMetadata>;
92}
93
94/// Runtime metadata hook for entities that declare
95/// `#[orm(soft_delete = SoftDelete)]`.
96///
97/// The public delete/read behavior lives in the `sql-orm` crate. Lower
98/// layers still see ordinary columns and ordinary query/update AST nodes.
99pub trait SoftDeleteEntity: core::Entity {
100    /// Returns soft-delete-owned columns for this entity when enabled.
101    fn soft_delete_policy() -> Option<core::EntityPolicyMetadata>;
102}
103
104/// Runtime value shape for the active tenant configured on a context.
105///
106/// `#[derive(TenantContext)]` implements this trait for user-defined structs
107/// with exactly one field. The field defines both the tenant column name and
108/// the SQL value used by tenant-scoped reads and writes.
109pub trait TenantContext: core::EntityPolicy {
110    /// Physical column name used by tenant-scoped entities.
111    const COLUMN_NAME: &'static str;
112
113    /// Converts the active tenant value into the SQL value compared in queries.
114    fn tenant_value(&self) -> core::SqlValue;
115}
116
117/// Runtime metadata hook for entities that opt into tenant scoping.
118///
119/// Entities without `#[orm(tenant = CurrentTenant)]` return `None` and remain
120/// cross-tenant even when a context has an active tenant configured.
121pub trait TenantScopedEntity: core::Entity {
122    /// Returns tenant-owned column metadata for tenant-scoped entities.
123    fn tenant_policy() -> Option<core::EntityPolicyMetadata>;
124}
125
126/// Contract generated for entities that can receive an included single
127/// navigation value.
128///
129/// This is used by `DbSetQuery::include::<T>(...)` for `belongs_to` and
130/// `has_one` navigations. Collection includes use a different loading strategy
131/// and are intentionally not part of this contract.
132pub trait IncludeNavigation<T>: core::Entity {
133    /// Attaches a loaded navigation value to the field named by `navigation`.
134    fn set_included_navigation(
135        &mut self,
136        navigation: &str,
137        value: Option<T>,
138    ) -> Result<(), core::OrmError>;
139}
140
141/// Contract generated for entities that can receive an included collection
142/// navigation value.
143///
144/// This is used by `DbSetQuery::include_many::<T>(...)` for `has_many`
145/// navigations. The query layer groups duplicate root rows before assigning
146/// the loaded collection.
147pub trait IncludeCollection<T>: core::Entity {
148    /// Attaches loaded collection values to the field named by `navigation`.
149    fn set_included_collection(
150        &mut self,
151        navigation: &str,
152        values: Vec<T>,
153    ) -> Result<(), core::OrmError>;
154}
155
156/// Marker value for a single related entity navigation.
157///
158/// Navigation fields are not persisted as columns. They exist so
159/// `#[derive(Entity)]` can attach navigation metadata to the entity while
160/// future loading APIs decide explicitly when related rows are fetched.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Navigation<T> {
163    value: Option<T>,
164}
165
166impl<T> Navigation<T> {
167    /// Creates an empty navigation value.
168    pub const fn empty() -> Self {
169        Self { value: None }
170    }
171
172    /// Creates a navigation value containing a loaded related entity.
173    pub fn loaded(value: T) -> Self {
174        Self { value: Some(value) }
175    }
176
177    /// Creates a navigation value from an optional related entity.
178    pub fn from_option(value: Option<T>) -> Self {
179        Self { value }
180    }
181
182    /// Returns the loaded related entity when one has been attached.
183    pub fn as_ref(&self) -> Option<&T> {
184        self.value.as_ref()
185    }
186
187    /// Replaces the loaded related entity.
188    pub fn set(&mut self, value: Option<T>) {
189        self.value = value;
190    }
191}
192
193impl<T> Default for Navigation<T> {
194    fn default() -> Self {
195        Self::empty()
196    }
197}
198
199/// Opt-in lazy single navigation wrapper.
200///
201/// This type never performs I/O by itself. It only records whether a related
202/// value has been explicitly loaded by an ORM operation such as `include(...)`
203/// or a future explicit lazy-loading API that receives a context-bearing value.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct LazyNavigation<T> {
206    value: Option<T>,
207    loaded: bool,
208}
209
210impl<T> LazyNavigation<T> {
211    /// Creates an unloaded lazy navigation.
212    pub const fn unloaded() -> Self {
213        Self {
214            value: None,
215            loaded: false,
216        }
217    }
218
219    /// Creates a loaded lazy navigation containing a related entity.
220    pub fn loaded(value: T) -> Self {
221        Self {
222            value: Some(value),
223            loaded: true,
224        }
225    }
226
227    /// Creates a loaded lazy navigation from an optional related entity.
228    pub fn from_option(value: Option<T>) -> Self {
229        Self {
230            value,
231            loaded: true,
232        }
233    }
234
235    /// Returns whether a load operation has populated this wrapper.
236    pub fn is_loaded(&self) -> bool {
237        self.loaded
238    }
239
240    /// Returns the loaded related entity when one is present.
241    ///
242    /// This is a memory-only accessor. It never executes SQL.
243    pub fn as_ref(&self) -> Option<&T> {
244        self.value.as_ref()
245    }
246
247    /// Replaces the loaded value and marks this wrapper as loaded.
248    pub fn set_loaded(&mut self, value: Option<T>) {
249        self.value = value;
250        self.loaded = true;
251    }
252
253    /// Clears the cached value and marks this wrapper as unloaded.
254    pub fn clear(&mut self) {
255        self.value = None;
256        self.loaded = false;
257    }
258}
259
260impl<T> Default for LazyNavigation<T> {
261    fn default() -> Self {
262        Self::unloaded()
263    }
264}
265
266/// Marker value for a collection navigation.
267///
268/// Collection navigation fields are ignored by column metadata and start empty
269/// when an entity is materialized without an explicit include/load operation.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct Collection<T> {
272    values: Vec<T>,
273}
274
275impl<T> Collection<T> {
276    /// Creates an empty collection navigation.
277    pub const fn empty() -> Self {
278        Self { values: Vec::new() }
279    }
280
281    /// Creates a loaded collection navigation from existing values.
282    pub fn from_vec(values: Vec<T>) -> Self {
283        Self { values }
284    }
285
286    /// Returns the loaded related entities.
287    pub fn as_slice(&self) -> &[T] {
288        &self.values
289    }
290}
291
292impl<T> Default for Collection<T> {
293    fn default() -> Self {
294        Self { values: Vec::new() }
295    }
296}
297
298/// Opt-in lazy collection navigation wrapper.
299///
300/// This type stores loaded values and load state, but it never owns a database
301/// context and never performs I/O from accessors, formatting, cloning or
302/// comparison. Loading must happen through an explicit ORM method.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct LazyCollection<T> {
305    values: Vec<T>,
306    loaded: bool,
307}
308
309impl<T> LazyCollection<T> {
310    /// Creates an unloaded lazy collection.
311    pub const fn unloaded() -> Self {
312        Self {
313            values: Vec::new(),
314            loaded: false,
315        }
316    }
317
318    /// Creates a loaded lazy collection from existing values.
319    pub fn from_vec(values: Vec<T>) -> Self {
320        Self {
321            values,
322            loaded: true,
323        }
324    }
325
326    /// Returns whether a load operation has populated this wrapper.
327    pub fn is_loaded(&self) -> bool {
328        self.loaded
329    }
330
331    /// Returns loaded related entities.
332    ///
333    /// This is a memory-only accessor. It never executes SQL.
334    pub fn as_slice(&self) -> &[T] {
335        &self.values
336    }
337
338    /// Replaces the loaded values and marks this wrapper as loaded.
339    pub fn set_loaded(&mut self, values: Vec<T>) {
340        self.values = values;
341        self.loaded = true;
342    }
343
344    /// Clears the cached values and marks this wrapper as unloaded.
345    pub fn clear(&mut self) {
346        self.values.clear();
347        self.loaded = false;
348    }
349}
350
351impl<T> Default for LazyCollection<T> {
352    fn default() -> Self {
353        Self::unloaded()
354    }
355}
356
357/// Builds a model snapshot from a context type that exposes entity metadata.
358///
359/// This is the helper used by consumer snapshot-export binaries.
360pub fn model_snapshot_from_source<S: MigrationModelSource>() -> migrate::ModelSnapshot {
361    migrate::ModelSnapshot::from_entities(S::entity_metadata())
362}
363
364/// Serializes the current model snapshot for a context as pretty JSON.
365///
366/// Consumer projects can print this from a small binary and pass it to the CLI
367/// through `migration add --snapshot-bin`.
368pub fn model_snapshot_json_from_source<S: MigrationModelSource>() -> Result<String, core::OrmError>
369{
370    model_snapshot_from_source::<S>().to_json_pretty()
371}
372
373pub mod prelude {
374    pub use crate::AliasedEntityColumn;
375    #[cfg(feature = "pool-bb8")]
376    pub use crate::connect_shared_from_pool;
377    pub use crate::{
378        ActiveRecord, ActiveTenant, AggregateProjections, AuditEntity, Collection,
379        CollectionIncludeStrategy, DbContext, DbContextEntitySet, DbSet, DbSetGroupedQuery,
380        DbSetQuery, DbSetQueryIncludeMany, DbSetQueryIncludeOne, EntityColumnAliasExt,
381        EntityColumnOrderExt, EntityColumnPredicateExt, EntityState, GroupByExpressions,
382        IncludeCollection, IncludeNavigation, LazyCollection, LazyNavigation, MigrationModelSource,
383        MssqlConnectionConfig, MssqlHealthCheckOptions, MssqlHealthCheckQuery,
384        MssqlOperationalOptions, MssqlParameterLogMode, MssqlPoolBackend, MssqlPoolOptions,
385        MssqlRetryOptions, MssqlSlowQueryOptions, MssqlTimeoutOptions, MssqlTracingOptions,
386        Navigation, PageRequest, PredicateCompositionExt, QueryHint, RawCommand, RawParam,
387        RawParams, RawQuery, SelectProjections, SharedConnection, SoftDeleteContext,
388        SoftDeleteEntity, SoftDeleteOperation, SoftDeleteProvider, SoftDeleteRequestValues,
389        SoftDeleteValues, TenantContext, TenantScopedEntity, Tracked, model_snapshot_from_source,
390        model_snapshot_json_from_source,
391    };
392    pub use crate::{
393        AuditContext, AuditOperation, AuditProvider, AuditRequestValues, AuditValues,
394        resolve_audit_values,
395    };
396    #[cfg(feature = "pool-bb8")]
397    pub use crate::{MssqlPool, MssqlPoolBuilder, MssqlPooledConnection};
398    pub use sql_orm_core::{
399        Changeset, ColumnMetadata, ColumnValue, Entity, EntityColumn, EntityMetadata, EntityPolicy,
400        EntityPolicyMetadata, ForeignKeyMetadata, FromRow, IdentityMetadata, IndexColumnMetadata,
401        IndexMetadata, Insertable, NavigationKind, NavigationMetadata, OrmError,
402        PrimaryKeyMetadata, ReferentialAction, Row, SqlServerType, SqlTypeMapping, SqlValue,
403    };
404    pub use sql_orm_macros::{
405        AuditFields, Changeset, DbContext, Entity, FromRow, Insertable, SoftDeleteFields,
406        TenantContext,
407    };
408    pub use sql_orm_query::{
409        AggregateExpr, AggregateOrderBy, AggregatePredicate, AggregateProjection, Join, JoinType,
410        SelectProjection,
411    };
412}
413
414#[cfg(test)]
415mod tests {
416    use super::prelude::{
417        ActiveRecord, ActiveTenant, AuditContext, AuditEntity, AuditFields, AuditOperation,
418        AuditProvider, AuditRequestValues, AuditValues, Changeset, ColumnValue, DbContext,
419        DbContextEntitySet, DbSet, Entity, EntityColumn, EntityColumnOrderExt,
420        EntityColumnPredicateExt, EntityMetadata, EntityPolicy, EntityPolicyMetadata, EntityState,
421        IdentityMetadata, Insertable, LazyCollection, LazyNavigation, MssqlConnectionConfig,
422        MssqlOperationalOptions, MssqlPoolBackend, MssqlPoolOptions, MssqlRetryOptions,
423        MssqlTimeoutOptions, NavigationKind, NavigationMetadata, OrmError, PageRequest,
424        PredicateCompositionExt, PrimaryKeyMetadata, QueryHint, RawCommand, RawParam, RawParams,
425        RawQuery, SelectProjection, SelectProjections, SharedConnection, SoftDeleteEntity,
426        SoftDeleteFields, SqlServerType, SqlTypeMapping, SqlValue, TenantContext,
427        TenantScopedEntity, Tracked,
428    };
429    use sql_orm_query::{Expr, OrderBy, Predicate, SortDirection, TableRef};
430    use std::time::Duration;
431
432    struct PublicEntity;
433
434    static PUBLIC_ENTITY_METADATA: EntityMetadata = EntityMetadata {
435        rust_name: "PublicEntity",
436        schema: "dbo",
437        table: "public_entities",
438        renamed_from: None,
439        columns: &[],
440        primary_key: PrimaryKeyMetadata {
441            name: None,
442            columns: &[],
443        },
444        indexes: &[],
445        foreign_keys: &[],
446        navigations: &[],
447    };
448
449    impl Entity for PublicEntity {
450        fn metadata() -> &'static EntityMetadata {
451            &PUBLIC_ENTITY_METADATA
452        }
453    }
454
455    struct PublicPolicy;
456
457    impl EntityPolicy for PublicPolicy {
458        const POLICY_NAME: &'static str = "public_policy";
459        const COLUMN_NAMES: &'static [&'static str] = &[];
460
461        fn columns() -> &'static [super::core::ColumnMetadata] {
462            &[]
463        }
464    }
465
466    #[allow(dead_code)]
467    #[derive(SoftDeleteFields)]
468    struct PublicSoftDelete {
469        #[orm(sql_type = "datetime2")]
470        deleted_at: Option<String>,
471
472        #[orm(nullable)]
473        #[orm(length = 120)]
474        deleted_by: Option<String>,
475    }
476
477    #[allow(dead_code)]
478    #[derive(AuditFields)]
479    struct PublicAudit {
480        #[orm(created_at)]
481        #[orm(default_sql = "SYSUTCDATETIME()")]
482        #[orm(sql_type = "datetime2")]
483        #[orm(updatable = false)]
484        created_at: String,
485
486        #[orm(created_by)]
487        #[orm(column = "created_by_user_id")]
488        created_by: Option<i64>,
489
490        #[orm(updated_by)]
491        #[orm(nullable)]
492        #[orm(length = 120)]
493        updated_by: Option<String>,
494    }
495
496    #[allow(dead_code)]
497    #[derive(TenantContext)]
498    struct PublicTenant {
499        #[orm(column = "company_id")]
500        tenant_id: i64,
501    }
502
503    #[test]
504    fn exposes_public_prelude() {
505        let error = OrmError::new("public-api");
506        let raw_query_type = core::any::type_name::<RawQuery<PublicEntity>>();
507        let raw_command_type = core::any::type_name::<RawCommand>();
508        let projection_type = core::any::type_name::<SelectProjection>();
509        let query_hint = QueryHint::Recompile;
510        fn assert_raw_param<T: RawParam>() {}
511        fn assert_raw_params<T: RawParams>() {}
512        fn assert_select_projections<T: SelectProjections>() {}
513
514        assert!(raw_query_type.contains("RawQuery"));
515        assert!(raw_command_type.contains("RawCommand"));
516        assert!(projection_type.contains("SelectProjection"));
517        assert_raw_param::<i64>();
518        assert_raw_param::<SqlValue>();
519        assert_raw_params::<(bool, i64)>();
520        assert_select_projections::<(EntityColumn<PublicEntity>,)>();
521        assert_eq!(query_hint, QueryHint::Recompile);
522        assert_eq!(error.message(), "public-api");
523        assert_eq!(
524            ColumnValue::new("email", SqlValue::String("ana@example.com".to_string())),
525            ColumnValue {
526                column_name: "email",
527                value: SqlValue::String("ana@example.com".to_string()),
528            }
529        );
530        assert_eq!(String::SQL_SERVER_TYPE, SqlServerType::NVarChar);
531        assert_eq!(PageRequest::new(2, 25).page, 2);
532    }
533
534    #[test]
535    fn exposes_entity_contract_in_prelude() {
536        assert_eq!(PublicEntity::metadata().table, "public_entities");
537    }
538
539    #[test]
540    fn exposes_navigation_metadata_contract_in_prelude() {
541        let navigation = NavigationMetadata::new(
542            "owner",
543            NavigationKind::BelongsTo,
544            "User",
545            "auth",
546            "users",
547            &["owner_id"],
548            &["id"],
549            Some("fk_posts_owner_id_users"),
550        );
551
552        assert_eq!(navigation.rust_field, "owner");
553        assert_eq!(navigation.kind, NavigationKind::BelongsTo);
554        assert!(navigation.targets_table("auth", "users"));
555        assert!(navigation.uses_foreign_key("fk_posts_owner_id_users"));
556    }
557
558    #[test]
559    fn lazy_navigation_wrappers_are_memory_only_state_containers() {
560        let mut owner = LazyNavigation::unloaded();
561        assert!(!owner.is_loaded());
562        assert_eq!(owner.as_ref(), None);
563
564        owner.set_loaded(Some(7_i64));
565        assert!(owner.is_loaded());
566        assert_eq!(owner.as_ref(), Some(&7_i64));
567
568        let cloned = owner.clone();
569        assert_eq!(
570            format!("{:?}", cloned),
571            "LazyNavigation { value: Some(7), loaded: true }"
572        );
573
574        owner.clear();
575        assert!(!owner.is_loaded());
576        assert_eq!(owner.as_ref(), None);
577
578        let mut children = LazyCollection::unloaded();
579        assert!(!children.is_loaded());
580        assert!(children.as_slice().is_empty());
581
582        children.set_loaded(vec![1_i64, 2_i64]);
583        assert!(children.is_loaded());
584        assert_eq!(children.as_slice(), &[1_i64, 2_i64]);
585
586        let cloned = children.clone();
587        assert_eq!(
588            format!("{:?}", cloned),
589            "LazyCollection { values: [1, 2], loaded: true }"
590        );
591
592        children.clear();
593        assert!(!children.is_loaded());
594        assert!(children.as_slice().is_empty());
595    }
596
597    #[test]
598    fn exposes_entity_policy_contract_in_prelude() {
599        assert_eq!(
600            PublicPolicy::metadata(),
601            EntityPolicyMetadata::new("public_policy", &[])
602        );
603    }
604
605    #[test]
606    fn exposes_audit_entity_contract_in_prelude() {
607        struct PublicAuditEntity;
608
609        impl Entity for PublicAuditEntity {
610            fn metadata() -> &'static EntityMetadata {
611                &PUBLIC_ENTITY_METADATA
612            }
613        }
614
615        impl AuditEntity for PublicAuditEntity {
616            fn audit_policy() -> Option<EntityPolicyMetadata> {
617                Some(EntityPolicyMetadata::new("audit", &[]))
618            }
619        }
620
621        assert_eq!(
622            PublicAuditEntity::audit_policy(),
623            Some(EntityPolicyMetadata::new("audit", &[]))
624        );
625    }
626
627    #[test]
628    fn exposes_soft_delete_contract_in_prelude() {
629        struct PublicSoftDeleteEntity;
630
631        impl Entity for PublicSoftDeleteEntity {
632            fn metadata() -> &'static EntityMetadata {
633                &PUBLIC_ENTITY_METADATA
634            }
635        }
636
637        impl SoftDeleteEntity for PublicSoftDeleteEntity {
638            fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
639                Some(EntityPolicyMetadata::new("soft_delete", &[]))
640            }
641        }
642
643        assert_eq!(
644            PublicSoftDeleteEntity::soft_delete_policy(),
645            Some(EntityPolicyMetadata::new("soft_delete", &[]))
646        );
647    }
648
649    #[test]
650    fn exposes_tenant_contract_in_prelude() {
651        struct PublicTenantEntity;
652
653        impl Entity for PublicTenantEntity {
654            fn metadata() -> &'static EntityMetadata {
655                &PUBLIC_ENTITY_METADATA
656            }
657        }
658
659        impl TenantScopedEntity for PublicTenantEntity {
660            fn tenant_policy() -> Option<EntityPolicyMetadata> {
661                Some(EntityPolicyMetadata::new("tenant", &[]))
662            }
663        }
664
665        assert_eq!(
666            PublicTenantEntity::tenant_policy(),
667            Some(EntityPolicyMetadata::new("tenant", &[]))
668        );
669    }
670
671    #[test]
672    fn exposes_audit_runtime_contract_in_prelude() {
673        struct PublicAuditProvider;
674
675        impl AuditProvider for PublicAuditProvider {
676            fn values(&self, context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
677                assert_eq!(context.operation, AuditOperation::Update);
678                assert!(context.request_values.is_some());
679
680                Ok(vec![ColumnValue::new(
681                    "updated_at",
682                    SqlValue::String("provider-updated-at".to_string()),
683                )])
684            }
685        }
686
687        let request_values = AuditRequestValues::new(vec![ColumnValue::new(
688            "updated_by",
689            SqlValue::String("request-updated-by".to_string()),
690        )]);
691        let context = AuditContext {
692            entity: PublicEntity::metadata(),
693            operation: AuditOperation::Update,
694            request_values: Some(&request_values),
695        };
696
697        let provider = PublicAuditProvider;
698        let values = provider.values(context).unwrap();
699
700        assert_eq!(request_values.values()[0].column_name, "updated_by");
701        assert_eq!(values[0].column_name, "updated_at");
702    }
703
704    #[test]
705    fn derives_audit_fields_policy_metadata_from_public_prelude() {
706        let metadata = PublicAudit::metadata();
707
708        assert_eq!(metadata.name, "audit");
709        assert_eq!(metadata.columns.len(), 3);
710        assert_eq!(metadata.columns[0].rust_field, "created_at");
711        assert_eq!(metadata.columns[0].column_name, "created_at");
712        assert_eq!(metadata.columns[0].sql_type, SqlServerType::DateTime2);
713        assert_eq!(metadata.columns[0].default_sql, Some("SYSUTCDATETIME()"));
714        assert!(metadata.columns[0].insertable);
715        assert!(!metadata.columns[0].updatable);
716        assert_eq!(metadata.columns[1].column_name, "created_by_user_id");
717        assert!(metadata.columns[1].nullable);
718        assert_eq!(metadata.columns[1].sql_type, SqlServerType::BigInt);
719        assert_eq!(metadata.columns[2].max_length, Some(120));
720        assert!(metadata.columns[2].updatable);
721        assert_eq!(
722            <PublicAudit as EntityPolicy>::COLUMN_NAMES,
723            &["created_at", "created_by_user_id", "updated_by"]
724        );
725
726        let audit_values = PublicAudit {
727            created_at: "2026-04-28T00:00:00Z".to_string(),
728            created_by: Some(7),
729            updated_by: None,
730        }
731        .audit_values();
732
733        assert_eq!(
734            audit_values,
735            vec![
736                ColumnValue::new(
737                    "created_at",
738                    SqlValue::String("2026-04-28T00:00:00Z".to_string())
739                ),
740                ColumnValue::new("created_by_user_id", SqlValue::I64(7)),
741                ColumnValue::new("updated_by", SqlValue::TypedNull(SqlServerType::NVarChar)),
742            ]
743        );
744    }
745
746    #[test]
747    fn derives_tenant_context_policy_metadata_from_public_prelude() {
748        let metadata = PublicTenant::metadata();
749        let tenant = PublicTenant { tenant_id: 42 };
750        let active_tenant = ActiveTenant::from_context(&tenant);
751
752        assert_eq!(metadata.name, "tenant");
753        assert_eq!(metadata.columns.len(), 1);
754        assert_eq!(metadata.columns[0].rust_field, "tenant_id");
755        assert_eq!(metadata.columns[0].column_name, "company_id");
756        assert_eq!(metadata.columns[0].sql_type, SqlServerType::BigInt);
757        assert!(metadata.columns[0].insertable);
758        assert!(!metadata.columns[0].updatable);
759        assert_eq!(
760            <PublicTenant as EntityPolicy>::COLUMN_NAMES,
761            &["company_id"]
762        );
763        assert_eq!(PublicTenant::COLUMN_NAME, "company_id");
764        assert_eq!(tenant.tenant_value(), SqlValue::I64(42));
765        assert_eq!(active_tenant.column_name, "company_id");
766        assert_eq!(active_tenant.value, SqlValue::I64(42));
767    }
768
769    #[test]
770    fn exposes_operational_configuration_surface() {
771        let options = MssqlOperationalOptions::new()
772            .with_timeouts(MssqlTimeoutOptions::new().with_query_timeout(Duration::from_secs(30)))
773            .with_retry(MssqlRetryOptions::enabled(
774                2,
775                Duration::from_millis(50),
776                Duration::from_secs(1),
777            ))
778            .with_pool(MssqlPoolOptions::bb8(12));
779        let config = MssqlConnectionConfig::from_connection_string_with_options(
780            "server=tcp:localhost,1433;database=master;user=sa;password=Password123;TrustServerCertificate=true",
781            options,
782        )
783        .unwrap();
784
785        assert_eq!(config.options().pool.backend, MssqlPoolBackend::Bb8);
786        assert_eq!(config.options().pool.max_size, 12);
787    }
788
789    #[cfg(feature = "pool-bb8")]
790    #[test]
791    fn exposes_pool_surface_when_feature_is_enabled() {
792        let builder = super::MssqlPool::builder().max_size(8);
793
794        assert_eq!(builder.options().max_size, 8);
795    }
796
797    #[cfg(feature = "pool-bb8")]
798    #[test]
799    fn exposes_dbcontext_pool_wiring_when_feature_is_enabled() {
800        let _from_pool = DerivedDbContext::from_pool;
801        let _shared_from_pool = super::connect_shared_from_pool;
802    }
803
804    #[test]
805    fn exposes_dbcontext_entity_set_contract_in_prelude() {
806        fn require_trait<C, E>()
807        where
808            C: DbContextEntitySet<E>,
809            E: Entity,
810        {
811        }
812
813        require_trait::<DerivedDbContext, DerivedUser>();
814    }
815
816    #[test]
817    fn exposes_dbcontext_health_check_contract_in_prelude() {
818        let _health_check = DerivedDbContext::health_check;
819        let _trait_health_check = <DerivedDbContext as DbContext>::health_check;
820    }
821
822    #[test]
823    fn exposes_dbcontext_soft_delete_runtime_helpers() {
824        let _with_soft_delete_provider = DerivedDbContext::with_soft_delete_provider;
825        let _with_soft_delete_request_values = DerivedDbContext::with_soft_delete_request_values;
826        let _with_soft_delete_values = DerivedDbContext::with_soft_delete_values::<SoftDelete>;
827        let _clear_soft_delete_request_values = DerivedDbContext::clear_soft_delete_request_values;
828        let _shared_with_soft_delete_values =
829            SharedConnection::with_soft_delete_values::<SoftDelete>;
830    }
831
832    #[test]
833    fn exposes_dbcontext_audit_runtime_helpers() {
834        let _with_audit_provider = DerivedDbContext::with_audit_provider;
835        let _with_audit_request_values = DerivedDbContext::with_audit_request_values;
836        let _clear_audit_request_values = DerivedDbContext::clear_audit_request_values;
837        let _shared_with_audit_provider = SharedConnection::with_audit_provider;
838        let _shared_with_audit_request_values = SharedConnection::with_audit_request_values;
839        let _shared_clear_audit_request_values = SharedConnection::clear_audit_request_values;
840    }
841
842    #[test]
843    fn exposes_dbcontext_tenant_runtime_helpers() {
844        let _with_tenant = DerivedDbContext::with_tenant::<PublicTenant>;
845        let _clear_tenant = DerivedDbContext::clear_tenant;
846        let _shared_with_tenant = SharedConnection::with_tenant::<PublicTenant>;
847        let _shared_clear_tenant = SharedConnection::clear_tenant;
848    }
849
850    #[test]
851    fn exposes_migration_model_source_contract_in_prelude() {
852        fn require_trait<C: super::MigrationModelSource>() {}
853
854        require_trait::<DerivedDbContext>();
855        assert_eq!(
856            <DerivedDbContext as super::MigrationModelSource>::entity_metadata()
857                .iter()
858                .map(|metadata| metadata.table)
859                .collect::<Vec<_>>(),
860            vec!["users", "audit_entries"]
861        );
862    }
863
864    #[test]
865    fn exposes_model_snapshot_export_helpers() {
866        let snapshot = super::model_snapshot_from_source::<DerivedDbContext>();
867        let json = super::model_snapshot_json_from_source::<DerivedDbContext>().unwrap();
868
869        assert_eq!(
870            snapshot
871                .schemas
872                .iter()
873                .flat_map(|schema| schema.tables.iter().map(|table| table.name.as_str()))
874                .collect::<Vec<_>>(),
875            vec!["users", "audit_entries"]
876        );
877        assert!(json.contains("\"name\": \"auth\""));
878        assert!(json.contains("\"name\": \"users\""));
879    }
880
881    #[test]
882    fn exposes_active_record_contract_in_prelude() {
883        fn require_trait<E: ActiveRecord>() {}
884
885        require_trait::<PublicEntity>();
886    }
887
888    #[test]
889    fn exposes_tracking_surface_in_prelude() {
890        let tracked = Tracked::from_loaded(String::from("tracked"));
891
892        assert_eq!(tracked.state(), EntityState::Unchanged);
893        assert_eq!(tracked.current(), "tracked");
894    }
895
896    #[allow(dead_code)]
897    #[derive(Entity, Debug, Clone)]
898    #[orm(table = "users", schema = "auth")]
899    #[orm(index(name = "ix_users_email_created_by", columns(email, created_by)))]
900    struct DerivedUser {
901        #[orm(primary_key)]
902        #[orm(identity)]
903        id: i64,
904
905        #[orm(length = 180)]
906        #[orm(unique)]
907        email: String,
908
909        #[orm(nullable)]
910        #[orm(index(name = "ix_users_display_name"))]
911        display_name: Option<String>,
912
913        #[orm(default_sql = "'system'")]
914        created_by: String,
915
916        #[orm(rowversion)]
917        version: Vec<u8>,
918    }
919
920    #[allow(dead_code)]
921    #[derive(Entity, Debug, Clone)]
922    struct AuditEntry {
923        id: i64,
924        payload: String,
925    }
926
927    #[allow(dead_code)]
928    #[derive(SoftDeleteFields)]
929    struct SoftDelete {
930        #[orm(deleted_at)]
931        deleted_at: Option<String>,
932    }
933
934    #[derive(Insertable, Debug, Clone)]
935    #[orm(entity = DerivedUser)]
936    struct NewDerivedUser {
937        email: String,
938        display_name: Option<String>,
939        #[orm(column = "created_by")]
940        author: String,
941    }
942
943    #[derive(Changeset, Debug, Clone)]
944    #[orm(entity = DerivedUser)]
945    struct UpdateDerivedUser {
946        email: Option<String>,
947        display_name: Option<Option<String>>,
948        #[orm(column = "created_by")]
949        author: Option<String>,
950    }
951
952    #[allow(dead_code)]
953    #[derive(DbContext, Debug, Clone)]
954    struct DerivedDbContext {
955        pub users: DbSet<DerivedUser>,
956        pub audit_entries: DbSet<AuditEntry>,
957    }
958
959    #[test]
960    fn derives_entity_metadata_from_struct_attributes() {
961        let metadata = DerivedUser::metadata();
962
963        assert_eq!(metadata.rust_name, "DerivedUser");
964        assert_eq!(metadata.schema, "auth");
965        assert_eq!(metadata.table, "users");
966        assert_eq!(metadata.primary_key.columns, &["id"]);
967        assert_eq!(metadata.indexes.len(), 3);
968
969        let id = metadata.field("id").expect("id column metadata");
970        assert_eq!(id.sql_type, SqlServerType::BigInt);
971        assert_eq!(id.identity, Some(IdentityMetadata::new(1, 1)));
972        assert!(!id.insertable);
973        assert!(!id.updatable);
974
975        let email = metadata.field("email").expect("email column metadata");
976        assert_eq!(email.sql_type, SqlServerType::NVarChar);
977        assert_eq!(email.max_length, Some(180));
978        assert!(!email.nullable);
979
980        let display_name = metadata
981            .field("display_name")
982            .expect("display_name column metadata");
983        assert!(display_name.nullable);
984        assert_eq!(display_name.max_length, Some(255));
985
986        let created_by = metadata
987            .field("created_by")
988            .expect("created_by column metadata");
989        assert_eq!(created_by.default_sql, Some("'system'"));
990
991        let version = metadata.field("version").expect("version column metadata");
992        assert_eq!(version.sql_type, SqlServerType::RowVersion);
993        assert!(version.rowversion);
994        assert!(!version.insertable);
995        assert!(!version.updatable);
996
997        assert_eq!(metadata.indexes[0].name, "ux_users_email");
998        assert!(metadata.indexes[0].unique);
999        assert_eq!(metadata.indexes[1].name, "ix_users_display_name");
1000        assert!(!metadata.indexes[1].unique);
1001        assert_eq!(metadata.indexes[2].name, "ix_users_email_created_by");
1002        assert_eq!(metadata.indexes[2].columns.len(), 2);
1003        assert_eq!(metadata.indexes[2].columns[0].column_name, "email");
1004        assert_eq!(metadata.indexes[2].columns[1].column_name, "created_by");
1005        assert!(!metadata.indexes[2].columns[0].descending);
1006        assert!(!metadata.indexes[2].columns[1].descending);
1007    }
1008
1009    #[test]
1010    fn derives_default_table_and_primary_key_convention() {
1011        let metadata = AuditEntry::metadata();
1012
1013        assert_eq!(metadata.schema, "dbo");
1014        assert_eq!(metadata.table, "audit_entries");
1015        assert_eq!(metadata.primary_key.columns, &["id"]);
1016
1017        let payload = metadata.field("payload").expect("payload column metadata");
1018        assert_eq!(payload.sql_type, SqlServerType::NVarChar);
1019        assert_eq!(payload.max_length, Some(255));
1020        assert!(payload.insertable);
1021        assert!(payload.updatable);
1022    }
1023
1024    #[test]
1025    fn exposes_static_columns_for_future_query_builder() {
1026        let email: EntityColumn<DerivedUser> = DerivedUser::email;
1027        let version = DerivedUser::version;
1028        let payload = AuditEntry::payload;
1029
1030        assert_eq!(email.rust_field(), "email");
1031        assert_eq!(email.column_name(), "email");
1032        assert_eq!(email.entity_metadata().table, "users");
1033        assert_eq!(email.metadata().max_length, Some(180));
1034
1035        assert_eq!(version.column_name(), "version");
1036        assert_eq!(version.metadata().sql_type, SqlServerType::RowVersion);
1037        assert!(!version.metadata().insertable);
1038
1039        assert_eq!(payload.entity_metadata().table, "audit_entries");
1040        assert_eq!(payload.metadata().column_name, "payload");
1041    }
1042
1043    #[test]
1044    fn exposes_public_column_predicate_extensions() {
1045        assert_eq!(
1046            DerivedUser::email.eq("ana@example.com".to_string()),
1047            Predicate::eq(
1048                Expr::from(DerivedUser::email),
1049                Expr::value(SqlValue::String("ana@example.com".to_string()))
1050            )
1051        );
1052        assert_eq!(
1053            DerivedUser::display_name.is_null(),
1054            Predicate::is_null(Expr::from(DerivedUser::display_name))
1055        );
1056        assert_eq!(
1057            DerivedUser::email.contains("@example.com"),
1058            Predicate::like(
1059                Expr::from(DerivedUser::email),
1060                Expr::value(SqlValue::String("%@example.com%".to_string()))
1061            )
1062        );
1063        assert_eq!(
1064            DerivedUser::email.asc(),
1065            OrderBy::new(TableRef::new("auth", "users"), "email", SortDirection::Asc)
1066        );
1067        assert_eq!(
1068            DerivedUser::email
1069                .contains("@example.com")
1070                .and(DerivedUser::display_name.is_not_null()),
1071            Predicate::and(vec![
1072                Predicate::like(
1073                    Expr::from(DerivedUser::email),
1074                    Expr::value(SqlValue::String("%@example.com%".to_string()))
1075                ),
1076                Predicate::is_not_null(Expr::from(DerivedUser::display_name))
1077            ])
1078        );
1079    }
1080
1081    #[test]
1082    fn derives_insertable_values_from_named_fields() {
1083        let insertable = NewDerivedUser {
1084            email: "ana@example.com".to_string(),
1085            display_name: None,
1086            author: "system".to_string(),
1087        };
1088
1089        let values = <NewDerivedUser as Insertable<DerivedUser>>::values(&insertable);
1090
1091        assert_eq!(
1092            values,
1093            vec![
1094                ColumnValue::new("email", SqlValue::String("ana@example.com".to_string())),
1095                ColumnValue::new("display_name", SqlValue::TypedNull(SqlServerType::NVarChar)),
1096                ColumnValue::new("created_by", SqlValue::String("system".to_string())),
1097            ]
1098        );
1099    }
1100
1101    #[test]
1102    fn derives_changeset_with_outer_option_semantics() {
1103        let changeset = UpdateDerivedUser {
1104            email: Some("ana.maria@example.com".to_string()),
1105            display_name: Some(None),
1106            author: None,
1107        };
1108
1109        let changes = <UpdateDerivedUser as Changeset<DerivedUser>>::changes(&changeset);
1110
1111        assert_eq!(
1112            changes,
1113            vec![
1114                ColumnValue::new(
1115                    "email",
1116                    SqlValue::String("ana.maria@example.com".to_string())
1117                ),
1118                ColumnValue::new("display_name", SqlValue::TypedNull(SqlServerType::NVarChar)),
1119            ]
1120        );
1121    }
1122}