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 let Some(changes) = root.current_change_set().changes().get(&root_key) {
169                for (field, value) in changes {
170                    node.values.insert(field.clone(), value.clone());
171                }
172            }
173            if let Some(version_prop) = descriptor.version_property() {
174                let authoritative_version = saved_version(was_new, was_deleted, original_version);
175                if let Some(version) = authoritative_version {
176                    node.values
177                        .insert(version_prop.name.clone(), Value::I64(version));
178                }
179            }
180            root.clear_committed();
181            Ok(node)
182        })
183    }
184}
185
186fn saved_version(was_new: bool, was_deleted: bool, original_version: Option<i64>) -> Option<i64> {
187    if was_new {
188        Some(1)
189    } else if was_deleted {
190        original_version.map(|version| -(version.abs() + 1))
191    } else {
192        original_version.map(|version| version + 1)
193    }
194}
195
196#[cfg(test)]
197mod saved_version_tests {
198    use super::saved_version;
199
200    #[test]
201    fn create_returns_initial_version() {
202        assert_eq!(saved_version(true, false, None), Some(1));
203    }
204
205    #[test]
206    fn update_returns_incremented_version() {
207        assert_eq!(saved_version(false, false, Some(7)), Some(8));
208    }
209
210    #[test]
211    fn delete_returns_next_negative_version() {
212        assert_eq!(saved_version(false, true, Some(7)), Some(-8));
213    }
214}
215
216// ---------------------------------------------------------------------------
217// Standalone graph-node extraction (no executor needed)
218// ---------------------------------------------------------------------------
219
220/// Convert a typed entity into a [`GraphNode`] tree.
221///
222/// This only requires metadata (entity descriptors) from the [`UserContext`],
223/// **not** the database executor.  It is the standalone equivalent of
224/// [`EntityDataService::graph_node_from_entity`].
225pub fn graph_node_from_entity<T: Entity>(
226    context: &UserContext,
227    entity: T,
228) -> Result<GraphNode, RuntimeError> {
229    let descriptor = T::entity_descriptor();
230    let loaded_fields = descriptor
231        .properties
232        .iter()
233        .filter(|property| entity.is_field_loaded(&property.name))
234        .map(|property| Value::Text(property.name.clone()))
235        .collect::<Vec<_>>();
236    let dirty_fields = entity.dirty_fields();
237    let original_values = entity.original_values();
238    let is_new = entity.is_new();
239    let is_deleted = entity.is_marked_as_delete();
240    let comment = entity.get_comment();
241    let mut node = graph_node_from_values(context, &descriptor.name, entity.into_values())?;
242    node.values
243        .insert("_loaded_fields".to_owned(), Value::List(loaded_fields));
244    node.dirty_fields = dirty_fields;
245    node.original_values = original_values.map(Into::into);
246    if is_new {
247        node.operation = GraphOperation::Create;
248    }
249    if is_deleted {
250        node.operation = GraphOperation::Remove;
251        node.relations.clear();
252    }
253    if let Some(c) = comment {
254        node.set_comment(c);
255    }
256    Ok(node)
257}
258
259/// Recursively convert entity mutation values into a [`GraphNode`] tree.
260///
261/// Relations are resolved via the entity descriptors stored in `context`.
262fn graph_node_from_values(
263    context: &UserContext,
264    entity: &str,
265    values: MutationValues,
266) -> Result<GraphNode, RuntimeError> {
267    let descriptor = context.require_entity(entity)?;
268    let mut node = GraphNode::new(entity);
269
270    for (field, value) in values {
271        if field == "_comment" {
272            if let Value::Text(comment) = value {
273                node.set_comment(comment);
274            }
275            continue;
276        }
277        if field == "_dirty_fields" {
278            if let Value::List(fields) = value {
279                let mut dirty = BTreeSet::new();
280                for f in fields {
281                    if let Value::Text(t) = f {
282                        dirty.insert(t);
283                    }
284                }
285                node.dirty_fields = Some(dirty);
286            }
287            continue;
288        }
289        if field == "_original_values" {
290            if let Value::Object(orig) = value {
291                node.original_values = Some(orig.into());
292            }
293            continue;
294        }
295        if field == "_is_new" {
296            if matches!(value, Value::Bool(true)) {
297                node.operation = GraphOperation::Create;
298            }
299            continue;
300        }
301        if field == "_is_deleted" {
302            if matches!(value, Value::Bool(true)) {
303                node.operation = GraphOperation::Remove;
304            }
305            continue;
306        }
307        let Some(relation) = descriptor.relation_by_name(&field) else {
308            node.values.insert(field, value);
309            continue;
310        };
311
312        match value {
313            Value::Null => {
314                node.relations.entry(field).or_default();
315            }
316            Value::Object(record) => {
317                let child =
318                    graph_node_from_values(context, &relation.target_entity, record.into())?;
319                node.relations.entry(field).or_default().push(child);
320            }
321            Value::List(values) => {
322                let children = node.relations.entry(field.clone()).or_default();
323                for value in values {
324                    let Value::Object(record) = value else {
325                        return Err(RuntimeError::Graph(format!(
326                            "relation {}.{} expects object children, got {:?}",
327                            entity, field, value
328                        )));
329                    };
330                    children.push(graph_node_from_values(
331                        context,
332                        &relation.target_entity,
333                        record.into(),
334                    )?);
335                }
336            }
337            other => {
338                return Err(RuntimeError::Graph(format!(
339                    "relation {}.{} expects object/list/null, got {:?}",
340                    entity, field, other
341                )));
342            }
343        }
344    }
345
346    Ok(node)
347}
348
349fn merge_relation_mutations_into_root(
350    root: &crate::EntityRuntimeState,
351    node: &GraphNode,
352) -> Result<(), RuntimeError> {
353    for children in node.relations.values() {
354        for child in children {
355            let id = child.values.get("id").cloned().ok_or_else(|| {
356                RuntimeError::Graph(format!(
357                    "related mutation {} is missing its id",
358                    child.entity
359                ))
360            })?;
361            let key = crate::EntityKey::new(child.entity.clone(), id);
362
363            match child.operation {
364                GraphOperation::Create => {
365                    root.mark_as_new(key.clone());
366                    for (field, value) in &child.values {
367                        root.set(key.clone(), field, value.clone());
368                    }
369                }
370                GraphOperation::Upsert => {
371                    if let Some(fields) = &child.dirty_fields {
372                        for field in fields {
373                            if let Some(value) = child.values.get(field) {
374                                root.set(key.clone(), field, value.clone());
375                            }
376                        }
377                    }
378                }
379                GraphOperation::Remove => root.mark_as_delete(key.clone()),
380                GraphOperation::Reference => {}
381            }
382
383            if let Some(version) = child
384                .original_values
385                .as_ref()
386                .and_then(|values| values.get("version"))
387                .and_then(Value::try_i64)
388            {
389                root.set_original_version(key, version);
390            }
391            merge_relation_mutations_into_root(root, child)?;
392        }
393    }
394    Ok(())
395}
396
397fn hydrate_ledger_relations(
398    context: &UserContext,
399    root: &crate::EntityRuntimeState,
400    node: &mut GraphNode,
401    visited: &mut BTreeSet<crate::EntityKey>,
402) -> Result<(), RuntimeError> {
403    let descriptor = context.require_entity(&node.entity)?;
404    for relation in &descriptor.relations {
405        let Some(local_value) = node.values.get(&relation.local_key).cloned() else {
406            continue;
407        };
408        let existing = node.relations.entry(relation.name.clone()).or_default();
409        let existing_keys = existing
410            .iter()
411            .filter_map(|child| {
412                child
413                    .values
414                    .get("id")
415                    .cloned()
416                    .map(|id| crate::EntityKey::new(child.entity.clone(), id))
417            })
418            .collect::<BTreeSet<_>>();
419        let mut discovered = Vec::new();
420        for (key, changes) in root.current_change_set().changes() {
421            if key.entity.as_ref() != relation.target_entity || existing_keys.contains(key) {
422                continue;
423            }
424            let foreign_value = if relation.foreign_key == "id" {
425                Some(&key.id)
426            } else {
427                changes.get(&relation.foreign_key)
428            };
429            if foreign_value != Some(&local_value) || !visited.insert(key.clone()) {
430                continue;
431            }
432            let mut values: crate::EntityValues = changes.clone().into();
433            values
434                .entry("id".to_owned())
435                .or_insert_with(|| key.id.clone());
436            let operation = if root.deleted_keys().contains(key) {
437                GraphOperation::Remove
438            } else if root.new_keys().contains(key) || root.get_original_version(key).is_none() {
439                GraphOperation::Create
440            } else {
441                GraphOperation::Upsert
442            };
443            let mut child = GraphNode::new(key.entity.to_string());
444            child.values = values;
445            child.operation = operation;
446            hydrate_ledger_relations(context, root, &mut child, visited)?;
447            discovered.push(child);
448        }
449        existing.extend(discovered);
450    }
451    Ok(())
452}
453
454fn preflight_graph(
455    context: &UserContext,
456    node: &mut GraphNode,
457    location: &ObjectLocation,
458    root: Option<&crate::EntityRuntimeState>,
459) -> Result<(), RuntimeError> {
460    if !matches!(
461        node.operation,
462        GraphOperation::Remove | GraphOperation::Reference
463    ) {
464        let before = node.values.clone();
465        let status = match node.operation {
466            GraphOperation::Create => crate::CheckObjectStatus::Create,
467            GraphOperation::Upsert => crate::CheckObjectStatus::Update,
468            GraphOperation::Remove | GraphOperation::Reference => unreachable!(),
469        };
470        crate::mark_entity_status(&mut node.values, status);
471        let result = context.check_and_fix_values_at(&node.entity, &mut node.values, location);
472        crate::clear_entity_status(&mut node.values);
473        result?;
474
475        if let Some(root) = root {
476            if let Some(id) = node.values.get("id").cloned() {
477                let key = crate::EntityKey::new(node.entity.clone(), id);
478                for (field, value) in &node.values {
479                    if before.get(field) != Some(value) {
480                        root.set(key.clone(), field.clone(), value.clone());
481                    }
482                }
483            }
484        }
485    }
486
487    for (relation, children) in &mut node.relations {
488        for (index, child) in children.iter_mut().enumerate() {
489            let child_location = location.clone().member(relation).element(index);
490            preflight_graph(context, child, &child_location, root)?;
491        }
492    }
493    Ok(())
494}
495
496// ---------------------------------------------------------------------------
497// AuditedSaveExt — the `.save(&context)` method on `Audited<T>`
498// ---------------------------------------------------------------------------
499
500/// Extension trait that provides the `.save(&context)` method on [`Audited<T>`](teaql_core::Audited).
501///
502/// # Example
503/// ```ignore
504/// use teaql_runtime::AuditedSaveExt;
505///
506/// school.audit_as("创建学校").save(&context).await?;
507/// ```
508pub trait AuditedSaveExt {
509    type Entity;
510
511    fn save<'a>(
512        self,
513        context: &'a UserContext,
514    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
515}
516
517impl<T> AuditedSaveExt for teaql_core::Audited<T>
518where
519    T: Entity + Send + 'static,
520{
521    type Entity = T;
522
523    fn save<'a>(
524        self,
525        context: &'a UserContext,
526    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
527        Box::pin(async move {
528            let entity_name = T::entity_descriptor().name;
529            let entity = self.into_entity(); // applies comment onto the entity
530            let mut node = graph_node_from_entity(context, entity)?;
531            preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
532            let saver = context
533                .require_resource::<Arc<dyn DynGraphSaver>>()
534                .map_err(|e| {
535                    RuntimeError::Graph(format!(
536                        "no DynGraphSaver registered — did you call register_executor()? ({})",
537                        e
538                    ))
539                })?;
540            let saved = saver.save_graph_dyn(context, node).await?;
541            T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
542                .map_err(|e| RuntimeError::Graph(e.to_string()))
543        })
544    }
545}
546
547/// Persist an audited generated entity, including pending ledger changes that
548/// may span multiple related entities sharing the same [`EntityRuntimeState`](crate::EntityRuntimeState).
549///
550/// Generated service crates use this as the implementation behind
551/// `entity.audit_as("why").save(&context)`. The audited wrapper is required by the
552/// function signature; no unaudited entity write entry point is exposed.
553#[doc(hidden)]
554pub async fn save_audited_ledger_entity<T>(
555    audited: teaql_core::Audited<T>,
556    context: &UserContext,
557) -> Result<T, RuntimeError>
558where
559    T: crate::LedgerEntity + Send + 'static,
560{
561    let evidence = Arc::new(std::sync::Mutex::new(Vec::new()));
562    let result = GRAPH_FIX_TIME
563        .scope(
564            teaql_core::time::Timestamp::now(),
565            GRAPH_FIX_EVIDENCE.scope(
566                evidence.clone(),
567                save_audited_ledger_entity_inner(audited, context),
568            ),
569        )
570        .await;
571    context.replace_last_fix_evidence(evidence.lock().unwrap().clone());
572    result
573}
574
575async fn save_audited_ledger_entity_inner<T>(
576    audited: teaql_core::Audited<T>,
577    context: &UserContext,
578) -> Result<T, RuntimeError>
579where
580    T: crate::LedgerEntity + Send + 'static,
581{
582    let entity_name = T::entity_descriptor().name;
583    let entity = audited.into_entity();
584    let root = entity.entity_runtime_state();
585    let mut node = graph_node_from_entity(context, entity)?;
586    let saver = context
587        .require_resource::<Arc<dyn DynGraphSaver>>()
588        .map_err(|e| {
589            RuntimeError::Graph(format!(
590                "no DynGraphSaver registered — did you call register_executor()? ({e})"
591            ))
592        })?;
593
594    if let Some(root) = root {
595        let root_id = node.values.get("id").cloned().unwrap_or(Value::I64(0));
596        let root_key = crate::EntityKey::new(node.entity.clone(), root_id);
597        if let Some(changes) = root.current_change_set().changes().get(&root_key) {
598            for (field, value) in changes {
599                node.values.insert(field.clone(), value.clone());
600            }
601        }
602        let mut visited = BTreeSet::from([root_key]);
603        hydrate_ledger_relations(context, &root, &mut node, &mut visited)?;
604        preflight_graph(context, &mut node, &ObjectLocation::root(), Some(&root))?;
605        merge_relation_mutations_into_root(&root, &node)?;
606        let has_ledger_changes = !root.current_change_set().changes().is_empty()
607            || !root.deleted_keys().is_empty()
608            || !root.new_keys().is_empty();
609        if has_ledger_changes {
610            let saved = saver.save_ledger_dyn(context, node, root).await?;
611            return T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
612                .map_err(|e| RuntimeError::Graph(e.to_string()));
613        }
614    }
615
616    preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
617    let saved = saver.save_graph_dyn(context, node).await?;
618    T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
619        .map_err(|e| RuntimeError::Graph(e.to_string()))
620}