elasticsearch_record 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27f0d74b39e373493b56511cb08e07ed2fc447da5f33cf5a23a8a080072f3d4e
4
- data.tar.gz: 52f5843b03ea19f3f236910f2268aadb1eba5b7eae48c59734d162fdd2bc6000
3
+ metadata.gz: b3d4d1dd73be19114191e80cba0ec2844ab3049e380998f8ba48fed78f5c5837
4
+ data.tar.gz: 252d5abb68af3199c73850337aac6d6ce2c6e379a9fd5f2bea1656447da29995
5
5
  SHA512:
6
- metadata.gz: '07733791c4f29ed331631d6ede8ba83b23ebad2af68afffda6e998eca7d09f4aad4a57a83c87e10dbd866a98bb8f77a6d4fffc905b1dd214d13f48313d603955'
7
- data.tar.gz: 4f3e9ffb52a06c4db77fb698b7e715eac3ad77aa259df5f66905890f69646b35e4f6ee809c52feae3180544de8e47943f6cff95faeff12c43bfaecc9d0e01cb4
6
+ metadata.gz: 635066f96fb6713a4719ac6fc2a4338dd967b7ca990c9d2c1e644ca05a749be4932f9a07277a42da5340530f0291d7a2bd06edcfdaf89cdcda844339ede6b4fa
7
+ data.tar.gz: 324d987c404a4dd0069d48c30866c6b854719eec91e3051321396ecf457b160dd16dfc29d47427c4e4a7193fc5d82e721f89013ae692ca9cc4642bb005503cb8
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![Gem Version](https://badge.fury.io/rb/elasticsearch_record.svg)](https://badge.fury.io/rb/elasticsearch_record)
7
7
  [![License](https://img.shields.io/github/license/ruby-smart/elasticsearch_record)](docs/LICENSE)
8
8
 
9
+ [![Coverage Status](https://coveralls.io/repos/github/ruby-smart/elasticsearch_record/badge.svg?branch=main&kill_cache=1)](https://coveralls.io/github/ruby-smart/elasticsearch_record?branch=main)
10
+
9
11
  ActiveRecord adapter for Elasticsearch
10
12
 
11
13
  _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionality for Elasticsearch._
@@ -14,9 +16,9 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
14
16
 
15
17
  **PLEASE NOTE:**
16
18
 
17
- - This is the `main`-branch, which currently supports rails **7.1** _(see section 'Rails_Versions' for supported versions)_
18
- - supports ActiveRecord ~> 7.1 + Elasticsearch >= 7.17
19
- - added features up to Elasticsearch `8.17.1`
19
+ - This is the `main`-branch, which supports rails **8.0** _(see section 'Rails_Versions' for supported versions)_
20
+ - supports ActiveRecord ~> 7.2 + Elasticsearch >= 7.17
21
+ - added features up to Elasticsearch `8.17.1` _(tested against `8.19.14`)_
20
22
  - _ES|QL_ queries _(`TYPE_ESQL` / the `esql.query` gate)_ require **Elasticsearch >= 8.11**, where the feature became
21
23
  generally available. All other features remain available from Elasticsearch `7.17`.
22
24
 
@@ -26,8 +28,23 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
26
28
 
27
29
  Supported rails versions:
28
30
 
31
+ ### Rails 8.0:
32
+ _(since gem version 4.0)_
33
+
34
+ https://github.com/ruby-smart/elasticsearch_record/tree/rails-8-0-stable
35
+
36
+ [![rails-7-1-stable](https://img.shields.io/badge/rails-8.0.stable-orange.svg)](https://github.com/ruby-smart/elasticsearch_record/tree/rails-8-0-stable)
37
+
38
+
39
+ ### Rails 7.2:
40
+ _(since gem version 3.0)_
41
+
42
+ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-2-stable
43
+
44
+ [![rails-7-1-stable](https://img.shields.io/badge/rails-7.2.stable-orange.svg)](https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-2-stable)
45
+
29
46
  ### Rails 7.1:
30
- _(since gem version 1.8)_
47
+ _(since gem version 2.0)_
31
48
 
32
49
  https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-1-stable
33
50
 
@@ -47,7 +64,7 @@ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-0-stable
47
64
  Add this line to your application's Gemfile:
48
65
 
49
66
  ```ruby
50
- gem 'elasticsearch_record', '~> 1.8'
67
+ gem 'elasticsearch_record'
51
68
 
52
69
  # alternative
53
70
  gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-1-stable'
@@ -63,6 +80,78 @@ Or install it yourself as:
63
80
 
64
81
  $ gem install elasticsearch_record
65
82
 
83
+ -----
84
+
85
+ ## Upgrading to 2.0
86
+
87
+ Version **2.0** contains breaking changes. Coming from **1.8.x**, check the following:
88
+
89
+ ### 1. Table (index) names are resolved by default
90
+
91
+ Every table statement now resolves its name through `#_env_table_name`, so the
92
+ `table_name_prefix` / `table_name_suffix` no longer have to be applied by hand in migrations or database statements
93
+ _(which silently wrote into another environment's index)_.
94
+
95
+ ```ruby
96
+ # 1.8.x - the prefix / suffix had to be applied manually
97
+ drop_table _env_table_name("settings")
98
+
99
+ # 2.0 - resolved on its own
100
+ drop_table "settings"
101
+ ```
102
+
103
+ Existing migrations keep working - `_env_table_name` is idempotent and still public, so a
104
+ hand-resolved name resolves to the very same index. A new `decorate:`-argument opts out per
105
+ call, and `ElasticsearchRecord.decorate_table_names` acts as a global kill-switch.
106
+
107
+ _see @ [environment-related-table-name](#environment-related-table-name), [opting out with `decorate: false`](#opting-out-with-decorate-false) & [global kill-switch](#global-kill-switch)_
108
+
109
+ ### 2. `select` raises on metadata fields
110
+
111
+ Metadata fields _(`_id`, `_score`, `_index`, ...)_ are not part of the `_source` node, so they
112
+ could never be resolved through the `_source`-filter this method builds - providing them
113
+ silently created a filter that never matched.
114
+
115
+ ```ruby
116
+ Search.select(:_id)
117
+ # => ActiveRecord::UnknownAttributeReference
118
+ ```
119
+
120
+ They are returned anyway and accessible on each record _(`Search.first._id`)_. To resolve the
121
+ metadata **without** transferring the `_source`, use the new `#meta_only!` method.
122
+
123
+ ### 3. `restore_table` replaces its `open:`-argument with `unblock:`
124
+
125
+ A restore runs through a `clone`, so the restored table **inherits** the 'write'-block of its
126
+ source - the table was open, but read-only. `unblock:` _(default: `true`)_ releases it again.
127
+ `ModelApi#restore!` follows along.
128
+
129
+ ```ruby
130
+ # 1.8.x
131
+ restore_table 'settings', from: 'settings-snapshot-2024', open: true
132
+
133
+ # 2.0
134
+ restore_table 'settings', from: 'settings-snapshot-2024', unblock: true
135
+ ```
136
+
137
+ ### 4. The plural table statements no longer skip AR-internal indices
138
+
139
+ `#open_tables`, `#close_tables`, `#refresh_tables` & `#truncate_tables` no longer subtract the
140
+ ActiveRecord-internal indices _(`schema_migrations` & `ar_internal_metadata`)_ - an explicitly
141
+ named index was silently dropped from the list. They now return an `Array` with an entry for
142
+ **every** provided name.
143
+
144
+ ### 5. `truncate_table` raises for AR-internal indices
145
+
146
+ The statement runs a `drop` & `create` and would wipe the migration state.
147
+ `#drop_table` stays unguarded by design _(ActiveRecord resets both tables through it)_.
148
+
149
+ ### 6. `esql` & `msearch` dropped their `async:`-argument
150
+
151
+ Both dispatch through the public `exec_query` now, since rails 7.1 made `internal_exec_query`
152
+ private - the `async:`-argument was dropped along with it.
153
+
154
+ -----
66
155
 
67
156
  ## Features
68
157
  * ActiveRecord's `create, read, update & delete` behaviours
@@ -72,6 +161,8 @@ Or install it yourself as:
72
161
  * additional relation methods to find records with `filter, must, must_not, should`
73
162
  * aggregated queries with Elasticsearch `aggregation` methods
74
163
  * resolve search response `hits`, `aggregations`, `buckets`, ... instead of ActiveRecord objects
164
+ * `SQL` & `ES|QL` queries resolve their **tabular** response into records _(`find_by_sql` with a String query, `find_by_esql`)_
165
+ * table (index) names are resolved within the current environment _(`table_name_prefix` / `table_name_suffix`)_
75
166
  * Third-party gem support
76
167
  * access `elasticsearch-dsl` query builder through `model.search{ ... }`
77
168
  * Schema
@@ -255,6 +346,11 @@ buckets = Search.where(name: 'A nice object').aggregate(:total, {sum: {field: :a
255
346
  results = Search.where(name: 'A nice object').pit_results
256
347
  # > [{ "_id": "abc123", "name": "A nice object", ...
257
348
 
349
+ # resolves ONLY the metadata nodes of each hit - the '_source' is not transferred at all.
350
+ # (this is the replacement for a - no longer supported - 'select(:_id)')
351
+ metas = Search.where(name: 'A nice object').meta_only!.results
352
+ # > [{ "_id": "abc123", "_index": "search", "_score": 1.0 }, ...
353
+
258
354
  # returns the total value of the query without querying again (it uses the total value from the response)
259
355
  scope = Search.where(name: 'A nice object').limit(5)
260
356
  results_count = scope.count
@@ -288,10 +384,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
288
384
  - must
289
385
  - should
290
386
  - aggregate
291
- - restrict
292
- - hits_only!
293
- - aggs_only!
294
- - total_only!
387
+ - select _(raises on metadata fields - see @ [Upgrading to 2.0](#2-select-raises-on-metadata-fields))_
295
388
 
296
389
  _see simple documentation about these methods @ {ElasticsearchRecord::Relation::QueryMethods rubydoc}_
297
390
 
@@ -308,7 +401,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
308
401
  - boxplot
309
402
  - stats
310
403
  - string_stats
311
- - matrix_stats
404
+ - matrix_stats _(requires at least two columns)_
312
405
  - median_absolute_deviation
313
406
  - calculate
314
407
 
@@ -316,6 +409,13 @@ _see simple documentation about these methods @ {ElasticsearchRecord::Relation::
316
409
 
317
410
  _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/main/lib/elasticsearch_record/relation/calculation_methods.rb) )_
318
411
 
412
+ ### Available query configuration methods
413
+
414
+ - hits_only! _(prevents to resolve aggs)_
415
+ - aggs_only! _(prevents to resolve hits / source data)_
416
+ - total_only! _(prevents to resolve aggs, hits / source data)_
417
+ - meta_only! _(resolves the metadata nodes (`_id`, `_score`, ...) of each hit without transferring the `_source`)_
418
+
319
419
  ### Available result methods
320
420
  - aggregations
321
421
  - buckets
@@ -544,7 +644,7 @@ Access these methods through the model's connection or within any `Migration`.
544
644
  **Example migration:**
545
645
 
546
646
  ```ruby
547
- class AddTests < ActiveRecord::Migration[7.0]
647
+ class AddTests < ActiveRecord::Migration[7.1]
548
648
  def up
549
649
  create_table "assignments", if_not_exists: true do |t|
550
650
  t.string :key, primary_key: true
@@ -647,7 +747,7 @@ A single migration can be created to be used within each environment:
647
747
 
648
748
  ```ruby
649
749
  # Example migration
650
- class AddSettings < ActiveRecord::Migration[7.0]
750
+ class AddSettings < ActiveRecord::Migration[7.1]
651
751
  def up
652
752
  # creates 'settings-pro' on production & 'settings-dev' on development
653
753
  create_table "settings", force: true do |t|
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [4.0.0] - 2026-09-08
4
+ * [add] **BREAKING**: requires `activerecord ~> 8.0.0`
5
+ * [add] `ElasticsearchRecord::Result#indexed_rows` - rails 8.0 instantiates records through this method instead of iterating the result set itself.
6
+ * [ref] `ElasticsearchAdapter#log` resolves the instrumenter through `#instrumenter` instead of the former `@instrumenter` ivar
7
+ * [ref] `ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaDumper` remove redundant blank lines
8
+
9
+ ## [3.0.0] - 2026-09-08
10
+ * [add] **BREAKING**: requires `activerecord ~> 7.2.0`
11
+ * [add] `Quoting::ClassMethods#quote_column_name` & `#quote_table_name` - identifier quoting is class-level & mandatory now
12
+ * [add] `ElasticsearchRecord::InternalMetadata` - always disabled _(the flag moved from the adapter to the database config)_
13
+ * [add] `patches/active_record/connection_pool_patch` - routes `#migrations_paths`, `#schema_migration` & `#internal_metadata` back to the adapter
14
+ * [add] specs for `ElasticsearchAdapter#translate_exception`
15
+ * [add] specs for `Relation::ValueMethods#limit_value=`
16
+ * [add] specs for `Core#id=`, `#id_was`, `#write_attribute` & `#read_attribute`
17
+ * [add] specs for `TableSettingDefinition`
18
+ * [add] specs for `InternalMetadata#enabled?`
19
+ * [ref] the adapter registers itself through `ActiveRecord::ConnectionAdapters.register` - `ConnectionHandling#elasticsearch_connection` removed
20
+ * [ref] the adapter provides `#internal_metadata` & `#migration_context` again; `#schema_migration` builds from the **pool**
21
+ * [ref] `SchemaMigration#integer_versions` resolves through `pool.with_connection`
22
+ * [ref] `#find_by_sql`, `#find_by_query` & `#find_by_esql` resolve through `with_connection`; `allow_retry:` forwarded down to `#internal_exec_query`
23
+ * [ref] `#_insert_record`, `#_insert_with_auto_increment` & `#cached_find_by_statement` take the connection as first argument
24
+ * [ref] `Arel::Collectors::ElasticsearchQuery#retryable` accessor added _(rails marks idempotent queries as retryable)_
25
+ * [ref] `assert_mutability!` -> `assert_modifiable!` & `ImmutableRelation` -> `UnmodifiableRelation`
26
+ * [ref] `Arel::Nodes::Or` is a nary node now - takes its children as a single `Array`
27
+ * [fix] `Querying::ClassMethods#search` to nest the `Elasticsearch::DSL` block result into the `body` and to only enter the DSL branch **if a block was provided**
28
+
3
29
  ## [2.0.0] - 2026-08-10
4
30
  * [add] `ElasticsearchRecord.decorate_table_names` _(default: `true`)_ as global kill-switch for the table name decoration - only provides the default for an omitted `decorate:`-argument, `#_env_table_name` stays unaffected
5
31
  * [add] `TableStatements#truncate_table` raises an `ArgumentError` for AR-internal indices - the statement runs a `drop` & `create` and would wipe the migration state. `#drop_table` stays unguarded by design _(AR resets both tables through it)_
@@ -14,7 +14,7 @@ DESC
14
14
 
15
15
  spec.homepage = "https://github.com/ruby-smart/elasticsearch_record"
16
16
  spec.license = "MIT"
17
- spec.required_ruby_version = ">= 2.7.0"
17
+ spec.required_ruby_version = ">= 3.1.0"
18
18
 
19
19
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
20
  spec.metadata["homepage_uri"] = spec.homepage
@@ -32,7 +32,7 @@ DESC
32
32
 
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency 'activerecord', '~> 7.1.0'
35
+ spec.add_dependency 'activerecord', '~> 8.0.0'
36
36
  spec.add_dependency 'elasticsearch', '>= 7.17'
37
37
 
38
38
  spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
@@ -137,10 +137,11 @@ module ActiveRecord
137
137
  # @param [Array] binds - not supported on the top-level and therefore ignored!
138
138
  # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
139
139
  # @param [Boolean] async
140
+ # @param [Boolean] allow_retry
140
141
  # @return [ElasticsearchRecord::Result]
141
- def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
142
+ def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false, allow_retry: false)
142
143
  build_result(
143
- internal_execute(query, name, async: async),
144
+ internal_execute(query, name, async: async, allow_retry: allow_retry),
144
145
  columns: query.columns
145
146
  )
146
147
  end
@@ -4,6 +4,22 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Elasticsearch
6
6
  module Quoting # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods # :nodoc:
10
+ # since rails 7.2 identifier quoting is class-level & mandatory (the abstract
11
+ # implementation raises a +NotImplementedError+). Elasticsearch fields & indices are
12
+ # plain JSON keys - so they pass through unquoted.
13
+ def quote_column_name(column_name)
14
+ column_name.to_s
15
+ end
16
+
17
+ # Quotes the table (index) name. Defaults to column name quoting.
18
+ def quote_table_name(table_name)
19
+ quote_column_name(table_name)
20
+ end
21
+ end
22
+
7
23
  def quoted_true
8
24
  'true'
9
25
  end
@@ -151,14 +151,12 @@ module ActiveRecord
151
151
  end
152
152
 
153
153
  tbl.puts " end"
154
- tbl.puts
155
154
 
156
155
  tbl.rewind
157
156
  stream.print tbl.read
158
157
  rescue => e
159
158
  stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
160
159
  stream.puts "# #{e.message}"
161
- stream.puts
162
160
  ensure
163
161
  self.table_name = nil
164
162
  end
@@ -22,16 +22,6 @@ 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
37
27
  include Elasticsearch::UnsupportedImplementation
@@ -168,7 +158,17 @@ module ActiveRecord # :nodoc:
168
158
  end
169
159
 
170
160
  def schema_migration # :nodoc:
171
- 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
172
172
  end
173
173
 
174
174
  # provide a table_name_prefix from the configuration to create & restrict schema creation
@@ -384,7 +384,9 @@ module ActiveRecord # :nodoc:
384
384
  # @raise [Exception] any exception encountered during execution, after translation.
385
385
  # @return [void]
386
386
  def log(gate, arguments, name = 'QUERY', async: false, statistics: nil, &block)
387
- @instrumenter.instrument(
387
+ # +instrumenter+ instead of the former +@instrumenter+ ivar - the abstract adapter
388
+ # resolves it per execution context now
389
+ instrumenter.instrument(
388
390
  "query.elasticsearch_record",
389
391
  gate: gate,
390
392
  name: name,
@@ -429,4 +431,12 @@ module ActiveRecord # :nodoc:
429
431
  ::ActiveRecord::Type.register(:range, ::ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
430
432
  end
431
433
  end
432
- end
434
+ end
435
+
436
+ # since rails 7.2 an adapter has to register itself - the former load-by-convention
437
+ # through +ConnectionHandling#elasticsearch_connection+ is gone
438
+ ActiveRecord::ConnectionAdapters.register(
439
+ 'elasticsearch',
440
+ 'ActiveRecord::ConnectionAdapters::ElasticsearchAdapter',
441
+ 'active_record/connection_adapters/elasticsearch_adapter'
442
+ )
@@ -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
 
@@ -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
@@ -7,7 +7,7 @@ module ElasticsearchRecord
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 2
10
+ MAJOR = 4
11
11
  MINOR = 0
12
12
  TINY = 0
13
13
  PRE = nil
@@ -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
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract/connection_pool'
4
+
5
+ module ElasticsearchRecord
6
+ module Patches
7
+ module ActiveRecord
8
+ # Since rails 7.2 the migration plumbing is resolved through the connection POOL
9
+ # (+ConnectionPool#migration_context+ builds from +#migrations_paths+, +#schema_migration+ &
10
+ # +#internal_metadata+) instead of the connection - so the adapters own implementations are
11
+ # no longer consulted. Without this patch a Elasticsearch pool would migrate with the generic
12
+ # +ActiveRecord::SchemaMigration+ (which resolves the index name through +ActiveRecord::Base+
13
+ # and only ever sees the first ten migrations - Elasticsearch default search size), enable the
14
+ # internal metadata index (the adapter cannot serve it) and fall back to the default
15
+ # 'db/migrate' path.
16
+ #
17
+ # The patch simply routes those factories back to the adapter class, which keeps the
18
+ # Elasticsearch-specific implementations in one place.
19
+ module ConnectionPoolPatch
20
+ def self.included(base)
21
+ base.send(:prepend, PrependMethods)
22
+ end
23
+
24
+ module PrependMethods
25
+ def migrations_paths
26
+ return super unless elasticsearch_pool?
27
+
28
+ db_config.migrations_paths || ['db/migrate_elasticsearch']
29
+ end
30
+
31
+ def schema_migration
32
+ return super unless elasticsearch_pool?
33
+
34
+ ElasticsearchRecord::SchemaMigration.new(self)
35
+ end
36
+
37
+ def internal_metadata
38
+ return super unless elasticsearch_pool?
39
+
40
+ ElasticsearchRecord::InternalMetadata.new(self)
41
+ end
42
+
43
+ private
44
+
45
+ # returns true, if the pools config resolves to the Elasticsearch adapter
46
+ # @return [Boolean]
47
+ def elasticsearch_pool?
48
+ db_config.adapter_class <= ::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # include once only!
57
+ ::ActiveRecord::ConnectionAdapters::ConnectionPool.include(ElasticsearchRecord::Patches::ActiveRecord::ConnectionPoolPatch) unless ::ActiveRecord::ConnectionAdapters::ConnectionPool.included_modules.include?(ElasticsearchRecord::Patches::ActiveRecord::ConnectionPoolPatch)
@@ -5,14 +5,15 @@ module ElasticsearchRecord
5
5
  module ClassMethods
6
6
  # insert a new record into the Elasticsearch index
7
7
  # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
8
+ # @param [ActiveRecord::ConnectionAdapters::ElasticsearchAdapter] connection
8
9
  # @param [ActiveModel::Attribute] values
9
10
  # @return [Object] id
10
- def _insert_record(values, returning)
11
+ def _insert_record(connection, values, returning)
11
12
  # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so the casted values gets resolved here
12
13
  values = values.transform_values(&:value)
13
14
 
14
15
  # resolve & update a auto_increment value, if configured
15
- _insert_with_auto_increment(values) do |arguments|
16
+ _insert_with_auto_increment(connection, values) do |arguments|
16
17
  # build new query
17
18
  query = ElasticsearchRecord::Query.new(
18
19
  index: table_name,
@@ -62,7 +63,7 @@ module ElasticsearchRecord
62
63
  private
63
64
 
64
65
  # Resolves the +auto_increment+ status from the tables +_meta+ attributes.
65
- def _insert_with_auto_increment(values)
66
+ def _insert_with_auto_increment(connection, values)
66
67
  # check, if the primary_key's value is provided.
67
68
  # so, no need to resolve a +auto_increment+ value, but provide the id directly
68
69
  if (id = values[self.primary_key]).present?
@@ -42,8 +42,9 @@ module ElasticsearchRecord
42
42
  # @param [String, Hash, ElasticsearchRecord::Query] sql
43
43
  # @param [Array] binds
44
44
  # @param [nil] preparable
45
+ # @param [Boolean] allow_retry
45
46
  # @param [Proc] block
46
- def find_by_sql(sql, binds = [], preparable: nil, &block)
47
+ def find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)
47
48
  query = case sql
48
49
  when String # really find by SQL
49
50
  ElasticsearchRecord::Query.new(
@@ -61,7 +62,11 @@ module ElasticsearchRecord
61
62
  sql
62
63
  end
63
64
 
64
- _load_from_sql(_query_by_sql(query, binds), &block)
65
+ result = with_connection do |c|
66
+ _query_by_sql(c, query, binds, preparable: preparable, allow_retry: allow_retry)
67
+ end
68
+
69
+ _load_from_sql(result, &block)
65
70
  end
66
71
 
67
72
  # finds records by query arguments
@@ -74,7 +79,11 @@ module ElasticsearchRecord
74
79
  # IMPORTANT: Always provide all columns to prevent unknown attributes that should be nil ...
75
80
  columns: source_column_names)
76
81
 
77
- _load_from_sql(_query_by_sql(query), &block)
82
+ result = with_connection do |c|
83
+ _query_by_sql(c, query)
84
+ end
85
+
86
+ _load_from_sql(result, &block)
78
87
  end
79
88
 
80
89
  # ES|QL query API
@@ -90,7 +99,11 @@ module ElasticsearchRecord
90
99
  # IMPORTANT: Always provide all columns
91
100
  columns: source_column_names)
92
101
 
93
- _load_from_sql(_query_by_sql(query), &block)
102
+ result = with_connection do |c|
103
+ _query_by_sql(c, query)
104
+ end
105
+
106
+ _load_from_sql(result, &block)
94
107
  end
95
108
 
96
109
  # executes a +esql+ by provided *ES|SL* query
@@ -123,17 +136,28 @@ module ElasticsearchRecord
123
136
  connection.exec_query(query, "#{name} Msearch")
124
137
  end
125
138
 
126
- # executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
139
+ # executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded.
140
+ #
141
+ # Without a block the trailing options-hash is used as query arguments.
142
+ # With a block the +Elasticsearch::DSL+ gem builds the query - if the gem is not
143
+ # available (or the block fails), the block is SILENTLY ignored and the
144
+ # options-hash is used instead.
127
145
  def search(*args, &block)
128
- begin
129
- # require the Elasticsearch::DSL gem, if loaded
130
- require 'elasticsearch/dsl'
131
- query = ::Elasticsearch::DSL::Search::Search.new(*args, &block).to_hash
132
- rescue LoadError
133
- query = args.extract_options!
134
- rescue
135
- query = args.extract_options!
136
- end
146
+ query = if block_given?
147
+ begin
148
+ # require the Elasticsearch::DSL gem, if loaded
149
+ require 'elasticsearch/dsl'
150
+ # PLEASE NOTE: +Search#to_hash+ returns the request BODY - it must be nested
151
+ # into the query arguments, otherwise it is sent as (invalid) URL parameters.
152
+ { body: ::Elasticsearch::DSL::Search::Search.new(*args, &block).to_hash }
153
+ rescue LoadError
154
+ args.extract_options!
155
+ rescue
156
+ args.extract_options!
157
+ end
158
+ else
159
+ args.extract_options!
160
+ end
137
161
 
138
162
  find_by_query(query)
139
163
  end
@@ -10,7 +10,7 @@ module ElasticsearchRecord
10
10
  # transforms the current relation into arel, compiles it to query and executes the query.
11
11
  # returns the result object.
12
12
  #
13
- # PLEASE NOTE: This makes the query +immutable+ and raises a +ActiveRecord::ImmutableRelation+
13
+ # PLEASE NOTE: This makes the query +immutable+ and raises a +ActiveRecord::UnmodifiableRelation+
14
14
  # if you try to change it's values.
15
15
  #
16
16
  # PLEASE NOTE: resolving records _(instantiate)_ is never possible after calling this method!
@@ -109,7 +109,8 @@ module ElasticsearchRecord # :nodoc:
109
109
  left = left[key]
110
110
  right = right[key]
111
111
 
112
- or_clause = Arel::Nodes::Or.new(left, right)
112
+ # since rails 7.2 +Arel::Nodes::Or+ is a nary node and takes its children as a single Array
113
+ or_clause = Arel::Nodes::Or.new([left, right])
113
114
 
114
115
  common.predicates[key] = ElasticsearchRecord::Relation::QueryClause.new(key, [Arel::Nodes::Grouping.new(or_clause)])
115
116
  common
@@ -260,7 +260,7 @@ module ElasticsearchRecord
260
260
  unless _valid_unscoping_values.include?(scope)
261
261
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{_valid_unscoping_values.to_a.join(", :")}."
262
262
  end
263
- assert_mutability!
263
+ assert_modifiable!
264
264
  @values.delete(scope)
265
265
  when Hash
266
266
  scope.each do |key, target_value|
@@ -8,7 +8,7 @@ module ElasticsearchRecord
8
8
 
9
9
  def kind_value=(value)
10
10
  # checks if records are already loaded - in this case we cannot mutate the query anymore
11
- assert_mutability!
11
+ assert_modifiable!
12
12
 
13
13
  @values[:kind] = value.to_sym
14
14
  end
@@ -18,7 +18,7 @@ module ElasticsearchRecord
18
18
  end
19
19
 
20
20
  def configure_value=(value)
21
- assert_mutability!
21
+ assert_modifiable!
22
22
 
23
23
  @values[:configure] = value
24
24
  end
@@ -28,7 +28,7 @@ module ElasticsearchRecord
28
28
  end
29
29
 
30
30
  def query_clause=(value)
31
- assert_mutability!
31
+ assert_modifiable!
32
32
 
33
33
  @values[:query] = value
34
34
  end
@@ -38,7 +38,7 @@ module ElasticsearchRecord
38
38
  end
39
39
 
40
40
  def aggs_clause=(value)
41
- assert_mutability!
41
+ assert_modifiable!
42
42
 
43
43
  @values[:aggs] = value
44
44
  end
@@ -172,6 +172,16 @@ module ElasticsearchRecord
172
172
  n ? computed_results.last(n) : computed_results.last
173
173
  end
174
174
 
175
+ # Returns the rows to instantiate records from - ActiveRecord resolves them through this
176
+ # method instead of iterating the result set itself.
177
+ # Each element is a Hash keyed by column / metadata name and therefore already provides the
178
+ # read surface of +ActiveRecord::Result::IndexedRow+ (+key?+, +[]+, +fetch+, +keys+, ...).
179
+ # used by +ActiveRecord+
180
+ # @return [Array<Hash>]
181
+ def indexed_rows # :nodoc:
182
+ computed_results
183
+ end
184
+
175
185
  # used by ActiveRecord
176
186
  def cancel # :nodoc:
177
187
  self
@@ -14,9 +14,12 @@ module ElasticsearchRecord
14
14
  sm = Arel::SelectManager.new(arel_table)
15
15
  sm.project(arel_table[primary_key])
16
16
  sm.order(arel_table[primary_key].asc)
17
- sm.take(connection.max_result_window(table_name))
18
17
 
19
- connection.select_values(sm, "#{self.class} Load")
18
+ @pool.with_connection do |connection|
19
+ sm.take(connection.max_result_window(table_name))
20
+
21
+ connection.select_values(sm, "#{self.class} Load")
22
+ end
20
23
  end
21
24
  end
22
25
  end
File without changes
@@ -22,6 +22,7 @@ module ElasticsearchRecord
22
22
  eager_autoload do
23
23
  autoload :Base
24
24
  autoload :Core
25
+ autoload :InternalMetadata
25
26
  autoload :ModelSchema
26
27
  autoload :ModelApi
27
28
  autoload :Persistence
@@ -84,6 +85,7 @@ end
84
85
 
85
86
  ActiveSupport.on_load(:active_record) do
86
87
  # load patches
88
+ require 'elasticsearch_record/patches/active_record/connection_pool_patch'
87
89
  require 'elasticsearch_record/patches/active_record/relation_merger_patch'
88
90
  require 'elasticsearch_record/patches/arel/select_core_patch'
89
91
  require 'elasticsearch_record/patches/arel/select_manager_patch'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 7.1.0
18
+ version: 8.0.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 7.1.0
25
+ version: 8.0.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: elasticsearch
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -186,8 +186,10 @@ files:
186
186
  - lib/elasticsearch_record/instrumentation/controller_runtime.rb
187
187
  - lib/elasticsearch_record/instrumentation/log_subscriber.rb
188
188
  - lib/elasticsearch_record/instrumentation/railtie.rb
189
+ - lib/elasticsearch_record/internal_metadata.rb
189
190
  - lib/elasticsearch_record/model_api.rb
190
191
  - lib/elasticsearch_record/model_schema.rb
192
+ - lib/elasticsearch_record/patches/active_record/connection_pool_patch.rb
191
193
  - lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb
192
194
  - lib/elasticsearch_record/patches/arel/select_core_patch.rb
193
195
  - lib/elasticsearch_record/patches/arel/select_manager_patch.rb
@@ -226,7 +228,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
228
  requirements:
227
229
  - - ">="
228
230
  - !ruby/object:Gem::Version
229
- version: 2.7.0
231
+ version: 3.1.0
230
232
  required_rubygems_version: !ruby/object:Gem::Requirement
231
233
  requirements:
232
234
  - - ">="