Class: Gitlab::ObjectHierarchy

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/object_hierarchy.rb

Overview

Retrieving of parent or child objects based on a base ActiveRecord relation.

This class uses recursive CTEs and as a result will only work on PostgreSQL.

Constant Summary collapse

DEPTH_COLUMN =
:depth

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ancestors_base, descendants_base = ancestors_base, options: {}) ⇒ ObjectHierarchy

ancestors_base - An instance of ActiveRecord::Relation for which to

get parent objects.

descendants_base - An instance of ActiveRecord::Relation for which to

get child objects. If omitted, ancestors_base is used.

Raises:

  • (ArgumentError)


16
17
18
19
20
21
22
23
24
# File 'lib/gitlab/object_hierarchy.rb', line 16

def initialize(ancestors_base, descendants_base = ancestors_base, options: {})
  raise ArgumentError, "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
  @unscoped_model = @model.unscoped
  @options = options
end

Instance Attribute Details

#ancestors_baseObject (readonly)

Returns the value of attribute ancestors_base.



10
11
12
# File 'lib/gitlab/object_hierarchy.rb', line 10

def ancestors_base
  @ancestors_base
end

#descendants_baseObject (readonly)

Returns the value of attribute descendants_base.



10
11
12
# File 'lib/gitlab/object_hierarchy.rb', line 10

def descendants_base
  @descendants_base
end

#modelObject (readonly)

Returns the value of attribute model.



10
11
12
# File 'lib/gitlab/object_hierarchy.rb', line 10

def model
  @model
end

#optionsObject (readonly)

Returns the value of attribute options.



10
11
12
# File 'lib/gitlab/object_hierarchy.rb', line 10

def options
  @options
end

#unscoped_modelObject (readonly)

Returns the value of attribute unscoped_model.



10
11
12
# File 'lib/gitlab/object_hierarchy.rb', line 10

def unscoped_model
  @unscoped_model
end

Instance Method Details

#all_objectsObject

Returns a relation that includes the base objects, their ancestors, and the descendants of the base objects.

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 objects are not supported, ancestors_base is returned. rubocop: disable CodeReuse/ActiveRecord



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/gitlab/object_hierarchy.rb', line 127

def all_objects
  ancestors = base_and_ancestors_cte
  descendants = base_and_descendants_cte

  ancestors_table = ancestors.alias_to(objects_table)
  descendants_table = descendants.alias_to(objects_table)

  ancestors_scope = unscoped_model.from(ancestors_table)
  descendants_scope = unscoped_model.from(descendants_table)

  relation = unscoped_model
    .with
    .recursive(ancestors.to_arel, descendants.to_arel)
    .from_union([
                  ancestors_scope,
                  descendants_scope
                ])

  read_only(relation)
end

#ancestors(upto: nil, hierarchy_order: nil) ⇒ Object

Returns the set of ancestors of a given relation, but excluding the given relation

Passing an ‘upto` will stop the recursion once the specified parent_id is reached. So all ancestors lower than the specified ancestor will be included. rubocop: disable CodeReuse/ActiveRecord



47
48
49
# File 'lib/gitlab/object_hierarchy.rb', line 47

def ancestors(upto: nil, hierarchy_order: nil)
  base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).where.not(id: ancestors_base.select(:id))
end

#base_and_ancestors(upto: nil, hierarchy_order: nil) ⇒ Object

Returns a relation that includes the ancestors_base set of objects and all their ancestors (recursively).

Passing an ‘upto` will stop the recursion once the specified parent_id is reached. So all ancestors lower than the specified ancestor will be included.

Passing a ‘hierarchy_order` with either `:asc` or `:desc` will cause the recursive query order from most nested object to root or from the root ancestor to most nested object respectively. This uses a `depth` column where `1` is defined as the depth for the base and increment as we go up each parent.

Note: By default the order is breadth-first rubocop: disable CodeReuse/ActiveRecord



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/gitlab/object_hierarchy.rb', line 67

def base_and_ancestors(upto: nil, hierarchy_order: nil)
  upto_id = upto.try(:id) || upto
  cte = base_and_ancestors_cte(upto_id, hierarchy_order)

  recursive_query = if hierarchy_order
                      # othewise depth won't be available for outer query
                      cte.apply_to(unscoped_model.all.select(objects_table[Arel.star])).order(depth: hierarchy_order)
                    else
                      cte.apply_to(unscoped_model.all)
                    end

  read_only(recursive_query)
end

#base_and_descendant_idsObject

Returns a relation that includes ID of the descendants_base set of objects and all their descendants IDs (recursively). rubocop: disable CodeReuse/ActiveRecord



99
100
101
# File 'lib/gitlab/object_hierarchy.rb', line 99

def base_and_descendant_ids
  read_only(base_and_descendant_ids_cte.apply_to(unscoped_model.select(objects_table[:id])))
end

#base_and_descendants(with_depth: false) ⇒ Object

Returns a relation that includes the descendants_base set of objects and all their descendants (recursively).

When ‘with_depth` is `true`, a `depth` column is included where it starts with `1` for the base objects and incremented as we go down the descendant tree rubocop: disable CodeReuse/ActiveRecord



88
89
90
91
92
93
# File 'lib/gitlab/object_hierarchy.rb', line 88

def base_and_descendants(with_depth: false)
  outer_select_relation = unscoped_model.all
  outer_select_relation = outer_select_relation.select(objects_table[Arel.star]) if with_depth # Otherwise Active Record will not select `depth` as it's not a table column

  read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(outer_select_relation))
end

#descendantsObject

Returns the set of descendants of a given relation, but excluding the given relation rubocop: disable CodeReuse/ActiveRecord



29
30
31
# File 'lib/gitlab/object_hierarchy.rb', line 29

def descendants
  base_and_descendants.where.not(id: descendants_base.select(:id))
end

#max_descendants_depthObject

Returns the maximum depth starting from the base A base object with no children has a maximum depth of ‘1`



36
37
38
# File 'lib/gitlab/object_hierarchy.rb', line 36

def max_descendants_depth
  base_and_descendants(with_depth: true).maximum(DEPTH_COLUMN)
end