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