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