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