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::manager::{DynamicEntityManager, DynamicRelationManager};
9use type_bridge_orm::session::backend::TxType;
10use type_bridge_orm::session::context::TransactionContext;
11use type_bridge_orm::{
12    DescriptorRegistry, DynamicAttributeMap, DynamicRolePlayerInput, EntityDescriptor,
13    InstalledRuntimeProjection, RelationDescriptor,
14};
15
16use crate::__codegen::{CompleteModel, EntityModel, HydrationCapability, RelationModel};
17use crate::entity_codec::{
18    hydrate_entity, lower_entity_create, map_validation_error, resolve_entity_authority,
19};
20use crate::entity_manager::rehydrate_written_entity;
21use crate::error::{Error, ModelValidationPhase};
22use crate::relation_codec::{hydrate_relation, lower_relation_create, resolve_relation_authority};
23use crate::relation_manager::{one_coalesced_row, rehydrate_written_relation};
24use crate::schema::Schema;
25use crate::{Database, Result};
26
27#[cfg(test)]
28mod tests;
29
30fn invalid_iid() -> Error {
31    Error::model_validation(
32        ModelValidationPhase::Input,
33        "invalid_iid",
34        vec!["iid".into()],
35        "IID is not canonical",
36        None,
37    )
38}
39
40fn schema_not_bound() -> Error {
41    Error::model_validation(
42        ModelValidationPhase::Input,
43        "schema_not_bound",
44        vec![],
45        "database is not schema-bound",
46        None,
47    )
48}
49
50/// One client-owned reusable read transaction over a schema-bound database.
51///
52/// Query sessions borrow this wrapper and execute every terminal on its one
53/// retained read context. Closing consumes the wrapper without commit
54/// semantics; dropping an open wrapper likewise cannot commit.
55pub struct ReadTransaction<'db, S: Schema> {
56    tx: TransactionContext,
57    db: &'db Database<S>,
58    installed: Arc<InstalledRuntimeProjection>,
59    registry: Arc<DescriptorRegistry>,
60}
61
62impl<S: Schema> std::fmt::Debug for ReadTransaction<'_, S> {
63    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        formatter
65            .debug_struct("ReadTransaction")
66            .field("database", &self.db.database_name())
67            .finish_non_exhaustive()
68    }
69}
70
71impl<'db, S: Schema> ReadTransaction<'db, S> {
72    pub(crate) async fn open(db: &'db Database<S>) -> Result<Self> {
73        let installed = Arc::clone(db.installed_schema().ok_or_else(schema_not_bound)?);
74        let registry = Arc::clone(db.match_registry().ok_or_else(schema_not_bound)?);
75        let tx = db
76            .inner_orm()
77            .transaction_context(TxType::Read)
78            .await
79            .map_err(Error::from_orm)?;
80        Ok(Self {
81            tx,
82            db,
83            installed,
84            registry,
85        })
86    }
87
88    /// Start one owner-branded query session borrowing this read context.
89    #[must_use]
90    pub fn query(&self) -> crate::query::QuerySession<'_, S> {
91        crate::query::QuerySession::borrowed(&self.installed, Arc::clone(&self.registry), &self.tx)
92    }
93
94    /// Close this read transaction without committing.
95    pub async fn close(self) -> Result<()> {
96        self.tx.close().await.map_err(Error::from_orm)
97    }
98}
99
100/// One client-owned open write transaction over a schema-bound database.
101///
102/// The wrapper is not cloneable and owns the sole retained engine context.
103/// Manager handles borrow the wrapper, so [`Self::commit`] and
104/// [`Self::rollback`] — which consume it — cannot run while a manager is
105/// retained, and a second terminal operation is unrepresentable. Operations
106/// never auto-commit: an operation error leaves terminal control with the
107/// caller, and dropping an open wrapper releases the context without commit.
108pub struct WriteTransaction<'db, S: Schema> {
109    tx: TransactionContext,
110    db: &'db Database<S>,
111}
112
113impl<S: Schema> std::fmt::Debug for WriteTransaction<'_, S> {
114    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        formatter
116            .debug_struct("WriteTransaction")
117            .field("database", &self.db.database_name())
118            .finish_non_exhaustive()
119    }
120}
121
122impl<'db, S: Schema> WriteTransaction<'db, S> {
123    pub(crate) async fn open(db: &'db Database<S>) -> Result<WriteTransaction<'db, S>> {
124        db.installed_schema().ok_or_else(schema_not_bound)?;
125        let tx = db
126            .inner_orm()
127            .transaction_context(TxType::Write)
128            .await
129            .map_err(Error::from_orm)?;
130        Ok(WriteTransaction { tx, db })
131    }
132
133    /// Create an exact entity manager borrowing this open transaction.
134    pub fn entities<M>(&self) -> TransactionEntityManager<'_, S, M>
135    where
136        M: EntityModel<Schema = S>,
137    {
138        TransactionEntityManager {
139            transaction: self,
140            marker: PhantomData,
141        }
142    }
143
144    /// Create an exact relation manager borrowing this open transaction.
145    pub fn relations<M>(&self) -> TransactionRelationManager<'_, S, M>
146    where
147        M: RelationModel<Schema = S>,
148    {
149        TransactionRelationManager {
150            transaction: self,
151            marker: PhantomData,
152        }
153    }
154
155    /// Commit every operation performed in this transaction, consuming it.
156    pub async fn commit(self) -> Result<()> {
157        self.tx.commit().await.map_err(Error::from_orm)
158    }
159
160    /// Roll back every operation performed in this transaction, consuming it.
161    pub async fn rollback(self) -> Result<()> {
162        self.tx.rollback().await.map_err(Error::from_orm)
163    }
164
165    fn installed(&self) -> Result<&InstalledRuntimeProjection> {
166        self.db
167            .installed_schema()
168            .map(Arc::as_ref)
169            .ok_or_else(schema_not_bound)
170    }
171}
172
173/// Schema-bound, model-branded exact entity manager borrowing one open
174/// client write transaction. Operations reuse the shared open context and
175/// never commit, roll back, or close it; errors are returned with the
176/// transaction left open for the caller to decide.
177pub struct TransactionEntityManager<'t, S: Schema, M: EntityModel<Schema = S>> {
178    transaction: &'t WriteTransaction<'t, S>,
179    marker: PhantomData<M>,
180}
181
182impl<'t, S: Schema, M: EntityModel<Schema = S>> Copy for TransactionEntityManager<'t, S, M> {}
183impl<'t, S: Schema, M: EntityModel<Schema = S>> Clone for TransactionEntityManager<'t, S, M> {
184    fn clone(&self) -> Self {
185        *self
186    }
187}
188
189impl<S, M> TransactionEntityManager<'_, S, M>
190where
191    S: Schema,
192    M: EntityModel<Schema = S> + CompleteModel,
193{
194    fn exact(
195        &self,
196    ) -> Result<(
197        TypeId,
198        &InstalledRuntimeProjection,
199        DynamicEntityManager<'static>,
200    )> {
201        let installed = self.transaction.installed()?;
202        let (id, descriptor): (TypeId, EntityDescriptor) = resolve_entity_authority(
203            M::TYPE_ID_JSON,
204            installed,
205            ModelValidationPhase::Input,
206            true,
207        )?;
208        let manager = DynamicEntityManager::with_canonical_transaction(
209            self.transaction.tx.clone(),
210            Arc::new(descriptor),
211        );
212        Ok((id, installed, manager))
213    }
214
215    /// Inserts one exact entity in the open transaction and returns its
216    /// complete freshly hydrated model without committing.
217    pub async fn insert(&self, input: M::Create) -> Result<M> {
218        let (id, installed, manager) = self.exact()?;
219        let attributes = lower_entity_create(input, &id, installed)?;
220        let iid = manager.insert(&attributes).await.map_err(Error::from_orm)?;
221        rehydrate_written_entity(&manager, &iid, &id, installed).await
222    }
223
224    /// Applies the exact key-or-insert put rule in the open transaction,
225    /// including complete replacement of non-key ownership for an existing
226    /// exact row, without committing.
227    pub async fn put(&self, input: M::Create) -> Result<M> {
228        let (id, installed, manager) = self.exact()?;
229        let attributes = lower_entity_create(input, &id, installed)?;
230        let iid = manager
231            .put_exact(&attributes)
232            .await
233            .map_err(Error::from_orm)?;
234        rehydrate_written_entity(&manager, &iid, &id, installed).await
235    }
236
237    /// Inserts each item in input order in the open transaction, returning
238    /// complete freshly hydrated models or one error, without committing.
239    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
240        self.write_many(inputs, false).await
241    }
242
243    /// Applies the per-item put rule in input order in the open transaction,
244    /// returning complete freshly hydrated models or one error, without
245    /// committing.
246    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
247        self.write_many(inputs, true).await
248    }
249
250    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
251        if inputs.is_empty() {
252            return Ok(Vec::new());
253        }
254        let (id, installed, manager) = self.exact()?;
255        let mut lowered = Vec::with_capacity(inputs.len());
256        for input in inputs {
257            lowered.push(lower_entity_create(input, &id, installed)?);
258        }
259        let iids = if put {
260            manager.put_many_exact(&lowered).await
261        } else {
262            manager.insert_many(&lowered).await
263        }
264        .map_err(Error::from_orm)?;
265        if iids.len() != lowered.len() {
266            return Err(Error::model_validation(
267                ModelValidationPhase::Hydration,
268                "iid_count_mismatch",
269                vec!["iid".into()],
270                "provider returned an unexpected IID count",
271                None,
272            ));
273        }
274        let mut out = Vec::with_capacity(iids.len());
275        for iid in iids {
276            out.push(rehydrate_written_entity(&manager, &iid, &id, installed).await?);
277        }
278        Ok(out)
279    }
280
281    /// Completely replaces non-key ownership on the exact model at canonical
282    /// `iid` in the open transaction, preserving that IID, without committing.
283    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
284        if !is_canonical_thing_iid(iid) {
285            return Err(invalid_iid());
286        }
287        let (id, installed, manager) = self.exact()?;
288        let attributes = lower_entity_create(input, &id, installed)?;
289        manager
290            .update_exact(iid, &attributes)
291            .await
292            .map_err(Error::from_orm)?;
293        rehydrate_written_entity(&manager, iid, &id, installed).await
294    }
295
296    /// Deletes only the exact model at canonical `iid` in the open
297    /// transaction, without committing.
298    pub async fn delete(&self, iid: &str) -> Result<()> {
299        if !is_canonical_thing_iid(iid) {
300            return Err(invalid_iid());
301        }
302        let (_id, _installed, manager) = self.exact()?;
303        manager
304            .delete_by_iid_exact(iid)
305            .await
306            .map_err(Error::from_orm)
307    }
308
309    /// Reads one exact model by canonical IID through the open transaction,
310    /// observing this transaction's uncommitted writes.
311    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
312        if !is_canonical_thing_iid(iid) {
313            return Err(invalid_iid());
314        }
315        let (id, installed, manager) = self.exact()?;
316        match manager
317            .get_by_iid_exact(iid)
318            .await
319            .map_err(Error::from_orm)?
320        {
321            None => Ok(None),
322            Some(row) => {
323                let hydrated = hydrate_entity(row, &id, installed)?;
324                M::materialize(&hydrated, &HydrationCapability::new())
325                    .map(Some)
326                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
327            }
328        }
329    }
330
331    /// Reads all exact models through the open transaction, observing this
332    /// transaction's uncommitted writes.
333    pub async fn all(&self) -> Result<Vec<M>> {
334        let (id, installed, manager) = self.exact()?;
335        let rows = manager.all_exact().await.map_err(Error::from_orm)?;
336        rows.into_iter()
337            .map(|row| {
338                let hydrated = hydrate_entity(row, &id, installed)?;
339                M::materialize(&hydrated, &HydrationCapability::new())
340                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
341            })
342            .collect()
343    }
344
345    /// Counts only exact models through the open transaction.
346    pub async fn count(&self) -> Result<u64> {
347        let (_id, _installed, manager) = self.exact()?;
348        manager.count_exact().await.map_err(Error::from_orm)
349    }
350}
351
352/// Schema-bound, model-branded exact relation manager borrowing one open
353/// client write transaction. Operations reuse the shared open context and
354/// never commit, roll back, or close it; errors are returned with the
355/// transaction left open for the caller to decide.
356pub struct TransactionRelationManager<'t, S: Schema, M: RelationModel<Schema = S>> {
357    transaction: &'t WriteTransaction<'t, S>,
358    marker: PhantomData<M>,
359}
360
361impl<'t, S: Schema, M: RelationModel<Schema = S>> Copy for TransactionRelationManager<'t, S, M> {}
362impl<'t, S: Schema, M: RelationModel<Schema = S>> Clone for TransactionRelationManager<'t, S, M> {
363    fn clone(&self) -> Self {
364        *self
365    }
366}
367
368impl<S, M> TransactionRelationManager<'_, S, M>
369where
370    S: Schema,
371    M: RelationModel<Schema = S> + CompleteModel,
372{
373    fn exact(
374        &self,
375    ) -> Result<(
376        TypeId,
377        &InstalledRuntimeProjection,
378        DynamicRelationManager<'static>,
379    )> {
380        let installed = self.transaction.installed()?;
381        let (id, descriptor): (TypeId, RelationDescriptor) = resolve_relation_authority(
382            M::TYPE_ID_JSON,
383            installed,
384            ModelValidationPhase::Input,
385            true,
386        )?;
387        let manager = DynamicRelationManager::with_canonical_transaction(
388            self.transaction.tx.clone(),
389            Arc::new(descriptor),
390        );
391        Ok((id, installed, manager))
392    }
393
394    /// Inserts one exact relation with its complete active role players in the
395    /// open transaction and returns its complete freshly hydrated model
396    /// without committing.
397    pub async fn insert(&self, input: M::Create) -> Result<M> {
398        let (id, installed, manager) = self.exact()?;
399        let prepared = lower_relation_create(input, &id, installed)?;
400        let iid = manager
401            .insert(&prepared.attributes, &prepared.role_players)
402            .await
403            .map_err(Error::from_orm)?;
404        rehydrate_written_relation(&manager, &iid, &id, installed).await
405    }
406
407    /// Applies the exact key-or-insert put rule in the open transaction,
408    /// including complete replacement of non-key ownership and active role
409    /// players for an existing exact row, without committing.
410    pub async fn put(&self, input: M::Create) -> Result<M> {
411        let (id, installed, manager) = self.exact()?;
412        let prepared = lower_relation_create(input, &id, installed)?;
413        let iid = manager
414            .put_exact(&prepared.attributes, &prepared.role_players)
415            .await
416            .map_err(Error::from_orm)?;
417        rehydrate_written_relation(&manager, &iid, &id, installed).await
418    }
419
420    /// Inserts each item in input order in the open transaction, returning
421    /// complete freshly hydrated models or one error, without committing.
422    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
423        self.write_many(inputs, false).await
424    }
425
426    /// Applies the per-item put rule in input order in the open transaction,
427    /// returning complete freshly hydrated models or one error, without
428    /// committing.
429    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
430        self.write_many(inputs, true).await
431    }
432
433    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
434        if inputs.is_empty() {
435            return Ok(Vec::new());
436        }
437        let (id, installed, manager) = self.exact()?;
438        let mut lowered: Vec<(DynamicAttributeMap, Vec<DynamicRolePlayerInput>)> =
439            Vec::with_capacity(inputs.len());
440        for input in inputs {
441            let prepared = lower_relation_create(input, &id, installed)?;
442            lowered.push((prepared.attributes, prepared.role_players));
443        }
444        let iids = if put {
445            manager.put_many_exact(&lowered).await
446        } else {
447            manager.insert_many(&lowered).await
448        }
449        .map_err(Error::from_orm)?;
450        if iids.len() != lowered.len() {
451            return Err(Error::model_validation(
452                ModelValidationPhase::Hydration,
453                "iid_count_mismatch",
454                vec!["iid".into()],
455                "provider returned an unexpected IID count",
456                None,
457            ));
458        }
459        let mut out = Vec::with_capacity(iids.len());
460        for iid in iids {
461            out.push(rehydrate_written_relation(&manager, &iid, &id, installed).await?);
462        }
463        Ok(out)
464    }
465
466    /// Completely replaces non-key ownership and the complete effective
467    /// active-role player set on the exact relation at canonical `iid` in the
468    /// open transaction, preserving that IID, without committing.
469    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
470        if !is_canonical_thing_iid(iid) {
471            return Err(invalid_iid());
472        }
473        let (id, installed, manager) = self.exact()?;
474        let prepared = lower_relation_create(input, &id, installed)?;
475        manager
476            .update_exact(iid, &prepared.attributes, &prepared.role_players)
477            .await
478            .map_err(Error::from_orm)?;
479        rehydrate_written_relation(&manager, iid, &id, installed).await
480    }
481
482    /// Deletes only the exact relation at canonical `iid` in the open
483    /// transaction, without committing.
484    pub async fn delete(&self, iid: &str) -> Result<()> {
485        if !is_canonical_thing_iid(iid) {
486            return Err(invalid_iid());
487        }
488        let (_id, _installed, manager) = self.exact()?;
489        manager
490            .delete_by_iid_exact(iid)
491            .await
492            .map_err(Error::from_orm)
493    }
494
495    /// Reads one exact coalesced relation by canonical IID through the open
496    /// transaction, observing this transaction's uncommitted writes.
497    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
498        if !is_canonical_thing_iid(iid) {
499            return Err(invalid_iid());
500        }
501        let (id, installed, manager) = self.exact()?;
502        let rows = manager
503            .get_by_iid_exact(iid)
504            .await
505            .map_err(Error::from_orm)?;
506        match one_coalesced_row(rows)? {
507            None => Ok(None),
508            Some(row) => {
509                let hydrated = hydrate_relation(row, &id, installed)?;
510                M::materialize(&hydrated, &HydrationCapability::new())
511                    .map(Some)
512                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
513            }
514        }
515    }
516
517    /// Reads all exact coalesced relations through the open transaction,
518    /// observing this transaction's uncommitted writes.
519    pub async fn all(&self) -> Result<Vec<M>> {
520        let (id, installed, manager) = self.exact()?;
521        let rows = manager.all_exact().await.map_err(Error::from_orm)?;
522        rows.into_iter()
523            .map(|row| {
524                let hydrated = hydrate_relation(row, &id, installed)?;
525                M::materialize(&hydrated, &HydrationCapability::new())
526                    .map_err(|error| map_validation_error(error, ModelValidationPhase::Hydration))
527            })
528            .collect()
529    }
530
531    /// Counts only exact relations through the open transaction.
532    pub async fn count(&self) -> Result<u64> {
533        let (_id, _installed, manager) = self.exact()?;
534        manager.count_exact().await.map_err(Error::from_orm)
535    }
536}