Skip to main content

type_bridge/
relation_manager.rs

1#![deny(missing_docs)]
2
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use type_bridge_contract::id::is_canonical_thing_iid;
7use type_bridge_orm::manager::DynamicRelationManager;
8use type_bridge_orm::session::backend::TxType;
9use type_bridge_orm::{DynamicAttributeMap, DynamicRelationRow, DynamicRolePlayerInput};
10
11use crate::__codegen::{CompleteModel, HydrationCapability, RelationModel, SubtypeRootModel};
12use crate::error::{Error, ModelValidationPhase};
13use crate::relation_codec::{
14    hydrate_relation, lower_relation_create, resolve_discovered_relation,
15    resolve_relation_authority,
16};
17use crate::schema::Schema;
18use crate::{Database, Result};
19
20#[cfg(test)]
21mod tests;
22
23fn invalid_iid() -> Error {
24    Error::model_validation(
25        ModelValidationPhase::Input,
26        "invalid_iid",
27        vec!["iid".into()],
28        "IID is not canonical",
29        None,
30    )
31}
32
33fn schema_not_bound() -> Error {
34    Error::model_validation(
35        ModelValidationPhase::Input,
36        "schema_not_bound",
37        vec![],
38        "database is not schema-bound",
39        None,
40    )
41}
42
43fn missing_post_write_row() -> Error {
44    Error::model_validation(
45        ModelValidationPhase::Hydration,
46        "missing_post_write_row",
47        vec!["iid".into()],
48        "written relation was not returned",
49        None,
50    )
51}
52
53fn ambiguous_provider_row() -> Error {
54    Error::model_validation(
55        ModelValidationPhase::Hydration,
56        "ambiguous_provider_row",
57        vec!["iid".into()],
58        "provider returned multiple coalesced rows for one exact IID",
59        None,
60    )
61}
62
63pub(crate) fn one_coalesced_row(
64    mut rows: Vec<DynamicRelationRow>,
65) -> Result<Option<DynamicRelationRow>> {
66    match rows.len() {
67        0 => Ok(None),
68        1 => Ok(Some(rows.remove(0))),
69        _ => Err(ambiguous_provider_row()),
70    }
71}
72
73/// Exact-fetch, coalesce, hydrate, and materialize one freshly written relation
74/// through the shared open context without any transaction-terminal operation.
75pub(crate) async fn rehydrate_written_relation<M>(
76    manager: &DynamicRelationManager<'_>,
77    iid: &str,
78    id: &type_bridge_contract::id::TypeId,
79    installed: &type_bridge_orm::InstalledRuntimeProjection,
80) -> Result<M>
81where
82    M: crate::__codegen::CompleteModel,
83{
84    let rows = manager
85        .get_by_iid_exact(iid)
86        .await
87        .map_err(Error::from_orm)?;
88    let row = one_coalesced_row(rows)?.ok_or_else(missing_post_write_row)?;
89    let hydrated = hydrate_relation(row, id, installed)?;
90    M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
91        crate::entity_codec::map_validation_error(error, ModelValidationPhase::Hydration)
92    })
93}
94
95/// Schema-bound, model-branded manager for exact relation operations.
96/// Exact reads and writes exclude subtypes; methods return client input/schema-validation,
97/// database/transaction/close, or hydration/model-validation errors as applicable.
98pub struct RelationManager<'db, S: Schema, M: RelationModel<Schema = S>> {
99    db: &'db Database<S>,
100    marker: PhantomData<M>,
101}
102
103impl<'db, S: Schema, M: RelationModel<Schema = S>> Copy for RelationManager<'db, S, M> {}
104impl<'db, S: Schema, M: RelationModel<Schema = S>> Clone for RelationManager<'db, S, M> {
105    fn clone(&self) -> Self {
106        *self
107    }
108}
109
110impl<S: Schema, M: RelationModel<Schema = S>> RelationManager<'_, S, M> {
111    pub(crate) fn new(db: &Database<S>) -> RelationManager<'_, S, M> {
112        RelationManager {
113            db,
114            marker: PhantomData,
115        }
116    }
117}
118
119impl<S, M> RelationManager<'_, S, M>
120where
121    S: Schema,
122    M: RelationModel<Schema = S> + CompleteModel,
123{
124    /// Inserts one exact relation with its complete active role players and returns the
125    /// complete freshly hydrated model. Errors may be input/schema validation,
126    /// database/transaction/close, or model hydration errors.
127    pub async fn insert(&self, input: M::Create) -> Result<M> {
128        self.write(input, false).await
129    }
130    /// Uses the projected exact-model key when one usable key is carried; otherwise inserts.
131    /// For an existing exact row, the supplied create value completely replaces non-key
132    /// ownership and the complete effective active-role player set while preserving the
133    /// exact IID and keys. It returns a complete freshly hydrated model.
134    pub async fn put(&self, input: M::Create) -> Result<M> {
135        self.write(input, true).await
136    }
137    /// Inserts each item and returns complete freshly hydrated models in input order, or one
138    /// error for the whole call with no partial result vector.
139    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
140        if inputs.is_empty() {
141            return Ok(Vec::new());
142        }
143        self.write_many(inputs, false).await
144    }
145    /// Applies the per-item [`Self::put`] key-or-insert rule, including complete replacement
146    /// of non-key ownership and active role players for existing exact rows, returning
147    /// complete freshly hydrated models in input order, or one error for the whole call with
148    /// no partial vector.
149    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
150        if inputs.is_empty() {
151            return Ok(Vec::new());
152        }
153        self.write_many(inputs, true).await
154    }
155    /// Completely replaces non-key ownership and the complete effective active-role player
156    /// set on the exact relation at canonical `iid`, preserves that IID and its keys, and
157    /// returns its complete freshly hydrated model.
158    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
159        if !is_canonical_thing_iid(iid) {
160            return Err(invalid_iid());
161        }
162        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
163        let (id, descriptor) = resolve_relation_authority(
164            M::TYPE_ID_JSON,
165            installed,
166            ModelValidationPhase::Input,
167            true,
168        )?;
169        let prepared = lower_relation_create(input, &id, installed)?;
170        let tx = self
171            .db
172            .inner_orm()
173            .transaction_context(TxType::Write)
174            .await
175            .map_err(Error::from_orm)?;
176        let manager = DynamicRelationManager::with_canonical_transaction(
177            tx.clone(),
178            Arc::new(descriptor.clone()),
179        );
180        if let Err(error) = manager
181            .update_exact(iid, &prepared.attributes, &prepared.role_players)
182            .await
183        {
184            let _ = tx.rollback().await;
185            return Err(Error::from_orm(error));
186        }
187        let rows = match manager.get_by_iid_exact(iid).await {
188            Ok(rows) => rows,
189            Err(error) => {
190                let _ = tx.rollback().await;
191                return Err(Error::from_orm(error));
192            }
193        };
194        let row = match one_coalesced_row(rows) {
195            Ok(Some(row)) => row,
196            Ok(None) => {
197                let _ = tx.rollback().await;
198                return Err(missing_post_write_row());
199            }
200            Err(error) => {
201                let _ = tx.rollback().await;
202                return Err(error);
203            }
204        };
205        let hydrated = match hydrate_relation(row, &id, installed) {
206            Ok(value) => value,
207            Err(error) => {
208                let _ = tx.rollback().await;
209                return Err(error);
210            }
211        };
212        let value = match M::materialize(&hydrated, &HydrationCapability::new()) {
213            Ok(value) => value,
214            Err(error) => {
215                let mapped = crate::entity_codec::map_validation_error(
216                    error,
217                    ModelValidationPhase::Hydration,
218                );
219                let _ = tx.rollback().await;
220                return Err(mapped);
221            }
222        };
223        tx.commit().await.map_err(Error::from_orm)?;
224        Ok(value)
225    }
226    /// Deletes only the exact relation at canonical `iid`; subtype instances are not
227    /// targeted.
228    pub async fn delete(&self, iid: &str) -> Result<()> {
229        if !is_canonical_thing_iid(iid) {
230            return Err(invalid_iid());
231        }
232        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
233        let (_id, descriptor) = resolve_relation_authority(
234            M::TYPE_ID_JSON,
235            installed,
236            ModelValidationPhase::Input,
237            true,
238        )?;
239        let tx = self
240            .db
241            .inner_orm()
242            .transaction_context(TxType::Write)
243            .await
244            .map_err(Error::from_orm)?;
245        let manager = DynamicRelationManager::with_canonical_transaction(
246            tx.clone(),
247            Arc::new(descriptor.clone()),
248        );
249        if let Err(error) = manager.delete_by_iid_exact(iid).await {
250            let _ = tx.rollback().await;
251            return Err(Error::from_orm(error));
252        }
253        tx.commit().await.map_err(Error::from_orm)
254    }
255    async fn write(&self, input: M::Create, put: bool) -> Result<M> {
256        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
257        let (id, descriptor) = resolve_relation_authority(
258            M::TYPE_ID_JSON,
259            installed,
260            ModelValidationPhase::Input,
261            true,
262        )?;
263        let prepared = lower_relation_create(input, &id, installed)?;
264        let tx = self
265            .db
266            .inner_orm()
267            .transaction_context(TxType::Write)
268            .await
269            .map_err(Error::from_orm)?;
270        let manager = DynamicRelationManager::with_canonical_transaction(
271            tx.clone(),
272            Arc::new(descriptor.clone()),
273        );
274        let iid = if put {
275            manager
276                .put_exact(&prepared.attributes, &prepared.role_players)
277                .await
278        } else {
279            manager
280                .insert(&prepared.attributes, &prepared.role_players)
281                .await
282        };
283        let iid = match iid {
284            Ok(iid) => iid,
285            Err(error) => {
286                let _ = tx.rollback().await;
287                return Err(Error::from_orm(error));
288            }
289        };
290        let value = match self
291            .hydrate_in_transaction(&manager, &iid, &id, installed)
292            .await
293        {
294            Ok(value) => value,
295            Err(error) => {
296                let _ = tx.rollback().await;
297                return Err(error);
298            }
299        };
300        tx.commit().await.map_err(Error::from_orm)?;
301        Ok(value)
302    }
303
304    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
305        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
306        let (id, descriptor) = resolve_relation_authority(
307            M::TYPE_ID_JSON,
308            installed,
309            ModelValidationPhase::Input,
310            true,
311        )?;
312        let mut lowered: Vec<(DynamicAttributeMap, Vec<DynamicRolePlayerInput>)> =
313            Vec::with_capacity(inputs.len());
314        for input in inputs {
315            let prepared = lower_relation_create(input, &id, installed)?;
316            lowered.push((prepared.attributes, prepared.role_players));
317        }
318        let tx = self
319            .db
320            .inner_orm()
321            .transaction_context(TxType::Write)
322            .await
323            .map_err(Error::from_orm)?;
324        let manager =
325            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
326        let iids = match if put {
327            manager.put_many_exact(&lowered).await
328        } else {
329            manager.insert_many(&lowered).await
330        } {
331            Ok(iids) if iids.len() == lowered.len() => iids,
332            Ok(_) => {
333                let _ = tx.rollback().await;
334                return Err(Error::model_validation(
335                    ModelValidationPhase::Hydration,
336                    "iid_count_mismatch",
337                    vec!["iid".into()],
338                    "provider returned an unexpected IID count",
339                    None,
340                ));
341            }
342            Err(error) => {
343                let _ = tx.rollback().await;
344                return Err(Error::from_orm(error));
345            }
346        };
347        let mut out = Vec::with_capacity(iids.len());
348        for iid in iids {
349            match self
350                .hydrate_in_transaction(&manager, &iid, &id, installed)
351                .await
352            {
353                Ok(value) => out.push(value),
354                Err(error) => {
355                    let _ = tx.rollback().await;
356                    return Err(error);
357                }
358            }
359        }
360        tx.commit().await.map_err(Error::from_orm)?;
361        Ok(out)
362    }
363
364    async fn hydrate_in_transaction(
365        &self,
366        manager: &DynamicRelationManager<'_>,
367        iid: &str,
368        id: &type_bridge_contract::id::TypeId,
369        installed: &type_bridge_orm::InstalledRuntimeProjection,
370    ) -> Result<M> {
371        rehydrate_written_relation(manager, iid, id, installed).await
372    }
373
374    /// Counts only exact relations, excluding subtypes.
375    pub async fn count(&self) -> Result<u64> {
376        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
377        let (_id, descriptor) = resolve_relation_authority(
378            M::TYPE_ID_JSON,
379            installed,
380            ModelValidationPhase::Input,
381            true,
382        )?;
383        DynamicRelationManager::new_canonical(self.db.inner_orm(), Arc::new(descriptor.clone()))
384            .count_exact()
385            .await
386            .map_err(Error::from_orm)
387    }
388
389    /// Reads one exact coalesced relation by canonical IID; invalid IIDs are rejected before
390    /// I/O and a valid but absent relation returns `None`.
391    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
392        if !is_canonical_thing_iid(iid) {
393            return Err(invalid_iid());
394        }
395        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
396        let (id, descriptor) = resolve_relation_authority(
397            M::TYPE_ID_JSON,
398            installed,
399            ModelValidationPhase::Input,
400            true,
401        )?;
402        let rows = DynamicRelationManager::new_canonical(
403            self.db.inner_orm(),
404            Arc::new(descriptor.clone()),
405        )
406        .get_by_iid_exact(iid)
407        .await
408        .map_err(Error::from_orm)?;
409        match one_coalesced_row(rows)? {
410            None => Ok(None),
411            Some(row) => {
412                let hydrated = hydrate_relation(row, &id, installed)?;
413                let value =
414                    M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
415                        crate::entity_codec::map_validation_error(
416                            error,
417                            ModelValidationPhase::Hydration,
418                        )
419                    })?;
420                Ok(Some(value))
421            }
422        }
423    }
424
425    /// Reads all exact coalesced relations in application result order, excluding subtypes;
426    /// each result is a complete hydrated model.
427    pub async fn all(&self) -> Result<Vec<M>> {
428        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
429        let (id, descriptor) = resolve_relation_authority(
430            M::TYPE_ID_JSON,
431            installed,
432            ModelValidationPhase::Input,
433            true,
434        )?;
435        let rows = DynamicRelationManager::new_canonical(
436            self.db.inner_orm(),
437            Arc::new(descriptor.clone()),
438        )
439        .all_exact()
440        .await
441        .map_err(Error::from_orm)?;
442        rows.into_iter()
443            .map(|row| {
444                let hydrated = hydrate_relation(row, &id, installed)?;
445                M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
446                    crate::entity_codec::map_validation_error(
447                        error,
448                        ModelValidationPhase::Hydration,
449                    )
450                })
451            })
452            .collect()
453    }
454}
455
456/// Read-only, schema/model-branded manager for an inclusive generated subtype association.
457/// Results are the generated associated leaf or closed family type; no writes are exposed.
458/// Reads and counts can return input/schema-validation, database/transaction/close, or
459/// hydration/model-validation errors.
460pub struct RelationSubtypeManager<
461    'db,
462    S: Schema,
463    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
464> {
465    db: &'db Database<S>,
466    marker: PhantomData<M>,
467}
468
469impl<'db, S: Schema, M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>>
470    RelationSubtypeManager<'db, S, M>
471{
472    pub(crate) fn new(db: &'db Database<S>) -> Self {
473        Self {
474            db,
475            marker: PhantomData,
476        }
477    }
478}
479
480impl<'db, S, M> RelationManager<'db, S, M>
481where
482    S: Schema,
483    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
484{
485    /// Switches only the read scope and result shape to the generated inclusive subtype
486    /// association; it does not add write operations.
487    pub fn subtypes(&self) -> RelationSubtypeManager<'db, S, M> {
488        RelationSubtypeManager::new(self.db)
489    }
490}
491
492impl<S, M> RelationSubtypeManager<'_, S, M>
493where
494    S: Schema,
495    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
496{
497    fn missing_concrete_row() -> Error {
498        Error::model_validation(
499            ModelValidationPhase::Hydration,
500            "missing_concrete_row",
501            vec!["iid".into()],
502            "discovered relation row is missing",
503            None,
504        )
505    }
506
507    async fn rehydrate_discovered(
508        tx: &type_bridge_orm::session::context::TransactionContext,
509        identity: &type_bridge_orm::DynamicRelationIdentity,
510        installed: &type_bridge_orm::InstalledRuntimeProjection,
511    ) -> Result<M::Subtypes> {
512        let (child_id, child_descriptor) =
513            resolve_discovered_relation(&identity.type_name, installed)?;
514        let child = DynamicRelationManager::with_canonical_transaction(
515            tx.clone(),
516            Arc::new(child_descriptor),
517        );
518        let rows = child
519            .get_by_iid_exact(&identity.iid)
520            .await
521            .map_err(Error::from_orm)?;
522        let row = one_coalesced_row(rows)?.ok_or_else(Self::missing_concrete_row)?;
523        let hydrated = hydrate_relation(row, &child_id, installed)?;
524        M::__tb_dispatch_subtype(&hydrated, &HydrationCapability::new()).map_err(|error| {
525            crate::entity_codec::map_validation_error(error, ModelValidationPhase::Hydration)
526        })
527    }
528
529    /// Reads one canonical IID across the root and its generated concrete descendants.
530    /// Invalid IIDs are rejected before I/O; a valid but absent IID returns `None`.
531    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M::Subtypes>> {
532        if !is_canonical_thing_iid(iid) {
533            return Err(invalid_iid());
534        }
535        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
536        let (_id, descriptor) = resolve_relation_authority(
537            M::TYPE_ID_JSON,
538            installed,
539            ModelValidationPhase::Input,
540            false,
541        )?;
542        let tx = self
543            .db
544            .inner_orm()
545            .transaction_context(TxType::Read)
546            .await
547            .map_err(Error::from_orm)?;
548        let manager =
549            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
550        let identity = match manager.discover_by_iid(iid).await {
551            Ok(value) => value,
552            Err(error) => {
553                let _ = tx.close().await;
554                return Err(Error::from_orm(error));
555            }
556        };
557        let out = match identity {
558            None => None,
559            Some(identity) => match Self::rehydrate_discovered(&tx, &identity, installed).await {
560                Ok(value) => Some(value),
561                Err(error) => {
562                    let _ = tx.close().await;
563                    return Err(error);
564                }
565            },
566        };
567        tx.close().await.map_err(Error::from_orm)?;
568        Ok(out)
569    }
570
571    /// Reads all root/descendant relations in application result order, materialized as the
572    /// generated leaf or family result. Validation, database, hydration, and close errors
573    /// are returned without exposing implementation details.
574    pub async fn all(&self) -> Result<Vec<M::Subtypes>> {
575        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
576        let (_id, descriptor) = resolve_relation_authority(
577            M::TYPE_ID_JSON,
578            installed,
579            ModelValidationPhase::Input,
580            false,
581        )?;
582        let tx = self
583            .db
584            .inner_orm()
585            .transaction_context(TxType::Read)
586            .await
587            .map_err(Error::from_orm)?;
588        let manager =
589            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
590        let identities = match manager.discover_all().await {
591            Ok(value) => value,
592            Err(error) => {
593                let _ = tx.close().await;
594                return Err(Error::from_orm(error));
595            }
596        };
597        let mut out = Vec::with_capacity(identities.len());
598        for identity in identities {
599            match Self::rehydrate_discovered(&tx, &identity, installed).await {
600                Ok(value) => out.push(value),
601                Err(error) => {
602                    let _ = tx.close().await;
603                    return Err(error);
604                }
605            }
606        }
607        tx.close().await.map_err(Error::from_orm)?;
608        Ok(out)
609    }
610
611    /// Counts the root and all concrete descendants using the inclusive subtype scope.
612    pub async fn count(&self) -> Result<u64> {
613        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
614        let (_id, descriptor) = resolve_relation_authority(
615            M::TYPE_ID_JSON,
616            installed,
617            ModelValidationPhase::Input,
618            false,
619        )?;
620        DynamicRelationManager::new_canonical(self.db.inner_orm(), Arc::new(descriptor))
621            .count()
622            .await
623            .map_err(Error::from_orm)
624    }
625}