torque-postgresql 0.2.16 → 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 +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
|