Class: Gitlab::ObjectHierarchy
- Inherits:
-
Object
- Object
- Gitlab::ObjectHierarchy
- 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, Ci::ProcessableObjectHierarchy, WorkItems::WorkItemHierarchy
Constant Summary collapse
- DEPTH_COLUMN =
:depth
Instance Attribute Summary collapse
-
#ancestors_base ⇒ Object
readonly
Returns the value of attribute ancestors_base.
-
#descendants_base ⇒ Object
readonly
Returns the value of attribute descendants_base.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#unscoped_model ⇒ Object
readonly
Returns the value of attribute unscoped_model.
Instance Method Summary collapse
-
#all_objects ⇒ Object
Returns a relation that includes the base objects, their ancestors, and the descendants of the base objects.
-
#ancestors(upto: nil, hierarchy_order: nil) ⇒ Object
Returns the set of ancestors of a given relation, but excluding the given relation.
-
#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).
-
#base_and_descendant_ids ⇒ Object
Returns a relation that includes ID of the descendants_base set of objects and all their descendants IDs (recursively).
-
#base_and_descendants(with_depth: false) ⇒ Object
Returns a relation that includes the descendants_base set of objects and all their descendants (recursively).
-
#descendants ⇒ Object
Returns the set of descendants of a given relation, but excluding the given relation.
-
#initialize(ancestors_base, descendants_base = ancestors_base, options: {}) ⇒ ObjectHierarchy
constructor
ancestors_base - An instance of ActiveRecord::Relation for which to get parent objects.
-
#max_descendants_depth ⇒ Object
Returns the maximum depth starting from the base A base object with no children has a maximum depth of
1.
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.
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 = end |
Instance Attribute Details
#ancestors_base ⇒ Object (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_base ⇒ Object (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 |
#model ⇒ Object (readonly)
Returns the value of attribute model.
10 11 12 |
# File 'lib/gitlab/object_hierarchy.rb', line 10 def model @model end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
10 11 12 |
# File 'lib/gitlab/object_hierarchy.rb', line 10 def @options end |
#unscoped_model ⇒ Object (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_objects ⇒ Object
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
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/gitlab/object_hierarchy.rb', line 119 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.
44 45 46 |
# File 'lib/gitlab/object_hierarchy.rb', line 44 def ancestors(upto: nil, hierarchy_order: nil) base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).id_not_in(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
63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/gitlab/object_hierarchy.rb', line 63 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_ids ⇒ Object
Returns a relation that includes ID of the descendants_base set of objects and all their descendants IDs (recursively).
92 93 94 |
# File 'lib/gitlab/object_hierarchy.rb', line 92 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
83 84 85 86 87 88 |
# File 'lib/gitlab/object_hierarchy.rb', line 83 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 |
#descendants ⇒ Object
Returns the set of descendants of a given relation, but excluding the given relation
28 29 30 |
# File 'lib/gitlab/object_hierarchy.rb', line 28 def descendants base_and_descendants.id_not_in(descendants_base.select(:id)) end |
#max_descendants_depth ⇒ Object
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 |