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
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'virtual_attributes'
|
@@ -0,0 +1 @@
|
|
1
|
+
require "active_record/virtual_attributes"
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
require "active_record/virtual_attributes/virtual_includes"
|
5
|
+
require "active_record/virtual_attributes/virtual_arel"
|
6
|
+
require "active_record/virtual_attributes/virtual_delegates"
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
module VirtualAttributes
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include ActiveRecord::VirtualAttributes::VirtualIncludes
|
12
|
+
include ActiveRecord::VirtualAttributes::VirtualArel
|
13
|
+
include ActiveRecord::VirtualAttributes::VirtualDelegates
|
14
|
+
|
15
|
+
module Type
|
16
|
+
# TODO: do we actually need symbol types?
|
17
|
+
class Symbol < ActiveRecord::Type::String
|
18
|
+
def type; :symbol; end
|
19
|
+
end
|
20
|
+
|
21
|
+
class StringSet < ActiveRecord::Type::Value
|
22
|
+
def type; :string_set; end
|
23
|
+
end
|
24
|
+
|
25
|
+
class NumericSet < ActiveRecord::Type::Value
|
26
|
+
def type; :numeric_set; end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ActiveRecord::Type.register(:numeric_set, Type::NumericSet)
|
31
|
+
ActiveRecord::Type.register(:string_set, Type::StringSet)
|
32
|
+
ActiveRecord::Type.register(:symbol, Type::Symbol)
|
33
|
+
|
34
|
+
included do
|
35
|
+
class_attribute :virtual_attributes_to_define, :instance_accessor => false
|
36
|
+
self.virtual_attributes_to_define = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
|
41
|
+
#
|
42
|
+
# Definition
|
43
|
+
#
|
44
|
+
|
45
|
+
# Compatibility method: `virtual_attribute` is a more accurate name
|
46
|
+
def virtual_column(name, type_or_options, **options)
|
47
|
+
if type_or_options.kind_of?(Hash)
|
48
|
+
options = options.merge(type_or_options)
|
49
|
+
type = options.delete(:type)
|
50
|
+
else
|
51
|
+
type = type_or_options
|
52
|
+
end
|
53
|
+
|
54
|
+
virtual_attribute(name, type, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def virtual_attribute(name, type, **options)
|
58
|
+
name = name.to_s
|
59
|
+
reload_schema_from_cache
|
60
|
+
|
61
|
+
self.virtual_attributes_to_define =
|
62
|
+
virtual_attributes_to_define.merge(name => [type, options])
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Introspection
|
67
|
+
#
|
68
|
+
|
69
|
+
def virtual_attribute?(name)
|
70
|
+
load_schema
|
71
|
+
has_attribute?(name) && (
|
72
|
+
!respond_to?(:column_for_attribute) ||
|
73
|
+
column_for_attribute(name).kind_of?(ActiveRecord::ConnectionAdapters::NullColumn)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def virtual_attribute_names
|
78
|
+
if respond_to?(:column_names)
|
79
|
+
attribute_names - column_names
|
80
|
+
else
|
81
|
+
attribute_names
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def attributes_builder # :nodoc:
|
86
|
+
unless defined?(@attributes_builder) && @attributes_builder
|
87
|
+
defaults = _default_attributes.except(*(column_names - [primary_key]))
|
88
|
+
# change necessary for rails 5.0 and 5.1 - (changed/introduced in https://github.com/rails/rails/pull/31894)
|
89
|
+
defaults = defaults.except(*virtual_attribute_names)
|
90
|
+
# end change
|
91
|
+
@attributes_builder = ActiveRecord::AttributeSet::Builder.new(attribute_types, defaults)
|
92
|
+
end
|
93
|
+
@attributes_builder
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def load_schema!
|
99
|
+
super
|
100
|
+
|
101
|
+
virtual_attributes_to_define.each do |name, (type, options)|
|
102
|
+
type = type.call if type.respond_to?(:call)
|
103
|
+
type = ActiveRecord::Type.lookup(type, **options.except(:uses, :arel)) if type.kind_of?(Symbol)
|
104
|
+
|
105
|
+
define_virtual_attribute(name, type, **options.slice(:uses, :arel))
|
106
|
+
end
|
107
|
+
|
108
|
+
virtual_delegates_to_define.each do |method_name, (method, options)|
|
109
|
+
define_virtual_delegate(method_name, method, options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def define_virtual_attribute(name, cast_type, uses: nil, arel: nil)
|
114
|
+
attribute_types[name] = cast_type
|
115
|
+
define_virtual_include(name, uses) if uses
|
116
|
+
define_virtual_arel(name, arel) if arel
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
require "active_record/virtual_attributes/virtual_reflections"
|
122
|
+
require "active_record/virtual_attributes/virtual_fields"
|
123
|
+
|
124
|
+
#
|
125
|
+
# Class extensions
|
126
|
+
#
|
127
|
+
|
128
|
+
# this patch is no longer necessary for 5.2
|
129
|
+
if ActiveRecord.version.to_s < "5.2"
|
130
|
+
require "active_record/attribute"
|
131
|
+
module ActiveRecord
|
132
|
+
# This is a bug in rails 5.0 and 5.1, but it is made much worse by virtual attributes
|
133
|
+
class Attribute
|
134
|
+
def with_value_from_database(value)
|
135
|
+
# self.class.from_database(name, value, type)
|
136
|
+
initialized? ? self.class.from_database(name, value, type) : self
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
require "active_record/virtual_attributes/virtual_total"
|
143
|
+
require "active_record/virtual_attributes/arel_groups"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# this is from https://github.com/rails/arel/pull/435
|
2
|
+
# this allows sorting and where clauses to work with virtual_attribute columns
|
3
|
+
if defined?(Arel::Nodes::Grouping)
|
4
|
+
module Arel
|
5
|
+
module Nodes
|
6
|
+
class Grouping
|
7
|
+
include Arel::Expressions
|
8
|
+
include Arel::AliasPredication
|
9
|
+
include Arel::OrderPredications
|
10
|
+
include Arel::Math
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "active_record/virtual_attributes/rspec/have_virtual_attribute"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# TODO: expose this to classes that include this gem
|
2
|
+
|
3
|
+
# legacy matcher
|
4
|
+
RSpec::Matchers.define :have_virtual_column do |name, type|
|
5
|
+
match do |klass|
|
6
|
+
expect(klass.has_attribute?(name)).to be_truthy
|
7
|
+
expect(klass.virtual_attribute?(name)).to be_truthy
|
8
|
+
expect(klass.type_for_attribute(name).type).to eq(type)
|
9
|
+
klass.instance_methods.include?(name.to_sym)
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message do |klass|
|
13
|
+
"expected #{klass.name} to have virtual column #{name.inspect} with type #{type.inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
failure_message_when_negated do |klass|
|
17
|
+
"expected #{klass.name} to not have virtual column #{name.inspect} with type #{type.inspect}"
|
18
|
+
end
|
19
|
+
|
20
|
+
description do
|
21
|
+
"expect the object to have the virtual column"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec::Matchers.define :have_virtual_attribute do |name, type|
|
26
|
+
match do |klass|
|
27
|
+
expect(klass.has_attribute?(name)).to be_truthy
|
28
|
+
expect(klass.virtual_attribute?(name)).to be_truthy
|
29
|
+
expect(klass.type_for_attribute(name).type).to eq(type)
|
30
|
+
klass.instance_methods.include?(name.to_sym)
|
31
|
+
end
|
32
|
+
|
33
|
+
failure_message do |klass|
|
34
|
+
"expected #{klass.name} to have virtual column #{name.inspect} with type #{type.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
failure_message_when_negated do |klass|
|
38
|
+
"expected #{klass.name} to not have virtual column #{name.inspect} with type #{type.inspect}"
|
39
|
+
end
|
40
|
+
|
41
|
+
description do
|
42
|
+
"expect the object to have the virtual column"
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module VirtualAttributes
|
3
|
+
# VirtualArel associates arel with an attribute
|
4
|
+
#
|
5
|
+
# Model.virtual_attribute :field, :string, :arel => -> (t) { t.grouping(t[:field2]) } }
|
6
|
+
# Model.select(:field)
|
7
|
+
#
|
8
|
+
# is equivalent to:
|
9
|
+
#
|
10
|
+
# Model.select(Model.arel_table.grouping(Model.arel_table[:field2]).as(:field))
|
11
|
+
# Model.attribute_supported_by_sql?(:field) # => true
|
12
|
+
module VirtualArel
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
class_attribute :_virtual_arel, :instance_accessor => false
|
17
|
+
self._virtual_arel = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def arel_attribute(column_name, arel_table = self.arel_table)
|
22
|
+
load_schema
|
23
|
+
if virtual_attribute?(column_name) && !attribute_alias?(column_name)
|
24
|
+
col = _virtual_arel[column_name.to_s]
|
25
|
+
col.call(arel_table) if col
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# supported by sql if
|
32
|
+
# - it is an attribute alias
|
33
|
+
# - it is an attribute that is non virtual
|
34
|
+
# - it is an attribute that is virtual and has arel defined
|
35
|
+
def attribute_supported_by_sql?(name)
|
36
|
+
load_schema
|
37
|
+
try(:attribute_alias?, name) ||
|
38
|
+
(has_attribute?(name) && (!virtual_attribute?(name) || !!_virtual_arel[name.to_s]))
|
39
|
+
end
|
40
|
+
private
|
41
|
+
|
42
|
+
def define_virtual_arel(name, arel)
|
43
|
+
self._virtual_arel = _virtual_arel.merge(name => arel)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module VirtualAttributes
|
3
|
+
# VirtualDelegate is the same as delegate, but adds sql support, and a default when a value is not found
|
4
|
+
#
|
5
|
+
# Model.belongs_to :association
|
6
|
+
# Model.virtual_delegate :field1, :field2, to: :association
|
7
|
+
#
|
8
|
+
# Model.select(:field1) # now works
|
9
|
+
module VirtualDelegates
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
class_attribute :virtual_delegates_to_define, :instance_accessor => false
|
14
|
+
self.virtual_delegates_to_define = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
#
|
20
|
+
# Definition
|
21
|
+
#
|
22
|
+
|
23
|
+
def virtual_delegate(*methods)
|
24
|
+
options = methods.extract_options!
|
25
|
+
unless (to = options[:to])
|
26
|
+
raise ArgumentError, 'Delegation needs an association. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
|
27
|
+
end
|
28
|
+
|
29
|
+
to = to.to_s
|
30
|
+
if to.include?(".") && methods.size > 1
|
31
|
+
raise ArgumentError, 'Delegation only supports specifying a method name when defining a single virtual method'
|
32
|
+
end
|
33
|
+
|
34
|
+
if to.count(".") > 1
|
35
|
+
raise ArgumentError, 'Delegation needs a single association. Supply an option hash with a :to key with only 1 period (e.g. delegate :hello, to: "greeter.greeting")'
|
36
|
+
end
|
37
|
+
|
38
|
+
allow_nil = options[:allow_nil]
|
39
|
+
default = options[:default]
|
40
|
+
|
41
|
+
# put method entry per method name.
|
42
|
+
# This better supports reloading of the class and changing the definitions
|
43
|
+
methods.each do |method|
|
44
|
+
method_prefix = virtual_delegate_name_prefix(options[:prefix], to)
|
45
|
+
method_name = "#{method_prefix}#{method}"
|
46
|
+
if to.include?(".") # to => "target.method"
|
47
|
+
to, method = to.split(".")
|
48
|
+
options[:to] = to
|
49
|
+
end
|
50
|
+
|
51
|
+
define_delegate(method_name, method, :to => to, :allow_nil => allow_nil, :default => default)
|
52
|
+
|
53
|
+
self.virtual_delegates_to_define =
|
54
|
+
virtual_delegates_to_define.merge(method_name => [method, options])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# define virtual_attribute for delegates
|
61
|
+
#
|
62
|
+
# this is called at schema load time (and not at class definition time)
|
63
|
+
#
|
64
|
+
# @param method_name [Symbol] name of the attribute on the source class to be defined
|
65
|
+
# @param col [Symbol] name of the attribute on the associated class to be referenced
|
66
|
+
# @option options :to [Symbol] name of the association from the source class to be referenced
|
67
|
+
# @option options :arel [Proc] (optional and not common)
|
68
|
+
# @option options :uses [Array|Symbol|Hash] sql includes hash. (default: to)
|
69
|
+
def define_virtual_delegate(method_name, col, options)
|
70
|
+
unless (to = options[:to]) && (to_ref = reflection_with_virtual(to.to_s))
|
71
|
+
raise ArgumentError, 'Delegation needs an association. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
|
72
|
+
end
|
73
|
+
|
74
|
+
col = col.to_s
|
75
|
+
type = to_ref.klass.type_for_attribute(col)
|
76
|
+
raise "unknown attribute #{to}##{col} referenced in #{name}" unless type
|
77
|
+
arel = virtual_delegate_arel(col, to_ref)
|
78
|
+
define_virtual_attribute(method_name, type, :uses => (options[:uses] || to), :arel => arel)
|
79
|
+
end
|
80
|
+
|
81
|
+
# see activesupport module/delegation.rb
|
82
|
+
def define_delegate(method_name, method, to: nil, allow_nil: nil, default: nil)
|
83
|
+
location = caller_locations(2, 1).first
|
84
|
+
file, line = location.path, location.lineno
|
85
|
+
|
86
|
+
# Attribute writer methods only accept one argument. Makes sure []=
|
87
|
+
# methods still accept two arguments.
|
88
|
+
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
|
89
|
+
default = default ? " || #{default.inspect}" : nil
|
90
|
+
# The following generated method calls the target exactly once, storing
|
91
|
+
# the returned value in a dummy variable.
|
92
|
+
#
|
93
|
+
# Reason is twofold: On one hand doing less calls is in general better.
|
94
|
+
# On the other hand it could be that the target has side-effects,
|
95
|
+
# whereas conceptually, from the user point of view, the delegator should
|
96
|
+
# be doing one call.
|
97
|
+
if allow_nil
|
98
|
+
method_def = <<-METHOD
|
99
|
+
def #{method_name}(#{definition})
|
100
|
+
return self[:#{method_name}]#{default} if has_attribute?(:#{method_name})
|
101
|
+
_ = #{to}
|
102
|
+
if !_.nil? || nil.respond_to?(:#{method})
|
103
|
+
_.#{method}(#{definition})
|
104
|
+
end#{default}
|
105
|
+
end
|
106
|
+
METHOD
|
107
|
+
else
|
108
|
+
exception = %(raise Module::DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
|
109
|
+
|
110
|
+
method_def = <<-METHOD
|
111
|
+
def #{method_name}(#{definition})
|
112
|
+
return self[:#{method_name}]#{default} if has_attribute?(:#{method_name})
|
113
|
+
_ = #{to}
|
114
|
+
_.#{method}(#{definition})#{default}
|
115
|
+
rescue NoMethodError => e
|
116
|
+
if _.nil? && e.name == :#{method}
|
117
|
+
#{exception}
|
118
|
+
else
|
119
|
+
raise
|
120
|
+
end
|
121
|
+
end
|
122
|
+
METHOD
|
123
|
+
end
|
124
|
+
method_def = method_def.split("\n").map(&:strip).join(';')
|
125
|
+
module_eval(method_def, file, line)
|
126
|
+
end
|
127
|
+
|
128
|
+
def virtual_delegate_name_prefix(prefix, to)
|
129
|
+
"#{prefix == true ? to : prefix}_" if prefix
|
130
|
+
end
|
131
|
+
|
132
|
+
# @param col [String] attribute name
|
133
|
+
# @param to_ref [Association] association from source class to target association
|
134
|
+
# @return [Proc] lambda to return arel that selects the attribute in a sub-query
|
135
|
+
# @return [Nil] if the attribute (col) can not be represented in sql.
|
136
|
+
#
|
137
|
+
# To generate a proc, the following cases must happen:
|
138
|
+
# - the column has sql (virtual_column with arel OR real sql attribute)
|
139
|
+
# - the association has sql representation (a real association has sql)
|
140
|
+
# - the association is to a single record (has_one or belongs_to)
|
141
|
+
#
|
142
|
+
# See select_from_alias for examples
|
143
|
+
|
144
|
+
def virtual_delegate_arel(col, to_ref)
|
145
|
+
# ensure the column has sql and the association is reachable via sql
|
146
|
+
# There is currently no way to propagate sql over a virtual association
|
147
|
+
if to_ref.klass.arel_attribute(col) && reflect_on_association(to_ref.name)
|
148
|
+
if to_ref.macro == :has_one || to_ref.macro == :belongs_to
|
149
|
+
blk = ->(arel) { arel.limit = 1 } if to_ref.macro == :has_one
|
150
|
+
lambda do |t|
|
151
|
+
if ActiveRecord.version.to_s >= "5.1"
|
152
|
+
join_keys = to_ref.join_keys
|
153
|
+
else
|
154
|
+
join_keys = to_ref.join_keys(to_ref.klass)
|
155
|
+
end
|
156
|
+
src_model_id = arel_attribute(join_keys.foreign_key, t)
|
157
|
+
VirtualDelegates.select_from_alias(to_ref, col, join_keys.key, src_model_id, &blk)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# select_from_alias: helper method for virtual_delegate_arel to construct the sql
|
165
|
+
# see also virtual_delegate_arel
|
166
|
+
#
|
167
|
+
# @param to_ref [Association] association from source class to target association
|
168
|
+
# @param col [String] attribute name
|
169
|
+
# @param to_model_col_name [String]
|
170
|
+
# @param src_model_id [Arel::Attribute]
|
171
|
+
# @return [Arel::Node] Arel representing the sql for this join
|
172
|
+
#
|
173
|
+
# example
|
174
|
+
#
|
175
|
+
# for the given belongs_to class definition:
|
176
|
+
#
|
177
|
+
# class Vm
|
178
|
+
# belongs_to :hosts #, :foreign_key => :host_id, :primary_key => :id
|
179
|
+
# virtual_delegate :name, :to => :host, :prefix => true, :allow_nil => true
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# The virtual_delegate calls:
|
183
|
+
#
|
184
|
+
# virtual_delegate_arel("name", Vm.reflection_with_virtual(:host))
|
185
|
+
#
|
186
|
+
# which calls:
|
187
|
+
#
|
188
|
+
# select_from_alias(Vm.reflection_with_virtual(:host), "name", "id", Vm.arel_table[:host_id])
|
189
|
+
#
|
190
|
+
# which produces the sql:
|
191
|
+
#
|
192
|
+
# SELECT to_model[col] from to_model where to_model[to_model_col_name] = src_model_table[:src_model_id]
|
193
|
+
# (SELECT "hosts"."name" FROM "hosts" WHERE "hosts"."id" = "vms"."host_id")
|
194
|
+
#
|
195
|
+
# ----
|
196
|
+
#
|
197
|
+
# for the given has_one class definition
|
198
|
+
#
|
199
|
+
# class Host
|
200
|
+
# has_one :hardware
|
201
|
+
# virtual_delegate :name, :to => :hardware, :prefix => true, :allow_nil => true
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# The virtual_delegate calls:
|
205
|
+
#
|
206
|
+
# virtual_delegate_arel("name", Host.reflection_with_virtual(:hardware))
|
207
|
+
#
|
208
|
+
# which at runtime will call select_from_alias:
|
209
|
+
#
|
210
|
+
# select_from_alias(Host.reflection_with_virtual(:hardware), "name", "host_id", Host.arel_table[:id])
|
211
|
+
#
|
212
|
+
# which produces the sql (ala arel):
|
213
|
+
#
|
214
|
+
# #select to_model[col] from to_model where to_model[to_model_col_name] = src_model_table[:src_model_id]
|
215
|
+
# (SELECT "hardwares"."name" FROM "hardwares" WHERE "hardwares"."host_id" = "hosts"."id")
|
216
|
+
#
|
217
|
+
# ----
|
218
|
+
#
|
219
|
+
# for the given self join class definition:
|
220
|
+
#
|
221
|
+
# class Vm
|
222
|
+
# belongs_to :src_template, :class => Vm
|
223
|
+
# virtual_delegate :name, :to => :src_template, :prefix => true, :allow_nil => true
|
224
|
+
# end
|
225
|
+
#
|
226
|
+
# The virtual_delegate calls:
|
227
|
+
#
|
228
|
+
# virtual_delegate_arel("name", Vm.reflection_with_virtual(:src_template))
|
229
|
+
#
|
230
|
+
# which calls:
|
231
|
+
#
|
232
|
+
# select_from_alias(Vm.reflection_with_virtual(:src_template), "name", "src_template_id", Vm.arel_table[:id])
|
233
|
+
#
|
234
|
+
# which produces the sql:
|
235
|
+
#
|
236
|
+
# #select to_model[col] from to_model where to_model[to_model_col_name] = src_model_table[:src_model_id]
|
237
|
+
# (SELECT "vms_sub"."name" FROM "vms" AS "vms_ss" WHERE "vms_ss"."id" = "vms"."src_template_id")
|
238
|
+
#
|
239
|
+
|
240
|
+
def self.select_from_alias(to_ref, col, to_model_col_name, src_model_id)
|
241
|
+
query = if to_ref.scope
|
242
|
+
to_ref.klass.instance_exec(nil, &to_ref.scope)
|
243
|
+
else
|
244
|
+
to_ref.klass.all
|
245
|
+
end
|
246
|
+
|
247
|
+
to_table = select_from_alias_table(to_ref.klass, src_model_id.relation)
|
248
|
+
to_model_id = to_ref.klass.arel_attribute(to_model_col_name, to_table)
|
249
|
+
to_column = to_ref.klass.arel_attribute(col, to_table)
|
250
|
+
arel = query.except(:select).select(to_column).arel
|
251
|
+
.from(to_table)
|
252
|
+
.where(to_model_id.eq(src_model_id))
|
253
|
+
|
254
|
+
yield arel if block_given?
|
255
|
+
|
256
|
+
Arel.sql("(#{arel.to_sql})")
|
257
|
+
end
|
258
|
+
|
259
|
+
# determine table reference to use for a sub query
|
260
|
+
#
|
261
|
+
# typically to_table is just the table used for the to_ref
|
262
|
+
# but if it is a self join, then it will also have an alias
|
263
|
+
def self.select_from_alias_table(to_klass, src_relation)
|
264
|
+
to_table = to_klass.arel_table
|
265
|
+
# if a self join, alias the second table to a different name
|
266
|
+
if to_table.table_name == src_relation.table_name
|
267
|
+
# use a dup to not modify the primary table in the model
|
268
|
+
to_table = to_table.dup
|
269
|
+
# use a table alias to not conflict with table name in the primary query
|
270
|
+
to_table.table_alias = "#{to_table.table_name}_sub"
|
271
|
+
end
|
272
|
+
to_table
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|