1use crate::audit_runtime::apply_audit_values;
2use crate::dbset_query::{DbSetQuery, tenant_value_matches_column_type};
3use crate::soft_delete_runtime::{
4 SoftDeleteOperation, SoftDeleteProvider, SoftDeleteRequestValues, SoftDeleteValues,
5 apply_soft_delete_values,
6};
7use crate::{AuditEntity, AuditOperation, AuditProvider, AuditRequestValues, AuditValues};
8use crate::{
9 IncludeCollection, RawCommand, RawQuery, SoftDeleteEntity, TenantContext, TenantScopedEntity,
10 Tracked, TrackingRegistry, TrackingRegistryHandle,
11};
12use core::future::Future;
13use std::marker::PhantomData;
14use std::sync::{
15 Arc,
16 atomic::{AtomicUsize, Ordering},
17};
18
19use crate::{EntityPersist, EntityPrimaryKey};
20use sql_orm_core::{
21 Changeset, Entity, EntityMetadata, FromRow, Insertable, NavigationKind, OrmError,
22 SqlTypeMapping, SqlValue,
23};
24use sql_orm_query::{
25 ColumnRef, DeleteQuery, Expr, InsertQuery, Predicate, SelectQuery, TableRef, UpdateQuery,
26};
27use sql_orm_sqlserver::SqlServerCompiler;
28use sql_orm_tiberius::{
29 MssqlConnection, MssqlConnectionConfig, MssqlOperationalOptions, TokioConnectionStream,
30};
31#[cfg(feature = "pool-bb8")]
32use sql_orm_tiberius::{MssqlPool, MssqlPooledConnection};
33
34#[derive(Clone)]
42pub struct SharedConnection {
43 inner: Arc<SharedConnectionInner>,
44 runtime: Arc<SharedConnectionRuntime>,
45}
46
47#[derive(Debug, Clone, PartialEq)]
53pub struct ActiveTenant {
54 pub column_name: &'static str,
56 pub value: SqlValue,
58}
59
60impl ActiveTenant {
61 pub fn from_context<T: TenantContext>(tenant: &T) -> Self {
63 Self {
64 column_name: T::COLUMN_NAME,
65 value: tenant.tenant_value(),
66 }
67 }
68}
69
70enum SharedConnectionInner {
71 Direct(Box<tokio::sync::Mutex<MssqlConnection<TokioConnectionStream>>>),
72 #[cfg(feature = "pool-bb8")]
73 Pool(Box<MssqlPool>),
74}
75
76#[derive(Clone, Default)]
77struct SharedConnectionRuntime {
78 audit_provider: Option<Arc<dyn AuditProvider>>,
79 audit_request_values: Option<Arc<AuditRequestValues>>,
80 soft_delete_provider: Option<Arc<dyn SoftDeleteProvider>>,
81 soft_delete_request_values: Option<Arc<SoftDeleteRequestValues>>,
82 active_tenant: Option<ActiveTenant>,
83 transaction_depth: Arc<AtomicUsize>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87enum SharedConnectionKind {
88 Direct,
89 #[cfg(feature = "pool-bb8")]
90 Pool,
91}
92
93pub enum SharedConnectionGuard<'a> {
94 Direct(tokio::sync::MutexGuard<'a, MssqlConnection<TokioConnectionStream>>),
96 #[cfg(feature = "pool-bb8")]
97 Pool(Box<MssqlPooledConnection<'a>>),
99}
100
101impl SharedConnection {
102 pub fn from_connection(connection: MssqlConnection<TokioConnectionStream>) -> Self {
105 Self {
106 inner: Arc::new(SharedConnectionInner::Direct(Box::new(
107 tokio::sync::Mutex::new(connection),
108 ))),
109 runtime: Arc::new(SharedConnectionRuntime::default()),
110 }
111 }
112
113 #[cfg(feature = "pool-bb8")]
114 pub fn from_pool(pool: MssqlPool) -> Self {
119 Self {
120 inner: Arc::new(SharedConnectionInner::Pool(Box::new(pool))),
121 runtime: Arc::new(SharedConnectionRuntime::default()),
122 }
123 }
124
125 pub fn with_audit_provider(&self, provider: Arc<dyn AuditProvider>) -> Self {
131 Self {
132 inner: Arc::clone(&self.inner),
133 runtime: Arc::new(SharedConnectionRuntime {
134 audit_provider: Some(provider),
135 audit_request_values: self.runtime.audit_request_values.clone(),
136 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
137 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
138 active_tenant: self.runtime.active_tenant.clone(),
139 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
140 }),
141 }
142 }
143
144 pub fn with_audit_request_values(&self, request_values: AuditRequestValues) -> Self {
149 Self {
150 inner: Arc::clone(&self.inner),
151 runtime: Arc::new(SharedConnectionRuntime {
152 audit_provider: self.runtime.audit_provider.clone(),
153 audit_request_values: Some(Arc::new(request_values)),
154 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
155 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
156 active_tenant: self.runtime.active_tenant.clone(),
157 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
158 }),
159 }
160 }
161
162 pub fn with_audit_values<V: AuditValues>(&self, values: V) -> Self {
168 self.with_audit_request_values(AuditRequestValues::new(values.audit_values()))
169 }
170
171 pub fn clear_audit_request_values(&self) -> Self {
173 Self {
174 inner: Arc::clone(&self.inner),
175 runtime: Arc::new(SharedConnectionRuntime {
176 audit_provider: self.runtime.audit_provider.clone(),
177 audit_request_values: None,
178 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
179 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
180 active_tenant: self.runtime.active_tenant.clone(),
181 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
182 }),
183 }
184 }
185
186 pub fn with_soft_delete_provider(&self, provider: Arc<dyn SoftDeleteProvider>) -> Self {
191 Self {
192 inner: Arc::clone(&self.inner),
193 runtime: Arc::new(SharedConnectionRuntime {
194 audit_provider: self.runtime.audit_provider.clone(),
195 audit_request_values: self.runtime.audit_request_values.clone(),
196 soft_delete_provider: Some(provider),
197 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
198 active_tenant: self.runtime.active_tenant.clone(),
199 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
200 }),
201 }
202 }
203
204 pub fn with_soft_delete_request_values(&self, request_values: SoftDeleteRequestValues) -> Self {
210 Self {
211 inner: Arc::clone(&self.inner),
212 runtime: Arc::new(SharedConnectionRuntime {
213 audit_provider: self.runtime.audit_provider.clone(),
214 audit_request_values: self.runtime.audit_request_values.clone(),
215 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
216 soft_delete_request_values: Some(Arc::new(request_values)),
217 active_tenant: self.runtime.active_tenant.clone(),
218 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
219 }),
220 }
221 }
222
223 pub fn with_soft_delete_values<V: SoftDeleteValues>(&self, values: V) -> Self {
228 self.with_soft_delete_request_values(SoftDeleteRequestValues::new(
229 values.soft_delete_values(),
230 ))
231 }
232
233 pub fn clear_soft_delete_request_values(&self) -> Self {
235 Self {
236 inner: Arc::clone(&self.inner),
237 runtime: Arc::new(SharedConnectionRuntime {
238 audit_provider: self.runtime.audit_provider.clone(),
239 audit_request_values: self.runtime.audit_request_values.clone(),
240 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
241 soft_delete_request_values: None,
242 active_tenant: self.runtime.active_tenant.clone(),
243 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
244 }),
245 }
246 }
247
248 pub fn with_tenant<T: TenantContext>(&self, tenant: T) -> Self {
254 Self {
255 inner: Arc::clone(&self.inner),
256 runtime: Arc::new(SharedConnectionRuntime {
257 audit_provider: self.runtime.audit_provider.clone(),
258 audit_request_values: self.runtime.audit_request_values.clone(),
259 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
260 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
261 active_tenant: Some(ActiveTenant::from_context(&tenant)),
262 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
263 }),
264 }
265 }
266
267 pub fn clear_tenant(&self) -> Self {
269 Self {
270 inner: Arc::clone(&self.inner),
271 runtime: Arc::new(SharedConnectionRuntime {
272 audit_provider: self.runtime.audit_provider.clone(),
273 audit_request_values: self.runtime.audit_request_values.clone(),
274 soft_delete_provider: self.runtime.soft_delete_provider.clone(),
275 soft_delete_request_values: self.runtime.soft_delete_request_values.clone(),
276 active_tenant: None,
277 transaction_depth: Arc::clone(&self.runtime.transaction_depth),
278 }),
279 }
280 }
281
282 pub async fn lock(&self) -> Result<SharedConnectionGuard<'_>, OrmError> {
287 match self.inner.as_ref() {
288 SharedConnectionInner::Direct(connection) => {
289 Ok(SharedConnectionGuard::Direct(connection.lock().await))
290 }
291 #[cfg(feature = "pool-bb8")]
292 SharedConnectionInner::Pool(pool) => {
293 Ok(SharedConnectionGuard::Pool(Box::new(pool.acquire().await?)))
294 }
295 }
296 }
297
298 fn kind(&self) -> SharedConnectionKind {
299 match self.inner.as_ref() {
300 SharedConnectionInner::Direct(_) => SharedConnectionKind::Direct,
301 #[cfg(feature = "pool-bb8")]
302 SharedConnectionInner::Pool(_) => SharedConnectionKind::Pool,
303 }
304 }
305
306 #[doc(hidden)]
307 pub fn is_transaction_active(&self) -> bool {
308 self.runtime.transaction_depth.load(Ordering::SeqCst) > 0
309 }
310
311 fn enter_transaction_scope(&self) {
312 self.runtime
313 .transaction_depth
314 .fetch_add(1, Ordering::SeqCst);
315 }
316
317 fn exit_transaction_scope(&self) {
318 let _ = self.runtime.transaction_depth.fetch_update(
319 Ordering::SeqCst,
320 Ordering::SeqCst,
321 |depth| Some(depth.saturating_sub(1)),
322 );
323 }
324
325 #[allow(dead_code)]
326 pub(crate) fn audit_provider(&self) -> Option<Arc<dyn AuditProvider>> {
327 self.runtime.audit_provider.clone()
328 }
329
330 #[allow(dead_code)]
331 pub(crate) fn audit_request_values(&self) -> Option<Arc<AuditRequestValues>> {
332 self.runtime.audit_request_values.clone()
333 }
334
335 pub(crate) fn soft_delete_provider(&self) -> Option<Arc<dyn SoftDeleteProvider>> {
336 self.runtime.soft_delete_provider.clone()
337 }
338
339 pub(crate) fn soft_delete_request_values(&self) -> Option<Arc<SoftDeleteRequestValues>> {
340 self.runtime.soft_delete_request_values.clone()
341 }
342
343 #[doc(hidden)]
344 pub fn active_tenant(&self) -> Option<ActiveTenant> {
346 self.runtime.active_tenant.clone()
347 }
348}
349
350fn ensure_transactions_supported(kind: SharedConnectionKind) -> Result<(), OrmError> {
351 match kind {
352 SharedConnectionKind::Direct => Ok(()),
353 #[cfg(feature = "pool-bb8")]
354 SharedConnectionKind::Pool => Err(OrmError::new(
355 "db.transaction is not supported for pooled connections yet; create the DbContext from a direct connection until pooled transactions pin one physical SQL Server connection for the entire closure",
356 )),
357 }
358}
359
360impl core::ops::Deref for SharedConnectionGuard<'_> {
361 type Target = MssqlConnection<TokioConnectionStream>;
362
363 fn deref(&self) -> &Self::Target {
364 match self {
365 SharedConnectionGuard::Direct(connection) => connection,
366 #[cfg(feature = "pool-bb8")]
367 SharedConnectionGuard::Pool(connection) => connection,
368 }
369 }
370}
371
372impl core::ops::DerefMut for SharedConnectionGuard<'_> {
373 fn deref_mut(&mut self) -> &mut Self::Target {
374 match self {
375 SharedConnectionGuard::Direct(connection) => connection,
376 #[cfg(feature = "pool-bb8")]
377 SharedConnectionGuard::Pool(connection) => connection,
378 }
379 }
380}
381
382pub trait DbContext: Sized {
389 fn from_shared_connection(connection: SharedConnection) -> Self;
391 fn shared_connection(&self) -> SharedConnection;
393 #[doc(hidden)]
394 fn tracking_registry(&self) -> TrackingRegistryHandle;
395
396 fn clear_tracker(&self) {
402 self.tracking_registry().clear();
403 }
404
405 fn health_check(&self) -> impl Future<Output = Result<(), OrmError>> + Send {
408 let shared_connection = self.shared_connection();
409
410 async move {
411 let mut connection = shared_connection.lock().await?;
412 connection.health_check().await
413 }
414 }
415
416 fn raw<T>(&self, sql: impl Into<String>) -> RawQuery<T>
421 where
422 T: FromRow + Send,
423 {
424 RawQuery::new(self.shared_connection(), sql)
425 }
426
427 fn raw_exec(&self, sql: impl Into<String>) -> RawCommand {
429 RawCommand::new(self.shared_connection(), sql)
430 }
431
432 fn transaction<F, Fut, T>(
440 &self,
441 operation: F,
442 ) -> impl Future<Output = Result<T, OrmError>> + Send
443 where
444 F: FnOnce(Self) -> Fut + Send,
445 Fut: Future<Output = Result<T, OrmError>> + Send,
446 T: Send,
447 {
448 let shared_connection = self.shared_connection();
449 async move {
450 ensure_transactions_supported(shared_connection.kind())?;
451
452 {
453 let mut connection = shared_connection.lock().await?;
454 connection.begin_transaction_scope().await?;
455 }
456 shared_connection.enter_transaction_scope();
457
458 let transaction_context = Self::from_shared_connection(shared_connection.clone());
459 let result = operation(transaction_context).await;
460
461 match result {
462 Ok(value) => {
463 let mut connection = shared_connection.lock().await?;
464 let commit_result = connection.commit_transaction().await;
465 shared_connection.exit_transaction_scope();
466 commit_result?;
467 Ok(value)
468 }
469 Err(error) => {
470 let mut connection = shared_connection.lock().await?;
471 let rollback_result = connection.rollback_transaction().await;
472 shared_connection.exit_transaction_scope();
473 rollback_result?;
474 Err(error)
475 }
476 }
477 }
478 }
479}
480
481pub trait DbContextEntitySet<E: Entity>: DbContext {
485 fn db_set(&self) -> &DbSet<E>;
487}
488
489#[derive(Clone)]
496pub struct DbSet<E: Entity> {
497 connection: Option<SharedConnection>,
498 tracking_registry: TrackingRegistryHandle,
499 _entity: PhantomData<fn() -> E>,
500}
501
502impl<E: Entity> DbSet<E> {
503 pub fn new(connection: SharedConnection) -> Self {
505 Self::with_tracking_registry(connection, Arc::new(TrackingRegistry::default()))
506 }
507
508 #[doc(hidden)]
509 pub fn with_tracking_registry(
510 connection: SharedConnection,
511 tracking_registry: TrackingRegistryHandle,
512 ) -> Self {
513 Self {
514 connection: Some(connection),
515 tracking_registry,
516 _entity: PhantomData,
517 }
518 }
519
520 #[cfg(test)]
521 pub(crate) fn disconnected() -> Self {
522 Self {
523 connection: None,
524 tracking_registry: Arc::new(TrackingRegistry::default()),
525 _entity: PhantomData,
526 }
527 }
528
529 pub fn entity_metadata(&self) -> &'static EntityMetadata {
531 E::metadata()
532 }
533
534 pub fn query(&self) -> DbSetQuery<E> {
540 DbSetQuery::new(
541 self.connection.as_ref().cloned(),
542 SelectQuery::from_entity::<E>(),
543 )
544 }
545
546 pub fn query_with(&self, select_query: SelectQuery) -> DbSetQuery<E> {
552 DbSetQuery::new(self.connection.as_ref().cloned(), select_query)
553 }
554
555 fn query_with_internal_visibility(&self, select_query: SelectQuery) -> DbSetQuery<E> {
556 DbSetQuery::new(self.connection.as_ref().cloned(), select_query).with_deleted()
557 }
558
559 pub async fn find<K>(&self, key: K) -> Result<Option<E>, OrmError>
564 where
565 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
566 K: SqlTypeMapping,
567 {
568 self.query_with(self.find_select_query(key)?).first().await
569 }
570
571 pub async fn find_tracked<K>(&self, key: K) -> Result<Option<Tracked<E>>, OrmError>
582 where
583 E: Clone + FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
584 K: SqlTypeMapping,
585 {
586 self.ensure_tracking_primary_key_scope()?;
587
588 let key = key.to_sql_value();
589 let mut tracked = self
590 .query_with(self.find_select_query_sql_value(key.clone())?)
591 .first()
592 .await
593 .map(|entity| entity.map(Tracked::from_loaded))?;
594
595 if let Some(entity) = tracked.as_mut() {
596 entity.attach_registry_loaded(Arc::clone(&self.tracking_registry), key)?;
597 }
598
599 Ok(tracked)
600 }
601
602 pub fn add_tracked(&self, entity: E) -> Tracked<E>
613 where
614 E: Clone,
615 {
616 let mut tracked = Tracked::from_added(entity);
617 tracked.attach_registry_added(Arc::clone(&self.tracking_registry));
618 tracked
619 }
620
621 pub fn remove_tracked(&self, tracked: &mut Tracked<E>) {
630 let was_added = tracked.state() == crate::EntityState::Added;
631 tracked.mark_deleted();
632
633 if was_added {
636 tracked.detach_registry();
637 }
638 }
639
640 pub fn detach_tracked(&self, tracked: &mut Tracked<E>) {
646 tracked.detach_registry();
647 }
648
649 #[doc(hidden)]
650 pub async fn save_tracked_added(&self) -> Result<usize, OrmError>
651 where
652 E: AuditEntity
653 + Clone
654 + EntityPersist
655 + EntityPrimaryKey
656 + FromRow
657 + Send
658 + TenantScopedEntity,
659 {
660 let tracked_entities = self.tracking_registry.tracked_for::<E>();
661 let has_pending_added = tracked_entities
662 .iter()
663 .any(|tracked| tracked.state() == crate::EntityState::Added);
664 if !has_pending_added {
665 return Ok(0);
666 }
667
668 self.ensure_tracking_primary_key_scope()?;
669
670 let mut saved = 0;
671
672 for tracked in tracked_entities {
673 if tracked.state() != crate::EntityState::Added {
674 continue;
675 }
676
677 let current: E = tracked.current_clone();
678 let persisted = self.insert_entity(¤t).await?;
679 let persisted_key = persisted.primary_key_value()?;
680
681 tracked.sync_persisted(persisted);
682 self.tracking_registry
683 .update_persisted_identity::<E>(tracked.registration_id(), persisted_key)?;
684 saved += 1;
685 }
686
687 Ok(saved)
688 }
689
690 #[doc(hidden)]
691 pub async fn save_tracked_deleted(&self) -> Result<usize, OrmError>
692 where
693 E: Clone
694 + EntityPersist
695 + EntityPrimaryKey
696 + FromRow
697 + Send
698 + SoftDeleteEntity
699 + TenantScopedEntity,
700 {
701 let tracked_entities = self.tracking_registry.tracked_for::<E>();
702 let has_pending_deleted = tracked_entities
703 .iter()
704 .any(|tracked| tracked.state() == crate::EntityState::Deleted);
705 if !has_pending_deleted {
706 return Ok(0);
707 }
708
709 self.ensure_tracking_primary_key_scope()?;
710
711 let mut saved = 0;
712
713 for tracked in tracked_entities {
714 if tracked.state() != crate::EntityState::Deleted {
715 continue;
716 }
717
718 let current: E = tracked.current_clone();
719 let key = current.primary_key_value()?;
720 let deleted = self
721 .delete_tracked_by_sql_value(key, current.concurrency_token()?)
722 .await?;
723
724 if !deleted {
725 return Err(OrmError::new(
726 "save_changes could not delete a tracked entity for the current primary key",
727 ));
728 }
729
730 self.tracking_registry.unregister(tracked.registration_id());
731 saved += 1;
732 }
733
734 Ok(saved)
735 }
736
737 #[doc(hidden)]
738 pub async fn save_tracked_modified(&self) -> Result<usize, OrmError>
739 where
740 E: AuditEntity
741 + Clone
742 + EntityPersist
743 + EntityPrimaryKey
744 + FromRow
745 + Send
746 + SoftDeleteEntity
747 + TenantScopedEntity,
748 {
749 let tracked_entities = self.tracking_registry.tracked_for::<E>();
750 let has_pending_modified = tracked_entities
751 .iter()
752 .any(|tracked| tracked.state() == crate::EntityState::Modified);
753 if !has_pending_modified {
754 return Ok(0);
755 }
756
757 self.ensure_tracking_primary_key_scope()?;
758
759 let mut saved = 0;
760
761 for tracked in tracked_entities {
762 if tracked.state() != crate::EntityState::Modified {
763 continue;
764 }
765
766 if !tracked.has_persisted_changes() {
767 tracked.accept_current();
768 continue;
769 }
770
771 let current: E = tracked.current_clone();
772 let key = current.primary_key_value()?;
773 let persisted = self
774 .update_entity_by_sql_value(key, ¤t, current.concurrency_token()?)
775 .await?
776 .ok_or_else(|| {
777 OrmError::new(
778 "save_changes could not update a tracked entity for the current primary key",
779 )
780 })?;
781
782 tracked.sync_persisted(persisted);
783 saved += 1;
784 }
785
786 Ok(saved)
787 }
788
789 pub async fn insert<I>(&self, insertable: I) -> Result<E, OrmError>
794 where
795 E: AuditEntity + FromRow + Send + TenantScopedEntity,
796 I: Insertable<E>,
797 {
798 let compiled = SqlServerCompiler::compile_insert(&self.insert_query(&insertable)?)?;
799 let shared_connection = self.require_connection()?;
800 let mut connection = shared_connection.lock().await?;
801 let inserted = connection.fetch_one(compiled).await?;
802
803 inserted.ok_or_else(|| OrmError::new("insert query did not return a row"))
804 }
805
806 pub async fn update<K, C>(&self, key: K, changeset: C) -> Result<Option<E>, OrmError>
813 where
814 E: AuditEntity + FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
815 K: SqlTypeMapping,
816 C: Changeset<E>,
817 {
818 let key = key.to_sql_value();
819 let concurrency_token = changeset.concurrency_token()?;
820 let compiled = SqlServerCompiler::compile_update(&self.update_query_sql_value_audited(
821 key.clone(),
822 changeset.changes(),
823 concurrency_token.clone(),
824 )?)?;
825 let shared_connection = self.require_connection()?;
826 let mut connection = shared_connection.lock().await?;
827 let updated = connection.fetch_one(compiled).await?;
828 drop(connection);
829
830 if updated.is_none()
831 && concurrency_token.is_some()
832 && self.exists_by_sql_value_internal(key).await?
833 {
834 return Err(OrmError::concurrency_conflict());
835 }
836
837 Ok(updated)
838 }
839
840 pub async fn delete<K>(&self, key: K) -> Result<bool, OrmError>
846 where
847 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
848 K: SqlTypeMapping,
849 {
850 self.delete_by_sql_value(key.to_sql_value(), None).await
851 }
852
853 pub(crate) async fn delete_by_sql_value(
854 &self,
855 key: SqlValue,
856 concurrency_token: Option<SqlValue>,
857 ) -> Result<bool, OrmError>
858 where
859 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
860 {
861 let shared_connection = self.require_connection()?;
862 let soft_delete_provider = shared_connection.soft_delete_provider();
863 let soft_delete_request_values = shared_connection.soft_delete_request_values();
864 let compiled = self.delete_compiled_query_sql_value(
865 key.clone(),
866 concurrency_token.clone(),
867 soft_delete_provider.as_deref(),
868 soft_delete_request_values.as_deref(),
869 )?;
870 let mut connection = shared_connection.lock().await?;
871 let result = connection.execute(compiled).await?;
872 let deleted = result.total() > 0;
873
874 drop(connection);
875
876 if !deleted && concurrency_token.is_some() && self.exists_by_sql_value_internal(key).await?
877 {
878 return Err(OrmError::concurrency_conflict());
879 }
880
881 Ok(deleted)
882 }
883
884 pub(crate) async fn delete_tracked_by_sql_value(
885 &self,
886 key: SqlValue,
887 concurrency_token: Option<SqlValue>,
888 ) -> Result<bool, OrmError>
889 where
890 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
891 {
892 self.delete_by_sql_value(key, concurrency_token).await
893 }
894
895 async fn find_by_sql_value_internal(&self, key: SqlValue) -> Result<Option<E>, OrmError>
896 where
897 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
898 {
899 self.query_with_internal_visibility(self.find_select_query_sql_value(key)?)
900 .first()
901 .await
902 }
903
904 pub(crate) async fn exists_by_sql_value_internal(&self, key: SqlValue) -> Result<bool, OrmError>
905 where
906 E: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
907 {
908 Ok(self.find_by_sql_value_internal(key).await?.is_some())
909 }
910
911 pub(crate) async fn insert_entity_values(
912 &self,
913 values: Vec<sql_orm_core::ColumnValue>,
914 ) -> Result<E, OrmError>
915 where
916 E: AuditEntity + FromRow + Send + TenantScopedEntity,
917 {
918 let compiled = SqlServerCompiler::compile_insert(&self.insert_query_values(values)?)?;
919 let shared_connection = self.require_connection()?;
920 let mut connection = shared_connection.lock().await?;
921 let inserted = connection.fetch_one(compiled).await?;
922
923 inserted.ok_or_else(|| OrmError::new("insert query did not return a row"))
924 }
925
926 pub(crate) async fn insert_entity(&self, entity: &E) -> Result<E, OrmError>
927 where
928 E: AuditEntity + EntityPersist + FromRow + Send + TenantScopedEntity,
929 {
930 self.insert_entity_values(entity.insert_values()).await
931 }
932
933 pub(crate) async fn update_entity_values_by_sql_value(
934 &self,
935 key: SqlValue,
936 changes: Vec<sql_orm_core::ColumnValue>,
937 concurrency_token: Option<SqlValue>,
938 ) -> Result<Option<E>, OrmError>
939 where
940 E: AuditEntity + FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
941 {
942 let compiled = SqlServerCompiler::compile_update(&self.update_query_sql_value_audited(
943 key.clone(),
944 changes,
945 concurrency_token.clone(),
946 )?)?;
947 let shared_connection = self.require_connection()?;
948 let mut connection = shared_connection.lock().await?;
949 let updated = connection.fetch_one(compiled).await?;
950 drop(connection);
951
952 if updated.is_none()
953 && concurrency_token.is_some()
954 && self.exists_by_sql_value_internal(key).await?
955 {
956 return Err(OrmError::concurrency_conflict());
957 }
958
959 Ok(updated)
960 }
961
962 pub(crate) async fn update_entity_by_sql_value(
963 &self,
964 key: SqlValue,
965 entity: &E,
966 concurrency_token: Option<SqlValue>,
967 ) -> Result<Option<E>, OrmError>
968 where
969 E: AuditEntity + EntityPersist + FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
970 {
971 self.update_entity_values_by_sql_value(key, entity.update_changes(), concurrency_token)
972 .await
973 }
974
975 pub fn shared_connection(&self) -> SharedConnection {
977 self.connection
978 .as_ref()
979 .expect("DbSet requires an initialized shared connection")
980 .clone()
981 }
982
983 pub async fn load_collection<J>(
989 &self,
990 entity: &mut E,
991 navigation: &'static str,
992 ) -> Result<(), OrmError>
993 where
994 E: EntityPrimaryKey + IncludeCollection<J>,
995 J: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
996 {
997 let related = self
998 .explicit_collection_query::<J>(entity, navigation)?
999 .all()
1000 .await?;
1001 entity.set_included_collection(navigation, related)
1002 }
1003
1004 pub async fn load_collection_tracked<J>(
1007 &self,
1008 tracked: &mut Tracked<E>,
1009 navigation: &'static str,
1010 ) -> Result<(), OrmError>
1011 where
1012 E: EntityPrimaryKey + IncludeCollection<J>,
1013 J: FromRow + Send + SoftDeleteEntity + TenantScopedEntity,
1014 {
1015 let related = self
1016 .explicit_collection_query::<J>(tracked.current(), navigation)?
1017 .all()
1018 .await?;
1019 tracked
1020 .current_mut_without_state_change()
1021 .set_included_collection(navigation, related)
1022 }
1023
1024 #[doc(hidden)]
1025 pub fn tracking_registry(&self) -> TrackingRegistryHandle {
1026 Arc::clone(&self.tracking_registry)
1027 }
1028
1029 fn require_connection(&self) -> Result<SharedConnection, OrmError> {
1030 self.connection
1031 .as_ref()
1032 .cloned()
1033 .ok_or_else(|| OrmError::new("DbSet requires an initialized shared connection"))
1034 }
1035
1036 fn active_tenant(&self) -> Option<ActiveTenant> {
1037 self.connection
1038 .as_ref()
1039 .and_then(SharedConnection::active_tenant)
1040 }
1041
1042 fn explicit_collection_query<J>(
1043 &self,
1044 entity: &E,
1045 navigation: &'static str,
1046 ) -> Result<DbSetQuery<J>, OrmError>
1047 where
1048 E: EntityPrimaryKey,
1049 J: Entity,
1050 {
1051 let navigation_metadata = E::metadata().navigation(navigation).ok_or_else(|| {
1052 OrmError::new(format!(
1053 "entity `{}` does not declare navigation `{}`",
1054 E::metadata().rust_name,
1055 navigation
1056 ))
1057 })?;
1058
1059 if navigation_metadata.kind != NavigationKind::HasMany {
1060 return Err(OrmError::new(format!(
1061 "explicit collection loading only supports has_many navigations; `{}` is {:?}",
1062 navigation_metadata.rust_field, navigation_metadata.kind
1063 )));
1064 }
1065
1066 if navigation_metadata.local_columns.len() != 1
1067 || navigation_metadata.target_columns.len() != 1
1068 {
1069 return Err(OrmError::new(
1070 "explicit collection loading currently supports only single-column navigation joins",
1071 ));
1072 }
1073
1074 let root_primary_key = E::metadata().primary_key.columns;
1075 if root_primary_key.len() != 1
1076 || root_primary_key[0] != navigation_metadata.local_columns[0]
1077 {
1078 return Err(OrmError::new(
1079 "explicit collection loading requires the has_many local column to be the root entity single-column primary key",
1080 ));
1081 }
1082
1083 let target_metadata = J::metadata();
1084 if navigation_metadata.target_schema != target_metadata.schema
1085 || navigation_metadata.target_table != target_metadata.table
1086 {
1087 return Err(OrmError::new(format!(
1088 "navigation `{}` on `{}` targets `{}.{}`, not entity `{}` (`{}.{}`)",
1089 navigation_metadata.rust_field,
1090 E::metadata().rust_name,
1091 navigation_metadata.target_schema,
1092 navigation_metadata.target_table,
1093 target_metadata.rust_name,
1094 target_metadata.schema,
1095 target_metadata.table
1096 )));
1097 }
1098
1099 let target_column = target_metadata
1100 .column(navigation_metadata.target_columns[0])
1101 .ok_or_else(|| {
1102 OrmError::new(format!(
1103 "entity `{}` metadata does not contain column `{}` required by explicit collection loading",
1104 target_metadata.rust_name, navigation_metadata.target_columns[0]
1105 ))
1106 })?;
1107
1108 let key = entity.primary_key_value()?;
1109 Ok(DbSetQuery::new(
1110 self.connection.as_ref().cloned(),
1111 SelectQuery::from_entity::<J>().filter(Predicate::eq(
1112 Expr::Column(ColumnRef::new(
1113 TableRef::for_entity::<J>(),
1114 target_column.rust_field,
1115 target_column.column_name,
1116 )),
1117 Expr::Value(key),
1118 )),
1119 ))
1120 }
1121}
1122
1123impl<E: Entity> std::fmt::Debug for DbSet<E> {
1124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1125 f.debug_struct("DbSet")
1126 .field("entity", &E::metadata().rust_name)
1127 .field("table", &E::metadata().table)
1128 .finish()
1129 }
1130}
1131
1132impl<E: Entity> DbSet<E> {
1133 fn find_select_query<K>(&self, key: K) -> Result<SelectQuery, OrmError>
1134 where
1135 K: SqlTypeMapping,
1136 {
1137 Ok(SelectQuery::from_entity::<E>().filter(self.primary_key_predicate(key)?))
1138 }
1139
1140 fn find_select_query_sql_value(&self, key: SqlValue) -> Result<SelectQuery, OrmError> {
1141 Ok(SelectQuery::from_entity::<E>().filter(self.primary_key_predicate_value(key)?))
1142 }
1143
1144 fn insert_query<I>(&self, insertable: &I) -> Result<InsertQuery, OrmError>
1145 where
1146 E: AuditEntity + TenantScopedEntity,
1147 I: Insertable<E>,
1148 {
1149 self.insert_query_values(insertable.values())
1150 }
1151
1152 fn insert_query_values(
1153 &self,
1154 values: Vec<sql_orm_core::ColumnValue>,
1155 ) -> Result<InsertQuery, OrmError>
1156 where
1157 E: AuditEntity + TenantScopedEntity,
1158 {
1159 let active_tenant = self.active_tenant();
1160 let audit_provider = self
1161 .connection
1162 .as_ref()
1163 .and_then(SharedConnection::audit_provider);
1164 let audit_request_values = self
1165 .connection
1166 .as_ref()
1167 .and_then(SharedConnection::audit_request_values);
1168 let values = apply_audit_values::<E>(
1169 AuditOperation::Insert,
1170 values,
1171 audit_provider.as_deref(),
1172 audit_request_values.as_deref(),
1173 )?;
1174 let values = self.tenant_insert_values(values, active_tenant.as_ref())?;
1175 Ok(InsertQuery::for_entity::<E, _>(&RawInsertable(values)))
1176 }
1177
1178 #[cfg(test)]
1179 fn insert_query_values_with_runtime_for_test(
1180 &self,
1181 values: Vec<sql_orm_core::ColumnValue>,
1182 audit_provider: Option<&dyn AuditProvider>,
1183 audit_request_values: Option<&AuditRequestValues>,
1184 ) -> Result<InsertQuery, OrmError>
1185 where
1186 E: AuditEntity + TenantScopedEntity,
1187 {
1188 let active_tenant = self.active_tenant();
1189 let values = apply_audit_values::<E>(
1190 AuditOperation::Insert,
1191 values,
1192 audit_provider,
1193 audit_request_values,
1194 )?;
1195 let values = self.tenant_insert_values(values, active_tenant.as_ref())?;
1196 Ok(InsertQuery::for_entity::<E, _>(&RawInsertable(values)))
1197 }
1198
1199 fn tenant_insert_values(
1200 &self,
1201 mut values: Vec<sql_orm_core::ColumnValue>,
1202 active_tenant: Option<&ActiveTenant>,
1203 ) -> Result<Vec<sql_orm_core::ColumnValue>, OrmError>
1204 where
1205 E: TenantScopedEntity,
1206 {
1207 let Some(policy) = E::tenant_policy() else {
1208 return Ok(values);
1209 };
1210
1211 if policy.columns.len() != 1 {
1212 return Err(OrmError::new(
1213 "tenant insert requires exactly one tenant policy column",
1214 ));
1215 }
1216
1217 let tenant_column = &policy.columns[0];
1218 let active_tenant = active_tenant.ok_or_else(|| {
1219 OrmError::new("tenant-scoped insert requires an active tenant in the DbContext")
1220 })?;
1221
1222 if active_tenant.column_name != tenant_column.column_name {
1223 return Err(OrmError::new(format!(
1224 "active tenant column `{}` does not match entity tenant column `{}`",
1225 active_tenant.column_name, tenant_column.column_name
1226 )));
1227 }
1228
1229 if !tenant_value_matches_column_type(&active_tenant.value, tenant_column) {
1230 return Err(OrmError::new(format!(
1231 "active tenant value is not compatible with entity tenant column `{}`",
1232 tenant_column.column_name
1233 )));
1234 }
1235
1236 let mut tenant_value_position = None;
1237 for (index, value) in values.iter().enumerate() {
1238 if value.column_name == tenant_column.column_name {
1239 if tenant_value_position.is_some() {
1240 return Err(OrmError::new(format!(
1241 "tenant-scoped insert contains duplicate tenant column `{}`",
1242 tenant_column.column_name
1243 )));
1244 }
1245
1246 tenant_value_position = Some(index);
1247 }
1248 }
1249
1250 if let Some(index) = tenant_value_position {
1251 if values[index].value != active_tenant.value {
1252 return Err(OrmError::new(format!(
1253 "tenant-scoped insert value for column `{}` does not match the active tenant",
1254 tenant_column.column_name
1255 )));
1256 }
1257
1258 return Ok(values);
1259 }
1260
1261 values.push(sql_orm_core::ColumnValue::new(
1262 tenant_column.column_name,
1263 active_tenant.value.clone(),
1264 ));
1265 Ok(values)
1266 }
1267
1268 #[cfg(test)]
1269 fn update_query<K, C>(&self, key: K, changeset: &C) -> Result<UpdateQuery, OrmError>
1270 where
1271 E: TenantScopedEntity,
1272 K: SqlTypeMapping,
1273 C: Changeset<E>,
1274 {
1275 let active_tenant = self.active_tenant();
1276 let mut query =
1277 UpdateQuery::for_entity::<E, C>(changeset).filter(self.primary_key_predicate(key)?);
1278
1279 if let Some(predicate) = self.tenant_write_predicate(active_tenant.as_ref())? {
1280 query = query.filter(predicate);
1281 }
1282
1283 if let Some(token) = changeset.concurrency_token()? {
1284 query = query.filter(self.rowversion_predicate_value(token)?);
1285 }
1286
1287 Ok(query)
1288 }
1289
1290 fn update_query_sql_value_audited(
1291 &self,
1292 key: SqlValue,
1293 changes: Vec<sql_orm_core::ColumnValue>,
1294 concurrency_token: Option<SqlValue>,
1295 ) -> Result<UpdateQuery, OrmError>
1296 where
1297 E: AuditEntity + TenantScopedEntity,
1298 {
1299 let active_tenant = self.active_tenant();
1300 let audit_provider = self
1301 .connection
1302 .as_ref()
1303 .and_then(SharedConnection::audit_provider);
1304 let audit_request_values = self
1305 .connection
1306 .as_ref()
1307 .and_then(SharedConnection::audit_request_values);
1308
1309 self.update_query_sql_value_with_audit_runtime(
1310 key,
1311 changes,
1312 concurrency_token,
1313 active_tenant.as_ref(),
1314 audit_provider.as_deref(),
1315 audit_request_values.as_deref(),
1316 )
1317 }
1318
1319 fn update_query_sql_value_with_audit_runtime(
1320 &self,
1321 key: SqlValue,
1322 changes: Vec<sql_orm_core::ColumnValue>,
1323 concurrency_token: Option<SqlValue>,
1324 active_tenant: Option<&ActiveTenant>,
1325 audit_provider: Option<&dyn AuditProvider>,
1326 audit_request_values: Option<&AuditRequestValues>,
1327 ) -> Result<UpdateQuery, OrmError>
1328 where
1329 E: AuditEntity + TenantScopedEntity,
1330 {
1331 let changes = apply_audit_values::<E>(
1332 AuditOperation::Update,
1333 changes,
1334 audit_provider,
1335 audit_request_values,
1336 )?;
1337
1338 self.update_query_sql_value_with_active_tenant(
1339 key,
1340 changes,
1341 concurrency_token,
1342 active_tenant,
1343 )
1344 }
1345
1346 fn update_query_sql_value_with_active_tenant(
1347 &self,
1348 key: SqlValue,
1349 changes: Vec<sql_orm_core::ColumnValue>,
1350 concurrency_token: Option<SqlValue>,
1351 active_tenant: Option<&ActiveTenant>,
1352 ) -> Result<UpdateQuery, OrmError>
1353 where
1354 E: TenantScopedEntity,
1355 {
1356 let mut query = UpdateQuery::for_entity::<E, _>(&RawChangeset(changes))
1357 .filter(self.primary_key_predicate_value(key)?);
1358
1359 if let Some(predicate) = self.tenant_write_predicate(active_tenant)? {
1360 query = query.filter(predicate);
1361 }
1362
1363 if let Some(token) = concurrency_token {
1364 query = query.filter(self.rowversion_predicate_value(token)?);
1365 }
1366
1367 Ok(query)
1368 }
1369
1370 #[cfg(test)]
1371 fn delete_query<K>(&self, key: K) -> Result<DeleteQuery, OrmError>
1372 where
1373 E: TenantScopedEntity,
1374 K: SqlTypeMapping,
1375 {
1376 let active_tenant = self.active_tenant();
1377 let mut query = DeleteQuery::from_entity::<E>().filter(self.primary_key_predicate(key)?);
1378
1379 if let Some(predicate) = self.tenant_write_predicate(active_tenant.as_ref())? {
1380 query = query.filter(predicate);
1381 }
1382
1383 Ok(query)
1384 }
1385
1386 #[cfg(test)]
1387 fn delete_query_sql_value(
1388 &self,
1389 key: SqlValue,
1390 concurrency_token: Option<SqlValue>,
1391 ) -> Result<DeleteQuery, OrmError>
1392 where
1393 E: TenantScopedEntity,
1394 {
1395 let active_tenant = self.active_tenant();
1396 self.delete_query_sql_value_with_active_tenant(
1397 key,
1398 concurrency_token,
1399 active_tenant.as_ref(),
1400 )
1401 }
1402
1403 fn delete_query_sql_value_with_active_tenant(
1404 &self,
1405 key: SqlValue,
1406 concurrency_token: Option<SqlValue>,
1407 active_tenant: Option<&ActiveTenant>,
1408 ) -> Result<DeleteQuery, OrmError>
1409 where
1410 E: TenantScopedEntity,
1411 {
1412 let mut query =
1413 DeleteQuery::from_entity::<E>().filter(self.primary_key_predicate_value(key)?);
1414
1415 if let Some(predicate) = self.tenant_write_predicate(active_tenant)? {
1416 query = query.filter(predicate);
1417 }
1418
1419 if let Some(token) = concurrency_token {
1420 query = query.filter(self.rowversion_predicate_value(token)?);
1421 }
1422
1423 Ok(query)
1424 }
1425
1426 fn delete_compiled_query_sql_value(
1427 &self,
1428 key: SqlValue,
1429 concurrency_token: Option<SqlValue>,
1430 soft_delete_provider: Option<&dyn SoftDeleteProvider>,
1431 request_values: Option<&SoftDeleteRequestValues>,
1432 ) -> Result<sql_orm_query::CompiledQuery, OrmError>
1433 where
1434 E: SoftDeleteEntity + TenantScopedEntity,
1435 {
1436 let active_tenant = self.active_tenant();
1437 self.delete_compiled_query_sql_value_with_active_tenant(
1438 key,
1439 concurrency_token,
1440 soft_delete_provider,
1441 request_values,
1442 active_tenant.as_ref(),
1443 )
1444 }
1445
1446 fn delete_compiled_query_sql_value_with_active_tenant(
1447 &self,
1448 key: SqlValue,
1449 concurrency_token: Option<SqlValue>,
1450 soft_delete_provider: Option<&dyn SoftDeleteProvider>,
1451 request_values: Option<&SoftDeleteRequestValues>,
1452 active_tenant: Option<&ActiveTenant>,
1453 ) -> Result<sql_orm_query::CompiledQuery, OrmError>
1454 where
1455 E: SoftDeleteEntity + TenantScopedEntity,
1456 {
1457 if E::soft_delete_policy().is_some() {
1458 let changes = apply_soft_delete_values::<E>(
1459 SoftDeleteOperation::Delete,
1460 Vec::new(),
1461 soft_delete_provider,
1462 request_values,
1463 )?;
1464
1465 if changes.is_empty() {
1466 return Err(OrmError::new(
1467 "soft_delete delete requires at least one runtime change",
1468 ));
1469 }
1470
1471 SqlServerCompiler::compile_update(&self.update_query_sql_value_with_active_tenant(
1472 key,
1473 changes,
1474 concurrency_token,
1475 active_tenant,
1476 )?)
1477 } else {
1478 SqlServerCompiler::compile_delete(&self.delete_query_sql_value_with_active_tenant(
1479 key,
1480 concurrency_token,
1481 active_tenant,
1482 )?)
1483 }
1484 }
1485
1486 fn tenant_write_predicate(
1487 &self,
1488 active_tenant: Option<&ActiveTenant>,
1489 ) -> Result<Option<Predicate>, OrmError>
1490 where
1491 E: TenantScopedEntity,
1492 {
1493 let Some(policy) = E::tenant_policy() else {
1494 return Ok(None);
1495 };
1496
1497 if policy.columns.len() != 1 {
1498 return Err(OrmError::new(
1499 "tenant write filter requires exactly one tenant policy column",
1500 ));
1501 }
1502
1503 let tenant_column = &policy.columns[0];
1504 let active_tenant = active_tenant.ok_or_else(|| {
1505 OrmError::new("tenant-scoped write requires an active tenant in the DbContext")
1506 })?;
1507
1508 if active_tenant.column_name != tenant_column.column_name {
1509 return Err(OrmError::new(format!(
1510 "active tenant column `{}` does not match entity tenant column `{}`",
1511 active_tenant.column_name, tenant_column.column_name
1512 )));
1513 }
1514
1515 if !tenant_value_matches_column_type(&active_tenant.value, tenant_column) {
1516 return Err(OrmError::new(format!(
1517 "active tenant value is not compatible with entity tenant column `{}`",
1518 tenant_column.column_name
1519 )));
1520 }
1521
1522 Ok(Some(Predicate::eq(
1523 Expr::Column(ColumnRef::new(
1524 TableRef::for_entity::<E>(),
1525 tenant_column.rust_field,
1526 tenant_column.column_name,
1527 )),
1528 Expr::Value(active_tenant.value.clone()),
1529 )))
1530 }
1531
1532 fn primary_key_predicate<K>(&self, key: K) -> Result<Predicate, OrmError>
1533 where
1534 K: SqlTypeMapping,
1535 {
1536 self.primary_key_predicate_value(key.to_sql_value())
1537 }
1538
1539 fn primary_key_predicate_value(&self, key: SqlValue) -> Result<Predicate, OrmError> {
1540 let metadata = E::metadata();
1541 let primary_key = metadata.primary_key_columns();
1542
1543 if primary_key.len() != 1 {
1544 return Err(OrmError::new(
1545 "DbSet currently supports this operation only for entities with a single primary key column",
1546 ));
1547 }
1548
1549 let column = primary_key[0];
1550
1551 Ok(Predicate::eq(
1552 Expr::Column(ColumnRef::new(
1553 TableRef::for_entity::<E>(),
1554 column.rust_field,
1555 column.column_name,
1556 )),
1557 Expr::Value(key),
1558 ))
1559 }
1560
1561 fn ensure_tracking_primary_key_scope(&self) -> Result<(), OrmError> {
1562 if E::metadata().primary_key_columns().len() == 1 {
1563 return Ok(());
1564 }
1565
1566 Err(OrmError::new(
1567 "change tracking currently supports only entities with a single primary key column",
1568 ))
1569 }
1570
1571 fn rowversion_predicate_value(&self, token: SqlValue) -> Result<Predicate, OrmError> {
1572 let metadata = E::metadata();
1573 let column = metadata.rowversion_column().ok_or_else(|| {
1574 OrmError::new("DbSet concurrency checks require an entity rowversion column")
1575 })?;
1576
1577 Ok(Predicate::eq(
1578 Expr::Column(ColumnRef::new(
1579 TableRef::for_entity::<E>(),
1580 column.rust_field,
1581 column.column_name,
1582 )),
1583 Expr::Value(token),
1584 ))
1585 }
1586}
1587
1588struct RawInsertable(Vec<sql_orm_core::ColumnValue>);
1589
1590impl<E: Entity> Insertable<E> for RawInsertable {
1591 fn values(&self) -> Vec<sql_orm_core::ColumnValue> {
1592 self.0.clone()
1593 }
1594}
1595
1596struct RawChangeset(Vec<sql_orm_core::ColumnValue>);
1597
1598impl<E: Entity> Changeset<E> for RawChangeset {
1599 fn changes(&self) -> Vec<sql_orm_core::ColumnValue> {
1600 self.0.clone()
1601 }
1602}
1603
1604pub async fn connect_shared(connection_string: &str) -> Result<SharedConnection, OrmError> {
1609 let connection = MssqlConnection::connect(connection_string).await?;
1610 Ok(SharedConnection::from_connection(connection))
1611}
1612
1613pub async fn connect_shared_with_options(
1615 connection_string: &str,
1616 options: MssqlOperationalOptions,
1617) -> Result<SharedConnection, OrmError> {
1618 let config =
1619 MssqlConnectionConfig::from_connection_string_with_options(connection_string, options)?;
1620 connect_shared_with_config(config).await
1621}
1622
1623pub async fn connect_shared_with_config(
1625 config: MssqlConnectionConfig,
1626) -> Result<SharedConnection, OrmError> {
1627 let connection = MssqlConnection::connect_with_config(config).await?;
1628 Ok(SharedConnection::from_connection(connection))
1629}
1630
1631#[cfg(feature = "pool-bb8")]
1632pub fn connect_shared_from_pool(pool: MssqlPool) -> SharedConnection {
1634 SharedConnection::from_pool(pool)
1635}
1636
1637#[cfg(test)]
1638mod tests {
1639 #[cfg(feature = "pool-bb8")]
1640 use super::ensure_transactions_supported;
1641 use super::{
1642 ActiveTenant, DbContext, DbContextEntitySet, DbSet, SharedConnectionKind,
1643 SharedConnectionRuntime,
1644 };
1645 use crate::{
1646 AuditEntity, AuditOperation, AuditProvider, AuditRequestValues, EntityPersist,
1647 EntityPersistMode, EntityPrimaryKey, IncludeCollection, IncludeNavigation,
1648 SoftDeleteContext, SoftDeleteEntity, SoftDeleteOperation, SoftDeleteProvider,
1649 SoftDeleteRequestValues, TenantScopedEntity, Tracked,
1650 };
1651 use sql_orm_core::{
1652 ColumnMetadata, ColumnValue, Entity, EntityMetadata, EntityPolicyMetadata,
1653 ForeignKeyMetadata, FromRow, Insertable, NavigationKind, NavigationMetadata, OrmError,
1654 PrimaryKeyMetadata, ReferentialAction, Row, SqlServerType, SqlValue,
1655 };
1656 use sql_orm_migrate::{
1657 ColumnSnapshot, MigrationOperation, ModelSnapshot, SchemaSnapshot, TableSnapshot,
1658 diff_column_operations, diff_schema_and_table_operations,
1659 };
1660 use sql_orm_query::{
1661 ColumnRef, DeleteQuery, Expr, InsertQuery, Predicate, SelectQuery, TableRef, UpdateQuery,
1662 };
1663
1664 #[derive(Debug, Clone)]
1665 struct TestEntity;
1666 struct VersionedEntity;
1667 struct TenantWriteEntity;
1668 struct AuditedWriteEntity;
1669 struct SoftDeleteEntityUnderTest;
1670 struct SoftDeleteVersionedEntity;
1671 #[derive(Debug, Clone)]
1672 struct CompositeKeyEntity;
1673 #[derive(Debug, Clone)]
1674 struct ExplicitLoadRoot {
1675 id: i64,
1676 children_loaded: usize,
1677 }
1678 struct ExplicitLoadChild;
1679 #[derive(Debug, Clone)]
1680 struct SingleNavigationRoot {
1681 navigation_loaded: bool,
1682 }
1683 #[derive(Debug, Clone)]
1684 struct SingleNavigationTarget;
1685 struct DummyContext {
1686 entities: DbSet<TestEntity>,
1687 }
1688 struct CompositeDummyContext {
1689 entities: DbSet<CompositeKeyEntity>,
1690 }
1691 struct NewTestEntity {
1692 name: String,
1693 active: bool,
1694 }
1695 struct NewTenantWriteEntity {
1696 name: String,
1697 tenant_id: Option<i64>,
1698 }
1699 struct UpdateTestEntity {
1700 name: Option<String>,
1701 active: Option<bool>,
1702 }
1703 struct UpdateVersionedEntity {
1704 name: Option<String>,
1705 version: Option<Vec<u8>>,
1706 }
1707 struct TestSoftDeleteProvider;
1708 struct TestAuditProvider;
1709
1710 static TEST_ENTITY_COLUMNS: [ColumnMetadata; 3] = [
1711 ColumnMetadata {
1712 rust_field: "id",
1713 column_name: "id",
1714 renamed_from: None,
1715 sql_type: SqlServerType::BigInt,
1716 nullable: false,
1717 primary_key: true,
1718 identity: None,
1719 default_sql: None,
1720 computed_sql: None,
1721 rowversion: false,
1722 insertable: true,
1723 updatable: false,
1724 max_length: None,
1725 precision: None,
1726 scale: None,
1727 },
1728 ColumnMetadata {
1729 rust_field: "name",
1730 column_name: "name",
1731 renamed_from: None,
1732 sql_type: SqlServerType::NVarChar,
1733 nullable: false,
1734 primary_key: false,
1735 identity: None,
1736 default_sql: None,
1737 computed_sql: None,
1738 rowversion: false,
1739 insertable: true,
1740 updatable: true,
1741 max_length: Some(120),
1742 precision: None,
1743 scale: None,
1744 },
1745 ColumnMetadata {
1746 rust_field: "active",
1747 column_name: "active",
1748 renamed_from: None,
1749 sql_type: SqlServerType::Bit,
1750 nullable: false,
1751 primary_key: false,
1752 identity: None,
1753 default_sql: None,
1754 computed_sql: None,
1755 rowversion: false,
1756 insertable: true,
1757 updatable: true,
1758 max_length: None,
1759 precision: None,
1760 scale: None,
1761 },
1762 ];
1763
1764 static TEST_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1765 rust_name: "TestEntity",
1766 schema: "dbo",
1767 table: "test_entities",
1768 renamed_from: None,
1769 columns: &TEST_ENTITY_COLUMNS,
1770 primary_key: PrimaryKeyMetadata {
1771 name: None,
1772 columns: &["id"],
1773 },
1774 indexes: &[],
1775 foreign_keys: &[],
1776 navigations: &[],
1777 };
1778
1779 static EXPLICIT_LOAD_ROOT_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
1780 rust_field: "id",
1781 column_name: "id",
1782 renamed_from: None,
1783 sql_type: SqlServerType::BigInt,
1784 nullable: false,
1785 primary_key: true,
1786 identity: None,
1787 default_sql: None,
1788 computed_sql: None,
1789 rowversion: false,
1790 insertable: true,
1791 updatable: false,
1792 max_length: None,
1793 precision: None,
1794 scale: None,
1795 }];
1796
1797 static EXPLICIT_LOAD_CHILD_COLUMNS: [ColumnMetadata; 2] = [
1798 ColumnMetadata {
1799 rust_field: "id",
1800 column_name: "id",
1801 renamed_from: None,
1802 sql_type: SqlServerType::BigInt,
1803 nullable: false,
1804 primary_key: true,
1805 identity: None,
1806 default_sql: None,
1807 computed_sql: None,
1808 rowversion: false,
1809 insertable: true,
1810 updatable: false,
1811 max_length: None,
1812 precision: None,
1813 scale: None,
1814 },
1815 ColumnMetadata {
1816 rust_field: "root_id",
1817 column_name: "root_id",
1818 renamed_from: None,
1819 sql_type: SqlServerType::BigInt,
1820 nullable: false,
1821 primary_key: false,
1822 identity: None,
1823 default_sql: None,
1824 computed_sql: None,
1825 rowversion: false,
1826 insertable: true,
1827 updatable: true,
1828 max_length: None,
1829 precision: None,
1830 scale: None,
1831 },
1832 ];
1833
1834 static EXPLICIT_LOAD_NAVIGATIONS: [NavigationMetadata; 1] = [NavigationMetadata::new(
1835 "children",
1836 NavigationKind::HasMany,
1837 "ExplicitLoadChild",
1838 "dbo",
1839 "explicit_load_children",
1840 &["id"],
1841 &["root_id"],
1842 Some("fk_explicit_load_children_root"),
1843 )];
1844
1845 static EXPLICIT_LOAD_CHILD_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata {
1846 name: "fk_explicit_load_children_root",
1847 columns: &["root_id"],
1848 referenced_schema: "dbo",
1849 referenced_table: "explicit_load_roots",
1850 referenced_columns: &["id"],
1851 on_delete: ReferentialAction::NoAction,
1852 on_update: ReferentialAction::NoAction,
1853 }];
1854
1855 static EXPLICIT_LOAD_ROOT_METADATA: EntityMetadata = EntityMetadata {
1856 rust_name: "ExplicitLoadRoot",
1857 schema: "dbo",
1858 table: "explicit_load_roots",
1859 renamed_from: None,
1860 columns: &EXPLICIT_LOAD_ROOT_COLUMNS,
1861 primary_key: PrimaryKeyMetadata {
1862 name: None,
1863 columns: &["id"],
1864 },
1865 indexes: &[],
1866 foreign_keys: &[],
1867 navigations: &EXPLICIT_LOAD_NAVIGATIONS,
1868 };
1869
1870 static EXPLICIT_LOAD_CHILD_METADATA: EntityMetadata = EntityMetadata {
1871 rust_name: "ExplicitLoadChild",
1872 schema: "dbo",
1873 table: "explicit_load_children",
1874 renamed_from: None,
1875 columns: &EXPLICIT_LOAD_CHILD_COLUMNS,
1876 primary_key: PrimaryKeyMetadata {
1877 name: None,
1878 columns: &["id"],
1879 },
1880 indexes: &[],
1881 foreign_keys: &EXPLICIT_LOAD_CHILD_FOREIGN_KEYS,
1882 navigations: &[],
1883 };
1884
1885 static SINGLE_NAVIGATION_ROOT_METADATA: EntityMetadata = EntityMetadata {
1886 rust_name: "SingleNavigationRoot",
1887 schema: "dbo",
1888 table: "single_navigation_roots",
1889 renamed_from: None,
1890 columns: &[],
1891 primary_key: PrimaryKeyMetadata {
1892 name: None,
1893 columns: &["id"],
1894 },
1895 indexes: &[],
1896 foreign_keys: &[],
1897 navigations: &[],
1898 };
1899
1900 static SINGLE_NAVIGATION_TARGET_METADATA: EntityMetadata = EntityMetadata {
1901 rust_name: "SingleNavigationTarget",
1902 schema: "dbo",
1903 table: "single_navigation_targets",
1904 renamed_from: None,
1905 columns: &[],
1906 primary_key: PrimaryKeyMetadata {
1907 name: None,
1908 columns: &["id"],
1909 },
1910 indexes: &[],
1911 foreign_keys: &[],
1912 navigations: &[],
1913 };
1914
1915 static COMPOSITE_KEY_ENTITY_COLUMNS: [ColumnMetadata; 2] = [
1916 ColumnMetadata {
1917 rust_field: "tenant_id",
1918 column_name: "tenant_id",
1919 renamed_from: None,
1920 sql_type: SqlServerType::BigInt,
1921 nullable: false,
1922 primary_key: true,
1923 identity: None,
1924 default_sql: None,
1925 computed_sql: None,
1926 rowversion: false,
1927 insertable: true,
1928 updatable: false,
1929 max_length: None,
1930 precision: None,
1931 scale: None,
1932 },
1933 ColumnMetadata {
1934 rust_field: "id",
1935 column_name: "id",
1936 renamed_from: None,
1937 sql_type: SqlServerType::BigInt,
1938 nullable: false,
1939 primary_key: true,
1940 identity: None,
1941 default_sql: None,
1942 computed_sql: None,
1943 rowversion: false,
1944 insertable: true,
1945 updatable: false,
1946 max_length: None,
1947 precision: None,
1948 scale: None,
1949 },
1950 ];
1951
1952 static COMPOSITE_KEY_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1953 rust_name: "CompositeKeyEntity",
1954 schema: "dbo",
1955 table: "composite_entities",
1956 renamed_from: None,
1957 columns: &COMPOSITE_KEY_ENTITY_COLUMNS,
1958 primary_key: PrimaryKeyMetadata {
1959 name: None,
1960 columns: &["tenant_id", "id"],
1961 },
1962 indexes: &[],
1963 foreign_keys: &[],
1964 navigations: &[],
1965 };
1966
1967 static VERSIONED_ENTITY_COLUMNS: [ColumnMetadata; 3] = [
1968 ColumnMetadata {
1969 rust_field: "id",
1970 column_name: "id",
1971 renamed_from: None,
1972 sql_type: SqlServerType::BigInt,
1973 nullable: false,
1974 primary_key: true,
1975 identity: None,
1976 default_sql: None,
1977 computed_sql: None,
1978 rowversion: false,
1979 insertable: true,
1980 updatable: false,
1981 max_length: None,
1982 precision: None,
1983 scale: None,
1984 },
1985 ColumnMetadata {
1986 rust_field: "name",
1987 column_name: "name",
1988 renamed_from: None,
1989 sql_type: SqlServerType::NVarChar,
1990 nullable: false,
1991 primary_key: false,
1992 identity: None,
1993 default_sql: None,
1994 computed_sql: None,
1995 rowversion: false,
1996 insertable: true,
1997 updatable: true,
1998 max_length: Some(120),
1999 precision: None,
2000 scale: None,
2001 },
2002 ColumnMetadata {
2003 rust_field: "version",
2004 column_name: "version",
2005 renamed_from: None,
2006 sql_type: SqlServerType::RowVersion,
2007 nullable: false,
2008 primary_key: false,
2009 identity: None,
2010 default_sql: None,
2011 computed_sql: None,
2012 rowversion: true,
2013 insertable: false,
2014 updatable: false,
2015 max_length: None,
2016 precision: None,
2017 scale: None,
2018 },
2019 ];
2020
2021 static VERSIONED_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2022 rust_name: "VersionedEntity",
2023 schema: "dbo",
2024 table: "versioned_entities",
2025 renamed_from: None,
2026 columns: &VERSIONED_ENTITY_COLUMNS,
2027 primary_key: PrimaryKeyMetadata {
2028 name: None,
2029 columns: &["id"],
2030 },
2031 indexes: &[],
2032 foreign_keys: &[],
2033 navigations: &[],
2034 };
2035
2036 static TENANT_WRITE_ENTITY_COLUMNS: [ColumnMetadata; 5] = [
2037 ColumnMetadata {
2038 rust_field: "id",
2039 column_name: "id",
2040 renamed_from: None,
2041 sql_type: SqlServerType::BigInt,
2042 nullable: false,
2043 primary_key: true,
2044 identity: None,
2045 default_sql: None,
2046 computed_sql: None,
2047 rowversion: false,
2048 insertable: true,
2049 updatable: false,
2050 max_length: None,
2051 precision: None,
2052 scale: None,
2053 },
2054 ColumnMetadata {
2055 rust_field: "name",
2056 column_name: "name",
2057 renamed_from: None,
2058 sql_type: SqlServerType::NVarChar,
2059 nullable: false,
2060 primary_key: false,
2061 identity: None,
2062 default_sql: None,
2063 computed_sql: None,
2064 rowversion: false,
2065 insertable: true,
2066 updatable: true,
2067 max_length: Some(120),
2068 precision: None,
2069 scale: None,
2070 },
2071 ColumnMetadata {
2072 rust_field: "tenant_id",
2073 column_name: "tenant_id",
2074 renamed_from: None,
2075 sql_type: SqlServerType::BigInt,
2076 nullable: false,
2077 primary_key: false,
2078 identity: None,
2079 default_sql: None,
2080 computed_sql: None,
2081 rowversion: false,
2082 insertable: true,
2083 updatable: false,
2084 max_length: None,
2085 precision: None,
2086 scale: None,
2087 },
2088 ColumnMetadata {
2089 rust_field: "version",
2090 column_name: "version",
2091 renamed_from: None,
2092 sql_type: SqlServerType::RowVersion,
2093 nullable: false,
2094 primary_key: false,
2095 identity: None,
2096 default_sql: None,
2097 computed_sql: None,
2098 rowversion: true,
2099 insertable: false,
2100 updatable: false,
2101 max_length: None,
2102 precision: None,
2103 scale: None,
2104 },
2105 ColumnMetadata {
2106 rust_field: "deleted_at",
2107 column_name: "deleted_at",
2108 renamed_from: None,
2109 sql_type: SqlServerType::DateTime2,
2110 nullable: true,
2111 primary_key: false,
2112 identity: None,
2113 default_sql: None,
2114 computed_sql: None,
2115 rowversion: false,
2116 insertable: false,
2117 updatable: true,
2118 max_length: None,
2119 precision: None,
2120 scale: None,
2121 },
2122 ];
2123
2124 static TENANT_WRITE_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2125 rust_name: "TenantWriteEntity",
2126 schema: "dbo",
2127 table: "tenant_write_entities",
2128 renamed_from: None,
2129 columns: &TENANT_WRITE_ENTITY_COLUMNS,
2130 primary_key: PrimaryKeyMetadata {
2131 name: None,
2132 columns: &["id"],
2133 },
2134 indexes: &[],
2135 foreign_keys: &[],
2136 navigations: &[],
2137 };
2138
2139 static SOFT_DELETE_ENTITY_COLUMNS: [ColumnMetadata; 3] = [
2140 ColumnMetadata {
2141 rust_field: "id",
2142 column_name: "id",
2143 renamed_from: None,
2144 sql_type: SqlServerType::BigInt,
2145 nullable: false,
2146 primary_key: true,
2147 identity: None,
2148 default_sql: None,
2149 computed_sql: None,
2150 rowversion: false,
2151 insertable: true,
2152 updatable: false,
2153 max_length: None,
2154 precision: None,
2155 scale: None,
2156 },
2157 ColumnMetadata {
2158 rust_field: "name",
2159 column_name: "name",
2160 renamed_from: None,
2161 sql_type: SqlServerType::NVarChar,
2162 nullable: false,
2163 primary_key: false,
2164 identity: None,
2165 default_sql: None,
2166 computed_sql: None,
2167 rowversion: false,
2168 insertable: true,
2169 updatable: true,
2170 max_length: Some(120),
2171 precision: None,
2172 scale: None,
2173 },
2174 ColumnMetadata {
2175 rust_field: "deleted_at",
2176 column_name: "deleted_at",
2177 renamed_from: None,
2178 sql_type: SqlServerType::DateTime2,
2179 nullable: true,
2180 primary_key: false,
2181 identity: None,
2182 default_sql: None,
2183 computed_sql: None,
2184 rowversion: false,
2185 insertable: false,
2186 updatable: true,
2187 max_length: None,
2188 precision: None,
2189 scale: None,
2190 },
2191 ];
2192
2193 static SOFT_DELETE_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2194 rust_name: "SoftDeleteEntityUnderTest",
2195 schema: "dbo",
2196 table: "soft_delete_entities",
2197 renamed_from: None,
2198 columns: &SOFT_DELETE_ENTITY_COLUMNS,
2199 primary_key: PrimaryKeyMetadata {
2200 name: None,
2201 columns: &["id"],
2202 },
2203 indexes: &[],
2204 foreign_keys: &[],
2205 navigations: &[],
2206 };
2207
2208 static SOFT_DELETE_VERSIONED_ENTITY_COLUMNS: [ColumnMetadata; 4] = [
2209 ColumnMetadata {
2210 rust_field: "id",
2211 column_name: "id",
2212 renamed_from: None,
2213 sql_type: SqlServerType::BigInt,
2214 nullable: false,
2215 primary_key: true,
2216 identity: None,
2217 default_sql: None,
2218 computed_sql: None,
2219 rowversion: false,
2220 insertable: true,
2221 updatable: false,
2222 max_length: None,
2223 precision: None,
2224 scale: None,
2225 },
2226 ColumnMetadata {
2227 rust_field: "name",
2228 column_name: "name",
2229 renamed_from: None,
2230 sql_type: SqlServerType::NVarChar,
2231 nullable: false,
2232 primary_key: false,
2233 identity: None,
2234 default_sql: None,
2235 computed_sql: None,
2236 rowversion: false,
2237 insertable: true,
2238 updatable: true,
2239 max_length: Some(120),
2240 precision: None,
2241 scale: None,
2242 },
2243 ColumnMetadata {
2244 rust_field: "deleted_at",
2245 column_name: "deleted_at",
2246 renamed_from: None,
2247 sql_type: SqlServerType::DateTime2,
2248 nullable: true,
2249 primary_key: false,
2250 identity: None,
2251 default_sql: None,
2252 computed_sql: None,
2253 rowversion: false,
2254 insertable: false,
2255 updatable: true,
2256 max_length: None,
2257 precision: None,
2258 scale: None,
2259 },
2260 ColumnMetadata {
2261 rust_field: "version",
2262 column_name: "version",
2263 renamed_from: None,
2264 sql_type: SqlServerType::RowVersion,
2265 nullable: false,
2266 primary_key: false,
2267 identity: None,
2268 default_sql: None,
2269 computed_sql: None,
2270 rowversion: true,
2271 insertable: false,
2272 updatable: false,
2273 max_length: None,
2274 precision: None,
2275 scale: None,
2276 },
2277 ];
2278
2279 static SOFT_DELETE_VERSIONED_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2280 rust_name: "SoftDeleteVersionedEntity",
2281 schema: "dbo",
2282 table: "soft_delete_versioned_entities",
2283 renamed_from: None,
2284 columns: &SOFT_DELETE_VERSIONED_ENTITY_COLUMNS,
2285 primary_key: PrimaryKeyMetadata {
2286 name: None,
2287 columns: &["id"],
2288 },
2289 indexes: &[],
2290 foreign_keys: &[],
2291 navigations: &[],
2292 };
2293
2294 static SOFT_DELETE_POLICY_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
2295 rust_field: "deleted_at",
2296 column_name: "deleted_at",
2297 renamed_from: None,
2298 sql_type: SqlServerType::DateTime2,
2299 nullable: true,
2300 primary_key: false,
2301 identity: None,
2302 default_sql: None,
2303 computed_sql: None,
2304 rowversion: false,
2305 insertable: false,
2306 updatable: true,
2307 max_length: None,
2308 precision: None,
2309 scale: None,
2310 }];
2311
2312 static AUDITED_WRITE_ENTITY_COLUMNS: [ColumnMetadata; 3] = [
2313 ColumnMetadata {
2314 rust_field: "id",
2315 column_name: "id",
2316 renamed_from: None,
2317 sql_type: SqlServerType::BigInt,
2318 nullable: false,
2319 primary_key: true,
2320 identity: None,
2321 default_sql: None,
2322 computed_sql: None,
2323 rowversion: false,
2324 insertable: true,
2325 updatable: false,
2326 max_length: None,
2327 precision: None,
2328 scale: None,
2329 },
2330 ColumnMetadata {
2331 rust_field: "name",
2332 column_name: "name",
2333 renamed_from: None,
2334 sql_type: SqlServerType::NVarChar,
2335 nullable: false,
2336 primary_key: false,
2337 identity: None,
2338 default_sql: None,
2339 computed_sql: None,
2340 rowversion: false,
2341 insertable: true,
2342 updatable: true,
2343 max_length: Some(120),
2344 precision: None,
2345 scale: None,
2346 },
2347 ColumnMetadata {
2348 rust_field: "updated_by",
2349 column_name: "updated_by",
2350 renamed_from: None,
2351 sql_type: SqlServerType::NVarChar,
2352 nullable: false,
2353 primary_key: false,
2354 identity: None,
2355 default_sql: None,
2356 computed_sql: None,
2357 rowversion: false,
2358 insertable: true,
2359 updatable: true,
2360 max_length: Some(120),
2361 precision: None,
2362 scale: None,
2363 },
2364 ];
2365
2366 static AUDITED_WRITE_ENTITY_METADATA: EntityMetadata = EntityMetadata {
2367 rust_name: "AuditedWriteEntity",
2368 schema: "dbo",
2369 table: "audited_write_entities",
2370 renamed_from: None,
2371 columns: &AUDITED_WRITE_ENTITY_COLUMNS,
2372 primary_key: PrimaryKeyMetadata {
2373 name: None,
2374 columns: &["id"],
2375 },
2376 indexes: &[],
2377 foreign_keys: &[],
2378 navigations: &[],
2379 };
2380
2381 static AUDITED_WRITE_POLICY_COLUMNS: [ColumnMetadata; 1] = [AUDITED_WRITE_ENTITY_COLUMNS[2]];
2382
2383 impl Entity for TestEntity {
2384 fn metadata() -> &'static EntityMetadata {
2385 &TEST_ENTITY_METADATA
2386 }
2387 }
2388
2389 impl Entity for CompositeKeyEntity {
2390 fn metadata() -> &'static EntityMetadata {
2391 &COMPOSITE_KEY_ENTITY_METADATA
2392 }
2393 }
2394
2395 impl Entity for VersionedEntity {
2396 fn metadata() -> &'static EntityMetadata {
2397 &VERSIONED_ENTITY_METADATA
2398 }
2399 }
2400
2401 impl Entity for TenantWriteEntity {
2402 fn metadata() -> &'static EntityMetadata {
2403 &TENANT_WRITE_ENTITY_METADATA
2404 }
2405 }
2406
2407 impl Entity for AuditedWriteEntity {
2408 fn metadata() -> &'static EntityMetadata {
2409 &AUDITED_WRITE_ENTITY_METADATA
2410 }
2411 }
2412
2413 impl Entity for SoftDeleteEntityUnderTest {
2414 fn metadata() -> &'static EntityMetadata {
2415 &SOFT_DELETE_ENTITY_METADATA
2416 }
2417 }
2418
2419 impl Entity for SoftDeleteVersionedEntity {
2420 fn metadata() -> &'static EntityMetadata {
2421 &SOFT_DELETE_VERSIONED_ENTITY_METADATA
2422 }
2423 }
2424
2425 impl Entity for ExplicitLoadRoot {
2426 fn metadata() -> &'static EntityMetadata {
2427 &EXPLICIT_LOAD_ROOT_METADATA
2428 }
2429 }
2430
2431 impl Entity for ExplicitLoadChild {
2432 fn metadata() -> &'static EntityMetadata {
2433 &EXPLICIT_LOAD_CHILD_METADATA
2434 }
2435 }
2436
2437 impl Entity for SingleNavigationRoot {
2438 fn metadata() -> &'static EntityMetadata {
2439 &SINGLE_NAVIGATION_ROOT_METADATA
2440 }
2441 }
2442
2443 impl Entity for SingleNavigationTarget {
2444 fn metadata() -> &'static EntityMetadata {
2445 &SINGLE_NAVIGATION_TARGET_METADATA
2446 }
2447 }
2448
2449 impl SoftDeleteEntity for TestEntity {
2450 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2451 None
2452 }
2453 }
2454
2455 impl AuditEntity for TestEntity {
2456 fn audit_policy() -> Option<EntityPolicyMetadata> {
2457 None
2458 }
2459 }
2460
2461 impl SoftDeleteEntity for CompositeKeyEntity {
2462 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2463 None
2464 }
2465 }
2466
2467 impl AuditEntity for CompositeKeyEntity {
2468 fn audit_policy() -> Option<EntityPolicyMetadata> {
2469 None
2470 }
2471 }
2472
2473 impl EntityPrimaryKey for CompositeKeyEntity {
2474 fn primary_key_value(&self) -> Result<SqlValue, OrmError> {
2475 Err(OrmError::new(
2476 "change tracking currently supports only entities with a single primary key column",
2477 ))
2478 }
2479 }
2480
2481 impl EntityPersist for CompositeKeyEntity {
2482 fn persist_mode(&self) -> Result<EntityPersistMode, OrmError> {
2483 Err(OrmError::new(
2484 "change tracking currently supports only entities with a single primary key column",
2485 ))
2486 }
2487
2488 fn insert_values(&self) -> Vec<ColumnValue> {
2489 Vec::new()
2490 }
2491
2492 fn update_changes(&self) -> Vec<ColumnValue> {
2493 vec![ColumnValue::new(
2494 "name",
2495 SqlValue::String("changed".to_string()),
2496 )]
2497 }
2498
2499 fn concurrency_token(&self) -> Result<Option<SqlValue>, OrmError> {
2500 Ok(None)
2501 }
2502
2503 fn sync_persisted(&mut self, persisted: Self) {
2504 *self = persisted;
2505 }
2506 }
2507
2508 impl SoftDeleteEntity for VersionedEntity {
2509 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2510 None
2511 }
2512 }
2513
2514 impl AuditEntity for VersionedEntity {
2515 fn audit_policy() -> Option<EntityPolicyMetadata> {
2516 None
2517 }
2518 }
2519
2520 impl SoftDeleteEntity for TenantWriteEntity {
2521 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2522 Some(EntityPolicyMetadata::new(
2523 "soft_delete",
2524 &TENANT_WRITE_ENTITY_COLUMNS[4..5],
2525 ))
2526 }
2527 }
2528
2529 impl AuditEntity for TenantWriteEntity {
2530 fn audit_policy() -> Option<EntityPolicyMetadata> {
2531 None
2532 }
2533 }
2534
2535 impl AuditEntity for AuditedWriteEntity {
2536 fn audit_policy() -> Option<EntityPolicyMetadata> {
2537 Some(EntityPolicyMetadata::new(
2538 "audit",
2539 &AUDITED_WRITE_POLICY_COLUMNS,
2540 ))
2541 }
2542 }
2543
2544 impl SoftDeleteEntity for SoftDeleteEntityUnderTest {
2545 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2546 Some(EntityPolicyMetadata::new(
2547 "soft_delete",
2548 &SOFT_DELETE_POLICY_COLUMNS,
2549 ))
2550 }
2551 }
2552
2553 impl AuditEntity for SoftDeleteEntityUnderTest {
2554 fn audit_policy() -> Option<EntityPolicyMetadata> {
2555 None
2556 }
2557 }
2558
2559 impl SoftDeleteEntity for SoftDeleteVersionedEntity {
2560 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2561 Some(EntityPolicyMetadata::new(
2562 "soft_delete",
2563 &SOFT_DELETE_POLICY_COLUMNS,
2564 ))
2565 }
2566 }
2567
2568 impl AuditEntity for SoftDeleteVersionedEntity {
2569 fn audit_policy() -> Option<EntityPolicyMetadata> {
2570 None
2571 }
2572 }
2573
2574 impl SoftDeleteEntity for ExplicitLoadChild {
2575 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2576 None
2577 }
2578 }
2579
2580 impl TenantScopedEntity for TestEntity {
2581 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2582 None
2583 }
2584 }
2585
2586 impl TenantScopedEntity for CompositeKeyEntity {
2587 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2588 None
2589 }
2590 }
2591
2592 impl TenantScopedEntity for VersionedEntity {
2593 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2594 None
2595 }
2596 }
2597
2598 impl TenantScopedEntity for TenantWriteEntity {
2599 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2600 Some(EntityPolicyMetadata::new(
2601 "tenant",
2602 &TENANT_WRITE_ENTITY_COLUMNS[2..3],
2603 ))
2604 }
2605 }
2606
2607 impl TenantScopedEntity for AuditedWriteEntity {
2608 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2609 None
2610 }
2611 }
2612
2613 impl TenantScopedEntity for SoftDeleteEntityUnderTest {
2614 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2615 None
2616 }
2617 }
2618
2619 impl TenantScopedEntity for SoftDeleteVersionedEntity {
2620 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2621 None
2622 }
2623 }
2624
2625 impl TenantScopedEntity for ExplicitLoadChild {
2626 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2627 None
2628 }
2629 }
2630
2631 impl FromRow for TestEntity {
2632 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
2633 Ok(Self)
2634 }
2635 }
2636
2637 impl FromRow for CompositeKeyEntity {
2638 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
2639 Ok(Self)
2640 }
2641 }
2642
2643 impl FromRow for ExplicitLoadChild {
2644 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
2645 Ok(Self)
2646 }
2647 }
2648
2649 impl EntityPrimaryKey for ExplicitLoadRoot {
2650 fn primary_key_value(&self) -> Result<SqlValue, OrmError> {
2651 Ok(SqlValue::I64(self.id))
2652 }
2653 }
2654
2655 impl EntityPersist for ExplicitLoadRoot {
2656 fn persist_mode(&self) -> Result<EntityPersistMode, OrmError> {
2657 Ok(EntityPersistMode::Update(SqlValue::I64(self.id)))
2658 }
2659
2660 fn insert_values(&self) -> Vec<ColumnValue> {
2661 Vec::new()
2662 }
2663
2664 fn update_changes(&self) -> Vec<ColumnValue> {
2665 Vec::new()
2666 }
2667
2668 fn concurrency_token(&self) -> Result<Option<SqlValue>, OrmError> {
2669 Ok(None)
2670 }
2671
2672 fn sync_persisted(&mut self, persisted: Self) {
2673 *self = persisted;
2674 }
2675 }
2676
2677 impl FromRow for ExplicitLoadRoot {
2678 fn from_row<R: Row>(_row: &R) -> Result<Self, OrmError> {
2679 Ok(Self {
2680 id: 7,
2681 children_loaded: 0,
2682 })
2683 }
2684 }
2685
2686 impl AuditEntity for ExplicitLoadRoot {
2687 fn audit_policy() -> Option<EntityPolicyMetadata> {
2688 None
2689 }
2690 }
2691
2692 impl SoftDeleteEntity for ExplicitLoadRoot {
2693 fn soft_delete_policy() -> Option<EntityPolicyMetadata> {
2694 None
2695 }
2696 }
2697
2698 impl TenantScopedEntity for ExplicitLoadRoot {
2699 fn tenant_policy() -> Option<EntityPolicyMetadata> {
2700 None
2701 }
2702 }
2703
2704 impl IncludeCollection<ExplicitLoadChild> for ExplicitLoadRoot {
2705 fn set_included_collection(
2706 &mut self,
2707 navigation: &str,
2708 values: Vec<ExplicitLoadChild>,
2709 ) -> Result<(), OrmError> {
2710 if navigation != "children" {
2711 return Err(OrmError::new("unexpected navigation"));
2712 }
2713
2714 self.children_loaded = values.len();
2715 Ok(())
2716 }
2717 }
2718
2719 impl IncludeNavigation<SingleNavigationTarget> for SingleNavigationRoot {
2720 fn set_included_navigation(
2721 &mut self,
2722 navigation: &str,
2723 value: Option<SingleNavigationTarget>,
2724 ) -> Result<(), OrmError> {
2725 if navigation != "target" {
2726 return Err(OrmError::new("unexpected navigation"));
2727 }
2728
2729 self.navigation_loaded = value.is_some();
2730 Ok(())
2731 }
2732 }
2733
2734 impl DbContext for DummyContext {
2735 fn from_shared_connection(_connection: super::SharedConnection) -> Self {
2736 unreachable!("DummyContext is only used in disconnected unit tests")
2737 }
2738
2739 fn shared_connection(&self) -> super::SharedConnection {
2740 panic!("DummyContext is only used in disconnected unit tests")
2741 }
2742
2743 fn tracking_registry(&self) -> crate::TrackingRegistryHandle {
2744 self.entities.tracking_registry()
2745 }
2746 }
2747
2748 impl DbContextEntitySet<TestEntity> for DummyContext {
2749 fn db_set(&self) -> &DbSet<TestEntity> {
2750 &self.entities
2751 }
2752 }
2753
2754 impl DbContext for CompositeDummyContext {
2755 fn from_shared_connection(_connection: super::SharedConnection) -> Self {
2756 unreachable!("CompositeDummyContext is only used in disconnected unit tests")
2757 }
2758
2759 fn shared_connection(&self) -> super::SharedConnection {
2760 panic!("CompositeDummyContext is only used in disconnected unit tests")
2761 }
2762
2763 fn tracking_registry(&self) -> crate::TrackingRegistryHandle {
2764 self.entities.tracking_registry()
2765 }
2766 }
2767
2768 impl DbContextEntitySet<CompositeKeyEntity> for CompositeDummyContext {
2769 fn db_set(&self) -> &DbSet<CompositeKeyEntity> {
2770 &self.entities
2771 }
2772 }
2773
2774 impl sql_orm_core::Insertable<TestEntity> for NewTestEntity {
2775 fn values(&self) -> Vec<ColumnValue> {
2776 vec![
2777 ColumnValue::new("name", SqlValue::String(self.name.clone())),
2778 ColumnValue::new("active", SqlValue::Bool(self.active)),
2779 ]
2780 }
2781 }
2782
2783 impl sql_orm_core::Insertable<TenantWriteEntity> for NewTenantWriteEntity {
2784 fn values(&self) -> Vec<ColumnValue> {
2785 let mut values = vec![ColumnValue::new(
2786 "name",
2787 SqlValue::String(self.name.clone()),
2788 )];
2789
2790 if let Some(tenant_id) = self.tenant_id {
2791 values.push(ColumnValue::new("tenant_id", SqlValue::I64(tenant_id)));
2792 }
2793
2794 values
2795 }
2796 }
2797
2798 impl sql_orm_core::Changeset<TestEntity> for UpdateTestEntity {
2799 fn changes(&self) -> Vec<ColumnValue> {
2800 let mut values = Vec::new();
2801
2802 if let Some(name) = &self.name {
2803 values.push(ColumnValue::new("name", SqlValue::String(name.clone())));
2804 }
2805
2806 if let Some(active) = self.active {
2807 values.push(ColumnValue::new("active", SqlValue::Bool(active)));
2808 }
2809
2810 values
2811 }
2812 }
2813
2814 impl sql_orm_core::Changeset<CompositeKeyEntity> for UpdateTestEntity {
2815 fn changes(&self) -> Vec<ColumnValue> {
2816 <Self as sql_orm_core::Changeset<TestEntity>>::changes(self)
2817 }
2818 }
2819
2820 impl sql_orm_core::Changeset<VersionedEntity> for UpdateVersionedEntity {
2821 fn changes(&self) -> Vec<ColumnValue> {
2822 let mut values = Vec::new();
2823
2824 if let Some(name) = &self.name {
2825 values.push(ColumnValue::new("name", SqlValue::String(name.clone())));
2826 }
2827
2828 values
2829 }
2830
2831 fn concurrency_token(&self) -> Result<Option<SqlValue>, sql_orm_core::OrmError> {
2832 Ok(self.version.clone().map(SqlValue::Bytes))
2833 }
2834 }
2835
2836 impl sql_orm_core::Changeset<TenantWriteEntity> for UpdateVersionedEntity {
2837 fn changes(&self) -> Vec<ColumnValue> {
2838 <Self as sql_orm_core::Changeset<VersionedEntity>>::changes(self)
2839 }
2840
2841 fn concurrency_token(&self) -> Result<Option<SqlValue>, sql_orm_core::OrmError> {
2842 <Self as sql_orm_core::Changeset<VersionedEntity>>::concurrency_token(self)
2843 }
2844 }
2845
2846 impl SoftDeleteProvider for TestSoftDeleteProvider {
2847 fn apply(
2848 &self,
2849 context: SoftDeleteContext<'_>,
2850 changes: &mut Vec<ColumnValue>,
2851 ) -> Result<(), OrmError> {
2852 assert_eq!(context.operation, SoftDeleteOperation::Delete);
2853 changes.push(ColumnValue::new(
2854 "deleted_at",
2855 SqlValue::String("2026-04-25T00:00:00".to_string()),
2856 ));
2857 Ok(())
2858 }
2859 }
2860
2861 impl AuditProvider for TestAuditProvider {
2862 fn values(&self, context: crate::AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
2863 assert_eq!(context.operation, AuditOperation::Update);
2864 Ok(vec![ColumnValue::new(
2865 "updated_by",
2866 SqlValue::String("audit-provider".to_string()),
2867 )])
2868 }
2869 }
2870
2871 #[test]
2872 fn direct_shared_connections_support_transactions() {
2873 assert_eq!(
2874 super::ensure_transactions_supported(SharedConnectionKind::Direct),
2875 Ok(())
2876 );
2877 }
2878
2879 #[test]
2880 fn transaction_depth_is_shared_across_runtime_clones() {
2881 let runtime = SharedConnectionRuntime::default();
2882 let cloned_runtime = runtime.clone();
2883
2884 runtime
2885 .transaction_depth
2886 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2887
2888 assert_eq!(
2889 cloned_runtime
2890 .transaction_depth
2891 .load(std::sync::atomic::Ordering::SeqCst),
2892 1
2893 );
2894 }
2895
2896 #[cfg(feature = "pool-bb8")]
2897 #[test]
2898 fn pooled_shared_connections_reject_transactions_until_pinned() {
2899 let error = ensure_transactions_supported(SharedConnectionKind::Pool).unwrap_err();
2900
2901 assert!(error.message().contains("pooled connections"));
2902 assert!(
2903 error
2904 .message()
2905 .contains("pin one physical SQL Server connection")
2906 );
2907 }
2908
2909 #[test]
2910 fn dbset_exposes_entity_metadata() {
2911 let dbset = DbSet::<TestEntity>::disconnected();
2912
2913 assert_eq!(dbset.entity_metadata().table, "test_entities");
2914 }
2915
2916 #[test]
2917 fn dbcontext_entity_set_trait_returns_typed_dbset() {
2918 let context = DummyContext {
2919 entities: DbSet::<TestEntity>::disconnected(),
2920 };
2921
2922 let dbset = <DummyContext as DbContextEntitySet<TestEntity>>::db_set(&context);
2923
2924 assert_eq!(dbset.entity_metadata().rust_name, "TestEntity");
2925 assert_eq!(dbset.entity_metadata().table, "test_entities");
2926 }
2927
2928 #[test]
2929 fn dbset_debug_includes_entity_name() {
2930 let dbset = DbSet::<TestEntity>::disconnected();
2931
2932 let rendered = format!("{dbset:?}");
2933
2934 assert!(rendered.contains("TestEntity"));
2935 assert!(rendered.contains("test_entities"));
2936 }
2937
2938 #[test]
2939 fn dbset_query_uses_entity_select_query_by_default() {
2940 let dbset = DbSet::<TestEntity>::disconnected();
2941
2942 assert_eq!(
2943 dbset.query().into_select_query(),
2944 SelectQuery::from_entity::<TestEntity>()
2945 );
2946 }
2947
2948 #[test]
2949 fn dbset_query_with_accepts_custom_select_query() {
2950 let dbset = DbSet::<TestEntity>::disconnected();
2951 let custom = SelectQuery::from_entity::<TestEntity>();
2952
2953 assert_eq!(dbset.query_with(custom.clone()).into_select_query(), custom);
2954 }
2955
2956 #[test]
2957 fn dbset_internal_query_visibility_bypasses_soft_delete_filter() {
2958 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
2959 let select = SelectQuery::from_entity::<SoftDeleteEntityUnderTest>();
2960
2961 assert_eq!(
2962 dbset
2963 .query_with_internal_visibility(select.clone())
2964 .into_select_query(),
2965 select
2966 );
2967 }
2968
2969 #[test]
2970 fn dbset_find_builds_select_query_for_single_primary_key() {
2971 let dbset = DbSet::<TestEntity>::disconnected();
2972
2973 let query = dbset.find_select_query(7_i64).unwrap();
2974
2975 assert_eq!(
2976 query,
2977 SelectQuery::from_entity::<TestEntity>().filter(Predicate::eq(
2978 Expr::Column(ColumnRef::new(
2979 TableRef::new("dbo", "test_entities"),
2980 "id",
2981 "id",
2982 )),
2983 Expr::Value(sql_orm_core::SqlValue::I64(7)),
2984 ))
2985 );
2986 }
2987
2988 #[test]
2989 fn dbset_find_rejects_composite_primary_keys() {
2990 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
2991
2992 let error = dbset.find_select_query(7_i64).unwrap_err();
2993
2994 assert_eq!(
2995 error.message(),
2996 "DbSet currently supports this operation only for entities with a single primary key column"
2997 );
2998 }
2999
3000 #[test]
3001 fn explicit_collection_loading_builds_related_entity_query() {
3002 let dbset = DbSet::<ExplicitLoadRoot>::disconnected();
3003 let root = ExplicitLoadRoot {
3004 id: 7,
3005 children_loaded: 0,
3006 };
3007
3008 let query = dbset
3009 .explicit_collection_query::<ExplicitLoadChild>(&root, "children")
3010 .unwrap()
3011 .into_select_query();
3012
3013 assert_eq!(
3014 query,
3015 SelectQuery::from_entity::<ExplicitLoadChild>().filter(Predicate::eq(
3016 Expr::Column(ColumnRef::new(
3017 TableRef::new("dbo", "explicit_load_children"),
3018 "root_id",
3019 "root_id",
3020 )),
3021 Expr::Value(SqlValue::I64(7)),
3022 ))
3023 );
3024 }
3025
3026 #[test]
3027 fn explicit_collection_loading_rejects_unknown_navigation() {
3028 let dbset = DbSet::<ExplicitLoadRoot>::disconnected();
3029 let root = ExplicitLoadRoot {
3030 id: 7,
3031 children_loaded: 0,
3032 };
3033
3034 let error = dbset
3035 .explicit_collection_query::<ExplicitLoadChild>(&root, "missing")
3036 .unwrap_err();
3037
3038 assert!(error.message().contains("does not declare navigation"));
3039 }
3040
3041 #[test]
3042 fn explicit_collection_loading_tracked_assignment_does_not_mark_modified() {
3043 let dbset = DbSet::<ExplicitLoadRoot>::disconnected();
3044 let mut tracked = Tracked::from_loaded(ExplicitLoadRoot {
3045 id: 7,
3046 children_loaded: 0,
3047 });
3048
3049 tracked
3050 .current_mut_without_state_change()
3051 .set_included_collection("children", vec![ExplicitLoadChild])
3052 .unwrap();
3053
3054 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3055 assert_eq!(tracked.current().children_loaded, 1);
3056 drop(dbset);
3057 }
3058
3059 #[test]
3060 fn tracked_navigation_assignment_does_not_register_related_graph() {
3061 let dbset = DbSet::<ExplicitLoadRoot>::disconnected();
3062 let registry = dbset.tracking_registry();
3063 let mut tracked = Tracked::from_loaded(ExplicitLoadRoot {
3064 id: 7,
3065 children_loaded: 0,
3066 });
3067 tracked.attach_registry(registry.clone());
3068
3069 tracked
3070 .current_mut_without_state_change()
3071 .set_included_collection("children", vec![ExplicitLoadChild])
3072 .unwrap();
3073
3074 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3075 assert_eq!(tracked.current().children_loaded, 1);
3076 assert_eq!(registry.tracked_for::<ExplicitLoadRoot>().len(), 1);
3077 assert_eq!(registry.tracked_for::<ExplicitLoadChild>().len(), 0);
3078 assert_eq!(registry.entry_count(), 1);
3079 }
3080
3081 #[test]
3082 fn tracked_single_navigation_assignment_does_not_register_related_graph() {
3083 let dbset = DbSet::<SingleNavigationRoot>::disconnected();
3084 let registry = dbset.tracking_registry();
3085 let mut tracked = Tracked::from_loaded(SingleNavigationRoot {
3086 navigation_loaded: false,
3087 });
3088 tracked.attach_registry(registry.clone());
3089
3090 tracked
3091 .current_mut_without_state_change()
3092 .set_included_navigation("target", Some(SingleNavigationTarget))
3093 .unwrap();
3094
3095 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3096 assert!(tracked.current().navigation_loaded);
3097 assert_eq!(registry.tracked_for::<SingleNavigationRoot>().len(), 1);
3098 assert_eq!(registry.tracked_for::<SingleNavigationTarget>().len(), 0);
3099 assert_eq!(registry.entry_count(), 1);
3100 }
3101
3102 #[tokio::test]
3103 async fn dbset_find_tracked_reuses_find_connection_path() {
3104 let dbset = DbSet::<TestEntity>::disconnected();
3105
3106 let error = dbset.find_tracked(7_i64).await.unwrap_err();
3107
3108 assert_eq!(
3109 error.message(),
3110 "DbSetQuery requires an initialized shared connection"
3111 );
3112 }
3113
3114 #[tokio::test]
3115 async fn dbset_find_tracked_rejects_composite_primary_keys_with_stable_error() {
3116 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3117
3118 let error = dbset.find_tracked(7_i64).await.unwrap_err();
3119
3120 assert_eq!(
3121 error.message(),
3122 "change tracking currently supports only entities with a single primary key column"
3123 );
3124 }
3125
3126 #[test]
3127 fn dbset_add_tracked_registers_added_entity_in_registry() {
3128 let dbset = DbSet::<TestEntity>::disconnected();
3129 let registry = dbset.tracking_registry();
3130
3131 let tracked = dbset.add_tracked(TestEntity);
3132
3133 assert_eq!(tracked.state(), crate::EntityState::Added);
3134 assert_eq!(registry.entry_count(), 1);
3135 assert_eq!(registry.registrations()[0].state, crate::EntityState::Added);
3136 }
3137
3138 #[tokio::test]
3139 async fn save_tracked_added_rejects_composite_primary_keys_before_sql() {
3140 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3141 let registry = dbset.tracking_registry();
3142 let tracked = dbset.add_tracked(CompositeKeyEntity);
3143
3144 let error = dbset.save_tracked_added().await.unwrap_err();
3145
3146 assert_eq!(tracked.state(), crate::EntityState::Added);
3147 assert_eq!(registry.entry_count(), 1);
3148 assert_eq!(
3149 error.message(),
3150 "change tracking currently supports only entities with a single primary key column"
3151 );
3152 }
3153
3154 #[tokio::test]
3155 async fn save_tracked_added_returns_zero_without_pending_added_entries() {
3156 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3157 let registry = dbset.tracking_registry();
3158 let mut tracked = Tracked::from_loaded(CompositeKeyEntity);
3159 tracked.attach_registry(registry.clone());
3160
3161 let saved = dbset.save_tracked_added().await.unwrap();
3162
3163 assert_eq!(saved, 0);
3164 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3165 assert_eq!(registry.entry_count(), 1);
3166 }
3167
3168 #[tokio::test]
3169 async fn mark_unchanged_on_added_entry_discards_pending_insert_before_validation() {
3170 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3171 let registry = dbset.tracking_registry();
3172 let mut tracked = dbset.add_tracked(CompositeKeyEntity);
3173
3174 tracked.mark_unchanged();
3175 let saved = dbset.save_tracked_added().await.unwrap();
3176
3177 assert_eq!(saved, 0);
3178 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3179 assert_eq!(registry.entry_count(), 1);
3180 assert_eq!(
3181 registry.registrations()[0].state,
3182 crate::EntityState::Unchanged
3183 );
3184 }
3185
3186 #[test]
3187 fn dbset_remove_tracked_marks_loaded_entity_as_deleted() {
3188 let dbset = DbSet::<TestEntity>::disconnected();
3189 let registry = dbset.tracking_registry();
3190 let mut tracked = Tracked::from_loaded(TestEntity);
3191 tracked.attach_registry(registry.clone());
3192
3193 dbset.remove_tracked(&mut tracked);
3194
3195 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3196 assert_eq!(registry.entry_count(), 1);
3197 assert_eq!(
3198 registry.registrations()[0].state,
3199 crate::EntityState::Deleted
3200 );
3201 }
3202
3203 #[test]
3204 fn dbset_remove_tracked_marks_modified_entity_as_deleted_without_detaching() {
3205 let dbset = DbSet::<TestEntity>::disconnected();
3206 let registry = dbset.tracking_registry();
3207 let mut tracked = Tracked::from_loaded(TestEntity);
3208 tracked.attach_registry(registry.clone());
3209 tracked.current_mut();
3210
3211 dbset.remove_tracked(&mut tracked);
3212
3213 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3214 assert_eq!(registry.entry_count(), 1);
3215 assert_eq!(
3216 registry.registrations()[0].state,
3217 crate::EntityState::Deleted
3218 );
3219 }
3220
3221 #[tokio::test]
3222 async fn save_tracked_deleted_rejects_composite_primary_keys_before_sql() {
3223 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3224 let registry = dbset.tracking_registry();
3225 let mut tracked = Tracked::from_loaded(CompositeKeyEntity);
3226 tracked.attach_registry(registry.clone());
3227
3228 dbset.remove_tracked(&mut tracked);
3229 let error = dbset.save_tracked_deleted().await.unwrap_err();
3230
3231 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3232 assert_eq!(registry.entry_count(), 1);
3233 assert_eq!(
3234 error.message(),
3235 "change tracking currently supports only entities with a single primary key column"
3236 );
3237 }
3238
3239 #[test]
3240 fn dbset_remove_tracked_cancels_pending_added_entity() {
3241 let dbset = DbSet::<TestEntity>::disconnected();
3242 let registry = dbset.tracking_registry();
3243 let mut tracked = dbset.add_tracked(TestEntity);
3244
3245 dbset.remove_tracked(&mut tracked);
3246
3247 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3248 assert_eq!(registry.entry_count(), 0);
3249 }
3250
3251 #[test]
3252 fn dbset_remove_tracked_is_idempotent_after_added_entry_was_cancelled() {
3253 let dbset = DbSet::<TestEntity>::disconnected();
3254 let registry = dbset.tracking_registry();
3255 let mut tracked = dbset.add_tracked(TestEntity);
3256
3257 dbset.remove_tracked(&mut tracked);
3258 dbset.remove_tracked(&mut tracked);
3259
3260 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3261 assert_eq!(registry.entry_count(), 0);
3262 }
3263
3264 #[tokio::test]
3265 async fn save_tracked_deleted_returns_zero_after_added_entry_was_cancelled() {
3266 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3267 let registry = dbset.tracking_registry();
3268 let mut tracked = dbset.add_tracked(CompositeKeyEntity);
3269
3270 dbset.remove_tracked(&mut tracked);
3271 let saved = dbset.save_tracked_deleted().await.unwrap();
3272
3273 assert_eq!(saved, 0);
3274 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3275 assert_eq!(registry.entry_count(), 0);
3276 }
3277
3278 #[tokio::test]
3279 async fn detach_tracked_added_entry_prevents_later_insert_without_resetting_state() {
3280 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3281 let registry = dbset.tracking_registry();
3282 let mut tracked = dbset.add_tracked(CompositeKeyEntity);
3283
3284 dbset.detach_tracked(&mut tracked);
3285 let saved = dbset.save_tracked_added().await.unwrap();
3286
3287 assert_eq!(saved, 0);
3288 assert_eq!(tracked.state(), crate::EntityState::Added);
3289 assert_eq!(registry.entry_count(), 0);
3290 }
3291
3292 #[test]
3293 fn dbset_detach_tracked_discards_pending_modified_entry() {
3294 let dbset = DbSet::<TestEntity>::disconnected();
3295 let registry = dbset.tracking_registry();
3296 let mut tracked = Tracked::from_loaded(TestEntity);
3297 tracked.attach_registry(registry.clone());
3298 tracked.current_mut();
3299
3300 dbset.detach_tracked(&mut tracked);
3301
3302 assert_eq!(tracked.state(), crate::EntityState::Modified);
3303 assert_eq!(registry.entry_count(), 0);
3304 }
3305
3306 #[tokio::test]
3307 async fn detach_tracked_deleted_entry_prevents_later_delete_without_resetting_state() {
3308 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3309 let registry = dbset.tracking_registry();
3310 let mut tracked = Tracked::from_loaded(CompositeKeyEntity);
3311 tracked.attach_registry(registry.clone());
3312
3313 dbset.remove_tracked(&mut tracked);
3314 dbset.detach_tracked(&mut tracked);
3315 let saved = dbset.save_tracked_deleted().await.unwrap();
3316
3317 assert_eq!(saved, 0);
3318 assert_eq!(tracked.state(), crate::EntityState::Deleted);
3319 assert_eq!(registry.entry_count(), 0);
3320 }
3321
3322 #[test]
3323 fn dbcontext_clear_tracker_removes_entries_without_resetting_wrappers() {
3324 let context = DummyContext {
3325 entities: DbSet::<TestEntity>::disconnected(),
3326 };
3327 let registry = <DummyContext as DbContext>::tracking_registry(&context);
3328 let added = context.entities.add_tracked(TestEntity);
3329 let mut modified = Tracked::from_loaded(TestEntity);
3330 modified.attach_registry(registry.clone());
3331 modified.mark_modified();
3332
3333 assert_eq!(registry.entry_count(), 2);
3334
3335 <DummyContext as DbContext>::clear_tracker(&context);
3336
3337 assert_eq!(registry.entry_count(), 0);
3338 assert_eq!(added.state(), crate::EntityState::Added);
3339 assert_eq!(modified.state(), crate::EntityState::Modified);
3340 }
3341
3342 #[tokio::test]
3343 async fn clear_tracker_discards_added_and_deleted_entries_before_save_phase_validation() {
3344 let context = CompositeDummyContext {
3345 entities: DbSet::<CompositeKeyEntity>::disconnected(),
3346 };
3347 let registry = <CompositeDummyContext as DbContext>::tracking_registry(&context);
3348 let added = context.entities.add_tracked(CompositeKeyEntity);
3349 let mut deleted = Tracked::from_loaded(CompositeKeyEntity);
3350 deleted.attach_registry(registry.clone());
3351 context.entities.remove_tracked(&mut deleted);
3352
3353 assert_eq!(registry.entry_count(), 2);
3354
3355 <CompositeDummyContext as DbContext>::clear_tracker(&context);
3356
3357 let added_saved = context.entities.save_tracked_added().await.unwrap();
3358 let deleted_saved = context.entities.save_tracked_deleted().await.unwrap();
3359
3360 assert_eq!(added_saved, 0);
3361 assert_eq!(deleted_saved, 0);
3362 assert_eq!(registry.entry_count(), 0);
3363 assert_eq!(added.state(), crate::EntityState::Added);
3364 assert_eq!(deleted.state(), crate::EntityState::Deleted);
3365 }
3366
3367 #[tokio::test]
3368 async fn save_tracked_modified_skips_update_when_persisted_snapshot_is_unchanged() {
3369 let dbset = DbSet::<ExplicitLoadRoot>::disconnected();
3370 let registry = dbset.tracking_registry();
3371 let mut tracked = Tracked::from_loaded(ExplicitLoadRoot {
3372 id: 7,
3373 children_loaded: 0,
3374 });
3375 tracked
3376 .attach_registry_loaded(registry.clone(), SqlValue::I64(7))
3377 .unwrap();
3378
3379 tracked.current_mut().children_loaded = 1;
3380
3381 let saved = dbset.save_tracked_modified().await.unwrap();
3382
3383 assert_eq!(saved, 0);
3384 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3385 assert_eq!(tracked.original().children_loaded, 1);
3386 assert_eq!(registry.entry_count(), 1);
3387 }
3388
3389 #[tokio::test]
3390 async fn save_tracked_modified_rejects_composite_primary_keys_before_sql() {
3391 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3392 let registry = dbset.tracking_registry();
3393 let mut tracked = Tracked::from_loaded(CompositeKeyEntity);
3394 tracked.attach_registry(registry.clone());
3395 tracked.current_mut();
3396
3397 let error = dbset.save_tracked_modified().await.unwrap_err();
3398
3399 assert_eq!(tracked.state(), crate::EntityState::Modified);
3400 assert_eq!(registry.entry_count(), 1);
3401 assert_eq!(
3402 error.message(),
3403 "change tracking currently supports only entities with a single primary key column"
3404 );
3405 }
3406
3407 #[tokio::test]
3408 async fn mark_unchanged_on_modified_entry_discards_pending_update_before_validation() {
3409 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3410 let registry = dbset.tracking_registry();
3411 let mut tracked = Tracked::from_loaded(CompositeKeyEntity);
3412 tracked.attach_registry(registry.clone());
3413 tracked.current_mut();
3414
3415 tracked.mark_unchanged();
3416 let saved = dbset.save_tracked_modified().await.unwrap();
3417
3418 assert_eq!(saved, 0);
3419 assert_eq!(tracked.state(), crate::EntityState::Unchanged);
3420 assert_eq!(registry.entry_count(), 1);
3421 assert_eq!(
3422 registry.registrations()[0].state,
3423 crate::EntityState::Unchanged
3424 );
3425 }
3426
3427 #[tokio::test]
3428 async fn save_tracked_modified_returns_zero_without_pending_modified_entries() {
3429 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3430 let registry = dbset.tracking_registry();
3431 let tracked = dbset.add_tracked(CompositeKeyEntity);
3432
3433 let saved = dbset.save_tracked_modified().await.unwrap();
3434
3435 assert_eq!(saved, 0);
3436 assert_eq!(tracked.state(), crate::EntityState::Added);
3437 assert_eq!(registry.entry_count(), 1);
3438 }
3439
3440 #[test]
3441 fn dbset_insert_builds_insert_query_for_entity() {
3442 let dbset = DbSet::<TestEntity>::disconnected();
3443 let insertable = NewTestEntity {
3444 name: "ana".to_string(),
3445 active: true,
3446 };
3447
3448 let query = dbset.insert_query(&insertable).unwrap();
3449
3450 assert_eq!(
3451 query,
3452 InsertQuery {
3453 into: TableRef::new("dbo", "test_entities"),
3454 values: vec![
3455 ColumnValue::new("name", SqlValue::String("ana".to_string())),
3456 ColumnValue::new("active", SqlValue::Bool(true)),
3457 ],
3458 }
3459 );
3460 }
3461
3462 #[test]
3463 fn dbset_insert_appends_active_tenant_for_tenant_scoped_entities() {
3464 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3465 let insertable = NewTenantWriteEntity {
3466 name: "tenant row".to_string(),
3467 tenant_id: None,
3468 };
3469 let active_tenant = ActiveTenant {
3470 column_name: "tenant_id",
3471 value: SqlValue::I64(42),
3472 };
3473
3474 let values = dbset
3475 .tenant_insert_values(insertable.values(), Some(&active_tenant))
3476 .unwrap();
3477
3478 assert_eq!(
3479 values,
3480 vec![
3481 ColumnValue::new("name", SqlValue::String("tenant row".to_string())),
3482 ColumnValue::new("tenant_id", SqlValue::I64(42)),
3483 ]
3484 );
3485 }
3486
3487 #[test]
3488 fn dbset_insert_accepts_matching_explicit_tenant_value() {
3489 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3490 let insertable = NewTenantWriteEntity {
3491 name: "tenant row".to_string(),
3492 tenant_id: Some(42),
3493 };
3494 let active_tenant = ActiveTenant {
3495 column_name: "tenant_id",
3496 value: SqlValue::I64(42),
3497 };
3498
3499 let values = dbset
3500 .tenant_insert_values(insertable.values(), Some(&active_tenant))
3501 .unwrap();
3502
3503 assert_eq!(
3504 values,
3505 vec![
3506 ColumnValue::new("name", SqlValue::String("tenant row".to_string())),
3507 ColumnValue::new("tenant_id", SqlValue::I64(42)),
3508 ]
3509 );
3510 }
3511
3512 #[test]
3513 fn dbset_insert_rejects_mismatched_explicit_tenant_value() {
3514 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3515 let insertable = NewTenantWriteEntity {
3516 name: "tenant row".to_string(),
3517 tenant_id: Some(7),
3518 };
3519 let active_tenant = ActiveTenant {
3520 column_name: "tenant_id",
3521 value: SqlValue::I64(42),
3522 };
3523
3524 let error = dbset
3525 .tenant_insert_values(insertable.values(), Some(&active_tenant))
3526 .unwrap_err();
3527
3528 assert!(error.message().contains("does not match the active tenant"));
3529 }
3530
3531 #[test]
3532 fn dbset_insert_fails_closed_without_active_tenant_for_tenant_scoped_entities() {
3533 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3534 let insertable = NewTenantWriteEntity {
3535 name: "tenant row".to_string(),
3536 tenant_id: None,
3537 };
3538
3539 let error = dbset
3540 .tenant_insert_values(insertable.values(), None)
3541 .unwrap_err();
3542
3543 assert!(
3544 error
3545 .message()
3546 .contains("tenant-scoped insert requires an active tenant")
3547 );
3548 }
3549
3550 #[test]
3551 fn tenant_security_guardrail_keeps_write_sql_tenant_scoped() {
3552 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3553 let provider = TestSoftDeleteProvider;
3554 let active_tenant = ActiveTenant {
3555 column_name: "tenant_id",
3556 value: SqlValue::I64(42),
3557 };
3558
3559 let insert_values = dbset
3560 .tenant_insert_values(
3561 vec![ColumnValue::new(
3562 "name",
3563 SqlValue::String("tenant row".to_string()),
3564 )],
3565 Some(&active_tenant),
3566 )
3567 .unwrap();
3568 let insert = super::SqlServerCompiler::compile_insert(&InsertQuery {
3569 into: TableRef::for_entity::<TenantWriteEntity>(),
3570 values: insert_values,
3571 })
3572 .unwrap();
3573 let update = super::SqlServerCompiler::compile_update(
3574 &dbset
3575 .update_query_sql_value_with_active_tenant(
3576 SqlValue::I64(7),
3577 vec![ColumnValue::new(
3578 "name",
3579 SqlValue::String("tenant row updated".to_string()),
3580 )],
3581 None,
3582 Some(&active_tenant),
3583 )
3584 .unwrap(),
3585 )
3586 .unwrap();
3587 let delete = super::SqlServerCompiler::compile_delete(
3588 &dbset
3589 .delete_query_sql_value_with_active_tenant(
3590 SqlValue::I64(7),
3591 None,
3592 Some(&active_tenant),
3593 )
3594 .unwrap(),
3595 )
3596 .unwrap();
3597 let soft_delete = dbset
3598 .delete_compiled_query_sql_value_with_active_tenant(
3599 SqlValue::I64(7),
3600 Some(SqlValue::Bytes(vec![9, 8, 7])),
3601 Some(&provider),
3602 None,
3603 Some(&active_tenant),
3604 )
3605 .unwrap();
3606
3607 assert_eq!(
3608 insert.sql,
3609 "INSERT INTO [dbo].[tenant_write_entities] ([name], [tenant_id]) OUTPUT INSERTED.* VALUES (@P1, @P2)"
3610 );
3611 assert_eq!(
3612 insert.params,
3613 vec![
3614 SqlValue::String("tenant row".to_string()),
3615 SqlValue::I64(42),
3616 ]
3617 );
3618
3619 for compiled in [&update, &delete, &soft_delete] {
3620 assert!(
3621 compiled
3622 .sql
3623 .contains("[dbo].[tenant_write_entities].[tenant_id] = @P"),
3624 "tenant-scoped write SQL must include tenant predicate: {}",
3625 compiled.sql
3626 );
3627 assert!(
3628 compiled.params.contains(&SqlValue::I64(42)),
3629 "tenant-scoped write params must include active tenant value: {:?}",
3630 compiled.params
3631 );
3632 }
3633
3634 assert!(
3635 !delete.sql.contains("OUTPUT INSERTED.*"),
3636 "physical delete should stay a DELETE statement while still tenant-scoped"
3637 );
3638 assert!(
3639 soft_delete.sql.starts_with("UPDATE "),
3640 "soft_delete route should remain logical UPDATE while tenant-scoped"
3641 );
3642 }
3643
3644 #[test]
3645 fn dbset_update_builds_update_query_for_entity_and_primary_key() {
3646 let dbset = DbSet::<TestEntity>::disconnected();
3647 let changeset = UpdateTestEntity {
3648 name: Some("ana maria".to_string()),
3649 active: Some(false),
3650 };
3651
3652 let query = dbset.update_query(7_i64, &changeset).unwrap();
3653
3654 assert_eq!(
3655 query,
3656 UpdateQuery::for_entity::<TestEntity, _>(&changeset).filter(Predicate::eq(
3657 Expr::Column(ColumnRef::new(
3658 TableRef::new("dbo", "test_entities"),
3659 "id",
3660 "id",
3661 )),
3662 Expr::Value(SqlValue::I64(7)),
3663 ))
3664 );
3665 }
3666
3667 #[test]
3668 fn dbset_update_rejects_composite_primary_keys() {
3669 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
3670 let changeset = UpdateTestEntity {
3671 name: Some("ana".to_string()),
3672 active: None,
3673 };
3674
3675 let error = dbset.update_query(7_i64, &changeset).unwrap_err();
3676
3677 assert_eq!(
3678 error.message(),
3679 "DbSet currently supports this operation only for entities with a single primary key column"
3680 );
3681 }
3682
3683 #[test]
3684 fn dbset_update_appends_rowversion_predicate_when_changeset_has_token() {
3685 let dbset = DbSet::<VersionedEntity>::disconnected();
3686 let changeset = UpdateVersionedEntity {
3687 name: Some("ana maria".to_string()),
3688 version: Some(vec![1, 2, 3, 4]),
3689 };
3690
3691 let query = dbset.update_query(7_i64, &changeset).unwrap();
3692
3693 assert_eq!(
3694 query,
3695 UpdateQuery::for_entity::<VersionedEntity, _>(&changeset).filter(Predicate::and(vec![
3696 Predicate::eq(
3697 Expr::Column(ColumnRef::new(
3698 TableRef::new("dbo", "versioned_entities"),
3699 "id",
3700 "id",
3701 )),
3702 Expr::Value(SqlValue::I64(7)),
3703 ),
3704 Predicate::eq(
3705 Expr::Column(ColumnRef::new(
3706 TableRef::new("dbo", "versioned_entities"),
3707 "version",
3708 "version",
3709 )),
3710 Expr::Value(SqlValue::Bytes(vec![1, 2, 3, 4])),
3711 ),
3712 ]))
3713 );
3714 }
3715
3716 #[test]
3717 fn dbset_update_appends_tenant_filter_before_rowversion_for_tenant_scoped_entities() {
3718 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3719 let changes = vec![ColumnValue::new(
3720 "name",
3721 SqlValue::String("tenant row".to_string()),
3722 )];
3723 let active_tenant = ActiveTenant {
3724 column_name: "tenant_id",
3725 value: SqlValue::I64(42),
3726 };
3727
3728 let query = dbset
3729 .update_query_sql_value_with_active_tenant(
3730 SqlValue::I64(7),
3731 changes,
3732 Some(SqlValue::Bytes(vec![1, 2, 3, 4])),
3733 Some(&active_tenant),
3734 )
3735 .unwrap();
3736 let compiled = super::SqlServerCompiler::compile_update(&query).unwrap();
3737
3738 assert_eq!(
3739 compiled.sql,
3740 "UPDATE [dbo].[tenant_write_entities] SET [name] = @P1 OUTPUT INSERTED.* WHERE ((([dbo].[tenant_write_entities].[id] = @P2) AND ([dbo].[tenant_write_entities].[tenant_id] = @P3)) AND ([dbo].[tenant_write_entities].[version] = @P4))"
3741 );
3742 assert_eq!(
3743 compiled.params,
3744 vec![
3745 SqlValue::String("tenant row".to_string()),
3746 SqlValue::I64(7),
3747 SqlValue::I64(42),
3748 SqlValue::Bytes(vec![1, 2, 3, 4]),
3749 ]
3750 );
3751 }
3752
3753 #[test]
3754 fn save_changes_modified_route_preserves_audit_request_values_before_provider_values() {
3755 let dbset = DbSet::<AuditedWriteEntity>::disconnected();
3756 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
3757 "updated_by",
3758 SqlValue::String("request-user".to_string()),
3759 )]);
3760
3761 let query = dbset
3762 .update_query_sql_value_with_audit_runtime(
3763 SqlValue::I64(7),
3764 vec![ColumnValue::new(
3765 "name",
3766 SqlValue::String("tracked audited row".to_string()),
3767 )],
3768 None,
3769 None,
3770 Some(&TestAuditProvider),
3771 Some(&request_values),
3772 )
3773 .unwrap();
3774 let compiled = super::SqlServerCompiler::compile_update(&query).unwrap();
3775
3776 assert_eq!(
3777 compiled.sql,
3778 "UPDATE [dbo].[audited_write_entities] SET [name] = @P1, [updated_by] = @P2 OUTPUT INSERTED.* WHERE ([dbo].[audited_write_entities].[id] = @P3)"
3779 );
3780 assert_eq!(
3781 compiled.params,
3782 vec![
3783 SqlValue::String("tracked audited row".to_string()),
3784 SqlValue::String("request-user".to_string()),
3785 SqlValue::I64(7),
3786 ]
3787 );
3788 }
3789
3790 #[test]
3791 fn save_changes_modified_route_preserves_tenant_and_rowversion_predicates() {
3792 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3793 let active_tenant = ActiveTenant {
3794 column_name: "tenant_id",
3795 value: SqlValue::I64(42),
3796 };
3797
3798 let query = dbset
3799 .update_query_sql_value_with_active_tenant(
3800 SqlValue::I64(7),
3801 vec![ColumnValue::new(
3802 "name",
3803 SqlValue::String("tracked tenant row".to_string()),
3804 )],
3805 Some(SqlValue::Bytes(vec![1, 2, 3, 4])),
3806 Some(&active_tenant),
3807 )
3808 .unwrap();
3809 let compiled = super::SqlServerCompiler::compile_update(&query).unwrap();
3810
3811 assert_eq!(
3812 compiled.sql,
3813 "UPDATE [dbo].[tenant_write_entities] SET [name] = @P1 OUTPUT INSERTED.* WHERE ((([dbo].[tenant_write_entities].[id] = @P2) AND ([dbo].[tenant_write_entities].[tenant_id] = @P3)) AND ([dbo].[tenant_write_entities].[version] = @P4))"
3814 );
3815 assert_eq!(
3816 compiled.params,
3817 vec![
3818 SqlValue::String("tracked tenant row".to_string()),
3819 SqlValue::I64(7),
3820 SqlValue::I64(42),
3821 SqlValue::Bytes(vec![1, 2, 3, 4]),
3822 ]
3823 );
3824 }
3825
3826 #[test]
3827 fn dbset_update_applies_audit_provider_values_before_compiling_update() {
3828 let dbset = DbSet::<AuditedWriteEntity>::disconnected();
3829 let provider = TestAuditProvider;
3830
3831 let query = dbset
3832 .update_query_sql_value_with_audit_runtime(
3833 SqlValue::I64(7),
3834 vec![ColumnValue::new(
3835 "name",
3836 SqlValue::String("audited row".to_string()),
3837 )],
3838 None,
3839 None,
3840 Some(&provider),
3841 None,
3842 )
3843 .unwrap();
3844 let compiled = super::SqlServerCompiler::compile_update(&query).unwrap();
3845
3846 assert_eq!(
3847 compiled.sql,
3848 "UPDATE [dbo].[audited_write_entities] SET [name] = @P1, [updated_by] = @P2 OUTPUT INSERTED.* WHERE ([dbo].[audited_write_entities].[id] = @P3)"
3849 );
3850 assert_eq!(
3851 compiled.params,
3852 vec![
3853 SqlValue::String("audited row".to_string()),
3854 SqlValue::String("audit-provider".to_string()),
3855 SqlValue::I64(7),
3856 ]
3857 );
3858 }
3859
3860 #[test]
3861 fn save_changes_added_route_preserves_audit_request_values_before_provider_values() {
3862 struct InsertAuditProvider;
3863
3864 impl AuditProvider for InsertAuditProvider {
3865 fn values(
3866 &self,
3867 context: crate::AuditContext<'_>,
3868 ) -> Result<Vec<ColumnValue>, OrmError> {
3869 assert_eq!(context.entity.table, "audited_write_entities");
3870 assert_eq!(context.operation, AuditOperation::Insert);
3871 assert!(context.request_values.is_some());
3872
3873 Ok(vec![ColumnValue::new(
3874 "updated_by",
3875 SqlValue::String("provider-user".to_string()),
3876 )])
3877 }
3878 }
3879
3880 let dbset = DbSet::<AuditedWriteEntity>::disconnected();
3881 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
3882 "updated_by",
3883 SqlValue::String("request-user".to_string()),
3884 )]);
3885
3886 let query = dbset
3887 .insert_query_values_with_runtime_for_test(
3888 vec![ColumnValue::new(
3889 "name",
3890 SqlValue::String("tracked audited insert".to_string()),
3891 )],
3892 Some(&InsertAuditProvider),
3893 Some(&request_values),
3894 )
3895 .unwrap();
3896 let compiled = super::SqlServerCompiler::compile_insert(&query).unwrap();
3897
3898 assert_eq!(
3899 compiled.sql,
3900 "INSERT INTO [dbo].[audited_write_entities] ([name], [updated_by]) OUTPUT INSERTED.* VALUES (@P1, @P2)"
3901 );
3902 assert_eq!(
3903 compiled.params,
3904 vec![
3905 SqlValue::String("tracked audited insert".to_string()),
3906 SqlValue::String("request-user".to_string()),
3907 ]
3908 );
3909 }
3910
3911 #[test]
3912 fn dbset_insert_applies_audit_request_values_before_provider_values() {
3913 struct InsertAuditProvider;
3914
3915 impl AuditProvider for InsertAuditProvider {
3916 fn values(
3917 &self,
3918 context: crate::AuditContext<'_>,
3919 ) -> Result<Vec<ColumnValue>, OrmError> {
3920 assert_eq!(context.entity.table, "audited_write_entities");
3921 assert_eq!(context.operation, AuditOperation::Insert);
3922 assert!(context.request_values.is_some());
3923
3924 Ok(vec![ColumnValue::new(
3925 "updated_by",
3926 SqlValue::String("provider".to_string()),
3927 )])
3928 }
3929 }
3930
3931 let dbset = DbSet::<AuditedWriteEntity>::disconnected();
3932 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
3933 "updated_by",
3934 SqlValue::String("request".to_string()),
3935 )]);
3936
3937 let query = dbset
3938 .insert_query_values_with_runtime_for_test(
3939 vec![ColumnValue::new(
3940 "name",
3941 SqlValue::String("audited insert".to_string()),
3942 )],
3943 Some(&InsertAuditProvider),
3944 Some(&request_values),
3945 )
3946 .unwrap();
3947 let compiled = super::SqlServerCompiler::compile_insert(&query).unwrap();
3948
3949 assert_eq!(
3950 compiled.sql,
3951 "INSERT INTO [dbo].[audited_write_entities] ([name], [updated_by]) OUTPUT INSERTED.* VALUES (@P1, @P2)"
3952 );
3953 assert_eq!(
3954 compiled.params,
3955 vec![
3956 SqlValue::String("audited insert".to_string()),
3957 SqlValue::String("request".to_string()),
3958 ]
3959 );
3960 }
3961
3962 #[test]
3963 fn dbset_update_fails_closed_without_active_tenant_for_tenant_scoped_entities() {
3964 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3965
3966 let error = dbset
3967 .update_query_sql_value_with_active_tenant(
3968 SqlValue::I64(7),
3969 vec![ColumnValue::new(
3970 "name",
3971 SqlValue::String("blocked".to_string()),
3972 )],
3973 None,
3974 None,
3975 )
3976 .unwrap_err();
3977
3978 assert!(
3979 error
3980 .message()
3981 .contains("tenant-scoped write requires an active tenant")
3982 );
3983 }
3984
3985 #[test]
3986 fn save_changes_deleted_route_preserves_soft_delete_request_tenant_and_rowversion() {
3987 let dbset = DbSet::<TenantWriteEntity>::disconnected();
3988 let request_values = SoftDeleteRequestValues::new(vec![ColumnValue::new(
3989 "deleted_at",
3990 SqlValue::String("2026-05-07T00:00:00".to_string()),
3991 )]);
3992 let active_tenant = ActiveTenant {
3993 column_name: "tenant_id",
3994 value: SqlValue::I64(42),
3995 };
3996
3997 let compiled = dbset
3998 .delete_compiled_query_sql_value_with_active_tenant(
3999 SqlValue::I64(7),
4000 Some(SqlValue::Bytes(vec![9, 8, 7])),
4001 None,
4002 Some(&request_values),
4003 Some(&active_tenant),
4004 )
4005 .unwrap();
4006
4007 assert_eq!(
4008 compiled.sql,
4009 "UPDATE [dbo].[tenant_write_entities] SET [deleted_at] = @P1 OUTPUT INSERTED.* WHERE ((([dbo].[tenant_write_entities].[id] = @P2) AND ([dbo].[tenant_write_entities].[tenant_id] = @P3)) AND ([dbo].[tenant_write_entities].[version] = @P4))"
4010 );
4011 assert_eq!(
4012 compiled.params,
4013 vec![
4014 SqlValue::String("2026-05-07T00:00:00".to_string()),
4015 SqlValue::I64(7),
4016 SqlValue::I64(42),
4017 SqlValue::Bytes(vec![9, 8, 7]),
4018 ]
4019 );
4020 }
4021
4022 #[test]
4023 fn dbset_delete_builds_delete_query_for_entity_and_primary_key() {
4024 let dbset = DbSet::<TestEntity>::disconnected();
4025
4026 let query = dbset.delete_query(7_i64).unwrap();
4027
4028 assert_eq!(
4029 query,
4030 DeleteQuery::from_entity::<TestEntity>().filter(Predicate::eq(
4031 Expr::Column(ColumnRef::new(
4032 TableRef::new("dbo", "test_entities"),
4033 "id",
4034 "id",
4035 )),
4036 Expr::Value(SqlValue::I64(7)),
4037 ))
4038 );
4039 }
4040
4041 #[test]
4042 fn dbset_delete_query_sql_value_builds_delete_query_for_entity_and_primary_key() {
4043 let dbset = DbSet::<TestEntity>::disconnected();
4044
4045 let query = dbset
4046 .delete_query_sql_value(SqlValue::I64(7), None)
4047 .unwrap();
4048
4049 assert_eq!(
4050 query,
4051 DeleteQuery::from_entity::<TestEntity>().filter(Predicate::eq(
4052 Expr::Column(ColumnRef::new(
4053 TableRef::new("dbo", "test_entities"),
4054 "id",
4055 "id",
4056 )),
4057 Expr::Value(SqlValue::I64(7)),
4058 ))
4059 );
4060 }
4061
4062 #[test]
4063 fn dbset_delete_query_sql_value_appends_rowversion_predicate_when_present() {
4064 let dbset = DbSet::<VersionedEntity>::disconnected();
4065
4066 let query = dbset
4067 .delete_query_sql_value(SqlValue::I64(7), Some(SqlValue::Bytes(vec![9, 8, 7])))
4068 .unwrap();
4069
4070 assert_eq!(
4071 query,
4072 DeleteQuery::from_entity::<VersionedEntity>().filter(Predicate::and(vec![
4073 Predicate::eq(
4074 Expr::Column(ColumnRef::new(
4075 TableRef::new("dbo", "versioned_entities"),
4076 "id",
4077 "id",
4078 )),
4079 Expr::Value(SqlValue::I64(7)),
4080 ),
4081 Predicate::eq(
4082 Expr::Column(ColumnRef::new(
4083 TableRef::new("dbo", "versioned_entities"),
4084 "version",
4085 "version",
4086 )),
4087 Expr::Value(SqlValue::Bytes(vec![9, 8, 7])),
4088 ),
4089 ]))
4090 );
4091 }
4092
4093 #[test]
4094 fn dbset_delete_appends_tenant_filter_for_tenant_scoped_entities() {
4095 let dbset = DbSet::<TenantWriteEntity>::disconnected();
4096 let active_tenant = ActiveTenant {
4097 column_name: "tenant_id",
4098 value: SqlValue::I64(42),
4099 };
4100
4101 let query = dbset
4102 .delete_query_sql_value_with_active_tenant(SqlValue::I64(7), None, Some(&active_tenant))
4103 .unwrap();
4104 let compiled = super::SqlServerCompiler::compile_delete(&query).unwrap();
4105
4106 assert_eq!(
4107 compiled.sql,
4108 "DELETE FROM [dbo].[tenant_write_entities] WHERE (([dbo].[tenant_write_entities].[id] = @P1) AND ([dbo].[tenant_write_entities].[tenant_id] = @P2))"
4109 );
4110 assert_eq!(compiled.params, vec![SqlValue::I64(7), SqlValue::I64(42)]);
4111 }
4112
4113 #[test]
4114 fn dbset_delete_compiled_query_uses_physical_delete_for_plain_entities() {
4115 let dbset = DbSet::<TestEntity>::disconnected();
4116
4117 let compiled = dbset
4118 .delete_compiled_query_sql_value(SqlValue::I64(7), None, None, None)
4119 .unwrap();
4120
4121 assert_eq!(
4122 compiled.sql,
4123 "DELETE FROM [dbo].[test_entities] WHERE ([dbo].[test_entities].[id] = @P1)"
4124 );
4125 assert_eq!(compiled.params, vec![SqlValue::I64(7)]);
4126 }
4127
4128 #[test]
4129 fn dbset_delete_compiled_query_uses_update_for_soft_delete_entities() {
4130 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
4131
4132 let provider = TestSoftDeleteProvider;
4133 let compiled = dbset
4134 .delete_compiled_query_sql_value(SqlValue::I64(7), None, Some(&provider), None)
4135 .unwrap();
4136
4137 assert_eq!(
4138 compiled.sql,
4139 "UPDATE [dbo].[soft_delete_entities] SET [deleted_at] = @P1 OUTPUT INSERTED.* WHERE ([dbo].[soft_delete_entities].[id] = @P2)"
4140 );
4141 assert_eq!(
4142 compiled.params,
4143 vec![
4144 SqlValue::String("2026-04-25T00:00:00".to_string()),
4145 SqlValue::I64(7),
4146 ]
4147 );
4148 }
4149
4150 #[test]
4151 fn dbset_delete_compiled_query_appends_rowversion_for_soft_delete_entities() {
4152 let dbset = DbSet::<SoftDeleteVersionedEntity>::disconnected();
4153
4154 let provider = TestSoftDeleteProvider;
4155 let compiled = dbset
4156 .delete_compiled_query_sql_value(
4157 SqlValue::I64(7),
4158 Some(SqlValue::Bytes(vec![9, 8, 7])),
4159 Some(&provider),
4160 None,
4161 )
4162 .unwrap();
4163
4164 assert_eq!(
4165 compiled.sql,
4166 "UPDATE [dbo].[soft_delete_versioned_entities] SET [deleted_at] = @P1 OUTPUT INSERTED.* WHERE (([dbo].[soft_delete_versioned_entities].[id] = @P2) AND ([dbo].[soft_delete_versioned_entities].[version] = @P3))"
4167 );
4168 assert_eq!(
4169 compiled.params,
4170 vec![
4171 SqlValue::String("2026-04-25T00:00:00".to_string()),
4172 SqlValue::I64(7),
4173 SqlValue::Bytes(vec![9, 8, 7]),
4174 ]
4175 );
4176 }
4177
4178 #[test]
4179 fn dbset_soft_delete_appends_tenant_filter_for_tenant_scoped_entities() {
4180 let dbset = DbSet::<TenantWriteEntity>::disconnected();
4181 let provider = TestSoftDeleteProvider;
4182 let active_tenant = ActiveTenant {
4183 column_name: "tenant_id",
4184 value: SqlValue::I64(42),
4185 };
4186
4187 let compiled = dbset
4188 .delete_compiled_query_sql_value_with_active_tenant(
4189 SqlValue::I64(7),
4190 Some(SqlValue::Bytes(vec![9, 8, 7])),
4191 Some(&provider),
4192 None,
4193 Some(&active_tenant),
4194 )
4195 .unwrap();
4196
4197 assert_eq!(
4198 compiled.sql,
4199 "UPDATE [dbo].[tenant_write_entities] SET [deleted_at] = @P1 OUTPUT INSERTED.* WHERE ((([dbo].[tenant_write_entities].[id] = @P2) AND ([dbo].[tenant_write_entities].[tenant_id] = @P3)) AND ([dbo].[tenant_write_entities].[version] = @P4))"
4200 );
4201 assert_eq!(
4202 compiled.params,
4203 vec![
4204 SqlValue::String("2026-04-25T00:00:00".to_string()),
4205 SqlValue::I64(7),
4206 SqlValue::I64(42),
4207 SqlValue::Bytes(vec![9, 8, 7]),
4208 ]
4209 );
4210 }
4211
4212 #[test]
4213 fn dbset_delete_compiled_query_rejects_soft_delete_without_runtime_values() {
4214 let dbset = DbSet::<SoftDeleteEntityUnderTest>::disconnected();
4215
4216 let error = dbset
4217 .delete_compiled_query_sql_value(SqlValue::I64(7), None, None, None)
4218 .unwrap_err();
4219
4220 assert_eq!(
4221 error,
4222 OrmError::new("soft_delete delete requires at least one runtime change")
4223 );
4224 }
4225
4226 #[test]
4227 fn soft_delete_security_guardrail_keeps_schema_and_delete_paths_logical() {
4228 let current = ModelSnapshot::from_entities(&[SoftDeleteEntityUnderTest::metadata()]);
4229 let previous = ModelSnapshot::new(vec![SchemaSnapshot::new(
4230 "dbo",
4231 vec![TableSnapshot::new(
4232 "soft_delete_entities",
4233 vec![
4234 ColumnSnapshot::from(&SOFT_DELETE_ENTITY_COLUMNS[0]),
4235 ColumnSnapshot::from(&SOFT_DELETE_ENTITY_COLUMNS[1]),
4236 ],
4237 None,
4238 vec!["id".to_string()],
4239 vec![],
4240 vec![],
4241 )],
4242 )]);
4243 let schema_operations =
4244 diff_schema_and_table_operations(&ModelSnapshot::default(), ¤t);
4245 let column_operations = diff_column_operations(&previous, ¤t);
4246
4247 let current_schema = current.schema("dbo").expect("dbo schema should exist");
4248 let table = current_schema
4249 .table("soft_delete_entities")
4250 .expect("soft delete table should exist");
4251 let deleted_at = table
4252 .column("deleted_at")
4253 .expect("soft delete column should be ordinary snapshot metadata");
4254
4255 assert_eq!(deleted_at.sql_type, SqlServerType::DateTime2);
4256 assert!(deleted_at.nullable);
4257 assert!(!deleted_at.insertable);
4258 assert!(deleted_at.updatable);
4259 assert!(
4260 schema_operations
4261 .iter()
4262 .any(|operation| matches!(operation, MigrationOperation::CreateTable(operation) if operation.table.name == "soft_delete_entities")),
4263 "soft_delete entities should create tables through the normal migration pipeline"
4264 );
4265 assert!(
4266 column_operations
4267 .iter()
4268 .any(|operation| matches!(operation, MigrationOperation::AddColumn(operation) if operation.column.name == "deleted_at")),
4269 "activating soft_delete should surface generated columns as AddColumn"
4270 );
4271
4272 let provider = TestSoftDeleteProvider;
4273 let compiled = DbSet::<SoftDeleteEntityUnderTest>::disconnected()
4274 .delete_compiled_query_sql_value(SqlValue::I64(7), None, Some(&provider), None)
4275 .expect("soft delete should compile as logical update");
4276
4277 assert!(
4278 compiled.sql.starts_with("UPDATE "),
4279 "soft_delete delete route must compile to UPDATE, got {}",
4280 compiled.sql
4281 );
4282 assert!(
4283 !compiled.sql.starts_with("DELETE "),
4284 "soft_delete delete route must never compile to physical DELETE"
4285 );
4286 assert!(compiled.sql.contains("[deleted_at] = @P1"));
4287 }
4288
4289 #[test]
4290 fn dbset_delete_rejects_composite_primary_keys() {
4291 let dbset = DbSet::<CompositeKeyEntity>::disconnected();
4292
4293 let error = dbset.delete_query(7_i64).unwrap_err();
4294
4295 assert_eq!(
4296 error.message(),
4297 "DbSet currently supports this operation only for entities with a single primary key column"
4298 );
4299 }
4300}