activerecord-virtual_attributes 6.1.1 → 7.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: 8a35d6c7665469341b76ae68e1a84180ccef887c47f56b7000efc2f0024d0a93
4
- data.tar.gz: ba57d73a5034e461d3c6bd77a50eb5475d1fe0dab8c3332f91142c893fa736a2
3
+ metadata.gz: cf8f363320713146ad286d76999f5640a9fc44f3e139f6e5377ac8220e476217
4
+ data.tar.gz: cbabad277bfef0e01da4d14b8942671770eb283c9692ef9eb7a07cbac5f25b68
5
5
  SHA512:
6
- metadata.gz: 55f8e509034dc3a13dae283733da176c308d25538c38595cebfd98008d07be372c0843752e547be6afb5af361fd1610900f05fc52cdcdfbd8bff834b66233060
7
- data.tar.gz: c4511ba748f911a985f308bdfa61f9d6c64c57ec1245003dea509205f3aa4e0085f20f7e9adb5674d3657d750c34e6c4d64d41ee5faebb18846c9e8ef3ea4c32
6
+ metadata.gz: 10295799b997217903b155c7ed84e9ee82bad2cf6c208b6aa140dc94e66df9b4ed8fbcf13418a2feadf075e7f93b2c550d7a94b698aae0c08b6d29a4d5ebe159
7
+ data.tar.gz: 1663fd7ab8af93e3419b6a1ae608c94d55df957b45d904e38b31a3b5f93667159e6ee10e667f560174b12cc2b66e936e20bcadc4ac98a45953ed3923f21cac94
data/.codeclimate.yml CHANGED
@@ -31,4 +31,4 @@ plugins:
31
31
  rubocop:
32
32
  enabled: true
33
33
  config: ".rubocop_cc.yml"
34
- channel: rubocop-0-82
34
+ channel: rubocop-1-56-3
@@ -13,6 +13,8 @@ jobs:
13
13
  matrix:
14
14
  ruby-version:
15
15
  - '2.7'
16
+ - '3.0'
17
+ - '3.1'
16
18
  services:
17
19
  postgres:
18
20
  image: postgres:13
@@ -23,7 +25,7 @@ jobs:
23
25
  ports:
24
26
  - 5432:5432
25
27
  mysql:
26
- image: mysql:8.0
28
+ image: mysql:8.4
27
29
  env:
28
30
  MYSQL_ROOT_PASSWORD: password
29
31
  MYSQL_DATABASE: virtual_attributes
@@ -41,7 +43,7 @@ jobs:
41
43
  MYSQL_HOST: 127.0.0.1
42
44
  MYSQL_PWD: password
43
45
  steps:
44
- - uses: actions/checkout@v2
46
+ - uses: actions/checkout@v4
45
47
  - name: Set up Ruby
46
48
  uses: ruby/setup-ruby@v1
47
49
  with:
@@ -54,7 +56,7 @@ jobs:
54
56
  run: bundle exec rake
55
57
  - name: Run PostgreSQL tests
56
58
  env:
57
- DB: pg
59
+ DB: postgresql
58
60
  COLLATE_SYMBOLS: false
59
61
  run: bundle exec rake
60
62
  - name: Run MySQL tests
@@ -62,8 +64,8 @@ jobs:
62
64
  DB: mysql2
63
65
  run: bundle exec rake
64
66
  - name: Report code coverage
65
- if: ${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '2.7' }}
67
+ if: ${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '3.1' }}
66
68
  continue-on-error: true
67
- uses: paambaati/codeclimate-action@v3.0.0
69
+ uses: paambaati/codeclimate-action@v8
68
70
  env:
69
71
  CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
data/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ The versioning of this gem follows ActiveRecord versioning, and does not follow
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [7.0.0] - 2024-08-01
8
+
9
+ * Use Arel.literal [#154](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/154)
10
+ * drop attribute_builder [#153](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/153)
11
+ * dropped virtual_aggregate [#152](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/152)
12
+ * resolve rubocops (also fix bin/console) [#151](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/151)
13
+ * Rails 7.0 support / dropping 6.1 [#150](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/150)
14
+ * condense includes produced by replace_virtual_fields [#149](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/149)
15
+ * fix bin/console [#148](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/148)
16
+ * Rails 7.0 support pt1 [#146](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/146)
17
+ * Fix sqlite3 v2 and rails [#140](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/140)
18
+ * Use custom Arel node [#114](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/114)
19
+
20
+ ## [6.1.2] - 2023-10-26
21
+
22
+ * Fix bind variables for joins with static strings [#124](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/124)
23
+ * Add `virtual_total` for `habtm` [#123](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/123)
24
+ * Fix: `:uses` clause now works with an array and nested hashes. [#120](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/120)
25
+ * Uses symbols in the `includes()` clause. defined by `virtual_attribute :uses` and virtual_delegate. [#128](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/128)
26
+
7
27
  ## [6.1.1] - 2022-08-09
8
28
 
9
29
  * fix HomogeneousIn clauses [#111](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/111)
@@ -86,7 +106,9 @@ The versioning of this gem follows ActiveRecord versioning, and does not follow
86
106
  * Initial Release
87
107
  * Extracted from ManageIQ/manageiq
88
108
 
89
- [Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v6.1.1...HEAD
109
+ [Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.0.0...HEAD
110
+ [7.0.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v6.1.2...v7.0.0
111
+ [6.1.2]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v6.1.1...v6.1.2
90
112
  [6.1.1]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v6.1.0...v6.1.1
91
113
  [6.1.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v3.0.0...v6.1.0
92
114
  [3.0.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v2.0.0...v3.0.0
data/Gemfile CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "~> 6.1.4"
5
+ gem "activerecord", "~>7.0.8"
6
6
  gem "mysql2"
7
7
  gem "pg"
8
- gem "sqlite3"
8
+ gem "sqlite3", "< 2"
9
9
 
10
10
  gemspec
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # VirtualAttributes
2
2
 
3
3
  [![CI](https://github.com/ManageIQ/activerecord-virtual_attributes/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/activerecord-virtual_attributes/actions/workflows/ci.yaml)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/e1a0c26941c00f4edb55/maintainability)](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/e1a0c26941c00f4edb55/test_coverage)](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes/test_coverage)
4
+ [![Maintainability](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes.svg)](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes/maintainability)
5
+ [![Test Coverage](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes/coverage.svg)](https://codeclimate.com/github/ManageIQ/activerecord-virtual_attributes/test_coverage)
6
6
 
7
7
  VirtualAttributes allows you to define a ruby method that acts like an attribute or relation.
8
8
 
@@ -40,14 +40,15 @@ TODO: Write usage instructions here
40
40
 
41
41
  ## Development
42
42
 
43
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
43
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
44
44
 
45
45
  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).
46
46
 
47
+ To test with different database adapters, set the DB environment variable:
47
48
 
48
- To test with different versions of ruby, use `wwtd` gem or
49
-
50
- DB=pg BUNDLE_GEMFILE=gemfiles/gemfile_${version-52}.gemfile bundle exec rake
49
+ DB=postgresql bundle exec rake
50
+ DB=mysql bundle exec rake
51
+ DB=sqlite3 bundle exec rake
51
52
 
52
53
  ## Contributing
53
54
 
@@ -56,4 +57,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Manage
56
57
  ## License
57
58
 
58
59
  This project is available as open source under the terms of the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
59
-
@@ -9,28 +9,35 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Keenan Brock"]
10
10
  spec.email = ["keenan@thebrocks.net"]
11
11
 
12
- spec.summary = %q{Access non-sql attributes from sql}
13
- spec.description = %q{Define attributes in arel}
12
+ spec.summary = "Access non-sql attributes from sql"
13
+ spec.description = "Define attributes in arel"
14
14
  spec.homepage = "https://github.com/ManageIQ/activerecord-virtual_attributes"
15
15
  spec.license = "Apache 2.0"
16
- spec.metadata["homepage_uri"] = spec.homepage
17
- spec.metadata["source_code_uri"] = "https://github.com/ManageIQ/activerecord-virtual_attributes"
18
- spec.metadata["changelog_uri"] = "https://github.com/ManageIQ/activerecord-virtual_attributes/blob/master/CHANGELOG.md"
16
+ spec.metadata = {
17
+ "homepage_uri" => spec.homepage,
18
+ "source_code_uri" => "https://github.com/ManageIQ/activerecord-virtual_attributes",
19
+ "changelog_uri" => "https://github.com/ManageIQ/activerecord-virtual_attributes/blob/master/CHANGELOG.md",
20
+ "rubygems_mfa_required" => "true"
21
+ }
19
22
 
20
23
  # Specify which files should be added to the gem when it is released.
21
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
26
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
27
  end
25
28
 
26
29
  spec.require_paths = ["lib"]
27
30
 
28
- spec.add_runtime_dependency "activerecord", "~> 6.1.0"
31
+ spec.add_runtime_dependency "activerecord", "~> 7.0"
29
32
 
30
33
  spec.add_development_dependency "byebug"
34
+ spec.add_development_dependency "database_cleaner-active_record", "~> 2.1"
31
35
  spec.add_development_dependency "db-query-matchers"
32
36
  spec.add_development_dependency "manageiq-style"
37
+ spec.add_development_dependency "mysql2"
38
+ spec.add_development_dependency "pg"
33
39
  spec.add_development_dependency "rake", "~> 13.0"
34
40
  spec.add_development_dependency "rspec", "~> 3.0"
35
41
  spec.add_development_dependency "simplecov", ">= 0.21.2"
42
+ spec.add_development_dependency "sqlite3"
36
43
  end
data/bin/console CHANGED
@@ -3,9 +3,12 @@
3
3
  require "bundler/setup"
4
4
  require "active_record-virtual_attributes"
5
5
 
6
+ # any helper that is not rspec specific
7
+ Dir['./spec/support/**/*.rb'].sort.select { |f| !File.read(f).include?("RSpec") }.each { |f| require f }
8
+
6
9
  # models for local testing
7
- require "rspec"
8
- Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
10
+ Database.new.setup.migrate
11
+ require_relative "../seed"
9
12
 
10
13
  require "irb"
11
14
  IRB.start(__FILE__)
data/bin/setup CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
  IFS=$'\n\t'
4
- set -vx
4
+ set -e
5
5
 
6
6
  bundle install
7
+ echo
7
8
 
8
- # Do any other automated setup that you need to do here
9
+ echo "Setting up the postgres database for specs..."
10
+ echo "SELECT 'CREATE DATABASE virtual_attributes' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'virtual_attributes')\gexec" | psql -U postgres
11
+
12
+ echo "Setting up the mysql database for specs..."
13
+ mysql -u root -e 'CREATE SCHEMA IF NOT EXISTS 'virtual_attributes';'
@@ -1,4 +1,4 @@
1
- RSpec::Matchers.define :have_virtual_attribute do |name, type|
1
+ 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
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module VirtualAttributes
3
- VERSION = "6.1.1".freeze
3
+ VERSION = "7.0.0".freeze
4
4
  end
5
5
  end
@@ -10,8 +10,13 @@ module ActiveRecord
10
10
  # Model.select(Model.arel_table.grouping(Model.arel_table[:field2]).as(:field))
11
11
  # Model.attribute_supported_by_sql?(:field) # => true
12
12
 
13
- # in essence, this is our Arel::Nodes::VirtualAttribute
14
- class Arel::Nodes::Grouping
13
+ class VirtualAttribute < Arel::Nodes::Grouping
14
+ def initialize(arel, name = nil, relation = nil)
15
+ super(arel)
16
+ @name = name
17
+ @relation = relation
18
+ end
19
+
15
20
  attr_accessor :name, :relation
16
21
 
17
22
  # methods from Arel::Nodes::Attribute
@@ -36,10 +41,8 @@ module ActiveRecord
36
41
  end
37
42
 
38
43
  module VirtualArel
39
- # This arel table proxy is our shim to get our functionality into rails
44
+ # This arel table proxy. This allows WHERE clauses to use virtual attributes
40
45
  class ArelTableProxy < Arel::Table
41
- attr_accessor :klass
42
-
43
46
  # overrides Arel::Table#[]
44
47
  # adds aliases and virtual attribute arel (aka sql)
45
48
  #
@@ -70,16 +73,9 @@ module ActiveRecord
70
73
  end
71
74
 
72
75
  module ClassMethods
73
- if ActiveRecord.version.to_s < "6.1"
74
- # ActiveRecord::Core 6.0 (every version of active record seems to do this differently)
75
- def arel_table
76
- @arel_table ||= ArelTableProxy.new(table_name, :type_caster => type_caster).tap { |t| t.klass = self }
77
- end
78
- else
79
- # ActiveRecord::Core 6.1
80
- def arel_table
81
- @arel_table ||= ArelTableProxy.new(table_name, :klass => self)
82
- end
76
+ # ActiveRecord::Core 6.1
77
+ def arel_table
78
+ @arel_table ||= ArelTableProxy.new(table_name, :klass => self)
83
79
  end
84
80
 
85
81
  # supported by sql if any are true:
@@ -104,10 +100,12 @@ module ActiveRecord
104
100
  return unless arel_lambda
105
101
 
106
102
  arel = arel_lambda.call(table)
107
- arel = Arel::Nodes::Grouping.new(arel) unless arel.kind_of?(Arel::Nodes::Grouping)
108
- arel.name = column_name
109
- arel.relation = table
110
- arel
103
+ # By convention, all attributes are defined with a grouping.
104
+ # Since we're adding a VirtualAttribute node, which is essentially a
105
+ # grouping, there is no need to keep both and end up with double parens
106
+ arel = arel.expr if arel.kind_of?(Arel::Nodes::Grouping)
107
+
108
+ VirtualAttribute.new(arel, column_name, table)
111
109
  end
112
110
 
113
111
  private
@@ -120,6 +118,8 @@ module ActiveRecord
120
118
  end
121
119
  end
122
120
 
121
+ # fixed in https://github.com/rails/rails/pull/45642
122
+ if ActiveRecord.version < Gem::Version.new(7.1)
123
123
  module Arel # :nodoc: all
124
124
  # rubocop:disable Naming/MethodName
125
125
  # rubocop:disable Naming/MethodParameterName
@@ -159,3 +159,4 @@ module Arel # :nodoc: all
159
159
  # rubocop:enable Naming/MethodParameterName
160
160
  # rubocop:enable Style/ConditionalAssignment
161
161
  end
162
+ end
@@ -43,7 +43,7 @@ module ActiveRecord
43
43
  method_prefix = virtual_delegate_name_prefix(options[:prefix], to)
44
44
  method_name = "#{method_prefix}#{method}"
45
45
  if to.include?(".") # to => "target.method"
46
- to, method = to.split(".")
46
+ to, method = to.split(".").map(&:to_sym)
47
47
  options[:to] = to
48
48
  end
49
49
 
@@ -74,12 +74,13 @@ module ActiveRecord
74
74
  type = options[:type] || to_ref.klass.type_for_attribute(col)
75
75
  type = ActiveRecord::Type.lookup(type) if type.kind_of?(Symbol)
76
76
  raise "unknown attribute #{to}##{col} referenced in #{name}" unless type
77
+
77
78
  arel = virtual_delegate_arel(col, to_ref)
78
79
  define_virtual_attribute(method_name, type, :uses => (options[:uses] || to), :arel => arel)
79
80
  end
80
81
 
81
82
  # see activesupport module/delegation.rb
82
- def define_delegate(method_name, method, to: nil, allow_nil: nil, default: nil)
83
+ def define_delegate(method_name, method, to: nil, allow_nil: nil, default: nil) # rubocop:disable Naming/MethodParameterName
83
84
  location = caller_locations(2, 1).first
84
85
  file, line = location.path, location.lineno
85
86
 
@@ -125,7 +126,7 @@ module ActiveRecord
125
126
  module_eval(method_def, file, line)
126
127
  end
127
128
 
128
- def virtual_delegate_name_prefix(prefix, to)
129
+ def virtual_delegate_name_prefix(prefix, to) # rubocop:disable Naming/MethodParameterName
129
130
  "#{prefix == true ? to : prefix}_" if prefix
130
131
  end
131
132
 
@@ -260,8 +261,7 @@ module ActiveRecord
260
261
 
261
262
  yield arel if block_given?
262
263
 
263
- # convert arel to sql to populate with bind variables
264
- ::Arel::Nodes::Grouping.new(Arel.sql(arel.to_sql))
264
+ arel
265
265
  end
266
266
 
267
267
  # determine table reference to use for a sub query
@@ -5,6 +5,7 @@ module ActiveRecord
5
5
  include ActiveRecord::VirtualAttributes
6
6
  include ActiveRecord::VirtualAttributes::VirtualReflections
7
7
 
8
+ # rubocop:disable Style/SingleLineMethods
8
9
  module NonARModels
9
10
  def dangerous_attribute_method?(_); false; end
10
11
 
@@ -14,6 +15,7 @@ module ActiveRecord
14
15
 
15
16
  def belongs_to_required_by_default; false; end
16
17
  end
18
+ # rubocop:enable Style/SingleLineMethods
17
19
 
18
20
  included do
19
21
  unless respond_to?(:dangerous_attribute_method?)
@@ -31,18 +33,20 @@ module ActiveRecord
31
33
  end
32
34
 
33
35
  def replace_virtual_fields(associations)
34
- return associations if associations.blank?
35
-
36
- case associations
37
- when String, Symbol
38
- virtual_field?(associations) ? replace_virtual_fields(virtual_includes(associations)) : associations
39
- when Array
40
- associations.collect { |association| replace_virtual_fields(association) }.compact
41
- when Hash
42
- replace_virtual_field_hash(associations)
43
- else
44
- associations
45
- end
36
+ return nil if associations.blank?
37
+
38
+ ret =
39
+ case associations
40
+ when String, Symbol
41
+ virtual_field?(associations) ? replace_virtual_fields(virtual_includes(associations)) : associations.to_sym
42
+ when Array
43
+ associations.filter_map { |association| replace_virtual_fields(association) }
44
+ when Hash
45
+ replace_virtual_field_hash(associations)
46
+ else
47
+ associations
48
+ end
49
+ simplify_includes(ret)
46
50
  end
47
51
 
48
52
  def replace_virtual_field_hash(associations)
@@ -67,9 +71,11 @@ module ActiveRecord
67
71
  def include_to_hash(value)
68
72
  case value
69
73
  when String, Symbol
70
- {value => {}}
74
+ {value.to_sym => {}}
71
75
  when Array
72
- value.flatten.each_with_object({}) { |k, h| h[k] = {} }
76
+ value.flatten.each_with_object({}) do |k, h|
77
+ merge_includes(h, k)
78
+ end
73
79
  when nil
74
80
  {}
75
81
  else
@@ -77,15 +83,33 @@ module ActiveRecord
77
83
  end
78
84
  end
79
85
 
80
- # @param [Hash] hash1
81
- # @param [Hash] hash2
86
+ # @param [Hash] hash1 (incoming hash is modified and returned)
87
+ # @param [Hash|Symbol|nil] hash2 (this hash will not be modified)
82
88
  def merge_includes(hash1, hash2)
83
89
  return hash1 if hash2.blank?
84
90
 
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)
91
+ # very common case.
92
+ # optimization to skip deep_merge and hash creation
93
+ if hash2.kind_of?(Symbol)
94
+ hash1[hash2] ||= {}
95
+ return hash1
96
+ end
97
+
98
+ hash1.deep_merge!(include_to_hash(hash2)) do |_k, v1, v2|
99
+ # this block is conflict resolution when a key has 2 values
100
+ merge_includes(include_to_hash(v1), v2)
101
+ end
102
+ end
103
+
104
+ # @param [Hash|Array|Symbol|nil]
105
+ def simplify_includes(ret)
106
+ case ret
107
+ when Hash
108
+ ret.size <= 1 && ret.values.first.blank? ? ret.keys.first : ret
109
+ when Array
110
+ ret.size <= 1 ? ret.first : ret
111
+ else
112
+ ret
89
113
  end
90
114
  end
91
115
  end
@@ -93,6 +117,25 @@ module ActiveRecord
93
117
  end
94
118
  end
95
119
 
120
+ def assert_klass_has_instance_method(klass, instance_method)
121
+ klass.instance_method(instance_method)
122
+ rescue NameError => err
123
+ msg = "#{klass} is missing the method our prepended code is expecting to patch. Was the undefined method removed or renamed upstream?\nSee: #{__FILE__}.\nThe NameError was: #{err}. "
124
+ raise NameError, msg
125
+ end
126
+
127
+ # Expect these methods to exist. (Otherwise we are patching the wrong methods)
128
+ %w[
129
+ grouped_records
130
+ preloaders_for_reflection
131
+ ].each { |method| assert_klass_has_instance_method(ActiveRecord::Associations::Preloader::Branch, method) }
132
+
133
+ %w[
134
+ build_select
135
+ arel_column
136
+ construct_join_dependency
137
+ ].each { |method| assert_klass_has_instance_method(ActiveRecord::Relation, method) }
138
+
96
139
  module ActiveRecord
97
140
  class Base
98
141
  include ActiveRecord::VirtualAttributes::VirtualFields
@@ -101,105 +144,81 @@ module ActiveRecord
101
144
  module Associations
102
145
  class Preloader
103
146
  prepend(Module.new {
104
- # preloader.rb active record 6.0
105
- # changed:
106
- # since grouped_records can return a hash/array, we need to handle those 2 new cases
107
- def preloaders_for_reflection(reflection, records, scope, polymorphic_parent)
108
- case reflection
109
- when Array
110
- reflection.flat_map { |ref| preloaders_on(ref, records, scope, polymorphic_parent) }
111
- when Hash
112
- preloaders_on(reflection, records, scope, polymorphic_parent)
113
- else
114
- super(reflection, records, scope)
147
+ # preloader is called with virtual attributes - need to resolve
148
+ def call
149
+ # Possibly overkill since all records probably have the same class and associations
150
+ # use a cache so we only convert includes once per base class
151
+ assoc_cache = Hash.new { |h, klass| h[klass] = klass.replace_virtual_fields(associations) }
152
+
153
+ # convert the includes with virtual attributes to includes with proper associations
154
+ records_by_assoc = records.group_by { |rec| assoc_cache[rec.class] }
155
+ # if these are the same includes, then do the preloader work
156
+ return super if records_by_assoc.size == 1 && records_by_assoc.keys.first == associations
157
+
158
+ # for each of the associations, run a preloader
159
+ records_by_assoc.each do |klass_associations, klass_records|
160
+ next if klass_associations.blank?
161
+
162
+ Array[klass_associations].each do |klass_association| # rubocop:disable Style/RedundantArrayConstructor
163
+ # this calls back into itself, but it will take the short circuit
164
+ Preloader.new(:records => klass_records, :associations => klass_association, :scope => scope).call
165
+ end
115
166
  end
116
167
  end
168
+ })
117
169
 
118
- # rubocop:disable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
119
- # preloader.rb active record 6.0
120
- # changed:
121
- # passing polymorphic around (and makes 5.2 more similar to 6.0)
122
- def preloaders_for_hash(association, records, scope, polymorphic_parent)
123
- association.flat_map { |parent, child|
124
- grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
125
- loaders = preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
126
- recs = loaders.flat_map(&:preloaded_records).uniq
127
- child_polymorphic_parent = reflection && reflection.respond_to?(:options) && reflection.options[:polymorphic]
128
- loaders.concat Array.wrap(child).flat_map { |assoc|
129
- preloaders_on assoc, recs, scope, child_polymorphic_parent
130
- }
131
- loaders
132
- end
133
- }
134
- end
170
+ class Branch
171
+ prepend(Module.new {
172
+ # from branched.rb 7.0
173
+ def grouped_records
174
+ h = {}
175
+ polymorphic_parent = !root? && parent.polymorphic?
176
+ source_records.each do |record|
177
+ # begin virtual_attributes changes
178
+ association = record.class.replace_virtual_fields(self.association)
179
+ # end virtual_attributes changes
135
180
 
136
- # preloader.rb active record 6.0
137
- # changed:
138
- # passing polymorphic_parent to preloaders_for_reflection
139
- def preloaders_for_one(association, records, scope, polymorphic_parent)
140
- grouped_records(association, records, polymorphic_parent)
141
- .flat_map do |reflection, reflection_records|
142
- preloaders_for_reflection(reflection, reflection_records, scope, polymorphic_parent)
181
+ reflection = record.class._reflect_on_association(association)
182
+ next if polymorphic_parent && !reflection || !record.association(association).klass
183
+ (h[reflection] ||= []) << record
143
184
  end
144
- end
185
+ h
186
+ end
145
187
 
146
- # preloader.rb active record 6.0, 6.1
147
- def grouped_records(orig_association, records, polymorphic_parent)
148
- h = {}
149
- records.each do |record|
150
- # The virtual_field lookup can return Symbol/Nil/Other (typically a Hash)
151
- # so the case statement and the cases for Nil/Other are new
188
+ # branched.rb 7.0
189
+ def preloaders_for_reflection(reflection, reflection_records)
190
+ reflection_records.group_by do |record|
191
+ # begin virtual_attributes changes
192
+ needed_association = record.class.replace_virtual_fields(association)
193
+ # end virtual_attributes changes
152
194
 
153
- # each class can resolve virtual_{attributes,includes} differently
154
- association = record.class.replace_virtual_fields(orig_association)
155
- # 1 line optimization for single element array:
156
- association = association.first if association.kind_of?(Array) && association.size == 1
195
+ klass = record.association(needed_association).klass
157
196
 
158
- case association
159
- when Symbol, String
160
- reflection = record.class._reflect_on_association(association)
161
- next if polymorphic_parent && !reflection || !record.association(association).klass
162
- when nil
163
- next
164
- else # need parent (preloaders_for_{hash,one}) to handle this Array/Hash
165
- reflection = association
197
+ if reflection.scope && reflection.scope.arity != 0
198
+ # For instance dependent scopes, the scope is potentially
199
+ # different for each record. To allow this we'll group each
200
+ # object separately into its own preloader
201
+ reflection_scope = reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass, record).inject(&:merge!)
202
+ end
203
+
204
+ [klass, reflection_scope]
205
+ end.map do |(rhs_klass, reflection_scope), rs|
206
+ preloader_for(reflection).new(rhs_klass, rs, reflection, scope, reflection_scope, associate_by_default)
166
207
  end
167
- (h[reflection] ||= []) << record
168
208
  end
169
- h
170
- end
171
- # rubocop:enable Style/BlockDelimiters, Lint/AmbiguousBlockAssociation, Style/MethodCallWithArgsParentheses
172
- })
209
+ })
210
+ end
173
211
  end
174
212
  end
175
213
 
176
214
  class Relation
177
- def without_virtual_includes
178
- filtered_includes = includes_values && klass.replace_virtual_fields(includes_values)
179
- if filtered_includes != includes_values
180
- spawn.tap { |other| other.includes_values = filtered_includes }
181
- else
182
- self
183
- end
184
- end
185
-
186
215
  include(Module.new {
187
- # From ActiveRecord::FinderMethods
188
- def apply_join_dependency(*args, **kargs, &block)
189
- real = without_virtual_includes
190
- if real.equal?(self)
191
- super
192
- else
193
- real.apply_join_dependency(*args, **kargs, &block)
194
- end
195
- end
196
-
197
216
  # From ActiveRecord::QueryMethods (rails 5.2 - 6.1)
198
217
  def build_select(arel)
199
218
  if select_values.any?
200
- cols = arel_columns(select_values.uniq).map do |col|
219
+ cols = arel_columns(select_values).map do |col|
201
220
  # if it is a virtual attribute, then add aliases to those columns
202
- if col.kind_of?(Arel::Nodes::Grouping) && col.name
221
+ if col.kind_of?(VirtualAttributes::VirtualAttribute)
203
222
  col.as(connection.quote_column_name(col.name))
204
223
  else
205
224
  col
@@ -211,9 +230,8 @@ module ActiveRecord
211
230
  end
212
231
  end
213
232
 
214
- # from ActiveRecord::QueryMethods (rails 5.2 - 6.0)
215
- # TODO: remove from rails 7.0
216
- def arel_column(field, &block)
233
+ # from ActiveRecord::QueryMethods (rails 5.2 - 7.0)
234
+ def arel_column(field)
217
235
  if virtual_attribute?(field) && (arel = table[field])
218
236
  arel
219
237
  else
@@ -222,19 +240,9 @@ module ActiveRecord
222
240
  end
223
241
 
224
242
  def construct_join_dependency(associations, join_type) # :nodoc:
225
- associations = klass.replace_virtual_fields(associations)
243
+ associations = klass.replace_virtual_fields(associations) || {}
226
244
  super
227
245
  end
228
-
229
- # From ActiveRecord::Calculations
230
- # introduces virtual includes support for calculate (we mostly use COUNT(*))
231
- def calculate(operation, attribute_name)
232
- # allow calculate to work with includes and a virtual attribute
233
- real = without_virtual_includes
234
- return super if real.equal?(self)
235
-
236
- real.calculate(operation, attribute_name)
237
- end
238
246
  })
239
247
  end
240
248
  end
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  end
18
18
 
19
19
  def virtual_has_many(name, options = {})
20
- define_method("#{name.to_s.singularize}_ids") do
20
+ define_method(:"#{name.to_s.singularize}_ids") do
21
21
  records = send(name)
22
22
  records.respond_to?(:ids) ? records.ids : records.collect(&:id)
23
23
  end
@@ -57,11 +57,11 @@ module ActiveRecord
57
57
  end
58
58
 
59
59
  def follow_associations(association_names)
60
- association_names.inject(self) { |klass, name| klass.try!(:reflect_on_association, name).try!(:klass) }
60
+ association_names.inject(self) { |klass, name| klass&.reflect_on_association(name)&.klass }
61
61
  end
62
62
 
63
63
  def follow_associations_with_virtual(association_names)
64
- association_names.inject(self) { |klass, name| klass.try!(:reflection_with_virtual, name).try!(:klass) }
64
+ association_names.inject(self) { |klass, name| klass&.reflection_with_virtual(name)&.klass }
65
65
  end
66
66
 
67
67
  # invalid associations return a nil
@@ -97,6 +97,7 @@ module ActiveRecord
97
97
 
98
98
  def add_virtual_reflection(reflection, name, uses, _options)
99
99
  raise ArgumentError, "macro must be specified" unless reflection
100
+
100
101
  reset_virtual_reflection_information
101
102
  _virtual_reflections[name.to_sym] = reflection
102
103
  define_virtual_include(name.to_s, uses)
@@ -49,35 +49,6 @@ module VirtualAttributes
49
49
  define_virtual_aggregate_method(name, relation, column, :average) { |values| values.count == 0 ? 0 : values.sum / values.count }
50
50
  end
51
51
 
52
- # @param method_name
53
- # :count :average :minimum :maximum :sum
54
- #
55
- # example:
56
- #
57
- # class Hardware
58
- # has_many :disks
59
- # virtual_sum :allocated_disk_storage, :disks, :size
60
- # end
61
- #
62
- # generates:
63
- #
64
- # def allocated_disk_storage
65
- # if disks.loaded?
66
- # disks.map(&:size).compact.sum
67
- # else
68
- # disks.sum(:size) || 0
69
- # end
70
- # end
71
- #
72
- # virtual_attribute :allocated_disk_storage, :integer, :uses => :disks, :arel => ...
73
- #
74
- # # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
75
-
76
- def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
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
52
  def define_virtual_aggregate_attribute(name, relation, method_name, column, options)
82
53
  reflection = reflect_on_association(relation)
83
54
 
@@ -101,7 +72,7 @@ module VirtualAttributes
101
72
  if has_attribute?(name)
102
73
  self[name] || 0
103
74
  elsif (rel = send(relation)).loaded?
104
- values = rel.map { |t| t.send(column) }.compact
75
+ values = rel.filter_map { |t| t.send(column) }
105
76
  if block_given?
106
77
  yield values
107
78
  else
@@ -114,7 +85,7 @@ module VirtualAttributes
114
85
  end
115
86
 
116
87
  def virtual_aggregate_arel(reflection, method_name, column)
117
- return unless reflection && reflection.macro == :has_many
88
+ return unless reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
118
89
 
119
90
  # need db access for the reflection join_keys, so delaying all this key lookup until call time
120
91
  lambda do |t|
@@ -137,14 +108,8 @@ module VirtualAttributes
137
108
  # query: SELECT COUNT(*) FROM foreign_table JOIN ... [WHERE main_table.id = foreign_table.id]
138
109
  query.where(join.right.expr)
139
110
 
140
- # convert bind variables from ? to actual values. otherwise, sql is incomplete
141
- conn = connection
142
- sql = conn.unprepared_statement { conn.to_sql(query) }
143
-
144
- # add () around query
145
- query = t.grouping(Arel::Nodes::SqlLiteral.new(sql))
146
111
  # add coalesce to ensure correct value comes out
147
- t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [query, Arel::Nodes::SqlLiteral.new("0")]))
112
+ Arel::Nodes::NamedFunction.new('COALESCE', [t.grouping(query), Arel.sql("0")])
148
113
  end
149
114
  end
150
115
  end
@@ -83,14 +83,6 @@ module ActiveRecord
83
83
  end
84
84
  end
85
85
 
86
- def attributes_builder # :nodoc:
87
- unless defined?(@attributes_builder) && @attributes_builder
88
- defaults = _default_attributes.except(*(column_names - [primary_key]))
89
- @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
90
- end
91
- @attributes_builder
92
- end
93
-
94
86
  private
95
87
 
96
88
  def load_schema!
data/renovate.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ]
6
+ }
data/seed.rb ADDED
@@ -0,0 +1,3 @@
1
+ Author.create_with_books(3)
2
+ Author.create_with_books(4)
3
+ Author.create_with_books(2)
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: 6.1.1
4
+ version: 7.0.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: 2022-08-09 00:00:00.000000000 Z
11
+ date: 2024-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 6.1.0
19
+ version: '7.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 6.1.0
26
+ version: '7.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: byebug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: database_cleaner-active_record
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: db-query-matchers
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,34 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mysql2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pg
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
69
111
  - !ruby/object:Gem::Dependency
70
112
  name: rake
71
113
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +150,20 @@ dependencies:
108
150
  - - ">="
109
151
  - !ruby/object:Gem::Version
110
152
  version: 0.21.2
153
+ - !ruby/object:Gem::Dependency
154
+ name: sqlite3
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
111
167
  description: Define attributes in arel
112
168
  email:
113
169
  - keenan@thebrocks.net
@@ -145,6 +201,8 @@ files:
145
201
  - lib/active_record/virtual_attributes/virtual_reflections.rb
146
202
  - lib/active_record/virtual_attributes/virtual_total.rb
147
203
  - lib/activerecord-virtual_attributes.rb
204
+ - renovate.json
205
+ - seed.rb
148
206
  homepage: https://github.com/ManageIQ/activerecord-virtual_attributes
149
207
  licenses:
150
208
  - Apache 2.0
@@ -152,6 +210,7 @@ metadata:
152
210
  homepage_uri: https://github.com/ManageIQ/activerecord-virtual_attributes
153
211
  source_code_uri: https://github.com/ManageIQ/activerecord-virtual_attributes
154
212
  changelog_uri: https://github.com/ManageIQ/activerecord-virtual_attributes/blob/master/CHANGELOG.md
213
+ rubygems_mfa_required: 'true'
155
214
  post_install_message:
156
215
  rdoc_options: []
157
216
  require_paths:
@@ -167,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
226
  - !ruby/object:Gem::Version
168
227
  version: '0'
169
228
  requirements: []
170
- rubygems_version: 3.1.6
229
+ rubygems_version: 3.5.9
171
230
  signing_key:
172
231
  specification_version: 4
173
232
  summary: Access non-sql attributes from sql