1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
# frozen_string_literal: true
module Clusters
class ClustersHierarchy
DEPTH_COLUMN = :depth
def initialize(clusterable)
@clusterable = clusterable
end
# Returns clusters in order from deepest to highest group
def base_and_ancestors
cte = recursive_cte
cte_alias = cte.table.alias(model.table_name)
model
.unscoped
.where.not('clusters.id' => nil)
.with
.recursive(cte.to_arel)
.from(cte_alias)
.order(depth_order_clause)
end
private
attr_reader :clusterable
def recursive_cte
cte = Gitlab::SQL::RecursiveCTE.new(:clusters_cte)
base_query = case clusterable
when ::Group
group_clusters_base_query
when ::Project
project_clusters_base_query
else
raise ArgumentError, "unknown type for #{clusterable}"
end
if clusterable.is_a?(::Project)
cte << same_namespace_management_clusters_query
end
cte << base_query
cte << parent_query(cte)
cte
end
# Returns project-level clusters where the project is the management project
# for the cluster. The management project has to be in the same namespace /
# group as the cluster's project.
#
# Support for management project in sub-groups is planned in
# https://gitlab.com/gitlab-org/gitlab/issues/34650
#
# NB: group_parent_id is un-used but we still need to match the same number of
# columns as other queries in the CTE.
def same_namespace_management_clusters_query
clusterable.management_clusters
.project_type
.select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
.for_project_namespace(clusterable.namespace_id)
end
# Management clusters should be first in the hierarchy so we use 0 for the
# depth column.
#
# Only applicable if the clusterable is a project (most especially when
# requesting project.deployment_platform).
def depth_order_clause
return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project)
order = <<~SQL
(CASE clusters.management_project_id
WHEN :project_id THEN 0
ELSE #{DEPTH_COLUMN}
END) ASC
SQL
values = {
project_id: clusterable.id
}
Arel.sql(model.sanitize_sql_array([Arel.sql(order), values]))
end
def group_clusters_base_query
group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id')
join_sources = ::Group.left_joins(:clusters).arel.join_sources
model
.unscoped
.select([clusters_star, group_parent_id_alias, "1 AS #{DEPTH_COLUMN}"])
.where(groups[:id].eq(clusterable.id))
.from(groups)
.joins(join_sources)
end
def project_clusters_base_query
projects = ::Project.arel_table
project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id')
join_sources = ::Project.left_joins(:clusters).arel.join_sources
model
.unscoped
.select([clusters_star, project_parent_id_alias, "1 AS #{DEPTH_COLUMN}"])
.where(projects[:id].eq(clusterable.id))
.from(projects)
.joins(join_sources)
end
def parent_query(cte)
group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id')
model
.unscoped
.select([clusters_star, group_parent_id_alias, cte.table[DEPTH_COLUMN] + 1])
.from([cte.table, groups])
.joins('LEFT OUTER JOIN cluster_groups ON cluster_groups.group_id = namespaces.id')
.joins('LEFT OUTER JOIN clusters ON cluster_groups.cluster_id = clusters.id')
.where(groups[:id].eq(cte.table[:group_parent_id]))
end
def model
Clusters::Cluster
end
def clusters
@clusters ||= model.arel_table
end
def groups
@groups ||= ::Group.arel_table
end
def clusters_star
@clusters_star ||= clusters[Arel.star]
end
def alias_as_column(value, alias_to)
Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
end
end
end
|