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 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
72pub trait MigrationModelSource {
77 fn entity_metadata() -> &'static [&'static EntityMetadata];
79}
80
81pub trait AuditEntity: core::Entity {
88 fn audit_policy() -> Option<core::EntityPolicyMetadata>;
90}
91
92pub trait SoftDeleteEntity: core::Entity {
98 fn soft_delete_policy() -> Option<core::EntityPolicyMetadata>;
100}
101
102pub trait TenantContext: core::EntityPolicy {
108 const COLUMN_NAME: &'static str;
110
111 fn tenant_value(&self) -> core::SqlValue;
113}
114
115pub trait TenantScopedEntity: core::Entity {
120 fn tenant_policy() -> Option<core::EntityPolicyMetadata>;
122}
123
124pub trait IncludeNavigation<T>: core::Entity {
131 fn set_included_navigation(
133 &mut self,
134 navigation: &str,
135 value: Option<T>,
136 ) -> Result<(), core::OrmError>;
137}
138
139pub trait IncludeCollection<T>: core::Entity {
146 fn set_included_collection(
148 &mut self,
149 navigation: &str,
150 values: Vec<T>,
151 ) -> Result<(), core::OrmError>;
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct Navigation<T> {
161 value: Option<T>,
162}
163
164impl<T> Navigation<T> {
165 pub const fn empty() -> Self {
167 Self { value: None }
168 }
169
170 pub fn loaded(value: T) -> Self {
172 Self { value: Some(value) }
173 }
174
175 pub fn from_option(value: Option<T>) -> Self {
177 Self { value }
178 }
179
180 pub fn as_ref(&self) -> Option<&T> {
182 self.value.as_ref()
183 }
184
185 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#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct LazyNavigation<T> {
204 value: Option<T>,
205 loaded: bool,
206}
207
208impl<T> LazyNavigation<T> {
209 pub const fn unloaded() -> Self {
211 Self {
212 value: None,
213 loaded: false,
214 }
215 }
216
217 pub fn loaded(value: T) -> Self {
219 Self {
220 value: Some(value),
221 loaded: true,
222 }
223 }
224
225 pub fn from_option(value: Option<T>) -> Self {
227 Self {
228 value,
229 loaded: true,
230 }
231 }
232
233 pub fn is_loaded(&self) -> bool {
235 self.loaded
236 }
237
238 pub fn as_ref(&self) -> Option<&T> {
242 self.value.as_ref()
243 }
244
245 pub fn set_loaded(&mut self, value: Option<T>) {
247 self.value = value;
248 self.loaded = true;
249 }
250
251 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#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct Collection<T> {
270 values: Vec<T>,
271}
272
273impl<T> Collection<T> {
274 pub const fn empty() -> Self {
276 Self { values: Vec::new() }
277 }
278
279 pub fn from_vec(values: Vec<T>) -> Self {
281 Self { values }
282 }
283
284 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#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct LazyCollection<T> {
303 values: Vec<T>,
304 loaded: bool,
305}
306
307impl<T> LazyCollection<T> {
308 pub const fn unloaded() -> Self {
310 Self {
311 values: Vec::new(),
312 loaded: false,
313 }
314 }
315
316 pub fn from_vec(values: Vec<T>) -> Self {
318 Self {
319 values,
320 loaded: true,
321 }
322 }
323
324 pub fn is_loaded(&self) -> bool {
326 self.loaded
327 }
328
329 pub fn as_slice(&self) -> &[T] {
333 &self.values
334 }
335
336 pub fn set_loaded(&mut self, values: Vec<T>) {
338 self.values = values;
339 self.loaded = true;
340 }
341
342 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
355pub fn model_snapshot_from_source<S: MigrationModelSource>() -> migrate::ModelSnapshot {
359 migrate::ModelSnapshot::from_entities(S::entity_metadata())
360}
361
362pub 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}