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::{DataServiceError, GraphNode, GraphOperation, RuntimeError, UserContext};
10
11// ---------------------------------------------------------------------------
12// DynGraphSaver — type-erased graph save capability
13// ---------------------------------------------------------------------------
14
15/// Object-safe trait for saving a [`GraphNode`] tree to the database.
16///
17/// A concrete implementation is registered in [`UserContext`] during setup so
18/// that [`Audited::save`] can persist entities without exposing the underlying
19/// executor type to business code.
20pub(crate) trait DynGraphSaver: Send + Sync {
21    fn save_graph_dyn<'a>(
22        &'a self,
23        context: &'a UserContext,
24        node: GraphNode,
25    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
26
27    fn save_ledger_dyn<'a>(
28        &'a self,
29        context: &'a UserContext,
30        node: GraphNode,
31        root: crate::EntityRuntimeState,
32    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
33}
34
35/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
36///
37/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
38/// The struct itself is zero-sized; the actual executor is retrieved from
39/// [`UserContext`] at call time.
40pub(crate) struct GraphSaverFor<E> {
41    _marker: PhantomData<fn() -> E>,
42}
43
44impl<E> GraphSaverFor<E> {
45    pub(crate) fn new() -> Self {
46        Self {
47            _marker: PhantomData,
48        }
49    }
50}
51
52impl<E> DynGraphSaver for GraphSaverFor<E>
53where
54    E: teaql_data_service::QueryExecutor
55        + teaql_data_service::MutationExecutor
56        + Send
57        + Sync
58        + 'static,
59{
60    fn save_graph_dyn<'a>(
61        &'a self,
62        context: &'a UserContext,
63        node: GraphNode,
64    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
65        Box::pin(async move {
66            let entity = node.entity.clone();
67            let eds = context
68                .entity_data_service::<E>(entity)
69                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
70            eds.save_graph_internal(node).await.map_err(|e| match e {
71                DataServiceError::Runtime(r) => r,
72                other => RuntimeError::Graph(other.to_string()),
73            })
74        })
75    }
76
77    fn save_ledger_dyn<'a>(
78        &'a self,
79        context: &'a UserContext,
80        mut node: GraphNode,
81        root: crate::EntityRuntimeState,
82    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
83        Box::pin(async move {
84            let entity = node.entity.clone();
85            let eds = context
86                .entity_data_service::<E>(&entity)
87                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
88            let descriptor = context.require_entity(&entity)?;
89            let id_prop = descriptor.id_property().ok_or_else(|| {
90                RuntimeError::Graph(format!("entity {entity} has no id property"))
91            })?;
92            let current_id = node
93                .values
94                .get(&id_prop.name)
95                .cloned()
96                .unwrap_or(Value::I64(0));
97            let root_key = crate::EntityKey::new(entity.clone(), current_id);
98            let was_new = root.new_keys().contains(&root_key);
99            let was_deleted = root.deleted_keys().contains(&root_key);
100            let original_version = root.get_original_version(&root_key);
101            let generated_ids = eds
102                .execute_ledger_plan_internal(root.clone())
103                .await
104                .map_err(|e| match e {
105                    DataServiceError::Runtime(r) => r,
106                    other => RuntimeError::Graph(other.to_string()),
107                })?;
108
109            if let Some(new_id) = generated_ids.get(&root_key) {
110                node.values.insert(id_prop.name.clone(), new_id.clone());
111            }
112            if let Some(changes) = root.current_change_set().changes().get(&root_key) {
113                for (field, value) in changes {
114                    node.values.insert(field.clone(), value.clone());
115                }
116            }
117            if let Some(version_prop) = descriptor.version_property() {
118                let authoritative_version = saved_version(was_new, was_deleted, original_version);
119                if let Some(version) = authoritative_version {
120                    node.values
121                        .insert(version_prop.name.clone(), Value::I64(version));
122                }
123            }
124            root.clear_committed();
125            Ok(node)
126        })
127    }
128}
129
130fn saved_version(was_new: bool, was_deleted: bool, original_version: Option<i64>) -> Option<i64> {
131    if was_new {
132        Some(1)
133    } else if was_deleted {
134        original_version.map(|version| -(version.abs() + 1))
135    } else {
136        original_version.map(|version| version + 1)
137    }
138}
139
140#[cfg(test)]
141mod saved_version_tests {
142    use super::saved_version;
143
144    #[test]
145    fn create_returns_initial_version() {
146        assert_eq!(saved_version(true, false, None), Some(1));
147    }
148
149    #[test]
150    fn update_returns_incremented_version() {
151        assert_eq!(saved_version(false, false, Some(7)), Some(8));
152    }
153
154    #[test]
155    fn delete_returns_next_negative_version() {
156        assert_eq!(saved_version(false, true, Some(7)), Some(-8));
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Standalone graph-node extraction (no executor needed)
162// ---------------------------------------------------------------------------
163
164/// Convert a typed entity into a [`GraphNode`] tree.
165///
166/// This only requires metadata (entity descriptors) from the [`UserContext`],
167/// **not** the database executor.  It is the standalone equivalent of
168/// [`EntityDataService::graph_node_from_entity`].
169pub fn graph_node_from_entity<T: Entity>(
170    context: &UserContext,
171    entity: T,
172) -> Result<GraphNode, RuntimeError> {
173    let descriptor = T::entity_descriptor();
174    let dirty_fields = entity.dirty_fields();
175    let original_values = entity.original_values();
176    let is_new = entity.is_new();
177    let is_deleted = entity.is_marked_as_delete();
178    let comment = entity.get_comment();
179    let mut node = graph_node_from_values(context, &descriptor.name, entity.into_values())?;
180    node.dirty_fields = dirty_fields;
181    node.original_values = original_values.map(Into::into);
182    if is_new {
183        node.operation = GraphOperation::Create;
184    }
185    if is_deleted {
186        node.operation = GraphOperation::Remove;
187        node.relations.clear();
188    }
189    if let Some(c) = comment {
190        node.set_comment(c);
191    }
192    Ok(node)
193}
194
195/// Recursively convert entity mutation values into a [`GraphNode`] tree.
196///
197/// Relations are resolved via the entity descriptors stored in `context`.
198fn graph_node_from_values(
199    context: &UserContext,
200    entity: &str,
201    values: MutationValues,
202) -> Result<GraphNode, RuntimeError> {
203    let descriptor = context.require_entity(entity)?;
204    let mut node = GraphNode::new(entity);
205
206    for (field, value) in values {
207        if field == "_comment" {
208            if let Value::Text(comment) = value {
209                node.set_comment(comment);
210            }
211            continue;
212        }
213        if field == "_dirty_fields" {
214            if let Value::List(fields) = value {
215                let mut dirty = BTreeSet::new();
216                for f in fields {
217                    if let Value::Text(t) = f {
218                        dirty.insert(t);
219                    }
220                }
221                node.dirty_fields = Some(dirty);
222            }
223            continue;
224        }
225        if field == "_original_values" {
226            if let Value::Object(orig) = value {
227                node.original_values = Some(orig.into());
228            }
229            continue;
230        }
231        if field == "_is_new" {
232            if matches!(value, Value::Bool(true)) {
233                node.operation = GraphOperation::Create;
234            }
235            continue;
236        }
237        if field == "_is_deleted" {
238            if matches!(value, Value::Bool(true)) {
239                node.operation = GraphOperation::Remove;
240            }
241            continue;
242        }
243        let Some(relation) = descriptor.relation_by_name(&field) else {
244            node.values.insert(field, value);
245            continue;
246        };
247
248        match value {
249            Value::Null => {
250                node.relations.entry(field).or_default();
251            }
252            Value::Object(record) => {
253                let child =
254                    graph_node_from_values(context, &relation.target_entity, record.into())?;
255                node.relations.entry(field).or_default().push(child);
256            }
257            Value::List(values) => {
258                let children = node.relations.entry(field.clone()).or_default();
259                for value in values {
260                    let Value::Object(record) = value else {
261                        return Err(RuntimeError::Graph(format!(
262                            "relation {}.{} expects object children, got {:?}",
263                            entity, field, value
264                        )));
265                    };
266                    children.push(graph_node_from_values(
267                        context,
268                        &relation.target_entity,
269                        record.into(),
270                    )?);
271                }
272            }
273            other => {
274                return Err(RuntimeError::Graph(format!(
275                    "relation {}.{} expects object/list/null, got {:?}",
276                    entity, field, other
277                )));
278            }
279        }
280    }
281
282    Ok(node)
283}
284
285fn merge_relation_mutations_into_root(
286    root: &crate::EntityRuntimeState,
287    node: &GraphNode,
288) -> Result<(), RuntimeError> {
289    for children in node.relations.values() {
290        for child in children {
291            let id = child.values.get("id").cloned().ok_or_else(|| {
292                RuntimeError::Graph(format!(
293                    "related mutation {} is missing its id",
294                    child.entity
295                ))
296            })?;
297            let key = crate::EntityKey::new(child.entity.clone(), id);
298
299            match child.operation {
300                GraphOperation::Create => {
301                    root.mark_as_new(key.clone());
302                    for (field, value) in &child.values {
303                        root.set(key.clone(), field, value.clone());
304                    }
305                }
306                GraphOperation::Upsert => {
307                    if let Some(fields) = &child.dirty_fields {
308                        for field in fields {
309                            if let Some(value) = child.values.get(field) {
310                                root.set(key.clone(), field, value.clone());
311                            }
312                        }
313                    }
314                }
315                GraphOperation::Remove => root.mark_as_delete(key.clone()),
316                GraphOperation::Reference => {}
317            }
318
319            if let Some(version) = child
320                .original_values
321                .as_ref()
322                .and_then(|values| values.get("version"))
323                .and_then(Value::try_i64)
324            {
325                root.set_original_version(key, version);
326            }
327            merge_relation_mutations_into_root(root, child)?;
328        }
329    }
330    Ok(())
331}
332
333// ---------------------------------------------------------------------------
334// AuditedSaveExt — the `.save(&context)` method on `Audited<T>`
335// ---------------------------------------------------------------------------
336
337/// Extension trait that provides the `.save(&context)` method on [`Audited<T>`](teaql_core::Audited).
338///
339/// # Example
340/// ```ignore
341/// use teaql_runtime::AuditedSaveExt;
342///
343/// school.audit_as("创建学校").save(&context).await?;
344/// ```
345pub trait AuditedSaveExt {
346    type Entity;
347
348    fn save<'a>(
349        self,
350        context: &'a UserContext,
351    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
352}
353
354impl<T> AuditedSaveExt for teaql_core::Audited<T>
355where
356    T: Entity + Send + 'static,
357{
358    type Entity = T;
359
360    fn save<'a>(
361        self,
362        context: &'a UserContext,
363    ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
364        Box::pin(async move {
365            let entity_name = T::entity_descriptor().name;
366            let entity = self.into_entity(); // applies comment onto the entity
367            let node = graph_node_from_entity(context, entity)?;
368            let saver = context
369                .require_resource::<Arc<dyn DynGraphSaver>>()
370                .map_err(|e| {
371                    RuntimeError::Graph(format!(
372                        "no DynGraphSaver registered — did you call register_executor()? ({})",
373                        e
374                    ))
375                })?;
376            let saved = saver.save_graph_dyn(context, node).await?;
377            T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
378                .map_err(|e| RuntimeError::Graph(e.to_string()))
379        })
380    }
381}
382
383/// Persist an audited generated entity, including pending ledger changes that
384/// may span multiple related entities sharing the same [`EntityRuntimeState`](crate::EntityRuntimeState).
385///
386/// Generated service crates use this as the implementation behind
387/// `entity.audit_as("why").save(&context)`. The audited wrapper is required by the
388/// function signature; no unaudited entity write entry point is exposed.
389#[doc(hidden)]
390pub async fn save_audited_ledger_entity<T>(
391    audited: teaql_core::Audited<T>,
392    context: &UserContext,
393) -> Result<T, RuntimeError>
394where
395    T: crate::LedgerEntity + Send + 'static,
396{
397    let entity_name = T::entity_descriptor().name;
398    let entity = audited.into_entity();
399    let root = entity.entity_runtime_state();
400    let node = graph_node_from_entity(context, entity)?;
401    let saver = context
402        .require_resource::<Arc<dyn DynGraphSaver>>()
403        .map_err(|e| {
404            RuntimeError::Graph(format!(
405                "no DynGraphSaver registered — did you call register_executor()? ({e})"
406            ))
407        })?;
408
409    if let Some(root) = root {
410        merge_relation_mutations_into_root(&root, &node)?;
411        let has_ledger_changes = !root.current_change_set().changes().is_empty()
412            || !root.deleted_keys().is_empty()
413            || !root.new_keys().is_empty();
414        if has_ledger_changes {
415            let saved = saver.save_ledger_dyn(context, node, root).await?;
416            return T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
417                .map_err(|e| RuntimeError::Graph(e.to_string()));
418        }
419    }
420
421    let saved = saver.save_graph_dyn(context, node).await?;
422    T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
423        .map_err(|e| RuntimeError::Graph(e.to_string()))
424}