2018-12-13 13:39:08 +05:30
# frozen_string_literal: true
2017-09-10 17:25:29 +05:30
module Gitlab
2019-02-15 15:39:39 +05:30
# Retrieving of parent or child objects based on a base ActiveRecord relation.
2017-09-10 17:25:29 +05:30
#
# This class uses recursive CTEs and as a result will only work on PostgreSQL.
2019-02-15 15:39:39 +05:30
class ObjectHierarchy
2019-07-07 11:18:12 +05:30
DEPTH_COLUMN = :depth
2021-06-08 01:23:25 +05:30
attr_reader :ancestors_base , :descendants_base , :model , :options , :unscoped_model
2017-09-10 17:25:29 +05:30
# ancestors_base - An instance of ActiveRecord::Relation for which to
2019-02-15 15:39:39 +05:30
# get parent objects.
2017-09-10 17:25:29 +05:30
# descendants_base - An instance of ActiveRecord::Relation for which to
2019-02-15 15:39:39 +05:30
# get child objects. If omitted, ancestors_base is used.
2020-11-24 15:15:51 +05:30
def initialize ( ancestors_base , descendants_base = ancestors_base , options : { } )
2021-06-08 01:23:25 +05:30
raise ArgumentError , " Model of ancestors_base does not match model of descendants_base " if ancestors_base . model != descendants_base . model
2017-09-10 17:25:29 +05:30
@ancestors_base = ancestors_base
@descendants_base = descendants_base
@model = ancestors_base . model
2021-06-08 01:23:25 +05:30
@unscoped_model = @model . unscoped
2020-11-24 15:15:51 +05:30
@options = options
2017-09-10 17:25:29 +05:30
end
2018-03-17 18:26:18 +05:30
# Returns the set of descendants of a given relation, but excluding the given
# relation
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2018-03-17 18:26:18 +05:30
def descendants
base_and_descendants . where . not ( id : descendants_base . select ( :id ) )
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2018-03-17 18:26:18 +05:30
2019-07-07 11:18:12 +05:30
# Returns the maximum depth starting from the base
# A base object with no children has a maximum depth of `1`
def max_descendants_depth
base_and_descendants ( with_depth : true ) . maximum ( DEPTH_COLUMN )
end
2018-03-17 18:26:18 +05:30
# 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.
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-02-15 15:39:39 +05:30
def ancestors ( upto : nil , hierarchy_order : nil )
base_and_ancestors ( upto : upto , hierarchy_order : hierarchy_order ) . where . not ( id : ancestors_base . select ( :id ) )
2018-03-17 18:26:18 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
# Returns a relation that includes the ancestors_base set of objects
2017-09-10 17:25:29 +05:30
# and all their ancestors (recursively).
2018-03-17 18:26:18 +05:30
#
# Passing an `upto` will stop the recursion once the specified parent_id is
2020-04-08 14:13:33 +05:30
# reached. So all ancestors *lower* than the specified ancestor will be
2018-03-17 18:26:18 +05:30
# included.
2019-02-15 15:39:39 +05:30
#
# 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.
2021-04-17 20:07:23 +05:30
#
# Note: By default the order is breadth-first
2019-02-15 15:39:39 +05:30
# rubocop: disable CodeReuse/ActiveRecord
def base_and_ancestors ( upto : nil , hierarchy_order : nil )
2022-08-27 11:52:29 +05:30
upto_id = upto . try ( :id ) || upto
cte = base_and_ancestors_cte ( upto_id , hierarchy_order )
2021-11-11 11:23:49 +05:30
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
2021-09-30 23:02:18 +05:30
read_only ( recursive_query )
2017-09-10 17:25:29 +05:30
end
2019-02-15 15:39:39 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-09-10 17:25:29 +05:30
2019-02-15 15:39:39 +05:30
# Returns a relation that includes the descendants_base set of objects
2017-09-10 17:25:29 +05:30
# and all their descendants (recursively).
2019-07-07 11:18:12 +05:30
#
# 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
2021-04-17 20:07:23 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-07-07 11:18:12 +05:30
def base_and_descendants ( with_depth : false )
2021-11-11 11:23:49 +05:30
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 ) )
2017-09-10 17:25:29 +05:30
end
2021-04-17 20:07:23 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-09-10 17:25:29 +05:30
2022-07-23 23:45:48 +05:30
# Returns a relation that includes ID of the descendants_base set of objects
# and all their descendants IDs (recursively).
# rubocop: disable CodeReuse/ActiveRecord
def base_and_descendant_ids
read_only ( base_and_descendant_ids_cte . apply_to ( unscoped_model . select ( objects_table [ :id ] ) ) )
end
# rubocop: enable CodeReuse/ActiveRecord
2019-02-15 15:39:39 +05:30
# Returns a relation that includes the base objects, their ancestors,
# and the descendants of the base objects.
2017-09-10 17:25:29 +05:30
#
# 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.
#
2019-02-15 15:39:39 +05:30
# If nested objects are not supported, ancestors_base is returned.
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-02-15 15:39:39 +05:30
def all_objects
2017-09-10 17:25:29 +05:30
ancestors = base_and_ancestors_cte
descendants = base_and_descendants_cte
2019-02-15 15:39:39 +05:30
ancestors_table = ancestors . alias_to ( objects_table )
descendants_table = descendants . alias_to ( objects_table )
2017-09-10 17:25:29 +05:30
2021-06-08 01:23:25 +05:30
ancestors_scope = unscoped_model . from ( ancestors_table )
descendants_scope = unscoped_model . from ( descendants_table )
2021-04-17 20:07:23 +05:30
2021-06-08 01:23:25 +05:30
relation = unscoped_model
2017-09-10 17:25:29 +05:30
. with
. recursive ( ancestors . to_arel , descendants . to_arel )
2018-12-05 23:21:45 +05:30
. from_union ( [
2022-11-25 23:54:43 +05:30
ancestors_scope ,
descendants_scope
] )
2018-03-17 18:26:18 +05:30
read_only ( relation )
2017-09-10 17:25:29 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-09-10 17:25:29 +05:30
private
2021-04-17 20:07:23 +05:30
# Remove the extra `depth` field using an INNER JOIN to avoid breaking UNION queries
# and ordering the rows based on the `depth` column to maintain the row order.
#
# rubocop: disable CodeReuse/ActiveRecord
def remove_depth_and_maintain_order ( relation , hierarchy_order : :asc )
joined_relation = model . joins ( " INNER JOIN ( #{ relation . select ( :id , :depth ) . to_sql } ) namespaces_join_table on namespaces_join_table.id = #{ model . table_name } .id " ) . order ( " namespaces_join_table.depth " = > hierarchy_order )
model . from ( Arel :: Nodes :: As . new ( joined_relation . arel , objects_table ) )
end
# rubocop: enable CodeReuse/ActiveRecord
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-02-15 15:39:39 +05:30
def base_and_ancestors_cte ( stop_id = nil , hierarchy_order = nil )
2017-09-10 17:25:29 +05:30
cte = SQL :: RecursiveCTE . new ( :base_and_ancestors )
2019-02-15 15:39:39 +05:30
base_query = ancestors_base . except ( :order )
2021-11-11 11:23:49 +05:30
base_query = base_query . select ( " 1 as #{ DEPTH_COLUMN } " , " ARRAY[ #{ objects_table . name } .id] AS tree_path " , " false AS tree_cycle " , base_query . default_select_columns ) if hierarchy_order
2019-02-15 15:39:39 +05:30
cte << base_query
2017-09-10 17:25:29 +05:30
# Recursively get all the ancestors of the base set.
2021-06-08 01:23:25 +05:30
parent_query = unscoped_model
2020-11-24 15:15:51 +05:30
. from ( from_tables ( cte ) )
. where ( ancestor_conditions ( cte ) )
2017-09-10 17:25:29 +05:30
. except ( :order )
2019-02-15 15:39:39 +05:30
2019-07-07 11:18:12 +05:30
if hierarchy_order
quoted_objects_table_name = model . connection . quote_table_name ( objects_table . name )
parent_query = parent_query . select (
cte . table [ DEPTH_COLUMN ] + 1 ,
" tree_path || #{ quoted_objects_table_name } .id " ,
" #{ quoted_objects_table_name } .id = ANY(tree_path) " ,
2021-11-11 11:23:49 +05:30
parent_query . default_select_columns
2019-07-07 11:18:12 +05:30
) . where ( cte . table [ :tree_cycle ] . eq ( false ) )
end
2020-11-24 15:15:51 +05:30
parent_query = parent_query . where ( parent_id_column ( cte ) . not_eq ( stop_id ) ) if stop_id
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
cte << parent_query
2017-09-10 17:25:29 +05:30
cte
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-09-10 17:25:29 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-07-07 11:18:12 +05:30
def base_and_descendants_cte ( with_depth : false )
2017-09-10 17:25:29 +05:30
cte = SQL :: RecursiveCTE . new ( :base_and_descendants )
2019-07-07 11:18:12 +05:30
base_query = descendants_base . except ( :order )
2021-11-11 11:23:49 +05:30
base_query = base_query . select ( " 1 AS #{ DEPTH_COLUMN } " , " ARRAY[ #{ objects_table . name } .id] AS tree_path " , " false AS tree_cycle " , base_query . default_select_columns ) if with_depth
2019-07-07 11:18:12 +05:30
cte << base_query
2017-09-10 17:25:29 +05:30
# Recursively get all the descendants of the base set.
2021-06-08 01:23:25 +05:30
descendants_query = unscoped_model
2020-11-24 15:15:51 +05:30
. from ( from_tables ( cte ) )
. where ( descendant_conditions ( cte ) )
2017-09-10 17:25:29 +05:30
. except ( :order )
2019-07-07 11:18:12 +05:30
if with_depth
quoted_objects_table_name = model . connection . quote_table_name ( objects_table . name )
descendants_query = descendants_query . select (
cte . table [ DEPTH_COLUMN ] + 1 ,
" tree_path || #{ quoted_objects_table_name } .id " ,
" #{ quoted_objects_table_name } .id = ANY(tree_path) " ,
2021-11-11 11:23:49 +05:30
descendants_query . default_select_columns
2019-07-07 11:18:12 +05:30
) . where ( cte . table [ :tree_cycle ] . eq ( false ) )
end
cte << descendants_query
2017-09-10 17:25:29 +05:30
cte
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-09-10 17:25:29 +05:30
2022-07-23 23:45:48 +05:30
# rubocop: disable CodeReuse/ActiveRecord
def base_and_descendant_ids_cte
cte = SQL :: RecursiveCTE . new ( :base_and_descendants )
base_query = descendants_base . except ( :order ) . select ( objects_table [ :id ] )
cte << base_query
# Recursively get all the descendants of the base set.
descendants_query = unscoped_model
. select ( objects_table [ :id ] )
. from ( from_tables ( cte ) )
. where ( descendant_conditions ( cte ) )
. except ( :order )
cte << descendants_query
cte
end
# rubocop: enable CodeReuse/ActiveRecord
2019-02-15 15:39:39 +05:30
def objects_table
2017-09-10 17:25:29 +05:30
model . arel_table
end
2018-03-17 18:26:18 +05:30
2020-11-24 15:15:51 +05:30
def parent_id_column ( cte )
cte . table [ :parent_id ]
end
def from_tables ( cte )
[ objects_table , cte . table ]
end
def ancestor_conditions ( cte )
objects_table [ :id ] . eq ( cte . table [ :parent_id ] )
end
def descendant_conditions ( cte )
objects_table [ :parent_id ] . eq ( cte . table [ :id ] )
end
2018-03-17 18:26:18 +05:30
def read_only ( relation )
# relations using a CTE are not safe to use with update_all as it will
# throw away the CTE, hence we mark them as read-only.
relation . extend ( Gitlab :: Database :: ReadOnlyRelation )
relation
end
2017-09-10 17:25:29 +05:30
end
end
2019-12-04 20:38:33 +05:30
2021-06-08 01:23:25 +05:30
Gitlab :: ObjectHierarchy . prepend_mod_with ( 'Gitlab::ObjectHierarchy' )