elasticsearch_record 1.7.2 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e957272489ab4ebcfc628f36441959f01e20264d215f3f8d020d461447b87bc
4
- data.tar.gz: a12a713313af1fd1dd83077b96255efe9cfeced3bcc5204739fd383499e6ad8a
3
+ metadata.gz: ef01a33de054cbae1fe582ba5f7d8a5910c73dd81e3395f8b6c07ec9d3a5fbe2
4
+ data.tar.gz: d574bafc90b201869727539fd0a0c148118d6f666f9f29400690613bd7cbbaa8
5
5
  SHA512:
6
- metadata.gz: 4ef80538ae0feca07575902168df24c56696b4c4cf608ca8466b4c495261296a4338709e302bf402b2596fe1bb0617faa3c1198a7fa6f6b96cc031bd4ea7831e
7
- data.tar.gz: 5ad60438e39b4f579d84959f897452d48ac1d4b261c7b65af7539ddb7b69601b1d963dd393ab647d5dbcef55c695ce0cf5e2ffbb6db217348ead523f8e373f4b
6
+ metadata.gz: 89ac7c99f35a36650c2ac8f00600f0c702ed8ec4c4aaa5d28ad86c329a66be3625d063ada9dd8c6630b8b30eea1a5283a0231a60c0d8021242adeae4efe86316
7
+ data.tar.gz: b44f713a1650100a925c94349fcb9fdce4b72bf5acb901c5ff04b871e0586115bf5e08e42f38cd9ecb0e550663977da48e430a739b67230ced4eb29aa4f5bb8b
data/README.md CHANGED
@@ -14,8 +14,8 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
14
14
 
15
15
  **PLEASE NOTE:**
16
16
 
17
- - This is the `rails-7-0-stable`-branch, which only supports rails **7.0** _(see section 'Rails_Versions' for supported versions)_
18
- - supports ActiveRecord ~> 7.0 + Elasticsearch >= 7.17
17
+ - This is the `rails-7-1-stable`-branch, which only supports rails **7.1** _(see section 'Rails_Versions' for supported versions)_
18
+ - supports ActiveRecord ~> 7.1 + Elasticsearch >= 7.17
19
19
 
20
20
  -----
21
21
 
@@ -44,10 +44,10 @@ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-0-stable
44
44
  Add this line to your application's Gemfile:
45
45
 
46
46
  ```ruby
47
- gem 'elasticsearch_record', '~> 1.7.0'
47
+ gem 'elasticsearch_record', '~> 1.8'
48
48
 
49
49
  # alternative
50
- gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-0-stable'
50
+ gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-1-stable'
51
51
 
52
52
  ```
53
53
 
@@ -87,7 +87,7 @@ To raise an exception while using transactions on a ElasticsearchRecord model, t
87
87
  However enabling this flag will surely fail transactional tests _(prevent this with 'use_transactional_tests=false')_
88
88
 
89
89
  ```ruby
90
- # config/initializers/elasticsearch_record.yml
90
+ # config/initializers/elasticsearch_record.rb
91
91
 
92
92
  # enable transactional exceptions
93
93
  ElasticsearchRecord.error_on_transaction = true
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [1.8.1] - 2024-05-07
4
+ * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
5
+ * [ref] `ElasticsearchRecord::Relation#limit` to detect `Float::INFINITY` to also set the **max_result_window**
6
+ * [fix] `ElasticsearchRecord::SchemaMigration` only returning the first ten migrations (broke migrated migrations)
7
+ * [fix] `ElasticsearchRecord::Relation::CalculationMethods#calculate` method incompatibility - renamed to `#calculate_aggregation` (+ alias to `#calculate`)
8
+ * [fix] `ElasticsearchRecord::ModelApi#bulk` method not correctly generating data for 'delete'
9
+
10
+ ## [1.8.0] - 2024-01-10
11
+ * [ref] major method & dependency refactoring for `rails 7.1` - _(Does **NOT** work with rails 7.0)_
12
+ * [add] new repository branch `rails-7-1-stable` to support different rails version
13
+ * [ref] gemspec to lock on rails 7.1
14
+
15
+ ## [1.7.3] - 2024-05-07 _(no gem release)_
16
+ * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
17
+ * [ref] `ElasticsearchRecord::Relation#limit` to detect `Float::INFINITY` to also set the **max_result_window**
18
+ * [fix] `ElasticsearchRecord::SchemaMigration` only returning the first ten migrations (broke migrated migrations)
19
+ * [fix] `ElasticsearchRecord::Relation::CalculationMethods#calculate` method incompatibility - renamed to `#calculate_aggregation` (+ alias to `#calculate`)
20
+ * [fix] `ElasticsearchRecord::ModelApi#bulk` method not correctly generating data for 'delete'
21
+
3
22
  ## [1.7.2] - 2024-01-10
4
23
  * [ref] gemspec to lock on rails 7.0
5
24
 
@@ -32,7 +32,7 @@ DESC
32
32
 
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency 'activerecord', '~> 7.0.0'
35
+ spec.add_dependency 'activerecord', '~> 7.1.0'
36
36
  spec.add_dependency 'elasticsearch', '>= 7.17'
37
37
 
38
38
  #spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
@@ -50,43 +50,12 @@ module ActiveRecord
50
50
  query.write?
51
51
  end
52
52
 
53
- # Executes the query object in the context of this connection and returns the raw result
54
- # from the connection adapter.
55
- # @param [ElasticsearchRecord::Query] query
56
- # @param [String (frozen)] name
57
- # @param [Boolean] async
58
- # @return [ElasticsearchRecord::Result]
59
- def execute(query, name = nil, async: false)
60
- # validate the query
61
- raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
62
- raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
63
-
64
- # checks for write query - raises an exception if connection is locked to readonly ...
65
- check_if_write_query(query)
66
-
67
- api(*query.gate, query.query_arguments, name, async: async)
68
- end
69
-
70
- # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
71
- # @param [ElasticsearchRecord::Query] query
72
- # @param [String (frozen)] name
73
- # @param [Array] binds - not supported on the top-level and therefore ignored!
74
- # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
75
- # @param [Boolean] async
76
- # @return [ElasticsearchRecord::Result]
77
- def exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
78
- build_result(
79
- execute(query, name, async: async),
80
- columns: query.columns
81
- )
82
- end
83
-
84
53
  # Executes insert +query+ statement in the context of this connection using
85
54
  # +binds+ as the bind substitutes. +name+ is logged along with
86
55
  # the executed +query+ arguments.
87
56
  # @return [ElasticsearchRecord::Result]
88
- def exec_insert(query, name = nil, binds = [], _pk = nil, _sequence_name = nil)
89
- result = exec_query(query, name, binds)
57
+ def exec_insert(query, name = nil, binds = [], _pk = nil, _sequence_name = nil, returning: nil)
58
+ result = internal_exec_query(query, name, binds)
90
59
 
91
60
  # fetch additional Elasticsearch response result
92
61
  # raise ::ElasticsearchRecord::ResponseResultError.new('created', result.result) unless result.result == 'created'
@@ -101,7 +70,7 @@ module ActiveRecord
101
70
  # expects a integer as return.
102
71
  # @return [Integer]
103
72
  def exec_update(query, name = nil, binds = [])
104
- result = exec_query(query, name, binds)
73
+ result = internal_exec_query(query, name, binds)
105
74
 
106
75
  # fetch additional Elasticsearch response result
107
76
  # raise ::ElasticsearchRecord::ResponseResultError.new('updated', result.result) unless result.result == 'updated'
@@ -115,7 +84,7 @@ module ActiveRecord
115
84
  # expects a integer as return.
116
85
  # @return [Integer]
117
86
  def exec_delete(query, name = nil, binds = [])
118
- result = exec_query(query, name, binds)
87
+ result = internal_exec_query(query, name, binds)
119
88
 
120
89
  # fetch additional Elasticsearch response result
121
90
  # raise ::ElasticsearchRecord::ResponseResultError.new('deleted', result.result) unless result.result == 'deleted'
@@ -135,7 +104,7 @@ module ActiveRecord
135
104
  type: ElasticsearchRecord::Query::TYPE_MSEARCH,
136
105
  body: queries.map { |q| { search: q.body } })
137
106
 
138
- exec_query(query, name, async: async)
107
+ internal_exec_query(query, name, async: async)
139
108
  end
140
109
 
141
110
  # executes a count query for provided arel
@@ -151,7 +120,7 @@ module ActiveRecord
151
120
  status: query.status,
152
121
  arguments: query.arguments)
153
122
 
154
- exec_query(query, name, async: async).response['count']
123
+ internal_exec_query(query, name, async: async).response['count']
155
124
  end
156
125
 
157
126
  # returns the last inserted id from the result.
@@ -159,6 +128,40 @@ module ActiveRecord
159
128
  def last_inserted_id(result)
160
129
  result.response['_id']
161
130
  end
131
+
132
+ private
133
+
134
+ # Executes the query object in the context of this connection and returns the raw result
135
+ # from the connection adapter.
136
+ # @param [ElasticsearchRecord::Query] query
137
+ # @param [String (frozen),nil] name
138
+ # @param [Boolean] async (default: false)
139
+ # @param [Boolean] allow_retry (default: false)
140
+ # @return [ElasticsearchRecord::Result]
141
+ def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: nil)
142
+ # validate the query
143
+ raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
144
+ raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
145
+
146
+ # checks for write query - raises an exception if connection is locked to readonly ...
147
+ check_if_write_query(query)
148
+
149
+ api(*query.gate, query.query_arguments, name, async: async)
150
+ end
151
+
152
+ # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
153
+ # @param [ElasticsearchRecord::Query] query
154
+ # @param [String (frozen),nil] name
155
+ # @param [Array] binds - not supported on the top-level and therefore ignored!
156
+ # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
157
+ # @param [Boolean] async
158
+ # @return [ElasticsearchRecord::Result]
159
+ def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
160
+ build_result(
161
+ internal_execute(query, name, async: async),
162
+ columns: query.columns
163
+ )
164
+ end
162
165
  end
163
166
  end
164
167
  end
@@ -8,11 +8,20 @@ module ActiveRecord
8
8
 
9
9
  included do
10
10
  # see @ ::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::NATIVE_DATABASE_TYPES.keys
11
- define_column_methods :string, :blob, :datetime, :bigint, :json, :binary, :boolean, :keyword,
12
- :constant_keyword, :wildcard, :long, :integer, :short, :byte, :double, :float,
13
- :half_float, :scaled_float, :unsigned_long, :date, :object, :flattened, :nested,
11
+ define_column_methods :string, :blob, :datetime, :bigint, :json,
12
+ :binary, :boolean,
13
+ :keyword, :constant_keyword, :wildcard,
14
+ :long, :integer, :short, :byte, :double, :float, :half_float, :scaled_float, :unsigned_long,
15
+ :date, :date_nanos,
16
+ :alias,
17
+ :object, :flattened, :nested, :join,
14
18
  :integer_range, :float_range, :long_range, :double_range, :date_range, :ip_range,
15
- :ip, :version, :text
19
+ :ip, :version, :murmur3,
20
+ :aggregate_metric_double, :histogram,
21
+ :text, :match_only_text, :completion, :search_as_you_type, :token_count,
22
+ :dense_vector, :sparse_vector, :rank_feature, :rank_features,
23
+ :geo_point, :geo_shape, :point, :shape,
24
+ :percolator
16
25
 
17
26
  # Appends a primary key definition to the table definition.
18
27
  # Can be called multiple times, but this is probably not a good idea.
@@ -223,7 +223,7 @@ module ActiveRecord
223
223
  # @param [String] _table_name
224
224
  # @param [Hash] field
225
225
  # @return [ActiveRecord::ConnectionAdapters::Column]
226
- def new_column_from_field(_table_name, field)
226
+ def new_column_from_field(_table_name, field, _definitions)
227
227
  ActiveRecord::ConnectionAdapters::Elasticsearch::Column.new(
228
228
  field["name"],
229
229
  field["null_value"],
@@ -23,31 +23,18 @@ require 'elasticsearch'
23
23
 
24
24
  module ActiveRecord # :nodoc:
25
25
  module ConnectionHandling # :nodoc:
26
- def elasticsearch_connection(config)
27
- config = config.symbolize_keys
28
-
29
- # move 'username' to 'user'
30
- config[:user] = config.delete(:username) if config[:username]
31
-
32
- # append 'port' to 'host'
33
- config[:host] += ":#{config.delete(:port)}" if config[:port] && config[:host]
34
-
35
- # move 'host' to 'hosts'
36
- config[:hosts] = config.delete(:host) if config[:host]
37
-
38
- # enable logging (Rails.logger)
39
- config[:logger] = logger if config.delete(:log)
26
+ def elasticsearch_adapter_class
27
+ ConnectionAdapters::ElasticsearchAdapter
28
+ end
40
29
 
41
- ConnectionAdapters::ElasticsearchAdapter.new(
42
- ConnectionAdapters::ElasticsearchAdapter.new_client(config),
43
- logger,
44
- config
45
- )
30
+ def elasticsearch_connection(config)
31
+ elasticsearch_adapter_class.new(config)
46
32
  end
47
33
  end
48
34
 
49
35
  module ConnectionAdapters # :nodoc:
50
36
  class ElasticsearchAdapter < AbstractAdapter
37
+ # defines the *Elasticsearch* adapter name.
51
38
  ADAPTER_NAME = "Elasticsearch"
52
39
 
53
40
  # defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
@@ -68,16 +55,20 @@ module ActiveRecord # :nodoc:
68
55
 
69
56
  class << self
70
57
  def base_structure_keys
71
- @base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
58
+ # using a class_variable to not reinitialize for descendants
59
+ @@base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
72
60
  end
73
61
 
74
62
  def new_client(config)
75
63
  # IMPORTANT: remove +adapter+ from config - otherwise we mess up with Faraday::AdapterRegistry
76
- client = ::Elasticsearch::Client.new(config.except(:adapter))
77
- client.ping unless config[:ping] == false
78
- client
64
+ client_config = config.except(:adapter)
65
+ # add rails logger manually, if +:log+ is true
66
+ client_config[:logger] = logger if client_config.delete(:log)
67
+
68
+ # build an return new client
69
+ ::Elasticsearch::Client.new(client_config)
79
70
  rescue ::Elastic::Transport::Transport::Errors::Unauthorized
80
- raise ActiveRecord::DatabaseConnectionError.username_error(config[:user])
71
+ raise ::ActiveRecord::DatabaseConnectionError.username_error(config[:user])
81
72
  rescue ::Elastic::Transport::Transport::ServerError => error
82
73
  raise ::ActiveRecord::ConnectionNotEstablished, error.message
83
74
  end
@@ -144,25 +135,35 @@ module ActiveRecord # :nodoc:
144
135
  # define native types - which will be used for schema-dumping
145
136
  NATIVE_DATABASE_TYPES = {
146
137
  primary_key: { name: 'long' }, # maybe this hae to changed to 'keyword'
147
- string: { name: 'keyword' },
148
- blob: { name: 'binary' },
149
- datetime: { name: 'date' },
150
- bigint: { name: 'long' },
151
- json: { name: 'object' }
138
+ string: { name: 'keyword' },
139
+ blob: { name: 'binary' },
140
+ datetime: { name: 'date' },
141
+ bigint: { name: 'long' },
142
+ json: { name: 'object' }
152
143
  }.merge(
153
144
  TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
154
145
  )
155
146
 
156
- def initialize(*args)
157
- super(*args)
147
+ def initialize(...)
148
+ super
149
+
150
+ # transform provided config
151
+ config = @config.dup
152
+
153
+ # move 'username' to 'user'
154
+ config[:user] = config.delete(:username) if config[:username]
155
+
156
+ # append 'port' to 'host'
157
+ config[:host] += ":#{config.delete(:port)}" if config[:port] && config[:host]
158
+
159
+ # move 'host' to 'hosts'
160
+ config[:hosts] = config.delete(:host) if config[:host]
158
161
 
159
- # prepared statements are not supported by Elasticsearch.
160
- # documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
161
- @prepared_statements = false
162
+ @connection_parameters = config
162
163
  end
163
164
 
164
165
  def schema_migration # :nodoc:
165
- @schema_migration ||= ElasticsearchRecord::SchemaMigration
166
+ ElasticsearchRecord::SchemaMigration.new(self)
166
167
  end
167
168
 
168
169
  # provide a table_name_prefix from the configuration to create & restrict schema creation
@@ -180,6 +181,12 @@ module ActiveRecord # :nodoc:
180
181
  @config[:migrations_paths] || ['db/migrate_elasticsearch']
181
182
  end
182
183
 
184
+ # prepared statements are not supported by Elasticsearch.
185
+ # documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
186
+ def default_prepared_statements
187
+ false
188
+ end
189
+
183
190
  # Does this adapter support transactions in general?
184
191
  # HINT: This is +NOT* an official setting and only introduced to ElasticsearchRecord
185
192
  def supports_transactions?
@@ -236,7 +243,7 @@ module ActiveRecord # :nodoc:
236
243
  raise ::StandardError, 'ASYNC api calls are not supported' if async
237
244
 
238
245
  # resolve the API target
239
- target = namespace == :core ? @connection : @connection.__send__(namespace)
246
+ target = namespace == :core ? raw_connection : raw_connection.__send__(namespace)
240
247
 
241
248
  __send__(:log, "#{namespace}.#{action}", arguments, name, async: async, log: log) do
242
249
  response = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -258,6 +265,30 @@ module ActiveRecord # :nodoc:
258
265
 
259
266
  private
260
267
 
268
+ def connect
269
+ @raw_connection = self.class.new_client(@connection_parameters)
270
+ rescue ::ActiveRecord::ConnectionNotEstablished => ex
271
+ raise ex.set_pool(@pool)
272
+ end
273
+
274
+ def reconnect
275
+ @raw_connection = nil
276
+ connect
277
+ end
278
+
279
+ def active?
280
+ !!@raw_connection&.ping
281
+ end
282
+
283
+ # Disconnects from the database if already connected.
284
+ # Otherwise, this method does nothing.
285
+ def disconnect!
286
+ super
287
+ @raw_connection = nil
288
+ end
289
+
290
+ alias :reset! :reconnect!
291
+
261
292
  def type_map
262
293
  TYPE_MAP
263
294
  end
@@ -8,8 +8,8 @@ module ElasticsearchRecord
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 1
11
- MINOR = 7
12
- TINY = 2
11
+ MINOR = 8
12
+ TINY = 1
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -45,8 +45,8 @@ module ElasticsearchRecord
45
45
  query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
46
46
 
47
47
  # final coloring
48
- name = color(name, name_color(payload[:name]), true)
49
- query = color(query, gate_color(payload[:gate], payload[:name]), true) if colorize_logging
48
+ name = color(name, name_color(payload[:name]), bold: true)
49
+ query = color(query, gate_color(payload[:gate], payload[:name]), bold: true) if colorize_logging
50
50
 
51
51
  debug " #{name} #{query.presence || '-/-'}"
52
52
  end
@@ -269,8 +269,11 @@ module ElasticsearchRecord
269
269
 
270
270
  _connection.api(:core, :bulk, {
271
271
  index: _index_name,
272
- body: if operation == :update
273
- data.map { |item| { operation => { _id: (item[:_id].presence || item['_id']), data: { doc: item.except(:_id, '_id') } } } }
272
+ body: case operation
273
+ when :update
274
+ data.map { |item| { update: { _id: (item[:_id].presence || item['_id']), data: { doc: item.except(:_id, '_id') } } } }
275
+ when :delete
276
+ data.map { |item| { delete: { _id: (item[:_id].presence || item['_id']) } } }
274
277
  else
275
278
  data.map { |item| { operation => { _id: (item[:_id].presence || item['_id']), data: item.except(:_id, '_id') } } }
276
279
  end,
@@ -58,7 +58,7 @@ module ElasticsearchRecord
58
58
 
59
59
  # clears schema-related instance variables.
60
60
  # @see ActiveRecord::ModelSchema::ClassMethods#reload_schema_from_cache
61
- def reload_schema_from_cache
61
+ def reload_schema_from_cache(recursive = true)
62
62
  # we also need to clear our custom-defined variables
63
63
  @source_column_names = nil
64
64
  @searchable_column_names = nil
@@ -7,7 +7,7 @@ module ElasticsearchRecord
7
7
  # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
8
8
  # @param [ActiveModel::Attribute] values
9
9
  # @return [Object] id
10
- def _insert_record(values)
10
+ def _insert_record(values, returning)
11
11
  # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so the casted values gets resolved here
12
12
  values = values.transform_values(&:value)
13
13
 
@@ -23,7 +23,7 @@ module ElasticsearchRecord
23
23
  refresh: true)
24
24
 
25
25
  # execute query and return inserted id
26
- connection.insert(query, "#{self} Create")
26
+ connection.insert(query, "#{self} Create", returning: returning)
27
27
  end
28
28
  end
29
29
 
@@ -105,7 +105,7 @@ module ElasticsearchRecord
105
105
  # IMPORTANT: Always provide all columns
106
106
  columns: source_column_names)
107
107
 
108
- connection.exec_query(query, "#{name} ES|QL", async: async)
108
+ connection.internal_exec_query(query, "#{name} ES|QL", async: async)
109
109
  end
110
110
 
111
111
 
@@ -122,7 +122,7 @@ module ElasticsearchRecord
122
122
  # IMPORTANT: Always provide all columns
123
123
  columns: source_column_names)
124
124
 
125
- connection.exec_query(query, "#{name} Msearch", async: async)
125
+ connection.internal_exec_query(query, "#{name} Msearch", async: async)
126
126
  end
127
127
 
128
128
  # executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
@@ -64,9 +64,12 @@ module ElasticsearchRecord
64
64
  #
65
65
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-boxplot-aggregation.html
66
66
  #
67
+ # @note returns *nil* on a *NullRelation*
68
+ #
67
69
  # @param [Symbol, String] column_name
70
+ # @return [Hash,nil]
68
71
  def boxplot(column_name)
69
- calculate(:boxplot, column_name)
72
+ calculate_aggregation(:boxplot, column_name)
70
73
  end
71
74
 
72
75
  # A multi-value metrics aggregation that computes stats over numeric values extracted from the aggregated documents. #
@@ -83,9 +86,12 @@ module ElasticsearchRecord
83
86
  #
84
87
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html
85
88
  #
89
+ # @note returns *nil* on a *NullRelation*
90
+ #
86
91
  # @param [Symbol, String] column_name
92
+ # @return [Hash,nil]
87
93
  def stats(column_name)
88
- calculate(:stats, column_name)
94
+ calculate_aggregation(:stats, column_name)
89
95
  end
90
96
 
91
97
  # A multi-value metrics aggregation that computes statistics over string values extracted from the aggregated documents.
@@ -102,9 +108,12 @@ module ElasticsearchRecord
102
108
  #
103
109
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-string-stats-aggregation.html
104
110
  #
111
+ # @note returns *nil* on a *NullRelation*
112
+ #
105
113
  # @param [Symbol, String] column_name
114
+ # @return [Hash,nil]
106
115
  def string_stats(column_name)
107
- calculate(:string_stats, column_name)
116
+ calculate_aggregation(:string_stats, column_name)
108
117
  end
109
118
 
110
119
  # The matrix_stats aggregation is a numeric aggregation that computes the following statistics over a set of document fields:
@@ -118,9 +127,12 @@ module ElasticsearchRecord
118
127
  #
119
128
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-matrix-stats-aggregation.html
120
129
  #
130
+ # @note returns *nil* on a *NullRelation*
131
+ #
121
132
  # @param [Array<Symbol|String>] column_names
133
+ # @return [Hash,nil]
122
134
  def matrix_stats(*column_names)
123
- calculate(:matrix_stats, *column_names)
135
+ calculate_aggregation(:matrix_stats, *column_names)
124
136
  end
125
137
 
126
138
  # A multi-value metrics aggregation that calculates one or more
@@ -140,9 +152,12 @@ module ElasticsearchRecord
140
152
  #
141
153
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html
142
154
  #
155
+ # @note returns *nil* on a *NullRelation*
156
+ #
143
157
  # @param [Symbol, String] column_name
158
+ # @return [Hash,nil]
144
159
  def percentiles(column_name)
145
- calculate(:percentiles, column_name, node: :values)
160
+ calculate_aggregation(:percentiles, column_name, node: :values)
146
161
  end
147
162
 
148
163
  # A multi-value metrics aggregation that calculates one or more
@@ -165,10 +180,13 @@ module ElasticsearchRecord
165
180
  #
166
181
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html
167
182
  #
183
+ # @note returns *nil* on a *NullRelation*
184
+ #
168
185
  # @param [Symbol, String] column_name
169
186
  # @param [Array] values
187
+ # @return [Hash,nil]
170
188
  def percentile_ranks(column_name, values)
171
- calculate(:percentile_ranks, column_name, opts: { values: values }, node: :values)
189
+ calculate_aggregation(:percentile_ranks, column_name, opts: { values: values }, node: :values)
172
190
  end
173
191
 
174
192
  # Calculates the cardinality on a given column. Returns +0+ if there's no row.
@@ -178,9 +196,12 @@ module ElasticsearchRecord
178
196
  #
179
197
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html
180
198
  #
199
+ # @note returns *nil* on a *NullRelation*
200
+ #
181
201
  # @param [Symbol, String] column_name
202
+ # @return [Integer,nil]
182
203
  def cardinality(column_name)
183
- calculate(:cardinality, column_name, node: :value)
204
+ calculate_aggregation(:cardinality, column_name, node: :value)
184
205
  end
185
206
 
186
207
  # Calculates the average value on a given column. Returns +nil+ if there's no row. See #calculate for examples with options.
@@ -189,9 +210,12 @@ module ElasticsearchRecord
189
210
  #
190
211
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html
191
212
  #
213
+ # @note returns *nil* on a *NullRelation*
214
+ #
192
215
  # @param [Symbol, String] column_name
216
+ # @return [Float,nil]
193
217
  def average(column_name)
194
- calculate(:avg, column_name, node: :value)
218
+ calculate_aggregation(:avg, column_name, node: :value)
195
219
  end
196
220
 
197
221
  # Calculates the minimum value on a given column. The value is returned
@@ -202,9 +226,12 @@ module ElasticsearchRecord
202
226
  #
203
227
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html
204
228
  #
229
+ # @note returns *nil* on a *NullRelation*
230
+ #
205
231
  # @param [Symbol, String] column_name
232
+ # @return [Float,nil]
206
233
  def minimum(column_name)
207
- calculate(:min, column_name, node: :value)
234
+ calculate_aggregation(:min, column_name, node: :value)
208
235
  end
209
236
 
210
237
  # Calculates the maximum value on a given column. The value is returned
@@ -215,9 +242,12 @@ module ElasticsearchRecord
215
242
  #
216
243
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html
217
244
  #
245
+ # @note returns *nil* on a *NullRelation*
246
+ #
218
247
  # @param [Symbol, String] column_name
248
+ # @return [Float,nil]
219
249
  def maximum(column_name)
220
- calculate(:max, column_name, node: :value)
250
+ calculate_aggregation(:max, column_name, node: :value)
221
251
  end
222
252
 
223
253
  # This single-value aggregation approximates the median absolute deviation of its search results.
@@ -232,9 +262,12 @@ module ElasticsearchRecord
232
262
  #
233
263
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html
234
264
  #
265
+ # @note returns *nil* on a *NullRelation*
266
+ #
235
267
  # @param [Symbol, String] column_name
268
+ # @return [Float,nil]
236
269
  def median_absolute_deviation(column_name)
237
- calculate(:median_absolute_deviation, column_name)
270
+ calculate_aggregation(:median_absolute_deviation, column_name)
238
271
  end
239
272
 
240
273
  # Calculates the sum of values on a given column. The value is returned
@@ -245,18 +278,27 @@ module ElasticsearchRecord
245
278
  #
246
279
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html
247
280
  #
281
+ # @note returns *nil* on a *NullRelation*
282
+ #
248
283
  # @param [Symbol, String] column_name (optional)
284
+ # @return [Float,nil]
249
285
  def sum(column_name)
250
- calculate(:sum, column_name, node: :value)
286
+ calculate_aggregation(:sum, column_name, node: :value)
251
287
  end
252
288
 
253
289
  # creates a aggregation with the provided metric (e.g. :sum) and columns.
254
290
  # returns the metric node (default: :value) from the aggregations result.
291
+ #
292
+ # @note returns *nil* on a *NullRelation*
293
+ #
255
294
  # @param [Symbol, String] metric
256
295
  # @param [Array<Symbol|String>] columns
257
296
  # @param [Hash] opts - additional arguments that get merged with the metric definition
258
297
  # @param [Symbol] node (default: nil)
259
- def calculate(metric, *columns, opts: {}, node: nil)
298
+ def calculate_aggregation(metric, *columns, opts: {}, node: nil)
299
+ # prevent execution on a *NullRelation*
300
+ return if null_relation?
301
+
260
302
  metric_key = "calculate_#{metric}"
261
303
 
262
304
  # spawn a new aggregation and return the aggs
@@ -272,6 +314,8 @@ module ElasticsearchRecord
272
314
  response[metric_key]
273
315
  end
274
316
  end
317
+
318
+ alias_method :calculate, :calculate_aggregation
275
319
  end
276
320
  end
277
321
  end
@@ -188,8 +188,9 @@ module ElasticsearchRecord
188
188
  end
189
189
 
190
190
  def none! # :nodoc:
191
+ @none = true
191
192
  # tell the query it 'failed'
192
- configure!(:__query__, status: :failed).extending!(ActiveRecord::NullRelation)
193
+ configure!(:__query__, status: :failed)
193
194
  end
194
195
 
195
196
  # creates a condition on the relation.
@@ -132,11 +132,10 @@ module ElasticsearchRecord
132
132
  # resolve only data from hits->hits[{_source}]
133
133
  current_results = if ids_only
134
134
  current_response['hits']['hits'].map { |result| result['_id'] }
135
- # future with helper
136
- # current_response['hits']['hits'].map.from_hash('_id')
137
135
  else
138
136
  current_response['hits']['hits'].map { |result| result['_source'].merge('_id' => result['_id']) }
139
137
  end
138
+
140
139
  current_results_length = current_results.length
141
140
 
142
141
  # check if we reached the required offset
@@ -45,7 +45,7 @@ module ElasticsearchRecord
45
45
 
46
46
  # overwrite the limit_value setter, to provide a special behaviour of auto-setting the +max_result_window+.
47
47
  def limit_value=(limit)
48
- if limit == '__max__' || (limit.nil? && delegate_query_nil_limit?)
48
+ if limit == '__max__' || limit == Float::INFINITY || (limit.nil? && delegate_query_nil_limit?)
49
49
  super(max_result_window)
50
50
  else
51
51
  super(limit)
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record/future_result"
4
+
3
5
  module ElasticsearchRecord
4
6
  class Result
5
7
  include Enumerable
6
8
 
7
9
  # creates an empty response
8
10
  # @return [ElasticsearchRecord::Result (frozen)]
9
- def self.empty
10
- new(nil).freeze
11
+ def self.empty(async: false) # :nodoc:
12
+ if async
13
+ EMPTY_ASYNC
14
+ else
15
+ EMPTY
16
+ end
11
17
  end
12
18
 
13
19
  attr_reader :response, :columns, :column_types
@@ -272,5 +278,11 @@ module ElasticsearchRecord
272
278
  []
273
279
  end
274
280
  end
281
+
282
+ EMPTY = new([].freeze, [].freeze, {}.freeze).freeze
283
+ private_constant :EMPTY
284
+
285
+ EMPTY_ASYNC = ::ActiveRecord::FutureResult::Complete.new(EMPTY).freeze
286
+ private_constant :EMPTY_ASYNC
275
287
  end
276
288
  end
@@ -3,47 +3,20 @@
3
3
  require "active_record/schema_migration"
4
4
 
5
5
  module ElasticsearchRecord
6
- class SchemaMigration < ElasticsearchRecord::Base # :nodoc:
7
- class << self
8
- def primary_key
9
- "version"
10
- end
6
+ class SchemaMigration < ::ActiveRecord::SchemaMigration
11
7
 
12
- def table_name
13
- "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
14
- end
15
-
16
- def create_table
17
- unless connection.table_exists?(table_name)
18
- connection.create_table(table_name, id: false) do |t|
19
- t.string :version, **connection.internal_string_options_for_primary_key
20
- end
21
- end
22
- end
23
-
24
- def drop_table
25
- connection.drop_table table_name, if_exists: true
26
- end
27
-
28
- def normalize_migration_number(number)
29
- "%.3d" % number.to_i
30
- end
31
-
32
- def normalized_versions
33
- all_versions.map { |v| normalize_migration_number v }
34
- end
35
-
36
- def all_versions
37
- order(:version).pluck(:version)
38
- end
39
-
40
- def table_exists?
41
- connection.data_source_exists?(table_name)
42
- end
8
+ def table_name
9
+ "#{ElasticsearchRecord::Base.table_name_prefix}#{ElasticsearchRecord::Base.schema_migrations_table_name}#{ElasticsearchRecord::Base.table_name_suffix}"
43
10
  end
44
11
 
45
- def version
46
- super.to_i
12
+ # overwrite method to fix the default limit (= 10) to support returning more than 10 migrations
13
+ def versions
14
+ sm = Arel::SelectManager.new(arel_table)
15
+ sm.project(arel_table[primary_key])
16
+ sm.order(arel_table[primary_key].asc)
17
+ sm.take(connection.max_result_window(table_name))
18
+
19
+ connection.select_values(sm, "#{self.class} Load")
47
20
  end
48
21
  end
49
22
  end
@@ -70,7 +70,6 @@ end
70
70
  ActiveSupport.on_load(:active_record) do
71
71
  # load patches
72
72
  require 'elasticsearch_record/patches/active_record/relation_merger_patch'
73
-
74
73
  require 'elasticsearch_record/patches/arel/select_core_patch'
75
74
  require 'elasticsearch_record/patches/arel/select_manager_patch'
76
75
  require 'elasticsearch_record/patches/arel/select_statement_patch'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.2
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 7.0.0
19
+ version: 7.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 7.0.0
26
+ version: 7.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: elasticsearch
29
29
  requirement: !ruby/object:Gem::Requirement