activerecord-virtual_attributes 1.5.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -101,186 +101,76 @@ module ActiveRecord
101
101
  module Associations
102
102
  class Preloader
103
103
  prepend(Module.new {
104
- if ActiveRecord.version.to_s >= "6.0"
105
- def preloaders_for_reflection(reflection, records, scope, polymorphic_parent)
106
- case reflection
107
- when Array
108
- reflection.flat_map { |ref| preloaders_on(ref, records, scope, polymorphic_parent) }
109
- when Hash
110
- preloaders_on(reflection, records, scope, polymorphic_parent)
111
- else
112
- super(reflection, records, scope)
113
- end
114
- end
115
-
116
- # rubocop:disable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
117
- # preloader.rb active record 6.0
118
- # changed:
119
- # since grouped_records can return a hash/array, we need to handle those 2 new cases
120
- def preloaders_for_hash(association, records, scope, polymorphic_parent)
121
- association.flat_map { |parent, child|
122
- grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
123
- loaders = preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
124
- recs = loaders.flat_map(&:preloaded_records).uniq
125
- child_polymorphic_parent = reflection && reflection.respond_to?(:options) && reflection.options[:polymorphic]
126
- loaders.concat Array.wrap(child).flat_map { |assoc|
127
- preloaders_on assoc, recs, scope, child_polymorphic_parent
128
- }
129
- loaders
130
- end
131
- }
132
- end
133
-
134
- # preloader.rb active record 6.0
135
- # changed:
136
- # since grouped_records can return a hash/array, we need to handle those 2 new cases
137
- def preloaders_for_one(association, records, scope, polymorphic_parent)
138
- grouped_records(association, records, polymorphic_parent)
139
- .flat_map do |reflection, reflection_records|
140
- preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
141
- end
104
+ # preloader.rb active record 6.0
105
+ # changed:
106
+ # since grouped_records can return a hash/array, we need to handle those 2 new cases
107
+ def preloaders_for_reflection(reflection, records, scope, polymorphic_parent)
108
+ case reflection
109
+ when Array
110
+ reflection.flat_map { |ref| preloaders_on(ref, records, scope, polymorphic_parent) }
111
+ when Hash
112
+ preloaders_on(reflection, records, scope, polymorphic_parent)
113
+ else
114
+ super(reflection, records, scope)
142
115
  end
116
+ end
143
117
 
144
- # preloader.rb active record 6.0
145
- # changed:
146
- def grouped_records(orig_association, records, polymorphic_parent)
147
- h = {}
148
- records.each do |record|
149
- # each class can resolve virtual_{attributes,includes} differently
150
- association = record.class.replace_virtual_fields(orig_association)
151
- # 1 line optimization for single element array:
152
- association = association.first if association.kind_of?(Array) && association.size == 1
153
-
154
- case association
155
- when Symbol, String
156
- reflection = record.class._reflect_on_association(association)
157
- next if polymorphic_parent && !reflection || !record.association(association).klass
158
- when nil
159
- next
160
- else # need parent (preloaders_for_{hash,one}) to handle this Array/Hash
161
- reflection = association
162
- end
163
- (h[reflection] ||= []) << record
118
+ # rubocop:disable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
119
+ # preloader.rb active record 6.0
120
+ # changed:
121
+ # passing polymorphic around (and makes 5.2 more similar to 6.0)
122
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
123
+ association.flat_map { |parent, child|
124
+ grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
125
+ loaders = preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
126
+ recs = loaders.flat_map(&:preloaded_records).uniq
127
+ child_polymorphic_parent = reflection && reflection.respond_to?(:options) && reflection.options[:polymorphic]
128
+ loaders.concat Array.wrap(child).flat_map { |assoc|
129
+ preloaders_on assoc, recs, scope, child_polymorphic_parent
130
+ }
131
+ loaders
164
132
  end
165
- h
166
- end
167
- # rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
168
- else
169
- def preloaders_for_one(association, records, scope)
170
- klass_map = records.compact.group_by(&:class)
133
+ }
134
+ end
171
135
 
172
- # new logic: preload virtual fields / virtual includes
173
- loaders = klass_map.keys.group_by { |klass| klass.virtual_includes(association) }.flat_map do |virtuals, klasses|
174
- subset = klasses.flat_map { |klass| klass_map[klass] }
175
- preload(subset, virtuals)
136
+ # preloader.rb active record 6.0
137
+ # changed:
138
+ # passing polymorphic_parent to preloaders_for_reflection
139
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
140
+ grouped_records(association, records, polymorphic_parent)
141
+ .flat_map do |reflection, reflection_records|
142
+ preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
176
143
  end
177
- # /new logic
144
+ end
178
145
 
179
- records_with_association = klass_map.select { |k, _rs| k.reflect_on_association(association) }.flat_map { |_k, rs| rs }
180
- if records_with_association.any?
181
- loaders.concat(super(association, records_with_association, scope))
146
+ # preloader.rb active record 6.0, 6.1
147
+ def grouped_records(orig_association, records, polymorphic_parent)
148
+ h = {}
149
+ records.each do |record|
150
+ # The virtual_field lookup can return Symbol/Nil/Other (typically a Hash)
151
+ # so the case statement and the cases for Nil/Other are new
152
+
153
+ # each class can resolve virtual_{attributes,includes} differently
154
+ association = record.class.replace_virtual_fields(orig_association)
155
+ # 1 line optimization for single element array:
156
+ association = association.first if association.kind_of?(Array) && association.size == 1
157
+
158
+ case association
159
+ when Symbol, String
160
+ reflection = record.class._reflect_on_association(association)
161
+ next if polymorphic_parent && !reflection || !record.association(association).klass
162
+ when nil
163
+ next
164
+ else # need parent (preloaders_for_{hash,one}) to handle this Array/Hash
165
+ reflection = association
182
166
  end
183
-
184
- loaders
167
+ (h[reflection] ||= []) << record
185
168
  end
169
+ h
186
170
  end
171
+ # rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
187
172
  })
188
173
  end
189
-
190
- # FIXME: Hopefully we can get this into Rails core so this is no longer
191
- # required in our codebase, but the rule that are broken here are mostly
192
- # due to the style of the Rails codebase conflicting with our own.
193
- # Ignoring them to avoid noise in RuboCop, but allow us to keep the same
194
- # syntax from the original codebase.
195
- #
196
- # rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
197
- # rubocop:disable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
198
- class JoinDependency
199
- def instantiate(result_set, *_, &block)
200
- primary_key = aliases.column_alias(join_root, join_root.primary_key)
201
-
202
- seen = Hash.new { |i, object_id|
203
- i[object_id] = Hash.new { |j, child_class|
204
- j[child_class] = {}
205
- }
206
- }
207
-
208
- model_cache = Hash.new { |h,klass| h[klass] = {} }
209
- parents = model_cache[join_root]
210
- column_aliases = aliases.column_aliases(join_root)
211
-
212
- # New Code
213
- #
214
- # This monkey patches the ActiveRecord::Associations::JoinDependency to
215
- # include columns into the main record that might have been added
216
- # through a `select` clause.
217
- #
218
- # This can be seen with the following:
219
- #
220
- # Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
221
- # .includes(:tags => {}).references(:tags)
222
- #
223
- # Which will produce a SQL SELECT statement kind of like this:
224
- #
225
- # SELECT "vms".*,
226
- # (<virtual_attribute_arel>) AS some_vm_virtual_col,
227
- # "vms"."id" AS t0_r0
228
- # "vms"."vendor" AS t0_r1
229
- # "vms"."format" AS t0_r1
230
- # "vms"."version" AS t0_r1
231
- # ...
232
- # "tags"."id" AS t1_r0
233
- # "tags"."name" AS t1_r1
234
- #
235
- # This is because rails is trying to reduce the number of queries
236
- # needed to fetch all of the records in the include, so it grabs the
237
- # columns for both of the tables together to do it. Unfortuantely (or
238
- # fortunately... depending on how you look at it), it does not remove
239
- # any `.select` columns from the query that is run in the process, so
240
- # that is brought along for the ride, but never used when this method
241
- # instanciates the objects.
242
- #
243
- # The "New Code" here simply also instanciates any extra rows that
244
- # might have been included in the select (virtual_columns) as well and
245
- # brought back with the result set.
246
- unless result_set.empty?
247
- join_dep_keys = aliases.columns.map(&:right)
248
- join_root_aliases = column_aliases.map(&:first)
249
- additional_attributes = result_set.first.keys
250
- .reject { |k| join_dep_keys.include?(k) }
251
- .reject { |k| join_root_aliases.include?(k) }
252
- column_aliases += if ActiveRecord.version.to_s >= "6.0"
253
- additional_attributes.map { |k| Aliases::Column.new(k, k) }
254
- else
255
- additional_attributes.map { |k| [k, k] }
256
- end
257
- end
258
- # End of New Code
259
-
260
- message_bus = ActiveSupport::Notifications.instrumenter
261
-
262
- payload = {
263
- record_count: result_set.length,
264
- class_name: join_root.base_klass.name
265
- }
266
-
267
- message_bus.instrument('instantiation.active_record', payload) do
268
- result_set.each { |row_hash|
269
- parent_key = primary_key ? row_hash[primary_key] : row_hash
270
- parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
271
- if ActiveRecord.version.to_s < "6.0"
272
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
273
- else
274
- construct(parent, join_root, row_hash, seen, model_cache)
275
- end
276
- }
277
- end
278
-
279
- parents.values
280
- end
281
- # rubocop:enable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
282
- # rubocop:enable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
283
- end
284
174
  end
285
175
 
286
176
  class Relation
@@ -295,99 +185,50 @@ module ActiveRecord
295
185
 
296
186
  include(Module.new {
297
187
  # From ActiveRecord::FinderMethods
298
- if ActiveRecord.version.to_s >= "5.2"
299
- def apply_join_dependency(*args, &block)
300
- real = without_virtual_includes
301
- if real.equal?(self)
302
- super
303
- else
304
- real.apply_join_dependency(*args, &block)
305
- end
306
- end
307
- else
308
- def find_with_associations(&block)
309
- real = without_virtual_includes
310
- if real.equal?(self)
311
- super
312
- else
313
- real.find_with_associations(&block)
314
- end
188
+ def apply_join_dependency(*args, **kargs, &block)
189
+ real = without_virtual_includes
190
+ if real.equal?(self)
191
+ super
192
+ else
193
+ real.apply_join_dependency(*args, **kargs, &block)
315
194
  end
316
195
  end
317
196
 
318
- # From ActiveRecord::QueryMethods (rails 5.2 - 6.0)
197
+ # From ActiveRecord::QueryMethods (rails 5.2 - 6.1)
319
198
  def build_select(arel)
320
199
  if select_values.any?
321
- arel.project(*arel_columns(select_values.uniq, true))
322
- elsif klass.ignored_columns.any?
323
- arel.project(*klass.column_names.map { |field| arel_attribute(field) })
324
- else
325
- arel.project(table[Arel.star])
326
- end
327
- end
328
-
329
- # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
330
- def arel_columns(columns, allow_alias = false)
331
- columns.flat_map do |field|
332
- case field
333
- when Symbol
334
- arel_column(field.to_s, allow_alias) do |attr_name|
335
- connection.quote_table_name(attr_name)
200
+ cols = arel_columns(select_values.uniq).map do |col|
201
+ # if it is a virtual attribute, then add aliases to those columns
202
+ if col.kind_of?(Arel::Nodes::Grouping) && col.name
203
+ col.as(connection.quote_column_name(col.name))
204
+ else
205
+ col
336
206
  end
337
- when String
338
- arel_column(field, allow_alias, &:itself)
339
- when Proc
340
- field.call
341
- else
342
- field
343
207
  end
344
- end
345
- end
346
-
347
- # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
348
- def arel_column(field, allow_alias = false, &block)
349
- field = klass.attribute_aliases[field] || field
350
- from = from_clause.name || from_clause.value
351
-
352
- if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
353
- arel_attribute(field)
354
- elsif virtual_attribute?(field)
355
- virtual_attribute_arel_column(field, allow_alias, &block)
208
+ arel.project(*cols)
356
209
  else
357
- yield field
210
+ super
358
211
  end
359
212
  end
360
213
 
361
- def virtual_attribute_arel_column(field, allow_alias)
362
- arel = arel_attribute(field)
363
- if arel.nil?
364
- yield field
365
- elsif allow_alias && arel && arel.respond_to?(:as) && !arel.kind_of?(Arel::Nodes::As) && !arel.try(:alias)
366
- arel.as(connection.quote_column_name(field.to_s))
367
- else
214
+ # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
215
+ # TODO: remove from rails 7.0
216
+ def arel_column(field, &block)
217
+ if virtual_attribute?(field) && (arel = table[field])
368
218
  arel
219
+ else
220
+ super
369
221
  end
370
222
  end
371
223
 
372
- # From ActiveRecord::QueryMethods
373
- def table_name_matches?(from)
374
- /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
375
- end
376
-
377
- # From ActiveRecord::QueryMethods
378
- def build_left_outer_joins(manager, outer_joins, *rest)
379
- outer_joins = klass.replace_virtual_fields(outer_joins)
380
- super if outer_joins.present?
224
+ def construct_join_dependency(associations, join_type) # :nodoc:
225
+ associations = klass.replace_virtual_fields(associations)
226
+ super
381
227
  end
382
228
 
383
229
  # From ActiveRecord::Calculations
230
+ # introduces virtual includes support for calculate (we mostly use COUNT(*))
384
231
  def calculate(operation, attribute_name)
385
- if ActiveRecord.version.to_s < "5.1"
386
- if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
387
- attribute_name = arel
388
- end
389
- end
390
-
391
232
  # allow calculate to work with includes and a virtual attribute
392
233
  real = without_virtual_includes
393
234
  return super if real.equal?(self)
@@ -5,7 +5,7 @@ module VirtualAttributes
5
5
  module ClassMethods
6
6
  private
7
7
 
8
- # define an attribute to calculating the total of a has many relationship
8
+ # define an attribute to calculate the total of a has many relationship
9
9
  #
10
10
  # example:
11
11
  #
@@ -25,24 +25,45 @@ module VirtualAttributes
25
25
  # # arel == (SELECT COUNT(*) FROM vms where ems.id = vms.ems_id)
26
26
  #
27
27
  def virtual_total(name, relation, options = {})
28
- define_virtual_size_method(name, relation)
29
28
  define_virtual_aggregate_attribute(name, relation, :count, Arel.star, options)
29
+ define_method(name) { (has_attribute?(name) ? self[name] : send(relation).try(:size)) || 0 }
30
30
  end
31
31
 
32
- # define an attribute to calculate the sum of a has may relationship
32
+ def virtual_sum(name, relation, column, options = {})
33
+ define_virtual_aggregate_attribute(name, relation, :sum, column, options)
34
+ define_virtual_aggregate_method(name, relation, column, :sum)
35
+ end
36
+
37
+ def virtual_minimum(name, relation, column, options = {})
38
+ define_virtual_aggregate_attribute(name, relation, :minimum, column, options)
39
+ define_virtual_aggregate_method(name, relation, column, :min, :minimum)
40
+ end
41
+
42
+ def virtual_maximum(name, relation, column, options = {})
43
+ define_virtual_aggregate_attribute(name, relation, :maximum, column, options)
44
+ define_virtual_aggregate_method(name, relation, column, :max, :maximum)
45
+ end
46
+
47
+ def virtual_average(name, relation, column, options = {})
48
+ define_virtual_aggregate_attribute(name, relation, :average, column, options)
49
+ define_virtual_aggregate_method(name, relation, column, :average) { |values| values.count == 0 ? 0 : values.sum / values.count }
50
+ end
51
+
52
+ # @param method_name
53
+ # :count :average :minimum :maximum :sum
33
54
  #
34
55
  # example:
35
56
  #
36
57
  # class Hardware
37
58
  # has_many :disks
38
- # virtual_aggregate :allocated_disk_storage, :disks, :sum, :size
59
+ # virtual_sum :allocated_disk_storage, :disks, :size
39
60
  # end
40
61
  #
41
62
  # generates:
42
63
  #
43
64
  # def allocated_disk_storage
44
65
  # if disks.loaded?
45
- # disks.blank? ? nil : disks.map(&:size).compact.sum
66
+ # disks.map(&:size).compact.sum
46
67
  # else
47
68
  # disks.sum(:size) || 0
48
69
  # end
@@ -53,10 +74,8 @@ module VirtualAttributes
53
74
  # # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
54
75
 
55
76
  def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
56
- return define_virtual_total(name, relation, options) if method_name == :size
57
-
58
- define_virtual_aggregate_method(name, relation, method_name, column)
59
- define_virtual_aggregate_attribute(name, relation, method_name, column, options)
77
+ return virtual_total(name, relation, options) if method_name == :size
78
+ return virtual_sum(name, relation, column, options) if method_name == :sum
60
79
  end
61
80
 
62
81
  def define_virtual_aggregate_attribute(name, relation, method_name, column, options)
@@ -77,20 +96,19 @@ module VirtualAttributes
77
96
  end
78
97
  end
79
98
 
80
- def define_virtual_size_method(name, relation)
81
- define_method(name) do
82
- (has_attribute?(name) ? self[name] : send(relation).try(:size)) || 0
83
- end
84
- end
85
-
86
- def define_virtual_aggregate_method(name, relation, method_name, column)
99
+ def define_virtual_aggregate_method(name, relation, column, ruby_method_name, arel_method_name = ruby_method_name)
87
100
  define_method(name) do
88
- if attribute_present?(name)
89
- self[name]
101
+ if has_attribute?(name)
102
+ self[name] || 0
90
103
  elsif (rel = send(relation)).loaded?
91
- rel.blank? ? nil : rel.map { |t| t.send(column) }.compact.send(method_name)
104
+ values = rel.map { |t| t.send(column) }.compact
105
+ if block_given?
106
+ yield values
107
+ else
108
+ values.blank? ? nil : values.send(ruby_method_name)
109
+ end
92
110
  else
93
- rel.try(method_name, column) || 0
111
+ rel.try(arel_method_name, column) || 0
94
112
  end
95
113
  end
96
114
  end
@@ -101,7 +119,7 @@ module VirtualAttributes
101
119
  # need db access for the reflection join_keys, so delaying all this key lookup until call time
102
120
  lambda do |t|
103
121
  # strings and symbols are converted across, arel objects are not
104
- column = reflection.klass.arel_attribute(column) unless column.respond_to?(:count)
122
+ column = reflection.klass.arel_table[column] unless column.respond_to?(:count)
105
123
 
106
124
  # query: SELECT COUNT(*) FROM main_table JOIN foreign_table ON main_table.id = foreign_table.id JOIN ...
107
125
  relation_query = joins(reflection.name).select(column.send(method_name))
@@ -121,18 +139,16 @@ module VirtualAttributes
121
139
 
122
140
  # convert bind variables from ? to actual values. otherwise, sql is incomplete
123
141
  conn = connection
124
- sql = if ActiveRecord.version.to_s >= "5.2"
125
- conn.unprepared_statement { conn.to_sql(query) }
126
- else
127
- conn.unprepared_statement { conn.to_sql(query, relation_query.bound_attributes) }
128
- end
142
+ sql = conn.unprepared_statement { conn.to_sql(query) }
129
143
 
130
144
  # add () around query
131
- t.grouping(Arel::Nodes::SqlLiteral.new(sql))
145
+ query = t.grouping(Arel::Nodes::SqlLiteral.new(sql))
146
+ # add coalesce to ensure correct value comes out
147
+ t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [query, Arel::Nodes::SqlLiteral.new("0")]))
132
148
  end
133
149
  end
134
150
  end
135
151
  end
136
152
  end
137
153
 
138
- ActiveRecord::Base.send(:include, VirtualAttributes::VirtualTotal)
154
+ ActiveRecord::Base.include VirtualAttributes::VirtualTotal
@@ -48,13 +48,9 @@ module ActiveRecord
48
48
  #
49
49
 
50
50
  # Compatibility method: `virtual_attribute` is a more accurate name
51
- def virtual_column(name, type_or_options, **options)
52
- if type_or_options.kind_of?(Hash)
53
- options = options.merge(type_or_options)
54
- type = options.delete(:type)
55
- else
56
- type = type_or_options
57
- end
51
+ def virtual_column(name, **options)
52
+ type = options.delete(:type)
53
+ raise ArgumentError, "missing :type attribute" unless type
58
54
 
59
55
  virtual_attribute(name, type, **options)
60
56
  end
@@ -90,14 +86,7 @@ module ActiveRecord
90
86
  def attributes_builder # :nodoc:
91
87
  unless defined?(@attributes_builder) && @attributes_builder
92
88
  defaults = _default_attributes.except(*(column_names - [primary_key]))
93
- # change necessary for rails 5.0 and 5.1 - (changed/introduced in https://github.com/rails/rails/pull/31894)
94
- defaults = defaults.except(*virtual_attribute_names)
95
- # end change
96
- @attributes_builder = if ActiveRecord.version.to_s >= "5.2"
97
- ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
98
- else
99
- ActiveRecord::AttributeSet::Builder.new(attribute_types, defaults)
100
- end
89
+ @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
101
90
  end
102
91
  @attributes_builder
103
92
  end
@@ -134,22 +123,7 @@ require "active_record/virtual_attributes/virtual_fields"
134
123
  # Class extensions
135
124
  #
136
125
 
137
- # this patch is no longer necessary for 5.2
138
- if ActiveRecord.version.to_s < "5.2"
139
- require "active_record/attribute"
140
- module ActiveRecord
141
- # This is a bug in rails 5.0 and 5.1, but it is made much worse by virtual attributes
142
- class Attribute
143
- def with_value_from_database(value)
144
- # self.class.from_database(name, value, type)
145
- initialized? ? self.class.from_database(name, value, type) : self
146
- end
147
- end
148
- end
149
- end
150
-
151
126
  require "active_record/virtual_attributes/virtual_total"
152
- require "active_record/virtual_attributes/arel_groups"
153
127
 
154
128
  # legacy support for sql types
155
129
  module VirtualAttributes