1extern 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
77pub trait MigrationModelSource {
82 fn entity_metadata() -> &'static [&'static EntityMetadata];
84}
85
86pub trait AuditEntity: core::Entity {
93 fn audit_policy() -> Option<core::EntityPolicyMetadata>;
95}
96
97pub trait SoftDeleteEntity: core::Entity {
103 fn soft_delete_policy() -> Option<core::EntityPolicyMetadata>;
105}
106
107pub trait TenantContext: core::EntityPolicy {
113 const COLUMN_NAME: &'static str;
115
116 fn tenant_value(&self) -> core::SqlValue;
118}
119
120pub trait TenantScopedEntity: core::Entity {
125 fn tenant_policy() -> Option<core::EntityPolicyMetadata>;
127}
128
129pub trait IncludeNavigation<T>: core::Entity {
136 fn set_included_navigation(
138 &mut self,
139 navigation: &str,
140 value: Option<T>,
141 ) -> Result<(), core::OrmError>;
142}
143
144pub trait IncludeCollection<T>: core::Entity {
151 fn set_included_collection(
153 &mut self,
154 navigation: &str,
155 values: Vec<T>,
156 ) -> Result<(), core::OrmError>;
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Navigation<T> {
166 value: Option<T>,
167}
168
169impl<T> Navigation<T> {
170 pub const fn empty() -> Self {
172 Self { value: None }
173 }
174
175 pub fn loaded(value: T) -> Self {
177 Self { value: Some(value) }
178 }
179
180 pub fn from_option(value: Option<T>) -> Self {
182 Self { value }
183 }
184
185 pub fn as_ref(&self) -> Option<&T> {
187 self.value.as_ref()
188 }
189
190 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#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct LazyNavigation<T> {
209 value: Option<T>,
210 loaded: bool,
211}
212
213impl<T> LazyNavigation<T> {
214 pub const fn unloaded() -> Self {
216 Self {
217 value: None,
218 loaded: false,
219 }
220 }
221
222 pub fn loaded(value: T) -> Self {
224 Self {
225 value: Some(value),
226 loaded: true,
227 }
228 }
229
230 pub fn from_option(value: Option<T>) -> Self {
232 Self {
233 value,
234 loaded: true,
235 }
236 }
237
238 pub fn is_loaded(&self) -> bool {
240 self.loaded
241 }
242
243 pub fn as_ref(&self) -> Option<&T> {
247 self.value.as_ref()
248 }
249
250 pub fn set_loaded(&mut self, value: Option<T>) {
252 self.value = value;
253 self.loaded = true;
254 }
255
256 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#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct Collection<T> {
275 values: Vec<T>,
276}
277
278impl<T> Collection<T> {
279 pub const fn empty() -> Self {
281 Self { values: Vec::new() }
282 }
283
284 pub fn from_vec(values: Vec<T>) -> Self {
286 Self { values }
287 }
288
289 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#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct LazyCollection<T> {
308 values: Vec<T>,
309 loaded: bool,
310}
311
312impl<T> LazyCollection<T> {
313 pub const fn unloaded() -> Self {
315 Self {
316 values: Vec::new(),
317 loaded: false,
318 }
319 }
320
321 pub fn from_vec(values: Vec<T>) -> Self {
323 Self {
324 values,
325 loaded: true,
326 }
327 }
328
329 pub fn is_loaded(&self) -> bool {
331 self.loaded
332 }
333
334 pub fn as_slice(&self) -> &[T] {
338 &self.values
339 }
340
341 pub fn set_loaded(&mut self, values: Vec<T>) {
343 self.values = values;
344 self.loaded = true;
345 }
346
347 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
360pub fn model_snapshot_from_source<S: MigrationModelSource>() -> migrate::ModelSnapshot {
364 migrate::ModelSnapshot::from_entities(S::entity_metadata())
365}
366
367pub 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}