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