elasticsearch_record 1.0.2 → 1.1.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/Gemfile.lock +10 -14
  4. data/README.md +180 -27
  5. data/docs/CHANGELOG.md +36 -18
  6. data/docs/LICENSE.txt +1 -1
  7. data/elasticsearch_record.gemspec +42 -0
  8. data/lib/active_record/connection_adapters/elasticsearch/column.rb +20 -6
  9. data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +142 -125
  10. data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +2 -23
  11. data/lib/active_record/connection_adapters/elasticsearch/schema_creation.rb +30 -0
  12. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/attribute_methods.rb +103 -0
  13. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/column_methods.rb +42 -0
  14. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +158 -0
  15. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +32 -0
  16. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +132 -0
  17. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_mapping_definition.rb +110 -0
  18. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +136 -0
  19. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +174 -0
  20. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +37 -0
  21. data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +110 -0
  22. data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +398 -174
  23. data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +232 -0
  24. data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +2 -0
  25. data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +32 -0
  26. data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +112 -19
  27. data/lib/arel/collectors/elasticsearch_query.rb +0 -1
  28. data/lib/arel/visitors/elasticsearch.rb +7 -579
  29. data/lib/arel/visitors/elasticsearch_base.rb +234 -0
  30. data/lib/arel/visitors/elasticsearch_query.rb +463 -0
  31. data/lib/arel/visitors/elasticsearch_schema.rb +124 -0
  32. data/lib/elasticsearch_record/core.rb +44 -10
  33. data/lib/elasticsearch_record/errors.rb +13 -0
  34. data/lib/elasticsearch_record/gem_version.rb +6 -2
  35. data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +27 -9
  36. data/lib/elasticsearch_record/model_schema.rb +5 -0
  37. data/lib/elasticsearch_record/persistence.rb +31 -26
  38. data/lib/elasticsearch_record/query.rb +56 -17
  39. data/lib/elasticsearch_record/querying.rb +17 -0
  40. data/lib/elasticsearch_record/relation/calculation_methods.rb +3 -0
  41. data/lib/elasticsearch_record/relation/core_methods.rb +57 -17
  42. data/lib/elasticsearch_record/relation/query_clause_tree.rb +38 -1
  43. data/lib/elasticsearch_record/relation/query_methods.rb +6 -0
  44. data/lib/elasticsearch_record/relation/result_methods.rb +15 -9
  45. data/lib/elasticsearch_record/result.rb +32 -5
  46. data/lib/elasticsearch_record/statement_cache.rb +2 -1
  47. data/lib/elasticsearch_record.rb +2 -2
  48. metadata +29 -11
  49. data/.ruby-version +0 -1
  50. data/lib/elasticsearch_record/schema_migration.rb +0 -30
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Visitors
5
+ module ElasticsearchBase
6
+ extend ActiveSupport::Concern
7
+
8
+ class UnsupportedVisitError < StandardError
9
+ def initialize(method_name)
10
+ super "Unsupported method: '#{method_name}'. Construct an Arel node instead!"
11
+ end
12
+ end
13
+
14
+ included do
15
+ attr_accessor :connection
16
+ attr_accessor :collector
17
+ end
18
+
19
+ class_methods do
20
+ def simple_dispatch_cache
21
+ @simple_dispatch_cache ||= Hash.new do |hash, klass|
22
+ hash[klass] = "visit_#{(klass.name.demodulize || 'unknown')}"
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize(connection)
28
+ super()
29
+ @connection = connection
30
+
31
+ # required for nested assignment.
32
+ # see +#assign+ method
33
+ @nested = false
34
+ @nested_args = []
35
+ end
36
+
37
+ def dispatch_as(mode)
38
+ current, @dispatch = @dispatch, (mode == :simple ? self.class.simple_dispatch_cache : self.class.dispatch_cache)
39
+
40
+ res = yield
41
+
42
+ @dispatch = current
43
+
44
+ res
45
+ end
46
+
47
+ def compile(node, collector = Arel::Collectors::ElasticsearchQuery.new)
48
+ # we don't need to forward the collector each time - we just set it and always access it, when we need.
49
+ self.collector = collector
50
+
51
+ # so we just visit the first node without any additionally provided collector ...
52
+ accept(node)
53
+
54
+ # ... and return the final result
55
+ self.collector.value
56
+ end
57
+
58
+ private
59
+
60
+ # auto prevent visits on missing nodes
61
+ def method_missing(method, *args, &block)
62
+ raise(UnsupportedVisitError, method.to_s) if method.to_s[0..4] == 'visit'
63
+
64
+ super
65
+ end
66
+
67
+ # collects and returns provided object 'visit' result.
68
+ # returns an array of results if a array was provided.
69
+ # does not validate if the object is present...
70
+ # @param [Object] obj
71
+ # @param [Symbol] method (default: :visit)
72
+ # @return [Object,nil]
73
+ def collect(obj, method = :visit)
74
+ if obj.is_a?(Array)
75
+ obj.map { |o| self.__send__(method, o) }
76
+ elsif obj.present?
77
+ self.__send__(method, obj)
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ # resolves the provided object 'visit' result.
84
+ # check if the object is present.
85
+ # does not return any values
86
+ # @param [Array|Object] objects
87
+ # @param [Symbol] method (default: :visit)
88
+ # @return [nil]
89
+ def resolve(objects, method = :visit)
90
+ return unless objects.present?
91
+
92
+ objects = [objects] unless objects.is_a?(Array)
93
+
94
+ objects.each do |obj|
95
+ self.__send__(method, obj)
96
+ end
97
+
98
+ nil
99
+ end
100
+
101
+ # assign provided args on the collector.
102
+ # The TOP-Level assignment must be a (key, value) while sub-assignments will be collected by provided block.
103
+ # Sub-assignments will never claim on the query but 'merged' to the TOP-assignment.
104
+ #
105
+ # assign(:query, {}) do
106
+ # #... do some stuff ...
107
+ # assign(:bool, {}) do
108
+ # assign(:x,99)
109
+ # assign({y: 45})
110
+ # end
111
+ # end
112
+ # #> query: {bool: {x: 99, y: 45}}
113
+ def assign(*args)
114
+ # resolve possible TOP-LEVEL assignment
115
+ key, value = args
116
+
117
+ # if a block was provided we want to collect the nested assignments
118
+ if block_given?
119
+ raise ArgumentError, "Unsupported assignment value for provided block (#{key}). Provide any Object as value!" if value.nil?
120
+
121
+ # set nested state to tell all nested assignments to not claim it's values
122
+ old_nested, @nested = @nested, true
123
+ old_nested_args, @nested_args = @nested_args, []
124
+
125
+ # call block, but don't interact with its return.
126
+ # nested args are separately stored
127
+ yield
128
+
129
+ # restore nested state
130
+ @nested = old_nested
131
+
132
+ # assign nested args
133
+ @nested_args.each do |nested_args|
134
+ # parent node +args+ is split into +key+ (0) & +value+ (1)
135
+ case value
136
+ when Array
137
+ if nested_args[0].is_a?(Array)
138
+ nested_args[0].each do |nested_arg|
139
+ value << nested_arg
140
+ end
141
+ elsif nested_args[0].nil?
142
+ # handle special case: nil delegates to the value = nested_args[1]
143
+ value << nested_args[1]
144
+ else
145
+ value << nested_args[0]
146
+ end
147
+ when Hash
148
+ if nested_args[0].is_a?(Hash)
149
+ value.merge!(nested_args[0])
150
+ elsif value[nested_args[0]].is_a?(Hash) && nested_args[1].is_a?(Hash)
151
+ value[nested_args[0]] = value[nested_args[0]].merge(nested_args[1])
152
+ elsif value[nested_args[0]].is_a?(Array) && nested_args[1].is_a?(Array)
153
+ value[nested_args[0]] += nested_args[1]
154
+ elsif nested_args[1].nil? && nested_args[2] != :__force__
155
+ value.delete(nested_args[0])
156
+ elsif nested_args[0].nil? && nested_args[1].is_a?(Hash)
157
+ # handle special case: nil delegates to the value = nested_args[1]
158
+ value.merge!(nested_args[1])
159
+ else
160
+ value[nested_args[0]] = nested_args[1]
161
+ end
162
+ when String
163
+ if nested_args[0].is_a?(Array)
164
+ value = value + nested_args[0].map(&:to_s).join
165
+ elsif nested_args[0].nil?
166
+ # handle special case: nil delegates to the value = nested_args[1]
167
+ if nested_args[1].is_a?(Array)
168
+ value = value + nested_args[1].map(&:to_s).join
169
+ else
170
+ value = value + nested_args[1].to_s
171
+ end
172
+ else
173
+ value = value + nested_args[0].to_s
174
+ end
175
+ # reassign value to args, since string is not a referenced object
176
+ args[1] = value
177
+ else
178
+ value = nested_args[0] unless nested_args.blank?
179
+ end
180
+ end
181
+
182
+ # clear nested args
183
+ @nested_args = old_nested_args
184
+ elsif args.compact.blank?
185
+ return
186
+ end
187
+
188
+ # for nested assignments we only want the assignable args - no +claim+ on the query!
189
+ if @nested
190
+ @nested_args << args
191
+ return
192
+ end
193
+
194
+ raise ArgumentError, "Unsupported assign key: '#{key}' for provided block. Provide a Symbol as key!" unless key.is_a?(Symbol)
195
+
196
+ claim(:assign, key, value)
197
+ end
198
+
199
+ # creates and sends a new claim to the collector.
200
+ # @param [Symbol] action - claim action (:index, :type, :status, :argument, :body, :assign)
201
+ # @param [Array] args - either <key>,<value> or <Hash{<key> => <value>, ...}> or <Array>
202
+ def claim(action, *args)
203
+ self.collector << [action, args]
204
+
205
+ # IMPORTANT: always return nil, to prevent unwanted assignments
206
+ nil
207
+ end
208
+
209
+ ###########
210
+ # HELPERS #
211
+ ###########
212
+
213
+ def unboundable?(value)
214
+ value.respond_to?(:unboundable?) && value.unboundable?
215
+ end
216
+
217
+ def invalid?(value)
218
+ value == '1=0'
219
+ end
220
+
221
+ def quote(value)
222
+ return value if Arel::Nodes::SqlLiteral === value
223
+ connection.quote value
224
+ end
225
+
226
+ # assigns a failed status to the current query
227
+ def failed!
228
+ claim(:status, ElasticsearchRecord::Query::STATUS_FAILED)
229
+
230
+ nil
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Visitors
5
+ module ElasticsearchQuery
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ ######################
11
+ # CORE VISITS (CRUD) #
12
+ ######################
13
+
14
+ # SELECT // SEARCH
15
+ def visit_Arel_Nodes_SelectStatement(o)
16
+ # prepare query
17
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_SEARCH)
18
+
19
+ resolve(o.cores) # visit_Arel_Nodes_SelectCore
20
+
21
+ resolve(o.orders) # visit_Sort
22
+ resolve(o.limit) # visit_Arel_Nodes_Limit
23
+ resolve(o.offset) # visit_Arel_Nodes_Offset
24
+
25
+ # configure is able to overwrite everything in the query
26
+ resolve(o.configure)
27
+ end
28
+
29
+ # UPDATE
30
+ def visit_Arel_Nodes_UpdateStatement(o)
31
+ # switch between updating a single Record or multiple by query
32
+ if o.relation.is_a?(::Arel::Table)
33
+ raise NotImplementedError, "if you've made it this far, something went wrong ..."
34
+ end
35
+
36
+ # prepare query
37
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_UPDATE_BY_QUERY)
38
+
39
+ # sets the index
40
+ resolve(o.relation)
41
+
42
+ # updating multiple entries need a script
43
+ assign(:script, {}) do
44
+ assign(:inline, "") do
45
+ updates = collect(o.values)
46
+ assign(updates.join('; ')) if updates.present?
47
+ end
48
+ end
49
+
50
+ # sets the search query
51
+ resolve(o, :visit_Query)
52
+
53
+ resolve(o.orders) # visit_Sort
54
+
55
+ assign(:max_docs, collect(o.limit.expr)) if o.limit.present?
56
+ resolve(o.offset)
57
+
58
+ # configure is able to overwrite everything
59
+ resolve(o.configure)
60
+ end
61
+
62
+ # DELETE
63
+ def visit_Arel_Nodes_DeleteStatement(o)
64
+ # switch between updating a single Record or multiple by query
65
+ if o.relation.is_a?(::Arel::Table)
66
+ raise NotImplementedError, "if you've made it this far, something went wrong ..."
67
+ end
68
+
69
+ # prepare query
70
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_DELETE_BY_QUERY)
71
+
72
+ # sets the index
73
+ resolve(o.relation)
74
+
75
+ # sets the search query
76
+ resolve(o, :visit_Query)
77
+
78
+ resolve(o.orders) # visit_Sort
79
+
80
+ assign(:max_docs, collect(o.limit.expr)) if o.limit.present?
81
+ resolve(o.offset)
82
+
83
+ # configure is able to overwrite everything
84
+ resolve(o.configure)
85
+ end
86
+
87
+ # INSERT
88
+ def visit_Arel_Nodes_InsertStatement(o)
89
+
90
+ # switch between updating a single Record or multiple by query
91
+ if o.relation.is_a?(::Arel::Table)
92
+ # prepare query
93
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_CREATE)
94
+
95
+ # sets the index
96
+ resolve(o.relation)
97
+
98
+ # sets create arguments
99
+ resolve(o, :visit_Create)
100
+ else
101
+ raise NotImplementedError
102
+ end
103
+ end
104
+
105
+ ##############################
106
+ # SUBSTRUCTURE VISITS (CRUD) #
107
+ ##############################
108
+
109
+ def visit_Arel_Nodes_SelectCore(o)
110
+ # sets the index
111
+ resolve(o.source)
112
+
113
+ # IMPORTANT: Since Elasticsearch does not store nil-values in the +_source+ / +doc+ it will NOT return
114
+ # empty / nil columns - instead the nil columns do not exist!!!
115
+ # This is a big mess, because those missing columns are +not+ editable or savable in any way after we initialize the record...
116
+ # To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query.
117
+ resource_klass = o.source.left.instance_variable_get(:@klass)
118
+ claim(:columns, resource_klass.source_column_names) if resource_klass.respond_to?(:source_column_names)
119
+
120
+ # sets the query
121
+ resolve(o, :visit_Query) if o.queries.present? || o.wheres.present?
122
+
123
+ # sets the aggs
124
+ resolve(o, :visit_Aggs) if o.aggs.present?
125
+
126
+ # sets the selects
127
+ resolve(o, :visit_Selects) if o.projections.present?
128
+ end
129
+
130
+ # CUSTOM node by elasticsearch_record
131
+ def visit_Query(o)
132
+ # in some cases we don't have a kind, but where conditions.
133
+ # in this case we force the kind as +:bool+.
134
+ kind = :bool if o.wheres.present? && o.kind.blank?
135
+
136
+ # resolve kind, if not already set
137
+ kind ||= o.kind.present? ? visit(o.kind.expr) : nil
138
+
139
+ # check for existing kind - we cannot create a node if we don't have any kind
140
+ return unless kind
141
+
142
+ assign(:query, {}) do
143
+ # this creates a kind node and creates nested queries
144
+ # e.g. :bool => { ... }
145
+ assign(kind, {}) do
146
+ # each query has a type (e.g.: :filter) and one or multiple statements.
147
+ # this is handled within the +visit_Arel_Nodes_SelectQuery+ method
148
+ o.queries.each do |query|
149
+ resolve(query) # visit_Arel_Nodes_SelectQuery
150
+
151
+ # assign additional opts on the type level
152
+ assign(query.opts) if query.opts.present?
153
+ end
154
+
155
+ # collect the where from predicate builds
156
+ # should call:
157
+ # - visit_Arel_Nodes_Equality
158
+ # - visit_Arel_Nodes_NotEqual
159
+ # - visit_Arel_Nodes_HomogeneousIn'
160
+ resolve(o.wheres) if o.wheres.present?
161
+
162
+ # annotations
163
+ resolve(o.comment) if o.respond_to?(:comment)
164
+ end
165
+ end
166
+ end
167
+
168
+ # CUSTOM node by elasticsearch_record
169
+ def visit_Aggs(o)
170
+ assign(:aggs, {}) do
171
+ o.aggs.each do |agg|
172
+ resolve(agg)
173
+
174
+ # we assign the opts on the top agg level
175
+ assign(agg.opts) if agg.opts.present?
176
+ end
177
+ end
178
+ end
179
+
180
+ # CUSTOM node by elasticsearch_record
181
+ def visit_Selects(o)
182
+ fields = collect(o.projections)
183
+
184
+ case fields[0]
185
+ when '*'
186
+ # force return all fields
187
+ # assign(:_source, true)
188
+ when ::ActiveRecord::FinderMethods::ONE_AS_ONE
189
+ # force return NO fields
190
+ assign(:_source, false)
191
+ else
192
+ assign(:_source, fields)
193
+ # also overwrite the columns in the query
194
+ claim(:columns, fields)
195
+ end
196
+ end
197
+
198
+ # CUSTOM node by elasticsearch_record
199
+ def visit_Create(o)
200
+ # sets values
201
+ if o.values
202
+ values = collect(o.values) # visit_Arel_Nodes_ValuesList
203
+ claim(:body, values) if values.present?
204
+ else
205
+ failed!
206
+ end
207
+ end
208
+
209
+ # CUSTOM node by elasticsearch_record
210
+ def visit_Arel_Nodes_SelectKind(o)
211
+ visit(o.expr)
212
+ end
213
+
214
+ # CUSTOM node by elasticsearch_record
215
+ def visit_Arel_Nodes_SelectConfigure(o)
216
+ attrs = visit(o.expr)
217
+
218
+ # we need to assign each key - value independently since +nil+ values will be treated as +delete+
219
+ attrs.each do |key, value|
220
+ assign(key, value)
221
+ end if attrs.present?
222
+ end
223
+
224
+ # CUSTOM node by elasticsearch_record
225
+ def visit_Arel_Nodes_SelectQuery(o)
226
+ # this creates a query select node (includes key, value(s) and additional opts)
227
+ # e.g.
228
+ # :filter => [ ... ]
229
+ # :must => [ ... ]
230
+
231
+ # the query value must always be a array, since it might be extended by where clause.
232
+ # assign(:filter, []) ...
233
+ assign(visit(o.left), []) do
234
+ # assign(terms: ...)
235
+ assign(visit(o.right))
236
+ end
237
+ end
238
+
239
+ # CUSTOM node by elasticsearch_record
240
+ def visit_Arel_Nodes_SelectAgg(o)
241
+ assign(visit(o.left) => visit(o.right))
242
+ end
243
+
244
+ # used to write new data to columns
245
+ def visit_Arel_Nodes_Assignment(o)
246
+ value = visit(o.right)
247
+
248
+ value_assign = if o.right.value_before_type_cast.is_a?(Symbol)
249
+ "ctx._source.#{value}"
250
+ else
251
+ quote(value)
252
+ end
253
+
254
+ "ctx._source.#{visit(o.left)} = #{value_assign}"
255
+ end
256
+
257
+ def visit_Arel_Nodes_Comment(o)
258
+ assign(:_name, o.values.join(' - '))
259
+ end
260
+
261
+ # directly assigns the offset to the current scope
262
+ def visit_Arel_Nodes_Offset(o)
263
+ assign(:from, visit(o.expr))
264
+ end
265
+
266
+ # directly assigns the size to the current scope
267
+ def visit_Arel_Nodes_Limit(o)
268
+ assign(:size, visit(o.expr))
269
+ end
270
+
271
+ def visit_Sort(o)
272
+ assign(:sort, {}) do
273
+ key = visit(o.expr)
274
+ dir = visit(o.direction)
275
+
276
+ # we support a special key: __rand__ to create a simple random method ...
277
+ if key == '__rand__'
278
+ assign({
279
+ "_script" => {
280
+ "script" => "Math.random()",
281
+ "type" => "number",
282
+ "order" => dir
283
+ }
284
+ })
285
+ else
286
+ assign(key => dir)
287
+ end
288
+ end
289
+ end
290
+
291
+ alias :visit_Arel_Nodes_Ascending :visit_Sort
292
+ alias :visit_Arel_Nodes_Descending :visit_Sort
293
+
294
+ # DIRECT ASSIGNMENT
295
+ def visit_Arel_Nodes_Equality(o)
296
+ right = visit(o.right)
297
+
298
+ return failed! if unboundable?(right) || invalid?(right)
299
+
300
+ key = visit(o.left)
301
+
302
+ if right.nil?
303
+ # transforms nil to exists
304
+ assign(:must_not, [{ exists: { field: key } }])
305
+ else
306
+ assign(:filter, [{ term: { key => right } }])
307
+ end
308
+ end
309
+
310
+ # DIRECT ASSIGNMENT
311
+ def visit_Arel_Nodes_NotEqual(o)
312
+ right = visit(o.right)
313
+
314
+ return failed! if unboundable?(right) || invalid?(right)
315
+
316
+ key = visit(o.left)
317
+
318
+ if right.nil?
319
+ # transforms nil to exists
320
+ assign(:filter, [{ exists: { field: key } }])
321
+ else
322
+ assign(:must_not, [{ term: { key => right } }])
323
+ end
324
+ end
325
+
326
+ # DIRECT FAIL
327
+ def visit_Arel_Nodes_Grouping(o)
328
+ # grouping is NOT supported and will force to fail the query
329
+ failed!
330
+ end
331
+
332
+ # DIRECT ASSIGNMENT
333
+ def visit_Arel_Nodes_HomogeneousIn(o)
334
+ self.collector.preparable = false
335
+
336
+ values = o.casted_values
337
+
338
+ # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
339
+ # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
340
+ # instead it will be send as bind and then re-delegated to the SQL collector.
341
+ #
342
+ # This only works for linear SQL-queries and not nested Hashes
343
+ # (otherwise we have to collect those binds, and replace them afterwards).
344
+ #
345
+ # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
346
+ # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
347
+ # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
348
+ self.collector.add_binds(values, o.proc_for_binds)
349
+
350
+ if o.type == :in
351
+ assign(:filter, [{ terms: { o.column_name => o.casted_values } }])
352
+ else
353
+ assign(:must_not, [{ terms: { o.column_name => o.casted_values } }])
354
+ end
355
+ end
356
+
357
+ def visit_Arel_Nodes_And(o)
358
+ collect(o.children)
359
+ end
360
+
361
+ # # toDo: doesn't work properly - maybe restructure OR-assignments
362
+ # def visit_Arel_Nodes_Or(o)
363
+ # # If the bool query includes at least one should clause and no must or filter clauses, the default value is 1.
364
+ # # Otherwise, the default value is 0.
365
+ # assign(:should, []) do
366
+ # assign(nil, {}) do
367
+ #
368
+ # stack = [o.right, o.left]
369
+ #
370
+ # while o = stack.pop
371
+ # if o.is_a?(Arel::Nodes::Or)
372
+ # stack.push o.right, o.left
373
+ # elsif o.is_a?(ElasticsearchRecord::Relation::QueryClause)
374
+ # assign(visit(o.ast[1]))
375
+ # else
376
+ # visit o
377
+ # end
378
+ # end
379
+ # end
380
+ # end
381
+ # end
382
+
383
+ def visit_Arel_Nodes_JoinSource(o)
384
+ visit(o.left) if o.left
385
+ raise ActiveRecord::StatementInvalid, "table joins are not supported (#{o.right})" if o.right.any?
386
+ end
387
+
388
+ def visit_Arel_Table(o)
389
+ raise ActiveRecord::StatementInvalid, "table alias are not supported (#{o.table_alias})" if o.table_alias
390
+
391
+ # set's the index name to be queried
392
+ claim(:index, o.name)
393
+ end
394
+
395
+ def visit_Struct_Raw(o)
396
+ o
397
+ end
398
+
399
+ alias :visit_Integer :visit_Struct_Raw
400
+ alias :visit_Symbol :visit_Struct_Raw
401
+ alias :visit_Hash :visit_Struct_Raw
402
+ alias :visit_NilClass :visit_Struct_Raw
403
+ alias :visit_String :visit_Struct_Raw
404
+ alias :visit_Arel_Nodes_SqlLiteral :visit_Struct_Raw
405
+
406
+
407
+ # used by insert / update statements.
408
+ # does not claim / assign any values!
409
+ # returns a Hash of key => value pairs
410
+ def visit_Arel_Nodes_ValuesList(o)
411
+ o.rows.reduce({}) do |m, row|
412
+ row.each do |attr|
413
+ m[visit(attr.name)] = visit(attr.value)
414
+ end
415
+ m
416
+ end
417
+ end
418
+
419
+ def visit_Struct_Value(o)
420
+ o.value
421
+ end
422
+
423
+ alias :visit_ActiveModel_Attribute_WithCastValue :visit_Struct_Value
424
+
425
+ def visit_Struct_Attribute(o)
426
+ o.name
427
+ end
428
+
429
+ alias :visit_Arel_Attributes_Attribute :visit_Struct_Attribute
430
+ alias :visit_Arel_Nodes_UnqualifiedColumn :visit_Struct_Attribute
431
+ alias :visit_ActiveModel_Attribute_FromUser :visit_Struct_Attribute
432
+
433
+ def visit_Struct_BindValue(o)
434
+ # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
435
+ # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
436
+ # instead it will be send as bind and then re-delegated to the SQL collector.
437
+ #
438
+ # This only works for linear SQL-queries and not nested Hashes
439
+ # (otherwise we have to collect those binds, and replace them afterwards).
440
+ #
441
+ # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
442
+ # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
443
+ # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
444
+ self.collector.add_bind(o)
445
+
446
+ o.value
447
+ end
448
+
449
+ alias :visit_ActiveModel_Attribute :visit_Struct_BindValue
450
+ alias :visit_ActiveRecord_Relation_QueryAttribute :visit_Struct_BindValue
451
+
452
+ ##############
453
+ # DATA TYPES #
454
+ ##############
455
+
456
+ def visit_Array(o)
457
+ collect(o)
458
+ end
459
+
460
+ alias :visit_Set :visit_Array
461
+ end
462
+ end
463
+ end