Skip to main content

teaql_runtime/
entity_save.rs

1use std::collections::BTreeSet;
2use std::future::Future;
3use std::marker::PhantomData;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use teaql_core::{Entity, MutationValues, Value};
8
9use crate::{
10    DataServiceError, GraphNode, GraphOperation, ObjectLocation, RuntimeError, UserContext,
11};
12
13tokio::task_local! {
14    static GRAPH_FIX_TIME: teaql_core::time::Timestamp;
15    static GRAPH_FIX_EVIDENCE: Arc<std::sync::Mutex<Vec<crate::FixEvidence>>>;
16}
17
18pub(crate) fn current_graph_fix_time() -> teaql_core::time::Timestamp {
19    GRAPH_FIX_TIME
20        .try_with(|value| *value)
21        .unwrap_or_else(|_| teaql_core::time::Timestamp::now())
22}
23
24pub(crate) fn record_graph_fix_evidence(evidence: crate::FixEvidence) {
25    let _ = GRAPH_FIX_EVIDENCE.try_with(|current| current.lock().unwrap().push(evidence));
26}
27
28// ---------------------------------------------------------------------------
29// DynGraphSaver — type-erased graph save capability
30// ---------------------------------------------------------------------------
31
32/// Object-safe trait for saving a [`GraphNode`] tree to the database.
33///
34/// A concrete implementation is registered in [`UserContext`] during setup so
35/// that [`Audited::save`] can persist entities without exposing the underlying
36/// executor type to business code.
37pub(crate) trait DynGraphSaver: Send + Sync {
38    fn save_graph_dyn<'a>(
39        &'a self,
40        context: &'a UserContext,
41        node: GraphNode,
42    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
43
44    fn save_ledger_dyn<'a>(
45        &'a self,
46        context: &'a UserContext,
47        node: GraphNode,
48        root: crate::EntityRuntimeState,
49    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
50}
51
52/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
53///
54/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
55/// The struct itself is zero-sized; the actual executor is retrieved from
56/// [`UserContext`] at call time.
57pub(crate) struct GraphSaverFor<E> {
58    _marker: PhantomData<fn() -> E>,
59}
60
61impl<E> GraphSaverFor<E> {
62    pub(crate) fn new() -> Self {
63        Self {
64            _marker: PhantomData,
65        }
66    }
67}
68
69impl<E> DynGraphSaver for GraphSaverFor<E>
70where
71    E: teaql_data_service::QueryExecutor
72        + teaql_data_service::MutationExecutor
73        + teaql_data_service::TransactionExecutor
74        + Send
75        + Sync
76        + 'static,
77    for<'tx> <E as teaql_data_service::TransactionExecutor>::Tx<'tx>: Send + Sync,
78{
79    fn save_graph_dyn<'a>(
80        &'a self,
81        context: &'a UserContext,
82        node: GraphNode,
83    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
84        Box::pin(async move {
85            let entity = node.entity.clone();
86            let executor = context
87                .require_resource::<E>()
88                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
89            let tx = teaql_data_service::TransactionExecutor::begin(&*executor)
90                .await
91                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
92            let result = {
93                let eds = crate::EntityDataService::for_executor(context, entity, &tx);
94                eds.save_graph_internal(node).await
95            };
96            match result {
97                Ok(saved) => {
98                    teaql_data_service::Transaction::commit(tx)
99                        .await
100                        .map_err(|e| RuntimeError::Graph(e.to_string()))?;
101                    Ok(saved)
102                }
103                Err(error) => {
104                    teaql_data_service::Transaction::rollback(tx)
105                        .await
106                        .map_err(|e| RuntimeError::Graph(e.to_string()))?;
107                    Err(match error {
108                        DataServiceError::Runtime(r) => r,
109                        other => RuntimeError::Graph(other.to_string()),
110                    })
111                }
112            }
113        })
114    }
115
116    fn save_ledger_dyn<'a>(
117        &'a self,
118        context: &'a UserContext,
119        mut node: GraphNode,
120        root: crate::EntityRuntimeState,
121    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
122        Box::pin(async move {
123            let entity = node.entity.clone();
124            let executor = context
125                .require_resource::<E>()
126                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
127            let tx = teaql_data_service::TransactionExecutor::begin(&*executor)
128                .await
129                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
130            let descriptor = context.require_entity(&entity)?;
131            let id_prop = descriptor.id_property().ok_or_else(|| {
132                RuntimeError::Graph(format!("entity {entity} has no id property"))
133            })?;
134            let current_id = node
135                .values
136                .get(&id_prop.name)
137                .cloned()
138                .unwrap_or(Value::I64(0));
139            let root_key = crate::EntityKey::new(entity.clone(), current_id);
140            let was_new = root.new_keys().contains(&root_key);
141            let was_deleted = root.deleted_keys().contains(&root_key);
142            let original_version = root.get_original_version(&root_key);
143            let result = {
144                let eds = crate::EntityDataService::for_executor(context, &entity, &tx);
145                eds.execute_ledger_plan_internal(root.clone()).await
146            };
147            let generated_ids = match result {
148                Ok(ids) => {
149                    teaql_data_service::Transaction::commit(tx)
150                        .await
151                        .map_err(|e| RuntimeError::Graph(e.to_string()))?;
152                    ids
153                }
154                Err(error) => {
155                    teaql_data_service::Transaction::rollback(tx)
156                        .await
157                        .map_err(|e| RuntimeError::Graph(e.to_string()))?;
158                    return Err(match error {
159                        DataServiceError::Runtime(r) => r,
160                        other => RuntimeError::Graph(other.to_string()),
161                    });
162                }
163            };
164
165            if let Some(new_id) = generated_ids.get(&root_key) {
166                node.values.insert(id_prop.name.clone(), new_id.clone());
167            }
168            if was_deleted {
169                if let Some(version_prop) = descriptor.version_property() {
170                    if let Some(version) = saved_version(was_new, true, original_version) {
171                        node.values
172                            .insert(version_prop.name.clone(), Value::I64(version));
173                    }
174                }
175            } else {
176                // The database is authoritative for generated IDs, optimistic
177                // versions, defaults, triggers, and provider-side conversions.
178                // Reconstructing the return value from the pending ledger can
179                // retain the pre-save version and silently disagree with the
180                // committed row.  Read the root back after commit, matching the
181                // ordinary graph-save contract.
182                let persisted_id = node.values.get(&id_prop.name).cloned().ok_or_else(|| {
183                    RuntimeError::Graph(format!(
184                        "saved {entity} missing identity field {}",
185                        id_prop.name
186                    ))
187                })?;
188                let eds = crate::EntityDataService::for_executor(context, &entity, &*executor);
189                node.values = eds
190                    .fetch_graph_current_row_internal(
191                        &entity,
192                        &id_prop.name,
193                        &persisted_id,
194                        Vec::new(),
195                    )
196                    .await
197                    .map_err(|error| RuntimeError::Graph(error.to_string()))?
198                    .map(Into::into)
199                    .ok_or_else(|| {
200                        RuntimeError::Graph(format!(
201                            "persisted {entity} record could not be read back"
202                        ))
203                    })?;
204            }
205            root.clear_committed();
206            Ok(node)
207        })
208    }
209}
210
211fn saved_version(was_new: bool, was_deleted: bool, original_version: Option<i64>) -> Option<i64> {
212    if was_new {
213        Some(1)
214    } else if was_deleted {
215        original_version.map(|version| -(version.abs() + 1))
216    } else {
217        original_version.map(|version| version + 1)
218    }
219}
220
221#[cfg(test)]
222mod saved_version_tests {
223    use super::saved_version;
224
225    #[test]
226    fn create_returns_initial_version() {
227        assert_eq!(saved_version(true, false, None), Some(1));
228    }
229
230    #[test]
231    fn update_returns_incremented_version() {
232        assert_eq!(saved_version(false, false, Some(7)), Some(8));
233    }
234
235    #[test]
236    fn delete_returns_next_negative_version() {
237        assert_eq!(saved_version(false, true, Some(7)), Some(-8));
238    }
239}
240
241// ---------------------------------------------------------------------------
242// Standalone graph-node extraction (no executor needed)
243// ---------------------------------------------------------------------------
244
245/// Convert a typed entity into a [`GraphNode`] tree.
246///
247/// This only requires metadata (entity descriptors) from the [`UserContext`],
248/// **not** the database executor.  It is the standalone equivalent of
249/// [`EntityDataService::graph_node_from_entity`].
250pub fn graph_node_from_entity<T: Entity>(
251    context: &UserContext,
252    entity: T,
253) -> Result<GraphNode, RuntimeError> {
254    let descriptor = T::entity_descriptor();
255    let loaded_fields = descriptor
256        .properties
257        .iter()
258        .filter(|property| entity.is_field_loaded(&property.name))
259        .map(|property| Value::Text(property.name.clone()))
260        .collect::<Vec<_>>();
261    let dirty_fields = entity.dirty_fields();
262    let original_values = entity.original_values();
263    let is_new = entity.is_new();
264    let is_deleted = entity.is_marked_as_delete();
265    let comment = entity.get_comment();
266    let mut node = graph_node_from_values(context, &descriptor.name, entity.into_values())?;
267    node.values
268        .insert("_loaded_fields".to_owned(), Value::List(loaded_fields));
269    node.dirty_fields = dirty_fields;
270    node.original_values = original_values.map(Into::into);
271    if is_new {
272        node.operation = GraphOperation::Create;
273    }
274    if is_deleted {
275        node.operation = GraphOperation::Remove;
276        node.relations.clear();
277    }
278    if let Some(c) = comment {
279        node.set_comment(c);
280    }
281    Ok(node)
282}
283
284/// Recursively convert entity mutation values into a [`GraphNode`] tree.
285///
286/// Relations are resolved via the entity descriptors stored in `context`.
287fn graph_node_from_values(
288    context: &UserContext,
289    entity: &str,
290    values: MutationValues,
291) -> Result<GraphNode, RuntimeError> {
292    let descriptor = context.require_entity(entity)?;
293    let mut node = GraphNode::new(entity);
294
295    for (field, value) in values {
296        if field == "_comment" {
297            if let Value::Text(comment) = value {
298                node.set_comment(comment);
299            }
300            continue;
301        }
302        if field == "_dirty_fields" {
303            if let Value::List(fields) = value {
304                let mut dirty = BTreeSet::new();
305                for f in fields {
306                    if let Value::Text(t) = f {
307                        dirty.insert(t);
308                    }
309                }
310                node.dirty_fields = Some(dirty);
311            }
312            continue;
313        }
314        if field == "_original_values" {
315            if let Value::Object(orig) = value {
316                node.original_values = Some(orig.into());
317            }
318            continue;
319        }
320        if field == "_is_new" {
321            if matches!(value, Value::Bool(true)) {
322                node.operation = GraphOperation::Create;
323            }
324            continue;
325        }
326        if field == "_is_deleted" {
327            if matches!(value, Value::Bool(true)) {
328                node.operation = GraphOperation::Remove;
329            }
330            continue;
331        }
332        let Some(relation) = descriptor.relation_by_name(&field) else {
333            node.values.insert(field, value);
334            continue;
335        };
336
337        match value {
338            Value::Null => {
339                node.relations.entry(field).or_default();
340            }
341            Value::Object(record) => {
342                let child =
343                    graph_node_from_values(context, &relation.target_entity, record.into())?;
344                node.relations.entry(field).or_default().push(child);
345            }
346            Value::List(values) => {
347                let children = node.relations.entry(field.clone()).or_default();
348                for value in values {
349                    let Value::Object(record) = value else {
350                        return Err(RuntimeError::Graph(format!(
351                            "relation {}.{} expects object children, got {:?}",
352                            entity, field, value
353                        )));
354                    };
355                    children.push(graph_node_from_values(
356                        context,
357                        &relation.target_entity,
358                        record.into(),
359                    )?);
360                }
361            }
362            other => {
363                return Err(RuntimeError::Graph(format!(
364                    "relation {}.{} expects object/list/null, got {:?}",
365                    entity, field, other
366                )));
367            }
368        }
369    }
370
371    Ok(node)
372}
373
374fn merge_relation_mutations_into_root(
375    root: &crate::EntityRuntimeState,
376    node: &GraphNode,
377) -> Result<(), RuntimeError> {
378    for children in node.relations.values() {
379        for child in children {
380            let id = child.values.get("id").cloned().ok_or_else(|| {
381                RuntimeError::Graph(format!(
382                    "related mutation {} is missing its id",
383                    child.entity
384                ))
385            })?;
386            let key = crate::EntityKey::new(child.entity.clone(), id);
387
388            match child.operation {
389                GraphOperation::Create => {
390                    root.mark_as_new(key.clone());
391                    for (field, value) in &child.values {
392                        root.set(key.clone(), field, value.clone());
393                    }
394                }
395                GraphOperation::Upsert => {
396                    if let Some(fields) = &child.dirty_fields {
397                        for field in fields {
398                            if let Some(value) = child.values.get(field) {
399                                root.set(key.clone(), field, value.clone());
400                            }
401                        }
402                    }
403                }
404                GraphOperation::Remove => root.mark_as_delete(key.clone()),
405                GraphOperation::Reference => {}
406            }
407
408            if let Some(version) = child
409                .original_values
410                .as_ref()
411                .and_then(|values| values.get("version"))
412                .and_then(Value::try_i64)
413            {
414                root.set_original_version(key, version);
415            }
416            merge_relation_mutations_into_root(root, child)?;
417        }
418    }
419    Ok(())
420}
421
422fn hydrate_ledger_relations(
423    context: &UserContext,
424    root: &crate::EntityRuntimeState,
425    node: &mut GraphNode,
426    visited: &mut BTreeSet<crate::EntityKey>,
427) -> Result<(), RuntimeError> {
428    let descriptor = context.require_entity(&node.entity)?;
429    for relation in &descriptor.relations {
430        let Some(local_value) = node.values.get(&relation.local_key).cloned() else {
431            continue;
432        };
433        let existing = node.relations.entry(relation.name.clone()).or_default();
434        let existing_keys = existing
435            .iter()
436            .filter_map(|child| {
437                child
438                    .values
439                    .get("id")
440                    .cloned()
441                    .map(|id| crate::EntityKey::new(child.entity.clone(), id))
442            })
443            .collect::<BTreeSet<_>>();
444        let mut discovered = Vec::new();
445        for (key, changes) in root.current_change_set().changes() {
446            if key.entity.as_ref() != relation.target_entity || existing_keys.contains(key) {
447                continue;
448            }
449            let foreign_value = if relation.foreign_key == "id" {
450                Some(&key.id)
451            } else {
452                changes.get(&relation.foreign_key)
453            };
454            if foreign_value != Some(&local_value) || !visited.insert(key.clone()) {
455                continue;
456            }
457            let mut values: crate::EntityValues = changes.clone().into();
458            values
459                .entry("id".to_owned())
460                .or_insert_with(|| key.id.clone());
461            let operation = if root.deleted_keys().contains(key) {
462                GraphOperation::Remove
463            } else if root.new_keys().contains(key) || root.get_original_version(key).is_none() {
464                GraphOperation::Create
465            } else {
466                GraphOperation::Upsert
467            };
468            let mut child = GraphNode::new(key.entity.to_string());
469            child.values = values;
470            child.operation = operation;
471            hydrate_ledger_relations(context, root, &mut child, visited)?;
472            discovered.push(child);
473        }
474        existing.extend(discovered);
475    }
476    Ok(())
477}
478
479fn preflight_graph(
480    context: &UserContext,
481    node: &mut GraphNode,
482    location: &ObjectLocation,
483    root: Option<&crate::EntityRuntimeState>,
484) -> Result<(), RuntimeError> {
485    if !matches!(
486        node.operation,
487        GraphOperation::Remove | GraphOperation::Reference
488    ) {
489        let before = node.values.clone();
490        let status = match node.operation {
491            GraphOperation::Create => crate::CheckObjectStatus::Create,
492            GraphOperation::Upsert => crate::CheckObjectStatus::Update,
493            GraphOperation::Remove | GraphOperation::Reference => unreachable!(),
494        };
495        crate::mark_entity_status(&mut node.values, status);
496        let result = context.check_and_fix_values_at(&node.entity, &mut node.values, location);
497        crate::clear_entity_status(&mut node.values);
498        result?;
499
500        if let Some(root) = root {
501            if let Some(id) = node.values.get("id").cloned() {
502                let key = crate::EntityKey::new(node.entity.clone(), id);
503                for (field, value) in &node.values {
504                    if before.get(field) != Some(value) {
505                        root.set(key.clone(), field.clone(), value.clone());
506                    }
507                }
508            }
509        }
510    }
511
512    for (relation, children) in &mut node.relations {
513        for (index, child) in children.iter_mut().enumerate() {
514            let child_location = location.clone().member(relation).element(index);
515            preflight_graph(context, child, &child_location, root)?;
516        }
517    }
518    Ok(())
519}
520
521// ---------------------------------------------------------------------------
522// AuditedSaveExt — the `.save(&context)` method on `Audited<T>`
523// ---------------------------------------------------------------------------
524
525/// Extension trait that provides the `.save(&context)` method on [`Audited<T>`](teaql_core::Audited).
526///
527/// # Example
528/// ```ignore
529/// use teaql_runtime::AuditedSaveExt;
530///
531/// school.audit_as("创建学校").save(&context).await?;
532/// ```
533pub trait AuditedSaveExt {
534    type Entity;
535
536    fn save<'a>(
537        self,
538        context: &'a UserContext,
539    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
540}
541
542impl<T> AuditedSaveExt for teaql_core::Audited<T>
543where
544    T: Entity + Send + 'static,
545{
546    type Entity = T;
547
548    fn save<'a>(
549        self,
550        context: &'a UserContext,
551    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
552        Box::pin(async move {
553            let entity_name = T::entity_descriptor().name;
554            let entity = self.into_entity(); // applies comment onto the entity
555            let mut node = graph_node_from_entity(context, entity)?;
556            preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
557            let saver = context
558                .require_resource::<Arc<dyn DynGraphSaver>>()
559                .map_err(|e| {
560                    RuntimeError::Graph(format!(
561                        "no DynGraphSaver registered — did you call register_executor()? ({})",
562                        e
563                    ))
564                })?;
565            let saved = saver.save_graph_dyn(context, node).await?;
566            T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
567                .map_err(|e| RuntimeError::Graph(e.to_string()))
568        })
569    }
570}
571
572/// Persist an audited generated entity, including pending ledger changes that
573/// may span multiple related entities sharing the same [`EntityRuntimeState`](crate::EntityRuntimeState).
574///
575/// Generated service crates use this as the implementation behind
576/// `entity.audit_as("why").save(&context)`. The audited wrapper is required by the
577/// function signature; no unaudited entity write entry point is exposed.
578#[doc(hidden)]
579pub async fn save_audited_ledger_entity<T>(
580    audited: teaql_core::Audited<T>,
581    context: &UserContext,
582) -> Result<T, RuntimeError>
583where
584    T: crate::LedgerEntity + Send + 'static,
585{
586    let evidence = Arc::new(std::sync::Mutex::new(Vec::new()));
587    let result = GRAPH_FIX_TIME
588        .scope(
589            teaql_core::time::Timestamp::now(),
590            GRAPH_FIX_EVIDENCE.scope(
591                evidence.clone(),
592                save_audited_ledger_entity_inner(audited, context),
593            ),
594        )
595        .await;
596    context.replace_last_fix_evidence(evidence.lock().unwrap().clone());
597    result
598}
599
600/// Persist an audited generated entity through an executor that is already
601/// bound to an outer transaction.
602///
603/// This function never commits or rolls back the executor. The returned
604/// mutation ledger must only be cleared after the owner commits the enclosing
605/// transaction; retaining it on rollback keeps the mutation intent retryable.
606#[doc(hidden)]
607pub async fn save_audited_ledger_entity_with_executor<T, E>(
608    audited: teaql_core::Audited<T>,
609    context: &UserContext,
610    executor: &E,
611) -> Result<(T, Option<crate::EntityRuntimeState>), RuntimeError>
612where
613    T: crate::LedgerEntity + Send + 'static,
614    E: teaql_data_service::QueryExecutor + teaql_data_service::MutationExecutor + Send + Sync,
615{
616    let evidence = Arc::new(std::sync::Mutex::new(Vec::new()));
617    let result = GRAPH_FIX_TIME
618        .scope(
619            teaql_core::time::Timestamp::now(),
620            GRAPH_FIX_EVIDENCE.scope(
621                evidence.clone(),
622                save_audited_ledger_entity_with_executor_inner(audited, context, executor),
623            ),
624        )
625        .await;
626    context.replace_last_fix_evidence(evidence.lock().unwrap().clone());
627    result
628}
629
630async fn save_audited_ledger_entity_with_executor_inner<T, E>(
631    audited: teaql_core::Audited<T>,
632    context: &UserContext,
633    executor: &E,
634) -> Result<(T, Option<crate::EntityRuntimeState>), RuntimeError>
635where
636    T: crate::LedgerEntity + Send + 'static,
637    E: teaql_data_service::QueryExecutor + teaql_data_service::MutationExecutor + Send + Sync,
638{
639    let entity = audited.into_entity();
640    let root = entity.entity_runtime_state();
641    let mut node = graph_node_from_entity(context, entity)?;
642
643    if let Some(root) = root {
644        let root_id = node.values.get("id").cloned().unwrap_or(Value::I64(0));
645        let root_key = crate::EntityKey::new(node.entity.clone(), root_id);
646        if let Some(changes) = root.current_change_set().changes().get(&root_key) {
647            for (field, value) in changes {
648                node.values.insert(field.clone(), value.clone());
649            }
650        }
651        let mut visited = BTreeSet::from([root_key.clone()]);
652        hydrate_ledger_relations(context, &root, &mut node, &mut visited)?;
653        preflight_graph(context, &mut node, &ObjectLocation::root(), Some(&root))?;
654        merge_relation_mutations_into_root(&root, &node)?;
655        let has_ledger_changes = !root.current_change_set().changes().is_empty()
656            || !root.deleted_keys().is_empty()
657            || !root.new_keys().is_empty();
658        if has_ledger_changes {
659            let entity_name = node.entity.clone();
660            let descriptor = context.require_entity(&entity_name)?;
661            let id_property = descriptor.id_property().ok_or_else(|| {
662                RuntimeError::Graph(format!("entity {entity_name} has no id property"))
663            })?;
664            let was_new = root.new_keys().contains(&root_key);
665            let was_deleted = root.deleted_keys().contains(&root_key);
666            let original_version = root.get_original_version(&root_key);
667            let data_service =
668                crate::EntityDataService::for_executor(context, &entity_name, executor);
669            let generated_ids = data_service
670                .execute_ledger_plan_internal(root.clone())
671                .await
672                .map_err(data_service_error_into_runtime)?;
673
674            if let Some(new_id) = generated_ids.get(&root_key) {
675                node.values.insert(id_property.name.clone(), new_id.clone());
676            }
677            if was_deleted {
678                if let Some(version_property) = descriptor.version_property() {
679                    if let Some(version) = saved_version(was_new, true, original_version) {
680                        node.values
681                            .insert(version_property.name.clone(), Value::I64(version));
682                    }
683                }
684            } else {
685                let persisted_id =
686                    node.values.get(&id_property.name).cloned().ok_or_else(|| {
687                        RuntimeError::Graph(format!(
688                            "saved {entity_name} missing identity field {}",
689                            id_property.name
690                        ))
691                    })?;
692                node.values = data_service
693                    .fetch_graph_current_row_internal(
694                        &entity_name,
695                        &id_property.name,
696                        &persisted_id,
697                        Vec::new(),
698                    )
699                    .await
700                    .map_err(data_service_error_into_runtime)?
701                    .map(Into::into)
702                    .ok_or_else(|| {
703                        RuntimeError::Graph(format!(
704                            "persisted {entity_name} record could not be read back"
705                        ))
706                    })?;
707            }
708            let entity = T::from_compact_row(teaql_core::CompactRow::from_map(node.values.into()))
709                .map_err(|error| RuntimeError::Graph(error.to_string()))?;
710            return Ok((entity, Some(root)));
711        }
712    }
713
714    preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
715    let entity_name = node.entity.clone();
716    let saved = crate::EntityDataService::for_executor(context, entity_name, executor)
717        .save_graph_internal(node)
718        .await
719        .map_err(data_service_error_into_runtime)?;
720    let entity = T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
721        .map_err(|error| RuntimeError::Graph(error.to_string()))?;
722    Ok((entity, None))
723}
724
725fn data_service_error_into_runtime<E: std::error::Error>(
726    error: DataServiceError<E>,
727) -> RuntimeError {
728    match error {
729        DataServiceError::Runtime(error) => error,
730        other => RuntimeError::Graph(other.to_string()),
731    }
732}
733
734async fn save_audited_ledger_entity_inner<T>(
735    audited: teaql_core::Audited<T>,
736    context: &UserContext,
737) -> Result<T, RuntimeError>
738where
739    T: crate::LedgerEntity + Send + 'static,
740{
741    let entity_name = T::entity_descriptor().name;
742    let entity = audited.into_entity();
743    let root = entity.entity_runtime_state();
744    let mut node = graph_node_from_entity(context, entity)?;
745    let saver = context
746        .require_resource::<Arc<dyn DynGraphSaver>>()
747        .map_err(|e| {
748            RuntimeError::Graph(format!(
749                "no DynGraphSaver registered — did you call register_executor()? ({e})"
750            ))
751        })?;
752
753    if let Some(root) = root {
754        let root_id = node.values.get("id").cloned().unwrap_or(Value::I64(0));
755        let root_key = crate::EntityKey::new(node.entity.clone(), root_id);
756        if let Some(changes) = root.current_change_set().changes().get(&root_key) {
757            for (field, value) in changes {
758                node.values.insert(field.clone(), value.clone());
759            }
760        }
761        let mut visited = BTreeSet::from([root_key]);
762        hydrate_ledger_relations(context, &root, &mut node, &mut visited)?;
763        preflight_graph(context, &mut node, &ObjectLocation::root(), Some(&root))?;
764        merge_relation_mutations_into_root(&root, &node)?;
765        let has_ledger_changes = !root.current_change_set().changes().is_empty()
766            || !root.deleted_keys().is_empty()
767            || !root.new_keys().is_empty();
768        if has_ledger_changes {
769            let saved = saver.save_ledger_dyn(context, node, root).await?;
770            return T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
771                .map_err(|e| RuntimeError::Graph(e.to_string()));
772        }
773    }
774
775    preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
776    let saved = saver.save_graph_dyn(context, node).await?;
777    T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
778        .map_err(|e| RuntimeError::Graph(e.to_string()))
779}