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