1use std::any::{Any, TypeId};
2use std::borrow::Cow;
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex, OnceLock, Weak};
5
6use teaql_core::{EntitySnapshot, MutationValues, SmartList, Value};
7
8#[derive(Debug, Clone)]
9pub struct EntityKey {
10 pub entity: Cow<'static, str>,
11 pub id: Value,
12 id_key: EntityIdentityKey,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16enum EntityIdentityKey {
17 Null,
18 Bool(bool),
19 I64(i64),
20 U64(u64),
21 F64(u64),
22 Decimal(rust_decimal::Decimal),
23 Text(String),
24 Date(chrono::NaiveDate),
25 Timestamp(i64),
26 Other(String),
27}
28
29impl EntityKey {
30 pub fn new(entity: impl Into<String>, id: impl Into<Value>) -> Self {
31 let id = id.into();
32 Self {
33 entity: Cow::Owned(entity.into()),
34 id_key: entity_identity_key(&id),
35 id,
36 }
37 }
38
39 pub fn new_static(entity: &'static str, id: impl Into<Value>) -> Self {
40 let id = id.into();
41 Self {
42 entity: Cow::Borrowed(entity),
43 id_key: entity_identity_key(&id),
44 id,
45 }
46 }
47}
48
49impl PartialEq for EntityKey {
50 fn eq(&self, other: &Self) -> bool {
51 self.entity == other.entity && self.id_key == other.id_key
52 }
53}
54
55impl Eq for EntityKey {}
56
57impl PartialOrd for EntityKey {
58 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
59 Some(self.cmp(other))
60 }
61}
62
63impl Ord for EntityKey {
64 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
65 self.entity
66 .cmp(&other.entity)
67 .then_with(|| self.id_key.cmp(&other.id_key))
68 }
69}
70
71fn entity_identity_key(value: &Value) -> EntityIdentityKey {
72 match value {
73 Value::Null | Value::TypedNull(_) => EntityIdentityKey::Null,
74 Value::Bool(value) => EntityIdentityKey::Bool(*value),
75 Value::I64(value) => EntityIdentityKey::I64(*value),
76 Value::U64(value) => EntityIdentityKey::U64(*value),
77 Value::F64(value) => EntityIdentityKey::F64(value.to_bits()),
78 Value::Decimal(value) => EntityIdentityKey::Decimal(*value),
79 Value::Text(value) => EntityIdentityKey::Text(value.clone()),
80 Value::Json(value) => EntityIdentityKey::Other(format!("json:{value}")),
81 Value::Date(value) => EntityIdentityKey::Date(*value),
82 Value::Timestamp(value) => EntityIdentityKey::Timestamp(value.0),
83 Value::Object(_) => EntityIdentityKey::Other("object".to_owned()),
84 Value::List(_) => EntityIdentityKey::Other("list".to_owned()),
85 }
86}
87
88#[derive(Default)]
89pub struct EntityGraphBuilder {
90 tables: HashMap<TypeId, EntityTable>,
91 relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
92}
93
94type EntityTable = HashMap<u64, Box<dyn Any + Send + Sync>>;
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97struct RelationListKey {
98 owner_entity: String,
99 owner_id: u64,
100 relation: String,
101}
102
103impl EntityGraphBuilder {
104 pub fn install<T>(&mut self, id: u64, entity: T)
105 where
106 T: Any + Send + Sync,
107 {
108 self.tables
109 .entry(TypeId::of::<T>())
110 .or_default()
111 .insert(id, Box::new(entity));
112 }
113
114 pub fn entity_count(&self) -> usize {
115 self.tables.values().map(HashMap::len).sum()
116 }
117
118 pub fn install_relation_list<T>(
119 &mut self,
120 owner_entity: impl Into<String>,
121 owner_id: u64,
122 relation: impl Into<String>,
123 list: SmartList<T>,
124 ) where
125 T: Any + Send + Sync,
126 {
127 self.relation_lists.insert(
128 RelationListKey {
129 owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
130 owner_id,
131 relation: relation.into(),
132 },
133 Box::new(list),
134 );
135 }
136
137 pub fn install_relation_option<T>(
138 &mut self,
139 owner_entity: impl Into<String>,
140 owner_id: u64,
141 relation: impl Into<String>,
142 value: Option<T>,
143 ) where
144 T: Any + Send + Sync,
145 {
146 self.relation_lists.insert(
147 RelationListKey {
148 owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
149 owner_id,
150 relation: relation.into(),
151 },
152 Box::new(value),
153 );
154 }
155
156 pub fn relation_list_count(&self) -> usize {
157 self.relation_lists.len()
158 }
159
160 fn freeze(self) -> FrozenEntityGraph {
161 FrozenEntityGraph {
162 tables: self.tables,
163 relation_lists: self.relation_lists,
164 }
165 }
166}
167
168impl std::fmt::Debug for EntityGraphBuilder {
169 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 formatter
171 .debug_struct("EntityGraphBuilder")
172 .field("entity_types", &self.tables.len())
173 .field("entities", &self.entity_count())
174 .field("relation_lists", &self.relation_list_count())
175 .finish()
176 }
177}
178
179struct FrozenEntityGraph {
180 tables: HashMap<TypeId, EntityTable>,
181 relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
182}
183
184impl std::fmt::Debug for FrozenEntityGraph {
185 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 formatter
187 .debug_struct("FrozenEntityGraph")
188 .field("entity_types", &self.tables.len())
189 .field(
190 "entities",
191 &self.tables.values().map(HashMap::len).sum::<usize>(),
192 )
193 .field("relation_lists", &self.relation_lists.len())
194 .finish()
195 }
196}
197
198#[derive(Debug, Clone, Default, PartialEq)]
199pub struct EntityChangeSet {
200 changes: BTreeMap<EntityKey, MutationValues>,
201}
202
203#[derive(Debug, Default)]
204struct OriginalVersions {
205 first: Option<(EntityKey, i64)>,
206 overflow: BTreeMap<EntityKey, i64>,
207}
208
209impl OriginalVersions {
210 fn clear(&mut self) {
211 self.first = None;
212 self.overflow.clear();
213 }
214
215 fn get(&self, key: &EntityKey) -> Option<i64> {
216 self.first
217 .as_ref()
218 .and_then(|(first_key, version)| (first_key == key).then_some(*version))
219 .or_else(|| self.overflow.get(key).copied())
220 }
221
222 fn insert(&mut self, key: EntityKey, version: i64) {
223 match &mut self.first {
224 None => self.first = Some((key, version)),
225 Some((first_key, first_version)) if first_key == &key => *first_version = version,
226 Some(_) => {
227 self.overflow.insert(key, version);
228 }
229 }
230 }
231}
232
233impl EntityChangeSet {
234 pub fn is_empty(&self) -> bool {
235 self.changes.is_empty()
236 }
237
238 pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
239 self.changes
240 .entry(key)
241 .or_default()
242 .insert(field.into(), value);
243 }
244
245 pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
246 self.changes.get(key).and_then(|changes| changes.get(field))
247 }
248
249 pub fn changes(&self) -> &BTreeMap<EntityKey, MutationValues> {
250 &self.changes
251 }
252
253 pub fn clear_entity(&mut self, key: &EntityKey) {
255 self.changes.remove(key);
256 }
257
258 pub fn field_names(&self, key: &EntityKey) -> BTreeSet<String> {
260 self.changes
261 .get(key)
262 .map(|record| record.keys().cloned().collect())
263 .unwrap_or_default()
264 }
265}
266
267#[derive(Debug, Clone, Default, PartialEq)]
268pub struct ChangeSetStack {
269 stack: Vec<EntityChangeSet>,
270}
271
272impl ChangeSetStack {
273 pub fn current_mut(&mut self) -> &mut EntityChangeSet {
274 if self.stack.is_empty() {
275 self.stack.push(EntityChangeSet::default());
276 }
277 self.stack.last_mut().expect("change set stack has current")
278 }
279
280 pub fn current(&self) -> Option<&EntityChangeSet> {
281 self.stack.last()
282 }
283
284 pub fn push(&mut self) {
285 self.stack.push(EntityChangeSet::default());
286 }
287
288 pub fn pop(&mut self) -> Option<EntityChangeSet> {
289 self.stack.pop()
290 }
291
292 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
293 self.stack
294 .iter()
295 .rev()
296 .find_map(|change_set| change_set.get(key, field).cloned())
297 }
298
299 pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
300 self.current_mut().set(key, field, value);
301 }
302
303 pub fn clear_current(&mut self) {
304 if let Some(current) = self.stack.last_mut() {
305 *current = EntityChangeSet::default();
306 }
307 }
308
309 pub fn clear_entity(&mut self, key: &EntityKey) {
311 for change_set in &mut self.stack {
312 change_set.clear_entity(key);
313 }
314 }
315
316 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
319 let mut fields = BTreeSet::new();
320 for change_set in &self.stack {
321 fields.extend(change_set.field_names(key));
322 }
323 fields
324 }
325}
326
327#[derive(Debug, Default)]
328pub struct RootContext {
329 change_sets: ChangeSetStack,
330 comment: Option<String>,
332 deleted_keys: std::collections::BTreeSet<EntityKey>,
335 new_keys: std::collections::BTreeSet<EntityKey>,
337 original_snapshot: Option<OriginalSnapshot>,
339 trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
341 original_versions: OriginalVersions,
343 is_new: bool,
345}
346
347#[derive(Debug)]
348pub struct EntityRoot {
349 inner: OnceLock<Arc<Mutex<RootContext>>>,
350 graph: EntityGraphReference,
351 loaded_snapshot: Option<teaql_core::CompactRow>,
352}
353
354#[derive(Debug)]
355enum EntityGraphReference {
356 Strong(Arc<OnceLock<FrozenEntityGraph>>),
357 Weak(Weak<OnceLock<FrozenEntityGraph>>),
358}
359
360impl EntityGraphReference {
361 fn preserve(&self) -> Self {
362 match self {
363 Self::Strong(graph) => Self::Strong(graph.clone()),
364 Self::Weak(graph) => Self::Weak(graph.clone()),
365 }
366 }
367
368 fn promote(&self) -> Self {
369 match self {
370 Self::Strong(graph) => Self::Strong(graph.clone()),
371 Self::Weak(graph) => graph
372 .upgrade()
373 .map(Self::Strong)
374 .unwrap_or_else(|| Self::Strong(Arc::default())),
375 }
376 }
377
378 fn weak(&self) -> Self {
379 match self {
380 Self::Strong(graph) => Self::Weak(Arc::downgrade(graph)),
381 Self::Weak(graph) => Self::Weak(graph.clone()),
382 }
383 }
384
385 fn pointer(&self) -> *const OnceLock<FrozenEntityGraph> {
386 match self {
387 Self::Strong(graph) => Arc::as_ptr(graph),
388 Self::Weak(graph) => graph.as_ptr(),
389 }
390 }
391
392 fn strong(&self) -> Option<&Arc<OnceLock<FrozenEntityGraph>>> {
393 match self {
394 Self::Strong(graph) => Some(graph),
395 Self::Weak(_) => None,
396 }
397 }
398
399 fn frozen(&self) -> Option<&FrozenEntityGraph> {
400 match self {
401 Self::Strong(graph) => graph.get(),
402 Self::Weak(graph) => {
403 let owner = graph.upgrade()?;
404 let frozen = owner.get()? as *const FrozenEntityGraph;
405 Some(unsafe { &*frozen })
410 }
411 }
412 }
413}
414
415impl Default for EntityRoot {
416 fn default() -> Self {
417 Self {
418 inner: OnceLock::new(),
419 graph: EntityGraphReference::Strong(Arc::default()),
420 loaded_snapshot: None,
421 }
422 }
423}
424
425impl Clone for EntityRoot {
426 fn clone(&self) -> Self {
427 let inner = OnceLock::new();
428 if let Some(context) = self.inner.get() {
429 let _ = inner.set(context.clone());
430 }
431 Self {
432 inner,
433 graph: self.graph.promote(),
434 loaded_snapshot: self.loaded_snapshot.clone(),
435 }
436 }
437}
438
439impl std::panic::UnwindSafe for EntityRoot {}
440impl std::panic::RefUnwindSafe for EntityRoot {}
441
442#[derive(Debug)]
443enum OriginalSnapshot {
444 Materialized(EntitySnapshot),
445 Compact(teaql_core::CompactRow),
446}
447
448impl PartialEq for EntityRoot {
449 fn eq(&self, other: &Self) -> bool {
450 match (self.inner.get(), other.inner.get()) {
451 (Some(left), Some(right)) => Arc::ptr_eq(left, right),
452 (None, None) => {
453 self.graph.pointer() == other.graph.pointer()
454 && self.loaded_snapshot == other.loaded_snapshot
455 }
456 _ => false,
457 }
458 }
459}
460
461impl EntityRoot {
462 #[cfg(test)]
463 fn has_mutation_context(&self) -> bool {
464 self.inner.get().is_some()
465 }
466
467 fn context(&self) -> &Arc<Mutex<RootContext>> {
468 self.inner
469 .get_or_init(|| Arc::new(Mutex::new(RootContext::default())))
470 }
471
472 fn read_context<R>(&self, default: R, read: impl FnOnce(&RootContext) -> R) -> R {
473 let Some(context) = self.inner.get() else {
474 return default;
475 };
476 let context = context.lock().unwrap_or_else(|error| error.into_inner());
477 read(&context)
478 }
479
480 fn write_context<R>(&self, write: impl FnOnce(&mut RootContext) -> R) -> R {
481 let mut context = self
482 .context()
483 .lock()
484 .unwrap_or_else(|error| error.into_inner());
485 write(&mut context)
486 }
487
488 pub fn fresh_with_shared_graph(source: &EntityRoot) -> Self {
489 Self {
490 inner: OnceLock::new(),
491 graph: source.graph.preserve(),
492 loaded_snapshot: None,
493 }
494 }
495
496 pub(crate) fn fresh_with_weak_graph(source: &EntityRoot) -> Self {
499 Self {
500 inner: OnceLock::new(),
501 graph: source.graph.weak(),
502 loaded_snapshot: None,
503 }
504 }
505
506 pub fn with_shared_graph(&self, source: &EntityRoot) -> Self {
509 Self {
510 inner: self.inner.clone(),
511 graph: source.graph.preserve(),
512 loaded_snapshot: self.loaded_snapshot.clone(),
513 }
514 }
515
516 pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
518 let Some(graph) = self.graph.strong() else {
519 return Err(builder);
520 };
521 graph
522 .set(builder.freeze())
523 .map_err(|graph| EntityGraphBuilder {
524 tables: graph.tables,
525 relation_lists: graph.relation_lists,
526 })
527 }
528
529 pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
531 where
532 T: Any + Send + Sync,
533 {
534 self.graph
535 .frozen()?
536 .tables
537 .get(&TypeId::of::<T>())?
538 .get(&id)?
539 .downcast_ref::<T>()
540 }
541
542 pub fn resolve_relation_list<T>(
543 &self,
544 owner_entity: &str,
545 owner_id: u64,
546 relation: &str,
547 ) -> Option<&SmartList<T>>
548 where
549 T: Any + Send + Sync,
550 {
551 self.graph
552 .frozen()?
553 .relation_lists
554 .get(&RelationListKey {
555 owner_entity: crate::canonical_id_space_entity(owner_entity),
556 owner_id,
557 relation: relation.to_owned(),
558 })?
559 .downcast_ref::<SmartList<T>>()
560 }
561
562 pub fn resolve_relation_option<T>(
563 &self,
564 owner_entity: &str,
565 owner_id: u64,
566 relation: &str,
567 ) -> Option<&Option<T>>
568 where
569 T: Any + Send + Sync,
570 {
571 self.graph
572 .frozen()?
573 .relation_lists
574 .get(&RelationListKey {
575 owner_entity: crate::canonical_id_space_entity(owner_entity),
576 owner_id,
577 relation: relation.to_owned(),
578 })?
579 .downcast_ref::<Option<T>>()
580 }
581
582 pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
583 self.graph.frozen().is_some_and(|graph| {
584 graph.relation_lists.contains_key(&RelationListKey {
585 owner_entity: crate::canonical_id_space_entity(owner_entity),
586 owner_id,
587 relation: relation.to_owned(),
588 })
589 })
590 }
591
592 pub fn push_change_set(&self) {
593 self.write_context(|context| context.change_sets.push());
594 }
595
596 pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
597 self.inner.get()?;
598 self.write_context(|context| context.change_sets.pop())
599 }
600
601 pub fn clear_current_change_set(&self) {
602 if self.inner.get().is_some() {
603 self.write_context(|context| context.change_sets.clear_current());
604 }
605 }
606
607 pub fn clear_committed(&self) {
610 if self.inner.get().is_some() {
611 self.write_context(|context| {
612 context.change_sets = ChangeSetStack::default();
613 context.deleted_keys.clear();
614 context.new_keys.clear();
615 context.original_versions.clear();
616 context.trace_chains.clear();
617 context.original_snapshot = None;
618 context.comment = None;
619 context.is_new = false;
620 });
621 }
622 }
623
624 pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
625 self.write_context(|context| context.change_sets.set(key, field, value.into()));
626 }
627
628 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
629 self.read_context(None, |context| context.change_sets.get(key, field))
630 }
631
632 pub fn current_change_set(&self) -> EntityChangeSet {
633 self.read_context(EntityChangeSet::default(), |context| {
634 context.change_sets.current().cloned().unwrap_or_default()
635 })
636 }
637
638 pub fn set_comment(&self, comment: impl Into<String>) {
641 self.write_context(|context| context.comment = Some(comment.into()));
642 }
643
644 pub fn get_comment(&self) -> Option<String> {
646 self.read_context(None, |context| context.comment.clone())
647 }
648
649 pub fn mark_as_new(&self, key: EntityKey) {
651 self.write_context(|context| {
652 context.new_keys.insert(key);
653 });
654 }
655
656 pub fn is_new(&self, key: &EntityKey) -> bool {
658 self.read_context(false, |context| context.new_keys.contains(key))
659 }
660
661 pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
663 self.write_context(|context| {
664 context.original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
665 });
666 }
667
668 pub fn set_original_compact_row(&mut self, row: teaql_core::CompactRow) {
670 self.loaded_snapshot = Some(row);
671 }
672
673 pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
675 if let Some(row) = &self.loaded_snapshot {
676 return Some(EntitySnapshot::from(row.clone().into_map()));
677 }
678 self.read_context(None, |context| {
679 context
680 .original_snapshot
681 .as_ref()
682 .map(|snapshot| match snapshot {
683 OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
684 OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
685 })
686 })
687 }
688
689 pub fn mark_as_delete(&self, key: EntityKey) {
694 self.write_context(|context| {
695 context.change_sets.clear_entity(&key);
696 context.deleted_keys.insert(key);
697 });
698 }
699
700 pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
702 self.read_context(false, |context| context.deleted_keys.contains(key))
703 }
704
705 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
708 self.read_context(BTreeSet::new(), |context| {
709 context.change_sets.changed_field_names(key)
710 })
711 }
712 pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
713 self.read_context(BTreeSet::new(), |context| context.deleted_keys.clone())
714 }
715
716 pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
717 self.read_context(BTreeSet::new(), |context| context.new_keys.clone())
718 }
719
720 pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
721 self.read_context(None, |context| context.original_versions.get(key))
722 .or_else(|| {
723 self.loaded_snapshot
724 .as_ref()?
725 .get("id")?
726 .try_u64()
727 .filter(|id| Some(*id) == key.id.try_u64())?;
728 self.loaded_snapshot.as_ref()?.get("version")?.try_i64()
729 })
730 }
731
732 pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
733 self.read_context(Vec::new(), |context| {
734 context.trace_chains.get(key).cloned().unwrap_or_default()
735 })
736 }
737
738 pub fn set_original_version(&self, key: EntityKey, version: i64) {
739 self.write_context(|context| context.original_versions.insert(key, version));
740 }
741}
742
743pub trait LedgerEntity: teaql_core::Entity {
744 fn entity_root(&self) -> Option<EntityRoot>;
745}
746
747#[cfg(test)]
748mod lazy_root_tests {
749 use super::*;
750
751 #[derive(Clone)]
752 struct GraphChild {
753 root: EntityRoot,
754 }
755
756 #[test]
757 fn loaded_snapshot_does_not_allocate_ledger_until_mutation() {
758 let mut root = EntityRoot::default();
759 root.set_original_compact_row(teaql_core::CompactRow::new(
760 Arc::from(["id".to_owned(), "version".to_owned()]),
761 vec![Value::U64(7), Value::I64(3)],
762 ));
763 let key = EntityKey::new_static("Example", 7_u64);
764
765 assert!(!root.has_mutation_context());
766 assert_eq!(root.get(&key, "name"), None);
767 assert_eq!(root.get_original_version(&key), Some(3));
768 assert!(!root.has_mutation_context());
769
770 root.set(key, "name", Value::Text("updated".to_owned()));
771 assert!(root.has_mutation_context());
772 }
773
774 #[test]
775 fn graph_owned_entities_do_not_keep_the_graph_alive() {
776 let root = EntityRoot::default();
777 let graph_owner = match &root.graph {
778 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
779 EntityGraphReference::Weak(_) => unreachable!(),
780 };
781 let mut builder = EntityGraphBuilder::default();
782 builder.install_relation_list(
783 "Owner",
784 1,
785 "children",
786 SmartList::from(vec![GraphChild {
787 root: EntityRoot::fresh_with_weak_graph(&root),
788 }]),
789 );
790 root.freeze_graph(builder).unwrap();
791
792 drop(root);
793 assert!(graph_owner.upgrade().is_none());
794 }
795
796 #[test]
797 fn cloning_a_graph_owned_entity_promotes_its_graph_anchor() {
798 let root = EntityRoot::default();
799 let graph_owner = match &root.graph {
800 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
801 EntityGraphReference::Weak(_) => unreachable!(),
802 };
803 let mut builder = EntityGraphBuilder::default();
804 builder.install_relation_list(
805 "Owner",
806 1,
807 "children",
808 SmartList::from(vec![GraphChild {
809 root: EntityRoot::fresh_with_weak_graph(&root),
810 }]),
811 );
812 root.freeze_graph(builder).unwrap();
813 let detached = root
814 .resolve_relation_list::<GraphChild>("Owner", 1, "children")
815 .unwrap()[0]
816 .clone();
817
818 drop(root);
819 assert!(graph_owner.upgrade().is_some());
820 assert!(detached.root.graph.frozen().is_some());
821 drop(detached);
822 assert!(graph_owner.upgrade().is_none());
823 }
824}