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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +74 -0
- data/README.md +216 -0
- data/Rakefile +8 -0
- data/docs/CHANGELOG.md +44 -0
- data/docs/CODE_OF_CONDUCT.md +84 -0
- data/docs/LICENSE.txt +21 -0
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +32 -0
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +149 -0
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +38 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +134 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +28 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +52 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +44 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +42 -0
- data/lib/active_record/connection_adapters/elasticsearch/type.rb +16 -0
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +197 -0
- data/lib/arel/collectors/elasticsearch_query.rb +112 -0
- data/lib/arel/nodes/select_agg.rb +22 -0
- data/lib/arel/nodes/select_configure.rb +9 -0
- data/lib/arel/nodes/select_kind.rb +9 -0
- data/lib/arel/nodes/select_query.rb +20 -0
- data/lib/arel/visitors/elasticsearch.rb +589 -0
- data/lib/elasticsearch_record/base.rb +14 -0
- data/lib/elasticsearch_record/core.rb +59 -0
- data/lib/elasticsearch_record/extensions/relation.rb +15 -0
- data/lib/elasticsearch_record/gem_version.rb +17 -0
- data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +39 -0
- data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +70 -0
- data/lib/elasticsearch_record/instrumentation/railtie.rb +16 -0
- data/lib/elasticsearch_record/instrumentation.rb +17 -0
- data/lib/elasticsearch_record/model_schema.rb +43 -0
- data/lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb +85 -0
- data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +64 -0
- data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +91 -0
- data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +41 -0
- data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +46 -0
- data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +60 -0
- data/lib/elasticsearch_record/persistence.rb +80 -0
- data/lib/elasticsearch_record/query.rb +129 -0
- data/lib/elasticsearch_record/querying.rb +90 -0
- data/lib/elasticsearch_record/relation/calculation_methods.rb +155 -0
- data/lib/elasticsearch_record/relation/core_methods.rb +64 -0
- data/lib/elasticsearch_record/relation/query_clause.rb +43 -0
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +94 -0
- data/lib/elasticsearch_record/relation/query_methods.rb +276 -0
- data/lib/elasticsearch_record/relation/result_methods.rb +222 -0
- data/lib/elasticsearch_record/relation/value_methods.rb +54 -0
- data/lib/elasticsearch_record/result.rb +236 -0
- data/lib/elasticsearch_record/statement_cache.rb +87 -0
- data/lib/elasticsearch_record/version.rb +10 -0
- data/lib/elasticsearch_record.rb +60 -0
- data/sig/elasticsearch_record.rbs +4 -0
- metadata +175 -0
@@ -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
|
+
|