elasticsearch_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +74 -0
  6. data/README.md +216 -0
  7. data/Rakefile +8 -0
  8. data/docs/CHANGELOG.md +44 -0
  9. data/docs/CODE_OF_CONDUCT.md +84 -0
  10. data/docs/LICENSE.txt +21 -0
  11. data/lib/active_record/connection_adapters/elasticsearch/column.rb +32 -0
  12. data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +149 -0
  13. data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +38 -0
  14. data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +134 -0
  15. data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +28 -0
  16. data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +52 -0
  17. data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +44 -0
  18. data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +42 -0
  19. data/lib/active_record/connection_adapters/elasticsearch/type.rb +16 -0
  20. data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +197 -0
  21. data/lib/arel/collectors/elasticsearch_query.rb +112 -0
  22. data/lib/arel/nodes/select_agg.rb +22 -0
  23. data/lib/arel/nodes/select_configure.rb +9 -0
  24. data/lib/arel/nodes/select_kind.rb +9 -0
  25. data/lib/arel/nodes/select_query.rb +20 -0
  26. data/lib/arel/visitors/elasticsearch.rb +589 -0
  27. data/lib/elasticsearch_record/base.rb +14 -0
  28. data/lib/elasticsearch_record/core.rb +59 -0
  29. data/lib/elasticsearch_record/extensions/relation.rb +15 -0
  30. data/lib/elasticsearch_record/gem_version.rb +17 -0
  31. data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +39 -0
  32. data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +70 -0
  33. data/lib/elasticsearch_record/instrumentation/railtie.rb +16 -0
  34. data/lib/elasticsearch_record/instrumentation.rb +17 -0
  35. data/lib/elasticsearch_record/model_schema.rb +43 -0
  36. data/lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb +85 -0
  37. data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +64 -0
  38. data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +91 -0
  39. data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +41 -0
  40. data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +46 -0
  41. data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +60 -0
  42. data/lib/elasticsearch_record/persistence.rb +80 -0
  43. data/lib/elasticsearch_record/query.rb +129 -0
  44. data/lib/elasticsearch_record/querying.rb +90 -0
  45. data/lib/elasticsearch_record/relation/calculation_methods.rb +155 -0
  46. data/lib/elasticsearch_record/relation/core_methods.rb +64 -0
  47. data/lib/elasticsearch_record/relation/query_clause.rb +43 -0
  48. data/lib/elasticsearch_record/relation/query_clause_tree.rb +94 -0
  49. data/lib/elasticsearch_record/relation/query_methods.rb +276 -0
  50. data/lib/elasticsearch_record/relation/result_methods.rb +222 -0
  51. data/lib/elasticsearch_record/relation/value_methods.rb +54 -0
  52. data/lib/elasticsearch_record/result.rb +236 -0
  53. data/lib/elasticsearch_record/statement_cache.rb +87 -0
  54. data/lib/elasticsearch_record/version.rb +10 -0
  55. data/lib/elasticsearch_record.rb +60 -0
  56. data/sig/elasticsearch_record.rbs +4 -0
  57. metadata +175 -0
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Nodes
5
+ class SelectConfigure < Unary
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Nodes
5
+ class SelectKind < Unary
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Nodes
5
+ class SelectQuery < Unary
6
+
7
+ def left
8
+ expr[0]
9
+ end
10
+
11
+ def right
12
+ expr[1]
13
+ end
14
+
15
+ def opts
16
+ expr[2]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,589 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arel/collectors/elasticsearch_query'
4
+
5
+ module Arel # :nodoc: all
6
+ module Visitors
7
+ 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
+ claim(:columns, o.source.left.instance_variable_get(:@klass).source_column_names)
267
+
268
+ # sets the query
269
+ resolve(o, :visit_Query) if o.queries.present? || o.wheres.present?
270
+
271
+ # sets the aggs
272
+ resolve(o, :visit_Aggs) if o.aggs.present?
273
+
274
+ # sets the selects
275
+ resolve(o, :visit_Selects) if o.projections.present?
276
+ end
277
+
278
+ # CUSTOM node by elasticsearch_record
279
+ def visit_Query(o)
280
+ # in some cases we don't have a kind, but where conditions.
281
+ # in this case we force the kind as +:bool+.
282
+ kind = :bool if o.wheres.present? && o.kind.blank?
283
+
284
+ # resolve kind, if not already set
285
+ kind ||= o.kind.present? ? visit(o.kind.expr) : nil
286
+
287
+ # check for existing kind - we cannot create a node if we don't have any kind
288
+ return unless kind
289
+
290
+ assign(:query, {}) do
291
+ # this creates a kind node and creates nested queries
292
+ # e.g. :bool => { ... }
293
+ assign(kind, {}) do
294
+ # each query has a type (e.g.: :filter) and one or multiple statements.
295
+ # this is handled within the +visit_Arel_Nodes_SelectQuery+ method
296
+ o.queries.each do |query|
297
+ resolve(query) # visit_Arel_Nodes_SelectQuery
298
+
299
+ # assign additional opts on the type level
300
+ assign(query.opts) if query.opts.present?
301
+ end
302
+
303
+ # collect the where from predicate builds
304
+ # should call:
305
+ # - visit_Arel_Nodes_Equality
306
+ # - visit_Arel_Nodes_NotEqual
307
+ # - visit_Arel_Nodes_HomogeneousIn'
308
+ resolve(o.wheres) if o.wheres.present?
309
+
310
+ # annotations
311
+ resolve(o.comment) if o.respond_to?(:comment)
312
+ end
313
+ end
314
+ end
315
+
316
+ # CUSTOM node by elasticsearch_record
317
+ def visit_Aggs(o)
318
+ assign(:aggs, {}) do
319
+ o.aggs.each do |agg|
320
+ resolve(agg)
321
+
322
+ # we assign the opts on the top agg level
323
+ assign(agg.opts) if agg.opts.present?
324
+ end
325
+ end
326
+ end
327
+
328
+ # CUSTOM node by elasticsearch_record
329
+ def visit_Selects(o)
330
+ fields = collect(o.projections)
331
+
332
+ case fields[0]
333
+ when '*'
334
+ # force return all fields
335
+ # assign(:_source, true)
336
+ when ::ActiveRecord::FinderMethods::ONE_AS_ONE
337
+ # force return NO fields
338
+ assign(:_source, false)
339
+ else
340
+ assign(:_source, fields)
341
+ # also overwrite the columns in the query
342
+ claim(:columns, fields)
343
+ end
344
+ end
345
+
346
+ # CUSTOM node by elasticsearch_record
347
+ def visit_Arel_Nodes_SelectKind(o)
348
+ visit(o.expr)
349
+ end
350
+
351
+ # CUSTOM node by elasticsearch_record
352
+ def visit_Arel_Nodes_SelectConfigure(o)
353
+ attrs = visit(o.expr)
354
+
355
+ # we need to assign each key - value independently since +nil+ values will be treated as +delete+
356
+ attrs.each do |key, value|
357
+ assign(key, value)
358
+ end if attrs.present?
359
+ end
360
+
361
+ # CUSTOM node by elasticsearch_record
362
+ def visit_Arel_Nodes_SelectQuery(o)
363
+ # this creates a query select node (includes key, value(s) and additional opts)
364
+ # e.g.
365
+ # :filter => [ ... ]
366
+ # :must => [ ... ]
367
+
368
+ # the query value must always be a array, since it might be extended by where clause.
369
+ # assign(:filter, []) ...
370
+ assign(visit(o.left), []) do
371
+ # assign(terms: ...)
372
+ assign(visit(o.right))
373
+ end
374
+ end
375
+
376
+ # CUSTOM node by elasticsearch_record
377
+ def visit_Arel_Nodes_SelectAgg(o)
378
+ assign(visit(o.left) => visit(o.right))
379
+ end
380
+
381
+ # used to write new data to columns
382
+ def visit_Arel_Nodes_Assignment(o)
383
+ value = visit(o.right)
384
+
385
+ value_assign = if o.right.value_before_type_cast.is_a?(Symbol)
386
+ "ctx._source.#{value}"
387
+ else
388
+ quote(value)
389
+ end
390
+
391
+ "ctx._source.#{visit(o.left)} = #{value_assign}"
392
+ end
393
+
394
+ def visit_Arel_Nodes_Comment(o)
395
+ assign(:_name, o.values.join(' - '))
396
+ end
397
+
398
+ # directly assigns the offset to the current scope
399
+ def visit_Arel_Nodes_Offset(o)
400
+ assign(:from, visit(o.expr))
401
+ end
402
+
403
+ # directly assigns the size to the current scope
404
+ def visit_Arel_Nodes_Limit(o)
405
+ assign(:size, visit(o.expr))
406
+ end
407
+
408
+ def visit_Sort(o)
409
+ assign(:sort, {}) do
410
+ key = visit(o.expr)
411
+ dir = visit(o.direction)
412
+
413
+ # we support a special key: _rand to create a simple random method ...
414
+ if key == '_rand'
415
+ assign({
416
+ "_script" => {
417
+ "script" => "Math.random()",
418
+ "type" => "number",
419
+ "order" => dir
420
+ }
421
+ })
422
+ else
423
+ assign(key => dir)
424
+ end
425
+ end
426
+ end
427
+
428
+ alias :visit_Arel_Nodes_Ascending :visit_Sort
429
+ alias :visit_Arel_Nodes_Descending :visit_Sort
430
+
431
+
432
+ # DIRECT ASSIGNMENT
433
+ def visit_Arel_Nodes_Equality(o)
434
+ right = visit(o.right)
435
+
436
+ return failed! if unboundable?(right)
437
+
438
+ key = visit(o.left)
439
+
440
+ if right.nil?
441
+ # transforms nil to exists
442
+ assign(:must_not, [{ exists: { field: key } }])
443
+ else
444
+ assign(:filter, [{ term: { key => right } }])
445
+ end
446
+ end
447
+
448
+ # DIRECT ASSIGNMENT
449
+ def visit_Arel_Nodes_NotEqual(o)
450
+ right = visit(o.right)
451
+
452
+ return failed! if unboundable?(right)
453
+
454
+ key = visit(o.left)
455
+
456
+ if right.nil?
457
+ # transforms nil to exists
458
+ assign(:filter, [{ exists: { field: key } }])
459
+ else
460
+ assign(:must_not, [{ term: { key => right } }])
461
+ end
462
+ end
463
+
464
+ # DIRECT FAIL
465
+ def visit_Arel_Nodes_Grouping(o)
466
+ # grouping is NOT supported and will force to fail the query
467
+ failed!
468
+ end
469
+
470
+ # DIRECT ASSIGNMENT
471
+ def visit_Arel_Nodes_HomogeneousIn(o)
472
+ self.collector.preparable = false
473
+
474
+ values = o.casted_values
475
+
476
+ # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
477
+ # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
478
+ # instead it will be send as bind and then re-delegated to the SQL collector.
479
+ #
480
+ # This only works for linear SQL-queries and not nested Hashes
481
+ # (otherwise we have to collect those binds, and replace them afterwards).
482
+ #
483
+ # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
484
+ # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
485
+ # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
486
+ self.collector.add_binds(values, o.proc_for_binds)
487
+
488
+ if o.type == :in
489
+ assign(:filter, [{ terms: { o.column_name => o.casted_values } }])
490
+ else
491
+ assign(:must_not, [{ terms: { o.column_name => o.casted_values } }])
492
+ end
493
+ end
494
+
495
+ def visit_Arel_Nodes_And(o)
496
+ collect(o.children)
497
+ end
498
+
499
+ def visit_Arel_Nodes_JoinSource(o)
500
+ sources = []
501
+ sources << visit(o.left) if o.left
502
+ sources += collect(o.right) if o.right
503
+
504
+ claim(:index, sources.join(', '))
505
+ end
506
+
507
+ def visit_Arel_Table(o)
508
+ raise ActiveRecord::StatementInvalid, "table alias are not supported (#{o.table_alias})" if o.table_alias
509
+
510
+ o.name
511
+ end
512
+
513
+ def visit_Struct_Raw(o)
514
+ o
515
+ end
516
+
517
+ alias :visit_Symbol :visit_Struct_Raw
518
+ alias :visit_Hash :visit_Struct_Raw
519
+ alias :visit_NilClass :visit_Struct_Raw
520
+ alias :visit_String :visit_Struct_Raw
521
+ alias :visit_Arel_Nodes_SqlLiteral :visit_Struct_Raw
522
+
523
+ def visit_Struct_Value(o)
524
+ o.value
525
+ end
526
+
527
+ alias :visit_Integer :visit_Struct_Value
528
+ alias :visit_ActiveModel_Attribute_WithCastValue :visit_Struct_Value
529
+
530
+ def visit_Struct_Attribute(o)
531
+ o.name
532
+ end
533
+
534
+ alias :visit_Arel_Attributes_Attribute :visit_Struct_Attribute
535
+ alias :visit_Arel_Nodes_UnqualifiedColumn :visit_Struct_Attribute
536
+ alias :visit_ActiveModel_Attribute_FromUser :visit_Struct_Attribute
537
+
538
+ def visit_Struct_BindValue(o)
539
+ # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
540
+ # will +not+ directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
541
+ # instead it will be send as bind and then re-delegated to the SQL collector.
542
+ #
543
+ # This only works for linear SQL-queries and not nested Hashes
544
+ # (otherwise we have to collect those binds, and replace them afterwards).
545
+ #
546
+ # Here, we'll directly assign the "real" _(casted)_ values but also provide a additional bind.
547
+ # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
548
+ # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
549
+ self.collector.add_bind(o)
550
+
551
+ o.value
552
+ end
553
+
554
+
555
+ alias :visit_ActiveModel_Attribute :visit_Struct_BindValue
556
+ alias :visit_ActiveRecord_Relation_QueryAttribute :visit_Struct_BindValue
557
+
558
+ ##############
559
+ # DATA TYPES #
560
+ ##############
561
+
562
+ def visit_Array(o)
563
+ collect(o)
564
+ end
565
+
566
+ alias :visit_Set :visit_Array
567
+
568
+ ###########
569
+ # HELPERS #
570
+ ###########
571
+
572
+ def unboundable?(value)
573
+ value.respond_to?(:unboundable?) && value.unboundable?
574
+ end
575
+
576
+ def quote(value)
577
+ return value if Arel::Nodes::SqlLiteral === value
578
+ @connection.quote value
579
+ end
580
+
581
+ # assigns a failed status to the current query
582
+ def failed!
583
+ claim(:status, ElasticsearchRecord::Query::STATUS_FAILED)
584
+
585
+ nil
586
+ end
587
+ end
588
+ end
589
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_record'
2
+
3
+ module ElasticsearchRecord
4
+ class Base < ::ActiveRecord::Base
5
+
6
+ include Core
7
+ include ModelSchema
8
+ include Persistence
9
+ include Querying
10
+
11
+ self.abstract_class = true
12
+ connects_to database: { writing: :elasticsearch, reading: :elasticsearch }
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ module ElasticsearchRecord
2
+ module Core
3
+ extend ActiveSupport::Concern
4
+
5
+ # in default, this reads the primary key column's value (+_id+).
6
+ # But since elasticsearch supports also additional "id" columns, we need to check against that.
7
+ def id
8
+ has_attribute?('id') ? _read_attribute('id') : super
9
+ end
10
+
11
+ # in default, this sets the primary key column's value (+_id+).
12
+ # But since elasticsearch supports also additional "id" columns, we need to check against that.
13
+ def id=(value)
14
+ has_attribute?('id') ? _write_attribute('id', value) : super
15
+ end
16
+
17
+ # overwrite the write_attribute method to write 'id', if present?
18
+ # see @ ActiveRecord::AttributeMethods::Write#write_attribute
19
+ def write_attribute(attr_name, value)
20
+ return _write_attribute('id', value) if attr_name.to_s == 'id' && has_attribute?('id')
21
+
22
+ super
23
+ end
24
+
25
+ # overwrite read_attribute method to read 'id', if present?
26
+ # see @ ActiveRecord::AttributeMethods::Read#read_attribute
27
+ def read_attribute(attr_name, &block)
28
+ return _read_attribute('id', &block) if attr_name.to_s == 'id' && has_attribute?('id')
29
+
30
+ super
31
+ end
32
+
33
+ module ClassMethods
34
+ # used to create a cacheable statement.
35
+ # This is a 1:1 copy, except that we use our own class +ElasticsearchRecord::StatementCache+
36
+ # see @ ActiveRecord::Core::ClassMethods#cached_find_by_statement
37
+ def cached_find_by_statement(key, &block)
38
+ cache = @find_by_statement_cache[connection.prepared_statements]
39
+ cache.compute_if_absent(key) { ElasticsearchRecord::StatementCache.create(connection, &block) }
40
+ end
41
+
42
+ private
43
+
44
+ # creates a new relation object and extends it with our own Relation.
45
+ # @see ActiveRecord::Core::ClassMethods#relation
46
+ def relation
47
+ relation = super
48
+ # sucks, but there is no other solution yet to NOT mess with
49
+ # ActiveRecord::Delegation::DelegateCache#initialize_relation_delegate_cache
50
+ relation.extend ElasticsearchRecord::Extensions::Relation
51
+ relation
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+
59
+