activerecord-virtual_attributes 1.4.0 → 1.5.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/.travis.yml +18 -8
- data/Appraisals +11 -6
- data/CHANGELOG.md +11 -1
- data/gemfiles/{virtual_attributes_50.gemfile → gemfile_50.gemfile} +1 -1
- data/gemfiles/gemfile_51.gemfile +10 -0
- data/gemfiles/{virtual_attributes_52.gemfile → gemfile_52.gemfile} +1 -1
- data/gemfiles/{virtual_attributes_51.gemfile → gemfile_60.gemfile} +2 -2
- data/lib/active_record/virtual_attributes.rb +5 -1
- data/lib/active_record/virtual_attributes/rspec/have_virtual_attribute.rb +1 -5
- data/lib/active_record/virtual_attributes/version.rb +1 -1
- data/lib/active_record/virtual_attributes/virtual_fields.rb +170 -45
- data/lib/active_record/virtual_attributes/virtual_total.rb +60 -48
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90da25c212d7507da2ca2789b1151b2e7cb7cb3936411a7e92823518299141b9
|
4
|
+
data.tar.gz: d2a3cc1662d29fa8625dab40610950a573c0014b9739dbb260184da47afdb2ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78c1edd3d1a17f5f4fcd147315e37c788cdfc3add3137a4190d568cc93ab1823d9de0e46a6f57f266d248104823578d5f6a86e076228ae640c52c035922910f4
|
7
|
+
data.tar.gz: 19ec2fafe3553b4ff809c1aae1aa6ccfd6f303216d1c35bc4a09f30e9dda2876a48fd531d9216e0a1a566de0434c90edd58936c12027b62bc9c70eb896235b50
|
data/.travis.yml
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
---
|
2
|
+
sudo: false
|
2
3
|
language: ruby
|
3
4
|
cache: bundler
|
4
5
|
rvm:
|
5
|
-
- 2.
|
6
|
-
- 2.
|
6
|
+
- 2.5.6
|
7
|
+
- 2.6.4
|
7
8
|
services:
|
8
|
-
- postgresql
|
9
9
|
- mysql
|
10
|
+
- postgresql
|
10
11
|
env:
|
11
|
-
- DB=sqlite3
|
12
|
-
- DB=pg
|
13
12
|
- DB=mysql2
|
13
|
+
- DB=pg
|
14
|
+
- DB=sqlite3
|
14
15
|
gemfile:
|
15
|
-
- gemfiles/
|
16
|
-
- gemfiles/
|
17
|
-
|
16
|
+
- gemfiles/gemfile_50.gemfile
|
17
|
+
- gemfiles/gemfile_51.gemfile
|
18
|
+
- gemfiles/gemfile_52.gemfile
|
19
|
+
- gemfiles/gemfile_60.gemfile
|
18
20
|
before_install:
|
19
21
|
- 'echo ''gem: --no-ri --no-rdoc --no-document'' > ~/.gemrc'
|
20
22
|
before_script:
|
@@ -26,3 +28,11 @@ before_script:
|
|
26
28
|
- sh -c "if [ '$DB' = 'mysql2' ]; then mysql -e 'DROP DATABASE IF EXISTS virtual_attributes; CREATE DATABASE virtual_attributes;'; fi"
|
27
29
|
after_script:
|
28
30
|
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
31
|
+
jobs:
|
32
|
+
allow_failures:
|
33
|
+
- gemfile: gemfiles/gemfile_52.gemfile
|
34
|
+
env: DB=mysql2
|
35
|
+
- gemfile: gemfiles/gemfile_52.gemfile
|
36
|
+
env: DB=pg
|
37
|
+
- gemfile: gemfiles/gemfile_52.gemfile
|
38
|
+
env: DB=sqlite3
|
data/Appraisals
CHANGED
@@ -1,18 +1,23 @@
|
|
1
|
-
%w(5.0.7 5.1.
|
2
|
-
|
3
|
-
appraise "#{db_gem}-#{ar_version.split('.').first(2).join}" do
|
1
|
+
%w(5.0.7 5.1.7 5.2.3 6.0.0).each do |ar_version|
|
2
|
+
appraise "gemfile-#{ar_version.split('.').first(2).join}" do
|
4
3
|
gem "activerecord", "~> #{ar_version}"
|
5
4
|
|
6
|
-
gem "pg"
|
7
5
|
if ar_version >= "5.0"
|
8
6
|
gem "mysql2"
|
7
|
+
elsif ar_version >= "4.2"
|
8
|
+
gem "mysql2", "~> 0.4.0"
|
9
|
+
end
|
10
|
+
|
11
|
+
if ar_version >= "5.0"
|
12
|
+
gem "pg"
|
9
13
|
else
|
10
|
-
gem "
|
14
|
+
gem "pg", "0.18.4"
|
11
15
|
end
|
16
|
+
|
12
17
|
if ar_version >= "5.2"
|
13
18
|
gem "sqlite3"
|
14
19
|
else
|
15
|
-
gem "sqlite3", "~> 1.3.
|
20
|
+
gem "sqlite3", "~> 1.3.13"
|
16
21
|
end
|
17
22
|
end
|
18
23
|
end
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,15 @@ a nice looking [Changelog](http://keepachangelog.com).
|
|
5
5
|
|
6
6
|
## Version [Unreleased]
|
7
7
|
|
8
|
+
## Version [1.5.0] <small>2019-12-02</small>
|
9
|
+
|
10
|
+
* `select()` no longer modifies `select_values`. It understands virtual attributes at a lower level.
|
11
|
+
* `includes()` can now handle all proper values presented.
|
12
|
+
* `virtual_total` added support for `has_many` `:through`
|
13
|
+
* `virtual_total` with a nil attribute value no longer executes an extra query
|
14
|
+
* rails 6.0 support, (rails 5.2 only fails `habtm` preloading)
|
15
|
+
* ruby 2.6.x support (no longer testing ruby 2.4)
|
16
|
+
|
8
17
|
## Version [1.4.0] <small>2019-07-13</small>
|
9
18
|
|
10
19
|
* fix includes to include all associations
|
@@ -45,7 +54,8 @@ a nice looking [Changelog](http://keepachangelog.com).
|
|
45
54
|
* Initial Release
|
46
55
|
* Extracted from ManageIQ/manageiq
|
47
56
|
|
48
|
-
[Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.
|
57
|
+
[Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.5.0...HEAD
|
58
|
+
[1.5.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.4.0...v1.5.0
|
49
59
|
[1.4.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.3.1...v1.4.0
|
50
60
|
[1.3.1]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.3.0...v1.3.1
|
51
61
|
[1.3.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.2.0...v1.3.0
|
@@ -93,7 +93,11 @@ module ActiveRecord
|
|
93
93
|
# change necessary for rails 5.0 and 5.1 - (changed/introduced in https://github.com/rails/rails/pull/31894)
|
94
94
|
defaults = defaults.except(*virtual_attribute_names)
|
95
95
|
# end change
|
96
|
-
@attributes_builder = ActiveRecord
|
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
|
97
101
|
end
|
98
102
|
@attributes_builder
|
99
103
|
end
|
@@ -2,7 +2,7 @@ RSpec::Matchers.define :have_virtual_attribute do |name, type|
|
|
2
2
|
match do |klass|
|
3
3
|
expect(klass.has_attribute?(name)).to be_truthy
|
4
4
|
expect(klass.virtual_attribute?(name)).to be_truthy
|
5
|
-
expect(klass.type_for_attribute(name).type).to
|
5
|
+
expect(klass.type_for_attribute(name.to_s).type).to(eq(type)) if type
|
6
6
|
klass.instance_methods.include?(name.to_sym)
|
7
7
|
end
|
8
8
|
|
@@ -13,10 +13,6 @@ RSpec::Matchers.define :have_virtual_attribute do |name, type|
|
|
13
13
|
failure_message_when_negated do |klass|
|
14
14
|
"expected #{klass.name} to not have virtual column #{name.inspect} with type #{type.inspect}"
|
15
15
|
end
|
16
|
-
|
17
|
-
description do
|
18
|
-
"expect the object to have the virtual column"
|
19
|
-
end
|
20
16
|
end
|
21
17
|
|
22
18
|
RSpec::Matchers.alias_matcher(:have_virtual_column, :have_virtual_attribute)
|
@@ -48,18 +48,16 @@ module ActiveRecord
|
|
48
48
|
def replace_virtual_field_hash(associations)
|
49
49
|
associations.each_with_object({}) do |(parent, child), h|
|
50
50
|
if virtual_field?(parent) # form virtual_attribute => {}
|
51
|
-
|
52
|
-
when String, Symbol
|
53
|
-
merge_includes(h, new_includes)
|
54
|
-
when Array
|
55
|
-
merge_includes(h, new_includes)
|
56
|
-
when Hash
|
57
|
-
merge_includes(h, new_includes)
|
58
|
-
end
|
51
|
+
merge_includes(h, replace_virtual_fields(virtual_includes(parent)))
|
59
52
|
else
|
60
53
|
reflection = reflect_on_association(parent.to_sym)
|
61
|
-
|
62
|
-
|
54
|
+
if reflection.nil?
|
55
|
+
merge_includes(h, parent)
|
56
|
+
elsif reflection.options[:polymorphic]
|
57
|
+
merge_includes(h, parent => child)
|
58
|
+
else
|
59
|
+
merge_includes(h, parent => reflection.klass.replace_virtual_fields(child) || {})
|
60
|
+
end
|
63
61
|
end
|
64
62
|
end
|
65
63
|
end
|
@@ -103,20 +101,88 @@ module ActiveRecord
|
|
103
101
|
module Associations
|
104
102
|
class Preloader
|
105
103
|
prepend(Module.new {
|
106
|
-
|
107
|
-
|
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
|
108
115
|
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
+
}
|
112
132
|
end
|
113
133
|
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
142
|
+
end
|
143
|
+
|
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
|
164
|
+
end
|
165
|
+
h
|
117
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)
|
171
|
+
|
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)
|
176
|
+
end
|
177
|
+
# /new logic
|
118
178
|
|
119
|
-
|
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))
|
182
|
+
end
|
183
|
+
|
184
|
+
loaders
|
185
|
+
end
|
120
186
|
end
|
121
187
|
})
|
122
188
|
end
|
@@ -152,7 +218,7 @@ module ActiveRecord
|
|
152
218
|
# This can be seen with the following:
|
153
219
|
#
|
154
220
|
# Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
|
155
|
-
# .includes(:tags => {}).references(:tags
|
221
|
+
# .includes(:tags => {}).references(:tags)
|
156
222
|
#
|
157
223
|
# Which will produce a SQL SELECT statement kind of like this:
|
158
224
|
#
|
@@ -183,8 +249,11 @@ module ActiveRecord
|
|
183
249
|
additional_attributes = result_set.first.keys
|
184
250
|
.reject { |k| join_dep_keys.include?(k) }
|
185
251
|
.reject { |k| join_root_aliases.include?(k) }
|
186
|
-
|
187
|
-
|
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
|
188
257
|
end
|
189
258
|
# End of New Code
|
190
259
|
|
@@ -199,7 +268,11 @@ module ActiveRecord
|
|
199
268
|
result_set.each { |row_hash|
|
200
269
|
parent_key = primary_key ? row_hash[primary_key] : row_hash
|
201
270
|
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
|
202
|
-
|
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
|
203
276
|
}
|
204
277
|
end
|
205
278
|
|
@@ -222,30 +295,83 @@ module ActiveRecord
|
|
222
295
|
|
223
296
|
include(Module.new {
|
224
297
|
# From ActiveRecord::FinderMethods
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# From ActiveRecord::QueryMethods (rails 5.2 - 6.0)
|
319
|
+
def build_select(arel)
|
320
|
+
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) })
|
229
324
|
else
|
230
|
-
|
325
|
+
arel.project(table[Arel.star])
|
231
326
|
end
|
232
327
|
end
|
233
328
|
|
234
|
-
#
|
235
|
-
def
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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)
|
336
|
+
end
|
337
|
+
when String
|
338
|
+
arel_column(field, allow_alias, &:itself)
|
339
|
+
when Proc
|
340
|
+
field.call
|
243
341
|
else
|
244
342
|
field
|
245
343
|
end
|
246
344
|
end
|
247
|
-
|
248
|
-
|
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)
|
356
|
+
else
|
357
|
+
yield field
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
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
|
368
|
+
arel
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
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)
|
249
375
|
end
|
250
376
|
|
251
377
|
# From ActiveRecord::QueryMethods
|
@@ -256,14 +382,13 @@ module ActiveRecord
|
|
256
382
|
|
257
383
|
# From ActiveRecord::Calculations
|
258
384
|
def calculate(operation, attribute_name)
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
263
389
|
end
|
264
|
-
# end work around 1
|
265
390
|
|
266
|
-
# allow calculate to work
|
391
|
+
# allow calculate to work with includes and a virtual attribute
|
267
392
|
real = without_virtual_includes
|
268
393
|
return super if real.equal?(self)
|
269
394
|
|
@@ -5,14 +5,9 @@ module VirtualAttributes
|
|
5
5
|
module ClassMethods
|
6
6
|
private
|
7
7
|
|
8
|
-
# define an attribute to calculating the total of a
|
9
|
-
def virtual_total(name, relation, options = {})
|
10
|
-
virtual_aggregate(name, relation, :size, nil, options)
|
11
|
-
end
|
12
|
-
|
13
|
-
# define an attribute to calculating the total of a child
|
8
|
+
# define an attribute to calculating the total of a has many relationship
|
14
9
|
#
|
15
|
-
# example
|
10
|
+
# example:
|
16
11
|
#
|
17
12
|
# class ExtManagementSystem
|
18
13
|
# has_many :vms
|
@@ -29,7 +24,14 @@ module VirtualAttributes
|
|
29
24
|
#
|
30
25
|
# # arel == (SELECT COUNT(*) FROM vms where ems.id = vms.ems_id)
|
31
26
|
#
|
32
|
-
|
27
|
+
def virtual_total(name, relation, options = {})
|
28
|
+
define_virtual_size_method(name, relation)
|
29
|
+
define_virtual_aggregate_attribute(name, relation, :count, Arel.star, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# define an attribute to calculate the sum of a has may relationship
|
33
|
+
#
|
34
|
+
# example:
|
33
35
|
#
|
34
36
|
# class Hardware
|
35
37
|
# has_many :disks
|
@@ -40,7 +42,7 @@ module VirtualAttributes
|
|
40
42
|
#
|
41
43
|
# def allocated_disk_storage
|
42
44
|
# if disks.loaded?
|
43
|
-
# disks.blank? ? nil : disks.map
|
45
|
+
# disks.blank? ? nil : disks.map(&:size).compact.sum
|
44
46
|
# else
|
45
47
|
# disks.sum(:size) || 0
|
46
48
|
# end
|
@@ -51,7 +53,13 @@ module VirtualAttributes
|
|
51
53
|
# # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
|
52
54
|
|
53
55
|
def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
|
56
|
+
return define_virtual_total(name, relation, options) if method_name == :size
|
57
|
+
|
54
58
|
define_virtual_aggregate_method(name, relation, method_name, column)
|
59
|
+
define_virtual_aggregate_attribute(name, relation, method_name, column, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def define_virtual_aggregate_attribute(name, relation, method_name, column, options)
|
55
63
|
reflection = reflect_on_association(relation)
|
56
64
|
|
57
65
|
if options.key?(:arel)
|
@@ -69,54 +77,58 @@ module VirtualAttributes
|
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
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
|
+
|
72
86
|
def define_virtual_aggregate_method(name, relation, method_name, column)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
(
|
80
|
-
begin
|
81
|
-
rel = send(relation)
|
82
|
-
if rel.loaded?
|
83
|
-
rel.blank? ? nil : (rel.map { |t| t.send(column).to_i } || 0).send(method_name)
|
84
|
-
else
|
85
|
-
# aggregates are not smart enough to handle virtual attributes
|
86
|
-
arel_column = rel.klass.arel_attribute(column)
|
87
|
-
rel.try(method_name, arel_column) || 0
|
88
|
-
end
|
89
|
-
end
|
87
|
+
define_method(name) do
|
88
|
+
if attribute_present?(name)
|
89
|
+
self[name]
|
90
|
+
elsif (rel = send(relation)).loaded?
|
91
|
+
rel.blank? ? nil : rel.map { |t| t.send(column) }.compact.send(method_name)
|
92
|
+
else
|
93
|
+
rel.try(method_name, column) || 0
|
90
94
|
end
|
91
95
|
end
|
92
96
|
end
|
93
97
|
|
94
98
|
def virtual_aggregate_arel(reflection, method_name, column)
|
95
|
-
return unless reflection && reflection.macro == :has_many
|
99
|
+
return unless reflection && reflection.macro == :has_many
|
100
|
+
|
101
|
+
# need db access for the reflection join_keys, so delaying all this key lookup until call time
|
96
102
|
lambda do |t|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
103
|
+
# strings and symbols are converted across, arel objects are not
|
104
|
+
column = reflection.klass.arel_attribute(column) unless column.respond_to?(:count)
|
105
|
+
|
106
|
+
# query: SELECT COUNT(*) FROM main_table JOIN foreign_table ON main_table.id = foreign_table.id JOIN ...
|
107
|
+
relation_query = joins(reflection.name).select(column.send(method_name))
|
108
|
+
query = relation_query.arel
|
109
|
+
|
110
|
+
# algorithm:
|
111
|
+
# - remove main_table from this sub query. (it is already in the primary query)
|
112
|
+
# - move the foreign_table from the JOIN to the FROM clause
|
113
|
+
# - move the main_table.id = foreign_table.id from the ON clause to the WHERE clause
|
114
|
+
|
115
|
+
# query: SELECT COUNT(*) FROM main_table [ ] JOIN ...
|
116
|
+
join = query.source.right.shift
|
117
|
+
# query: SELECT COUNT(*) FROM [foreign_table] JOIN ...
|
118
|
+
query.source.left = join.left
|
119
|
+
# query: SELECT COUNT(*) FROM foreign_table JOIN ... [WHERE main_table.id = foreign_table.id]
|
120
|
+
query.where(join.right.expr)
|
111
121
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
122
|
+
# convert bind variables from ? to actual values. otherwise, sql is incomplete
|
123
|
+
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
|
118
129
|
|
119
|
-
|
130
|
+
# add () around query
|
131
|
+
t.grouping(Arel::Nodes::SqlLiteral.new(sql))
|
120
132
|
end
|
121
133
|
end
|
122
134
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-virtual_attributes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keenan Brock
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-12-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -118,9 +118,10 @@ files:
|
|
118
118
|
- activerecord-virtual_attributes.gemspec
|
119
119
|
- bin/console
|
120
120
|
- bin/setup
|
121
|
-
- gemfiles/
|
122
|
-
- gemfiles/
|
123
|
-
- gemfiles/
|
121
|
+
- gemfiles/gemfile_50.gemfile
|
122
|
+
- gemfiles/gemfile_51.gemfile
|
123
|
+
- gemfiles/gemfile_52.gemfile
|
124
|
+
- gemfiles/gemfile_60.gemfile
|
124
125
|
- init.rb
|
125
126
|
- lib/active_record-virtual_attributes.rb
|
126
127
|
- lib/active_record/virtual_attributes.rb
|