activerecord-virtual_attributes 1.5.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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