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,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Elasticsearch
6
+ module Quoting # :nodoc:
7
+ # Quotes the column value to help prevent
8
+ def quote(value)
9
+ case value
10
+ # those values do not need to be quoted
11
+ when BigDecimal, Numeric, String, Symbol, nil, true, false then value
12
+ when ActiveSupport::Duration then value.to_i
13
+ when Array then value.map { |val| quote(val) }
14
+ when Hash then value.transform_values { |val| quote(val) }
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def quoted_true
21
+ true
22
+ end
23
+
24
+ def unquoted_true
25
+ true
26
+ end
27
+
28
+ def quoted_false
29
+ false
30
+ end
31
+
32
+ def unquoted_false
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Elasticsearch
6
+ module SchemaStatements # :nodoc:
7
+ # Returns the relation names usable to back Active Record models.
8
+ # For Elasticsearch this means all indices - which also includes system +dot+ '.' indices.
9
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#data_sources
10
+ # @return [Array<String>]
11
+ def data_sources
12
+ api(:indices, :get_settings, { index: :_all }, 'SCHEMA').keys
13
+ end
14
+
15
+ # returns a hash of all mappings by provided index_name
16
+ # @param [String] index_name
17
+ # @return [Hash]
18
+ def mappings(index_name)
19
+ api(:indices, :get_mapping, { index: index_name }, 'SCHEMA').dig(index_name, 'mappings')
20
+ end
21
+
22
+ # returns a hash of all settings by provided index_name
23
+ # @param [String] index_name
24
+ # @return [Hash]
25
+ def settings(index_name)
26
+ api(:indices, :get_settings, { index: index_name }, 'SCHEMA').dig(index_name, 'settings','index')
27
+ end
28
+
29
+ # Returns the list of a table's column names, data types, and default values.
30
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#columns
31
+ # @see ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#column_definitions
32
+ # @param [String] table_name
33
+ # @return [Array<Hash>]
34
+ def column_definitions(table_name)
35
+ structure = mappings(table_name)
36
+ raise(ActiveRecord::StatementInvalid, "Could not find elasticsearch index '#{table_name}'") if structure.blank? || structure['properties'].blank?
37
+
38
+ # since the received mappings do not have the "primary" +_id+-column we manually need to add this here
39
+ # The BASE_STRUCTURE will also include some meta keys like '_score', '_type', ...
40
+ ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::BASE_STRUCTURE + structure['properties'].map { |key, prop|
41
+ # mappings can have +fields+ - we also want them for 'query-conditions'
42
+ # that can be resolved through +.column_names+
43
+ fields = prop.delete('fields') || []
44
+
45
+ # we need to merge the name & possible nested fields (which are also searchable)
46
+ prop.merge('name' => key, 'fields' => fields.map { |fkey, _field| "#{key}.#{fkey}" })
47
+ }
48
+ end
49
+
50
+ # creates a new column object from provided field Hash
51
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#columns
52
+ # @see ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#new_column_from_field
53
+ # @param [String] _table_name
54
+ # @param [Hash] field
55
+ # @return [ActiveRecord::ConnectionAdapters::Column]
56
+ def new_column_from_field(_table_name, field)
57
+ # fallback for possible empty type
58
+ field_type = field['type'].presence || (field['properties'].present? ? 'nested' : 'object')
59
+
60
+ ActiveRecord::ConnectionAdapters::Elasticsearch::Column.new(
61
+ field["name"],
62
+ field["null_value"],
63
+ fetch_type_metadata(field_type),
64
+ field['null'].nil? ? true : field['null'],
65
+ nil,
66
+ comment: field['meta'] ? field['meta'].map { |k, v| "#{k}: #{v}" }.join(' | ') : nil,
67
+ virtual: field['virtual'],
68
+ fields: field['fields']
69
+ )
70
+ end
71
+
72
+ # lookups from building the @columns_hash.
73
+ # since Elasticsearch has the "feature" to provide multicast values on any type, we need to fetch them ...
74
+ # you know, ES can return an integer or an array of integers for any column ...
75
+ # @param [ActiveRecord::ConnectionAdapters::Elasticsearch::Column] column
76
+ # @return [ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue]
77
+ def lookup_cast_type_from_column(column)
78
+ type_map.lookup(:multicast_value, super)
79
+ end
80
+
81
+ # Returns a array of tables primary keys.
82
+ # PLEASE NOTE: Elasticsearch does not have a concept of primary key.
83
+ # The only thing that uniquely identifies a document is the index together with the +_id+.
84
+ # To not break the "ConnectionAdapters" concept we simulate this through the BASE_STRUCTURE.
85
+ # We know, we can just return '_id' here ...
86
+ # @see ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#primary_keys
87
+ # @param [String] _table_name
88
+ def primary_keys(_table_name)
89
+ ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::BASE_STRUCTURE
90
+ .select { |f| f["primary"] }
91
+ .map { |f| f["name"] }
92
+ end
93
+
94
+ # Checks to see if the data source +name+ exists on the database.
95
+ #
96
+ # data_source_exists?(:ebooks)
97
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#data_source_exists?
98
+ # @param [String, Symbol] name
99
+ # @return [Boolean]
100
+ def data_source_exists?(name)
101
+ # response returns boolean
102
+ api(:indices, :exists?, { index: name }, 'SCHEMA')
103
+ end
104
+
105
+ # Returns an array of table names defined in the database.
106
+ # For Elasticsearch this means all normal indices (no system +dot+ '.' indices)
107
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#tables
108
+ # @return [Array<String>]
109
+ def tables
110
+ data_sources.reject { |key| key[0] == '.' }
111
+ end
112
+
113
+ # Checks to see if the table +table_name+ exists on the database.
114
+ #
115
+ # table_exists?(:developers)
116
+ #
117
+ # @see ActiveRecord::ConnectionAdapters::SchemaStatements#table_exists?
118
+ # @param [String, Symbol] table_name
119
+ # @return [Boolean]
120
+ def table_exists?(table_name)
121
+ # just reference to the data sources
122
+ data_source_exists?(table_name)
123
+ end
124
+
125
+ # returns the maximum allowed size for queries.
126
+ # The query will raise an ActiveRecord::StatementInvalid if the requested limit is above this value.
127
+ # @return [Integer]
128
+ def max_result_window
129
+ 10000
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Elasticsearch
4
+ module Type # :nodoc:
5
+ class FormatString < ActiveRecord::Type::String
6
+ attr_reader :format
7
+
8
+ def initialize(**args)
9
+ @format = args.delete(:format).presence || /.*/
10
+ super
11
+ end
12
+
13
+ def type
14
+ :format_string
15
+ end
16
+
17
+ private
18
+
19
+ def cast_value(value)
20
+ return value unless ::String === value
21
+ return '' unless value.match(format)
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Elasticsearch
4
+ module Type # :nodoc:
5
+ class MulticastValue < ActiveRecord::Type::Value
6
+
7
+ attr_reader :nested_type
8
+
9
+ def initialize(nested_type: nil, **)
10
+ @nested_type = nested_type || ActiveRecord::Type::Value.new
11
+ end
12
+
13
+ def type
14
+ nested_type.type
15
+ end
16
+
17
+ # overwrites the default deserialize behaviour
18
+ # @param [Object] value
19
+ # @return [Object,nil] deserialized object
20
+ def deserialize(value)
21
+ cast(_deserialize(value))
22
+ end
23
+
24
+ private
25
+
26
+ def _deserialize(value)
27
+ # check for the special +object+ type which is forced to be casted
28
+ return _deserialize_by_nested_type(value) if nested_type.type == :object && nested_type.forced?
29
+
30
+ if value.is_a?(Array)
31
+ value.map { |val| _deserialize_by_nested_type(val) }
32
+ elsif value.is_a?(Hash)
33
+ value.reduce({}) { |m, (key, val)|
34
+ m[key] = _deserialize_by_nested_type(val)
35
+ m
36
+ }
37
+ else
38
+ _deserialize_by_nested_type(value)
39
+ end
40
+ end
41
+
42
+ # in some cases we cannot deserialize, since the ES-type don't match well with the provided value
43
+ # but the result should be ok, as it is (e.g. 'Hash')...
44
+ # so we rescue here with just the provided value
45
+ def _deserialize_by_nested_type(value)
46
+ nested_type.deserialize(value) rescue value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Elasticsearch
4
+ module Type # :nodoc:
5
+ class Object < ActiveRecord::Type::Value
6
+ attr_reader :cast_type, :default
7
+
8
+ # creates a new object type which can be natively every value.
9
+ # Providing a +cast+ will call the value with this method (or callback)
10
+ # @param [nil, Symbol, String, Proc] cast - the cast type
11
+ # @param [nil, Object] default
12
+ # @param [Boolean] force - force value to be casted (used by the MulticastValue type) - (default: false)
13
+ def initialize(cast: nil, default: nil, force: false, **)
14
+ @cast_type = cast
15
+ @default = default
16
+ @force = force
17
+ end
18
+
19
+ def type
20
+ :object
21
+ end
22
+
23
+ def forced?
24
+ @force
25
+ end
26
+
27
+ private
28
+
29
+ # cast value by provided cast_method
30
+ def cast_value(value)
31
+ case self.cast_type
32
+ when Symbol, String
33
+ value.public_send(self.cast_type) rescue default
34
+ when Proc
35
+ self.cast_type.(value) rescue default
36
+ else
37
+ value
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Elasticsearch
4
+ module Type # :nodoc:
5
+ class Range < MulticastValue
6
+
7
+ def type
8
+ "range_#{nested_type.type}".to_sym
9
+ end
10
+
11
+ private
12
+
13
+ def cast_value(value)
14
+ return (0..0) unless value.is_a?(Hash)
15
+ # check for existing gte & lte
16
+
17
+ min_value = if value['gte']
18
+ value['gte']
19
+ elsif value['gt']
20
+ value['gt'] + 1
21
+ else
22
+ nil
23
+ end
24
+
25
+ max_value = if value['lte']
26
+ value['lte']
27
+ elsif value['lt']
28
+ value['lt'] - 1
29
+ else
30
+ nil
31
+ end
32
+
33
+ return (0..0) if min_value.nil? || max_value.nil?
34
+
35
+ # build & return range
36
+ (min_value..max_value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/elasticsearch/type/format_string'
4
+ require 'active_record/connection_adapters/elasticsearch/type/multicast_value'
5
+ require 'active_record/connection_adapters/elasticsearch/type/object'
6
+ require 'active_record/connection_adapters/elasticsearch/type/range'
7
+
8
+ module ActiveRecord
9
+ module ConnectionAdapters
10
+ module Elasticsearch
11
+ module Type # :nodoc:
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters'
4
+
5
+ # new
6
+ require 'active_record/connection_adapters/elasticsearch/column'
7
+ require 'active_record/connection_adapters/elasticsearch/database_statements'
8
+ require 'active_record/connection_adapters/elasticsearch/quoting'
9
+ require 'active_record/connection_adapters/elasticsearch/schema_statements'
10
+ require 'active_record/connection_adapters/elasticsearch/type'
11
+
12
+ require 'arel/visitors/elasticsearch'
13
+ require 'arel/collectors/elasticsearch_query'
14
+
15
+ gem 'elasticsearch'
16
+ require 'elasticsearch'
17
+
18
+ module ActiveRecord # :nodoc:
19
+ module ConnectionHandling # :nodoc:
20
+ def elasticsearch_connection(config)
21
+ config = config.symbolize_keys
22
+
23
+ # move 'host' to 'hosts'
24
+ config[:hosts] = config.delete(:host) if config[:host]
25
+
26
+ # enable logging (Rails.logger)
27
+ config[:logger] = logger if config.delete(:log)
28
+
29
+ ConnectionAdapters::ElasticsearchAdapter.new(
30
+ ConnectionAdapters::ElasticsearchAdapter.new_client(config),
31
+ logger,
32
+ config
33
+ )
34
+ end
35
+ end
36
+
37
+ module ConnectionAdapters # :nodoc:
38
+ class ElasticsearchAdapter < AbstractAdapter
39
+ ADAPTER_NAME = "Elasticsearch"
40
+
41
+ # defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
42
+ BASE_STRUCTURE = [
43
+ { 'name' => '_id', 'type' => 'string', 'null' => false, 'primary' => true },
44
+ { 'name' => '_index', 'type' => 'string', 'null' => false, 'virtual' => true },
45
+ { 'name' => '_score', 'type' => 'float', 'null' => false, 'virtual' => true },
46
+ { 'name' => '_type', 'type' => 'string', 'null' => false, 'virtual' => true }
47
+ ].freeze
48
+
49
+ include Elasticsearch::Quoting
50
+ include Elasticsearch::SchemaStatements
51
+ include Elasticsearch::DatabaseStatements
52
+
53
+ class << self
54
+ def base_structure_keys
55
+ @base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
56
+ end
57
+
58
+ def new_client(config)
59
+ # IMPORTANT: remove +adapter+ from config - otherwise we mess up with Faraday::AdapterRegistry
60
+ client = ::Elasticsearch::Client.new(config.except(:adapter))
61
+ client.ping
62
+ client
63
+ rescue ::Elastic::Transport::Transport::ServerError => error
64
+ raise ::ActiveRecord::ConnectionNotEstablished, error.message
65
+ end
66
+
67
+ private
68
+
69
+ def initialize_type_map(m)
70
+ m.register_type 'binary', Type::Binary.new
71
+ m.register_type 'boolean', Type::Boolean.new
72
+ m.register_type 'keyword', Type::String.new
73
+
74
+ m.alias_type 'constant_keyword', 'keyword'
75
+ m.alias_type 'wildcard', 'keyword'
76
+
77
+ # maybe use integer 8 here ...
78
+ m.register_type 'long', Type::BigInteger.new
79
+ m.register_type 'integer', Type::Integer.new
80
+ m.register_type 'short', Type::Integer.new(limit: 2)
81
+ m.register_type 'byte', Type::Integer.new(limit: 1)
82
+ m.register_type 'double', Type::Float.new(limit: 8)
83
+ m.register_type 'float', Type::Float.new(limit: 4)
84
+ m.register_type 'half_float', Type::Float.new(limit: 2)
85
+ m.register_type 'scaled_float', Type::Float.new(limit: 8, scale: 8)
86
+ m.register_type 'unsigned_long', Type::UnsignedInteger.new
87
+
88
+ m.register_type 'date', Type::DateTime.new
89
+
90
+ # force a hash
91
+ m.register_type 'object', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h, force: true)
92
+ m.alias_type 'flattened', "object"
93
+
94
+ # array of objects
95
+ m.register_type 'nested', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h)
96
+
97
+ ip_type = ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString.new(format: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)
98
+
99
+ m.register_type 'integer_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Integer.new)
100
+ m.register_type 'float_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Float.new(limit: 4))
101
+ m.register_type 'long_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Integer.new(limit: 8))
102
+ m.register_type 'double_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Float.new(limit: 8))
103
+ m.register_type 'date_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::DateTime.new)
104
+ m.register_type 'ip_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: ip_type)
105
+
106
+ m.register_type 'ip', ip_type
107
+ m.register_type 'version', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString.new(format: /^\d+\.\d+\.\d+[\-\+A-Za-z\.]*$/)
108
+ # m.register_type 'murmur3', Murmur3.new
109
+
110
+ m.register_type 'text', Type::Text.new
111
+
112
+ # this special Type is required to parse a ES-value into the +nested_type+, array or hash.
113
+ # For arrays & hashes it tries to cast the values with the provided +nested_type+
114
+ # but falls back to provided value if cast fails.
115
+ # This type cannot be accessed through the mapping and is only called @ #lookup_cast_type_from_column
116
+ # @see ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaStatements#lookup_cast_type_from_column
117
+ m.register_type :multicast_value do |_type, nested_type|
118
+ ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue.new(nested_type: nested_type)
119
+ end
120
+ end
121
+ end
122
+
123
+ # reinitialize the constant with new types
124
+ TYPE_MAP = ActiveRecord::Type::HashLookupTypeMap.new.tap { |m| initialize_type_map(m) }
125
+
126
+ def initialize(*args)
127
+ super(*args)
128
+
129
+ # prepared statements are not supported by Elasticsearch.
130
+ # documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
131
+ @prepared_statements = false
132
+ end
133
+
134
+ private
135
+
136
+ def type_map
137
+ TYPE_MAP
138
+ end
139
+
140
+ # catch Elasticsearch Transport-errors to be treated as +StatementInvalid+ (the original message is still readable ...)
141
+ def translate_exception(exception, message:, sql:, binds:)
142
+ case exception
143
+ when Elastic::Transport::Transport::ServerError
144
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
145
+ else
146
+ # just forward the exception ...
147
+ exception
148
+ end
149
+ end
150
+
151
+ # provide a custom log instrumenter for elasticsearch subscribers
152
+ def log(gate, arguments, name, async: false, &block)
153
+ @instrumenter.instrument(
154
+ "query.elasticsearch_record",
155
+ gate: gate,
156
+ name: name,
157
+ arguments: gate == 'core.msearch' ? arguments.deep_dup : arguments,
158
+ async: async) do
159
+ @lock.synchronize(&block)
160
+ rescue => e
161
+ raise translate_exception_class(e, arguments, [])
162
+ end
163
+ end
164
+
165
+ # returns a new collector for the Arel visitor.
166
+ # @return [Arel::Collectors::ElasticsearchQuery]
167
+ def collector
168
+ # IMPORTANT: since prepared statements doesn't make sense for elasticsearch,
169
+ # we don't have to check for +prepared_statements+ here.
170
+ # Also, bindings are (currently) not supported.
171
+ # So, we just need a query collector...
172
+ Arel::Collectors::ElasticsearchQuery.new
173
+ end
174
+
175
+ # returns a new visitor to compile Arel into Elasticsearch Hashes (in this case we use a query object)
176
+ # @return [Arel::Visitors::Elasticsearch]
177
+ def arel_visitor
178
+ Arel::Visitors::Elasticsearch.new(self)
179
+ end
180
+
181
+ # Builds the result object.
182
+ #
183
+ # This is an internal hook to make possible connection adapters to build
184
+ # custom result objects with response-specific data.
185
+ # @return [ElasticsearchRecord::Result]
186
+ def build_result(response, columns: [], column_types: {})
187
+ ElasticsearchRecord::Result.new(response, columns, column_types)
188
+ end
189
+
190
+ # register native types
191
+ ActiveRecord::Type.register(:format_string, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
192
+ ActiveRecord::Type.register(:multicast_value, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
193
+ ActiveRecord::Type.register(:object, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
194
+ ActiveRecord::Type.register(:range, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elasticsearch_record/query'
4
+
5
+ module Arel # :nodoc: all
6
+ module Collectors
7
+ class ElasticsearchQuery < ::ElasticsearchRecord::Query
8
+
9
+ # required for ActiveRecord
10
+ attr_accessor :preparable
11
+
12
+ def initialize
13
+ # force initialize a body as hash
14
+ super(body: {})
15
+
16
+ # @binds = []
17
+ @bind_index = 1
18
+ end
19
+
20
+ # send a proposal to this query
21
+ # @param [Symbol] action - the claim action
22
+ # @param [Array] args - args to claim
23
+ def claim(action, *args)
24
+ case action
25
+ when :index
26
+ # change the index name
27
+ @index = args[0]
28
+ when :type
29
+ # change the query type
30
+ @type = args[0]
31
+ when :status
32
+ # change the query status
33
+ @status = args[0]
34
+ when :columns
35
+ # change the query columns
36
+ @columns = args[0]
37
+ when :arguments
38
+ # change the query arguments
39
+ @arguments = args[0]
40
+ when :argument
41
+ # adds / sets any argument
42
+ if args.length == 2
43
+ @arguments[args[0]] = args[1]
44
+ else # should be a hash
45
+ @arguments.merge!(args[0])
46
+ end
47
+ when :body
48
+ # set the body var
49
+ @body = args[0]
50
+ when :assign
51
+ # calls a assign on the body
52
+ assign(*args)
53
+ else
54
+ raise "Unsupported claim action: #{action}"
55
+ end
56
+ end
57
+
58
+ def <<(claim)
59
+ self.claim(claim[0], *claim[1])
60
+ end
61
+
62
+ # used by the +Arel::Visitors::Elasticsearch#compile+ method (and default Arel visitors)
63
+ # todo: maybe return arguments with :_meta information instead of self ...
64
+ def value
65
+ self
66
+ end
67
+
68
+ # IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
69
+ # will +not+ be directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
70
+ # instead it will be send as bind and then re-delegated to the SQL collector.
71
+ #
72
+ # This only works for linear SQL-queries and not nested Hashes
73
+ # (otherwise we have to collect those binds, and replace them afterwards).
74
+ #
75
+ # This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
76
+ # (see @ ActiveRecord::StatementCache::PartialQueryCollector)
77
+ def add_bind(bind, &block)
78
+ @bind_index += 1
79
+
80
+ self
81
+ end
82
+
83
+ # @see Arel::Collectors::ElasticsearchQuery#add_bind
84
+ def add_binds(binds, proc_for_binds = nil, &block)
85
+ @bind_index += binds.size
86
+ self
87
+ end
88
+
89
+ private
90
+
91
+ # calls a assign on the body
92
+ def assign(key, value)
93
+ # check for special provided key, to claim through an assign
94
+ if key == :__claim__
95
+ if value.is_a?(Array)
96
+ value.each do |arg|
97
+ vkey = arg.keys.first
98
+ claim(vkey, arg[vkey])
99
+ end
100
+ else
101
+ vkey = value.keys.first
102
+ claim(vkey, value[vkey])
103
+ end
104
+ elsif value.nil?
105
+ @body.delete(key)
106
+ else
107
+ @body[key] = value
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Nodes
5
+ class SelectAgg < Unary
6
+
7
+ def left
8
+ expr[0]
9
+ end
10
+
11
+ def right
12
+ return expr[1].reduce({}) { |m, data| m.merge(data) } if expr[1].is_a?(Array)
13
+
14
+ expr[1]
15
+ end
16
+
17
+ def opts
18
+ expr[2]
19
+ end
20
+ end
21
+ end
22
+ end