elasticsearch_record 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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