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
@@ -1,590 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'arel/visitors/elasticsearch_base'
4
+ require 'arel/visitors/elasticsearch_query'
5
+ require 'arel/visitors/elasticsearch_schema'
6
+
3
7
  require 'arel/collectors/elasticsearch_query'
4
8
 
5
9
  module Arel # :nodoc: all
6
10
  module Visitors
7
11
  class Elasticsearch < Arel::Visitors::Visitor
8
- class UnsupportedVisitError < StandardError
9
- def initialize(dispatch_method)
10
- super "Unsupported dispatch method: #{dispatch_method}. Construct an Arel node instead."
11
- end
12
- end
13
-
14
- attr_accessor :collector
15
-
16
- def initialize(connection)
17
- super()
18
- @connection = connection
19
-
20
- # required for nested assignment.
21
- # see +#assign+ method
22
- @nested = false
23
- @nested_args = []
24
- end
25
-
26
- def compile(node, collector = Arel::Collectors::ElasticsearchQuery.new)
27
- # we don't need to forward the collector each time - we just set it and always access it, when we need.
28
- self.collector = collector
29
-
30
- # so we just visit the first node without any additionally provided collector ...
31
- accept(node)
32
-
33
- # ... and return the final result
34
- self.collector.value
35
- end
36
-
37
- private
38
-
39
- # auto prevent visits on missing nodes
40
- def method_missing(method, *args, &block)
41
- raise(UnsupportedVisitError, method.to_s) if method.to_s[0..4] == 'visit'
42
-
43
- super
44
- end
45
-
46
- # collects and returns provided object 'visit' result.
47
- # returns an array of results if a array was provided.
48
- # does not validate if the object is present...
49
- # @param [Object] obj
50
- # @param [Symbol] method (default: :visit)
51
- # @return [Object,nil]
52
- def collect(obj, method = :visit)
53
- if obj.is_a?(Array)
54
- obj.map { |o| self.__send__(method, o) }
55
- elsif obj.present?
56
- self.__send__(method, obj)
57
- else
58
- nil
59
- end
60
- end
61
-
62
- # resolves the provided object 'visit' result.
63
- # check if the object is present.
64
- # does not return any values
65
- # @param [Object] obj
66
- # @param [Symbol] method (default: :visit)
67
- # @return [nil]
68
- def resolve(obj, method = :visit)
69
- return unless obj.present?
70
-
71
- objects = obj.is_a?(Array) ? obj : [obj]
72
- objects.each do |obj|
73
- self.__send__(method, obj)
74
- end
75
-
76
- nil
77
- end
78
-
79
- # assign provided args on the collector.
80
- # The TOP-Level assignment must be a (key, value) while sub-assignments will be collected by provided block.
81
- # Sub-assignments will never claim on the query but 'merged' to the TOP-assignment.
82
- #
83
- # assign(:query, {}) do
84
- # #... do some stuff ...
85
- # assign(:bool, {}) do
86
- # assign(:x,99)
87
- # assign({y: 45})
88
- # end
89
- # end
90
- # #> query: {bool: {x: 99, y: 45}}
91
- def assign(*args)
92
- # resolve possible TOP-LEVEL assignment
93
- key, value = args
94
-
95
- # if a block was provided we want to collect the nested assignments
96
- if block_given?
97
- raise ArgumentError, "Unsupported assignment value for provided block (#{key}). Provide any Object as value!" if value.nil?
98
-
99
- # set nested state to tell all nested assignments to not claim it's values
100
- old_nested, @nested = @nested, true
101
- old_nested_args, @nested_args = @nested_args, []
102
-
103
- # call block, but don't interact with its return.
104
- # nested args are separately stored
105
- yield
106
-
107
- # restore nested state
108
- @nested = old_nested
109
-
110
- # assign nested args
111
- @nested_args.each do |nested_args|
112
- # ARRAY assignment
113
- case value
114
- when Array
115
- if nested_args[0].is_a?(Array)
116
- nested_args[0].each do |nested_arg|
117
- value << nested_arg
118
- end
119
- else
120
- value << nested_args[0]
121
- end
122
- when Hash
123
- if nested_args[0].is_a?(Hash)
124
- value.merge!(nested_args[0])
125
- elsif value[nested_args[0]].is_a?(Hash) && nested_args[1].is_a?(Hash)
126
- value[nested_args[0]] = value[nested_args[0]].merge(nested_args[1])
127
- elsif value[nested_args[0]].is_a?(Array) && nested_args[1].is_a?(Array)
128
- value[nested_args[0]] += nested_args[1]
129
- elsif nested_args[1].nil?
130
- value.delete(nested_args[0])
131
- else
132
- value[nested_args[0]] = nested_args[1]
133
- end
134
- when String
135
- if nested_args[0].is_a?(Array)
136
- value = value + nested_args[0].map(&:to_s).join
137
- else
138
- value = value + nested_args[0].to_s
139
- end
140
- else
141
- value = nested_args[0] unless nested_args.blank?
142
- end
143
- end
144
-
145
- # clear nested args
146
- @nested_args = old_nested_args
147
- end
148
-
149
- # for nested assignments we only want the assignable args - no +claim+ on the query!
150
- if @nested
151
- @nested_args << args
152
- return
153
- end
154
-
155
- raise ArgumentError, "Unsupported assign key: '#{key}' for provided block. Provide a Symbol as key!" unless key.is_a?(Symbol)
156
-
157
- claim(:assign, key, value)
158
- end
159
-
160
- # creates and sends a new claim to the collector.
161
- # @param [Symbol] action - claim action (:index, :type, :status, :argument, :body, :assign)
162
- # @param [Array] args - either <key>,<value> or <Hash{<key> => <value>, ...}> or <Array>
163
- def claim(action, *args)
164
- self.collector << [action, args]
165
- end
166
-
167
- ######################
168
- # CORE VISITS (CRUD) #
169
- ######################
170
-
171
- # SELECT // SEARCH
172
- def visit_Arel_Nodes_SelectStatement(o)
173
- # prepare query
174
- claim(:type, ::ElasticsearchRecord::Query::TYPE_SEARCH)
175
-
176
- resolve(o.cores) # visit_Arel_Nodes_SelectCore
177
-
178
- resolve(o.orders) # visit_Sort
179
- resolve(o.limit) # visit_Arel_Nodes_Limit
180
- resolve(o.offset) # visit_Arel_Nodes_Offset
181
-
182
- # configure is able to overwrite everything in the query
183
- resolve(o.configure)
184
- end
185
-
186
- # UPDATE
187
- def visit_Arel_Nodes_UpdateStatement(o)
188
- # switch between updating a single Record or multiple by query
189
- if o.relation.is_a?(::Arel::Table)
190
- raise NotImplementedError, "if you've made it this far, something went wrong ..."
191
- end
192
-
193
- # prepare query
194
- claim(:type, ::ElasticsearchRecord::Query::TYPE_UPDATE_BY_QUERY)
195
-
196
- # sets the index
197
- resolve(o.relation)
198
-
199
- # updating multiple entries need a script
200
- assign(:script, {}) do
201
- assign(:inline, "") do
202
- updates = collect(o.values)
203
- assign(updates.join('; ')) if updates
204
- end
205
- end
206
-
207
- # sets the search query
208
- resolve(o, :visit_Query)
209
-
210
- resolve(o.orders) # visit_Sort
211
-
212
- assign(:max_docs, collect(o.limit.expr)) if o.limit.present?
213
- resolve(o.offset)
214
-
215
- # configure is able to overwrite everything
216
- resolve(o.configure)
217
- end
218
-
219
- # DELETE
220
- def visit_Arel_Nodes_DeleteStatement(o)
221
- # switch between updating a single Record or multiple by query
222
- if o.relation.is_a?(::Arel::Table)
223
- raise NotImplementedError, "if you've made it this far, something went wrong ..."
224
- end
225
-
226
- # prepare query
227
- claim(:type, ::ElasticsearchRecord::Query::TYPE_DELETE_BY_QUERY)
228
-
229
- # sets the index
230
- resolve(o.relation)
231
-
232
- # sets the search query
233
- resolve(o, :visit_Query)
234
-
235
- resolve(o.orders) # visit_Sort
236
-
237
- assign(:max_docs, collect(o.limit.expr)) if o.limit.present?
238
- resolve(o.offset)
239
-
240
- # configure is able to overwrite everything
241
- resolve(o.configure)
242
- end
243
-
244
- # INSERT
245
- def visit_Arel_Nodes_InsertStatement(o)
246
- # switch between updating a single Record or multiple by query
247
- if o.relation.is_a?(::Arel::Table)
248
- raise NotImplementedError, "if you've made it this far, something went wrong ..."
249
- end
250
-
251
- raise NotImplementedError
252
- end
253
-
254
- ##############################
255
- # SUBSTRUCTURE VISITS (CRUD) #
256
- ##############################
257
-
258
- def visit_Arel_Nodes_SelectCore(o)
259
- # sets the index
260
- resolve(o.source)
261
-
262
- # IMPORTANT: Since Elasticsearch does not store nil-values in the +_source+ / +doc+ it will NOT return
263
- # empty / nil columns - instead the nil columns do not exist!!!
264
- # This is a big mess, because those missing columns are +not+ editable or savable in any way after we initialize the record...
265
- # To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query.
266
- resource_klass = o.source.left.instance_variable_get(:@klass)
267
- claim(:columns, resource_klass.source_column_names) if resource_klass.respond_to?(:source_column_names)
268
-
269
- # sets the query
270
- resolve(o, :visit_Query) if o.queries.present? || o.wheres.present?
271
-
272
- # sets the aggs
273
- resolve(o, :visit_Aggs) if o.aggs.present?
274
-
275
- # sets the selects
276
- resolve(o, :visit_Selects) if o.projections.present?
277
- end
278
-
279
- # CUSTOM node by elasticsearch_record
280
- def visit_Query(o)
281
- # in some cases we don't have a kind, but where conditions.
282
- # in this case we force the kind as +:bool+.
283
- kind = :bool if o.wheres.present? && o.kind.blank?
284
-
285
- # resolve kind, if not already set
286
- kind ||= o.kind.present? ? visit(o.kind.expr) : nil
287
-
288
- # check for existing kind - we cannot create a node if we don't have any kind
289
- return unless kind
290
-
291
- assign(:query, {}) do
292
- # this creates a kind node and creates nested queries
293
- # e.g. :bool => { ... }
294
- assign(kind, {}) do
295
- # each query has a type (e.g.: :filter) and one or multiple statements.
296
- # this is handled within the +visit_Arel_Nodes_SelectQuery+ method
297
- o.queries.each do |query|
298
- resolve(query) # visit_Arel_Nodes_SelectQuery
299
-
300
- # assign additional opts on the type level
301
- assign(query.opts) if query.opts.present?
302
- end
303
-
304
- # collect the where from predicate builds
305
- # should call:
306
- # - visit_Arel_Nodes_Equality
307
- # - visit_Arel_Nodes_NotEqual
308
- # - visit_Arel_Nodes_HomogeneousIn'
309
- resolve(o.wheres) if o.wheres.present?
310
-
311
- # annotations
312
- resolve(o.comment) if o.respond_to?(:comment)
313
- end
314
- end
315
- end
316
-
317
- # CUSTOM node by elasticsearch_record
318
- def visit_Aggs(o)
319
- assign(:aggs, {}) do
320
- o.aggs.each do |agg|
321
- resolve(agg)
322
-
323
- # we assign the opts on the top agg level
324
- assign(agg.opts) if agg.opts.present?
325
- end
326
- end
327
- end
328
-
329
- # CUSTOM node by elasticsearch_record
330
- def visit_Selects(o)
331
- fields = collect(o.projections)
332
-
333
- case fields[0]
334
- when '*'
335
- # force return all fields
336
- # assign(:_source, true)
337
- when ::ActiveRecord::FinderMethods::ONE_AS_ONE
338
- # force return NO fields
339
- assign(:_source, false)
340
- else
341
- assign(:_source, fields)
342
- # also overwrite the columns in the query
343
- claim(:columns, fields)
344
- end
345
- end
346
-
347
- # CUSTOM node by elasticsearch_record
348
- def visit_Arel_Nodes_SelectKind(o)
349
- visit(o.expr)
350
- end
351
-
352
- # CUSTOM node by elasticsearch_record
353
- def visit_Arel_Nodes_SelectConfigure(o)
354
- attrs = visit(o.expr)
355
-
356
- # we need to assign each key - value independently since +nil+ values will be treated as +delete+
357
- attrs.each do |key, value|
358
- assign(key, value)
359
- end if attrs.present?
360
- end
361
-
362
- # CUSTOM node by elasticsearch_record
363
- def visit_Arel_Nodes_SelectQuery(o)
364
- # this creates a query select node (includes key, value(s) and additional opts)
365
- # e.g.
366
- # :filter => [ ... ]
367
- # :must => [ ... ]
368
-
369
- # the query value must always be a array, since it might be extended by where clause.
370
- # assign(:filter, []) ...
371
- assign(visit(o.left), []) do
372
- # assign(terms: ...)
373
- assign(visit(o.right))
374
- end
375
- end
376
-
377
- # CUSTOM node by elasticsearch_record
378
- def visit_Arel_Nodes_SelectAgg(o)
379
- assign(visit(o.left) => visit(o.right))
380
- end
381
-
382
- # used to write new data to columns
383
- def visit_Arel_Nodes_Assignment(o)
384
- value = visit(o.right)
385
-
386
- value_assign = if o.right.value_before_type_cast.is_a?(Symbol)
387
- "ctx._source.#{value}"
388
- else
389
- quote(value)
390
- end
391
-
392
- "ctx._source.#{visit(o.left)} = #{value_assign}"
393
- end
394
-
395
- def visit_Arel_Nodes_Comment(o)
396
- assign(:_name, o.values.join(' - '))
397
- end
398
-
399
- # directly assigns the offset to the current scope
400
- def visit_Arel_Nodes_Offset(o)
401
- assign(:from, visit(o.expr))
402
- end
403
-
404
- # directly assigns the size to the current scope
405
- def visit_Arel_Nodes_Limit(o)
406
- assign(:size, visit(o.expr))
407
- end
408
-
409
- def visit_Sort(o)
410
- assign(:sort, {}) do
411
- key = visit(o.expr)
412
- dir = visit(o.direction)
413
-
414
- # we support a special key: __rand__ to create a simple random method ...
415
- if key == '__rand__'
416
- assign({
417
- "_script" => {
418
- "script" => "Math.random()",
419
- "type" => "number",
420
- "order" => dir
421
- }
422
- })
423
- else
424
- assign(key => dir)
425
- end
426
- end
427
- end
428
-
429
- alias :visit_Arel_Nodes_Ascending :visit_Sort
430
- alias :visit_Arel_Nodes_Descending :visit_Sort
431
-
432
-
433
- # DIRECT ASSIGNMENT
434
- def visit_Arel_Nodes_Equality(o)
435
- right = visit(o.right)
436
-
437
- return failed! if unboundable?(right)
438
-
439
- key = visit(o.left)
440
-
441
- if right.nil?
442
- # transforms nil to exists
443
- assign(:must_not, [{ exists: { field: key } }])
444
- else
445
- assign(:filter, [{ term: { key => right } }])
446
- end
447
- end
448
-
449
- # DIRECT ASSIGNMENT
450
- def visit_Arel_Nodes_NotEqual(o)
451
- right = visit(o.right)
452
-
453
- return failed! if unboundable?(right)
454
-
455
- key = visit(o.left)
456
-
457
- if right.nil?
458
- # transforms nil to exists
459
- assign(:filter, [{ exists: { field: key } }])
460
- else
461
- assign(:must_not, [{ term: { key => right } }])
462
- end
463
- end
464
-
465
- # DIRECT FAIL
466
- def visit_Arel_Nodes_Grouping(o)
467
- # grouping is NOT supported and will force to fail the query
468
- failed!
469
- end
470
-
471
- # DIRECT ASSIGNMENT
472
- def visit_Arel_Nodes_HomogeneousIn(o)
473
- self.collector.preparable = false
474
-
475
- values = o.casted_values
476
-
477
- # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
478
- # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
479
- # instead it will be send as bind and then re-delegated to the SQL collector.
480
- #
481
- # This only works for linear SQL-queries and not nested Hashes
482
- # (otherwise we have to collect those binds, and replace them afterwards).
483
- #
484
- # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
485
- # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
486
- # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
487
- self.collector.add_binds(values, o.proc_for_binds)
488
-
489
- if o.type == :in
490
- assign(:filter, [{ terms: { o.column_name => o.casted_values } }])
491
- else
492
- assign(:must_not, [{ terms: { o.column_name => o.casted_values } }])
493
- end
494
- end
495
-
496
- def visit_Arel_Nodes_And(o)
497
- collect(o.children)
498
- end
499
-
500
- def visit_Arel_Nodes_JoinSource(o)
501
- sources = []
502
- sources << visit(o.left) if o.left
503
- sources += collect(o.right) if o.right
504
-
505
- claim(:index, sources.join(', '))
506
- end
507
-
508
- def visit_Arel_Table(o)
509
- raise ActiveRecord::StatementInvalid, "table alias are not supported (#{o.table_alias})" if o.table_alias
510
-
511
- o.name
512
- end
513
-
514
- def visit_Struct_Raw(o)
515
- o
516
- end
517
-
518
- alias :visit_Symbol :visit_Struct_Raw
519
- alias :visit_Hash :visit_Struct_Raw
520
- alias :visit_NilClass :visit_Struct_Raw
521
- alias :visit_String :visit_Struct_Raw
522
- alias :visit_Arel_Nodes_SqlLiteral :visit_Struct_Raw
523
-
524
- def visit_Struct_Value(o)
525
- o.value
526
- end
527
-
528
- alias :visit_Integer :visit_Struct_Value
529
- alias :visit_ActiveModel_Attribute_WithCastValue :visit_Struct_Value
530
-
531
- def visit_Struct_Attribute(o)
532
- o.name
533
- end
534
-
535
- alias :visit_Arel_Attributes_Attribute :visit_Struct_Attribute
536
- alias :visit_Arel_Nodes_UnqualifiedColumn :visit_Struct_Attribute
537
- alias :visit_ActiveModel_Attribute_FromUser :visit_Struct_Attribute
538
-
539
- def visit_Struct_BindValue(o)
540
- # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
541
- # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
542
- # instead it will be send as bind and then re-delegated to the SQL collector.
543
- #
544
- # This only works for linear SQL-queries and not nested Hashes
545
- # (otherwise we have to collect those binds, and replace them afterwards).
546
- #
547
- # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
548
- # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
549
- # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
550
- self.collector.add_bind(o)
551
-
552
- o.value
553
- end
554
-
555
-
556
- alias :visit_ActiveModel_Attribute :visit_Struct_BindValue
557
- alias :visit_ActiveRecord_Relation_QueryAttribute :visit_Struct_BindValue
558
-
559
- ##############
560
- # DATA TYPES #
561
- ##############
562
-
563
- def visit_Array(o)
564
- collect(o)
565
- end
566
-
567
- alias :visit_Set :visit_Array
568
-
569
- ###########
570
- # HELPERS #
571
- ###########
572
-
573
- def unboundable?(value)
574
- value.respond_to?(:unboundable?) && value.unboundable?
575
- end
576
-
577
- def quote(value)
578
- return value if Arel::Nodes::SqlLiteral === value
579
- @connection.quote value
580
- end
581
-
582
- # assigns a failed status to the current query
583
- def failed!
584
- claim(:status, ElasticsearchRecord::Query::STATUS_FAILED)
12
+ include ElasticsearchBase
13
+ include ElasticsearchQuery
14
+ include ElasticsearchSchema
585
15
 
586
- nil
587
- end
588
16
  end
589
17
  end
590
18
  end