# frozen_string_literal: true module Namespaces module Traversal module LinearScopes extend ActiveSupport::Concern include AsCte class_methods do # When filtering namespaces by the traversal_ids column to compile a # list of namespace IDs, it can be faster to reference the ID in # traversal_ids than the primary key ID column. def as_ids return super unless use_traversal_ids? select(Arel.sql('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)]').as('id')) end def roots return super unless use_traversal_ids_roots? root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct unscoped.where(id: root_ids) end def self_and_ancestors(include_self: true, upto: nil, hierarchy_order: nil) return super unless use_traversal_ids_for_ancestor_scopes? self_and_ancestors_from_inner_join(include_self: include_self, upto: upto, hierarchy_order: hierarchy_order) end def self_and_ancestor_ids(include_self: true) return super unless use_traversal_ids_for_ancestor_scopes? self_and_ancestors(include_self: include_self).as_ids end def self_and_descendants(include_self: true) return super unless use_traversal_ids_for_descendants_scopes? self_and_descendants_with_comparison_operators(include_self: include_self) end def self_and_descendant_ids(include_self: true) return super unless use_traversal_ids_for_descendants_scopes? self_and_descendants(include_self: include_self).as_ids end def self_and_hierarchy return super unless use_traversal_ids_for_self_and_hierarchy_scopes? unscoped.from_union([all.self_and_ancestors, all.self_and_descendants(include_self: false)]) end def order_by_depth(hierarchy_order) return all unless hierarchy_order depth_order = hierarchy_order == :asc ? :desc : :asc all .select(Namespace.default_select_columns, 'array_length(traversal_ids, 1) as depth') .order(depth: depth_order, id: :asc) end # Produce a query of the form: SELECT * FROM namespaces; # # When we have queries that break this SELECT * format we can run in to errors. # For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c def normal_select unscoped.from(all, :namespaces) end private def use_traversal_ids? Feature.enabled?(:use_traversal_ids) end def use_traversal_ids_roots? Feature.enabled?(:use_traversal_ids_roots) && use_traversal_ids? end def use_traversal_ids_for_ancestor_scopes? Feature.enabled?(:use_traversal_ids_for_ancestor_scopes) && use_traversal_ids? end def use_traversal_ids_for_descendants_scopes? Feature.enabled?(:use_traversal_ids_for_descendants_scopes) && use_traversal_ids? end def use_traversal_ids_for_self_and_hierarchy_scopes? Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes) && use_traversal_ids? end def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil) base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte) unnest = if include_self base_cte.table[:traversal_ids] else base_cte_traversal_ids = 'base_ancestors_cte.traversal_ids' traversal_ids_range = "1:array_length(#{base_cte_traversal_ids},1)-1" Arel.sql("#{base_cte_traversal_ids}[#{traversal_ids_range}]") end ancestor_subselect = "SELECT DISTINCT #{unnest_func(unnest).to_sql} FROM base_ancestors_cte" ancestors_join = <<~SQL INNER JOIN (#{ancestor_subselect}) AS ancestors(ancestor_id) ON namespaces.id = ancestors.ancestor_id SQL namespaces = Arel::Table.new(:namespaces) records = unscoped .with(base_cte.to_arel) .from(namespaces) .joins(ancestors_join) .order_by_depth(hierarchy_order) if upto upto_ancestor_ids = unscoped.where(id: upto).select(unnest_func(Arel.sql('traversal_ids'))) records = records.where.not(id: upto_ancestor_ids) end records end def self_and_descendants_with_comparison_operators(include_self: true) base = all.select(:id, :traversal_ids) base_cte = base.as_cte(:descendants_base_cte) namespaces = Arel::Table.new(:namespaces) superset_cte = self.superset_cte(base_cte.table.name) withs = [base_cte.to_arel, superset_cte.to_arel] # Order is important. namespace should be last to handle future joins. froms = [superset_cte.table, namespaces] base_ref = froms.first # Bound the search space to ourselves (optional) and descendants. # # WHERE next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids records = unscoped .distinct .with(*withs) .from(froms) .where(next_sibling_func(base_ref[:traversal_ids]).gt(namespaces[:traversal_ids])) # AND base_cte.traversal_ids <= namespaces.traversal_ids if include_self records.where(base_ref[:traversal_ids].lteq(namespaces[:traversal_ids])) else records.where(base_ref[:traversal_ids].lt(namespaces[:traversal_ids])) end end def next_sibling_func(*args) Arel::Nodes::NamedFunction.new('next_traversal_ids_sibling', args) end def unnest_func(*args) Arel::Nodes::NamedFunction.new('unnest', args) end def superset_cte(base_name) superset_sql = <<~SQL SELECT d1.traversal_ids FROM #{base_name} d1 WHERE NOT EXISTS ( SELECT 1 FROM #{base_name} d2 WHERE d2.id = ANY(d1.traversal_ids) AND d2.id <> d1.id ) SQL Gitlab::SQL::CTE.new(:superset, superset_sql, materialized: false) end end end end end