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