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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +8 -6
- data/.github/workflows/ci.yaml +74 -0
- data/.rubocop.yml +3 -2
- data/.rubocop_cc.yml +3 -3
- data/.whitesource +3 -0
- data/CHANGELOG.md +49 -13
- data/Gemfile +3 -3
- data/README.md +10 -9
- 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 +61 -10
- data/lib/active_record/virtual_attributes/virtual_delegates.rb +19 -14
- data/lib/active_record/virtual_attributes/virtual_fields.rb +84 -243
- data/lib/active_record/virtual_attributes/virtual_total.rb +44 -28
- data/lib/active_record/virtual_attributes.rb +4 -30
- metadata +31 -23
- data/.travis.yml +0 -38
- 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 -14
@@ -101,186 +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
|
-
# 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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
166
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
144
|
+
end
|
178
145
|
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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.
|
197
|
+
# From ActiveRecord::QueryMethods (rails 5.2 - 6.1)
|
319
198
|
def build_select(arel)
|
320
199
|
if select_values.any?
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
-
|
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
|
-
|
210
|
+
super
|
358
211
|
end
|
359
212
|
end
|
360
213
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
#
|
373
|
-
|
374
|
-
|
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
|
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
|
@@ -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,
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|