Skip to main content

sql_orm/
context.rs

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/// Shared database access handle used by contexts, `DbSet`s, raw SQL, and
36/// transactions.
37///
38/// A `SharedConnection` can wrap either one direct SQL Server connection or,
39/// behind the `pool-bb8` feature, a pool. Runtime values such as audit values,
40/// soft-delete values, and the active tenant are stored alongside the physical
41/// connection handle and are preserved when derived contexts clone the handle.
42#[derive(Clone)]
43pub struct SharedConnection {
44    inner: Arc<SharedConnectionInner>,
45    runtime: Arc<SharedConnectionRuntime>,
46    transaction_scope: Option<usize>,
47}
48
49/// Active tenant value currently attached to a shared connection.
50///
51/// Tenant-scoped entities compare their tenant policy column with this value
52/// before compiling reads and writes. A column mismatch or missing tenant fails
53/// closed for tenant-scoped entities.
54#[derive(Debug, Clone, PartialEq)]
55pub struct ActiveTenant {
56    /// Physical tenant column name expected by tenant-scoped entities.
57    pub column_name: &'static str,
58    /// SQL value compared against the tenant column.
59    pub value: SqlValue,
60}
61
62impl ActiveTenant {
63    /// Normalizes a user-defined tenant context into the runtime tenant value.
64    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    /// Guard for a direct connection held through an async mutex.
136    Direct(tokio::sync::MutexGuard<'a, MssqlConnection<TokioConnectionStream>>),
137    #[cfg(feature = "pool-bb8")]
138    /// Guard for one connection acquired from a pool.
139    Pool(Box<MssqlPooledConnection<'a>>),
140    #[cfg(feature = "pool-bb8")]
141    /// Guard for the connection pinned to the active pooled transaction.
142    PinnedPool(tokio::sync::MutexGuard<'a, Option<MssqlPooledConnection<'static>>>),
143}
144
145impl SharedConnection {
146    /// Creates a shared handle from an already-open direct SQL Server
147    /// connection.
148    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    /// Creates a shared handle backed by an `MssqlPool`.
160    ///
161    /// Each operation acquires a pooled connection as needed. Runtime context
162    /// values still live on the `SharedConnection` wrapper, not inside the pool.
163    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    /// Returns a clone of this handle with an audit provider configured.
172    ///
173    /// The provider is consulted by insert/update paths for entities declaring
174    /// `#[orm(audit = Audit)]` after explicit mutation values and request
175    /// values have had priority.
176    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    /// Returns a clone of this handle with low-level audit request values.
196    ///
197    /// Prefer `with_audit_values(...)` when using a struct derived with
198    /// `#[derive(AuditFields)]`.
199    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    /// Returns a clone of this handle with typed audit request values.
219    ///
220    /// The same struct used for `#[derive(AuditFields)]` can be passed here as
221    /// runtime values. Values are converted to `AuditRequestValues` and keep
222    /// the existing precedence rules.
223    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    /// Returns a clone of this handle with audit request values cleared.
228    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    /// Returns a clone of this handle with a soft-delete provider configured.
248    ///
249    /// The provider is used by delete paths for entities declaring
250    /// `#[orm(soft_delete = SoftDelete)]`.
251    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    /// Returns a clone of this handle with low-level soft-delete request
271    /// values.
272    ///
273    /// Prefer `with_soft_delete_values(...)` when using a struct derived with
274    /// `#[derive(SoftDeleteFields)]`.
275    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    /// Returns a clone of this handle with typed soft-delete request values.
295    ///
296    /// The same struct used for `#[derive(SoftDeleteFields)]` can be passed
297    /// here as runtime delete values.
298    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    /// Returns a clone of this handle with soft-delete request values cleared.
305    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    /// Returns a clone of this handle with an active tenant configured.
325    ///
326    /// Tenant-scoped reads and writes fail closed if this tenant is absent,
327    /// has a different column name, or has a value incompatible with the tenant
328    /// column.
329    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    /// Returns a clone of this handle without an active tenant.
349    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    /// Acquires the underlying SQL Server connection for one operation.
369    ///
370    /// Direct connections lock the shared mutex. Pooled connections acquire a
371    /// connection from the pool for the lifetime of the returned guard.
372    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    /// Returns the active tenant attached to this handle, if any.
673    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
733/// Application database context contract.
734///
735/// `#[derive(DbContext)]` implements this trait for structs whose fields are
736/// `DbSet<T>`. The trait centralizes connection access, health checks, raw SQL,
737/// transactions, and `save_changes()` support while keeping SQL
738/// generation in `sql-orm-sqlserver` and execution in `sql-orm-tiberius`.
739pub trait DbContext: Sized {
740    /// Builds a context from an existing shared connection handle.
741    fn from_shared_connection(connection: SharedConnection) -> Self;
742    /// Returns the shared connection handle used by this context.
743    fn shared_connection(&self) -> SharedConnection;
744    #[doc(hidden)]
745    fn tracking_registry(&self) -> TrackingRegistryHandle;
746
747    /// Clears every tracking entry currently registered on this
748    /// context.
749    ///
750    /// This does not execute SQL. Pending inserts, updates and deletes are
751    /// discarded from the unit of work represented by the current tracker.
752    fn clear_tracker(&self) {
753        self.tracking_registry().clear();
754    }
755
756    /// Executes the configured SQL Server health check through the current
757    /// connection handle.
758    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    /// Creates a typed raw SQL query.
768    ///
769    /// Raw SQL is executed exactly as written after ORM parameter handling; it
770    /// does not automatically apply tenant or soft-delete filters.
771    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    /// Creates a raw SQL command for statements that do not materialize rows.
779    fn raw_exec(&self, sql: impl Into<String>) -> RawCommand {
780        RawCommand::new(self.shared_connection(), sql)
781    }
782
783    /// Executes an operation inside a transaction on a direct shared
784    /// connection.
785    ///
786    /// The closure receives a context bound to the same shared connection and
787    /// runtime values. Returning `Ok` commits; returning `Err` rolls back.
788    /// Contexts backed by a pool currently return an error because pooled
789    /// transactions must pin one physical connection for the full closure.
790    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
811/// Gives generic code access to the `DbSet<E>` declared on a context.
812///
813/// `#[derive(DbContext)]` implements this for each entity set field.
814pub trait DbContextEntitySet<E: Entity>: DbContext {
815    /// Returns the typed set for entity `E`.
816    fn db_set(&self) -> &DbSet<E>;
817}
818
819/// Typed entry point for querying and persisting one entity type.
820///
821/// `DbSet<E>` is normally declared as a field on a derived `DbContext`. It
822/// builds query ASTs, applies runtime policies such as tenant and soft-delete
823/// visibility, compiles through the SQL Server crate, and executes through the
824/// shared Tiberius connection handle.
825#[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    /// Creates a set backed by the given shared connection.
834    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    /// Returns the static metadata generated for entity `E`.
860    pub fn entity_metadata(&self) -> &'static EntityMetadata {
861        E::metadata()
862    }
863
864    /// Starts a query for the full entity.
865    ///
866    /// Tenant and soft-delete visibility are materialized when the query is
867    /// compiled or executed, so callers cannot bypass those policies through
868    /// the public query surface.
869    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    /// Starts a query from a caller-provided `SelectQuery`.
878    ///
879    /// This is useful for advanced composition while still routing execution
880    /// through `DbSetQuery`, so mandatory tenant and soft-delete behavior can
881    /// be applied before SQL compilation.
882    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    /// Finds one entity by its single-column primary key.
894    ///
895    /// Composite primary keys are rejected in this stage. Tenant and
896    /// soft-delete policies are applied through the normal query path.
897    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    /// Loads an entity by its single-column primary key and wraps it in the
906    /// snapshot-based tracking container.
907    ///
908    /// The loaded row is registered in this context's tracker using entity
909    /// type, schema, table and primary key value. If that identity already has
910    /// a detached registry entry, the returned wrapper reattaches to the
911    /// registry-owned snapshot instead of creating a duplicate. The public
912    /// first-stable-cut policy allows only one live `Tracked<E>` handle for the
913    /// same persisted identity in one context: loading that identity while
914    /// another wrapper is still attached returns `OrmError`. Composite primary
915    /// keys are rejected with a stable tracking error in the first stable cut.
916    /// Included navigation graphs are not registered automatically; use
917    /// explicit tracking entry points for every entity that should participate
918    /// in `save_changes()`.
919    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    /// Registers a new in-memory entity as tracked in `Added`
941    /// state so a later `save_changes()` can persist it via `insert`.
942    ///
943    /// `Added` entries use a temporary identity until persistence. Entities
944    /// with composite primary keys can be held in memory, but `save_changes()`
945    /// rejects them before executing SQL in the first stable cut. A successful
946    /// tracked insert replaces the temporary identity with the persisted
947    /// single-column primary key returned by SQL Server. Dropping the returned
948    /// wrapper keeps the pending insert in the registry; use explicit detach
949    /// or clear APIs to discard it.
950    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    /// Marks a tracked entity for deletion so a later `save_changes()` can
960    /// persist it through the regular delete pipeline.
961    ///
962    /// Calling this on an `Added` wrapper cancels the pending insert locally:
963    /// the wrapper becomes `Deleted` and is detached from the tracker, so no
964    /// database delete is issued by a later `save_changes()`. Calling this on
965    /// a loaded or modified wrapper marks only that wrapper; relationship
966    /// wrappers are not interpreted as cascade instructions.
967    pub fn remove_tracked(&self, tracked: &mut Tracked<E>) {
968        let was_added = tracked.state() == crate::EntityState::Added;
969        tracked.mark_deleted();
970
971        // Deleting an entity that was never inserted should simply cancel the
972        // pending tracked insert instead of issuing a database delete.
973        if was_added {
974            tracked.detach_registry();
975        }
976    }
977
978    /// Detaches a tracked wrapper from this context's tracker.
979    ///
980    /// Detach does not execute SQL and does not reset the wrapper state. It
981    /// only removes the entry from the context unit of work so later
982    /// `save_changes()` calls ignore it.
983    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(&current).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, &current, 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    /// Inserts a new row and materializes the inserted entity.
1128    ///
1129    /// The insert path applies tenant insert fill/validation and audit runtime
1130    /// values for entities that opt into those policies.
1131    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    /// Updates one row by single-column primary key and materializes the
1145    /// updated entity when a row matched.
1146    ///
1147    /// Rowversion mismatches are surfaced as `OrmError::ConcurrencyConflict`
1148    /// when the entity still exists. Tenant and audit policies are applied by
1149    /// the shared update pipeline.
1150    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    /// Deletes one row by single-column primary key.
1179    ///
1180    /// Entities with `soft_delete` emit an `UPDATE` through the soft-delete
1181    /// pipeline; other entities emit a physical `DELETE`. The return value is
1182    /// `true` when a row was affected.
1183    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    /// Returns the shared connection handle backing this set.
1314    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    /// Explicitly loads a `has_many` collection navigation into an already
1322    /// materialized entity.
1323    ///
1324    /// This performs I/O only at this call site. It does not install lazy
1325    /// loading behavior on the entity or navigation field.
1326    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    /// Explicitly loads a `has_many` collection navigation into a tracked
1351    /// entity without marking it as modified.
1352    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
1975/// Opens a direct SQL Server connection and wraps it in a `SharedConnection`.
1976///
1977/// Derived contexts use this helper behind their generated `connect(...)`
1978/// constructors.
1979pub 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
1984/// Opens a direct SQL Server connection with explicit operational options.
1985pub 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
1994/// Opens a direct SQL Server connection from a fully parsed configuration.
1995pub 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")]
2003/// Wraps an existing SQL Server pool in a `SharedConnection`.
2004pub 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(), &current);
5097        let column_operations = diff_column_operations(&previous, &current);
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}