activerecord-virtual_attributes 1.3.1 → 3.0.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: 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: []