CREATE TABLE IF NOT EXISTS cluster_node_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, parent_group_id UUID REFERENCES cluster_node_groups(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT, sort_order INTEGER NOT NULL DEFAULT 0, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT cluster_node_groups_name_check CHECK (length(trim(name)) > 0), CONSTRAINT cluster_node_groups_not_self_parent CHECK (parent_group_id IS NULL OR parent_group_id <> id), UNIQUE (cluster_id, id) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_cluster_node_groups_unique_sibling_name ON cluster_node_groups(cluster_id, COALESCE(parent_group_id, '00000000-0000-0000-0000-000000000000'::uuid), lower(name)); CREATE INDEX IF NOT EXISTS idx_cluster_node_groups_parent ON cluster_node_groups(cluster_id, parent_group_id, sort_order, name); CREATE TABLE IF NOT EXISTS cluster_node_group_memberships ( cluster_id UUID NOT NULL, node_id UUID NOT NULL, group_id UUID NOT NULL, assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata JSONB NOT NULL DEFAULT '{}'::JSONB, PRIMARY KEY (cluster_id, node_id), FOREIGN KEY (cluster_id, node_id) REFERENCES cluster_memberships(cluster_id, node_id) ON DELETE CASCADE, FOREIGN KEY (cluster_id, group_id) REFERENCES cluster_node_groups(cluster_id, id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_cluster_node_group_memberships_group ON cluster_node_group_memberships(cluster_id, group_id);