torque-postgresql 0.2.16 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +76 -3
- data/lib/torque-postgresql.rb +1 -0
- data/lib/torque/postgresql.rb +6 -0
- data/lib/torque/postgresql/adapter.rb +2 -4
- data/lib/torque/postgresql/adapter/database_statements.rb +23 -9
- data/lib/torque/postgresql/adapter/oid.rb +12 -1
- data/lib/torque/postgresql/adapter/oid/box.rb +28 -0
- data/lib/torque/postgresql/adapter/oid/circle.rb +37 -0
- data/lib/torque/postgresql/adapter/oid/enum.rb +9 -5
- data/lib/torque/postgresql/adapter/oid/enum_set.rb +44 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +59 -0
- data/lib/torque/postgresql/adapter/oid/range.rb +52 -0
- data/lib/torque/postgresql/adapter/oid/segment.rb +73 -0
- data/lib/torque/postgresql/adapter/quoting.rb +21 -0
- data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +10 -1
- data/lib/torque/postgresql/arel.rb +3 -0
- data/lib/torque/postgresql/arel/infix_operation.rb +42 -0
- data/lib/torque/postgresql/arel/nodes.rb +32 -0
- data/lib/torque/postgresql/arel/operations.rb +18 -0
- data/lib/torque/postgresql/arel/visitors.rb +28 -2
- data/lib/torque/postgresql/associations.rb +8 -0
- data/lib/torque/postgresql/associations/association.rb +30 -0
- data/lib/torque/postgresql/associations/association_scope.rb +116 -0
- data/lib/torque/postgresql/associations/belongs_to_many_association.rb +117 -0
- data/lib/torque/postgresql/associations/builder.rb +2 -0
- data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +121 -0
- data/lib/torque/postgresql/associations/builder/has_many.rb +15 -0
- data/lib/torque/postgresql/associations/join_dependency/join_association.rb +15 -0
- data/lib/torque/postgresql/associations/preloader.rb +25 -0
- data/lib/torque/postgresql/associations/preloader/association.rb +64 -0
- data/lib/torque/postgresql/attributes.rb +2 -0
- data/lib/torque/postgresql/attributes/builder.rb +1 -0
- data/lib/torque/postgresql/attributes/builder/enum.rb +23 -15
- data/lib/torque/postgresql/attributes/builder/period.rb +452 -0
- data/lib/torque/postgresql/attributes/enum.rb +11 -8
- data/lib/torque/postgresql/attributes/enum_set.rb +256 -0
- data/lib/torque/postgresql/attributes/lazy.rb +1 -1
- data/lib/torque/postgresql/attributes/period.rb +31 -0
- data/lib/torque/postgresql/attributes/type_map.rb +3 -5
- data/lib/torque/postgresql/autosave_association.rb +40 -0
- data/lib/torque/postgresql/auxiliary_statement.rb +201 -198
- data/lib/torque/postgresql/auxiliary_statement/settings.rb +20 -12
- data/lib/torque/postgresql/base.rb +161 -2
- data/lib/torque/postgresql/config.rb +91 -9
- data/lib/torque/postgresql/geometry_builder.rb +92 -0
- data/lib/torque/postgresql/i18n.rb +1 -1
- data/lib/torque/postgresql/railtie.rb +18 -5
- data/lib/torque/postgresql/reflection.rb +21 -0
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +109 -0
- data/lib/torque/postgresql/reflection/association_reflection.rb +30 -0
- data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +44 -0
- data/lib/torque/postgresql/reflection/has_many_reflection.rb +13 -0
- data/lib/torque/postgresql/reflection/runtime_reflection.rb +12 -0
- data/lib/torque/postgresql/reflection/through_reflection.rb +11 -0
- data/lib/torque/postgresql/relation.rb +11 -10
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +11 -18
- data/lib/torque/postgresql/relation/inheritance.rb +2 -2
- data/lib/torque/postgresql/relation/merger.rb +11 -7
- data/lib/torque/postgresql/schema_cache.rb +1 -1
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/range.rb +40 -0
- metadata +41 -9
@@ -0,0 +1,256 @@
|
|
1
|
+
module Torque
|
2
|
+
module PostgreSQL
|
3
|
+
module Attributes
|
4
|
+
class EnumSet < Set
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
class EnumSetError < Enum::EnumError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
delegate :each, to: :members
|
13
|
+
delegate :values, :members, :texts, :to_options, :valid?, :size,
|
14
|
+
:length, :connection_specification_name, to: :enum_source
|
15
|
+
|
16
|
+
# Find or create the class that will handle the value
|
17
|
+
def lookup(name, enum_klass)
|
18
|
+
const = name.to_s.camelize + 'Set'
|
19
|
+
namespace = Torque::PostgreSQL.config.enum.namespace
|
20
|
+
|
21
|
+
return namespace.const_get(const) if namespace.const_defined?(const)
|
22
|
+
|
23
|
+
klass = Class.new(EnumSet)
|
24
|
+
klass.const_set('EnumSource', enum_klass)
|
25
|
+
namespace.const_set(const, klass)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Provide a method on the given class to setup which enum sets will be
|
29
|
+
# manually initialized
|
30
|
+
def include_on(klass)
|
31
|
+
method_name = Torque::PostgreSQL.config.enum.set_method
|
32
|
+
klass.singleton_class.class_eval do
|
33
|
+
define_method(method_name) do |*args, **options|
|
34
|
+
Torque::PostgreSQL::Attributes::TypeMap.decorate(self, args, **options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# The original Enum implementation, for individual values
|
40
|
+
def enum_source
|
41
|
+
const_get('EnumSource')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Use the power to get a sample of the value
|
45
|
+
def sample
|
46
|
+
new(rand(0..((2 ** size) - 1)))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Overpass new so blank values return only nil
|
50
|
+
def new(*values)
|
51
|
+
return Lazy.new(self, []) if values.compact.blank?
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get the type name from its class name
|
56
|
+
def type_name
|
57
|
+
@type_name ||= enum_source.type_name + '[]'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Fetch a value from the list
|
61
|
+
# see https://github.com/rails/rails/blob/v5.0.0/activerecord/lib/active_record/fixtures.rb#L656
|
62
|
+
# see https://github.com/rails/rails/blob/v5.0.0/activerecord/lib/active_record/validations/uniqueness.rb#L101
|
63
|
+
def fetch(value, *)
|
64
|
+
new(value.to_s) if values.include?(value)
|
65
|
+
end
|
66
|
+
alias [] fetch
|
67
|
+
|
68
|
+
# Get the power, 2 ** index, of each element
|
69
|
+
def power(*values)
|
70
|
+
values.flatten.map do |item|
|
71
|
+
item = item.to_i if item.is_a?(Enum)
|
72
|
+
item = values.index(item) unless item.is_a?(Numeric)
|
73
|
+
|
74
|
+
next 0 if item.nil? || item >= size
|
75
|
+
2 ** item
|
76
|
+
end.reduce(:+)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Build an active record scope for a given atribute agains a value
|
80
|
+
def scope(attribute, value)
|
81
|
+
attribute.contains(Array.wrap(value))
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Allows checking value existance
|
87
|
+
def respond_to_missing?(method_name, include_private = false)
|
88
|
+
valid?(method_name) || super
|
89
|
+
end
|
90
|
+
|
91
|
+
# Allow fast creation of values
|
92
|
+
def method_missing(method_name, *arguments)
|
93
|
+
return super if self == Enum
|
94
|
+
valid?(method_name) ? new(method_name.to_s) : super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Override string initializer to check for a valid value
|
99
|
+
def initialize(*values)
|
100
|
+
items =
|
101
|
+
if values.size === 1 && values.first.is_a?(Numeric)
|
102
|
+
transform_power(values.first)
|
103
|
+
else
|
104
|
+
transform_values(values)
|
105
|
+
end
|
106
|
+
|
107
|
+
@hash = items.zip(Array.new(items.size, true)).to_h
|
108
|
+
end
|
109
|
+
|
110
|
+
# Allow comparison between values of the same enum
|
111
|
+
def <=>(other)
|
112
|
+
raise_comparison(other) if other.is_a?(EnumSet) && other.class != self.class
|
113
|
+
|
114
|
+
to_i <=>
|
115
|
+
case other
|
116
|
+
when Numeric, EnumSet then other.to_i
|
117
|
+
when String, Symbol then self.class.power(other.to_s)
|
118
|
+
when Array, Set then self.class.power(*other)
|
119
|
+
else raise_comparison(other)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Only allow value comparison with values of the same class
|
124
|
+
def ==(other)
|
125
|
+
(self <=> other) == 0
|
126
|
+
rescue EnumSetError
|
127
|
+
false
|
128
|
+
end
|
129
|
+
alias eql? ==
|
130
|
+
|
131
|
+
# It only accepts if the other value is valid
|
132
|
+
def replace(*values)
|
133
|
+
super(transform_values(values))
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get a translated version of the value
|
137
|
+
def text(attr = nil, model = nil)
|
138
|
+
map { |item| item.text(attr, model) }.to_sentence
|
139
|
+
end
|
140
|
+
alias to_s text
|
141
|
+
|
142
|
+
# Get the index of the value
|
143
|
+
def to_i
|
144
|
+
self.class.power(@hash.keys)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Change the inspection to show the enum name
|
148
|
+
def inspect
|
149
|
+
map(&:inspect).inspect
|
150
|
+
end
|
151
|
+
|
152
|
+
# Replace the setter by instantiating the value
|
153
|
+
def []=(key, value)
|
154
|
+
super(key, instantiate(value))
|
155
|
+
end
|
156
|
+
|
157
|
+
# Override the merge method to ensure formatted values
|
158
|
+
def merge(other)
|
159
|
+
super other.map(&method(:instantiate))
|
160
|
+
end
|
161
|
+
|
162
|
+
# Override bitwise & operator to ensure formatted values
|
163
|
+
def &(other)
|
164
|
+
other = other.entries.map(&method(:instantiate))
|
165
|
+
values = @hash.keys.select { |k| other.include?(k) }
|
166
|
+
self.class.new(values)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Operations that requries the other values to be transformed as well
|
170
|
+
%i[add delete include? subtract].each do |method_name|
|
171
|
+
define_method(method_name) do |other|
|
172
|
+
other =
|
173
|
+
if other.is_a?(self.class)
|
174
|
+
other
|
175
|
+
elsif other.is_a?(::Enumerable)
|
176
|
+
other.map(&method(:instantiate))
|
177
|
+
else
|
178
|
+
instantiate(other)
|
179
|
+
end
|
180
|
+
|
181
|
+
super(other)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
# Create a new enum instance of the value
|
188
|
+
def instantiate(value)
|
189
|
+
value.is_a?(self.class.enum_source) ? value : self.class.enum_source.new(value)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Turn a binary (power) definition into real values
|
193
|
+
def transform_power(value)
|
194
|
+
list = value.to_s(2).reverse.chars.map.with_index do |item, idx|
|
195
|
+
next idx if item.eql?('1')
|
196
|
+
end
|
197
|
+
|
198
|
+
raise raise_invalid(value) if list.size > self.class.size
|
199
|
+
self.class.members.values_at(*list.compact)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Turn all the values into their respective Enum representations
|
203
|
+
def transform_values(values)
|
204
|
+
values = values.first if values.size.eql?(1) && values.first.is_a?(::Enumerable)
|
205
|
+
values.map(&method(:instantiate))
|
206
|
+
end
|
207
|
+
|
208
|
+
# Check for valid '?' and '!' methods
|
209
|
+
def respond_to_missing?(method_name, include_private = false)
|
210
|
+
name = method_name.to_s
|
211
|
+
|
212
|
+
return true if name.chomp!('?')
|
213
|
+
name.chomp!('!') && self.class.valid?(name)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Allow '_' to be associated to '-'
|
217
|
+
def method_missing(method_name, *arguments)
|
218
|
+
name = method_name.to_s
|
219
|
+
|
220
|
+
if name.chomp!('?')
|
221
|
+
include?(name)
|
222
|
+
elsif name.chomp!('!')
|
223
|
+
add(name) unless include?(name)
|
224
|
+
else
|
225
|
+
super
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Throw an exception for invalid valus
|
230
|
+
def raise_invalid(value)
|
231
|
+
if value.is_a?(Numeric)
|
232
|
+
raise EnumSetError, "#{value.inspect} is out of bounds of #{self.class.name}"
|
233
|
+
else
|
234
|
+
raise EnumSetError, "#{value.inspect} is not valid for #{self.class.name}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Throw an exception for comparasion between different enums
|
239
|
+
def raise_comparison(other)
|
240
|
+
raise EnumSetError, "Comparison of #{self.class.name} with #{self.inspect} failed"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Create the methods related to the attribute to handle the enum type
|
245
|
+
TypeMap.register_type Adapter::OID::EnumSet do |subtype, attribute, options = nil|
|
246
|
+
# Generate methods on self class
|
247
|
+
builder = Builder::Enum.new(self, attribute, subtype, options || {})
|
248
|
+
break if builder.conflicting?
|
249
|
+
builder.build
|
250
|
+
|
251
|
+
# Mark the enum as defined
|
252
|
+
defined_enums[attribute] = subtype.klass
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Torque
|
2
|
+
module PostgreSQL
|
3
|
+
module Attributes
|
4
|
+
# For naw, period doesn't have it's own class
|
5
|
+
module Period
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Provide a method on the given class to setup which period columns
|
9
|
+
# will be manually initialized
|
10
|
+
def include_on(klass)
|
11
|
+
method_name = Torque::PostgreSQL.config.period.base_method
|
12
|
+
klass.singleton_class.class_eval do
|
13
|
+
define_method(method_name) do |*args, **options|
|
14
|
+
Torque::PostgreSQL::Attributes::TypeMap.decorate(self, args, **options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create the methods related to the attribute to handle the enum type
|
23
|
+
TypeMap.register_type Adapter::OID::Range do |subtype, attribute, options = nil|
|
24
|
+
# Generate methods on self class
|
25
|
+
builder = Builder::Period.new(self, attribute, subtype, options || {})
|
26
|
+
break if builder.conflicting?
|
27
|
+
builder.build
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Torque
|
2
2
|
module PostgreSQL
|
3
3
|
module Attributes
|
4
|
+
# Remove type map and add decorators direct to ActiveRecord::Base
|
4
5
|
module TypeMap
|
5
6
|
|
6
7
|
class << self
|
@@ -65,19 +66,16 @@ module Torque
|
|
65
66
|
# Check whether the given attribute on the given klass is
|
66
67
|
# decorable by this type mapper
|
67
68
|
def decorable?(key, klass, attribute)
|
68
|
-
key.
|
69
|
-
(decorable.key?(klass) && decorable[klass].include?(attribute.to_s))
|
69
|
+
decorable.key?(klass) && decorable[klass].include?(attribute.to_s)
|
70
70
|
end
|
71
71
|
|
72
72
|
# Message when trying to define multiple types
|
73
73
|
def raise_type_defined(key)
|
74
|
-
raise ArgumentError, <<-MSG.
|
74
|
+
raise ArgumentError, <<-MSG.squish
|
75
75
|
Type #{key} is already defined here: #{types[key].source_location.join(':')}
|
76
76
|
MSG
|
77
77
|
end
|
78
|
-
|
79
78
|
end
|
80
|
-
|
81
79
|
end
|
82
80
|
end
|
83
81
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Torque
|
2
|
+
module PostgreSQL
|
3
|
+
module AutosaveAssociation
|
4
|
+
module ClassMethods
|
5
|
+
def add_autosave_association_callbacks(reflection)
|
6
|
+
return super unless reflection.connected_through_array? &&
|
7
|
+
reflection.macro.eql?(:belongs_to_many)
|
8
|
+
|
9
|
+
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
10
|
+
define_non_cyclic_method(save_method) { save_belongs_to_many_array(reflection) }
|
11
|
+
|
12
|
+
before_save(:before_save_collection_association)
|
13
|
+
after_save(:after_save_collection_association) if ::ActiveRecord::Base
|
14
|
+
.instance_methods.include?(:after_save_collection_association)
|
15
|
+
|
16
|
+
before_create(save_method)
|
17
|
+
before_update(save_method)
|
18
|
+
|
19
|
+
define_autosave_validation_callbacks(reflection)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def save_belongs_to_many_array(reflection)
|
24
|
+
save_collection_association(reflection)
|
25
|
+
|
26
|
+
association = association_instance_get(reflection.name)
|
27
|
+
return unless association
|
28
|
+
|
29
|
+
klass_fk = reflection.foreign_key
|
30
|
+
acpk = reflection.active_record_primary_key
|
31
|
+
|
32
|
+
records = association.target.each_with_object(klass_fk)
|
33
|
+
write_attribute(acpk, records.map(&:read_attribute).compact)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
::ActiveRecord::Base.singleton_class.prepend(AutosaveAssociation::ClassMethods)
|
38
|
+
::ActiveRecord::Base.include(AutosaveAssociation)
|
39
|
+
end
|
40
|
+
end
|
@@ -6,19 +6,7 @@ module Torque
|
|
6
6
|
TABLE_COLUMN_AS_STRING = /\A(?:"?(\w+)"?\.)?"?(\w+)"?\z/.freeze
|
7
7
|
|
8
8
|
class << self
|
9
|
-
|
10
|
-
#
|
11
|
-
# The attributes separation means
|
12
|
-
# exposed_attributes -> Will be projected to the main query
|
13
|
-
# selected_attributes -> Will be selected on the configurated query
|
14
|
-
# join_attributes -> Will be used to join the the queries
|
15
|
-
[:exposed_attributes, :selected_attributes, :query, :join_attributes,
|
16
|
-
:join_type, :requires].each do |attribute|
|
17
|
-
define_method(attribute) do
|
18
|
-
setup
|
19
|
-
instance_variable_get("@#{attribute}")
|
20
|
-
end
|
21
|
-
end
|
9
|
+
attr_reader :config
|
22
10
|
|
23
11
|
# Find or create the class that will handle statement
|
24
12
|
def lookup(name, base)
|
@@ -29,13 +17,28 @@ module Torque
|
|
29
17
|
|
30
18
|
# Create a new instance of an auxiliary statement
|
31
19
|
def instantiate(statement, base, options = nil)
|
32
|
-
klass = base
|
20
|
+
klass = while base < ActiveRecord::Base
|
21
|
+
list = base.auxiliary_statements_list
|
22
|
+
break list[statement] if list.present? && list.key?(statement)
|
23
|
+
|
24
|
+
base = base.superclass
|
25
|
+
end
|
26
|
+
|
33
27
|
return klass.new(options) unless klass.nil?
|
34
|
-
raise ArgumentError, <<-MSG.
|
28
|
+
raise ArgumentError, <<-MSG.squish
|
35
29
|
There's no '#{statement}' auxiliary statement defined for #{base.class.name}.
|
36
30
|
MSG
|
37
31
|
end
|
38
32
|
|
33
|
+
# Fast access to statement build
|
34
|
+
def build(statement, base, options = nil, bound_attributes = [])
|
35
|
+
klass = instantiate(statement, base, options)
|
36
|
+
result = klass.build(base)
|
37
|
+
|
38
|
+
bound_attributes.concat(klass.bound_attributes)
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
39
42
|
# Identify if the query set may be used as a relation
|
40
43
|
def relation_query?(obj)
|
41
44
|
!obj.nil? && obj.respond_to?(:ancestors) && \
|
@@ -47,21 +50,45 @@ module Torque
|
|
47
50
|
!obj.nil? && obj.is_a?(::Arel::SelectManager)
|
48
51
|
end
|
49
52
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
def
|
53
|
-
|
54
|
-
|
53
|
+
# A way to create auxiliary statements outside of models configurations,
|
54
|
+
# being able to use on extensions
|
55
|
+
def create(table_or_settings, &block)
|
56
|
+
klass = Class.new(AuxiliaryStatement)
|
57
|
+
|
58
|
+
if block_given?
|
59
|
+
klass.instance_variable_set(:@table_name, table_or_settings)
|
60
|
+
klass.configurator(block)
|
61
|
+
elsif relation_query?(table_or_settings)
|
62
|
+
klass.configurator(query: table_or_settings)
|
63
|
+
else
|
64
|
+
klass.configurator(table_or_settings)
|
65
|
+
end
|
66
|
+
|
67
|
+
klass
|
55
68
|
end
|
56
69
|
|
57
|
-
#
|
58
|
-
def
|
59
|
-
|
70
|
+
# Set a configuration block or static hash
|
71
|
+
def configurator(config)
|
72
|
+
if config.is_a?(Hash)
|
73
|
+
# Map the aliases
|
74
|
+
config[:attributes] = config.delete(:select) if config.key?(:select)
|
75
|
+
|
76
|
+
# Create the struct that mocks a configuration result
|
77
|
+
config = OpenStruct.new(config)
|
78
|
+
table_name = config[:query]&.klass&.name&.underscore
|
79
|
+
instance_variable_set(:@table_name, table_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
@config = config
|
60
83
|
end
|
61
84
|
|
62
|
-
#
|
63
|
-
def
|
64
|
-
|
85
|
+
# Run a configuration block or get the static configuration
|
86
|
+
def configure(base, instance)
|
87
|
+
return @config unless @config.respond_to?(:call)
|
88
|
+
|
89
|
+
settings = Settings.new(base, instance)
|
90
|
+
settings.instance_exec(settings, &@config)
|
91
|
+
settings
|
65
92
|
end
|
66
93
|
|
67
94
|
# Get the arel version of the statement table
|
@@ -73,164 +100,148 @@ module Torque
|
|
73
100
|
def table_name
|
74
101
|
@table_name ||= self.name.demodulize.split('_').first.underscore
|
75
102
|
end
|
103
|
+
end
|
76
104
|
|
77
|
-
|
78
|
-
|
79
|
-
@base_table ||= base.arel_table
|
80
|
-
end
|
105
|
+
delegate :config, :table, :table_name, :relation, :configure, :relation_query?,
|
106
|
+
to: :class
|
81
107
|
|
82
|
-
|
83
|
-
def query_table
|
84
|
-
@query_table ||= query.arel_table
|
85
|
-
end
|
108
|
+
attr_reader :bound_attributes
|
86
109
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
elsif (as_string = TABLE_COLUMN_AS_STRING.match(column.to_s))
|
92
|
-
column = as_string[2]
|
93
|
-
arel_table = ::Arel::Table.new(as_string[1]) unless as_string[1].nil?
|
94
|
-
end
|
110
|
+
# Start a new auxiliary statement giving extra options
|
111
|
+
def initialize(*args)
|
112
|
+
options = args.extract_options!
|
113
|
+
args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
|
95
114
|
|
96
|
-
|
97
|
-
|
98
|
-
|
115
|
+
@join = options.fetch(:join, {})
|
116
|
+
@args = options.fetch(args_key, {})
|
117
|
+
@where = options.fetch(:where, {})
|
118
|
+
@select = options.fetch(:select, {})
|
119
|
+
@join_type = options.fetch(:join_type, nil)
|
120
|
+
@bound_attributes = []
|
121
|
+
end
|
99
122
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
setup! unless setup?
|
104
|
-
end
|
123
|
+
# Build the statement on the given arel and return the WITH statement
|
124
|
+
def build(base)
|
125
|
+
prepare(base)
|
105
126
|
|
106
|
-
|
107
|
-
|
108
|
-
defined?(@query) && @query
|
109
|
-
end
|
127
|
+
# Add the join condition to the list
|
128
|
+
base.joins_values += [build_join(base)]
|
110
129
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
settings.instance_exec(settings, &@config)
|
115
|
-
|
116
|
-
@join_type = settings.join_type || :inner
|
117
|
-
@requires = Array[settings.requires].flatten.compact
|
118
|
-
@query = settings.query
|
119
|
-
|
120
|
-
# Manually set the query table when it's not an relation query
|
121
|
-
@query_table = settings.query_table unless relation_query?(@query)
|
122
|
-
|
123
|
-
# Reset all the used attributes
|
124
|
-
@selected_attributes = []
|
125
|
-
@exposed_attributes = []
|
126
|
-
@join_attributes = []
|
127
|
-
|
128
|
-
# Generate attributes projections
|
129
|
-
attributes_projections(settings.attributes)
|
130
|
-
|
131
|
-
# Generate join projections
|
132
|
-
if settings.join.present?
|
133
|
-
joins_projections(settings.join)
|
134
|
-
elsif relation_query?(@query)
|
135
|
-
check_auto_join(settings.polymorphic)
|
136
|
-
else
|
137
|
-
raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ')
|
138
|
-
You must provide the join columns when using '#{query.class.name}'
|
139
|
-
as a query object on #{self.class.name}.
|
140
|
-
MSG
|
141
|
-
end
|
142
|
-
end
|
130
|
+
# Return the statement with its dependencies
|
131
|
+
[@dependencies, ::Arel::Nodes::As.new(table, build_query(base))]
|
132
|
+
end
|
143
133
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
134
|
+
private
|
135
|
+
# Setup the statement using the class configuration
|
136
|
+
def prepare(base)
|
137
|
+
settings = configure(base, self)
|
138
|
+
requires = Array.wrap(settings.requires).flatten.compact
|
139
|
+
@dependencies = ensure_dependencies(requires, base).flatten.compact
|
140
|
+
|
141
|
+
@join_type ||= settings.join_type || :inner
|
142
|
+
@query = settings.query
|
143
|
+
|
144
|
+
# Call a proc to get the real query
|
145
|
+
if @query.methods.include?(:call)
|
146
|
+
call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
|
147
|
+
@query = @query.call(*call_args)
|
148
|
+
@args = []
|
153
149
|
end
|
154
150
|
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
151
|
+
# Manually set the query table when it's not an relation query
|
152
|
+
@query_table = settings.query_table unless relation_query?(@query)
|
153
|
+
@select = settings.attributes.merge(@select) if settings.attributes.present?
|
154
|
+
|
155
|
+
# Merge join settings
|
156
|
+
if settings.join.present?
|
157
|
+
@join = settings.join.merge(@join)
|
158
|
+
elsif settings.through.present?
|
159
|
+
@association = settings.through.to_s
|
160
|
+
elsif relation_query?(@query)
|
161
|
+
@association = base.reflections.find do |name, reflection|
|
162
|
+
break name if @query.klass.eql? reflection.klass
|
163
163
|
end
|
164
164
|
end
|
165
|
+
end
|
165
166
|
|
166
|
-
|
167
|
-
|
168
|
-
#
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
167
|
+
# Build the string or arel query
|
168
|
+
def build_query(base)
|
169
|
+
# Expose columns and get the list of the ones for select
|
170
|
+
columns = expose_columns(base, @query.try(:arel_table))
|
171
|
+
|
172
|
+
# Prepare the query depending on its type
|
173
|
+
if @query.is_a?(String)
|
174
|
+
args = @args.map{ |k, v| [k, base.connection.quote(v)] }.to_h
|
175
|
+
::Arel.sql("(#{@query})" % args)
|
176
|
+
elsif relation_query?(@query)
|
177
|
+
@query = @query.where(@where) if @where.present?
|
178
|
+
@bound_attributes.concat(@query.send(:bound_attributes))
|
179
|
+
@query.select(*columns).arel
|
180
|
+
else
|
181
|
+
raise ArgumentError, <<-MSG.squish
|
182
|
+
Only String and ActiveRecord::Base objects are accepted as query objects,
|
183
|
+
#{@query.class.name} given for #{self.class.name}.
|
184
|
+
MSG
|
182
185
|
end
|
183
|
-
|
186
|
+
end
|
184
187
|
|
185
|
-
|
186
|
-
|
188
|
+
# Build the join statement that will be sent to the main arel
|
189
|
+
def build_join(base)
|
190
|
+
conditions = table.create_and([])
|
191
|
+
builder = base.predicate_builder
|
192
|
+
foreign_table = base.arel_table
|
187
193
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
|
194
|
+
# Check if it's necessary to load the join from an association
|
195
|
+
if @association.present?
|
196
|
+
association = base.reflections[@association]
|
192
197
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
@join_type = options.fetch(:join_type, join_type)
|
197
|
-
end
|
198
|
+
# Require source of a through reflection
|
199
|
+
if association.through_reflection?
|
200
|
+
base.joins(association.source_reflection_name)
|
198
201
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
202
|
+
# Changes the base of the connection to the reflection table
|
203
|
+
builder = association.klass.predicate_builder
|
204
|
+
foreign_table = ::Arel::Table.new(association.plural_name)
|
205
|
+
end
|
203
206
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
207
|
+
# Add the scopes defined by the reflection
|
208
|
+
if association.respond_to?(:join_scope)
|
209
|
+
args = [@query.arel_table]
|
210
|
+
args << base if association.method(:join_scope).arity.eql?(2)
|
211
|
+
@query.merge(association.join_scope(*args))
|
212
|
+
end
|
208
213
|
|
209
|
-
|
210
|
-
|
211
|
-
|
214
|
+
# Add the join constraints
|
215
|
+
constraint = association.build_join_constraint(table, foreign_table)
|
216
|
+
constraint = constraint.children if constraint.is_a?(::Arel::Nodes::And)
|
217
|
+
conditions.children.concat(Array.wrap(constraint))
|
218
|
+
end
|
212
219
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
220
|
+
# Build all conditions for the join on statement
|
221
|
+
@join.inject(conditions.children) do |arr, (left, right)|
|
222
|
+
left = project(left, foreign_table)
|
223
|
+
item = right.is_a?(Symbol) ? project(right).eq(left) : builder.build(left, right)
|
224
|
+
arr.push(item)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Raise an error when there's no join conditions
|
228
|
+
raise ArgumentError, <<-MSG.squish if conditions.children.empty?
|
229
|
+
You must provide the join columns when using '#{@query.class.name}'
|
230
|
+
as a query object on #{self.class.name}.
|
231
|
+
MSG
|
218
232
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
233
|
+
# Expose join columns
|
234
|
+
if relation_query?(@query)
|
235
|
+
query_table = @query.arel_table
|
236
|
+
conditions.children.each do |item|
|
237
|
+
@query.select_values += [query_table[item.left.name]] \
|
238
|
+
if item.left.relation.eql?(table)
|
239
|
+
end
|
225
240
|
end
|
226
241
|
|
227
|
-
|
228
|
-
|
229
|
-
base.auxiliary_statements_values += [instance]
|
242
|
+
# Build the join based on the join type
|
243
|
+
arel_join.new(table, table.create_on(conditions))
|
230
244
|
end
|
231
|
-
end
|
232
|
-
|
233
|
-
private
|
234
245
|
|
235
246
|
# Get the class of the join on arel
|
236
247
|
def arel_join
|
@@ -240,60 +251,52 @@ module Torque
|
|
240
251
|
when :right then ::Arel::Nodes::RightOuterJoin
|
241
252
|
when :full then ::Arel::Nodes::FullOuterJoin
|
242
253
|
else
|
243
|
-
raise ArgumentError, <<-MSG.
|
254
|
+
raise ArgumentError, <<-MSG.squish
|
244
255
|
The '#{@join_type}' is not implemented as a join type.
|
245
256
|
MSG
|
246
257
|
end
|
247
258
|
end
|
248
259
|
|
249
|
-
# Mount the
|
250
|
-
def
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
# Call a proc to get the query
|
256
|
-
if query.methods.include?(:call)
|
257
|
-
call_args = query.try(:arity) === 0 ? [] : [OpenStruct.new(args)]
|
258
|
-
query = query.call(*call_args)
|
259
|
-
args = []
|
260
|
+
# Mount the list of selected attributes
|
261
|
+
def expose_columns(base, query_table = nil)
|
262
|
+
# Add select columns to the query and get exposed columns
|
263
|
+
@select.map do |left, right|
|
264
|
+
base.select_extra_values += [table[right.to_s]]
|
265
|
+
project(left, query_table).as(right.to_s) if query_table
|
260
266
|
end
|
267
|
+
end
|
261
268
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
#{query.class.name} given for #{self.class.name}.
|
269
|
+
# Ensure that all the dependencies are loaded in the base relation
|
270
|
+
def ensure_dependencies(list, base)
|
271
|
+
with_options = list.extract_options!.to_a
|
272
|
+
(list + with_options).map do |dependent, options|
|
273
|
+
dependent_klass = base.model.auxiliary_statements_list[dependent]
|
274
|
+
|
275
|
+
raise ArgumentError, <<-MSG.squish if dependent_klass.nil?
|
276
|
+
The '#{dependent}' auxiliary statement dependency can't found on
|
277
|
+
#{self.class.name}.
|
272
278
|
MSG
|
273
|
-
end
|
274
|
-
end
|
275
279
|
|
276
|
-
|
277
|
-
|
278
|
-
join_attributes + @join.map do |left, right|
|
279
|
-
if right.is_a?(Symbol)
|
280
|
-
project(left, base_table).eq(project(right))
|
281
|
-
else
|
282
|
-
project(left).eq(right)
|
280
|
+
next if base.auxiliary_statements_values.any? do |cte|
|
281
|
+
cte.is_a?(dependent_klass)
|
283
282
|
end
|
283
|
+
|
284
|
+
AuxiliaryStatement.build(dependent, base, options, bound_attributes)
|
284
285
|
end
|
285
286
|
end
|
286
287
|
|
287
|
-
#
|
288
|
-
def
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
column =
|
293
|
-
|
288
|
+
# Project a column on a given table, or use the column table
|
289
|
+
def project(column, arel_table = nil)
|
290
|
+
if column.respond_to?(:as)
|
291
|
+
return column
|
292
|
+
elsif (as_string = TABLE_COLUMN_AS_STRING.match(column.to_s))
|
293
|
+
column = as_string[2]
|
294
|
+
arel_table = ::Arel::Table.new(as_string[1]) unless as_string[1].nil?
|
294
295
|
end
|
295
|
-
end
|
296
296
|
|
297
|
+
arel_table ||= table
|
298
|
+
arel_table[column.to_s]
|
299
|
+
end
|
297
300
|
end
|
298
301
|
end
|
299
302
|
end
|