activerecord-virtual_attributes 1.3.1 → 3.0.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: d35dc5f3ef872b957e24553488667ea6e1f948e56d741d6f5dbf5a7612dc1208
4
- data.tar.gz: 8980c2297bb237b520aafd79177e727c29628b3e807349df8ec153e259f3ccb6
3
+ metadata.gz: f205435ff8ec8ecda2020356916188e8c145dedfd57e9e9a89c7c3c743afa57a
4
+ data.tar.gz: 3485ee96f78567ef4fd98eec35dc6350313c543c6e5606c1ad4191812a25d534
5
5
  SHA512:
6
- metadata.gz: a42caa72fee1075051551c5c12c14b71696be135ed27eaf755936bde51b63a4953a51458ad22dd7fd40808c2aae9c661b93b0aee5e6a3d684acd85a21461f788
7
- data.tar.gz: 7ca0eb7fde02046e0921a81d84735be127cd068ee554550fd4dc903da2b105f025064ab3e8a728fb9e26d2586dbd892a569eb8a6cac6557d19770a9fc28945c0
6
+ metadata.gz: 700653df253780ef026120bdaa639e14adc4ed2c5ee735575d6a42a8335833791ab8dff596de8195b3c07f9da68d55e535a70cd20edc7b3ed2d973cf23aab177
7
+ data.tar.gz: e4ae06737ca3973621e673b18388f0bc9a0012efbe2e569857de4b718a7a89ae3324f27ff3cbac91d0d7c8da6456bd36cee1161dec771b6af3f8bde0ced8f65b
@@ -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:
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
@@ -3,7 +3,41 @@
3
3
  Doing our best at supporting [SemVer](http://semver.org/) with
4
4
  a nice looking [Changelog](http://keepachangelog.com).
5
5
 
6
- ## Version [Unreleased]
6
+ ## Version [Unreleased] <small>tbd</small>
7
+
8
+ ## Version [3.0.0] <small>2020-09-28</small>
9
+
10
+ * fix virtual_aggregate to return a consistent 0 when calculating a sum of no records
11
+ * fix virtual delegate to include the type column when fetching associated models for polymorphism
12
+ * add virtual_average, virtual_minimum, and virtual_maximum
13
+
14
+ ## Version [2.0.0] <small>2020-05-22</small>
15
+
16
+ * This is a trivial release, but because it modifies a public interface, the jump makes it look significant.
17
+ * removed legacy virtual_column parameter support. (it is not ruby 2.7 compatible)
18
+ * fixed warnings in ruby 2.7
19
+
20
+ ## Version [1.6.0] <small>2019-12-02</small>
21
+
22
+ * rails 5.2 support
23
+ * fix Arel#name error
24
+ * Display deprecation notices for invalid associations (rather than throw an error)
25
+
26
+ ## Version [1.5.0] <small>2019-12-02</small>
27
+
28
+ * `select()` no longer modifies `select_values`. It understands virtual attributes at a lower level.
29
+ * `includes()` can now handle all proper values presented.
30
+ * `virtual_total` added support for `has_many` `:through`
31
+ * `virtual_total` with a nil attribute value no longer executes an extra query
32
+ * rails 6.0 support, (rails 5.2 only fails `habtm` preloading)
33
+ * ruby 2.6.x support (no longer testing ruby 2.4)
34
+
35
+ ## Version [1.4.0] <small>2019-07-13</small>
36
+
37
+ * fix includes to include all associations
38
+ * fix bin/console to now actually run
39
+ * select no longer munges field attribute
40
+ * support virtual attributes in left_outer_joins
7
41
 
8
42
  ## Version [1.3.1] <small>2019-06-06</small>
9
43
 
@@ -38,7 +72,12 @@ a nice looking [Changelog](http://keepachangelog.com).
38
72
  * Initial Release
39
73
  * Extracted from ManageIQ/manageiq
40
74
 
41
- [Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.3.1...HEAD
75
+ [Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v3.0.0...HEAD
76
+ [3.0.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v2.0.0...v3.0.0
77
+ [2.0.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.6.0...v2.0.0
78
+ [1.6.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.5.0...v1.6.0
79
+ [1.5.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.4.0...v1.5.0
80
+ [1.4.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.3.1...v1.4.0
42
81
  [1.3.1]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.3.0...v1.3.1
43
82
  [1.3.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.2.0...v1.3.0
44
83
  [1.2.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v1.1.0...v1.2.0
data/README.md CHANGED
@@ -42,6 +42,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
42
42
 
43
43
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
44
 
45
+
46
+ To test with different versions of ruby, use `wwtd` gem or
47
+
48
+ DB=pg BUNDLE_GEMFILE=gemfiles/gemfile_${version-52}.gemfile beer "$@"
49
+
45
50
  ## Contributing
46
51
 
47
52
  Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/activerecord-virtual_attributes .
@@ -28,6 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency "activerecord", ">= 5.0"
29
29
 
30
30
  spec.add_development_dependency "appraisal"
31
+ spec.add_development_dependency "byebug"
32
+ spec.add_development_dependency "db-query-matchers", "~>0.10"
31
33
  spec.add_development_dependency "rake", "~> 10.0"
32
34
  spec.add_development_dependency "rspec", "~> 3.0"
33
35
  spec.add_development_dependency "simplecov"
@@ -1,14 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
- require "virtual_attributes"
4
+ require "active_record-virtual_attributes"
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
6
+ # models for local testing
7
+ require "rspec"
8
+ Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
12
9
 
13
10
  require "irb"
14
11
  IRB.start(__FILE__)
@@ -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: "../"
data/init.rb CHANGED
@@ -1 +1 @@
1
- require 'virtual_attributes'
1
+ require 'active_record-virtual_attributes'
@@ -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, type_or_options, **options)
52
- if type_or_options.kind_of?(Hash)
53
- options = options.merge(type_or_options)
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
@@ -93,7 +89,11 @@ module ActiveRecord
93
89
  # change necessary for rails 5.0 and 5.1 - (changed/introduced in https://github.com/rails/rails/pull/31894)
94
90
  defaults = defaults.except(*virtual_attribute_names)
95
91
  # end change
96
- @attributes_builder = ActiveRecord::AttributeSet::Builder.new(attribute_types, defaults)
92
+ @attributes_builder = if ActiveRecord.version.to_s >= "5.2"
93
+ ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
94
+ else
95
+ ActiveRecord::AttributeSet::Builder.new(attribute_types, defaults)
96
+ end
97
97
  end
98
98
  @attributes_builder
99
99
  end
@@ -1,6 +1,7 @@
1
1
  # this is from https://github.com/rails/arel/pull/435
2
2
  # this allows sorting and where clauses to work with virtual_attribute columns
3
- if defined?(Arel::Nodes::Grouping)
3
+ # no longer needed for rails 6.0 and up (change was merged)
4
+ if ActiveRecord.version.to_s < "6.0" && defined?(Arel::Nodes::Grouping)
4
5
  module Arel
5
6
  module Nodes
6
7
  class Grouping
@@ -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.3.1"
3
+ VERSION = "3.0.0".freeze
4
4
  end
5
5
  end
@@ -2,13 +2,17 @@ module ActiveRecord
2
2
  module VirtualAttributes
3
3
  # VirtualArel associates arel with an attribute
4
4
  #
5
- # Model.virtual_attribute :field, :string, :arel => -> (t) { t.grouping(t[:field2]) } }
5
+ # Model.virtual_attribute :field, :string, :arel => ->(t) { t.grouping(t[:field2]) } }
6
6
  # Model.select(:field)
7
7
  #
8
8
  # is equivalent to:
9
9
  #
10
10
  # Model.select(Model.arel_table.grouping(Model.arel_table[:field2]).as(:field))
11
11
  # Model.attribute_supported_by_sql?(:field) # => true
12
+ class Arel::Nodes::Grouping
13
+ attr_accessor :name
14
+ end
15
+
12
16
  module VirtualArel
13
17
  extend ActiveSupport::Concern
14
18
 
@@ -21,8 +25,11 @@ module ActiveRecord
21
25
  def arel_attribute(column_name, arel_table = self.arel_table)
22
26
  load_schema
23
27
  if virtual_attribute?(column_name) && !attribute_alias?(column_name)
24
- col = _virtual_arel[column_name.to_s]
25
- col.call(arel_table) if col
28
+ if (col = _virtual_arel[column_name.to_s])
29
+ arel = col.call(arel_table)
30
+ arel.name = column_name if arel.kind_of?(Arel::Nodes::Grouping)
31
+ arel
32
+ end
26
33
  else
27
34
  super
28
35
  end
@@ -240,6 +240,7 @@ module ActiveRecord
240
240
  # (SELECT "vms_sub"."name" FROM "vms" AS "vms_ss" WHERE "vms_ss"."id" = "vms"."src_template_id")
241
241
  #
242
242
 
243
+ # Based upon ActiveRecord AssociationScope.scope
243
244
  def self.select_from_alias(to_ref, col, to_model_col_name, src_model_id)
244
245
  query = if to_ref.scope
245
246
  to_ref.klass.instance_exec(nil, &to_ref.scope)
@@ -247,6 +248,7 @@ module ActiveRecord
247
248
  to_ref.klass.all
248
249
  end
249
250
 
251
+ src_model = to_ref.active_record
250
252
  to_table = select_from_alias_table(to_ref.klass, src_model_id.relation)
251
253
  to_model_id = to_ref.klass.arel_attribute(to_model_col_name, to_table)
252
254
  to_column = to_ref.klass.arel_attribute(col, to_table)
@@ -254,6 +256,13 @@ module ActiveRecord
254
256
  .from(to_table)
255
257
  .where(to_model_id.eq(src_model_id))
256
258
 
259
+ # :type is in the reflection definition (meaning it is polymorphic)
260
+ if to_ref.type
261
+ # get the class name (e.g. "Host")
262
+ polymorphic_type = src_model.base_class.name
263
+ arel = arel.where(to_ref.klass.arel_attribute(to_ref.type).eq(polymorphic_type))
264
+ end
265
+
257
266
  yield arel if block_given?
258
267
 
259
268
  Arel.sql("(#{arel.to_sql})")
@@ -48,20 +48,46 @@ 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
- h[new_includes] = {}
54
- when Array
55
- new_includes.each { |association| h[association] = {} }
56
- when Hash
57
- h.deep_merge!(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
- h[parent] = reflection.nil? || reflection.options[:polymorphic] ? {} : reflection.klass.replace_virtual_fields(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
62
61
  end
63
62
  end
64
63
  end
64
+
65
+ # @param [Hash, Array, String, Symbol] value
66
+ # @return [Hash]
67
+ def include_to_hash(value)
68
+ case value
69
+ when String, Symbol
70
+ {value => {}}
71
+ when Array
72
+ value.flatten.each_with_object({}) { |k, h| h[k] = {} }
73
+ when nil
74
+ {}
75
+ else
76
+ value
77
+ end
78
+ end
79
+
80
+ # @param [Hash] hash1
81
+ # @param [Hash] hash2
82
+ def merge_includes(hash1, hash2)
83
+ return hash1 if hash2.blank?
84
+
85
+ hash1 = include_to_hash(hash1)
86
+ hash2 = include_to_hash(hash2)
87
+ hash1.deep_merge!(hash2) do |_k, v1, v2|
88
+ merge_includes(v1, v2)
89
+ end
90
+ end
65
91
  end
66
92
  end
67
93
  end
@@ -75,20 +101,142 @@ module ActiveRecord
75
101
  module Associations
76
102
  class Preloader
77
103
  prepend(Module.new {
78
- def preloaders_for_one(association, records, scope)
79
- klass_map = records.compact.group_by(&:class)
104
+ if ActiveRecord.version.to_s >= "6.0"
105
+ # preloader.rb active record 6.0
106
+ # changed:
107
+ # since grouped_records can return a hash/array, we need to handle those 2 new cases
108
+ def preloaders_for_reflection(reflection, records, scope, polymorphic_parent)
109
+ case reflection
110
+ when Array
111
+ reflection.flat_map { |ref| preloaders_on(ref, records, scope, polymorphic_parent) }
112
+ when Hash
113
+ preloaders_on(reflection, records, scope, polymorphic_parent)
114
+ else
115
+ super(reflection, records, scope)
116
+ end
117
+ end
118
+ elsif ActiveRecord.version.to_s >= "5.2" # < 6.0
119
+ # preloader.rb active record 6.0
120
+ # else block changed to reflect how 5.2 preloaders_for_one works
121
+ def preloaders_for_reflection(reflection, records, scope, polymorphic_parent)
122
+ case reflection
123
+ when Array
124
+ reflection.flat_map { |ref| preloaders_on(ref, records, scope, polymorphic_parent) }
125
+ when Hash
126
+ preloaders_on(reflection, records, scope, polymorphic_parent)
127
+ else
128
+ records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
129
+ loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
130
+ loader.run(self)
131
+ loader
132
+ end
133
+ end
134
+ end
80
135
 
81
- loaders = klass_map.keys.group_by { |klass| klass.virtual_includes(association) }.flat_map do |virtuals, klasses|
82
- subset = klasses.flat_map { |klass| klass_map[klass] }
83
- preload(subset, virtuals)
136
+ # preloader.rb active record 6.0
137
+ # since this deals with polymorphic_parent, it makes everything easier to just define it
138
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
139
+ case association
140
+ when Hash
141
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
142
+ when Symbol, String
143
+ preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
144
+ else
145
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
146
+ end
147
+ end
148
+ end
149
+
150
+ if ActiveRecord.version.to_s >= "5.2"
151
+ # rubocop:disable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
152
+ # preloader.rb active record 6.0
153
+ # changed:
154
+ # passing polymorphic around (and makes 5.2 more similar to 6.0)
155
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
156
+ association.flat_map { |parent, child|
157
+ grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
158
+ loaders = preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
159
+ recs = loaders.flat_map(&:preloaded_records).uniq
160
+ child_polymorphic_parent = reflection && reflection.respond_to?(:options) && reflection.options[:polymorphic]
161
+ loaders.concat Array.wrap(child).flat_map { |assoc|
162
+ preloaders_on assoc, recs, scope, child_polymorphic_parent
163
+ }
164
+ loaders
165
+ end
166
+ }
84
167
  end
85
168
 
86
- records_with_association = klass_map.select { |k, _rs| k.reflect_on_association(association) }.flat_map { |_k, rs| rs }
87
- if records_with_association.any?
88
- loaders.concat(super(association, records_with_association, scope))
169
+ # preloader.rb active record 6.0
170
+ # changed:
171
+ # passing polymorphic_parent to preloaders_for_reflection
172
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
173
+ grouped_records(association, records, polymorphic_parent)
174
+ .flat_map do |reflection, reflection_records|
175
+ preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
176
+ end
89
177
  end
90
178
 
91
- loaders
179
+ # preloader.rb active record 6.0
180
+ # changed:
181
+ # different from 5.2. But not called outside these redefined methods here, so it works fine
182
+ # did add compact to fix a 5.2 double preload nil bug
183
+ def grouped_records(orig_association, records, polymorphic_parent)
184
+ h = {}
185
+ records.compact.each do |record|
186
+ # each class can resolve virtual_{attributes,includes} differently
187
+ association = record.class.replace_virtual_fields(orig_association)
188
+ # 1 line optimization for single element array:
189
+ association = association.first if association.kind_of?(Array) && association.size == 1
190
+
191
+ case association
192
+ when Symbol, String
193
+ # 4/24/20 we want to revert #67 once we handle all these error cases in our codebase.
194
+ reflection = record.class._reflect_on_association(association)
195
+ display_virtual_attribute_deprecation("#{record.class.name}.#{association} does not exist") if !reflection && !polymorphic_parent
196
+ next if !reflection || !record.association(association).klass
197
+ when nil
198
+ next
199
+ else # need parent (preloaders_for_{hash,one}) to handle this Array/Hash
200
+ reflection = association
201
+ end
202
+ (h[reflection] ||= []) << record
203
+ end
204
+ h
205
+ end
206
+ # rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
207
+
208
+ def display_virtual_attribute_deprecation(str)
209
+ short_caller = caller
210
+ # if debugging is turned on, don't prune the backtrace.
211
+ # if debugging is off, prune down to the line where the sql is executed
212
+ # this defaults to false and only displays 1 line number.
213
+ unless ActiveSupport::Deprecation.debug
214
+ bc = ActiveSupport::BacktraceCleaner.new
215
+ bc.add_silencer { |line| line =~ /virtual_fields/ }
216
+ bc.add_silencer { |line| line =~ /active_record/ }
217
+ short_caller = bc.clean(caller)
218
+ end
219
+
220
+ ActiveSupport::Deprecation.warn(str, short_caller)
221
+ end
222
+ else
223
+ def preloaders_for_one(association, records, scope)
224
+ klass_map = records.compact.group_by(&:class)
225
+
226
+ # new logic: preload virtual fields / virtual includes
227
+ loaders = klass_map.keys.group_by { |klass| klass.virtual_includes(association) }.flat_map do |virtuals, klasses|
228
+ subset = klasses.flat_map { |klass| klass_map[klass] }
229
+ preload(subset, virtuals)
230
+ end
231
+ # /new logic
232
+
233
+ records_with_association = klass_map.select { |k, _rs| k.reflect_on_association(association) }.flat_map { |_k, rs| rs }
234
+ if records_with_association.any?
235
+ loaders.concat(super(association, records_with_association, scope))
236
+ end
237
+
238
+ loaders
239
+ end
92
240
  end
93
241
  })
94
242
  end
@@ -100,7 +248,7 @@ module ActiveRecord
100
248
  # syntax from the original codebase.
101
249
  #
102
250
  # rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
103
- # rubocop:disable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
251
+ # rubocop:disable Layout/AlignHash
104
252
  class JoinDependency
105
253
  def instantiate(result_set, *_, &block)
106
254
  primary_key = aliases.column_alias(join_root, join_root.primary_key)
@@ -116,48 +264,7 @@ module ActiveRecord
116
264
  column_aliases = aliases.column_aliases(join_root)
117
265
 
118
266
  # New Code
119
- #
120
- # This monkey patches the ActiveRecord::Associations::JoinDependency to
121
- # include columns into the main record that might have been added
122
- # through a `select` clause.
123
- #
124
- # This can be seen with the following:
125
- #
126
- # Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
127
- # .includes(:tags => {}).references(:tags => {})
128
- #
129
- # Which will produce a SQL SELECT statement kind of like this:
130
- #
131
- # SELECT "vms".*,
132
- # (<virtual_attribute_arel>) AS some_vm_virtual_col,
133
- # "vms"."id" AS t0_r0
134
- # "vms"."vendor" AS t0_r1
135
- # "vms"."format" AS t0_r1
136
- # "vms"."version" AS t0_r1
137
- # ...
138
- # "tags"."id" AS t1_r0
139
- # "tags"."name" AS t1_r1
140
- #
141
- # This is because rails is trying to reduce the number of queries
142
- # needed to fetch all of the records in the include, so it grabs the
143
- # columns for both of the tables together to do it. Unfortuantely (or
144
- # fortunately... depending on how you look at it), it does not remove
145
- # any `.select` columns from the query that is run in the process, so
146
- # that is brought along for the ride, but never used when this method
147
- # instanciates the objects.
148
- #
149
- # The "New Code" here simply also instanciates any extra rows that
150
- # might have been included in the select (virtual_columns) as well and
151
- # brought back with the result set.
152
- unless result_set.empty?
153
- join_dep_keys = aliases.columns.map(&:right)
154
- join_root_aliases = column_aliases.map(&:first)
155
- additional_attributes = result_set.first.keys
156
- .reject { |k| join_dep_keys.include?(k) }
157
- .reject { |k| join_root_aliases.include?(k) }
158
- .map { |k| [k, k] }
159
- column_aliases += additional_attributes
160
- end
267
+ column_aliases += select_values_from_references(column_aliases, result_set) if result_set.present?
161
268
  # End of New Code
162
269
 
163
270
  message_bus = ActiveSupport::Notifications.instrumenter
@@ -171,14 +278,64 @@ module ActiveRecord
171
278
  result_set.each { |row_hash|
172
279
  parent_key = primary_key ? row_hash[primary_key] : row_hash
173
280
  parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
174
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
281
+ if ActiveRecord.version.to_s < "6.0"
282
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
283
+ else
284
+ construct(parent, join_root, row_hash, seen, model_cache)
285
+ end
175
286
  }
176
287
  end
177
288
 
178
289
  parents.values
179
290
  end
180
291
  # rubocop:enable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
181
- # rubocop:enable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
292
+ # rubocop:enable Layout/AlignHash
293
+
294
+ #
295
+ # This monkey patches the ActiveRecord::Associations::JoinDependency to
296
+ # include columns into the main record that might have been added
297
+ # through a `select` clause.
298
+ #
299
+ # This can be seen with the following:
300
+ #
301
+ # Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
302
+ # .includes(:tags => {}).references(:tags)
303
+ #
304
+ # Which will produce a SQL SELECT statement kind of like this:
305
+ #
306
+ # SELECT "vms".*,
307
+ # (<virtual_attribute_arel>) AS some_vm_virtual_col,
308
+ # "vms"."id" AS t0_r0
309
+ # "vms"."vendor" AS t0_r1
310
+ # "vms"."format" AS t0_r1
311
+ # "vms"."version" AS t0_r1
312
+ # ...
313
+ # "tags"."id" AS t1_r0
314
+ # "tags"."name" AS t1_r1
315
+ #
316
+ # This is because rails is trying to reduce the number of queries
317
+ # needed to fetch all of the records in the include, so it grabs the
318
+ # columns for both of the tables together to do it. Unfortunately (or
319
+ # fortunately... depending on how you look at it), it does not remove
320
+ # any `.select` columns from the query that is run in the process, so
321
+ # that is brought along for the ride, but never used when this method
322
+ # instanciates the objects.
323
+ #
324
+ # The "New Code" here simply also instanciates any extra rows that
325
+ # might have been included in the select (virtual_columns) as well and
326
+ # brought back with the result set.
327
+ def select_values_from_references(column_aliases, result_set)
328
+ join_dep_keys = aliases.columns.map(&:right)
329
+ join_root_aliases = column_aliases.map(&:first)
330
+ additional_attributes = result_set.first.keys
331
+ .reject { |k| join_dep_keys.include?(k) }
332
+ .reject { |k| join_root_aliases.include?(k) }
333
+ if ActiveRecord.version.to_s >= "6.0"
334
+ additional_attributes.map { |k| Aliases::Column.new(k, k) }
335
+ else
336
+ additional_attributes.map { |k| [k, k] }
337
+ end
338
+ end
182
339
  end
183
340
  end
184
341
 
@@ -194,43 +351,100 @@ module ActiveRecord
194
351
 
195
352
  include(Module.new {
196
353
  # From ActiveRecord::FinderMethods
197
- def find_with_associations(&block)
198
- real = without_virtual_includes
199
- if real.equal?(self)
200
- super
354
+ if ActiveRecord.version.to_s >= "5.2"
355
+ def apply_join_dependency(*args, &block)
356
+ real = without_virtual_includes
357
+ if real.equal?(self)
358
+ super
359
+ else
360
+ real.apply_join_dependency(*args, &block)
361
+ end
362
+ end
363
+ else
364
+ def find_with_associations(&block)
365
+ real = without_virtual_includes
366
+ if real.equal?(self)
367
+ super
368
+ else
369
+ real.find_with_associations(&block)
370
+ end
371
+ end
372
+ end
373
+
374
+ # From ActiveRecord::QueryMethods (rails 5.2 - 6.0)
375
+ def build_select(arel)
376
+ if select_values.any?
377
+ arel.project(*arel_columns(select_values.uniq, true))
378
+ elsif klass.ignored_columns.any?
379
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
201
380
  else
202
- real.find_with_associations(&block)
381
+ arel.project(table[Arel.star])
203
382
  end
204
383
  end
205
384
 
206
- # From ActiveRecord::QueryMethods
207
- def select(*fields)
208
- return super if block_given? || fields.empty?
209
- # support virtual attributes by adding an alias to the sql phrase for the column
210
- # it does not add an as() if the column already has an as
211
- # this code is based upon _select()
212
- fields.flatten!
213
- fields.map! do |field|
214
- if virtual_attribute?(field) && (arel = klass.arel_attribute(field)) && arel.respond_to?(:as)
215
- arel.as(connection.quote_column_name(field.to_s))
385
+ # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
386
+ def arel_columns(columns, allow_alias = false)
387
+ columns.flat_map do |field|
388
+ case field
389
+ when Symbol
390
+ arel_column(field.to_s, allow_alias) do |attr_name|
391
+ connection.quote_table_name(attr_name)
392
+ end
393
+ when String
394
+ arel_column(field, allow_alias, &:itself)
395
+ when Proc
396
+ field.call
216
397
  else
217
398
  field
218
399
  end
219
400
  end
220
- # end support virtual attributes
221
- super
401
+ end
402
+
403
+ # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
404
+ def arel_column(field, allow_alias = false, &block)
405
+ field = klass.attribute_aliases[field] || field
406
+ from = from_clause.name || from_clause.value
407
+
408
+ if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
409
+ arel_attribute(field)
410
+ elsif virtual_attribute?(field)
411
+ virtual_attribute_arel_column(field, allow_alias, &block)
412
+ else
413
+ yield field
414
+ end
415
+ end
416
+
417
+ def virtual_attribute_arel_column(field, allow_alias)
418
+ arel = arel_attribute(field)
419
+ if arel.nil?
420
+ yield field
421
+ elsif allow_alias && arel && arel.respond_to?(:as) && !arel.kind_of?(Arel::Nodes::As) && !arel.try(:alias)
422
+ arel.as(connection.quote_column_name(field.to_s))
423
+ else
424
+ arel
425
+ end
426
+ end
427
+
428
+ # From ActiveRecord::QueryMethods
429
+ def table_name_matches?(from)
430
+ /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
431
+ end
432
+
433
+ # From ActiveRecord::QueryMethods
434
+ def build_left_outer_joins(manager, outer_joins, *rest)
435
+ outer_joins = klass.replace_virtual_fields(outer_joins)
436
+ super if outer_joins.present?
222
437
  end
223
438
 
224
439
  # From ActiveRecord::Calculations
225
440
  def calculate(operation, attribute_name)
226
- # work around 1 until https://github.com/rails/rails/pull/25304 gets merged
227
- # This allows attribute_name to be a virtual_attribute
228
- if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
229
- attribute_name = arel
441
+ if ActiveRecord.version.to_s < "5.1"
442
+ if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
443
+ attribute_name = arel
444
+ end
230
445
  end
231
- # end work around 1
232
446
 
233
- # allow calculate to work when including a virtual attribute
447
+ # allow calculate to work with includes and a virtual attribute
234
448
  real = without_virtual_includes
235
449
  return super if real.equal?(self)
236
450
 
@@ -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 calculate 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,18 +24,46 @@ 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_aggregate_attribute(name, relation, :count, Arel.star, options)
29
+ define_method(name) { (has_attribute?(name) ? self[name] : send(relation).try(:size)) || 0 }
30
+ end
31
+
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
54
+ #
55
+ # example:
33
56
  #
34
57
  # class Hardware
35
58
  # has_many :disks
36
- # virtual_aggregate :allocated_disk_storage, :disks, :sum, :size
59
+ # virtual_sum :allocated_disk_storage, :disks, :size
37
60
  # end
38
61
  #
39
62
  # generates:
40
63
  #
41
64
  # def allocated_disk_storage
42
65
  # if disks.loaded?
43
- # disks.blank? ? nil : disks.map { |t| t.size.to_i }.sum
66
+ # disks.map(&:size).compact.sum
44
67
  # else
45
68
  # disks.sum(:size) || 0
46
69
  # end
@@ -51,7 +74,11 @@ module VirtualAttributes
51
74
  # # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
52
75
 
53
76
  def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
54
- define_virtual_aggregate_method(name, relation, method_name, column)
77
+ return virtual_total(name, relation, options) if method_name == :size
78
+ return virtual_sum(name, relation, column, options) if method_name == :sum
79
+ end
80
+
81
+ def define_virtual_aggregate_attribute(name, relation, method_name, column, options)
55
82
  reflection = reflect_on_association(relation)
56
83
 
57
84
  if options.key?(:arel)
@@ -69,54 +96,59 @@ module VirtualAttributes
69
96
  end
70
97
  end
71
98
 
72
- 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
99
+ def define_virtual_aggregate_method(name, relation, column, ruby_method_name, arel_method_name = ruby_method_name)
100
+ define_method(name) do
101
+ if has_attribute?(name)
102
+ self[name] || 0
103
+ elsif (rel = send(relation)).loaded?
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
110
+ else
111
+ rel.try(arel_method_name, column) || 0
90
112
  end
91
113
  end
92
114
  end
93
115
 
94
116
  def virtual_aggregate_arel(reflection, method_name, column)
95
- return unless reflection && reflection.macro == :has_many && !reflection.options[:through]
117
+ return unless reflection && reflection.macro == :has_many
118
+
119
+ # need db access for the reflection join_keys, so delaying all this key lookup until call time
96
120
  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]))
121
+ # strings and symbols are converted across, arel objects are not
122
+ column = reflection.klass.arel_attribute(column) unless column.respond_to?(:count)
123
+
124
+ # query: SELECT COUNT(*) FROM main_table JOIN foreign_table ON main_table.id = foreign_table.id JOIN ...
125
+ relation_query = joins(reflection.name).select(column.send(method_name))
126
+ query = relation_query.arel
127
+
128
+ # algorithm:
129
+ # - remove main_table from this sub query. (it is already in the primary query)
130
+ # - move the foreign_table from the JOIN to the FROM clause
131
+ # - move the main_table.id = foreign_table.id from the ON clause to the WHERE clause
132
+
133
+ # query: SELECT COUNT(*) FROM main_table [ ] JOIN ...
134
+ join = query.source.right.shift
135
+ # query: SELECT COUNT(*) FROM [foreign_table] JOIN ...
136
+ query.source.left = join.left
137
+ # query: SELECT COUNT(*) FROM foreign_table JOIN ... [WHERE main_table.id = foreign_table.id]
138
+ query.where(join.right.expr)
111
139
 
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)
140
+ # convert bind variables from ? to actual values. otherwise, sql is incomplete
141
+ conn = connection
142
+ sql = if ActiveRecord.version.to_s >= "5.2"
143
+ conn.unprepared_statement { conn.to_sql(query) }
144
+ else
145
+ conn.unprepared_statement { conn.to_sql(query, relation_query.bound_attributes) }
146
+ end
118
147
 
119
- t.grouping(Arel::Nodes::SqlLiteral.new(query.to_sql))
148
+ # add () around query
149
+ query = t.grouping(Arel::Nodes::SqlLiteral.new(sql))
150
+ # add coalesce to ensure correct value comes out
151
+ t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [query, Arel::Nodes::SqlLiteral.new("0")]))
120
152
  end
121
153
  end
122
154
  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.3.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keenan Brock
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-06 00:00:00.000000000 Z
11
+ date: 2020-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: db-query-matchers
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rake
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -104,9 +132,10 @@ files:
104
132
  - activerecord-virtual_attributes.gemspec
105
133
  - bin/console
106
134
  - bin/setup
107
- - gemfiles/virtual_attributes_50.gemfile
108
- - gemfiles/virtual_attributes_51.gemfile
109
- - gemfiles/virtual_attributes_52.gemfile
135
+ - gemfiles/gemfile_50.gemfile
136
+ - gemfiles/gemfile_51.gemfile
137
+ - gemfiles/gemfile_52.gemfile
138
+ - gemfiles/gemfile_60.gemfile
110
139
  - init.rb
111
140
  - lib/active_record-virtual_attributes.rb
112
141
  - lib/active_record/virtual_attributes.rb
@@ -128,7 +157,7 @@ metadata:
128
157
  homepage_uri: https://github.com/ManageIQ/activerecord-virtual_attributes
129
158
  source_code_uri: https://github.com/ManageIQ/activerecord-virtual_attributes
130
159
  changelog_uri: https://github.com/ManageIQ/activerecord-virtual_attributes/blob/master/CHANGELOG.md
131
- post_install_message:
160
+ post_install_message:
132
161
  rdoc_options: []
133
162
  require_paths:
134
163
  - lib
@@ -143,9 +172,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
172
  - !ruby/object:Gem::Version
144
173
  version: '0'
145
174
  requirements: []
146
- rubyforge_project:
175
+ rubyforge_project:
147
176
  rubygems_version: 2.7.6.2
148
- signing_key:
177
+ signing_key:
149
178
  specification_version: 4
150
179
  summary: Access non-sql attributes from sql
151
180
  test_files: []