activerecord-virtual_attributes 1.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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +27 -0
- data/Appraisals +18 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +202 -0
- data/README.md +46 -0
- data/Rakefile +8 -0
- data/activerecord-virtual_attributes.gemspec +33 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/virtual_attributes_50.gemfile +10 -0
- data/gemfiles/virtual_attributes_51.gemfile +10 -0
- data/gemfiles/virtual_attributes_52.gemfile +10 -0
- data/init.rb +1 -0
- data/lib/active_record-virtual_attributes.rb +1 -0
- data/lib/active_record/virtual_attributes.rb +143 -0
- data/lib/active_record/virtual_attributes/arel_groups.rb +14 -0
- data/lib/active_record/virtual_attributes/rspec.rb +1 -0
- data/lib/active_record/virtual_attributes/rspec/have_virtual_attribute.rb +44 -0
- data/lib/active_record/virtual_attributes/version.rb +5 -0
- data/lib/active_record/virtual_attributes/virtual_arel.rb +48 -0
- data/lib/active_record/virtual_attributes/virtual_delegates.rb +276 -0
- data/lib/active_record/virtual_attributes/virtual_fields.rb +234 -0
- data/lib/active_record/virtual_attributes/virtual_includes.rb +33 -0
- data/lib/active_record/virtual_attributes/virtual_reflections.rb +114 -0
- data/lib/active_record/virtual_attributes/virtual_total.rb +134 -0
- data/lib/activerecord-virtual_attributes.rb +1 -0
- metadata +132 -0
@@ -0,0 +1,234 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module VirtualAttributes
|
3
|
+
module VirtualFields
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ActiveRecord::VirtualAttributes
|
6
|
+
include ActiveRecord::VirtualAttributes::VirtualReflections
|
7
|
+
|
8
|
+
module NonARModels
|
9
|
+
def dangerous_attribute_method?(_); false; end
|
10
|
+
|
11
|
+
def generated_association_methods; self; end
|
12
|
+
|
13
|
+
def add_autosave_association_callbacks(*_args); self; end
|
14
|
+
|
15
|
+
def belongs_to_required_by_default; false; end
|
16
|
+
end
|
17
|
+
|
18
|
+
included do
|
19
|
+
unless respond_to?(:dangerous_attribute_method?)
|
20
|
+
extend NonARModels
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def virtual_fields_base?
|
26
|
+
!(superclass < VirtualFields)
|
27
|
+
end
|
28
|
+
|
29
|
+
def virtual_field?(name)
|
30
|
+
virtual_attribute?(name) || virtual_reflection?(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_virtual_fields(associations)
|
34
|
+
case associations
|
35
|
+
when String, Symbol
|
36
|
+
virtual_field?(associations) ? nil : associations
|
37
|
+
when Array
|
38
|
+
associations.collect { |association| remove_virtual_fields(association) }.compact
|
39
|
+
when Hash
|
40
|
+
associations.each_with_object({}) do |(parent, child), h|
|
41
|
+
next if virtual_field?(parent)
|
42
|
+
reflection = reflect_on_association(parent.to_sym)
|
43
|
+
h[parent] = reflection.options[:polymorphic] ? nil : reflection.klass.remove_virtual_fields(child) if reflection
|
44
|
+
end
|
45
|
+
else
|
46
|
+
associations
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module ActiveRecord
|
55
|
+
class Base
|
56
|
+
include ActiveRecord::VirtualAttributes::VirtualFields
|
57
|
+
end
|
58
|
+
|
59
|
+
module Associations
|
60
|
+
class Preloader
|
61
|
+
prepend Module.new {
|
62
|
+
def preloaders_for_one(association, records, scope)
|
63
|
+
klass_map = records.compact.group_by(&:class)
|
64
|
+
|
65
|
+
loaders = klass_map.keys.group_by { |klass| klass.virtual_includes(association) }.flat_map do |virtuals, klasses|
|
66
|
+
subset = klasses.flat_map { |klass| klass_map[klass] }
|
67
|
+
preload(subset, virtuals)
|
68
|
+
end
|
69
|
+
|
70
|
+
records_with_association = klass_map.select { |k, rs| k.reflect_on_association(association) }.flat_map { |k, rs| rs }
|
71
|
+
if records_with_association.any?
|
72
|
+
loaders.concat(super(association, records_with_association, scope))
|
73
|
+
end
|
74
|
+
|
75
|
+
loaders
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
# FIXME: Hopefully we can get this into Rails core so this is no longer
|
81
|
+
# required in our codebase, but the rule that are broken here are mostly
|
82
|
+
# due to the style of the Rails codebase conflicting with our own.
|
83
|
+
# Ignoring them to avoid noise in RuboCop, but allow us to keep the same
|
84
|
+
# syntax from the original codebase.
|
85
|
+
#
|
86
|
+
# rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
|
87
|
+
# rubocop:disable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
|
88
|
+
class JoinDependency
|
89
|
+
def instantiate(result_set, *_, &block)
|
90
|
+
primary_key = aliases.column_alias(join_root, join_root.primary_key)
|
91
|
+
|
92
|
+
seen = Hash.new { |i, object_id|
|
93
|
+
i[object_id] = Hash.new { |j, child_class|
|
94
|
+
j[child_class] = {}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
model_cache = Hash.new { |h,klass| h[klass] = {} }
|
99
|
+
parents = model_cache[join_root]
|
100
|
+
column_aliases = aliases.column_aliases(join_root)
|
101
|
+
|
102
|
+
# New Code
|
103
|
+
#
|
104
|
+
# This monkey patches the ActiveRecord::Associations::JoinDependency to
|
105
|
+
# include columns into the main record that might have been added
|
106
|
+
# through a `select` clause.
|
107
|
+
#
|
108
|
+
# This can be seen with the following:
|
109
|
+
#
|
110
|
+
# Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col)
|
111
|
+
# .includes(:tags => {}).references(:tags => {})
|
112
|
+
#
|
113
|
+
# Which will produce a SQL SELECT statement kind of like this:
|
114
|
+
#
|
115
|
+
# SELECT "vms".*,
|
116
|
+
# (<virtual_attribute_arel>) AS some_vm_virtual_col,
|
117
|
+
# "vms"."id" AS t0_r0
|
118
|
+
# "vms"."vendor" AS t0_r1
|
119
|
+
# "vms"."format" AS t0_r1
|
120
|
+
# "vms"."version" AS t0_r1
|
121
|
+
# ...
|
122
|
+
# "tags"."id" AS t1_r0
|
123
|
+
# "tags"."name" AS t1_r1
|
124
|
+
#
|
125
|
+
# This is because rails is trying to reduce the number of queries
|
126
|
+
# needed to fetch all of the records in the include, so it grabs the
|
127
|
+
# columns for both of the tables together to do it. Unfortuantely (or
|
128
|
+
# fortunately... depending on how you look at it), it does not remove
|
129
|
+
# any `.select` columns from the query that is run in the process, so
|
130
|
+
# that is brought along for the ride, but never used when this method
|
131
|
+
# instanciates the objects.
|
132
|
+
#
|
133
|
+
# The "New Code" here simply also instanciates any extra rows that
|
134
|
+
# might have been included in the select (virtual_columns) as well and
|
135
|
+
# brought back with the result set.
|
136
|
+
unless result_set.empty?
|
137
|
+
join_dep_keys = aliases.columns.map(&:right)
|
138
|
+
join_root_aliases = column_aliases.map(&:first)
|
139
|
+
additional_attributes = result_set.first.keys
|
140
|
+
.reject { |k| join_dep_keys.include?(k) }
|
141
|
+
.reject { |k| join_root_aliases.include?(k) }
|
142
|
+
.map { |k| [k, k] }
|
143
|
+
column_aliases += additional_attributes
|
144
|
+
end
|
145
|
+
# End of New Code
|
146
|
+
|
147
|
+
message_bus = ActiveSupport::Notifications.instrumenter
|
148
|
+
|
149
|
+
payload = {
|
150
|
+
record_count: result_set.length,
|
151
|
+
class_name: join_root.base_klass.name
|
152
|
+
}
|
153
|
+
|
154
|
+
message_bus.instrument('instantiation.active_record', payload) do
|
155
|
+
result_set.each { |row_hash|
|
156
|
+
parent_key = primary_key ? row_hash[primary_key] : row_hash
|
157
|
+
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
|
158
|
+
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
parents.values
|
163
|
+
end
|
164
|
+
# rubocop:enable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax
|
165
|
+
# rubocop:enable Layout/AlignHash, Metrics/AbcSize, Metrics/MethodLength
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Relation
|
170
|
+
def without_virtual_includes
|
171
|
+
filtered_includes = includes_values && klass.remove_virtual_fields(includes_values)
|
172
|
+
if filtered_includes != includes_values
|
173
|
+
spawn.tap { |other| other.includes_values = filtered_includes }
|
174
|
+
else
|
175
|
+
self
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
include(Module.new {
|
180
|
+
# From ActiveRecord::FinderMethods
|
181
|
+
def find_with_associations
|
182
|
+
real = without_virtual_includes
|
183
|
+
return super if real.equal?(self)
|
184
|
+
|
185
|
+
if ActiveRecord.version.to_s >= "5.1"
|
186
|
+
recs, join_dep = real.find_with_associations { |relation, join_dependency| [relation, join_dependency] }
|
187
|
+
else
|
188
|
+
recs = real.find_with_associations
|
189
|
+
end
|
190
|
+
MiqPreloader.preload(recs, preload_values + includes_values) if includes_values
|
191
|
+
|
192
|
+
# when 5.0 support is dropped, assume a block given
|
193
|
+
if block_given?
|
194
|
+
yield recs, join_dep
|
195
|
+
end
|
196
|
+
recs
|
197
|
+
end
|
198
|
+
|
199
|
+
# From ActiveRecord::QueryMethods
|
200
|
+
def select(*fields)
|
201
|
+
return super if block_given? || fields.empty?
|
202
|
+
# support virtual attributes by adding an alias to the sql phrase for the column
|
203
|
+
# it does not add an as() if the column already has an as
|
204
|
+
# this code is based upon _select()
|
205
|
+
fields.flatten!
|
206
|
+
fields.map! do |field|
|
207
|
+
if virtual_attribute?(field) && (arel = klass.arel_attribute(field)) && arel.respond_to?(:as)
|
208
|
+
arel.as(field.to_s)
|
209
|
+
else
|
210
|
+
field
|
211
|
+
end
|
212
|
+
end
|
213
|
+
# end support virtual attributes
|
214
|
+
super
|
215
|
+
end
|
216
|
+
|
217
|
+
# From ActiveRecord::Calculations
|
218
|
+
def calculate(operation, attribute_name)
|
219
|
+
# work around 1 until https://github.com/rails/rails/pull/25304 gets merged
|
220
|
+
# This allows attribute_name to be a virtual_attribute
|
221
|
+
if (arel = klass.arel_attribute(attribute_name)) && virtual_attribute?(attribute_name)
|
222
|
+
attribute_name = arel
|
223
|
+
end
|
224
|
+
# end work around 1
|
225
|
+
|
226
|
+
# allow calculate to work when including a virtual attribute
|
227
|
+
real = without_virtual_includes
|
228
|
+
return super if real.equal?(self)
|
229
|
+
|
230
|
+
real.calculate(operation, attribute_name)
|
231
|
+
end
|
232
|
+
})
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module VirtualAttributes
|
3
|
+
# VirtualIncludes associates an includes with an attribute
|
4
|
+
#
|
5
|
+
# Model.virtual_attribute :field, :string, :includes => :table
|
6
|
+
# Model.includes(:field)
|
7
|
+
#
|
8
|
+
# is equivalent to:
|
9
|
+
#
|
10
|
+
# Model.includes(:table)
|
11
|
+
module VirtualIncludes
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
class_attribute :_virtual_includes, :instance_accessor => false
|
16
|
+
self._virtual_includes = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def virtual_includes(name)
|
21
|
+
load_schema
|
22
|
+
_virtual_includes[name.to_s]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def define_virtual_include(name, uses)
|
28
|
+
self._virtual_includes = _virtual_includes.merge(name => uses)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module VirtualAttributes
|
3
|
+
module VirtualReflections
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ActiveRecord::VirtualAttributes::VirtualIncludes
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
#
|
10
|
+
# Definition
|
11
|
+
#
|
12
|
+
|
13
|
+
def virtual_has_one(name, options = {})
|
14
|
+
uses = options.delete(:uses)
|
15
|
+
reflection = ActiveRecord::Associations::Builder::HasOne.build(self, name, nil, options)
|
16
|
+
add_virtual_reflection(reflection, name, uses, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def virtual_has_many(name, options = {})
|
20
|
+
define_method("#{name.to_s.singularize}_ids") do
|
21
|
+
records = send(name)
|
22
|
+
records.respond_to?(:ids) ? records.ids : records.collect(&:id)
|
23
|
+
end
|
24
|
+
uses = options.delete(:uses)
|
25
|
+
reflection = ActiveRecord::Associations::Builder::HasMany.build(self, name, nil, options)
|
26
|
+
add_virtual_reflection(reflection, name, uses, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def virtual_belongs_to(name, options = {})
|
30
|
+
uses = options.delete(:uses)
|
31
|
+
reflection = ActiveRecord::Associations::Builder::BelongsTo.build(self, name, nil, options)
|
32
|
+
add_virtual_reflection(reflection, name, uses, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def virtual_reflection?(name)
|
36
|
+
virtual_reflections.key?(name.to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
def virtual_reflection(name)
|
40
|
+
virtual_reflections[name.to_sym]
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Introspection
|
45
|
+
#
|
46
|
+
|
47
|
+
def virtual_reflections
|
48
|
+
(virtual_fields_base? ? {} : superclass.virtual_reflections).merge(_virtual_reflections)
|
49
|
+
end
|
50
|
+
|
51
|
+
def reflections_with_virtual
|
52
|
+
reflections.symbolize_keys.merge(virtual_reflections)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reflection_with_virtual(association)
|
56
|
+
virtual_reflection(association) || reflect_on_association(association)
|
57
|
+
end
|
58
|
+
|
59
|
+
def follow_associations(association_names)
|
60
|
+
association_names.inject(self) { |klass, name| klass.try!(:reflect_on_association, name).try!(:klass) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def follow_associations_with_virtual(association_names)
|
64
|
+
association_names.inject(self) { |klass, name| klass.try!(:reflection_with_virtual, name).try!(:klass) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# invalid associations return a nil
|
68
|
+
# real reflections are followed
|
69
|
+
# a virtual association will stop the traversal
|
70
|
+
# @returns [nil, Array<Relation>]
|
71
|
+
def collect_reflections(association_names)
|
72
|
+
klass = self
|
73
|
+
association_names.each_with_object([]) do |name, ret|
|
74
|
+
reflection = klass.reflect_on_association(name)
|
75
|
+
if reflection.nil?
|
76
|
+
if klass.reflection_with_virtual(name)
|
77
|
+
break(ret)
|
78
|
+
else
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
klass = reflection.klass
|
83
|
+
ret << reflection
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def collect_reflections_with_virtual(association_names)
|
88
|
+
klass = self
|
89
|
+
association_names.collect do |name|
|
90
|
+
reflection = klass.reflection_with_virtual(name) || break
|
91
|
+
klass = reflection.klass
|
92
|
+
reflection
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def add_virtual_reflection(reflection, name, uses, _options)
|
99
|
+
raise ArgumentError, "macro must be specified" unless reflection
|
100
|
+
reset_virtual_reflection_information
|
101
|
+
_virtual_reflections[name.to_sym] = reflection
|
102
|
+
define_virtual_include(name.to_s, uses)
|
103
|
+
end
|
104
|
+
|
105
|
+
def reset_virtual_reflection_information
|
106
|
+
end
|
107
|
+
|
108
|
+
def _virtual_reflections
|
109
|
+
@virtual_reflections ||= {}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module VirtualAttributes
|
2
|
+
module VirtualTotal
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
private
|
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
|
14
|
+
#
|
15
|
+
# example 1:
|
16
|
+
#
|
17
|
+
# class ExtManagementSystem
|
18
|
+
# has_many :vms
|
19
|
+
# virtual_total :total_vms, :vms
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# generates:
|
23
|
+
#
|
24
|
+
# def total_vms
|
25
|
+
# vms.count
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# virtual_attribute :total_vms, :integer, :uses => :vms, :arel => ...
|
29
|
+
#
|
30
|
+
# # arel == (SELECT COUNT(*) FROM vms where ems.id = vms.ems_id)
|
31
|
+
#
|
32
|
+
# example 2:
|
33
|
+
#
|
34
|
+
# class Hardware
|
35
|
+
# has_many :disks
|
36
|
+
# virtual_aggregate :allocated_disk_storage, :disks, :sum, :size
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# generates:
|
40
|
+
#
|
41
|
+
# def allocated_disk_storage
|
42
|
+
# if disks.loaded?
|
43
|
+
# disks.blank? ? nil : disks.map { |t| t.size.to_i }.sum
|
44
|
+
# else
|
45
|
+
# disks.sum(:size) || 0
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# virtual_attribute :allocated_disk_storage, :integer, :uses => :disks, :arel => ...
|
50
|
+
#
|
51
|
+
# # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
|
52
|
+
|
53
|
+
def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
|
54
|
+
define_virtual_aggregate_method(name, relation, method_name, column)
|
55
|
+
reflection = reflect_on_association(relation)
|
56
|
+
|
57
|
+
if options.key?(:arel)
|
58
|
+
arel = options.dup.delete(:arel)
|
59
|
+
# if there is no relation to get to the arel, have to throw it away
|
60
|
+
arel = nil if !arel || !reflection
|
61
|
+
else
|
62
|
+
arel = virtual_aggregate_arel(reflection, method_name, column)
|
63
|
+
end
|
64
|
+
|
65
|
+
if arel
|
66
|
+
virtual_attribute name, :integer, :uses => options[:uses] || relation, :arel => arel
|
67
|
+
else
|
68
|
+
virtual_attribute name, :integer, **options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
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
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def virtual_aggregate_arel(reflection, method_name, column)
|
95
|
+
return unless reflection && reflection.macro == :has_many && !reflection.options[:through]
|
96
|
+
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
|
+
# ordering will probably screw up aggregations, so clear this out from
|
104
|
+
# any calls
|
105
|
+
#
|
106
|
+
# only clear this out if this isn't a `:size` call as well, since doing
|
107
|
+
# a COUNT(*) will allow any ORDER BY to still work properly. This is
|
108
|
+
# to avoid any possible edge cases by clearing out the order clause.
|
109
|
+
query.order_values = [] if method_name != :size
|
110
|
+
|
111
|
+
foreign_table = reflection.klass.arel_table
|
112
|
+
# need db access for the keys, so delaying all this lookup until call time
|
113
|
+
if ActiveRecord.version.to_s >= "5.1"
|
114
|
+
join_keys = reflection.join_keys
|
115
|
+
else
|
116
|
+
join_keys = reflection.join_keys(reflection.klass)
|
117
|
+
end
|
118
|
+
query = query.where(t[join_keys.foreign_key].eq(foreign_table[join_keys.key]))
|
119
|
+
|
120
|
+
arel_column = if method_name == :size
|
121
|
+
Arel.star.count
|
122
|
+
else
|
123
|
+
reflection.klass.arel_attribute(column).send(method_name)
|
124
|
+
end
|
125
|
+
query = query.select(arel_column)
|
126
|
+
|
127
|
+
t.grouping(Arel::Nodes::SqlLiteral.new(query.to_sql))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
ActiveRecord::Base.send(:include, VirtualAttributes::VirtualTotal)
|