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