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