summaryrefslogtreecommitdiff
path: root/lib/gitlab/group_hierarchy.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/group_hierarchy.rb')
-rw-r--r--lib/gitlab/group_hierarchy.rb111
1 files changed, 111 insertions, 0 deletions
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
new file mode 100644
index 00000000000..5a31e56cb30
--- /dev/null
+++ b/lib/gitlab/group_hierarchy.rb
@@ -0,0 +1,111 @@
+module Gitlab
+ # Retrieving of parent or child groups based on a base ActiveRecord relation.
+ #
+ # This class uses recursive CTEs and as a result will only work on PostgreSQL.
+ class GroupHierarchy
+ attr_reader :ancestors_base, :descendants_base, :model
+
+ # ancestors_base - An instance of ActiveRecord::Relation for which to
+ # get parent groups.
+ # descendants_base - An instance of ActiveRecord::Relation for which to
+ # get child groups. If omitted, ancestors_base is used.
+ def initialize(ancestors_base, descendants_base = ancestors_base)
+ raise ArgumentError.new("Model of ancestors_base does not match model of descendants_base") if ancestors_base.model != descendants_base.model
+
+ @ancestors_base = ancestors_base
+ @descendants_base = descendants_base
+ @model = ancestors_base.model
+ end
+
+ # Returns a relation that includes the ancestors_base set of groups
+ # and all their ancestors (recursively).
+ def base_and_ancestors
+ return ancestors_base unless Group.supports_nested_groups?
+
+ base_and_ancestors_cte.apply_to(model.all)
+ end
+
+ # Returns a relation that includes the descendants_base set of groups
+ # and all their descendants (recursively).
+ def base_and_descendants
+ return descendants_base unless Group.supports_nested_groups?
+
+ base_and_descendants_cte.apply_to(model.all)
+ end
+
+ # Returns a relation that includes the base groups, their ancestors,
+ # and the descendants of the base groups.
+ #
+ # The resulting query will roughly look like the following:
+ #
+ # WITH RECURSIVE ancestors AS ( ... ),
+ # descendants AS ( ... )
+ # SELECT *
+ # FROM (
+ # SELECT *
+ # FROM ancestors namespaces
+ #
+ # UNION
+ #
+ # SELECT *
+ # FROM descendants namespaces
+ # ) groups;
+ #
+ # Using this approach allows us to further add criteria to the relation with
+ # Rails thinking it's selecting data the usual way.
+ #
+ # If nested groups are not supported, ancestors_base is returned.
+ def all_groups
+ return ancestors_base unless Group.supports_nested_groups?
+
+ ancestors = base_and_ancestors_cte
+ descendants = base_and_descendants_cte
+
+ ancestors_table = ancestors.alias_to(groups_table)
+ descendants_table = descendants.alias_to(groups_table)
+
+ union = SQL::Union.new([model.unscoped.from(ancestors_table),
+ model.unscoped.from(descendants_table)])
+
+ model
+ .unscoped
+ .with
+ .recursive(ancestors.to_arel, descendants.to_arel)
+ .from("(#{union.to_sql}) #{model.table_name}")
+ end
+
+ private
+
+ def base_and_ancestors_cte
+ cte = SQL::RecursiveCTE.new(:base_and_ancestors)
+
+ cte << ancestors_base.except(:order)
+
+ # Recursively get all the ancestors of the base set.
+ cte << model
+ .from([groups_table, cte.table])
+ .where(groups_table[:id].eq(cte.table[:parent_id]))
+ .except(:order)
+
+ cte
+ end
+
+ def base_and_descendants_cte
+ cte = SQL::RecursiveCTE.new(:base_and_descendants)
+
+ cte << descendants_base.except(:order)
+
+ # Recursively get all the descendants of the base set.
+ cte << model
+ .from([groups_table, cte.table])
+ .where(groups_table[:parent_id].eq(cte.table[:id]))
+ .except(:order)
+
+ cte
+ end
+
+ def groups_table
+ model.arel_table
+ end
+ end
+end