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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +76 -3
  3. data/lib/torque-postgresql.rb +1 -0
  4. data/lib/torque/postgresql.rb +6 -0
  5. data/lib/torque/postgresql/adapter.rb +2 -4
  6. data/lib/torque/postgresql/adapter/database_statements.rb +23 -9
  7. data/lib/torque/postgresql/adapter/oid.rb +12 -1
  8. data/lib/torque/postgresql/adapter/oid/box.rb +28 -0
  9. data/lib/torque/postgresql/adapter/oid/circle.rb +37 -0
  10. data/lib/torque/postgresql/adapter/oid/enum.rb +9 -5
  11. data/lib/torque/postgresql/adapter/oid/enum_set.rb +44 -0
  12. data/lib/torque/postgresql/adapter/oid/line.rb +59 -0
  13. data/lib/torque/postgresql/adapter/oid/range.rb +52 -0
  14. data/lib/torque/postgresql/adapter/oid/segment.rb +73 -0
  15. data/lib/torque/postgresql/adapter/quoting.rb +21 -0
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +10 -1
  18. data/lib/torque/postgresql/arel.rb +3 -0
  19. data/lib/torque/postgresql/arel/infix_operation.rb +42 -0
  20. data/lib/torque/postgresql/arel/nodes.rb +32 -0
  21. data/lib/torque/postgresql/arel/operations.rb +18 -0
  22. data/lib/torque/postgresql/arel/visitors.rb +28 -2
  23. data/lib/torque/postgresql/associations.rb +8 -0
  24. data/lib/torque/postgresql/associations/association.rb +30 -0
  25. data/lib/torque/postgresql/associations/association_scope.rb +116 -0
  26. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +117 -0
  27. data/lib/torque/postgresql/associations/builder.rb +2 -0
  28. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +121 -0
  29. data/lib/torque/postgresql/associations/builder/has_many.rb +15 -0
  30. data/lib/torque/postgresql/associations/join_dependency/join_association.rb +15 -0
  31. data/lib/torque/postgresql/associations/preloader.rb +25 -0
  32. data/lib/torque/postgresql/associations/preloader/association.rb +64 -0
  33. data/lib/torque/postgresql/attributes.rb +2 -0
  34. data/lib/torque/postgresql/attributes/builder.rb +1 -0
  35. data/lib/torque/postgresql/attributes/builder/enum.rb +23 -15
  36. data/lib/torque/postgresql/attributes/builder/period.rb +452 -0
  37. data/lib/torque/postgresql/attributes/enum.rb +11 -8
  38. data/lib/torque/postgresql/attributes/enum_set.rb +256 -0
  39. data/lib/torque/postgresql/attributes/lazy.rb +1 -1
  40. data/lib/torque/postgresql/attributes/period.rb +31 -0
  41. data/lib/torque/postgresql/attributes/type_map.rb +3 -5
  42. data/lib/torque/postgresql/autosave_association.rb +40 -0
  43. data/lib/torque/postgresql/auxiliary_statement.rb +201 -198
  44. data/lib/torque/postgresql/auxiliary_statement/settings.rb +20 -12
  45. data/lib/torque/postgresql/base.rb +161 -2
  46. data/lib/torque/postgresql/config.rb +91 -9
  47. data/lib/torque/postgresql/geometry_builder.rb +92 -0
  48. data/lib/torque/postgresql/i18n.rb +1 -1
  49. data/lib/torque/postgresql/railtie.rb +18 -5
  50. data/lib/torque/postgresql/reflection.rb +21 -0
  51. data/lib/torque/postgresql/reflection/abstract_reflection.rb +109 -0
  52. data/lib/torque/postgresql/reflection/association_reflection.rb +30 -0
  53. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +44 -0
  54. data/lib/torque/postgresql/reflection/has_many_reflection.rb +13 -0
  55. data/lib/torque/postgresql/reflection/runtime_reflection.rb +12 -0
  56. data/lib/torque/postgresql/reflection/through_reflection.rb +11 -0
  57. data/lib/torque/postgresql/relation.rb +11 -10
  58. data/lib/torque/postgresql/relation/auxiliary_statement.rb +11 -18
  59. data/lib/torque/postgresql/relation/inheritance.rb +2 -2
  60. data/lib/torque/postgresql/relation/merger.rb +11 -7
  61. data/lib/torque/postgresql/schema_cache.rb +1 -1
  62. data/lib/torque/postgresql/version.rb +1 -1
  63. data/lib/torque/range.rb +40 -0
  64. 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
@@ -16,7 +16,7 @@ module Torque
16
16
  end
17
17
 
18
18
  def inspect
19
- 'nil'
19
+ 'nil'.freeze
20
20
  end
21
21
 
22
22
  def __class__
@@ -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.class.auto_initialize? ||
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.strip
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
- # These attributes require that the class is setup
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.auxiliary_statements_list[statement]
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.strip
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
- # Set a configuration block, if the class is already set up, just clean
51
- # the query and wait it to be setup again
52
- def configurator(block)
53
- @config = block
54
- @query = nil
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
- # Get the base class associated to this statement
58
- def base
59
- self.parent
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
- # Get the name of the base class
63
- def base_name
64
- base.name
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
- # Get the arel table of the base class
78
- def base_table
79
- @base_table ||= base.arel_table
80
- end
105
+ delegate :config, :table, :table_name, :relation, :configure, :relation_query?,
106
+ to: :class
81
107
 
82
- # Get the arel table of the query
83
- def query_table
84
- @query_table ||= query.arel_table
85
- end
108
+ attr_reader :bound_attributes
86
109
 
87
- # Project a column on a given table, or use the column table
88
- def project(column, arel_table = nil)
89
- if column.respond_to?(:as)
90
- return column
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
- arel_table ||= table
97
- arel_table[column.to_s]
98
- end
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
- private
101
- # Just setup the class if it's not setup
102
- def setup
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
- # Check if the class is setup
107
- def setup?
108
- defined?(@query) && @query
109
- end
127
+ # Add the join condition to the list
128
+ base.joins_values += [build_join(base)]
110
129
 
111
- # Setup the class
112
- def setup!
113
- settings = Settings.new(self)
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
- # Iterate the attributes settings
145
- # Attributes (left => right)
146
- # left -> query.selected_attributes AS right
147
- # right -> table.exposed_attributes
148
- def attributes_projections(list)
149
- list.each do |left, right|
150
- @exposed_attributes << project(right)
151
- @selected_attributes << project(left, query_table).as(right.to_s)
152
- end
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
- # Iterate the join settings
156
- # Join (left => right)
157
- # left -> base.join_attributes.eq(right)
158
- # right -> table.selected_attributes
159
- def joins_projections(list)
160
- list.each do |left, right|
161
- @selected_attributes << project(right, query_table)
162
- @join_attributes << project(left, base_table).eq(project(right))
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
- # Check if it's possible to identify the connection between the main
167
- # query and the statement query
168
- #
169
- # First, identify the foreign key column name, then check if it exists
170
- # on the query and then create the projections
171
- def check_auto_join(polymorphic)
172
- foreign_key = (polymorphic.present? ? polymorphic : base_name)
173
- foreign_key = foreign_key.to_s.foreign_key
174
- if query.columns_hash.key?(foreign_key)
175
- joins_projections(base.primary_key => foreign_key)
176
- if polymorphic.present?
177
- foreign_type = foreign_key.gsub(/_id$/, '_type')
178
- @selected_attributes << project(foreign_type, query_table)
179
- @join_attributes << project(foreign_type).eq(base_name)
180
- end
181
- end
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
- end
186
+ end
184
187
 
185
- delegate :exposed_attributes, :join_attributes, :selected_attributes, :join_type, :table,
186
- :query_table, :base_table, :requires, :project, :relation_query?, to: :class
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
- # Start a new auxiliary statement giving extra options
189
- def initialize(*args)
190
- options = args.extract_options!
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
- @join = options.fetch(:join, {})
194
- @args = options.fetch(args_key, {})
195
- @select = options.fetch(:select, {})
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
- # Get the columns that will be selected for this statement
200
- def columns
201
- exposed_attributes + @select.values.map(&method(:project))
202
- end
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
- # Build the statement on the given arel and return the WITH statement
205
- def build_arel(arel, base)
206
- # Build the join for this statement
207
- arel.join(table, arel_join).on(*join_columns)
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
- # Return the subquery for this statement
210
- ::Arel::Nodes::As.new(table, mount_query)
211
- end
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
- # Get the bound attributes from statement qeury
214
- def bound_attributes
215
- return [] unless relation_query?(self.class.query)
216
- self.class.query.send(:bound_attributes)
217
- end
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
- # Ensure that all the dependencies are loaded in the base relation
220
- def ensure_dependencies!(base)
221
- requires.each do |dependent|
222
- dependent_klass = base.model.auxiliary_statements_list[dependent]
223
- next if base.auxiliary_statements_values.any? do |cte|
224
- cte.is_a?(dependent_klass)
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
- instance = AuxiliaryStatement.instantiate(dependent, base)
228
- instance.ensure_dependencies!(base)
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.strip
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 query base on it's class
250
- def mount_query
251
- klass = self.class
252
- query = klass.query
253
- args = @args
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
- # Prepare the query depending on its type
263
- if query.is_a?(String)
264
- args = args.map{ |k, v| [k, klass.parent.connection.quote(v)] }.to_h
265
- ::Arel::Nodes::SqlLiteral.new("(#{query})" % args)
266
- elsif relation_query?(query)
267
- query.select(*select_columns).arel
268
- else
269
- raise ArgumentError, <<-MSG.strip
270
- Only String and ActiveRecord::Base objects are accepted as query objects,
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
- # Mount the list of join attributes with the additional ones
277
- def join_columns
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
- # Mount the list of selected attributes with the additional ones
288
- def select_columns
289
- selected_attributes + @select.map do |left, right|
290
- project(left, query_table).as(right.to_s)
291
- end + @join.map do |left, right|
292
- column = right.is_a?(Symbol) ? right : left
293
- project(column, query_table)
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