activerecord-virtual_attributes 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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