112 lines
3.4 KiB
Ruby
112 lines
3.4 KiB
Ruby
|
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
|