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.

Direct Known Subclasses

Ci::PipelineObjectHierarchy

Constant Summary collapse

DEPTH_COLUMN =
:depth

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ancestors_base, descendants_base = ancestors_base) ⇒ 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
# File 'lib/gitlab/object_hierarchy.rb', line 16

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

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

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


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/gitlab/object_hierarchy.rb', line 103

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)

  relation = model
    .unscoped
    .with
    .recursive(ancestors.to_arel, descendants.to_arel)
    .from_union([
      model.unscoped.from(ancestors_table),
      model.unscoped.from(descendants_table)
    ])

  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


45
46
47
# File 'lib/gitlab/object_hierarchy.rb', line 45

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. rubocop: disable CodeReuse/ActiveRecord


63
64
65
66
67
68
# File 'lib/gitlab/object_hierarchy.rb', line 63

def base_and_ancestors(upto: nil, hierarchy_order: nil)
  recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all)
  recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order

  read_only(recursive_query)
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


76
77
78
# File 'lib/gitlab/object_hierarchy.rb', line 76

def base_and_descendants(with_depth: false)
  read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(model.all))
end

#descendantsObject

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


27
28
29
# File 'lib/gitlab/object_hierarchy.rb', line 27

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`


34
35
36
# File 'lib/gitlab/object_hierarchy.rb', line 34

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