Skip to main content

type_bridge/
transaction.rs

1#![deny(missing_docs)]
2//! Client-owned write transactions and their borrowing exact managers.
3
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use type_bridge_contract::id::{TypeId, is_canonical_thing_iid};
8use type_bridge_orm::_descriptor::{EntityDescriptor, RelationDescriptor};
9use type_bridge_orm::_manager::{DynamicEntityManager, DynamicRelationManager};
10use type_bridge_orm::_registry::DescriptorRegistry;
11use type_bridge_orm::session::backend::TxType;
12use type_bridge_orm::session::context::TransactionContext;
13use type_bridge_orm::{
14    AnswerCancellation, DynamicAttributeMap, DynamicRolePlayerInput, InstalledRuntimeProjection,
15    ProjectedBatchOperation, ProjectedCrudExecutor, QueryExecutionResourceLimits,
16};
17
18use crate::__codegen::{CompleteModel, EntityModel, HydrationCapability, RelationModel};
19use crate::entity_codec::{
20    hydrate_entity, lower_entity_create, map_validation_error, resolve_entity_authority,
21};
22use crate::entity_manager::rehydrate_written_entity;
23use crate::error::{Error, ModelValidationPhase};
24use crate::hooks::{CrudOperation, ModelKind};
25use crate::projected_batch::{
26    create_rows, delete_rows, execute_borrowed_delete, execute_borrowed_things, prepare_batch,
27    update_rows, uses_successor_batch_runtime, validate_binding_row_count,
28};
29use crate::projected_codec::{materialize_projected, project_create};
30use crate::relation_codec::{hydrate_relation, lower_relation_create, resolve_relation_authority};
31use crate::relation_manager::rehydrate_written_relation;
32use crate::schema::Schema;
33use crate::{Database, Result};
34
35#[cfg(test)]
36mod tests;
37
38fn invalid_iid() -> Error {
39    Error::model_validation(
40        ModelValidationPhase::Input,
41        "invalid_iid",
42        vec!["iid".into()],
43        "IID is not canonical",
44        None,
45    )
46}
47
48fn schema_not_bound() -> Error {
49    Error::model_validation(
50        ModelValidationPhase::Input,
51        "schema_not_bound",
52        vec![],
53        "database is not schema-bound",
54        None,
55    )
56}
57
58/// One client-owned reusable read transaction over a schema-bound database.
59///
60/// Query sessions borrow this wrapper and execute every terminal on its one
61/// retained read context. Closing consumes the wrapper without commit
62/// semantics; dropping an open wrapper likewise cannot commit.
63pub struct ReadTransaction<'db, S: Schema> {
64    tx: TransactionContext,
65    db: &'db Database<S>,
66    installed: Arc<InstalledRuntimeProjection>,
67    registry: Arc<DescriptorRegistry>,
68}
69
70impl<S: Schema> std::fmt::Debug for ReadTransaction<'_, S> {
71    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        formatter
73            .debug_struct("ReadTransaction")
74            .field("database", &self.db.database_name())
75            .finish_non_exhaustive()
76    }
77}
78
79impl<'db, S: Schema> ReadTransaction<'db, S> {
80    pub(crate) async fn open(db: &'db Database<S>) -> Result<Self> {
81        let installed = Arc::clone(db.installed_schema().ok_or_else(schema_not_bound)?);
82        let registry = Arc::clone(db.match_registry().ok_or_else(schema_not_bound)?);
83        let tx = db
84            .inner_orm()
85            .transaction_context(TxType::Read)
86            .await
87            .map_err(Error::from_orm)?;
88        Ok(Self {
89            tx,
90            db,
91            installed,
92            registry,
93        })
94    }
95
96    /// Create a read-only exact entity manager borrowing this transaction.
97    /// Every derived filter terminal reuses the retained read context.
98    pub fn entities<M>(&self) -> crate::projected_filter::ReadEntityManager<'_, S, M>
99    where
100        M: crate::__codegen::EntityModel<Schema = S> + crate::__codegen::CompleteModel,
101    {
102        crate::projected_filter::ReadEntityManager::new(&self.installed, &self.tx)
103    }
104
105    /// Create a read-only exact relation manager borrowing this transaction.
106    /// Every derived filter terminal reuses the retained read context.
107    pub fn relations<M>(&self) -> crate::projected_filter::ReadRelationManager<'_, S, M>
108    where
109        M: crate::__codegen::RelationModel<Schema = S> + crate::__codegen::CompleteModel,
110    {
111        crate::projected_filter::ReadRelationManager::new(&self.installed, &self.tx)
112    }
113
114    /// Start one owner-branded query session borrowing this read context.
115    #[must_use]
116    pub fn query(&self) -> crate::query::QuerySession<'_, S> {
117        self.query_with_resources(
118            QueryExecutionResourceLimits::default(),
119            AnswerCancellation::default(),
120        )
121    }
122
123    /// Start one borrowed query session with one common tighten-only resource
124    /// policy and caller-owned cooperative cancellation signal.
125    #[must_use]
126    pub fn query_with_resources(
127        &self,
128        resources: QueryExecutionResourceLimits,
129        cancellation: AnswerCancellation,
130    ) -> crate::query::QuerySession<'_, S> {
131        crate::query::QuerySession::borrowed(
132            &self.installed,
133            Arc::clone(&self.registry),
134            &self.tx,
135            self.db.operation_limits(resources),
136            cancellation,
137        )
138    }
139
140    /// Close this read transaction without committing.
141    pub async fn close(self) -> Result<()> {
142        self.tx.close().await.map_err(Error::from_orm)
143    }
144}
145
146/// One client-owned open write transaction over a schema-bound database.
147///
148/// The wrapper is not cloneable and owns the sole retained engine context.
149/// Manager handles borrow the wrapper, so [`Self::commit`] and
150/// [`Self::rollback`] — which consume it — cannot run while a manager is
151/// retained, and a second terminal operation is unrepresentable. Operations
152/// never auto-commit: an operation error leaves terminal control with the
153/// caller, and dropping an open wrapper releases the context without commit.
154/// Under the successor runtime, a batch failure before mutation dispatch
155/// leaves the transaction active, while a failure after dispatch makes it
156/// rollback-only and [`Self::commit`] returns `transaction_rollback_only`.
157pub struct WriteTransaction<'db, S: Schema> {
158    tx: TransactionContext,
159    db: &'db Database<S>,
160}
161
162impl<S: Schema> std::fmt::Debug for WriteTransaction<'_, S> {
163    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        formatter
165            .debug_struct("WriteTransaction")
166            .field("database", &self.db.database_name())
167            .finish_non_exhaustive()
168    }
169}
170
171impl<'db, S: Schema> WriteTransaction<'db, S> {
172    pub(crate) async fn open(db: &'db Database<S>) -> Result<WriteTransaction<'db, S>> {
173        db.installed_schema().ok_or_else(schema_not_bound)?;
174        let tx = db
175            .inner_orm()
176            .transaction_context(TxType::Write)
177            .await
178            .map_err(Error::from_orm)?;
179        Ok(WriteTransaction { tx, db })
180    }
181
182    /// Create an exact entity manager borrowing this open transaction.
183    pub fn entities<M>(&self) -> TransactionEntityManager<'_, S, M>
184    where
185        M: EntityModel<Schema = S>,
186    {
187        TransactionEntityManager {
188            transaction: self,
189            marker: PhantomData,
190        }
191    }
192
193    /// Create an exact relation manager borrowing this open transaction.
194    pub fn relations<M>(&self) -> TransactionRelationManager<'_, S, M>
195    where
196        M: RelationModel<Schema = S>,
197    {
198        TransactionRelationManager {
199            transaction: self,
200            marker: PhantomData,
201        }
202    }
203
204    /// Commit every operation performed in this transaction, consuming it.
205    pub async fn commit(self) -> Result<()> {
206        if uses_successor_batch_runtime(self.installed()?) {
207            self.tx
208                .commit_sdk()
209                .await
210                .map_err(|error| Error::from_projected_batch(error, ModelValidationPhase::Input))
211        } else {
212            self.tx.commit().await.map_err(Error::from_orm)
213        }
214    }
215
216    /// Roll back every operation performed in this transaction, consuming it.
217    pub async fn rollback(self) -> Result<()> {
218        self.tx.rollback().await.map_err(Error::from_orm)
219    }
220
221    fn installed(&self) -> Result<&InstalledRuntimeProjection> {
222        self.db
223            .installed_schema()
224            .map(Arc::as_ref)
225            .ok_or_else(schema_not_bound)
226    }
227}
228
229/// Schema-bound, model-branded exact entity manager borrowing one open
230/// client write transaction. Operations reuse the shared open context and
231/// never commit, roll back, or close it; errors are returned with the
232/// transaction left open for the caller to decide.
233pub struct TransactionEntityManager<'t, S: Schema, M: EntityModel<Schema = S>> {
234    transaction: &'t WriteTransaction<'t, S>,
235    marker: PhantomData<M>,
236}
237
238impl<'t, S: Schema, M: EntityModel<Schema = S>> Copy for TransactionEntityManager<'t, S, M> {}
239impl<'t, S: Schema, M: EntityModel<Schema = S>> Clone for TransactionEntityManager<'t, S, M> {
240    fn clone(&self) -> Self {
241        *self
242    }
243}
244
245impl<S, M> TransactionEntityManager<'_, S, M>
246where
247    S: Schema,
248    M: EntityModel<Schema = S> + CompleteModel,
249{
250    fn exact(
251        &self,
252    ) -> Result<(
253        TypeId,
254        &InstalledRuntimeProjection,
255        DynamicEntityManager<'static>,
256    )> {
257        let installed = self.transaction.installed()?;
258        let (id, descriptor): (TypeId, EntityDescriptor) = resolve_entity_authority(
259            M::TYPE_ID_JSON,
260            installed,
261            ModelValidationPhase::Input,
262            true,
263        )?;
264        let manager = DynamicEntityManager::with_canonical_transaction(
265            self.transaction.tx.clone(),
266            Arc::new(descriptor),
267        );
268        Ok((id, installed, manager))
269    }
270
271    /// Inserts one exact entity in the open transaction and returns its
272    /// complete freshly hydrated model without committing.
273    pub async fn insert(&self, input: M::Create) -> Result<M> {
274        let (id, installed, _manager) = self.exact()?;
275        let create = project_create(input, &id, installed)?;
276        let projected = ProjectedCrudExecutor::new(installed)
277            .insert_entity_in_transaction_with_compatibility(&self.transaction.tx, &create)
278            .await
279            .map_err(|error| {
280                Error::from_projected_crud(error, ModelKind::Entity, Some(CrudOperation::Insert))
281            })?;
282        materialize_projected(projected, installed)
283    }
284
285    /// Applies the exact key-or-insert put rule in the open transaction,
286    /// including complete replacement of non-key ownership for an existing
287    /// exact row, without committing.
288    pub async fn put(&self, input: M::Create) -> Result<M> {
289        let (id, installed, _manager) = self.exact()?;
290        let create = project_create(input, &id, installed)?;
291        let projected = ProjectedCrudExecutor::new(installed)
292            .put_entity_in_transaction_with_compatibility(&self.transaction.tx, &create)
293            .await
294            .map_err(|error| {
295                Error::from_projected_crud(error, ModelKind::Entity, Some(CrudOperation::Put))
296            })?;
297        materialize_projected(projected, installed)
298    }
299
300    /// Inserts each item in input order in the open transaction, returning
301    /// complete freshly hydrated models or one error, without committing.
302    /// A successor-runtime failure after mutation dispatch makes the
303    /// transaction rollback-only; a pre-dispatch failure leaves it active.
304    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
305        if uses_successor_batch_runtime(self.transaction.installed()?) {
306            validate_binding_row_count(inputs.len())?;
307        }
308        self.write_many(inputs, false).await
309    }
310
311    /// Applies the per-item put rule in input order in the open transaction,
312    /// returning complete freshly hydrated models or one error, without
313    /// committing. A successor-runtime failure after mutation dispatch makes
314    /// the transaction rollback-only; a pre-dispatch failure leaves it active.
315    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
316        if uses_successor_batch_runtime(self.transaction.installed()?) {
317            validate_binding_row_count(inputs.len())?;
318        }
319        self.write_many(inputs, true).await
320    }
321
322    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
323        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
324        if inputs.is_empty() && !successor {
325            return Ok(Vec::new());
326        }
327        let (id, installed, manager) = self.exact()?;
328        if successor {
329            let rows = create_rows(installed, &id, inputs)?;
330            let operation = if put {
331                ProjectedBatchOperation::Put
332            } else {
333                ProjectedBatchOperation::Insert
334            };
335            let batch = prepare_batch(installed, id, operation, rows)?;
336            return execute_borrowed_things(&self.transaction.tx, installed, &batch).await;
337        }
338        let mut lowered = Vec::with_capacity(inputs.len());
339        for input in inputs {
340            lowered.push(lower_entity_create(input, &id, installed)?);
341        }
342        let iids = if put {
343            manager.put_many_exact(&lowered).await
344        } else {
345            manager.insert_many(&lowered).await
346        }
347        .map_err(Error::from_orm)?;
348        if iids.len() != lowered.len() {
349            return Err(Error::model_validation(
350                ModelValidationPhase::Hydration,
351                "iid_count_mismatch",
352                vec!["iid".into()],
353                "provider returned an unexpected IID count",
354                None,
355            ));
356        }
357        let mut out = Vec::with_capacity(iids.len());
358        for iid in iids {
359            out.push(rehydrate_written_entity(&manager, &iid, &id, installed).await?);
360        }
361        Ok(out)
362    }
363
364    /// Completely replaces non-key ownership on the exact model at canonical
365    /// `iid` in the open transaction, preserving that IID, without committing.
366    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
367        if !is_canonical_thing_iid(iid) {
368            return Err(invalid_iid());
369        }
370        let (id, installed, _manager) = self.exact()?;
371        let create = project_create(input, &id, installed)?;
372        let projected = ProjectedCrudExecutor::new(installed)
373            .update_entity_in_transaction_with_compatibility(&self.transaction.tx, iid, &create)
374            .await
375            .map_err(|error| {
376                Error::from_projected_crud(error, ModelKind::Entity, Some(CrudOperation::Update))
377            })?;
378        materialize_projected(projected, installed)
379    }
380
381    /// Deletes only the exact model at canonical `iid` in the open
382    /// transaction, without committing.
383    pub async fn delete(&self, iid: &str) -> Result<()> {
384        if !is_canonical_thing_iid(iid) {
385            return Err(invalid_iid());
386        }
387        let (id, installed, _manager) = self.exact()?;
388        ProjectedCrudExecutor::new(installed)
389            .delete_entity_by_iid_in_transaction_with_compatibility(&self.transaction.tx, &id, iid)
390            .await
391            .map_err(|error| {
392                Error::from_projected_crud(error, ModelKind::Entity, Some(CrudOperation::Delete))
393            })
394    }
395
396    /// Replaces each exact entity identified by its canonical IID in input
397    /// order without committing. Successor-runtime failures after mutation
398    /// dispatch make the transaction rollback-only.
399    pub async fn update_many(&self, inputs: Vec<(String, M::Create)>) -> Result<Vec<M>> {
400        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
401        if successor {
402            validate_binding_row_count(inputs.len())?;
403        }
404        if inputs.is_empty() && !successor {
405            return Ok(Vec::new());
406        }
407        if successor {
408            let (id, installed, _manager) = self.exact()?;
409            let rows = update_rows(installed, &id, inputs)?;
410            let batch = prepare_batch(installed, id, ProjectedBatchOperation::Update, rows)?;
411            return execute_borrowed_things(&self.transaction.tx, installed, &batch).await;
412        }
413        if inputs.iter().any(|(iid, _)| !is_canonical_thing_iid(iid)) {
414            return Err(invalid_iid());
415        }
416        let (id, installed, manager) = self.exact()?;
417
418        let mut prepared = Vec::with_capacity(inputs.len());
419        for (iid, input) in inputs {
420            prepared.push((iid, lower_entity_create(input, &id, installed)?));
421        }
422        let mut output = Vec::with_capacity(prepared.len());
423        for (iid, attributes) in prepared {
424            manager
425                .update_exact(&iid, &attributes)
426                .await
427                .map_err(Error::from_orm)?;
428            output.push(rehydrate_written_entity(&manager, &iid, &id, installed).await?);
429        }
430        Ok(output)
431    }
432
433    /// Deletes every exact entity at the supplied canonical IIDs without
434    /// committing. Successor-runtime failures after mutation dispatch make
435    /// the transaction rollback-only.
436    pub async fn delete_many(&self, iids: &[String]) -> Result<()> {
437        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
438        if successor {
439            validate_binding_row_count(iids.len())?;
440        }
441        if iids.is_empty() && !successor {
442            return Ok(());
443        }
444        if successor {
445            let (id, installed, _manager) = self.exact()?;
446            let batch = prepare_batch(
447                installed,
448                id,
449                ProjectedBatchOperation::Delete,
450                delete_rows(iids)?,
451            )?;
452            return execute_borrowed_delete(&self.transaction.tx, installed, &batch).await;
453        }
454        if iids.iter().any(|iid| !is_canonical_thing_iid(iid)) {
455            return Err(invalid_iid());
456        }
457        let (_id, _installed, manager) = self.exact()?;
458        for iid in iids {
459            manager
460                .delete_by_iid_exact(iid)
461                .await
462                .map_err(Error::from_orm)?;
463        }
464        Ok(())
465    }
466
467    /// Reads one exact model by canonical IID through the open transaction,
468    /// observing this transaction's uncommitted writes.
469    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
470        if !is_canonical_thing_iid(iid) {
471            return Err(invalid_iid());
472        }
473        let (id, installed, _manager) = self.exact()?;
474        ProjectedCrudExecutor::new(installed)
475            .get_entity_by_iid_in_transaction_with_compatibility(&self.transaction.tx, &id, iid)
476            .await
477            .map_err(|error| Error::from_projected_crud(error, ModelKind::Entity, None))?
478            .map(|projected| materialize_projected(projected, installed))
479            .transpose()
480    }
481
482    /// Reads all exact models through the open transaction, observing this
483    /// transaction's uncommitted writes.
484    pub async fn all(&self) -> Result<Vec<M>> {
485        let (id, installed, manager) = self.exact()?;
486        let rows = manager.all_exact().await.map_err(Error::from_orm)?;
487        rows.into_iter()
488            .map(|row| {
489                let hydrated = hydrate_entity(row, &id, installed)?;
490                M::materialize(&hydrated, &HydrationCapability::new())
491                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
492            })
493            .collect()
494    }
495
496    /// Counts only exact models through the open transaction.
497    pub async fn count(&self) -> Result<u64> {
498        let (id, installed, _manager) = self.exact()?;
499        ProjectedCrudExecutor::new(installed)
500            .count_entities_in_transaction_with_compatibility(&self.transaction.tx, &id)
501            .await
502            .map_err(|error| Error::from_projected_crud(error, ModelKind::Entity, None))
503    }
504}
505
506/// Schema-bound, model-branded exact relation manager borrowing one open
507/// client write transaction. Operations reuse the shared open context and
508/// never commit, roll back, or close it; errors are returned with the
509/// transaction left open for the caller to decide.
510pub struct TransactionRelationManager<'t, S: Schema, M: RelationModel<Schema = S>> {
511    transaction: &'t WriteTransaction<'t, S>,
512    marker: PhantomData<M>,
513}
514
515impl<'t, S: Schema, M: RelationModel<Schema = S>> Copy for TransactionRelationManager<'t, S, M> {}
516impl<'t, S: Schema, M: RelationModel<Schema = S>> Clone for TransactionRelationManager<'t, S, M> {
517    fn clone(&self) -> Self {
518        *self
519    }
520}
521
522impl<S, M> TransactionRelationManager<'_, S, M>
523where
524    S: Schema,
525    M: RelationModel<Schema = S> + CompleteModel,
526{
527    fn exact(
528        &self,
529    ) -> Result<(
530        TypeId,
531        &InstalledRuntimeProjection,
532        DynamicRelationManager<'static>,
533    )> {
534        let installed = self.transaction.installed()?;
535        let (id, descriptor): (TypeId, RelationDescriptor) = resolve_relation_authority(
536            M::TYPE_ID_JSON,
537            installed,
538            ModelValidationPhase::Input,
539            true,
540        )?;
541        let manager = DynamicRelationManager::with_canonical_transaction(
542            self.transaction.tx.clone(),
543            Arc::new(descriptor),
544        );
545        Ok((id, installed, manager))
546    }
547
548    /// Inserts one exact relation with its complete active role players in the
549    /// open transaction and returns its complete freshly hydrated model
550    /// without committing.
551    pub async fn insert(&self, input: M::Create) -> Result<M> {
552        let (id, installed, _manager) = self.exact()?;
553        let create = project_create(input, &id, installed)?;
554        let projected = ProjectedCrudExecutor::new(installed)
555            .insert_relation_in_transaction_with_compatibility(&self.transaction.tx, &create)
556            .await
557            .map_err(|error| {
558                Error::from_projected_crud(error, ModelKind::Relation, Some(CrudOperation::Insert))
559            })?;
560        materialize_projected(projected, installed)
561    }
562
563    /// Applies the exact key-or-insert put rule in the open transaction,
564    /// including complete replacement of non-key ownership and active role
565    /// players for an existing exact row, without committing.
566    pub async fn put(&self, input: M::Create) -> Result<M> {
567        let (id, installed, _manager) = self.exact()?;
568        let create = project_create(input, &id, installed)?;
569        let projected = ProjectedCrudExecutor::new(installed)
570            .put_relation_in_transaction_with_compatibility(&self.transaction.tx, &create)
571            .await
572            .map_err(|error| {
573                Error::from_projected_crud(error, ModelKind::Relation, Some(CrudOperation::Put))
574            })?;
575        materialize_projected(projected, installed)
576    }
577
578    /// Inserts each item in input order in the open transaction, returning
579    /// complete freshly hydrated models or one error, without committing.
580    /// A successor-runtime failure after mutation dispatch makes the
581    /// transaction rollback-only; a pre-dispatch failure leaves it active.
582    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
583        if uses_successor_batch_runtime(self.transaction.installed()?) {
584            validate_binding_row_count(inputs.len())?;
585        }
586        self.write_many(inputs, false).await
587    }
588
589    /// Applies the per-item put rule in input order in the open transaction,
590    /// returning complete freshly hydrated models or one error, without
591    /// committing. A successor-runtime failure after mutation dispatch makes
592    /// the transaction rollback-only; a pre-dispatch failure leaves it active.
593    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
594        if uses_successor_batch_runtime(self.transaction.installed()?) {
595            validate_binding_row_count(inputs.len())?;
596        }
597        self.write_many(inputs, true).await
598    }
599
600    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
601        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
602        if inputs.is_empty() && !successor {
603            return Ok(Vec::new());
604        }
605        let (id, installed, manager) = self.exact()?;
606        if successor {
607            let rows = create_rows(installed, &id, inputs)?;
608            let operation = if put {
609                ProjectedBatchOperation::Put
610            } else {
611                ProjectedBatchOperation::Insert
612            };
613            let batch = prepare_batch(installed, id, operation, rows)?;
614            return execute_borrowed_things(&self.transaction.tx, installed, &batch).await;
615        }
616        let mut lowered: Vec<(DynamicAttributeMap, Vec<DynamicRolePlayerInput>)> =
617            Vec::with_capacity(inputs.len());
618        for input in inputs {
619            let prepared = lower_relation_create(input, &id, installed)?;
620            lowered.push((prepared.attributes, prepared.role_players));
621        }
622        let iids = if put {
623            manager.put_many_exact(&lowered).await
624        } else {
625            manager.insert_many(&lowered).await
626        }
627        .map_err(Error::from_orm)?;
628        if iids.len() != lowered.len() {
629            return Err(Error::model_validation(
630                ModelValidationPhase::Hydration,
631                "iid_count_mismatch",
632                vec!["iid".into()],
633                "provider returned an unexpected IID count",
634                None,
635            ));
636        }
637        let mut out = Vec::with_capacity(iids.len());
638        for iid in iids {
639            out.push(rehydrate_written_relation(&manager, &iid, &id, installed).await?);
640        }
641        Ok(out)
642    }
643
644    /// Completely replaces non-key ownership and the complete effective
645    /// active-role player set on the exact relation at canonical `iid` in the
646    /// open transaction, preserving that IID, without committing.
647    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
648        if !is_canonical_thing_iid(iid) {
649            return Err(invalid_iid());
650        }
651        let (id, installed, _manager) = self.exact()?;
652        let create = project_create(input, &id, installed)?;
653        let projected = ProjectedCrudExecutor::new(installed)
654            .update_relation_in_transaction_with_compatibility(&self.transaction.tx, iid, &create)
655            .await
656            .map_err(|error| {
657                Error::from_projected_crud(error, ModelKind::Relation, Some(CrudOperation::Update))
658            })?;
659        materialize_projected(projected, installed)
660    }
661
662    /// Deletes only the exact relation at canonical `iid` in the open
663    /// transaction, without committing.
664    pub async fn delete(&self, iid: &str) -> Result<()> {
665        if !is_canonical_thing_iid(iid) {
666            return Err(invalid_iid());
667        }
668        let (id, installed, _manager) = self.exact()?;
669        ProjectedCrudExecutor::new(installed)
670            .delete_relation_by_iid_in_transaction_with_compatibility(
671                &self.transaction.tx,
672                &id,
673                iid,
674            )
675            .await
676            .map_err(|error| {
677                Error::from_projected_crud(error, ModelKind::Relation, Some(CrudOperation::Delete))
678            })
679    }
680
681    /// Replaces each exact relation identified by its canonical IID in input
682    /// order without committing. Successor-runtime failures after mutation
683    /// dispatch make the transaction rollback-only.
684    pub async fn update_many(&self, inputs: Vec<(String, M::Create)>) -> Result<Vec<M>> {
685        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
686        if successor {
687            validate_binding_row_count(inputs.len())?;
688        }
689        if inputs.is_empty() && !successor {
690            return Ok(Vec::new());
691        }
692        if successor {
693            let (id, installed, _manager) = self.exact()?;
694            let rows = update_rows(installed, &id, inputs)?;
695            let batch = prepare_batch(installed, id, ProjectedBatchOperation::Update, rows)?;
696            return execute_borrowed_things(&self.transaction.tx, installed, &batch).await;
697        }
698        if inputs.iter().any(|(iid, _)| !is_canonical_thing_iid(iid)) {
699            return Err(invalid_iid());
700        }
701        let (id, installed, manager) = self.exact()?;
702
703        let mut prepared = Vec::with_capacity(inputs.len());
704        for (iid, input) in inputs {
705            prepared.push((iid, lower_relation_create(input, &id, installed)?));
706        }
707        let mut output = Vec::with_capacity(prepared.len());
708        for (iid, replacement) in prepared {
709            manager
710                .update_exact(&iid, &replacement.attributes, &replacement.role_players)
711                .await
712                .map_err(Error::from_orm)?;
713            output.push(rehydrate_written_relation(&manager, &iid, &id, installed).await?);
714        }
715        Ok(output)
716    }
717
718    /// Deletes every exact relation at the supplied canonical IIDs without
719    /// committing. Successor-runtime failures after mutation dispatch make
720    /// the transaction rollback-only.
721    pub async fn delete_many(&self, iids: &[String]) -> Result<()> {
722        let successor = uses_successor_batch_runtime(self.transaction.installed()?);
723        if successor {
724            validate_binding_row_count(iids.len())?;
725        }
726        if iids.is_empty() && !successor {
727            return Ok(());
728        }
729        if successor {
730            let (id, installed, _manager) = self.exact()?;
731            let batch = prepare_batch(
732                installed,
733                id,
734                ProjectedBatchOperation::Delete,
735                delete_rows(iids)?,
736            )?;
737            return execute_borrowed_delete(&self.transaction.tx, installed, &batch).await;
738        }
739        if iids.iter().any(|iid| !is_canonical_thing_iid(iid)) {
740            return Err(invalid_iid());
741        }
742        let (_id, _installed, manager) = self.exact()?;
743        for iid in iids {
744            manager
745                .delete_by_iid_exact(iid)
746                .await
747                .map_err(Error::from_orm)?;
748        }
749        Ok(())
750    }
751
752    /// Reads one exact coalesced relation by canonical IID through the open
753    /// transaction, observing this transaction's uncommitted writes.
754    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
755        if !is_canonical_thing_iid(iid) {
756            return Err(invalid_iid());
757        }
758        let (id, installed, _manager) = self.exact()?;
759        ProjectedCrudExecutor::new(installed)
760            .get_relation_by_iid_in_transaction_with_compatibility(&self.transaction.tx, &id, iid)
761            .await
762            .map_err(|error| Error::from_projected_crud(error, ModelKind::Relation, None))?
763            .map(|projected| materialize_projected(projected, installed))
764            .transpose()
765    }
766
767    /// Reads all exact coalesced relations through the open transaction,
768    /// observing this transaction's uncommitted writes.
769    pub async fn all(&self) -> Result<Vec<M>> {
770        let (id, installed, manager) = self.exact()?;
771        let rows = manager.all_exact().await.map_err(Error::from_orm)?;
772        rows.into_iter()
773            .map(|row| {
774                let hydrated = hydrate_relation(row, &id, installed)?;
775                M::materialize(&hydrated, &HydrationCapability::new())
776                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
777            })
778            .collect()
779    }
780
781    /// Counts only exact relations through the open transaction.
782    pub async fn count(&self) -> Result<u64> {
783        let (id, installed, _manager) = self.exact()?;
784        ProjectedCrudExecutor::new(installed)
785            .count_relations_in_transaction_with_compatibility(&self.transaction.tx, &id)
786            .await
787            .map_err(|error| Error::from_projected_crud(error, ModelKind::Relation, None))
788    }
789}