summaryrefslogtreecommitdiff
path: root/lib/gitlab/group_hierarchy.rb
blob: 5a31e56cb3077e2de9b10d775abf86731a3d1d70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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