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