| 1010 | } |
| 1011 | |
| 1012 | fn create_view( |
| 1013 | &mut self, |
| 1014 | name: &str, |
| 1015 | kind: ViewScope, |
| 1016 | parent: Option<u64>, |
| 1017 | ) -> PristineResult<ViewState> { |
| 1018 | // Check if view already exists |
| 1019 | { |
| 1020 | let table = self.txn.open_table(VIEWS)?; |
| 1021 | if table.get(name)?.is_some() { |
| 1022 | return Err(PristineError::ViewAlreadyExists { |
| 1023 | name: name.to_string(), |
| 1024 | }); |
| 1025 | } |
| 1026 | } |
| 1027 | |
| 1028 | // Validate parent exists if specified, and detect cycles |
| 1029 | if let Some(parent_id) = parent { |
| 1030 | let parent_view = ViewTxnT::get_view_by_id(self, parent_id)?.ok_or_else(|| { |
| 1031 | PristineError::ViewNotFound { |
| 1032 | name: format!("parent view id={}", parent_id), |
| 1033 | } |
| 1034 | })?; |
| 1035 | |
| 1036 | // Cycle detection: walk the parent chain from the proposed parent |
| 1037 | // upward. If we ever encounter our own (not-yet-allocated) name, |
| 1038 | // there's a cycle. Since the view doesn't exist yet, we only need |
| 1039 | // to check that the parent chain terminates without revisiting |
| 1040 | // `parent_id` — which is guaranteed as long as the existing graph |
| 1041 | // is acyclic and we're adding a leaf. |
| 1042 | // |
| 1043 | // However, we also guard against the degenerate case where someone |
| 1044 | // passes parent == self (once IDs are known). Since we haven't |
| 1045 | // allocated an ID yet, the only risk is the parent chain itself |
| 1046 | // being cyclic (which would be a pre-existing bug). We do a bounded |
| 1047 | // walk as a safety check. |
| 1048 | let mut visited = std::collections::HashSet::new(); |
| 1049 | visited.insert(parent_id); |
| 1050 | let mut cursor = parent_view.parent; |
| 1051 | while let Some(ancestor_id) = cursor { |
| 1052 | if !visited.insert(ancestor_id) { |
| 1053 | // We've seen this ID before — cycle detected in existing chain |
| 1054 | return Err(PristineError::ViewCycleDetected { |
| 1055 | name: name.to_string(), |
| 1056 | parent_name: parent_view.name.clone(), |
| 1057 | }); |
| 1058 | } |
| 1059 | match ViewTxnT::get_view_by_id(self, ancestor_id)? { |
| 1060 | Some(ancestor) => cursor = ancestor.parent, |
| 1061 | None => break, // Broken chain — parent doesn't exist (shouldn't happen) |
| 1062 | } |
| 1063 | } |
| 1064 | } |
| 1065 | |
| 1066 | // Allocate ID and create state |
| 1067 | let id = self.next_view_id.fetch_add(1, Ordering::SeqCst); |
| 1068 | let state = ViewState::with_scope(id, name.to_string(), kind, parent); |
| 1069 | |