Skip to main content

reddb_server/runtime/
impl_tree.rs

1//! Tree management over graph collections.
2
3use crate::application::entity::json_to_metadata_value;
4use crate::application::ports::RuntimeEntityPort;
5use crate::storage::unified::{Metadata, MetadataValue};
6use reddb_rql::ast::{CreateTreeQuery, DropTreeQuery, TreeCommand, TreeNodeSpec, TreePosition};
7
8use super::*;
9
10const TREE_CHILD_EDGE_LABEL: &str = "TREE_CHILD";
11const TREE_METADATA_PREFIX: &str = "red.tree.";
12const TREE_METADATA_NAME: &str = "red.tree.name";
13const TREE_METADATA_MAX_CHILDREN: &str = "red.tree.max_children";
14const TREE_METADATA_CHILD_INDEX: &str = "red.tree.child_index";
15const TREE_OWNERSHIP_OWNED: &str = "owned";
16const TREE_AUTO_FIX_CONSERVATIVE: &str = "conservative";
17
18#[derive(Debug, Clone)]
19struct TreeIssue {
20    code: &'static str,
21    message: String,
22    entity_id: Option<EntityId>,
23}
24
25#[derive(Debug, Clone)]
26struct TreeNodeState {
27    metadata: Metadata,
28    max_children_override: Option<usize>,
29}
30
31#[derive(Debug, Clone)]
32struct TreeChildLink {
33    edge_id: EntityId,
34    parent_id: EntityId,
35    child_id: EntityId,
36    child_index: usize,
37}
38
39#[derive(Debug, Clone)]
40struct TreeState {
41    nodes: BTreeMap<EntityId, TreeNodeState>,
42    structural_edges: BTreeMap<EntityId, TreeChildLink>,
43    children_by_parent: BTreeMap<EntityId, Vec<TreeChildLink>>,
44    parent_by_child: BTreeMap<EntityId, TreeChildLink>,
45}
46
47#[derive(Debug, Clone)]
48struct TreeValidation {
49    issues: Vec<TreeIssue>,
50    depths: BTreeMap<EntityId, usize>,
51}
52
53#[derive(Debug, Clone)]
54struct TreeRebalancePlanRow {
55    node_id: EntityId,
56    old_parent_id: EntityId,
57    new_parent_id: EntityId,
58    old_index: usize,
59    new_index: usize,
60    old_depth: usize,
61    new_depth: usize,
62    changed: bool,
63}
64
65impl RedDBRuntime {
66    pub fn execute_create_tree(
67        &self,
68        raw_query: &str,
69        query: &CreateTreeQuery,
70    ) -> RedDBResult<RuntimeQueryResult> {
71        self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
72        if query.default_max_children == 0 {
73            return Err(RedDBError::Query(
74                "tree default max children must be positive".to_string(),
75            ));
76        }
77
78        if self
79            .inner
80            .db
81            .tree_definition(&query.collection, &query.name)
82            .is_some()
83        {
84            if query.if_not_exists {
85                return Ok(RuntimeQueryResult::ok_message(
86                    raw_query.to_string(),
87                    &format!("tree '{}.{}' already exists", query.collection, query.name),
88                    "create",
89                ));
90            }
91            return Err(RedDBError::Query(format!(
92                "tree '{}.{}' already exists",
93                query.collection, query.name
94            )));
95        }
96
97        let root = self.create_node_unchecked(self.build_tree_create_node_input(
98            &query.collection,
99            &query.name,
100            &query.root,
101            false,
102        )?)?;
103        self.ensure_tree_root_metadata(
104            &query.collection,
105            root.id,
106            &query.name,
107            query.root.max_children,
108        )?;
109        let now = current_tree_unix_ms();
110        let definition = crate::physical::PhysicalTreeDefinition {
111            collection: query.collection.clone(),
112            name: query.name.clone(),
113            root_id: root.id.raw(),
114            default_max_children: query.default_max_children,
115            ordered_children: true,
116            ownership: TREE_OWNERSHIP_OWNED.to_string(),
117            auto_fix_mode: TREE_AUTO_FIX_CONSERVATIVE.to_string(),
118            created_at_unix_ms: now,
119            updated_at_unix_ms: now,
120        };
121        self.inner
122            .db
123            .save_tree_definition(definition)
124            .map_err(|err| RedDBError::Internal(err.to_string()))?;
125        self.inner
126            .db
127            .persist_metadata()
128            .map_err(|err| RedDBError::Internal(err.to_string()))?;
129        self.note_table_write(&query.collection);
130        // Issue #120 — surface the tree name in the schema-vocabulary,
131        // scoped to its parent collection. Trees behave like an
132        // index in the lookup model.
133        self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::CreateIndex {
134            collection: query.collection.clone(),
135            index: query.name.clone(),
136            columns: Vec::new(),
137        });
138
139        Ok(RuntimeQueryResult::ok_records(
140            raw_query.to_string(),
141            vec![
142                "collection".to_string(),
143                "tree_name".to_string(),
144                "root_id".to_string(),
145                "default_max_children".to_string(),
146            ],
147            vec![vec![
148                (
149                    "collection".to_string(),
150                    Value::text(query.collection.clone()),
151                ),
152                ("tree_name".to_string(), Value::text(query.name.clone())),
153                ("root_id".to_string(), Value::UnsignedInteger(root.id.raw())),
154                (
155                    "default_max_children".to_string(),
156                    Value::UnsignedInteger(query.default_max_children as u64),
157                ),
158            ]],
159            "create",
160        ))
161    }
162
163    pub fn execute_drop_tree(
164        &self,
165        raw_query: &str,
166        query: &DropTreeQuery,
167    ) -> RedDBResult<RuntimeQueryResult> {
168        self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
169        let Some(definition) = self
170            .inner
171            .db
172            .tree_definition(&query.collection, &query.name)
173        else {
174            if query.if_exists {
175                return Ok(RuntimeQueryResult::ok_message(
176                    raw_query.to_string(),
177                    &format!("tree '{}.{}' does not exist", query.collection, query.name),
178                    "drop",
179                ));
180            }
181            return Err(RedDBError::NotFound(format!(
182                "tree '{}.{}' not found",
183                query.collection, query.name
184            )));
185        };
186
187        let state = self.load_tree_state(&definition)?;
188        let node_ids: BTreeSet<EntityId> = state.nodes.keys().copied().collect();
189        let edge_ids = self.graph_edges_touching_nodes(&definition.collection, &node_ids)?;
190
191        for edge_id in edge_ids {
192            let _ = self.delete_entity_internal(&definition.collection, edge_id)?;
193        }
194
195        let mut ordered_nodes: Vec<EntityId> = node_ids.into_iter().collect();
196        ordered_nodes.sort_by(|left, right| right.cmp(left));
197        for node_id in ordered_nodes {
198            let _ = self.delete_entity_internal(&definition.collection, node_id)?;
199        }
200
201        self.inner
202            .db
203            .remove_tree_definition(&query.collection, &query.name)
204            .map_err(|err| RedDBError::Internal(err.to_string()))?;
205        self.inner
206            .db
207            .persist_metadata()
208            .map_err(|err| RedDBError::Internal(err.to_string()))?;
209        self.note_table_write(&query.collection);
210        // Issue #120 — invalidate the schema-vocabulary tree entry.
211        self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::DropIndex {
212            collection: query.collection.clone(),
213            index: query.name.clone(),
214        });
215
216        Ok(RuntimeQueryResult::ok_message(
217            raw_query.to_string(),
218            &format!("tree '{}.{}' dropped", query.collection, query.name),
219            "drop",
220        ))
221    }
222
223    pub fn execute_tree_command(
224        &self,
225        raw_query: &str,
226        command: &TreeCommand,
227    ) -> RedDBResult<RuntimeQueryResult> {
228        self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
229        match command {
230            TreeCommand::Insert {
231                collection,
232                tree_name,
233                parent_id,
234                node,
235                position,
236            } => self.execute_tree_insert(
237                raw_query,
238                collection,
239                tree_name,
240                EntityId::new(*parent_id),
241                node,
242                *position,
243            ),
244            TreeCommand::Move {
245                collection,
246                tree_name,
247                node_id,
248                parent_id,
249                position,
250            } => self.execute_tree_move(
251                raw_query,
252                collection,
253                tree_name,
254                EntityId::new(*node_id),
255                EntityId::new(*parent_id),
256                *position,
257            ),
258            TreeCommand::Delete {
259                collection,
260                tree_name,
261                node_id,
262            } => {
263                self.execute_tree_delete(raw_query, collection, tree_name, EntityId::new(*node_id))
264            }
265            TreeCommand::Validate {
266                collection,
267                tree_name,
268            } => self.execute_tree_validate(raw_query, collection, tree_name),
269            TreeCommand::Rebalance {
270                collection,
271                tree_name,
272                dry_run,
273            } => self.execute_tree_rebalance(raw_query, collection, tree_name, *dry_run),
274        }
275    }
276
277    fn execute_tree_insert(
278        &self,
279        raw_query: &str,
280        collection: &str,
281        tree_name: &str,
282        parent_id: EntityId,
283        node: &TreeNodeSpec,
284        position: TreePosition,
285    ) -> RedDBResult<RuntimeQueryResult> {
286        let definition = self.require_tree_definition(collection, tree_name)?;
287        let state = self.load_tree_state(&definition)?;
288        self.ensure_tree_operable(&definition, &state)?;
289
290        if !state.nodes.contains_key(&parent_id) {
291            return Err(RedDBError::NotFound(format!(
292                "parent node '{}' was not found in tree '{}.{}'",
293                parent_id.raw(),
294                collection,
295                tree_name
296            )));
297        }
298
299        let mut children = child_id_list(&state.children_by_parent, parent_id);
300        let insert_index = resolve_tree_insert_position(position, children.len())?;
301        let parent_limit = effective_max_children(&definition, &state, parent_id)?;
302        if children.len() >= parent_limit {
303            return Err(RedDBError::Query(format!(
304                "parent node '{}' is at capacity ({}) in tree '{}.{}'",
305                parent_id.raw(),
306                parent_limit,
307                collection,
308                tree_name
309            )));
310        }
311
312        let created = self.create_node_unchecked(
313            self.build_tree_create_node_input(collection, tree_name, node, true)?,
314        )?;
315        children.insert(insert_index, created.id);
316        self.rewrite_parent_children(collection, tree_name, parent_id, &children, &state)?;
317        self.touch_tree_definition_timestamp(&definition)?;
318        self.note_table_write(collection);
319
320        Ok(RuntimeQueryResult::ok_records(
321            raw_query.to_string(),
322            vec![
323                "collection".to_string(),
324                "tree_name".to_string(),
325                "node_id".to_string(),
326                "parent_id".to_string(),
327                "child_index".to_string(),
328            ],
329            vec![vec![
330                (
331                    "collection".to_string(),
332                    Value::text(collection.to_string()),
333                ),
334                ("tree_name".to_string(), Value::text(tree_name.to_string())),
335                (
336                    "node_id".to_string(),
337                    Value::UnsignedInteger(created.id.raw()),
338                ),
339                (
340                    "parent_id".to_string(),
341                    Value::UnsignedInteger(parent_id.raw()),
342                ),
343                (
344                    "child_index".to_string(),
345                    Value::UnsignedInteger(insert_index as u64),
346                ),
347            ]],
348            "insert",
349        ))
350    }
351
352    fn execute_tree_move(
353        &self,
354        raw_query: &str,
355        collection: &str,
356        tree_name: &str,
357        node_id: EntityId,
358        new_parent_id: EntityId,
359        position: TreePosition,
360    ) -> RedDBResult<RuntimeQueryResult> {
361        let definition = self.require_tree_definition(collection, tree_name)?;
362        let state = self.load_tree_state(&definition)?;
363        self.ensure_tree_operable(&definition, &state)?;
364
365        if node_id.raw() == definition.root_id {
366            return Err(RedDBError::Query("cannot move the tree root".to_string()));
367        }
368        if !state.nodes.contains_key(&node_id) {
369            return Err(RedDBError::NotFound(format!(
370                "node '{}' was not found in tree '{}.{}'",
371                node_id.raw(),
372                collection,
373                tree_name
374            )));
375        }
376        if !state.nodes.contains_key(&new_parent_id) {
377            return Err(RedDBError::NotFound(format!(
378                "parent node '{}' was not found in tree '{}.{}'",
379                new_parent_id.raw(),
380                collection,
381                tree_name
382            )));
383        }
384        if node_id == new_parent_id {
385            return Err(RedDBError::Query(
386                "node cannot be moved under itself".to_string(),
387            ));
388        }
389        if subtree_ids(&state, node_id).contains(&new_parent_id) {
390            return Err(RedDBError::Query(
391                "node cannot be moved under one of its descendants".to_string(),
392            ));
393        }
394
395        let current_parent = state
396            .parent_by_child
397            .get(&node_id)
398            .map(|link| link.parent_id)
399            .ok_or_else(|| {
400                RedDBError::Query(format!(
401                    "node '{}' does not have a structural parent in tree '{}.{}'",
402                    node_id.raw(),
403                    collection,
404                    tree_name
405                ))
406            })?;
407
408        let mut old_parent_children = child_id_list(&state.children_by_parent, current_parent);
409        old_parent_children.retain(|child| *child != node_id);
410
411        let mut new_parent_children = if current_parent == new_parent_id {
412            old_parent_children.clone()
413        } else {
414            child_id_list(&state.children_by_parent, new_parent_id)
415        };
416
417        let insert_index = resolve_tree_insert_position(position, new_parent_children.len())?;
418        let new_parent_limit = effective_max_children(&definition, &state, new_parent_id)?;
419        if current_parent != new_parent_id && new_parent_children.len() >= new_parent_limit {
420            return Err(RedDBError::Query(format!(
421                "parent node '{}' is at capacity ({}) in tree '{}.{}'",
422                new_parent_id.raw(),
423                new_parent_limit,
424                collection,
425                tree_name
426            )));
427        }
428
429        new_parent_children.insert(insert_index, node_id);
430
431        if current_parent == new_parent_id {
432            self.rewrite_parent_children(
433                collection,
434                tree_name,
435                current_parent,
436                &new_parent_children,
437                &state,
438            )?;
439        } else {
440            self.rewrite_parent_children(
441                collection,
442                tree_name,
443                current_parent,
444                &old_parent_children,
445                &state,
446            )?;
447            self.rewrite_parent_children(
448                collection,
449                tree_name,
450                new_parent_id,
451                &new_parent_children,
452                &state,
453            )?;
454        }
455
456        self.touch_tree_definition_timestamp(&definition)?;
457        self.note_table_write(collection);
458
459        Ok(RuntimeQueryResult::ok_records(
460            raw_query.to_string(),
461            vec![
462                "collection".to_string(),
463                "tree_name".to_string(),
464                "node_id".to_string(),
465                "old_parent_id".to_string(),
466                "new_parent_id".to_string(),
467                "child_index".to_string(),
468            ],
469            vec![vec![
470                (
471                    "collection".to_string(),
472                    Value::text(collection.to_string()),
473                ),
474                ("tree_name".to_string(), Value::text(tree_name.to_string())),
475                ("node_id".to_string(), Value::UnsignedInteger(node_id.raw())),
476                (
477                    "old_parent_id".to_string(),
478                    Value::UnsignedInteger(current_parent.raw()),
479                ),
480                (
481                    "new_parent_id".to_string(),
482                    Value::UnsignedInteger(new_parent_id.raw()),
483                ),
484                (
485                    "child_index".to_string(),
486                    Value::UnsignedInteger(insert_index as u64),
487                ),
488            ]],
489            "update",
490        ))
491    }
492
493    fn execute_tree_delete(
494        &self,
495        raw_query: &str,
496        collection: &str,
497        tree_name: &str,
498        node_id: EntityId,
499    ) -> RedDBResult<RuntimeQueryResult> {
500        let definition = self.require_tree_definition(collection, tree_name)?;
501        let state = self.load_tree_state(&definition)?;
502        self.ensure_tree_operable(&definition, &state)?;
503
504        if !state.nodes.contains_key(&node_id) {
505            return Err(RedDBError::NotFound(format!(
506                "node '{}' was not found in tree '{}.{}'",
507                node_id.raw(),
508                collection,
509                tree_name
510            )));
511        }
512
513        if node_id.raw() == definition.root_id {
514            return self.execute_drop_tree(
515                raw_query,
516                &DropTreeQuery {
517                    collection: collection.to_string(),
518                    name: tree_name.to_string(),
519                    if_exists: false,
520                },
521            );
522        }
523
524        let subtree = subtree_ids(&state, node_id);
525        let parent_id = state
526            .parent_by_child
527            .get(&node_id)
528            .map(|link| link.parent_id)
529            .ok_or_else(|| {
530                RedDBError::Query(format!(
531                    "node '{}' does not have a structural parent in tree '{}.{}'",
532                    node_id.raw(),
533                    collection,
534                    tree_name
535                ))
536            })?;
537        let surviving_children: Vec<EntityId> = child_id_list(&state.children_by_parent, parent_id)
538            .into_iter()
539            .filter(|child| *child != node_id)
540            .collect();
541
542        let edge_ids = self.graph_edges_touching_nodes(collection, &subtree)?;
543        for edge_id in edge_ids {
544            let _ = self.delete_entity_internal(collection, edge_id)?;
545        }
546
547        let mut ordered_nodes: Vec<EntityId> = subtree.iter().copied().collect();
548        ordered_nodes.sort_by(|left, right| right.cmp(left));
549        for subtree_node in ordered_nodes {
550            let _ = self.delete_entity_internal(collection, subtree_node)?;
551        }
552
553        self.rewrite_parent_children(
554            collection,
555            tree_name,
556            parent_id,
557            &surviving_children,
558            &state,
559        )?;
560        self.touch_tree_definition_timestamp(&definition)?;
561        self.note_table_write(collection);
562
563        Ok(RuntimeQueryResult::ok_records(
564            raw_query.to_string(),
565            vec![
566                "collection".to_string(),
567                "tree_name".to_string(),
568                "deleted_root_id".to_string(),
569                "deleted_node_count".to_string(),
570            ],
571            vec![vec![
572                (
573                    "collection".to_string(),
574                    Value::text(collection.to_string()),
575                ),
576                ("tree_name".to_string(), Value::text(tree_name.to_string())),
577                (
578                    "deleted_root_id".to_string(),
579                    Value::UnsignedInteger(node_id.raw()),
580                ),
581                (
582                    "deleted_node_count".to_string(),
583                    Value::UnsignedInteger(subtree.len() as u64),
584                ),
585            ]],
586            "delete",
587        ))
588    }
589
590    fn execute_tree_validate(
591        &self,
592        raw_query: &str,
593        collection: &str,
594        tree_name: &str,
595    ) -> RedDBResult<RuntimeQueryResult> {
596        let definition = self.require_tree_definition(collection, tree_name)?;
597        let state = self.load_tree_state(&definition)?;
598        let validation = self.validate_tree_state(&definition, &state);
599
600        if validation.issues.is_empty() {
601            return Ok(RuntimeQueryResult::ok_records(
602                raw_query.to_string(),
603                vec![
604                    "ok".to_string(),
605                    "code".to_string(),
606                    "message".to_string(),
607                    "entity_id".to_string(),
608                ],
609                vec![vec![
610                    ("ok".to_string(), Value::Boolean(true)),
611                    ("code".to_string(), Value::text("ok".to_string())),
612                    (
613                        "message".to_string(),
614                        Value::text("tree is valid".to_string()),
615                    ),
616                    ("entity_id".to_string(), Value::Null),
617                ]],
618                "select",
619            ));
620        }
621
622        let rows = validation
623            .issues
624            .iter()
625            .map(|issue| {
626                vec![
627                    ("ok".to_string(), Value::Boolean(false)),
628                    ("code".to_string(), Value::text(issue.code.to_string())),
629                    ("message".to_string(), Value::text(issue.message.clone())),
630                    (
631                        "entity_id".to_string(),
632                        issue
633                            .entity_id
634                            .map(|entity_id| Value::UnsignedInteger(entity_id.raw()))
635                            .unwrap_or(Value::Null),
636                    ),
637                ]
638            })
639            .collect();
640        Ok(RuntimeQueryResult::ok_records(
641            raw_query.to_string(),
642            vec![
643                "ok".to_string(),
644                "code".to_string(),
645                "message".to_string(),
646                "entity_id".to_string(),
647            ],
648            rows,
649            "select",
650        ))
651    }
652
653    fn execute_tree_rebalance(
654        &self,
655        raw_query: &str,
656        collection: &str,
657        tree_name: &str,
658        dry_run: bool,
659    ) -> RedDBResult<RuntimeQueryResult> {
660        let definition = self.require_tree_definition(collection, tree_name)?;
661        let state = self.load_tree_state(&definition)?;
662        self.ensure_tree_operable(&definition, &state)?;
663        let plan = self.plan_tree_rebalance(&definition, &state)?;
664
665        if !dry_run {
666            let existing_edges: Vec<EntityId> = state.structural_edges.keys().copied().collect();
667            for edge_id in existing_edges {
668                let _ = self.delete_entity_internal(collection, edge_id)?;
669            }
670
671            for row in &plan {
672                self.create_edge_unchecked(self.build_tree_structural_edge_input(
673                    collection,
674                    tree_name,
675                    row.new_parent_id,
676                    row.node_id,
677                    row.new_index,
678                ))?;
679            }
680
681            self.touch_tree_definition_timestamp(&definition)?;
682            self.note_table_write(collection);
683        }
684
685        let rows = plan
686            .iter()
687            .map(|row| {
688                vec![
689                    (
690                        "node_id".to_string(),
691                        Value::UnsignedInteger(row.node_id.raw()),
692                    ),
693                    (
694                        "old_parent_id".to_string(),
695                        Value::UnsignedInteger(row.old_parent_id.raw()),
696                    ),
697                    (
698                        "new_parent_id".to_string(),
699                        Value::UnsignedInteger(row.new_parent_id.raw()),
700                    ),
701                    (
702                        "old_index".to_string(),
703                        Value::UnsignedInteger(row.old_index as u64),
704                    ),
705                    (
706                        "new_index".to_string(),
707                        Value::UnsignedInteger(row.new_index as u64),
708                    ),
709                    (
710                        "old_depth".to_string(),
711                        Value::UnsignedInteger(row.old_depth as u64),
712                    ),
713                    (
714                        "new_depth".to_string(),
715                        Value::UnsignedInteger(row.new_depth as u64),
716                    ),
717                    ("changed".to_string(), Value::Boolean(row.changed)),
718                ]
719            })
720            .collect();
721
722        Ok(RuntimeQueryResult::ok_records(
723            raw_query.to_string(),
724            vec![
725                "node_id".to_string(),
726                "old_parent_id".to_string(),
727                "new_parent_id".to_string(),
728                "old_index".to_string(),
729                "new_index".to_string(),
730                "old_depth".to_string(),
731                "new_depth".to_string(),
732                "changed".to_string(),
733            ],
734            rows,
735            "select",
736        ))
737    }
738
739    fn build_tree_create_node_input(
740        &self,
741        collection: &str,
742        tree_name: &str,
743        node: &TreeNodeSpec,
744        allow_max_children: bool,
745    ) -> RedDBResult<crate::application::CreateNodeInput> {
746        if allow_max_children && node.max_children == Some(0) {
747            return Err(RedDBError::Query(
748                "node max children must be positive".to_string(),
749            ));
750        }
751        self.ensure_tree_spec_is_user_safe(node)?;
752        let mut metadata = build_tree_node_metadata(tree_name, &node.metadata)?;
753        if allow_max_children {
754            if let Some(max_children) = node.max_children {
755                metadata.push((
756                    TREE_METADATA_MAX_CHILDREN.to_string(),
757                    MetadataValue::Int(max_children as i64),
758                ));
759            }
760        }
761        Ok(crate::application::CreateNodeInput {
762            collection: collection.to_string(),
763            label: node.label.clone(),
764            node_type: node.node_type.clone(),
765            properties: node.properties.clone(),
766            metadata,
767            embeddings: Vec::new(),
768            table_links: Vec::new(),
769            node_links: Vec::new(),
770        })
771    }
772
773    fn build_tree_structural_edge_input(
774        &self,
775        collection: &str,
776        tree_name: &str,
777        parent_id: EntityId,
778        child_id: EntityId,
779        child_index: usize,
780    ) -> crate::application::CreateEdgeInput {
781        crate::application::CreateEdgeInput {
782            collection: collection.to_string(),
783            label: TREE_CHILD_EDGE_LABEL.to_string(),
784            from: parent_id,
785            to: child_id,
786            weight: Some(1.0),
787            properties: Vec::new(),
788            metadata: vec![
789                (
790                    TREE_METADATA_NAME.to_string(),
791                    MetadataValue::String(tree_name.to_string()),
792                ),
793                (
794                    TREE_METADATA_CHILD_INDEX.to_string(),
795                    MetadataValue::Int(child_index as i64),
796                ),
797            ],
798        }
799    }
800
801    fn ensure_tree_spec_is_user_safe(&self, node: &TreeNodeSpec) -> RedDBResult<()> {
802        for (key, _) in &node.metadata {
803            if key.starts_with(TREE_METADATA_PREFIX) {
804                return Err(RedDBError::Query(format!(
805                    "metadata key '{}' is reserved for managed trees",
806                    key
807                )));
808            }
809        }
810        Ok(())
811    }
812
813    fn require_tree_definition(
814        &self,
815        collection: &str,
816        tree_name: &str,
817    ) -> RedDBResult<crate::physical::PhysicalTreeDefinition> {
818        self.inner
819            .db
820            .tree_definition(collection, tree_name)
821            .ok_or_else(|| {
822                RedDBError::NotFound(format!("tree '{}.{}' not found", collection, tree_name))
823            })
824    }
825
826    fn touch_tree_definition_timestamp(
827        &self,
828        definition: &crate::physical::PhysicalTreeDefinition,
829    ) -> RedDBResult<()> {
830        let mut updated = definition.clone();
831        updated.updated_at_unix_ms = current_tree_unix_ms();
832        self.inner
833            .db
834            .save_tree_definition(updated)
835            .map_err(|err| RedDBError::Internal(err.to_string()))?;
836        self.inner
837            .db
838            .persist_metadata()
839            .map_err(|err| RedDBError::Internal(err.to_string()))?;
840        Ok(())
841    }
842
843    fn ensure_tree_root_metadata(
844        &self,
845        collection: &str,
846        root_id: EntityId,
847        tree_name: &str,
848        max_children_override: Option<usize>,
849    ) -> RedDBResult<()> {
850        let store = self.inner.db.store();
851        let mut metadata = store.get_metadata(collection, root_id).unwrap_or_default();
852        metadata.set(
853            TREE_METADATA_NAME.to_string(),
854            MetadataValue::String(tree_name.to_string()),
855        );
856        match max_children_override {
857            Some(value) => metadata.set(
858                TREE_METADATA_MAX_CHILDREN.to_string(),
859                MetadataValue::Int(value as i64),
860            ),
861            None => {
862                metadata.remove(TREE_METADATA_MAX_CHILDREN);
863            }
864        }
865        store
866            .set_metadata(collection, root_id, metadata)
867            .map_err(|err| RedDBError::Internal(err.to_string()))
868    }
869
870    fn load_tree_state(
871        &self,
872        definition: &crate::physical::PhysicalTreeDefinition,
873    ) -> RedDBResult<TreeState> {
874        let store = self.inner.db.store();
875        let Some(manager) = store.get_collection(&definition.collection) else {
876            return Ok(TreeState {
877                nodes: BTreeMap::new(),
878                structural_edges: BTreeMap::new(),
879                children_by_parent: BTreeMap::new(),
880                parent_by_child: BTreeMap::new(),
881            });
882        };
883
884        let mut all_nodes = BTreeMap::new();
885        let mut candidate_links = Vec::new();
886        let mut member_node_ids = BTreeSet::new();
887        let root_id = EntityId::new(definition.root_id);
888
889        for entity in manager.query_all(|entity| {
890            matches!(
891                entity.kind,
892                EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
893            )
894        }) {
895            let metadata = store
896                .get_metadata(&definition.collection, entity.id)
897                .unwrap_or_default();
898            match &entity.kind {
899                EntityKind::GraphNode(_) => {
900                    if metadata_tree_name(&metadata) == Some(definition.name.as_str()) {
901                        member_node_ids.insert(entity.id);
902                    }
903                    all_nodes.insert(
904                        entity.id,
905                        TreeNodeState {
906                            max_children_override: metadata_usize(
907                                &metadata,
908                                TREE_METADATA_MAX_CHILDREN,
909                            ),
910                            metadata,
911                        },
912                    );
913                }
914                EntityKind::GraphEdge(edge_kind) if edge_kind.label == TREE_CHILD_EDGE_LABEL => {
915                    let Some(parent_id) = parse_tree_entity_id_text(&edge_kind.from_node) else {
916                        continue;
917                    };
918                    let Some(child_id) = parse_tree_entity_id_text(&edge_kind.to_node) else {
919                        continue;
920                    };
921                    if metadata_tree_name(&metadata) == Some(definition.name.as_str()) {
922                        member_node_ids.insert(parent_id);
923                        member_node_ids.insert(child_id);
924                    }
925                    let child_index =
926                        metadata_usize(&metadata, TREE_METADATA_CHILD_INDEX).unwrap_or(usize::MAX);
927                    candidate_links.push(TreeChildLink {
928                        edge_id: entity.id,
929                        parent_id,
930                        child_id,
931                        child_index,
932                    });
933                }
934                _ => {}
935            }
936        }
937
938        let mut candidate_children_by_parent: BTreeMap<EntityId, Vec<TreeChildLink>> =
939            BTreeMap::new();
940        for link in &candidate_links {
941            candidate_children_by_parent
942                .entry(link.parent_id)
943                .or_default()
944                .push(link.clone());
945        }
946
947        for links in candidate_children_by_parent.values_mut() {
948            links.sort_by(|left, right| {
949                left.child_index
950                    .cmp(&right.child_index)
951                    .then_with(|| left.edge_id.cmp(&right.edge_id))
952                    .then_with(|| left.child_id.cmp(&right.child_id))
953            });
954            for (derived_index, link) in links.iter_mut().enumerate() {
955                link.child_index = derived_index;
956            }
957        }
958
959        let mut queue = VecDeque::new();
960        queue.push_back(root_id);
961        while let Some(node_id) = queue.pop_front() {
962            if !member_node_ids.insert(node_id) {
963                continue;
964            }
965            if let Some(children) = candidate_children_by_parent.get(&node_id) {
966                for child in children {
967                    member_node_ids.insert(child.parent_id);
968                    member_node_ids.insert(child.child_id);
969                    queue.push_back(child.child_id);
970                }
971            }
972        }
973
974        let mut nodes = BTreeMap::new();
975        for node_id in &member_node_ids {
976            if let Some(node) = all_nodes.get(node_id) {
977                nodes.insert(*node_id, node.clone());
978            }
979        }
980
981        let mut structural_edges = BTreeMap::new();
982        let mut children_by_parent: BTreeMap<EntityId, Vec<TreeChildLink>> = BTreeMap::new();
983        let mut parent_by_child = BTreeMap::new();
984        for links in candidate_children_by_parent.values() {
985            for link in links {
986                if !(member_node_ids.contains(&link.parent_id)
987                    && member_node_ids.contains(&link.child_id))
988                {
989                    continue;
990                }
991                structural_edges.insert(link.edge_id, link.clone());
992                children_by_parent
993                    .entry(link.parent_id)
994                    .or_default()
995                    .push(link.clone());
996                parent_by_child.insert(link.child_id, link.clone());
997            }
998        }
999
1000        for links in children_by_parent.values_mut() {
1001            links.sort_by(|left, right| {
1002                left.child_index
1003                    .cmp(&right.child_index)
1004                    .then_with(|| left.child_id.cmp(&right.child_id))
1005            });
1006        }
1007
1008        Ok(TreeState {
1009            nodes,
1010            structural_edges,
1011            children_by_parent,
1012            parent_by_child,
1013        })
1014    }
1015
1016    fn validate_tree_state(
1017        &self,
1018        definition: &crate::physical::PhysicalTreeDefinition,
1019        state: &TreeState,
1020    ) -> TreeValidation {
1021        let mut issues = Vec::new();
1022        let root_id = EntityId::new(definition.root_id);
1023        if !state.nodes.contains_key(&root_id) {
1024            issues.push(TreeIssue {
1025                code: "missing_root",
1026                message: format!(
1027                    "root node '{}' is missing from tree '{}.{}'",
1028                    definition.root_id, definition.collection, definition.name
1029                ),
1030                entity_id: Some(root_id),
1031            });
1032        }
1033
1034        for (node_id, node) in &state.nodes {
1035            match metadata_tree_name(&node.metadata) {
1036                Some(name) if name == definition.name => {}
1037                Some(_) => issues.push(TreeIssue {
1038                    code: "node_tree_name_mismatch",
1039                    message: format!(
1040                        "node '{}' is part of tree '{}.{}' but is missing '{}' metadata",
1041                        node_id.raw(),
1042                        definition.collection,
1043                        definition.name,
1044                        TREE_METADATA_NAME
1045                    ),
1046                    entity_id: Some(*node_id),
1047                }),
1048                None => {}
1049            }
1050            if metadata_invalid_max_children(&node.metadata) {
1051                issues.push(TreeIssue {
1052                    code: "invalid_max_children",
1053                    message: format!(
1054                        "node '{}' has invalid '{}' metadata",
1055                        node_id.raw(),
1056                        TREE_METADATA_MAX_CHILDREN
1057                    ),
1058                    entity_id: Some(*node_id),
1059                });
1060            }
1061        }
1062
1063        let mut parent_counts: BTreeMap<EntityId, usize> = BTreeMap::new();
1064        for link in state.structural_edges.values() {
1065            parent_counts
1066                .entry(link.child_id)
1067                .and_modify(|count| *count += 1)
1068                .or_insert(1);
1069            if !state.nodes.contains_key(&link.parent_id) {
1070                issues.push(TreeIssue {
1071                    code: "missing_parent_node",
1072                    message: format!(
1073                        "structural edge '{}' references missing parent node '{}'",
1074                        link.edge_id.raw(),
1075                        link.parent_id.raw()
1076                    ),
1077                    entity_id: Some(link.edge_id),
1078                });
1079            }
1080            if !state.nodes.contains_key(&link.child_id) {
1081                issues.push(TreeIssue {
1082                    code: "missing_child_node",
1083                    message: format!(
1084                        "structural edge '{}' references missing child node '{}'",
1085                        link.edge_id.raw(),
1086                        link.child_id.raw()
1087                    ),
1088                    entity_id: Some(link.edge_id),
1089                });
1090            }
1091        }
1092
1093        for (child_id, count) in parent_counts {
1094            if count > 1 {
1095                issues.push(TreeIssue {
1096                    code: "multiple_parents",
1097                    message: format!("node '{}' has multiple structural parents", child_id.raw()),
1098                    entity_id: Some(child_id),
1099                });
1100            }
1101        }
1102
1103        if let Some(link) = state.parent_by_child.get(&root_id) {
1104            issues.push(TreeIssue {
1105                code: "root_has_parent",
1106                message: format!(
1107                    "root node '{}' has a structural parent '{}'",
1108                    root_id.raw(),
1109                    link.parent_id.raw()
1110                ),
1111                entity_id: Some(root_id),
1112            });
1113        }
1114
1115        for node_id in state.nodes.keys() {
1116            if *node_id == root_id {
1117                continue;
1118            }
1119            if !state.parent_by_child.contains_key(node_id) {
1120                issues.push(TreeIssue {
1121                    code: "missing_parent",
1122                    message: format!(
1123                        "node '{}' is disconnected from root '{}'",
1124                        node_id.raw(),
1125                        root_id.raw()
1126                    ),
1127                    entity_id: Some(*node_id),
1128                });
1129            }
1130        }
1131
1132        for (parent_id, links) in &state.children_by_parent {
1133            let mut seen = BTreeSet::new();
1134            for (expected_index, link) in links.iter().enumerate() {
1135                if !seen.insert(link.child_index) {
1136                    issues.push(TreeIssue {
1137                        code: "duplicate_child_index",
1138                        message: format!(
1139                            "parent '{}' has duplicate child index '{}'",
1140                            parent_id.raw(),
1141                            link.child_index
1142                        ),
1143                        entity_id: Some(link.edge_id),
1144                    });
1145                }
1146                if link.child_index != expected_index {
1147                    issues.push(TreeIssue {
1148                        code: "non_compact_child_index",
1149                        message: format!(
1150                            "parent '{}' children must use compact indexes starting at zero",
1151                            parent_id.raw()
1152                        ),
1153                        entity_id: Some(link.edge_id),
1154                    });
1155                    break;
1156                }
1157            }
1158
1159            if let Ok(limit) = effective_max_children(definition, state, *parent_id) {
1160                if links.len() > limit {
1161                    issues.push(TreeIssue {
1162                        code: "max_children_exceeded",
1163                        message: format!(
1164                            "parent '{}' has {} children but limit is {}",
1165                            parent_id.raw(),
1166                            links.len(),
1167                            limit
1168                        ),
1169                        entity_id: Some(*parent_id),
1170                    });
1171                }
1172            }
1173        }
1174
1175        let mut depths = BTreeMap::new();
1176        let mut visited = BTreeSet::new();
1177        let mut stack = BTreeSet::new();
1178        if state.nodes.contains_key(&root_id) {
1179            validate_tree_cycles(
1180                root_id,
1181                0,
1182                state,
1183                &mut visited,
1184                &mut stack,
1185                &mut depths,
1186                &mut issues,
1187            );
1188        }
1189
1190        for node_id in state.nodes.keys() {
1191            if !depths.contains_key(node_id) {
1192                issues.push(TreeIssue {
1193                    code: "unreachable_node",
1194                    message: format!(
1195                        "node '{}' is not reachable from root '{}'",
1196                        node_id.raw(),
1197                        root_id.raw()
1198                    ),
1199                    entity_id: Some(*node_id),
1200                });
1201            }
1202        }
1203
1204        TreeValidation { issues, depths }
1205    }
1206
1207    fn ensure_tree_operable(
1208        &self,
1209        definition: &crate::physical::PhysicalTreeDefinition,
1210        state: &TreeState,
1211    ) -> RedDBResult<()> {
1212        let validation = self.validate_tree_state(definition, state);
1213        let blocking_issue = validation.issues.into_iter().find(|issue| {
1214            !matches!(
1215                issue.code,
1216                "duplicate_child_index" | "non_compact_child_index"
1217            )
1218        });
1219        if let Some(issue) = blocking_issue {
1220            return Err(RedDBError::Query(format!(
1221                "tree '{}.{}' is not operable: {}",
1222                definition.collection, definition.name, issue.message
1223            )));
1224        }
1225        Ok(())
1226    }
1227
1228    fn rewrite_parent_children(
1229        &self,
1230        collection: &str,
1231        tree_name: &str,
1232        parent_id: EntityId,
1233        child_ids: &[EntityId],
1234        current_state: &TreeState,
1235    ) -> RedDBResult<()> {
1236        if let Some(existing_links) = current_state.children_by_parent.get(&parent_id) {
1237            for link in existing_links {
1238                let _ = self.delete_entity_internal(collection, link.edge_id)?;
1239            }
1240        }
1241        for (index, child_id) in child_ids.iter().enumerate() {
1242            self.create_edge_unchecked(self.build_tree_structural_edge_input(
1243                collection, tree_name, parent_id, *child_id, index,
1244            ))?;
1245        }
1246        Ok(())
1247    }
1248
1249    fn graph_edges_touching_nodes(
1250        &self,
1251        collection: &str,
1252        nodes: &BTreeSet<EntityId>,
1253    ) -> RedDBResult<BTreeSet<EntityId>> {
1254        let store = self.inner.db.store();
1255        let Some(manager) = store.get_collection(collection) else {
1256            return Ok(BTreeSet::new());
1257        };
1258        let mut edge_ids = BTreeSet::new();
1259        for entity in manager.query_all(|entity| matches!(entity.kind, EntityKind::GraphEdge(_))) {
1260            let EntityKind::GraphEdge(edge_kind) = &entity.kind else {
1261                continue;
1262            };
1263            let Some(from_id) = parse_tree_entity_id_text(&edge_kind.from_node) else {
1264                continue;
1265            };
1266            let Some(to_id) = parse_tree_entity_id_text(&edge_kind.to_node) else {
1267                continue;
1268            };
1269            if nodes.contains(&from_id) || nodes.contains(&to_id) {
1270                edge_ids.insert(entity.id);
1271            }
1272        }
1273        Ok(edge_ids)
1274    }
1275
1276    fn delete_entity_internal(&self, collection: &str, id: EntityId) -> RedDBResult<bool> {
1277        let store = self.inner.db.store();
1278        let deleted = store
1279            .delete(collection, id)
1280            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1281        if deleted {
1282            store.context_index().remove_entity(id);
1283            self.cdc_emit(
1284                crate::replication::cdc::ChangeOperation::Delete,
1285                collection,
1286                id.raw(),
1287                "entity",
1288            );
1289        }
1290        Ok(deleted)
1291    }
1292
1293    fn plan_tree_rebalance(
1294        &self,
1295        definition: &crate::physical::PhysicalTreeDefinition,
1296        state: &TreeState,
1297    ) -> RedDBResult<Vec<TreeRebalancePlanRow>> {
1298        let validation = self.validate_tree_state(definition, state);
1299        let root_id = EntityId::new(definition.root_id);
1300        if !validation.depths.contains_key(&root_id) {
1301            return Err(RedDBError::Query(format!(
1302                "tree '{}.{}' is missing its root '{}'",
1303                definition.collection, definition.name, definition.root_id
1304            )));
1305        }
1306
1307        let ordered = flatten_tree_preorder(state, root_id);
1308        let mut assignment_queue = VecDeque::new();
1309        assignment_queue.push_back(root_id);
1310        let mut assigned_children_count: BTreeMap<EntityId, usize> = BTreeMap::new();
1311        let mut new_depths = BTreeMap::new();
1312        new_depths.insert(root_id, 0usize);
1313
1314        let mut plan = Vec::with_capacity(ordered.len());
1315        for node_id in ordered {
1316            loop {
1317                let Some(candidate_parent) = assignment_queue.front().copied() else {
1318                    return Err(RedDBError::Query(format!(
1319                        "tree '{}.{}' does not have enough aggregate capacity to rebalance",
1320                        definition.collection, definition.name
1321                    )));
1322                };
1323                let assigned = assigned_children_count
1324                    .get(&candidate_parent)
1325                    .copied()
1326                    .unwrap_or(0);
1327                let limit = effective_max_children(definition, state, candidate_parent)?;
1328                if assigned < limit {
1329                    break;
1330                }
1331                assignment_queue.pop_front();
1332            }
1333
1334            let parent_id = assignment_queue.front().copied().ok_or_else(|| {
1335                RedDBError::Query(format!(
1336                    "tree '{}.{}' does not have enough aggregate capacity to rebalance",
1337                    definition.collection, definition.name
1338                ))
1339            })?;
1340            let entry = assigned_children_count.entry(parent_id).or_insert(0);
1341            let new_index = *entry;
1342            *entry += 1;
1343            let parent_depth = new_depths.get(&parent_id).copied().unwrap_or(0);
1344            new_depths.insert(node_id, parent_depth + 1);
1345            assignment_queue.push_back(node_id);
1346
1347            let old_link = state.parent_by_child.get(&node_id).ok_or_else(|| {
1348                RedDBError::Query(format!(
1349                    "node '{}' is missing a structural parent in tree '{}.{}'",
1350                    node_id.raw(),
1351                    definition.collection,
1352                    definition.name
1353                ))
1354            })?;
1355            let old_depth = validation.depths.get(&node_id).copied().unwrap_or(0);
1356            let new_depth = new_depths.get(&node_id).copied().unwrap_or(0);
1357            let changed = old_link.parent_id != parent_id
1358                || old_link.child_index != new_index
1359                || old_depth != new_depth;
1360
1361            plan.push(TreeRebalancePlanRow {
1362                node_id,
1363                old_parent_id: old_link.parent_id,
1364                new_parent_id: parent_id,
1365                old_index: old_link.child_index,
1366                new_index,
1367                old_depth,
1368                new_depth,
1369                changed,
1370            });
1371        }
1372
1373        Ok(plan)
1374    }
1375}
1376
1377fn current_tree_unix_ms() -> u128 {
1378    SystemTime::now()
1379        .duration_since(UNIX_EPOCH)
1380        .unwrap_or_default()
1381        .as_millis()
1382}
1383
1384fn build_tree_node_metadata(
1385    tree_name: &str,
1386    metadata_entries: &[(String, Value)],
1387) -> RedDBResult<Vec<(String, MetadataValue)>> {
1388    let mut metadata = Vec::with_capacity(metadata_entries.len() + 1);
1389    metadata.push((
1390        TREE_METADATA_NAME.to_string(),
1391        MetadataValue::String(tree_name.to_string()),
1392    ));
1393    for (key, value) in metadata_entries {
1394        metadata.push((key.clone(), storage_value_to_tree_metadata(value)?));
1395    }
1396    Ok(metadata)
1397}
1398
1399fn storage_value_to_tree_metadata(value: &Value) -> RedDBResult<MetadataValue> {
1400    Ok(match value {
1401        Value::Null => MetadataValue::Null,
1402        Value::Boolean(value) => MetadataValue::Bool(*value),
1403        Value::Integer(value) => MetadataValue::Int(*value),
1404        Value::UnsignedInteger(value) => {
1405            if *value <= i64::MAX as u64 {
1406                MetadataValue::Int(*value as i64)
1407            } else {
1408                MetadataValue::Timestamp(*value)
1409            }
1410        }
1411        Value::Float(value) => MetadataValue::Float(*value),
1412        Value::Text(value) => MetadataValue::String(value.to_string()),
1413        Value::Blob(value) => MetadataValue::Bytes(value.clone()),
1414        Value::Timestamp(value) => {
1415            if *value < 0 {
1416                return Err(RedDBError::Query(
1417                    "negative timestamps are not supported in tree metadata".to_string(),
1418                ));
1419            }
1420            MetadataValue::Timestamp(*value as u64)
1421        }
1422        Value::Json(bytes) => {
1423            let json = crate::json::from_slice::<crate::json::Value>(bytes).map_err(|err| {
1424                RedDBError::Query(format!("failed to decode JSON metadata value: {err}"))
1425            })?;
1426            json_to_metadata_value(&json)?
1427        }
1428        Value::Vector(values) => MetadataValue::Array(
1429            values
1430                .iter()
1431                .map(|value| MetadataValue::Float(*value as f64))
1432                .collect(),
1433        ),
1434        Value::Array(values) => MetadataValue::Array(
1435            values
1436                .iter()
1437                .map(storage_value_to_tree_metadata)
1438                .collect::<RedDBResult<Vec<_>>>()?,
1439        ),
1440        other => MetadataValue::String(format!("{other:?}")),
1441    })
1442}
1443
1444fn resolve_tree_insert_position(position: TreePosition, len: usize) -> RedDBResult<usize> {
1445    match position {
1446        TreePosition::First => Ok(0),
1447        TreePosition::Last => Ok(len),
1448        TreePosition::Index(index) if index <= len => Ok(index),
1449        TreePosition::Index(index) => Err(RedDBError::Query(format!(
1450            "tree child position {} is out of bounds for {} children",
1451            index, len
1452        ))),
1453    }
1454}
1455
1456fn child_id_list(
1457    children_by_parent: &BTreeMap<EntityId, Vec<TreeChildLink>>,
1458    parent_id: EntityId,
1459) -> Vec<EntityId> {
1460    children_by_parent
1461        .get(&parent_id)
1462        .map(|links| links.iter().map(|link| link.child_id).collect())
1463        .unwrap_or_default()
1464}
1465
1466fn subtree_ids(state: &TreeState, root_id: EntityId) -> BTreeSet<EntityId> {
1467    let mut ids = BTreeSet::new();
1468    let mut queue = VecDeque::new();
1469    queue.push_back(root_id);
1470    while let Some(node_id) = queue.pop_front() {
1471        if !ids.insert(node_id) {
1472            continue;
1473        }
1474        if let Some(children) = state.children_by_parent.get(&node_id) {
1475            for child in children {
1476                queue.push_back(child.child_id);
1477            }
1478        }
1479    }
1480    ids
1481}
1482
1483fn flatten_tree_preorder(state: &TreeState, root_id: EntityId) -> Vec<EntityId> {
1484    let mut ordered = Vec::new();
1485    flatten_tree_preorder_inner(state, root_id, &mut ordered);
1486    ordered
1487}
1488
1489fn flatten_tree_preorder_inner(state: &TreeState, current: EntityId, ordered: &mut Vec<EntityId>) {
1490    if let Some(children) = state.children_by_parent.get(&current) {
1491        for child in children {
1492            ordered.push(child.child_id);
1493            flatten_tree_preorder_inner(state, child.child_id, ordered);
1494        }
1495    }
1496}
1497
1498fn validate_tree_cycles(
1499    node_id: EntityId,
1500    depth: usize,
1501    state: &TreeState,
1502    visited: &mut BTreeSet<EntityId>,
1503    stack: &mut BTreeSet<EntityId>,
1504    depths: &mut BTreeMap<EntityId, usize>,
1505    issues: &mut Vec<TreeIssue>,
1506) {
1507    if stack.contains(&node_id) {
1508        issues.push(TreeIssue {
1509            code: "cycle_detected",
1510            message: format!("cycle detected at node '{}'", node_id.raw()),
1511            entity_id: Some(node_id),
1512        });
1513        return;
1514    }
1515    if !visited.insert(node_id) {
1516        return;
1517    }
1518    stack.insert(node_id);
1519    depths.insert(node_id, depth);
1520    if let Some(children) = state.children_by_parent.get(&node_id) {
1521        for child in children {
1522            validate_tree_cycles(
1523                child.child_id,
1524                depth + 1,
1525                state,
1526                visited,
1527                stack,
1528                depths,
1529                issues,
1530            );
1531        }
1532    }
1533    stack.remove(&node_id);
1534}
1535
1536fn effective_max_children(
1537    definition: &crate::physical::PhysicalTreeDefinition,
1538    state: &TreeState,
1539    node_id: EntityId,
1540) -> RedDBResult<usize> {
1541    let Some(node) = state.nodes.get(&node_id) else {
1542        return Err(RedDBError::NotFound(format!(
1543            "node '{}' was not found in tree '{}.{}'",
1544            node_id.raw(),
1545            definition.collection,
1546            definition.name
1547        )));
1548    };
1549    if let Some(value) = node.max_children_override {
1550        if value == 0 {
1551            return Err(RedDBError::Query(format!(
1552                "node '{}' has non-positive max children override",
1553                node_id.raw()
1554            )));
1555        }
1556        return Ok(value);
1557    }
1558    Ok(definition.default_max_children)
1559}
1560
1561fn metadata_tree_name(metadata: &Metadata) -> Option<&str> {
1562    match metadata.get(TREE_METADATA_NAME) {
1563        Some(MetadataValue::String(value)) => Some(value.as_str()),
1564        _ => None,
1565    }
1566}
1567
1568fn metadata_usize(metadata: &Metadata, key: &str) -> Option<usize> {
1569    match metadata.get(key) {
1570        Some(MetadataValue::Int(value)) if *value >= 0 => Some(*value as usize),
1571        Some(MetadataValue::Timestamp(value)) => (*value).try_into().ok(),
1572        _ => None,
1573    }
1574}
1575
1576fn metadata_invalid_max_children(metadata: &Metadata) -> bool {
1577    match metadata.get(TREE_METADATA_MAX_CHILDREN) {
1578        None => false,
1579        Some(MetadataValue::Int(value)) => *value <= 0,
1580        Some(MetadataValue::Timestamp(value)) => *value == 0,
1581        _ => true,
1582    }
1583}
1584
1585fn parse_tree_entity_id_text(value: &str) -> Option<EntityId> {
1586    value
1587        .strip_prefix('e')
1588        .unwrap_or(value)
1589        .parse::<u64>()
1590        .ok()
1591        .filter(|value| *value > 0)
1592        .map(EntityId::new)
1593}