{"record":{"id":"c854b6b890ff0e02","repo":"zed-industries/zed","slug":"circularnesting","errorCode":"CircularNesting","errorMessage":"CircularNesting","messagePattern":"CircularNesting","errorType":"error_code","errorClass":"RpcError","httpStatus":null,"severity":"error","filePath":"crates/collab/src/db/queries/channels.rs","lineNumber":930,"sourceCode":"        channel_id: ChannelId,\n        new_parent_id: ChannelId,\n        admin_id: UserId,\n    ) -> Result<(ChannelId, Vec<Channel>)> {\n        self.transaction(|tx| async move {\n            let channel = self.get_channel_internal(channel_id, &tx).await?;\n            self.check_user_is_channel_admin(&channel, admin_id, &tx)\n                .await?;\n            let new_parent = self.get_channel_internal(new_parent_id, &tx).await?;\n\n            if new_parent.root_id() != channel.root_id() {\n                Err(anyhow!(ErrorCode::WrongMoveTarget))?;\n            }\n\n            if new_parent\n                .ancestors_including_self()\n                .any(|id| id == channel.id)\n            {\n                Err(anyhow!(ErrorCode::CircularNesting))?;\n            }\n\n            if channel.visibility == ChannelVisibility::Public\n                && new_parent.visibility != ChannelVisibility::Public\n            {\n                Err(anyhow!(ErrorCode::BadPublicNesting))?;\n            }\n\n            let root_id = channel.root_id();\n            let new_parent_path = new_parent.path();\n            let old_path = format!(\"{}{}/\", channel.parent_path, channel.id);\n            let new_path = format!(\"{}{}/\", new_parent_path, channel.id);\n            let new_order = max_order(&new_parent_path, &tx).await? + 1;\n\n            let mut model = channel.into_active_model();\n            model.parent_path = ActiveValue::Set(new_parent.path());\n            model.channel_order = ActiveValue::Set(new_order);\n            let channel = model.update(&*tx).await?;","sourceCodeStart":912,"sourceCodeEnd":948,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/crates/collab/src/db/queries/channels.rs#L912-L948","documentation":"ErrorCode::CircularNesting from move_channel (crates/collab/src/db/queries/channels.rs:930): the new parent's ancestor chain (ancestors_including_self) contains the channel being moved, i.e. the new parent is the channel itself or one of its descendants. Nesting a channel inside its own subtree would create a cycle in the materialized parent_path.","triggerScenarios":"move_channel where new_parent_id == channel_id, or new_parent is a direct child/descendant of the moved channel — classic when swapping a parent and child ('move A into B' where B is already inside A).","commonSituations":"Drag-and-drop races with stale tree state: the UI moves B under A, then a queued move puts A under B; two admins restructuring concurrently; client caching that still shows the pre-move hierarchy.","solutions":["Client-side, reject any drop target whose ancestor chain includes the dragged channel before sending the RPC","Re-fetch the channel tree after any successful move so subsequent moves validate against fresh parent paths","To swap hierarchy levels, first move the child out of the subtree to a neutral sibling, then perform the second move"],"exampleFix":"// before\nclient.move_channel(channel_id, channel_id /*self*/, admin_id).await?;\n\n// after\nlet ok = !new_parent\n    .ancestors_including_self()\n    .any(|id| id == channel.id);\nassert!(ok, \"circular move\");\nclient.move_channel(channel_id, new_parent_id, admin_id).await?;","handlingStrategy":"validation","validationCode":"// Reject drops onto the channel itself or any descendant\nlet circular = new_parent\n    .ancestors_including_self()\n    .any(|id| id == channel.id);\nif circular {\n    return Err(anyhow!(\"cannot move a channel into its own subtree\"));\n}","typeGuard":null,"tryCatchPattern":"match db.move_channel(channel_id, new_parent_id, admin_id).await {\n    Ok(v) => Ok(v),\n    Err(err) if err.to_string().contains(\"CircularNesting\") => {\n        Err(anyhow!(\"move would create a cycle; pick a target outside the subtree\"))\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Run the ancestors_including_self check client-side on every drop candidate","Serialize UI moves: wait for one move to finish (tree refreshed) before starting the next","To invert a parent/child pair, move the child to a neutral sibling first"],"tags":["collab","channels","tree","cycle"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}