elasticsearch_record 1.8.2 → 3.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -0
  3. data/.yardopts +0 -0
  4. data/Gemfile +7 -0
  5. data/README.md +177 -19
  6. data/Rakefile +0 -0
  7. data/docs/CHANGELOG.md +66 -0
  8. data/docs/CODE_OF_CONDUCT.md +0 -0
  9. data/docs/LICENSE +0 -0
  10. data/elasticsearch_record.gemspec +3 -3
  11. data/lib/active_record/connection_adapters/elasticsearch/column.rb +0 -0
  12. data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +19 -17
  13. data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +16 -0
  14. data/lib/active_record/connection_adapters/elasticsearch/schema_creation.rb +0 -0
  15. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/attribute_methods.rb +0 -0
  16. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/clone_table_definition.rb +0 -0
  17. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +6 -0
  18. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +0 -0
  19. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +0 -0
  20. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_meta_definition.rb +0 -0
  21. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +7 -5
  22. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +3 -1
  23. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +0 -0
  24. data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +24 -3
  25. data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +39 -34
  26. data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +251 -72
  27. data/lib/active_record/connection_adapters/elasticsearch/transactions.rb +0 -0
  28. data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +0 -0
  29. data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +0 -0
  30. data/lib/active_record/connection_adapters/elasticsearch/type/nested.rb +0 -0
  31. data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +0 -0
  32. data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +0 -0
  33. data/lib/active_record/connection_adapters/elasticsearch/type.rb +0 -0
  34. data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +0 -0
  35. data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +164 -99
  36. data/lib/arel/collectors/elasticsearch_query.rb +3 -0
  37. data/lib/arel/nodes/select_agg.rb +0 -0
  38. data/lib/arel/nodes/select_configure.rb +0 -0
  39. data/lib/arel/nodes/select_kind.rb +0 -0
  40. data/lib/arel/nodes/select_query.rb +0 -0
  41. data/lib/arel/visitors/elasticsearch.rb +0 -0
  42. data/lib/arel/visitors/elasticsearch_query.rb +51 -9
  43. data/lib/elasticsearch_record/base.rb +0 -0
  44. data/lib/elasticsearch_record/core.rb +1 -1
  45. data/lib/elasticsearch_record/errors.rb +0 -0
  46. data/lib/elasticsearch_record/extensions/relation.rb +0 -0
  47. data/lib/elasticsearch_record/gem_version.rb +3 -3
  48. data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +0 -0
  49. data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +11 -9
  50. data/lib/elasticsearch_record/instrumentation/railtie.rb +0 -0
  51. data/lib/elasticsearch_record/instrumentation.rb +0 -0
  52. data/lib/elasticsearch_record/internal_metadata.rb +19 -0
  53. data/lib/elasticsearch_record/model_api.rb +8 -5
  54. data/lib/elasticsearch_record/model_schema.rb +1 -1
  55. data/lib/elasticsearch_record/patches/active_record/connection_pool_patch.rb +57 -0
  56. data/lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb +0 -0
  57. data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +0 -0
  58. data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +0 -0
  59. data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +0 -0
  60. data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +0 -0
  61. data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +0 -0
  62. data/lib/elasticsearch_record/persistence.rb +39 -21
  63. data/lib/elasticsearch_record/query.rb +53 -33
  64. data/lib/elasticsearch_record/querying.rb +43 -21
  65. data/lib/elasticsearch_record/relation/calculation_methods.rb +38 -2
  66. data/lib/elasticsearch_record/relation/core_methods.rb +1 -1
  67. data/lib/elasticsearch_record/relation/query_clause.rb +0 -0
  68. data/lib/elasticsearch_record/relation/query_clause_tree.rb +2 -1
  69. data/lib/elasticsearch_record/relation/query_methods.rb +31 -1
  70. data/lib/elasticsearch_record/relation/result_methods.rb +47 -43
  71. data/lib/elasticsearch_record/relation/value_methods.rb +4 -4
  72. data/lib/elasticsearch_record/result.rb +144 -60
  73. data/lib/elasticsearch_record/schema_migration.rb +5 -2
  74. data/lib/elasticsearch_record/statement_cache.rb +0 -0
  75. data/lib/elasticsearch_record/tasks/elasticsearch_database_tasks.rb +0 -0
  76. data/lib/elasticsearch_record/version.rb +0 -0
  77. data/lib/elasticsearch_record.rb +17 -0
  78. metadata +22 -9
@@ -22,23 +22,23 @@ gem 'elasticsearch'
22
22
  require 'elasticsearch'
23
23
 
24
24
  module ActiveRecord # :nodoc:
25
- module ConnectionHandling # :nodoc:
26
- def elasticsearch_adapter_class
27
- ConnectionAdapters::ElasticsearchAdapter
28
- end
29
-
30
- def elasticsearch_connection(config)
31
- elasticsearch_adapter_class.new(config)
32
- end
33
- end
34
-
35
25
  module ConnectionAdapters # :nodoc:
36
26
  class ElasticsearchAdapter < AbstractAdapter
27
+ include Elasticsearch::UnsupportedImplementation
28
+ include Elasticsearch::Quoting
29
+ include Elasticsearch::DatabaseStatements
30
+ include Elasticsearch::SchemaStatements
31
+ include Elasticsearch::TableStatements
32
+ include Elasticsearch::Transactions
33
+
37
34
  # defines the *Elasticsearch* adapter name.
38
- ADAPTER_NAME = "Elasticsearch"
35
+ # @return [String]
36
+ ADAPTER_NAME = "Elasticsearch".freeze
39
37
 
40
38
  # defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
41
- BASE_STRUCTURE = [
39
+ # - see @ https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html
40
+ # @return [Hash]
41
+ METADATA_FIELDS = [
42
42
  { 'name' => '_id', 'type' => 'keyword', 'meta' => { 'primary_key' => 'true' } },
43
43
  { 'name' => '_index', 'type' => 'keyword', 'virtual' => true },
44
44
  { 'name' => '_score', 'type' => 'float', 'virtual' => true },
@@ -46,17 +46,10 @@ module ActiveRecord # :nodoc:
46
46
  { 'name' => '_ignored', 'type' => 'boolean', 'virtual' => true }
47
47
  ].freeze
48
48
 
49
- include Elasticsearch::UnsupportedImplementation
50
- include Elasticsearch::Quoting
51
- include Elasticsearch::DatabaseStatements
52
- include Elasticsearch::SchemaStatements
53
- include Elasticsearch::TableStatements
54
- include Elasticsearch::Transactions
55
-
56
49
  class << self
57
- def base_structure_keys
50
+ def metadata_keys
58
51
  # using a class_variable to not reinitialize for descendants
59
- @@base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
52
+ @@metadata_keys ||= METADATA_FIELDS.map { |struct| struct['name'] }.freeze
60
53
  end
61
54
 
62
55
  def new_client(config)
@@ -65,7 +58,7 @@ module ActiveRecord # :nodoc:
65
58
  # add rails logger manually, if +:log+ is true
66
59
  client_config[:logger] = logger if client_config.delete(:log)
67
60
 
68
- # build an return new client
61
+ # build and return new client
69
62
  ::Elasticsearch::Client.new(client_config)
70
63
  rescue ::Elastic::Transport::Transport::Errors::Unauthorized
71
64
  raise ::ActiveRecord::DatabaseConnectionError.username_error(config[:user])
@@ -129,20 +122,22 @@ module ActiveRecord # :nodoc:
129
122
  end
130
123
  end
131
124
 
125
+ # -- TYPE MAP -- DO NOT insert before this line (*initialize_type_map* needs to be rewritten)
132
126
  # reinitialize the constant with new types
133
127
  TYPE_MAP = ActiveRecord::Type::HashLookupTypeMap.new.tap { |m| initialize_type_map(m) }
134
128
 
135
129
  # define native types - which will be used for schema-dumping
130
+ # @return [Hash]
136
131
  NATIVE_DATABASE_TYPES = {
137
132
  primary_key: { name: 'long' }, # maybe this hae to changed to 'keyword'
138
133
  string: { name: 'keyword' },
139
134
  blob: { name: 'binary' },
140
135
  datetime: { name: 'date' },
141
136
  bigint: { name: 'long' },
142
- json: { name: 'object' }
143
- }.merge(
144
- TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
145
- )
137
+ json: { name: 'object' },
138
+
139
+ **TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
140
+ }.freeze
146
141
 
147
142
  def initialize(...)
148
143
  super
@@ -163,15 +158,27 @@ module ActiveRecord # :nodoc:
163
158
  end
164
159
 
165
160
  def schema_migration # :nodoc:
166
- ElasticsearchRecord::SchemaMigration.new(self)
161
+ ElasticsearchRecord::SchemaMigration.new(pool)
162
+ end
163
+
164
+ def internal_metadata # :nodoc:
165
+ ElasticsearchRecord::InternalMetadata.new(pool)
166
+ end
167
+
168
+ # since rails 7.2 the migration context is resolved through the connection pool -
169
+ # provided here as connection method again (used by +#assume_migrated_upto_version+)
170
+ def migration_context # :nodoc:
171
+ pool.migration_context
167
172
  end
168
173
 
169
174
  # provide a table_name_prefix from the configuration to create & restrict schema creation
175
+ # HINT: This is not an official setting and only introduced to ElasticsearchRecord.
170
176
  def table_name_prefix
171
177
  @config.fetch(:table_name_prefix, '')
172
178
  end
173
179
 
174
180
  # provide a table_name_suffix from the configuration to create & restrict schema creation
181
+ # HINT: This is not an official setting and only introduced to ElasticsearchRecord.
175
182
  def table_name_suffix
176
183
  @config.fetch(:table_name_suffix, '')
177
184
  end
@@ -188,7 +195,7 @@ module ActiveRecord # :nodoc:
188
195
  end
189
196
 
190
197
  # Does this adapter support transactions in general?
191
- # HINT: This is +NOT* an official setting and only introduced to ElasticsearchRecord
198
+ # HINT: This is not an official setting and only introduced to ElasticsearchRecord.
192
199
  def supports_transactions?
193
200
  false
194
201
  end
@@ -229,51 +236,69 @@ module ActiveRecord # :nodoc:
229
236
  NATIVE_DATABASE_TYPES
230
237
  end
231
238
 
232
- # calls the +elasticsearch-api+ endpoints by provided namespace and action.
233
- # if a block was provided it'll yield the response.body and returns the blocks result.
234
- # otherwise it will return the response itself...
235
- # @param [Symbol] namespace - the API namespace (e.g. indices, nodes, sql, ...)
236
- # @param [Symbol] action - the API action to call in tha namespace
237
- # @param [Hash] arguments - action arguments
239
+ # calls the +elasticsearch-api+ endpoints by provided gate and returns a response object.
240
+ # @param [Symbol,String] gate - the API namespace & action gate (e.g. 'indices.get','cluster.health', :bulk, ...)
241
+ # @param [Hash] arguments - gate arguments
238
242
  # @param [String (frozen)] name - the logging name
239
- # @param [Boolean] async - send async (default: false) - currently not supported
240
- # @param [Boolean] log - send log to instrumenter (default: true)
243
+ # @param [Boolean] async - send async (default: false) - NOT supported!
244
+ # @param [Boolean] allow_retry - allows to retry a possible failing query (default: false)
245
+ # @param [Boolean] materialize_transactions - materializes transactions (default: false) - NOT supported!
241
246
  # @return [Elasticsearch::API::Response, Object]
242
- def api(namespace, action, arguments = {}, name = 'API', async: false, log: true)
247
+ def api(gate, arguments = {}, name = 'API', async: false, allow_retry: false, materialize_transactions: false)
248
+ raise ::ArgumentError, "ElasticsearchRecord API call is now using a single `gate` instead of providing `namespace & action`.\n'core' namespace must not provided (only provide action symbol) - any other must be provided as string (e.g. 'nodes.stats')" if arguments.is_a?(Symbol)
243
249
  raise ::StandardError, 'ASYNC api calls are not supported' if async
244
250
 
245
- # resolve the API target
246
- target = namespace == :core ? raw_connection : raw_connection.__send__(namespace)
247
-
248
- __send__(:log, "#{namespace}.#{action}", arguments, name, async: async, log: log) do
249
- response = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
250
- target.__send__(action, arguments)
251
- end
252
-
253
- if response.is_a?(::Elasticsearch::API::Response)
254
- # reverse information for the LogSubscriber - shows the 'query-time' in the logs
255
- # this works, since we use a referenced hash ...
256
- arguments[:_qt] = response['took']
251
+ # drop 'core.' prefix, if provided
252
+ if gate.is_a?(String) && gate[0..4] == 'core.'
253
+ # add deprecation warning
254
+ ::ActiveRecord.deprecator.warn(<<~MSG)
255
+ Providing the 'core.' namespace prefix in the `gate` parameter is deprecated and will be removed in a future version.
256
+ Please provide only the action symbol (e.g.:bulk) or the full namespace and action as a string (e.g.'nodes.stats')
257
+ MSG
258
+
259
+ # IMPORTANT: only strip the *prefix* - a +gsub+ would also replace any later occurrence
260
+ # (e.g. 'core.score.thing' would become :sthing)
261
+ gate = gate.delete_prefix('core.').to_sym
262
+ end
257
263
 
258
- # raise timeouts
259
- raise(ActiveRecord::StatementTimeout, "Elasticsearch api request failed due a timeout") if response['timed_out']
264
+ # PLEASE NOTE: Don't remove the +statistics+ assignment here.
265
+ # - this is required as referenced hash for the instrumentation
266
+ log(gate, arguments, name, async: async, statistics: (statistics = {})) do
267
+ with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
268
+ response = ::ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
269
+ # determinate the correct target from the provided gate
270
+ if gate.is_a?(Symbol) || !gate.include?('.')
271
+ conn.__send__(gate, arguments)
272
+ else
273
+ namespace, action = gate.split('.')
274
+ conn.__send__(namespace).__send__(action, arguments)
275
+ end
276
+ end
277
+
278
+ # marks the connection as verified (an AbstractAdapter method, which is used to decide how errors and connections are handled)
279
+ verified!
280
+
281
+ if response.is_a?(::Elasticsearch::API::Response)
282
+ # reverse information for the LogSubscriber - shows the 'query-time' in the logs
283
+ # this works, since we use a referenced hash ...
284
+ statistics[:took] = response['took']
285
+
286
+ # raise timeouts
287
+ raise(::ActiveRecord::StatementTimeout, "Elasticsearch api request failed due a timeout") if response['timed_out']
288
+ end
289
+
290
+ # return response
291
+ response
260
292
  end
261
-
262
- response
263
293
  end
264
294
  end
265
295
 
266
- private
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
296
  def reconnect
275
- @raw_connection = nil
276
- connect
297
+ @lock.synchronize do
298
+ # no need to 'close' a connection for +::Elasticsearch::Client+ / +::Elastic::Transport+
299
+ @raw_connection = nil
300
+ connect
301
+ end
277
302
  end
278
303
 
279
304
  def active?
@@ -283,62 +308,94 @@ module ActiveRecord # :nodoc:
283
308
  # Disconnects from the database if already connected.
284
309
  # Otherwise, this method does nothing.
285
310
  def disconnect!
286
- super
287
- @raw_connection = nil
311
+ @lock.synchronize do
312
+ super
313
+ # no need to 'close' a connection for +::Elasticsearch::Client+ / +::Elastic::Transport+
314
+ @raw_connection = nil
315
+ end
288
316
  end
289
317
 
290
318
  alias :reset! :reconnect!
291
319
 
320
+ private
321
+
322
+ def connect
323
+ @raw_connection = self.class.new_client(@connection_parameters)
324
+ rescue ::ActiveRecord::ConnectionNotEstablished => ex
325
+ raise ex.set_pool(@pool)
326
+ end
327
+
292
328
  def type_map
293
329
  TYPE_MAP
294
330
  end
295
331
 
332
+ # OVERWRITE - to support +::ActiveRecord::StatementTimeout+ as possible retry.
333
+ # IMPORTANT: From the point ES supports transactions (+#supports_transactions?+) this must be redefined with a *super* call.
334
+ def retryable_query_error?(exception)
335
+ exception.is_a?(::ActiveRecord::StatementTimeout) || exception.is_a?(::ActiveRecord::Deadlocked) || exception.is_a?(::ActiveRecord::LockWaitTimeout)
336
+ end
337
+
296
338
  # catch Elasticsearch Transport-errors to be treated as +StatementInvalid+ (the original message is still readable ...)
297
339
  def translate_exception(exception, message:, sql:, binds:)
298
340
  case exception
299
341
  when ::Elastic::Transport::Transport::Errors::ClientClosedRequest
300
- ::ActiveRecord::QueryCanceled.new(message, sql: sql, binds: binds)
342
+ ::ActiveRecord::QueryCanceled.new(message, sql: sql, binds: binds, connection_pool: @pool)
301
343
  when ::Elastic::Transport::Transport::Errors::RequestTimeout
302
- ::ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds)
344
+ ::ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
303
345
  when ::Elastic::Transport::Transport::Errors::Conflict
304
- ::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds)
346
+ ::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
305
347
  when ::Elastic::Transport::Transport::Errors::BadRequest
306
348
  if exception.message.match?(/resource_already_exists_exception/)
307
- ::ActiveRecord::DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
349
+ ::ActiveRecord::DatabaseAlreadyExists.new(message, sql: sql, binds: binds, connection_pool: @pool)
308
350
  else
309
- ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
351
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
310
352
  end
311
353
  when ::Elastic::Transport::Transport::Errors::Unauthorized
312
354
  ::ActiveRecord::DatabaseConnectionError.username_error(@config[:username])
313
355
  # must be last 'Elastic' error
314
356
  when ::Elastic::Transport::Transport::ServerError
315
- ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
357
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
316
358
  else
317
359
  # just forward the exception ...
318
360
  exception
319
361
  end
320
362
  end
321
363
 
322
- # provide a custom log instrumenter for elasticsearch subscribers
323
- def log(gate, arguments, name, async: false, log: true, &block)
324
- if log
325
- @instrumenter.instrument(
326
- "query.elasticsearch_record",
327
- gate: gate,
328
- name: name,
329
- arguments: gate == 'core.msearch' ? arguments.deep_dup : arguments,
330
- async: async) do
331
- @lock.synchronize(&block)
332
- rescue => e
333
- raise translate_exception_class(e, arguments, [])
334
- end
335
- else
336
- begin
337
- @lock.synchronize(&block)
338
- rescue => e
339
- raise translate_exception_class(e, arguments, [])
340
- end
341
- end
364
+ # Logs and instruments Elasticsearch operations.
365
+ #
366
+ # This method facilitates logging and instrumentation of Elasticsearch queries
367
+ # by delegating the execution to the instrumenter. It also captures any raised
368
+ # exceptions during execution, translating them into a domain-specific class.
369
+ #
370
+ # Queries are tracked under the "query.elasticsearch_record" event, allowing
371
+ # listeners or subscribers to analyze the performance and behavior of the
372
+ # queries.
373
+ #
374
+ # A `translate_exception_class` helper is used to ensure that any exceptions
375
+ # raised are appropriately captured and translated with contextual details.
376
+ #
377
+ # @param [Symbol, String] gate the target gate for the Elasticsearch operation,
378
+ # representing either a `<namespace>.<action>` or an `core.<action>`.
379
+ # @param [Hash] arguments the request parameters to be sent for the operation.
380
+ # @param [String] name a label for the operation, defaulting to 'QUERY'.
381
+ # @param [Boolean] async whether the operation is asynchronous. Defaults to `false`.
382
+ # @param [Hash, nil] statistics an optional hash for gathering operation metadata.
383
+ # @yield [block] the operation block to be executed under logging and instrumentation.
384
+ # @raise [Exception] any exception encountered during execution, after translation.
385
+ # @return [void]
386
+ def log(gate, arguments, name = 'QUERY', async: false, statistics: nil, &block)
387
+ @instrumenter.instrument(
388
+ "query.elasticsearch_record",
389
+ gate: gate,
390
+ name: name,
391
+ arguments: arguments,
392
+ async: async,
393
+ statistics: statistics,
394
+ connection: self,
395
+ &block
396
+ )
397
+ rescue => e
398
+ raise translate_exception_class(e, arguments, [])
342
399
  end
343
400
 
344
401
  # returns a new collector for the Arel visitor.
@@ -348,13 +405,13 @@ module ActiveRecord # :nodoc:
348
405
  # so we don't have to check for +prepared_statements+ here.
349
406
  # Also, bindings are (currently) not supported.
350
407
  # So, we just need a single, simple query collector...
351
- Arel::Collectors::ElasticsearchQuery.new
408
+ ::Arel::Collectors::ElasticsearchQuery.new
352
409
  end
353
410
 
354
411
  # returns a new visitor to compile Arel into Elasticsearch Hashes (in this case we use a query object)
355
412
  # @return [Arel::Visitors::Elasticsearch]
356
413
  def arel_visitor
357
- Arel::Visitors::Elasticsearch.new(self)
414
+ ::Arel::Visitors::Elasticsearch.new(self)
358
415
  end
359
416
 
360
417
  # Builds the result object.
@@ -362,14 +419,22 @@ module ActiveRecord # :nodoc:
362
419
  # custom result objects with response-specific data.
363
420
  # @return [ElasticsearchRecord::Result]
364
421
  def build_result(response, columns: [], column_types: {})
365
- ElasticsearchRecord::Result.new(response, columns, column_types)
422
+ ::ElasticsearchRecord::Result.new(response, columns, column_types)
366
423
  end
367
424
 
368
425
  # register native types
369
- ActiveRecord::Type.register(:format_string, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
370
- ActiveRecord::Type.register(:multicast_value, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
371
- ActiveRecord::Type.register(:object, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
372
- ActiveRecord::Type.register(:range, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
426
+ ::ActiveRecord::Type.register(:format_string, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
427
+ ::ActiveRecord::Type.register(:multicast_value, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
428
+ ::ActiveRecord::Type.register(:object, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
429
+ ::ActiveRecord::Type.register(:range, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
373
430
  end
374
431
  end
375
- end
432
+ end
433
+
434
+ # since rails 7.2 an adapter has to register itself - the former load-by-convention
435
+ # through +ConnectionHandling#elasticsearch_connection+ is gone
436
+ ActiveRecord::ConnectionAdapters.register(
437
+ 'elasticsearch',
438
+ 'ActiveRecord::ConnectionAdapters::ElasticsearchAdapter',
439
+ 'active_record/connection_adapters/elasticsearch_adapter'
440
+ )
@@ -9,6 +9,9 @@ module Arel # :nodoc: all
9
9
  # required for ActiveRecord
10
10
  attr_accessor :preparable
11
11
 
12
+ # required for ActiveRecord (rails 7.2 marks idempotent queries as retryable)
13
+ attr_accessor :retryable
14
+
12
15
  # returns the current bind index (default: 1)
13
16
  attr_reader :bind_index
14
17
 
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -122,7 +122,8 @@ module Arel # :nodoc: all
122
122
  # IMPORTANT: Since Elasticsearch does not store nil-values in the +_source+ / +doc+ it will NOT return
123
123
  # empty / nil columns - instead the nil columns do not exist!!!
124
124
  # This is a big mess, because those missing columns are +not+ editable or savable in any way after we initialize the record...
125
- # To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query.
125
+ # To prevent NOT-accessible attributes, we need to provide the "full-column-definition" to the query as +default+.
126
+ # This may be overwritten by the 'visit_Selects'
126
127
  resource_klass = o.source.left.instance_variable_get(:@klass)
127
128
  claim(:columns, resource_klass.source_column_names) if resource_klass.respond_to?(:source_column_names)
128
129
 
@@ -193,22 +194,54 @@ module Arel # :nodoc: all
193
194
  when '*'
194
195
  # force return all fields
195
196
  # assign(:_source, true)
197
+ when ::ElasticsearchRecord::Query::COLUMNS_NONE
198
+ # force return NO fields - metadata fields ('_id', '_score', ...) are not part of the
199
+ # +_source+ and are always returned on the document level, so they stay accessible.
200
+ assign(:_source, false)
201
+
202
+ # clears the columns claimed by +visit_Arel_Nodes_SelectCore+ - a projection is the only
203
+ # place that runs late enough to undo them.
204
+ # HINT: an empty array equals the +Query+ default, so +Result#_results_from_hits+ takes its
205
+ # "no columns" branch and returns the raw document. Together with the +_source: false+ above
206
+ # this leaves exactly the metadata fields.
207
+ claim(:columns, [])
196
208
  when ::ActiveRecord::FinderMethods::ONE_AS_ONE
197
209
  # force return NO fields
198
210
  assign(:_source, false)
211
+
212
+ # also clear the columns in the query (which will be forwarded to +ElasticsearchRecord::Result+)
213
+ # HINT: If future changes rely on the first column value (in this case '1' -> SELECT 1 AS one) this must be fixed within the +ElasticsearchRecord::Result+.
214
+ # Maybe check on the columns[0] value within the #rows method ...
215
+ claim(:columns, %w[one])
199
216
  else
200
- assign(:_source, fields)
201
- # also overwrite the columns in the query
217
+ # IMPORTANT: metadata fields (like '_id' or '_score') are NOT part of the +_source+ node - they
218
+ # are always returned on the document level. Providing them to the +_source+-filter would create
219
+ # a filter that never matches, so they must be removed here.
220
+ source_fields = fields - ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys
221
+
222
+ # if ONLY metadata fields were provided we must not resolve any +_source+ at all.
223
+ assign(:_source, source_fields.presence || false)
224
+
225
+ # also overwrite the columns in the query (which will be forwarded to +ElasticsearchRecord::Result+)
226
+ # HINT: metadata fields must stay within the columns - they are resolved from the document level.
202
227
  claim(:columns, fields)
203
228
  end
204
229
  end
205
230
 
206
231
  # CUSTOM node by elasticsearch_record
207
232
  def visit_Create(o)
208
- # sets values
209
- if o.values
210
- values = collect(o.values) # visit_Arel_Nodes_ValuesList
211
- claim(:body, values) if values.present?
233
+ # detect, if *columns* where provided.
234
+ # This happens through the +::Arel::InsertManager#insert+ by splitting up the columns & values.
235
+ values = if o.columns.present?
236
+ # IMPORTANT: we do not "visit" the columns & rows here but directly build a final hash.
237
+ # o.values.rows.*first* (first) is correct here, since +::Arel::InsertManager#create_values+ will provide all values in a nested Array
238
+ collect(Hash[o.columns.map(&:name).zip(o.values.rows.first)])
239
+ elsif o.values.present?
240
+ collect(o.values)
241
+ end
242
+
243
+ if values.present?
244
+ claim(:body, values)
212
245
  else
213
246
  failed!
214
247
  end
@@ -356,9 +389,9 @@ module Arel # :nodoc: all
356
389
  self.collector.add_binds(values, o.proc_for_binds)
357
390
 
358
391
  if o.type == :in
359
- assign(:filter, [{ terms: { o.column_name => o.casted_values } }])
392
+ assign(:filter, [{ terms: { visit(o.left) => o.casted_values } }])
360
393
  else
361
- assign(:must_not, [{ terms: { o.column_name => o.casted_values } }])
394
+ assign(:must_not, [{ terms: { visit(o.left) => o.casted_values } }])
362
395
  end
363
396
  end
364
397
 
@@ -454,6 +487,7 @@ module Arel # :nodoc: all
454
487
 
455
488
  # alias for ATTRIBUTE returns
456
489
  alias :visit_Arel_Attributes_Attribute :visit_Struct_Attribute
490
+ alias :visit_Arel_Attribute :visit_Struct_Attribute
457
491
  alias :visit_Arel_Nodes_UnqualifiedColumn :visit_Struct_Attribute
458
492
  alias :visit_ActiveModel_Attribute_FromUser :visit_Struct_Attribute
459
493
 
@@ -487,6 +521,14 @@ module Arel # :nodoc: all
487
521
 
488
522
  # alias for ARRAY returns
489
523
  alias :visit_Set :visit_Array
524
+
525
+ def visit_Arel_Nodes_True(o)
526
+ true
527
+ end
528
+
529
+ def visit_Arel_Nodes_False(o)
530
+ false
531
+ end
490
532
  end
491
533
  end
492
534
  end
File without changes
@@ -96,7 +96,7 @@ module ElasticsearchRecord
96
96
  # used to create a cacheable statement.
97
97
  # This is a 1:1 copy, except that we use our own class +ElasticsearchRecord::StatementCache+
98
98
  # see @ ActiveRecord::Core::ClassMethods#cached_find_by_statement
99
- def cached_find_by_statement(key, &block)
99
+ def cached_find_by_statement(connection, key, &block)
100
100
  cache = @find_by_statement_cache[connection.prepared_statements]
101
101
  cache.compute_if_absent(key) { ElasticsearchRecord::StatementCache.create(connection, &block) }
102
102
  end
File without changes
File without changes
@@ -7,9 +7,9 @@ module ElasticsearchRecord
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 1
11
- MINOR = 8
12
- TINY = 2
10
+ MAJOR = 3
11
+ MINOR = 0
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -37,12 +37,14 @@ module ElasticsearchRecord
37
37
  end
38
38
  name = "CACHE #{name}" if payload[:cached]
39
39
 
40
- # nice feature: displays the REAL query-time from elasticsearch response (_qt)
40
+ # nice feature: displays the REAL query-time from elasticsearch response
41
41
  # this is handled through the +::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter#api+ method
42
- name = "#{name} (took: #{payload[:arguments][:_qt].round(1)}ms)" if payload[:arguments][:_qt]
42
+ if payload[:statistics].present?
43
+ name = "#{name} (took: #{payload[:statistics][:took].round(1)}ms)" if payload[:statistics][:took]
44
+ end
43
45
 
44
46
  # build query
45
- query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
47
+ query = payload[:arguments].inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
46
48
 
47
49
  # final coloring
48
50
  name = color(name, name_color(payload[:name]), bold: true)
@@ -62,21 +64,21 @@ module ElasticsearchRecord
62
64
  end
63
65
 
64
66
  def gate_color(gate, name)
65
- case gate
67
+ case gate.to_s
66
68
  # SELECTS
67
- when 'core.get', 'core.mget', 'core.search', 'core.msearch', 'core.count', 'core.exists', 'sql.query'
69
+ when 'get', 'mget', 'search', 'msearch', 'count', 'exists', 'sql.query'
68
70
  BLUE
69
71
  # DELETES
70
- when 'core.delete', 'core.delete_by_query'
72
+ when 'delete', 'delete_by_query'
71
73
  RED
72
74
  # CREATES
73
- when 'core.create', 'core.reindex'
75
+ when 'create', 'reindex'
74
76
  GREEN
75
77
  # UPDATES
76
- when 'core.update', 'core.update_by_query'
78
+ when 'update', 'update_by_query'
77
79
  YELLOW
78
80
  # MIXINS
79
- when /indices\.\w+/, 'core.bulk', 'core.index'
81
+ when /indices\.\w+/, 'bulk', 'index'
80
82
  if name.end_with?('Pit Delete')
81
83
  RED
82
84
  else
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/internal_metadata"
4
+
5
+ module ElasticsearchRecord
6
+ class InternalMetadata < ::ActiveRecord::InternalMetadata
7
+
8
+ # overwrite method to always disable the internal metadata index.
9
+ #
10
+ # Elasticsearch cannot serve the internal metadata table
11
+ # (see +ElasticsearchAdapter#use_metadata_table?+) - but since rails 7.2 the flag is
12
+ # resolved through the database config (default: enabled) instead of the adapter,
13
+ # so a default configuration would suddenly create & write a 'ar_internal_metadata'
14
+ # index on the cluster.
15
+ def enabled?
16
+ false
17
+ end
18
+ end
19
+ end