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