elasticsearch_record 1.0.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 (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
+