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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12889257087183346e6a2d382f1105adcd42dcf0d56e8cfb3174ad9bc03e93a4
4
- data.tar.gz: 422aa8fc3a9115014feca968087c32f52641daaaab04db3308fe7382377b6b7d
3
+ metadata.gz: 90da25c212d7507da2ca2789b1151b2e7cb7cb3936411a7e92823518299141b9
4
+ data.tar.gz: d2a3cc1662d29fa8625dab40610950a573c0014b9739dbb260184da47afdb2ac
5
5
  SHA512:
6
- metadata.gz: 92c23a0d47b390b39c1576e28ae444891dc7c4fda8b4ee493541d760aa7784442b6be95ea5f4dbd7d742feec949defd5640a5b923c755aa8162076a3fdd0700b
7
- data.tar.gz: f2ee6541b6b0aaa5dcd6d25e2019bbd97ead4276495f688e71ff614463a1c818f12106a38bfb13c801e91880c8910fecd71878d9aee23f7e4c6d178010e06d91
6
+ metadata.gz: 78c1edd3d1a17f5f4fcd147315e37c788cdfc3add3137a4190d568cc93ab1823d9de0e46a6f57f266d248104823578d5f6a86e076228ae640c52c035922910f4
7
+ data.tar.gz: 19ec2fafe3553b4ff809c1aae1aa6ccfd6f303216d1c35bc4a09f30e9dda2876a48fd531d9216e0a1a566de0434c90edd58936c12027b62bc9c70eb896235b50
@@ -1,20 +1,22 @@
1
1
  ---
2
+ sudo: false
2
3
  language: ruby
3
4
  cache: bundler
4
5
  rvm:
5
- - 2.4.1
6
- - 2.5.3
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/virtual_attributes_50.gemfile
16
- - gemfiles/virtual_attributes_51.gemfile
17
- #- gemfiles/virtual_attributes_52.gemfile
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.6 5.2.0).each do |ar_version|
2
- db_gem = "virtual_attributes"
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 "mysql2", '~> 0.4.0'
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.6"
20
+ gem "sqlite3", "~> 1.3.13"
16
21
  end
17
22
  end
18
23
  end
@@ -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.4.0...HEAD
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
@@ -5,6 +5,6 @@ source "https://rubygems.org"
5
5
  gem "activerecord", "~> 5.0.7"
6
6
  gem "mysql2"
7
7
  gem "pg"
8
- gem "sqlite3", "~> 1.3.6"
8
+ gem "sqlite3", "~> 1.3.13"
9
9
 
10
10
  gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.1.7"
6
+ gem "mysql2"
7
+ gem "pg"
8
+ gem "sqlite3", "~> 1.3.13"
9
+
10
+ gemspec path: "../"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "~> 5.2.0"
5
+ gem "activerecord", "~> 5.2.3"
6
6
  gem "mysql2"
7
7
  gem "pg"
8
8
  gem "sqlite3"
@@ -2,9 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "~> 5.1.6"
5
+ gem "activerecord", "~> 6.0.0"
6
6
  gem "mysql2"
7
7
  gem "pg"
8
- gem "sqlite3", "~> 1.3.6"
8
+ gem "sqlite3"
9
9
 
10
10
  gemspec path: "../"
@@ -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::AttributeSet::Builder.new(attribute_types, defaults)
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 eq(type)
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)
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module VirtualAttributes
3
- VERSION = "1.4.0".freeze
3
+ VERSION = "1.5.0".freeze
4
4
  end
5
5
  end
@@ -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
- case (new_includes = replace_virtual_fields(virtual_includes(parent)))
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
- new_child = reflection.nil? || reflection.options[:polymorphic] ? {} : reflection.klass.replace_virtual_fields(child) || {}
62
- merge_includes(h, parent => new_child)
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
- def preloaders_for_one(association, records, scope)
107
- klass_map = records.compact.group_by(&:class)
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
- loaders = klass_map.keys.group_by { |klass| klass.virtual_includes(association) }.flat_map do |virtuals, klasses|
110
- subset = klasses.flat_map { |klass| klass_map[klass] }
111
- preload(subset, virtuals)
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
- records_with_association = klass_map.select { |k, _rs| k.reflect_on_association(association) }.flat_map { |_k, rs| rs }
115
- if records_with_association.any?
116
- loaders.concat(super(association, records_with_association, scope))
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
- loaders
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
- .map { |k| [k, k] }
187
- column_aliases += additional_attributes
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
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
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
- def find_with_associations(&block)
226
- real = without_virtual_includes
227
- if real.equal?(self)
228
- super
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
- real.find_with_associations(&block)
325
+ arel.project(table[Arel.star])
231
326
  end
232
327
  end
233
328
 
234
- # From ActiveRecord::QueryMethods
235
- def select(*fields)
236
- return super if block_given? || fields.empty?
237
- # support virtual attributes by adding an alias to the sql phrase for the column
238
- # it does not add an as() if the column already has an as
239
- # this code is based upon _select()
240
- fields = fields.flatten.map! do |field|
241
- if virtual_attribute?(field) && (arel = klass.arel_attribute(field)) && arel.respond_to?(:as)
242
- arel.as(connection.quote_column_name(field.to_s))
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
- # end support virtual attributes
248
- super
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
- # work around 1 until https://github.com/rails/rails/pull/25304 gets merged
260
- # This allows attribute_name to be a virtual_attribute
261
- if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
262
- attribute_name = arel
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 when including a virtual attribute
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 child
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 1:
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
- # example 2:
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 { |t| t.size.to_i }.sum
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
- if method_name == :size
74
- define_method(name) do
75
- (attribute_present?(name) ? self[name] : nil) || send(relation).try(:size) || 0
76
- end
77
- else
78
- define_method(name) do
79
- (attribute_present?(name) ? self[name] : nil) ||
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 && !reflection.options[:through]
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
- query = if reflection.scope
98
- reflection.klass.instance_exec(nil, &reflection.scope)
99
- else
100
- reflection.klass.all
101
- end
102
-
103
- foreign_table = reflection.klass.arel_table
104
- # need db access for the keys, so delaying all this lookup until call time
105
- if ActiveRecord.version.to_s >= "5.1"
106
- join_keys = reflection.join_keys
107
- else
108
- join_keys = reflection.join_keys(reflection.klass)
109
- end
110
- query = query.except(:order).where(t[join_keys.foreign_key].eq(foreign_table[join_keys.key]))
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
- arel_column = if method_name == :size
113
- Arel.star.count
114
- else
115
- reflection.klass.arel_attribute(column).send(method_name)
116
- end
117
- query = query.select(arel_column)
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
- t.grouping(Arel::Nodes::SqlLiteral.new(query.to_sql))
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.0
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-07-12 00:00:00.000000000 Z
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/virtual_attributes_50.gemfile
122
- - gemfiles/virtual_attributes_51.gemfile
123
- - gemfiles/virtual_attributes_52.gemfile
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