activerecord-virtual_attributes 2.0.0 → 6.1.1
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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +8 -6
- data/.github/workflows/ci.yaml +69 -0
- data/.rubocop.yml +3 -2
- data/.rubocop_cc.yml +3 -3
- data/.whitesource +3 -0
- data/CHANGELOG.md +39 -16
- data/Gemfile +3 -3
- data/README.md +12 -10
- data/activerecord-virtual_attributes.gemspec +5 -4
- data/lib/active_record/virtual_attributes/version.rb +1 -1
- data/lib/active_record/virtual_attributes/virtual_arel.rb +118 -13
- data/lib/active_record/virtual_attributes/virtual_delegates.rb +19 -14
- data/lib/active_record/virtual_attributes/virtual_fields.rb +83 -298
- data/lib/active_record/virtual_attributes/virtual_total.rb +44 -28
- data/lib/active_record/virtual_attributes.rb +1 -23
- metadata +31 -23
- data/.travis.yml +0 -30
- data/Appraisals +0 -23
- data/gemfiles/gemfile_50.gemfile +0 -10
- data/gemfiles/gemfile_51.gemfile +0 -10
- data/gemfiles/gemfile_52.gemfile +0 -10
- data/gemfiles/gemfile_60.gemfile +0 -10
- data/lib/active_record/virtual_attributes/arel_groups.rb +0 -15
@@ -101,242 +101,76 @@ module ActiveRecord
|
|
101
101
|
module Associations
|
102
102
|
class Preloader
|
103
103
|
prepend(Module.new {
|
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
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
# preloader.rb active record 6.0
|
137
|
-
# since this deals with polymorphic_parent, it makes everything easier to just define it
|
138
|
-
def preloaders_on(association, records, scope, polymorphic_parent = false)
|
139
|
-
case association
|
140
|
-
when Hash
|
141
|
-
preloaders_for_hash(association, records, scope, polymorphic_parent)
|
142
|
-
when Symbol, String
|
143
|
-
preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
|
144
|
-
else
|
145
|
-
raise ArgumentError, "#{association.inspect} was not recognized for preload"
|
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)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
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
|
146
132
|
end
|
147
|
-
|
133
|
+
}
|
148
134
|
end
|
149
135
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
|
158
|
-
loaders = preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
|
159
|
-
recs = loaders.flat_map(&:preloaded_records).uniq
|
160
|
-
child_polymorphic_parent = reflection && reflection.respond_to?(:options) && reflection.options[:polymorphic]
|
161
|
-
loaders.concat Array.wrap(child).flat_map { |assoc|
|
162
|
-
preloaders_on assoc, recs, scope, child_polymorphic_parent
|
163
|
-
}
|
164
|
-
loaders
|
165
|
-
end
|
166
|
-
}
|
167
|
-
end
|
168
|
-
|
169
|
-
# preloader.rb active record 6.0
|
170
|
-
# changed:
|
171
|
-
# passing polymorphic_parent to preloaders_for_reflection
|
172
|
-
def preloaders_for_one(association, records, scope, polymorphic_parent)
|
173
|
-
grouped_records(association, records, polymorphic_parent)
|
174
|
-
.flat_map do |reflection, reflection_records|
|
175
|
-
preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# preloader.rb active record 6.0
|
180
|
-
# changed:
|
181
|
-
# different from 5.2. But not called outside these redefined methods here, so it works fine
|
182
|
-
# did add compact to fix a 5.2 double preload nil bug
|
183
|
-
def grouped_records(orig_association, records, polymorphic_parent)
|
184
|
-
h = {}
|
185
|
-
records.compact.each do |record|
|
186
|
-
# each class can resolve virtual_{attributes,includes} differently
|
187
|
-
association = record.class.replace_virtual_fields(orig_association)
|
188
|
-
# 1 line optimization for single element array:
|
189
|
-
association = association.first if association.kind_of?(Array) && association.size == 1
|
190
|
-
|
191
|
-
case association
|
192
|
-
when Symbol, String
|
193
|
-
# 4/24/20 we want to revert #67 once we handle all these error cases in our codebase.
|
194
|
-
reflection = record.class._reflect_on_association(association)
|
195
|
-
display_virtual_attribute_deprecation("#{record.class.name}.#{association} does not exist") if !reflection && !polymorphic_parent
|
196
|
-
next if !reflection || !record.association(association).klass
|
197
|
-
when nil
|
198
|
-
next
|
199
|
-
else # need parent (preloaders_for_{hash,one}) to handle this Array/Hash
|
200
|
-
reflection = association
|
201
|
-
end
|
202
|
-
(h[reflection] ||= []) << record
|
203
|
-
end
|
204
|
-
h
|
205
|
-
end
|
206
|
-
# rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
|
207
|
-
|
208
|
-
def display_virtual_attribute_deprecation(str)
|
209
|
-
short_caller = caller
|
210
|
-
# if debugging is turned on, don't prune the backtrace.
|
211
|
-
# if debugging is off, prune down to the line where the sql is executed
|
212
|
-
# this defaults to false and only displays 1 line number.
|
213
|
-
unless ActiveSupport::Deprecation.debug
|
214
|
-
bc = ActiveSupport::BacktraceCleaner.new
|
215
|
-
bc.add_silencer { |line| line =~ /virtual_fields/ }
|
216
|
-
bc.add_silencer { |line| line =~ /active_record/ }
|
217
|
-
short_caller = bc.clean(caller)
|
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)
|
218
143
|
end
|
144
|
+
end
|
219
145
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
225
152
|
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
end
|
231
|
-
# /new logic
|
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
|
232
157
|
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
236
166
|
end
|
237
|
-
|
238
|
-
loaders
|
167
|
+
(h[reflection] ||= []) << record
|
239
168
|
end
|
169
|
+
h
|
240
170
|
end
|
171
|
+
# rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
|
241
172
|
})
|
242
173
|
end
|
243
|
-
|
244
|
-
# FIXME: Hopefully we can get this into Rails core so this is no longer
|
245
|
-
# required in our codebase, but the rule that are broken here are mostly
|
246
|
-
# due to the style of the Rails codebase conflicting with our own.
|
247
|
-
# Ignoring them to avoid noise in RuboCop, but allow us to keep the same
|
248
|
-
# syntax from the original codebase.
|
249
|
-
#
|
250
|
-
# rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
|
251
|
-
# rubocop:disable Layout/AlignHash
|
252
|
-
class JoinDependency
|
253
|
-
def instantiate(result_set, *_, &block)
|
254
|
-
primary_key = aliases.column_alias(join_root, join_root.primary_key)
|
255
|
-
|
256
|
-
seen = Hash.new { |i, object_id|
|
257
|
-
i[object_id] = Hash.new { |j, child_class|
|
258
|
-
j[child_class] = {}
|
259
|
-
}
|
260
|
-
}
|
261
|
-
|
262
|
-
model_cache = Hash.new { |h,klass| h[klass] = {} }
|
263
|
-
parents = model_cache[join_root]
|
264
|
-
column_aliases = aliases.column_aliases(join_root)
|
265
|
-
|
266
|
-
# New Code
|
267
|
-
column_aliases += select_values_from_references(column_aliases, result_set) if result_set.present?
|
268
|
-
# End of New Code
|
269
|
-
|
270
|
-
message_bus = ActiveSupport::Notifications.instrumenter
|
271
|
-
|
272
|
-
payload = {
|
273
|
-
record_count: result_set.length,
|
274
|
-
class_name: join_root.base_klass.name
|
275
|
-
}
|
276
|
-
|
277
|
-
message_bus.instrument('instantiation.active_record', payload) do
|
278
|
-
result_set.each { |row_hash|
|
279
|
-
parent_key = primary_key ? row_hash[primary_key] : row_hash
|
280
|
-
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
|
281
|
-
if ActiveRecord.version.to_s < "6.0"
|
282
|
-
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
|
283
|
-
else
|
284
|
-
construct(parent, join_root, row_hash, seen, model_cache)
|
285
|
-
end
|
286
|
-
}
|
287
|
-
end
|
288
|
-
|
289
|
-
parents.values
|
290
|
-
end
|
291
|
-
# rubocop:enable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
|
292
|
-
# rubocop:enable Layout/AlignHash
|
293
|
-
|
294
|
-
#
|
295
|
-
# This monkey patches the ActiveRecord::Associations::JoinDependency to
|
296
|
-
# include columns into the main record that might have been added
|
297
|
-
# through a `select` clause.
|
298
|
-
#
|
299
|
-
# This can be seen with the following:
|
300
|
-
#
|
301
|
-
# Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
|
302
|
-
# .includes(:tags => {}).references(:tags)
|
303
|
-
#
|
304
|
-
# Which will produce a SQL SELECT statement kind of like this:
|
305
|
-
#
|
306
|
-
# SELECT "vms".*,
|
307
|
-
# (<virtual_attribute_arel>) AS some_vm_virtual_col,
|
308
|
-
# "vms"."id" AS t0_r0
|
309
|
-
# "vms"."vendor" AS t0_r1
|
310
|
-
# "vms"."format" AS t0_r1
|
311
|
-
# "vms"."version" AS t0_r1
|
312
|
-
# ...
|
313
|
-
# "tags"."id" AS t1_r0
|
314
|
-
# "tags"."name" AS t1_r1
|
315
|
-
#
|
316
|
-
# This is because rails is trying to reduce the number of queries
|
317
|
-
# needed to fetch all of the records in the include, so it grabs the
|
318
|
-
# columns for both of the tables together to do it. Unfortunately (or
|
319
|
-
# fortunately... depending on how you look at it), it does not remove
|
320
|
-
# any `.select` columns from the query that is run in the process, so
|
321
|
-
# that is brought along for the ride, but never used when this method
|
322
|
-
# instanciates the objects.
|
323
|
-
#
|
324
|
-
# The "New Code" here simply also instanciates any extra rows that
|
325
|
-
# might have been included in the select (virtual_columns) as well and
|
326
|
-
# brought back with the result set.
|
327
|
-
def select_values_from_references(column_aliases, result_set)
|
328
|
-
join_dep_keys = aliases.columns.map(&:right)
|
329
|
-
join_root_aliases = column_aliases.map(&:first)
|
330
|
-
additional_attributes = result_set.first.keys
|
331
|
-
.reject { |k| join_dep_keys.include?(k) }
|
332
|
-
.reject { |k| join_root_aliases.include?(k) }
|
333
|
-
if ActiveRecord.version.to_s >= "6.0"
|
334
|
-
additional_attributes.map { |k| Aliases::Column.new(k, k) }
|
335
|
-
else
|
336
|
-
additional_attributes.map { |k| [k, k] }
|
337
|
-
end
|
338
|
-
end
|
339
|
-
end
|
340
174
|
end
|
341
175
|
|
342
176
|
class Relation
|
@@ -351,99 +185,50 @@ module ActiveRecord
|
|
351
185
|
|
352
186
|
include(Module.new {
|
353
187
|
# From ActiveRecord::FinderMethods
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
real.apply_join_dependency(*args, &block)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
else
|
364
|
-
def find_with_associations(&block)
|
365
|
-
real = without_virtual_includes
|
366
|
-
if real.equal?(self)
|
367
|
-
super
|
368
|
-
else
|
369
|
-
real.find_with_associations(&block)
|
370
|
-
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)
|
371
194
|
end
|
372
195
|
end
|
373
196
|
|
374
|
-
# From ActiveRecord::QueryMethods (rails 5.2 - 6.
|
197
|
+
# From ActiveRecord::QueryMethods (rails 5.2 - 6.1)
|
375
198
|
def build_select(arel)
|
376
199
|
if select_values.any?
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
end
|
384
|
-
|
385
|
-
# from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
|
386
|
-
def arel_columns(columns, allow_alias = false)
|
387
|
-
columns.flat_map do |field|
|
388
|
-
case field
|
389
|
-
when Symbol
|
390
|
-
arel_column(field.to_s, allow_alias) do |attr_name|
|
391
|
-
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
|
392
206
|
end
|
393
|
-
when String
|
394
|
-
arel_column(field, allow_alias, &:itself)
|
395
|
-
when Proc
|
396
|
-
field.call
|
397
|
-
else
|
398
|
-
field
|
399
207
|
end
|
400
|
-
|
401
|
-
end
|
402
|
-
|
403
|
-
# from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
|
404
|
-
def arel_column(field, allow_alias = false, &block)
|
405
|
-
field = klass.attribute_aliases[field] || field
|
406
|
-
from = from_clause.name || from_clause.value
|
407
|
-
|
408
|
-
if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
|
409
|
-
arel_attribute(field)
|
410
|
-
elsif virtual_attribute?(field)
|
411
|
-
virtual_attribute_arel_column(field, allow_alias, &block)
|
208
|
+
arel.project(*cols)
|
412
209
|
else
|
413
|
-
|
210
|
+
super
|
414
211
|
end
|
415
212
|
end
|
416
213
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
elsif allow_alias && arel && arel.respond_to?(:as) && !arel.kind_of?(Arel::Nodes::As) && !arel.try(:alias)
|
422
|
-
arel.as(connection.quote_column_name(field.to_s))
|
423
|
-
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])
|
424
218
|
arel
|
219
|
+
else
|
220
|
+
super
|
425
221
|
end
|
426
222
|
end
|
427
223
|
|
428
|
-
#
|
429
|
-
|
430
|
-
|
431
|
-
end
|
432
|
-
|
433
|
-
# From ActiveRecord::QueryMethods
|
434
|
-
def build_left_outer_joins(manager, outer_joins, *rest)
|
435
|
-
outer_joins = klass.replace_virtual_fields(outer_joins)
|
436
|
-
super if outer_joins.present?
|
224
|
+
def construct_join_dependency(associations, join_type) # :nodoc:
|
225
|
+
associations = klass.replace_virtual_fields(associations)
|
226
|
+
super
|
437
227
|
end
|
438
228
|
|
439
229
|
# From ActiveRecord::Calculations
|
230
|
+
# introduces virtual includes support for calculate (we mostly use COUNT(*))
|
440
231
|
def calculate(operation, attribute_name)
|
441
|
-
if ActiveRecord.version.to_s < "5.1"
|
442
|
-
if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
|
443
|
-
attribute_name = arel
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
232
|
# allow calculate to work with includes and a virtual attribute
|
448
233
|
real = without_virtual_includes
|
449
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
|
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
|
-
|
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
|
-
#
|
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.
|
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
|
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
|
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
|
89
|
-
self[name]
|
101
|
+
if has_attribute?(name)
|
102
|
+
self[name] || 0
|
90
103
|
elsif (rel = send(relation)).loaded?
|
91
|
-
|
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(
|
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.
|
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
|
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.
|
154
|
+
ActiveRecord::Base.include VirtualAttributes::VirtualTotal
|
@@ -86,14 +86,7 @@ module ActiveRecord
|
|
86
86
|
def attributes_builder # :nodoc:
|
87
87
|
unless defined?(@attributes_builder) && @attributes_builder
|
88
88
|
defaults = _default_attributes.except(*(column_names - [primary_key]))
|
89
|
-
|
90
|
-
defaults = defaults.except(*virtual_attribute_names)
|
91
|
-
# end change
|
92
|
-
@attributes_builder = if ActiveRecord.version.to_s >= "5.2"
|
93
|
-
ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
|
94
|
-
else
|
95
|
-
ActiveRecord::AttributeSet::Builder.new(attribute_types, defaults)
|
96
|
-
end
|
89
|
+
@attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
|
97
90
|
end
|
98
91
|
@attributes_builder
|
99
92
|
end
|
@@ -130,22 +123,7 @@ require "active_record/virtual_attributes/virtual_fields"
|
|
130
123
|
# Class extensions
|
131
124
|
#
|
132
125
|
|
133
|
-
# this patch is no longer necessary for 5.2
|
134
|
-
if ActiveRecord.version.to_s < "5.2"
|
135
|
-
require "active_record/attribute"
|
136
|
-
module ActiveRecord
|
137
|
-
# This is a bug in rails 5.0 and 5.1, but it is made much worse by virtual attributes
|
138
|
-
class Attribute
|
139
|
-
def with_value_from_database(value)
|
140
|
-
# self.class.from_database(name, value, type)
|
141
|
-
initialized? ? self.class.from_database(name, value, type) : self
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
126
|
require "active_record/virtual_attributes/virtual_total"
|
148
|
-
require "active_record/virtual_attributes/arel_groups"
|
149
127
|
|
150
128
|
# legacy support for sql types
|
151
129
|
module VirtualAttributes
|