Skip to content

Commit 7cddb9a

Browse files
authored
Merge pull request #23601 from kbrock/report_requires3
convert MiqReport from references to eager_load
2 parents def94bd + 13b942b commit 7cddb9a

File tree

9 files changed

+197
-113
lines changed

9 files changed

+197
-113
lines changed

app/models/cloud_tenant.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ def self.post_refresh_ems(ems_id, _)
187187
end
188188

189189
def self.tenant_joins_clause(scope)
190-
scope.includes(:source_tenant, :ext_management_system)
191-
.references(:source_tenant, :ext_management_system)
190+
scope.left_outer_joins(:source_tenant, :ext_management_system)
192191
end
193192
end

app/models/flavor.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ def self.class_by_ems(ext_management_system)
4343
ext_management_system&.class_by_ems(:Flavor)
4444
end
4545

46+
# NOTE: tenant (source_tenant) is far away, so it brings back a lot of models. can we use left_joins instead?
4647
def self.tenant_joins_clause(scope)
47-
scope.includes(:cloud_tenants => "source_tenant", :ext_management_system => {})
48-
.references(:cloud_tenants, :tenants, :ext_management_system)
48+
scope.left_outer_joins(:cloud_tenants => "source_tenant", :ext_management_system => {})
4949
end
5050

5151
# Create a flavor as a queued task and return the task id. The queue name and

app/models/miq_report/generator.rb

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,12 @@ def get_include
8888
include_as_hash(include.presence || invent_report_includes)
8989
end
9090

91-
def polymorphic_includes
92-
@polymorphic_includes ||= begin
93-
top_level_rels = Array(get_include.try(:keys)) + Array(get_include_for_find.try(:keys))
94-
95-
top_level_rels.uniq.each_with_object([]) do |assoc, polymorphic_rels|
96-
reflection = db_class.reflect_on_association(assoc)
97-
polymorphic_rels << assoc if reflection && reflection.polymorphic?
98-
end
99-
end
100-
end
101-
102-
def get_include_for_find_rbac
103-
polymorphic_includes.each_with_object(get_include_for_find.dup) do |key, includes|
104-
includes.delete(key)
105-
end
91+
def get_ar_includes
92+
include_for_find
10693
end
10794

108-
def get_include_rbac
109-
polymorphic_includes.each_with_object(get_include.dup) do |key, includes|
110-
includes.delete(key)
111-
end
95+
def get_ar_references
96+
db_class.prune_references(get_include)
11297
end
11398

11499
def invent_includes
@@ -118,16 +103,14 @@ def invent_includes
118103
# would like this format to go away
119104
# will go away when we drop build_reportable_data
120105
def invent_report_includes
121-
return {} unless col_order
122-
123-
col_order.each_with_object({}) do |col, ret|
124-
next unless col.include?(".")
106+
(Array(col_order) + Array(sortby)).each_with_object({}) do |col, ret|
107+
next if !col.include?(".") || col.include?("virtual_custom")
125108

126109
*rels, column = col.split(".")
127-
if col !~ /managed\./ && col !~ /virtual_custom/
128-
(rels.inject(ret) { |h, rel| h[rel] ||= {} }["columns"] ||= []) << column
129-
end
130-
end
110+
dest = rels.inject(ret) { |h, rel| (h["include"] ||= {})[rel] ||= {} }["columns"] ||= []
111+
# sortby tends to be a duplicate. So suppress duplicates for all
112+
dest << column unless dest.include?(column)
113+
end["include"]
131114
end
132115

133116
def include_as_hash(includes = include, klass = db_class, klass_cols = cols)
@@ -290,8 +273,8 @@ def generate_daily_metric_rollup_results(options = {})
290273
.where(where_clause)
291274
.where(options[:where_clause])
292275
.where(:timestamp => performance_report_time_range)
293-
.includes(get_include_for_find)
294-
.references(db_class.includes_to_references(get_include))
276+
.preload(get_ar_includes)
277+
.eager_load(get_ar_references)
295278
.limit(options[:limit])
296279
results = Rbac.filtered(results, :class => db,
297280
:filter => conditions,
@@ -305,8 +288,8 @@ def generate_interval_metric_results(options = {})
305288
results = db_class.with_interval_and_time_range(interval, performance_report_time_range)
306289
.where(where_clause)
307290
.where(options[:where_clause])
308-
.includes(get_include_for_find)
309-
.references(db_class.includes_to_references(get_include))
291+
.preload(get_ar_includes)
292+
.eager_load(get_ar_references)
310293
.limit(options[:limit])
311294

312295
# Rbac will only add miq_expression for hourly report. It will not work properly for daily because many values are rolled up from hourly.
@@ -339,8 +322,8 @@ def generate_basic_results(options = {})
339322
rbac_opts = options.merge(
340323
:targets => targets,
341324
:filter => conditions,
342-
:include_for_find => get_include_for_find_rbac,
343-
:references => get_include_rbac,
325+
:include_for_find => get_ar_includes,
326+
:references => get_ar_references,
344327
:skip_counts => true
345328
)
346329

app/models/miq_report/search.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def paged_view_search(options = {})
8686
self.display_filter = options.delete(:display_filter_hash) if options[:display_filter_hash]
8787
self.display_filter = options.delete(:display_filter_block) if options[:display_filter_block]
8888

89-
includes = get_include_for_find
89+
includes = get_ar_includes
9090
self.extras ||= {}
9191
if extras[:target_ids_for_paging] && db_class.column_names.include?('id')
9292
return get_cached_page(limited_ids(limit, offset), includes, options)
@@ -97,7 +97,7 @@ def paged_view_search(options = {})
9797
search_options = options.merge(:class => db,
9898
:conditions => conditions,
9999
:include_for_find => includes,
100-
:references => get_include
100+
:references => get_ar_references
101101
)
102102
search_options.merge!(:limit => limit, :offset => offset, :order => order) if order
103103
search_options[:extra_cols] = va_sql_cols if va_sql_cols.present?

app/models/mixins/cloud_tenancy_mixin.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ def tenant_id_clause_format(tenant_ids)
1313
end
1414

1515
def tenant_joins_clause(scope)
16-
scope.includes(:cloud_tenant => "source_tenant", :ext_management_system => {})
17-
.references(:cloud_tenants, :tenants, :ext_management_systems) # needed for the where to work
16+
scope.left_outer_joins(:cloud_tenant => "source_tenant", :ext_management_system => {})
1817
end
1918
end
2019
end

lib/extensions/ar_references.rb

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
module ArReferences
2-
# Given a nested hash of associations (used by includes)
3-
# convert into an array of table names (used by references)
4-
# If given an array of table names, will output the same array
5-
def includes_to_references(inc)
6-
return [] unless inc
2+
# Tags bring back too many rows, since we can not limit on the particular tag
3+
# So dont require them (meaning we don't join to the taggings table)
4+
SKIP_TABLES = [:tags, :taggings].freeze
75

8-
inc = Array(inc) unless inc.kind_of?(Hash)
9-
inc.flat_map do |n, v|
10-
if (ref = reflect_on_association(n.to_sym)) && !ref.polymorphic?
11-
n_table = ref.table_name
12-
v_tables = v ? ref.klass.try(:includes_to_references, v) : []
13-
[n_table] + v_tables
14-
elsif reflection_with_virtual(n.to_sym) # ignore polymorphic and virtual attribute
15-
[]
16-
else # it is probably a table name - keep it
17-
n
6+
# Given a nested hash of associations (used by includes, preload, and eager_load)
7+
# prune out polymorphic, and references to tags
8+
def prune_references(inc)
9+
return {} unless inc
10+
11+
inc = Array(inc) unless inc.kind_of?(Hash) || inc.kind_of?(Array)
12+
inc.each_with_object({}) do |(n, v), ret|
13+
n = n.to_sym
14+
if (ref = reflect_on_association(n))
15+
if !ref.polymorphic? && !SKIP_TABLES.include?(n)
16+
ret[n] = (v.present? && ref.klass.try(:prune_references, v)) || {}
17+
end
18+
# ignore virtual collections and virtual attribute
19+
elsif !reflection_with_virtual(n) && !virtual_attribute?(n)
20+
# Think this is an error. letting it slide (assuming it will throw an error elsewhere)
21+
ret[n] = {}
1822
end
1923
end
2024
end

lib/rbac/filterer.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,9 @@ def search(options = {})
296296
scope = scope.except(:offset, :limit, :order)
297297
scope = scope_targets(klass, scope, user_filters, user, miq_group)
298298
.where(conditions).where(sub_filter).where(where_clause).where(exp_sql).where(ids_clause)
299-
.includes(include_for_find).includes(exp_includes)
300299
.order(order)
301300

302-
scope = include_references(scope, klass, references, exp_includes)
301+
scope = include_references(scope, klass, include_for_find, references, exp_includes)
303302
scope = scope.limit(limit).offset(offset) if attrs[:apply_limit_in_sql]
304303

305304
# SELECT col1, (SELECT "abc") AS virtual_col
@@ -324,11 +323,17 @@ def search(options = {})
324323
# Hence auth_count calculated from inner_scope.
325324
#
326325
if inline_view?(options, scope)
327-
inner_scope = scope.except(:select, :includes, :references)
328-
scope.includes_values.each { |hash| inner_scope = add_joins(klass, inner_scope, hash) }
326+
inner_scope = scope.except(:select, :includes, :references, :eager_load, :preload)
327+
# similar to include_references but using joins
328+
# TODO: optimization: Can we remove these references from the outer query?
329+
# To do this, we may need to introduce join() to complete the triad: include/reference/join
330+
inner_scope = add_joins(klass, inner_scope, references)
331+
# TODO: do we want to klass.prune_references(exp_includes)? (see also include_references)
332+
inner_scope = add_joins(klass, inner_scope, exp_includes)
329333
if inner_scope.order_values.present?
330334
inner_scope = apply_select(klass, inner_scope, select_from_order_columns(inner_scope.order_values))
331335
end
336+
# TODO: Convert from(inner_scope.to_sql) to from(inner_scope)
332337
scope = scope.from(Arel.sql("(#{inner_scope.to_sql})").as(scope.table_name))
333338
.except(:offset, :limit, :where)
334339

@@ -424,25 +429,42 @@ def select_from_order_columns(columns)
424429
end
425430
end
426431

427-
def include_references(scope, klass, references, exp_includes)
428-
scope.references(klass.includes_to_references(references)).references(klass.includes_to_references(exp_includes))
432+
def include_references(scope, klass, includes, references, exp_includes)
433+
if scope.respond_to?(:eager_load)
434+
# TODO: do we want to klass.prune_references(exp_includes)? (see same comment for inline_view? section)
435+
scope.eager_load(references || {}).eager_load(exp_includes || {}).preload(includes)
436+
else
437+
# This is the AAAR / QueryRelation branch
438+
# TODO: drop this fallback once https://github.com/ManageIQ/query_relation/pull/43 is merged
439+
scope.references(references || {}).references(exp_includes || {}).includes(includes)
440+
end
429441
end
430442

431443
# @param includes [Array, Hash]
432444
def add_joins(klass, scope, includes)
433445
return scope unless includes
434446

447+
# NOTE: We should be pre-pruning polymorphic out of here, since they shouldn't be in references.
448+
# TODO: do we want to be less lenient? i.e.: raise PolymorphicError if reflection&.polymorphic?
435449
includes = Array(includes) unless includes.kind_of?(Enumerable)
436450
includes.each do |association, value|
437451
reflection = klass.reflect_on_association(association)
438452
if reflection && !reflection.polymorphic?
439453
scope = value ? scope.left_outer_joins(association => value) : scope.left_outer_joins(association)
440-
scope = scope.distinct if reflection.try(:collection?)
441454
end
442455
end
456+
# This adds a DISTINCT to safeguard "JOIN has_many" screwing up the LIMIT.
457+
scope = scope.distinct if !scope.distinct_value && is_collection?(klass, includes)
443458
scope
444459
end
445460

461+
def is_collection?(klass, includes)
462+
Array(includes).any? do |association, value|
463+
reflection = klass.reflect_on_association(association)
464+
reflection&.collection? || !reflection.polymorphic? && is_collection?(reflection.klass, value)
465+
end
466+
end
467+
446468
def filtered(objects, options = {})
447469
Rbac.search(options.reverse_merge(:targets => objects, :skip_counts => true)).first
448470
end
Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
RSpec.describe "ar_references" do
2-
describe ".includes_to_references" do
2+
describe ".prune_references" do
33
it "supports none" do
4-
expect(Vm.includes_to_references(nil)).to eq([])
5-
expect(Vm.includes_to_references([])).to eq([])
4+
expect(Vm.prune_references(nil)).to eq({})
5+
expect(Vm.prune_references([])).to eq({})
66
end
77

88
it "supports arrays" do
9-
expect(Vm.includes_to_references(%w[host operating_system])).to eq(%w[hosts operating_systems])
10-
expect(Vm.includes_to_references(%i[host])).to eq(%w[hosts])
9+
expect(Vm.prune_references(%w[host operating_system])).to eq(:host => {}, :operating_system => {})
10+
expect(Vm.prune_references(%i[host])).to eq(:host => {})
1111
end
1212

1313
it "supports hashes" do
14-
expect(Hardware.includes_to_references(:vm => {})).to eq(%w[vms])
15-
expect(Hardware.includes_to_references(:vm => :ext_management_system)).to eq(%w[vms ext_management_systems])
16-
expect(Hardware.includes_to_references(:vm => {:ext_management_system => {}})).to eq(%w[vms ext_management_systems])
14+
expect(Hardware.prune_references(:vm => {})).to eq(:vm => {})
15+
expect(Hardware.prune_references(:vm => :ext_management_system)).to eq(:vm => {:ext_management_system => {}})
16+
expect(Hardware.prune_references(:vm => {:ext_management_system => {}})).to eq(:vm => {:ext_management_system => {}})
1717
end
1818

19-
it "supports table array" do
20-
expect(Vm.includes_to_references(%w[hosts operating_systems])).to eq(%w[hosts operating_systems])
19+
it "skips tags" do
20+
expect(Vm.prune_references(:taggings => {})).to eq({})
21+
end
22+
23+
it "skips virtual has many" do
24+
expect(Vm.prune_references(:processes => {})).to eq({})
25+
end
26+
27+
it "skips virtual attributes" do
28+
expect(Vm.prune_references(:archived => {}, :platform => {})).to eq({})
29+
end
30+
31+
it "skips polymorphic references" do
32+
expect(MetricRollup.prune_references(:resource=>{})).to eq({})
33+
expect(MiqGroup.prune_references(:tenant => :source)).to eq({:tenant => {}})
34+
end
35+
36+
it "skips uses with a polymorphic reference" do
37+
expect(MetricRollup.prune_references(:v_derived_storage_used => {})).to eq({})
2138
end
2239
end
2340
end

0 commit comments

Comments
 (0)