Skip to main content

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