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
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
|