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::{
12    CompleteModel, EncodedCreate, HydrationCapability, IntoEncodedCreate, RelationModel,
13    SubtypeRootModel,
14};
15use crate::error::{Error, ModelValidationPhase};
16use crate::hooks::{CrudOperation, HookRunner, LifecycleHook, ModelKind};
17use crate::relation_codec::{
18    hydrate_relation, lower_relation_create, resolve_discovered_relation,
19    resolve_relation_authority,
20};
21use crate::schema::Schema;
22use crate::{Database, Result};
23
24#[cfg(test)]
25mod tests;
26
27fn invalid_iid() -> Error {
28    Error::model_validation(
29        ModelValidationPhase::Input,
30        "invalid_iid",
31        vec!["iid".into()],
32        "IID is not canonical",
33        None,
34    )
35}
36
37fn schema_not_bound() -> Error {
38    Error::model_validation(
39        ModelValidationPhase::Input,
40        "schema_not_bound",
41        vec![],
42        "database is not schema-bound",
43        None,
44    )
45}
46
47fn missing_post_write_row() -> Error {
48    Error::model_validation(
49        ModelValidationPhase::Hydration,
50        "missing_post_write_row",
51        vec!["iid".into()],
52        "written relation was not returned",
53        None,
54    )
55}
56
57fn ambiguous_provider_row() -> Error {
58    Error::model_validation(
59        ModelValidationPhase::Hydration,
60        "ambiguous_provider_row",
61        vec!["iid".into()],
62        "provider returned multiple coalesced rows for one exact IID",
63        None,
64    )
65}
66
67pub(crate) fn one_coalesced_row(
68    mut rows: Vec<DynamicRelationRow>,
69) -> Result<Option<DynamicRelationRow>> {
70    match rows.len() {
71        0 => Ok(None),
72        1 => Ok(Some(rows.remove(0))),
73        _ => Err(ambiguous_provider_row()),
74    }
75}
76
77/// Exact-fetch, coalesce, hydrate, and materialize one freshly written relation
78/// through the shared open context without any transaction-terminal operation.
79pub(crate) async fn rehydrate_written_relation<M>(
80    manager: &DynamicRelationManager<'_>,
81    iid: &str,
82    id: &type_bridge_contract::id::TypeId,
83    installed: &type_bridge_orm::InstalledRuntimeProjection,
84) -> Result<M>
85where
86    M: crate::__codegen::CompleteModel,
87{
88    let rows = manager
89        .get_by_iid_exact(iid)
90        .await
91        .map_err(Error::from_orm)?;
92    let row = one_coalesced_row(rows)?.ok_or_else(missing_post_write_row)?;
93    let hydrated = hydrate_relation(row, id, installed)?;
94    M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
95        crate::entity_codec::map_validation_error(error, ModelValidationPhase::Hydration)
96    })
97}
98
99/// Schema-bound, model-branded manager for exact relation operations.
100/// Exact reads and writes exclude subtypes; methods return client input/schema-validation,
101/// database/transaction/close, or hydration/model-validation errors as applicable.
102pub struct RelationManager<'db, S: Schema, M: RelationModel<Schema = S>> {
103    db: &'db Database<S>,
104    hooks: HookRunner,
105    marker: PhantomData<M>,
106}
107
108impl<'db, S: Schema, M: RelationModel<Schema = S>> Clone for RelationManager<'db, S, M> {
109    fn clone(&self) -> Self {
110        Self {
111            db: self.db,
112            hooks: self.hooks.clone(),
113            marker: PhantomData,
114        }
115    }
116}
117
118impl<S: Schema, M: RelationModel<Schema = S>> RelationManager<'_, S, M> {
119    pub(crate) fn new(db: &Database<S>) -> RelationManager<'_, S, M> {
120        RelationManager {
121            db,
122            hooks: HookRunner::default(),
123            marker: PhantomData,
124        }
125    }
126}
127
128impl<S, M> RelationManager<'_, S, M>
129where
130    S: Schema,
131    M: RelationModel<Schema = S> + CompleteModel,
132{
133    /// Register one generated-model lifecycle hook on this manager.
134    /// Pre-hooks run in registration order and post-hooks in reverse order.
135    pub fn add_hook(&mut self, hook: Arc<dyn LifecycleHook>) -> &mut Self {
136        self.hooks.add(hook);
137        self
138    }
139
140    fn encode_hook_input(input: &M::Create) -> Result<EncodedCreate> {
141        input.clone().into_encoded_create().map_err(|error| {
142            crate::entity_codec::map_validation_error(error, ModelValidationPhase::Input)
143        })
144    }
145
146    /// Inserts one exact relation with its complete active role players and returns the
147    /// complete freshly hydrated model. Errors may be input/schema validation,
148    /// database/transaction/close, or model hydration errors.
149    pub async fn insert(&self, input: M::Create) -> Result<M> {
150        if !self.hooks.has_hooks() {
151            return self.write(input, false).await;
152        }
153        let encoded = Self::encode_hook_input(&input)?;
154        let state = self
155            .hooks
156            .run_pre(
157                M::TYPE_ID_JSON,
158                ModelKind::Relation,
159                CrudOperation::Insert,
160                None,
161                Some(&encoded),
162            )
163            .await?;
164        let output = self.write(input, false).await?;
165        self.hooks
166            .run_post(
167                M::TYPE_ID_JSON,
168                ModelKind::Relation,
169                CrudOperation::Insert,
170                Some(output.iid()),
171                Some(&encoded),
172                state,
173            )
174            .await;
175        Ok(output)
176    }
177    /// Uses the projected exact-model key when one usable key is carried; otherwise inserts.
178    /// For an existing exact row, the supplied create value completely replaces non-key
179    /// ownership and the complete effective active-role player set while preserving the
180    /// exact IID and keys. It returns a complete freshly hydrated model.
181    pub async fn put(&self, input: M::Create) -> Result<M> {
182        if !self.hooks.has_hooks() {
183            return self.write(input, true).await;
184        }
185        let encoded = Self::encode_hook_input(&input)?;
186        let state = self
187            .hooks
188            .run_pre(
189                M::TYPE_ID_JSON,
190                ModelKind::Relation,
191                CrudOperation::Put,
192                None,
193                Some(&encoded),
194            )
195            .await?;
196        let output = self.write(input, true).await?;
197        self.hooks
198            .run_post(
199                M::TYPE_ID_JSON,
200                ModelKind::Relation,
201                CrudOperation::Put,
202                Some(output.iid()),
203                Some(&encoded),
204                state,
205            )
206            .await;
207        Ok(output)
208    }
209    /// Inserts each item and returns complete freshly hydrated models in input order, or one
210    /// error for the whole call with no partial result vector.
211    pub async fn insert_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
212        if inputs.is_empty() {
213            return Ok(Vec::new());
214        }
215        if !self.hooks.has_hooks() {
216            return self.write_many(inputs, false).await;
217        }
218        let encoded = inputs
219            .iter()
220            .map(Self::encode_hook_input)
221            .collect::<Result<Vec<_>>>()?;
222        let mut states = Vec::with_capacity(inputs.len());
223        for input in &encoded {
224            states.push(
225                self.hooks
226                    .run_pre(
227                        M::TYPE_ID_JSON,
228                        ModelKind::Relation,
229                        CrudOperation::Insert,
230                        None,
231                        Some(input),
232                    )
233                    .await?,
234            );
235        }
236        let outputs = self.write_many(inputs, false).await?;
237        for ((input, output), state) in encoded.iter().zip(&outputs).zip(states) {
238            self.hooks
239                .run_post(
240                    M::TYPE_ID_JSON,
241                    ModelKind::Relation,
242                    CrudOperation::Insert,
243                    Some(output.iid()),
244                    Some(input),
245                    state,
246                )
247                .await;
248        }
249        Ok(outputs)
250    }
251    /// Applies the per-item [`Self::put`] key-or-insert rule, including complete replacement
252    /// of non-key ownership and active role players for existing exact rows, returning
253    /// complete freshly hydrated models in input order, or one error for the whole call with
254    /// no partial vector.
255    pub async fn put_many(&self, inputs: Vec<M::Create>) -> Result<Vec<M>> {
256        if inputs.is_empty() {
257            return Ok(Vec::new());
258        }
259        if !self.hooks.has_hooks() {
260            return self.write_many(inputs, true).await;
261        }
262        let encoded = inputs
263            .iter()
264            .map(Self::encode_hook_input)
265            .collect::<Result<Vec<_>>>()?;
266        let mut states = Vec::with_capacity(inputs.len());
267        for input in &encoded {
268            states.push(
269                self.hooks
270                    .run_pre(
271                        M::TYPE_ID_JSON,
272                        ModelKind::Relation,
273                        CrudOperation::Put,
274                        None,
275                        Some(input),
276                    )
277                    .await?,
278            );
279        }
280        let outputs = self.write_many(inputs, true).await?;
281        for ((input, output), state) in encoded.iter().zip(&outputs).zip(states) {
282            self.hooks
283                .run_post(
284                    M::TYPE_ID_JSON,
285                    ModelKind::Relation,
286                    CrudOperation::Put,
287                    Some(output.iid()),
288                    Some(input),
289                    state,
290                )
291                .await;
292        }
293        Ok(outputs)
294    }
295    /// Completely replaces non-key ownership and the complete effective active-role player
296    /// set on the exact relation at canonical `iid`, preserves that IID and its keys, and
297    /// returns its complete freshly hydrated model.
298    pub async fn update(&self, iid: &str, input: M::Create) -> Result<M> {
299        if !is_canonical_thing_iid(iid) {
300            return Err(invalid_iid());
301        }
302        if !self.hooks.has_hooks() {
303            return self.update_write(iid, input).await;
304        }
305        let encoded = Self::encode_hook_input(&input)?;
306        let state = self
307            .hooks
308            .run_pre(
309                M::TYPE_ID_JSON,
310                ModelKind::Relation,
311                CrudOperation::Update,
312                Some(iid),
313                Some(&encoded),
314            )
315            .await?;
316        let output = self.update_write(iid, input).await?;
317        self.hooks
318            .run_post(
319                M::TYPE_ID_JSON,
320                ModelKind::Relation,
321                CrudOperation::Update,
322                Some(output.iid()),
323                Some(&encoded),
324                state,
325            )
326            .await;
327        Ok(output)
328    }
329
330    async fn update_write(&self, iid: &str, input: M::Create) -> Result<M> {
331        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
332        let (id, descriptor) = resolve_relation_authority(
333            M::TYPE_ID_JSON,
334            installed,
335            ModelValidationPhase::Input,
336            true,
337        )?;
338        let prepared = lower_relation_create(input, &id, installed)?;
339        let tx = self
340            .db
341            .inner_orm()
342            .transaction_context(TxType::Write)
343            .await
344            .map_err(Error::from_orm)?;
345        let manager = DynamicRelationManager::with_canonical_transaction(
346            tx.clone(),
347            Arc::new(descriptor.clone()),
348        );
349        if let Err(error) = manager
350            .update_exact(iid, &prepared.attributes, &prepared.role_players)
351            .await
352        {
353            let _ = tx.rollback().await;
354            return Err(Error::from_orm(error));
355        }
356        let rows = match manager.get_by_iid_exact(iid).await {
357            Ok(rows) => rows,
358            Err(error) => {
359                let _ = tx.rollback().await;
360                return Err(Error::from_orm(error));
361            }
362        };
363        let row = match one_coalesced_row(rows) {
364            Ok(Some(row)) => row,
365            Ok(None) => {
366                let _ = tx.rollback().await;
367                return Err(missing_post_write_row());
368            }
369            Err(error) => {
370                let _ = tx.rollback().await;
371                return Err(error);
372            }
373        };
374        let hydrated = match hydrate_relation(row, &id, installed) {
375            Ok(value) => value,
376            Err(error) => {
377                let _ = tx.rollback().await;
378                return Err(error);
379            }
380        };
381        let value = match M::materialize(&hydrated, &HydrationCapability::new()) {
382            Ok(value) => value,
383            Err(error) => {
384                let mapped = crate::entity_codec::map_validation_error(
385                    error,
386                    ModelValidationPhase::Hydration,
387                );
388                let _ = tx.rollback().await;
389                return Err(mapped);
390            }
391        };
392        tx.commit().await.map_err(Error::from_orm)?;
393        Ok(value)
394    }
395    /// Deletes only the exact relation at canonical `iid`; subtype instances are not
396    /// targeted.
397    pub async fn delete(&self, iid: &str) -> Result<()> {
398        if !is_canonical_thing_iid(iid) {
399            return Err(invalid_iid());
400        }
401        if !self.hooks.has_hooks() {
402            return self.delete_write(iid).await;
403        }
404        let state = self
405            .hooks
406            .run_pre(
407                M::TYPE_ID_JSON,
408                ModelKind::Relation,
409                CrudOperation::Delete,
410                Some(iid),
411                None,
412            )
413            .await?;
414        self.delete_write(iid).await?;
415        self.hooks
416            .run_post(
417                M::TYPE_ID_JSON,
418                ModelKind::Relation,
419                CrudOperation::Delete,
420                Some(iid),
421                None,
422                state,
423            )
424            .await;
425        Ok(())
426    }
427
428    async fn delete_write(&self, iid: &str) -> Result<()> {
429        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
430        let (_id, descriptor) = resolve_relation_authority(
431            M::TYPE_ID_JSON,
432            installed,
433            ModelValidationPhase::Input,
434            true,
435        )?;
436        let tx = self
437            .db
438            .inner_orm()
439            .transaction_context(TxType::Write)
440            .await
441            .map_err(Error::from_orm)?;
442        let manager = DynamicRelationManager::with_canonical_transaction(
443            tx.clone(),
444            Arc::new(descriptor.clone()),
445        );
446        if let Err(error) = manager.delete_by_iid_exact(iid).await {
447            let _ = tx.rollback().await;
448            return Err(Error::from_orm(error));
449        }
450        tx.commit().await.map_err(Error::from_orm)
451    }
452
453    /// Atomically replaces each exact relation identified by its canonical IID and returns
454    /// complete freshly hydrated models in input order. Every input and pre-hook completes
455    /// before database work begins; any write or hydration failure rolls back the whole batch.
456    pub async fn update_many(&self, inputs: Vec<(String, M::Create)>) -> Result<Vec<M>> {
457        if inputs.is_empty() {
458            return Ok(Vec::new());
459        }
460        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
461        let (id, descriptor) = resolve_relation_authority(
462            M::TYPE_ID_JSON,
463            installed,
464            ModelValidationPhase::Input,
465            true,
466        )?;
467        let mut prepared = Vec::with_capacity(inputs.len());
468        for (iid, input) in inputs {
469            if !is_canonical_thing_iid(&iid) {
470                return Err(invalid_iid());
471            }
472            let encoded = Self::encode_hook_input(&input)?;
473            let lowered = lower_relation_create(input, &id, installed)?;
474            prepared.push((iid, encoded, lowered));
475        }
476
477        let mut states = Vec::new();
478        if self.hooks.has_hooks() {
479            states.reserve(prepared.len());
480            for (iid, encoded, _) in &prepared {
481                states.push(
482                    self.hooks
483                        .run_pre(
484                            M::TYPE_ID_JSON,
485                            ModelKind::Relation,
486                            CrudOperation::Update,
487                            Some(iid),
488                            Some(encoded),
489                        )
490                        .await?,
491                );
492            }
493        }
494
495        let tx = self
496            .db
497            .inner_orm()
498            .transaction_context(TxType::Write)
499            .await
500            .map_err(Error::from_orm)?;
501        let manager =
502            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
503        let mut outputs = Vec::with_capacity(prepared.len());
504        for (iid, _, lowered) in &prepared {
505            if let Err(error) = manager
506                .update_exact(iid, &lowered.attributes, &lowered.role_players)
507                .await
508            {
509                let _ = tx.rollback().await;
510                return Err(Error::from_orm(error));
511            }
512            match rehydrate_written_relation::<M>(&manager, iid, &id, installed).await {
513                Ok(output) => outputs.push(output),
514                Err(error) => {
515                    let _ = tx.rollback().await;
516                    return Err(error);
517                }
518            }
519        }
520        tx.commit().await.map_err(Error::from_orm)?;
521
522        if self.hooks.has_hooks() {
523            for (((_, encoded, _), output), state) in prepared.iter().zip(&outputs).zip(states) {
524                self.hooks
525                    .run_post(
526                        M::TYPE_ID_JSON,
527                        ModelKind::Relation,
528                        CrudOperation::Update,
529                        Some(output.iid()),
530                        Some(encoded),
531                        state,
532                    )
533                    .await;
534            }
535        }
536        Ok(outputs)
537    }
538
539    /// Atomically deletes every exact relation at the supplied canonical IIDs. All IIDs and
540    /// pre-hooks are accepted before database work begins; any write failure rolls back the
541    /// whole batch, and post-hook failures do not change the committed result.
542    pub async fn delete_many(&self, iids: &[String]) -> Result<()> {
543        if iids.is_empty() {
544            return Ok(());
545        }
546        if iids.iter().any(|iid| !is_canonical_thing_iid(iid)) {
547            return Err(invalid_iid());
548        }
549        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
550        let (_id, descriptor) = resolve_relation_authority(
551            M::TYPE_ID_JSON,
552            installed,
553            ModelValidationPhase::Input,
554            true,
555        )?;
556
557        let mut states = Vec::new();
558        if self.hooks.has_hooks() {
559            states.reserve(iids.len());
560            for iid in iids {
561                states.push(
562                    self.hooks
563                        .run_pre(
564                            M::TYPE_ID_JSON,
565                            ModelKind::Relation,
566                            CrudOperation::Delete,
567                            Some(iid),
568                            None,
569                        )
570                        .await?,
571                );
572            }
573        }
574
575        let tx = self
576            .db
577            .inner_orm()
578            .transaction_context(TxType::Write)
579            .await
580            .map_err(Error::from_orm)?;
581        let manager =
582            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
583        for iid in iids {
584            if let Err(error) = manager.delete_by_iid_exact(iid).await {
585                let _ = tx.rollback().await;
586                return Err(Error::from_orm(error));
587            }
588        }
589        tx.commit().await.map_err(Error::from_orm)?;
590
591        if self.hooks.has_hooks() {
592            for (iid, state) in iids.iter().zip(states) {
593                self.hooks
594                    .run_post(
595                        M::TYPE_ID_JSON,
596                        ModelKind::Relation,
597                        CrudOperation::Delete,
598                        Some(iid),
599                        None,
600                        state,
601                    )
602                    .await;
603            }
604        }
605        Ok(())
606    }
607
608    async fn write(&self, input: M::Create, put: bool) -> Result<M> {
609        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
610        let (id, descriptor) = resolve_relation_authority(
611            M::TYPE_ID_JSON,
612            installed,
613            ModelValidationPhase::Input,
614            true,
615        )?;
616        let prepared = lower_relation_create(input, &id, installed)?;
617        let tx = self
618            .db
619            .inner_orm()
620            .transaction_context(TxType::Write)
621            .await
622            .map_err(Error::from_orm)?;
623        let manager = DynamicRelationManager::with_canonical_transaction(
624            tx.clone(),
625            Arc::new(descriptor.clone()),
626        );
627        let iid = if put {
628            manager
629                .put_exact(&prepared.attributes, &prepared.role_players)
630                .await
631        } else {
632            manager
633                .insert(&prepared.attributes, &prepared.role_players)
634                .await
635        };
636        let iid = match iid {
637            Ok(iid) => iid,
638            Err(error) => {
639                let _ = tx.rollback().await;
640                return Err(Error::from_orm(error));
641            }
642        };
643        let value = match self
644            .hydrate_in_transaction(&manager, &iid, &id, installed)
645            .await
646        {
647            Ok(value) => value,
648            Err(error) => {
649                let _ = tx.rollback().await;
650                return Err(error);
651            }
652        };
653        tx.commit().await.map_err(Error::from_orm)?;
654        Ok(value)
655    }
656
657    async fn write_many(&self, inputs: Vec<M::Create>, put: bool) -> Result<Vec<M>> {
658        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
659        let (id, descriptor) = resolve_relation_authority(
660            M::TYPE_ID_JSON,
661            installed,
662            ModelValidationPhase::Input,
663            true,
664        )?;
665        let mut lowered: Vec<(DynamicAttributeMap, Vec<DynamicRolePlayerInput>)> =
666            Vec::with_capacity(inputs.len());
667        for input in inputs {
668            let prepared = lower_relation_create(input, &id, installed)?;
669            lowered.push((prepared.attributes, prepared.role_players));
670        }
671        let tx = self
672            .db
673            .inner_orm()
674            .transaction_context(TxType::Write)
675            .await
676            .map_err(Error::from_orm)?;
677        let manager =
678            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
679        let iids = match if put {
680            manager.put_many_exact(&lowered).await
681        } else {
682            manager.insert_many(&lowered).await
683        } {
684            Ok(iids) if iids.len() == lowered.len() => iids,
685            Ok(_) => {
686                let _ = tx.rollback().await;
687                return Err(Error::model_validation(
688                    ModelValidationPhase::Hydration,
689                    "iid_count_mismatch",
690                    vec!["iid".into()],
691                    "provider returned an unexpected IID count",
692                    None,
693                ));
694            }
695            Err(error) => {
696                let _ = tx.rollback().await;
697                return Err(Error::from_orm(error));
698            }
699        };
700        let mut out = Vec::with_capacity(iids.len());
701        for iid in iids {
702            match self
703                .hydrate_in_transaction(&manager, &iid, &id, installed)
704                .await
705            {
706                Ok(value) => out.push(value),
707                Err(error) => {
708                    let _ = tx.rollback().await;
709                    return Err(error);
710                }
711            }
712        }
713        tx.commit().await.map_err(Error::from_orm)?;
714        Ok(out)
715    }
716
717    async fn hydrate_in_transaction(
718        &self,
719        manager: &DynamicRelationManager<'_>,
720        iid: &str,
721        id: &type_bridge_contract::id::TypeId,
722        installed: &type_bridge_orm::InstalledRuntimeProjection,
723    ) -> Result<M> {
724        rehydrate_written_relation(manager, iid, id, installed).await
725    }
726
727    /// Counts only exact relations, excluding subtypes.
728    pub async fn count(&self) -> Result<u64> {
729        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
730        let (_id, descriptor) = resolve_relation_authority(
731            M::TYPE_ID_JSON,
732            installed,
733            ModelValidationPhase::Input,
734            true,
735        )?;
736        DynamicRelationManager::new_canonical(self.db.inner_orm(), Arc::new(descriptor.clone()))
737            .count_exact()
738            .await
739            .map_err(Error::from_orm)
740    }
741
742    /// Reads one exact coalesced relation by canonical IID; invalid IIDs are rejected before
743    /// I/O and a valid but absent relation returns `None`.
744    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M>> {
745        if !is_canonical_thing_iid(iid) {
746            return Err(invalid_iid());
747        }
748        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
749        let (id, descriptor) = resolve_relation_authority(
750            M::TYPE_ID_JSON,
751            installed,
752            ModelValidationPhase::Input,
753            true,
754        )?;
755        let rows = DynamicRelationManager::new_canonical(
756            self.db.inner_orm(),
757            Arc::new(descriptor.clone()),
758        )
759        .get_by_iid_exact(iid)
760        .await
761        .map_err(Error::from_orm)?;
762        match one_coalesced_row(rows)? {
763            None => Ok(None),
764            Some(row) => {
765                let hydrated = hydrate_relation(row, &id, installed)?;
766                let value =
767                    M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
768                        crate::entity_codec::map_validation_error(
769                            error,
770                            ModelValidationPhase::Hydration,
771                        )
772                    })?;
773                Ok(Some(value))
774            }
775        }
776    }
777
778    /// Reads all exact coalesced relations in application result order, excluding subtypes;
779    /// each result is a complete hydrated model.
780    pub async fn all(&self) -> Result<Vec<M>> {
781        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
782        let (id, descriptor) = resolve_relation_authority(
783            M::TYPE_ID_JSON,
784            installed,
785            ModelValidationPhase::Input,
786            true,
787        )?;
788        let rows = DynamicRelationManager::new_canonical(
789            self.db.inner_orm(),
790            Arc::new(descriptor.clone()),
791        )
792        .all_exact()
793        .await
794        .map_err(Error::from_orm)?;
795        rows.into_iter()
796            .map(|row| {
797                let hydrated = hydrate_relation(row, &id, installed)?;
798                M::materialize(&hydrated, &HydrationCapability::new()).map_err(|error| {
799                    crate::entity_codec::map_validation_error(
800                        error,
801                        ModelValidationPhase::Hydration,
802                    )
803                })
804            })
805            .collect()
806    }
807}
808
809/// Read-only, schema/model-branded manager for an inclusive generated subtype association.
810/// Results are the generated associated leaf or closed family type; no writes are exposed.
811/// Reads and counts can return input/schema-validation, database/transaction/close, or
812/// hydration/model-validation errors.
813pub struct RelationSubtypeManager<
814    'db,
815    S: Schema,
816    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
817> {
818    db: &'db Database<S>,
819    marker: PhantomData<M>,
820}
821
822impl<'db, S: Schema, M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>>
823    RelationSubtypeManager<'db, S, M>
824{
825    pub(crate) fn new(db: &'db Database<S>) -> Self {
826        Self {
827            db,
828            marker: PhantomData,
829        }
830    }
831}
832
833impl<'db, S, M> RelationManager<'db, S, M>
834where
835    S: Schema,
836    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
837{
838    /// Switches only the read scope and result shape to the generated inclusive subtype
839    /// association; it does not add write operations.
840    pub fn subtypes(&self) -> RelationSubtypeManager<'db, S, M> {
841        RelationSubtypeManager::new(self.db)
842    }
843}
844
845impl<S, M> RelationSubtypeManager<'_, S, M>
846where
847    S: Schema,
848    M: SubtypeRootModel<Schema = S> + RelationModel<Schema = S>,
849{
850    fn missing_concrete_row() -> Error {
851        Error::model_validation(
852            ModelValidationPhase::Hydration,
853            "missing_concrete_row",
854            vec!["iid".into()],
855            "discovered relation row is missing",
856            None,
857        )
858    }
859
860    async fn rehydrate_discovered(
861        tx: &type_bridge_orm::session::context::TransactionContext,
862        identity: &type_bridge_orm::DynamicRelationIdentity,
863        installed: &type_bridge_orm::InstalledRuntimeProjection,
864    ) -> Result<M::Subtypes> {
865        let (child_id, child_descriptor) =
866            resolve_discovered_relation(&identity.type_name, installed)?;
867        let child = DynamicRelationManager::with_canonical_transaction(
868            tx.clone(),
869            Arc::new(child_descriptor),
870        );
871        let rows = child
872            .get_by_iid_exact(&identity.iid)
873            .await
874            .map_err(Error::from_orm)?;
875        let row = one_coalesced_row(rows)?.ok_or_else(Self::missing_concrete_row)?;
876        let hydrated = hydrate_relation(row, &child_id, installed)?;
877        M::__tb_dispatch_subtype(&hydrated, &HydrationCapability::new()).map_err(|error| {
878            crate::entity_codec::map_validation_error(error, ModelValidationPhase::Hydration)
879        })
880    }
881
882    /// Reads one canonical IID across the root and its generated concrete descendants.
883    /// Invalid IIDs are rejected before I/O; a valid but absent IID returns `None`.
884    pub async fn get_by_iid(&self, iid: &str) -> Result<Option<M::Subtypes>> {
885        if !is_canonical_thing_iid(iid) {
886            return Err(invalid_iid());
887        }
888        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
889        let (_id, descriptor) = resolve_relation_authority(
890            M::TYPE_ID_JSON,
891            installed,
892            ModelValidationPhase::Input,
893            false,
894        )?;
895        let tx = self
896            .db
897            .inner_orm()
898            .transaction_context(TxType::Read)
899            .await
900            .map_err(Error::from_orm)?;
901        let manager =
902            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
903        let identity = match manager.discover_by_iid(iid).await {
904            Ok(value) => value,
905            Err(error) => {
906                let _ = tx.close().await;
907                return Err(Error::from_orm(error));
908            }
909        };
910        let out = match identity {
911            None => None,
912            Some(identity) => match Self::rehydrate_discovered(&tx, &identity, installed).await {
913                Ok(value) => Some(value),
914                Err(error) => {
915                    let _ = tx.close().await;
916                    return Err(error);
917                }
918            },
919        };
920        tx.close().await.map_err(Error::from_orm)?;
921        Ok(out)
922    }
923
924    /// Reads all root/descendant relations in application result order, materialized as the
925    /// generated leaf or family result. Validation, database, hydration, and close errors
926    /// are returned without exposing implementation details.
927    pub async fn all(&self) -> Result<Vec<M::Subtypes>> {
928        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
929        let (_id, descriptor) = resolve_relation_authority(
930            M::TYPE_ID_JSON,
931            installed,
932            ModelValidationPhase::Input,
933            false,
934        )?;
935        let tx = self
936            .db
937            .inner_orm()
938            .transaction_context(TxType::Read)
939            .await
940            .map_err(Error::from_orm)?;
941        let manager =
942            DynamicRelationManager::with_canonical_transaction(tx.clone(), Arc::new(descriptor));
943        let identities = match manager.discover_all().await {
944            Ok(value) => value,
945            Err(error) => {
946                let _ = tx.close().await;
947                return Err(Error::from_orm(error));
948            }
949        };
950        let mut out = Vec::with_capacity(identities.len());
951        for identity in identities {
952            match Self::rehydrate_discovered(&tx, &identity, installed).await {
953                Ok(value) => out.push(value),
954                Err(error) => {
955                    let _ = tx.close().await;
956                    return Err(error);
957                }
958            }
959        }
960        tx.close().await.map_err(Error::from_orm)?;
961        Ok(out)
962    }
963
964    /// Counts the root and all concrete descendants using the inclusive subtype scope.
965    pub async fn count(&self) -> Result<u64> {
966        let installed = self.db.installed_schema().ok_or_else(schema_not_bound)?;
967        let (_id, descriptor) = resolve_relation_authority(
968            M::TYPE_ID_JSON,
969            installed,
970            ModelValidationPhase::Input,
971            false,
972        )?;
973        DynamicRelationManager::new_canonical(self.db.inner_orm(), Arc::new(descriptor))
974            .count()
975            .await
976            .map_err(Error::from_orm)
977    }
978}