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