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
28pub(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
52pub(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
216pub 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
259fn 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
496pub 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(); 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#[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}