debian-mirror-gitlab/lib/gitlab/object_hierarchy.rb

276 lines
10 KiB
Ruby
Raw Permalink Normal View History

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')