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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbb80c1852041338e02e17314a2a70e852db077e2724383fe05d1990dc5c601e
4
- data.tar.gz: 5d1f2b445e83e602b16ac571b8b3824784490f9c365a304b4be06aaa02620cb2
3
+ metadata.gz: df21aa38516f4e56830e16ffcff4164b65541a869e3964771771eacdf4e8ad16
4
+ data.tar.gz: 7ba579e28c1a85e7f890654c99c675eb60dd2fddf1b1eb877b0967e17f1d7097
5
5
  SHA512:
6
- metadata.gz: 80ac0419ba02c1d7cc563446067caff43f7ee5b1a92c1155b5d3c2871652b06d5a8998aae4da6ee5c44289c7579a87b1346c15ae15630c11ab8cded34412e524
7
- data.tar.gz: 1559d1ab3873e002c867c88726358a4d1c3f875e087f8b37ec58d9c8ee196fc757490c246409c1bc32871e9210b617e6c48473c7fe6fe68e97ab47e66da010ec
6
+ metadata.gz: cb521dc265ec3a517ebda7b8b6e60b5471b1411bd8f04d6be72098e95177e32af06267bc9a2e2881699d0f7f317c3bf1ef77b6613d9657df6ac7347affa80cf9
7
+ data.tar.gz: 981b0955f21d2260d794cc1946ed560fa70d0e429b5c0bc97d2c9c5b29b396af471cd8d6460929fa451913ad44f7d9992c9b8c0d8a6cd6a7035c338147161138
data/.rspec CHANGED
File without changes
data/.yardopts CHANGED
File without changes
data/Gemfile CHANGED
@@ -4,3 +4,10 @@ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in elasticsearch_record.gemspec
6
6
  gemspec
7
+
8
+ # Pin the Elasticsearch client to the 8.x line for local development & specs.
9
+ # The gemspec intentionally allows '>= 7.17', but the 9.x client sends an
10
+ # 'Accept: application/vnd.elasticsearch+json; compatible-with=9' header that
11
+ # 8.x servers reject with a media_type_header_exception (HTTP 400).
12
+ # Keep this in sync with the server you develop against.
13
+ gem 'elasticsearch', '~> 8.0'
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,10 +16,11 @@ _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.16.1`
20
- - tested with Elasticsearch `8.15.2`
19
+ - This is the `main`-branch, which supports rails **7.2** _(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`)_
22
+ - _ES|QL_ queries _(`TYPE_ESQL` / the `esql.query` gate)_ require **Elasticsearch >= 8.11**, where the feature became
23
+ generally available. All other features remain available from Elasticsearch `7.17`.
21
24
 
22
25
  -----
23
26
 
@@ -25,6 +28,13 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
25
28
 
26
29
  Supported rails versions:
27
30
 
31
+ ### Rails 7.2:
32
+ _(since gem version 2.1)_
33
+
34
+ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-2-stable
35
+
36
+ [![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)
37
+
28
38
  ### Rails 7.1:
29
39
  _(since gem version 1.8)_
30
40
 
@@ -46,11 +56,11 @@ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-0-stable
46
56
  Add this line to your application's Gemfile:
47
57
 
48
58
  ```ruby
49
- gem 'elasticsearch_record', '~> 1.8'
59
+ gem 'elasticsearch_record'
50
60
 
51
61
  # alternative
52
62
  gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-1-stable'
53
- gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-70-stable'
63
+ gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-0-stable'
54
64
 
55
65
  ```
56
66
 
@@ -62,6 +72,78 @@ Or install it yourself as:
62
72
 
63
73
  $ gem install elasticsearch_record
64
74
 
75
+ -----
76
+
77
+ ## Upgrading to 2.0
78
+
79
+ Version **2.0** contains breaking changes. Coming from **1.8.x**, check the following:
80
+
81
+ ### 1. Table (index) names are resolved by default
82
+
83
+ Every table statement now resolves its name through `#_env_table_name`, so the
84
+ `table_name_prefix` / `table_name_suffix` no longer have to be applied by hand in migrations or database statements
85
+ _(which silently wrote into another environment's index)_.
86
+
87
+ ```ruby
88
+ # 1.8.x - the prefix / suffix had to be applied manually
89
+ drop_table _env_table_name("settings")
90
+
91
+ # 2.0 - resolved on its own
92
+ drop_table "settings"
93
+ ```
94
+
95
+ Existing migrations keep working - `_env_table_name` is idempotent and still public, so a
96
+ hand-resolved name resolves to the very same index. A new `decorate:`-argument opts out per
97
+ call, and `ElasticsearchRecord.decorate_table_names` acts as a global kill-switch.
98
+
99
+ _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)_
100
+
101
+ ### 2. `select` raises on metadata fields
102
+
103
+ Metadata fields _(`_id`, `_score`, `_index`, ...)_ are not part of the `_source` node, so they
104
+ could never be resolved through the `_source`-filter this method builds - providing them
105
+ silently created a filter that never matched.
106
+
107
+ ```ruby
108
+ Search.select(:_id)
109
+ # => ActiveRecord::UnknownAttributeReference
110
+ ```
111
+
112
+ They are returned anyway and accessible on each record _(`Search.first._id`)_. To resolve the
113
+ metadata **without** transferring the `_source`, use the new `#meta_only!` method.
114
+
115
+ ### 3. `restore_table` replaces its `open:`-argument with `unblock:`
116
+
117
+ A restore runs through a `clone`, so the restored table **inherits** the 'write'-block of its
118
+ source - the table was open, but read-only. `unblock:` _(default: `true`)_ releases it again.
119
+ `ModelApi#restore!` follows along.
120
+
121
+ ```ruby
122
+ # 1.8.x
123
+ restore_table 'settings', from: 'settings-snapshot-2024', open: true
124
+
125
+ # 2.0
126
+ restore_table 'settings', from: 'settings-snapshot-2024', unblock: true
127
+ ```
128
+
129
+ ### 4. The plural table statements no longer skip AR-internal indices
130
+
131
+ `#open_tables`, `#close_tables`, `#refresh_tables` & `#truncate_tables` no longer subtract the
132
+ ActiveRecord-internal indices _(`schema_migrations` & `ar_internal_metadata`)_ - an explicitly
133
+ named index was silently dropped from the list. They now return an `Array` with an entry for
134
+ **every** provided name.
135
+
136
+ ### 5. `truncate_table` raises for AR-internal indices
137
+
138
+ The statement runs a `drop` & `create` and would wipe the migration state.
139
+ `#drop_table` stays unguarded by design _(ActiveRecord resets both tables through it)_.
140
+
141
+ ### 6. `esql` & `msearch` dropped their `async:`-argument
142
+
143
+ Both dispatch through the public `exec_query` now, since rails 7.1 made `internal_exec_query`
144
+ private - the `async:`-argument was dropped along with it.
145
+
146
+ -----
65
147
 
66
148
  ## Features
67
149
  * ActiveRecord's `create, read, update & delete` behaviours
@@ -71,6 +153,8 @@ Or install it yourself as:
71
153
  * additional relation methods to find records with `filter, must, must_not, should`
72
154
  * aggregated queries with Elasticsearch `aggregation` methods
73
155
  * resolve search response `hits`, `aggregations`, `buckets`, ... instead of ActiveRecord objects
156
+ * `SQL` & `ES|QL` queries resolve their **tabular** response into records _(`find_by_sql` with a String query, `find_by_esql`)_
157
+ * table (index) names are resolved within the current environment _(`table_name_prefix` / `table_name_suffix`)_
74
158
  * Third-party gem support
75
159
  * access `elasticsearch-dsl` query builder through `model.search{ ... }`
76
160
  * Schema
@@ -254,6 +338,11 @@ buckets = Search.where(name: 'A nice object').aggregate(:total, {sum: {field: :a
254
338
  results = Search.where(name: 'A nice object').pit_results
255
339
  # > [{ "_id": "abc123", "name": "A nice object", ...
256
340
 
341
+ # resolves ONLY the metadata nodes of each hit - the '_source' is not transferred at all.
342
+ # (this is the replacement for a - no longer supported - 'select(:_id)')
343
+ metas = Search.where(name: 'A nice object').meta_only!.results
344
+ # > [{ "_id": "abc123", "_index": "search", "_score": 1.0 }, ...
345
+
257
346
  # returns the total value of the query without querying again (it uses the total value from the response)
258
347
  scope = Search.where(name: 'A nice object').limit(5)
259
348
  results_count = scope.count
@@ -287,10 +376,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
287
376
  - must
288
377
  - should
289
378
  - aggregate
290
- - restrict
291
- - hits_only!
292
- - aggs_only!
293
- - total_only!
379
+ - select _(raises on metadata fields - see @ [Upgrading to 2.0](#2-select-raises-on-metadata-fields))_
294
380
 
295
381
  _see simple documentation about these methods @ {ElasticsearchRecord::Relation::QueryMethods rubydoc}_
296
382
 
@@ -307,7 +393,7 @@ _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/ma
307
393
  - boxplot
308
394
  - stats
309
395
  - string_stats
310
- - matrix_stats
396
+ - matrix_stats _(requires at least two columns)_
311
397
  - median_absolute_deviation
312
398
  - calculate
313
399
 
@@ -315,6 +401,13 @@ _see simple documentation about these methods @ {ElasticsearchRecord::Relation::
315
401
 
316
402
  _(also see @ [github](https://github.com/ruby-smart/elasticsearch_record/blob/main/lib/elasticsearch_record/relation/calculation_methods.rb) )_
317
403
 
404
+ ### Available query configuration methods
405
+
406
+ - hits_only! _(prevents to resolve aggs)_
407
+ - aggs_only! _(prevents to resolve hits / source data)_
408
+ - total_only! _(prevents to resolve aggs, hits / source data)_
409
+ - meta_only! _(resolves the metadata nodes (`_id`, `_score`, ...) of each hit without transferring the `_source`)_
410
+
318
411
  ### Available result methods
319
412
  - aggregations
320
413
  - buckets
@@ -543,7 +636,7 @@ Access these methods through the model's connection or within any `Migration`.
543
636
  **Example migration:**
544
637
 
545
638
  ```ruby
546
- class AddTests < ActiveRecord::Migration[7.0]
639
+ class AddTests < ActiveRecord::Migration[7.1]
547
640
  def up
548
641
  create_table "assignments", if_not_exists: true do |t|
549
642
  t.string :key, primary_key: true
@@ -616,17 +709,29 @@ end
616
709
  ```
617
710
 
618
711
  ## environment-related-table-name:
619
- Using the `_env_table_name`-method will resolve the table (index) name within the current environment,
620
- even if the environments shares the same cluster ...
712
+ Table (index) names are resolved within the current environment, even if the environments share the
713
+ same cluster ...
621
714
 
622
715
  This can be provided through the `database.yml` by using the `table_name_prefix/suffix` configuration keys.
623
- Within the migration the `_env_table_name`-method must be used in combination with the table (index) base name.
716
+ **Every table statement applies them by default**, so a migration only ever names the table (index)
717
+ _base_ name.
624
718
 
625
719
  **Example:**
626
720
  Production uses a index suffix with '-pro', development uses '-dev' - they share the same cluster, but different indexes.
627
721
 
628
- For the **settings** table:
722
+ ```yml
723
+ production:
724
+ elasticsearch:
725
+ # ...
726
+ table_name_suffix: '-pro'
727
+
728
+ development:
729
+ elasticsearch:
730
+ # ...
731
+ table_name_suffix: '-dev'
732
+ ```
629
733
 
734
+ For the **settings** table / index this results in the following names:
630
735
  * settings-pro
631
736
  * settings-dev
632
737
 
@@ -634,9 +739,10 @@ A single migration can be created to be used within each environment:
634
739
 
635
740
  ```ruby
636
741
  # Example migration
637
- class AddSettings < ActiveRecord::Migration[7.0]
742
+ class AddSettings < ActiveRecord::Migration[7.1]
638
743
  def up
639
- create_table _env_table_name("settings"), force: true do |t|
744
+ # creates 'settings-pro' on production & 'settings-dev' on development
745
+ create_table "settings", force: true do |t|
640
746
  t.mapping :created_at, :date
641
747
  t.mapping :key, :integer do |m|
642
748
  m.primary_key = true
@@ -653,11 +759,63 @@ class AddSettings < ActiveRecord::Migration[7.0]
653
759
  end
654
760
 
655
761
  def down
656
- drop_table _env_table_name("settings")
762
+ drop_table "settings"
657
763
  end
658
764
  end
659
765
  ```
660
766
 
767
+ ### opting out with `decorate: false`
768
+
769
+ Provide `decorate: false` to address an index by its **literal** name:
770
+
771
+ ```ruby
772
+ # addresses 'settings-pro' - even from a '-dev' suffixed connection
773
+ drop_table "settings-pro", decorate: false
774
+ ```
775
+
776
+ This is required in two cases:
777
+
778
+ * the name is **already resolved** _(e.g. `Model.table_name`, or a name read back from `#tables`)_
779
+ * the base name itself **starts with the prefix** or **ends with the suffix** - `_env_table_name`
780
+ keeps itself idempotent through a `start_with?` / `end_with?` check and cannot tell such a name
781
+ apart from an already resolved one
782
+
783
+ The flag only ever applies to table (index) names - `alias`, `mapping`, `setting` & `meta` names are
784
+ never touched. Statements taking **two** names _(`clone_table`, `rename_table`, `reindex_table`,
785
+ `restore_table`, `backup_table`, `create_table copy_from:`)_ resolve both.
786
+
787
+ The **schema statements** _(`table_exists?`, `table_schema`, `table_mappings`, `table_settings`,
788
+ `columns`, ...)_ are deliberately **not** decorated - ActiveRecord and the schema dumper call them
789
+ with an already resolved index name.
790
+
791
+ The `_env_table_name`-method itself is still public, so existing migrations keep working - it is now
792
+ redundant, since it resolves the very same name the statement would resolve on its own.
793
+
794
+ ### global kill-switch
795
+
796
+ The default of a **not explicitly provided** `decorate:` argument is resolved from a global flag:
797
+
798
+ ```ruby
799
+ # e.g. in an initializer
800
+ ElasticsearchRecord.decorate_table_names = false
801
+ ```
802
+
803
+ Setting it to `false` restores the former, opt-in behaviour, where the decoration had to be applied
804
+ by hand through `_env_table_name`. A single statement can still opt in or out on its own, so
805
+ `decorate: true` keeps working while the flag is off:
806
+
807
+ ```ruby
808
+ ElasticsearchRecord.decorate_table_names = false
809
+
810
+ drop_table "settings" # => drops 'settings'
811
+ drop_table "settings", decorate: true # => drops 'settings-dev'
812
+ drop_table _env_table_name("settings") # => drops 'settings-dev' (the former syntax)
813
+ ```
814
+
815
+ The schema dumper follows the flag: while the decoration is globally disabled it dumps the **full**
816
+ index name with an explicit `decorate: false`, so a dumped schema stays correct even if the flag is
817
+ flipped back on before it is loaded.
818
+
661
819
  ## Docs
662
820
 
663
821
  [CHANGELOG](docs/CHANGELOG.md)
data/Rakefile CHANGED
File without changes
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [3.0.0] - 2026-09-08
4
+ * [add] **BREAKING**: requires `activerecord ~> 7.2.0`
5
+ * [add] `Quoting::ClassMethods#quote_column_name` & `#quote_table_name` - identifier quoting is class-level & mandatory now
6
+ * [add] `ElasticsearchRecord::InternalMetadata` - always disabled _(the flag moved from the adapter to the database config)_
7
+ * [add] `patches/active_record/connection_pool_patch` - routes `#migrations_paths`, `#schema_migration` & `#internal_metadata` back to the adapter
8
+ * [add] specs for `ElasticsearchAdapter#translate_exception`
9
+ * [add] specs for `Relation::ValueMethods#limit_value=`
10
+ * [add] specs for `Core#id=`, `#id_was`, `#write_attribute` & `#read_attribute`
11
+ * [add] specs for `TableSettingDefinition`
12
+ * [add] specs for `InternalMetadata#enabled?`
13
+ * [ref] the adapter registers itself through `ActiveRecord::ConnectionAdapters.register` - `ConnectionHandling#elasticsearch_connection` removed
14
+ * [ref] the adapter provides `#internal_metadata` & `#migration_context` again; `#schema_migration` builds from the **pool**
15
+ * [ref] `SchemaMigration#integer_versions` resolves through `pool.with_connection`
16
+ * [ref] `#find_by_sql`, `#find_by_query` & `#find_by_esql` resolve through `with_connection`; `allow_retry:` forwarded down to `#internal_exec_query`
17
+ * [ref] `#_insert_record`, `#_insert_with_auto_increment` & `#cached_find_by_statement` take the connection as first argument
18
+ * [ref] `Arel::Collectors::ElasticsearchQuery#retryable` accessor added _(rails marks idempotent queries as retryable)_
19
+ * [ref] `assert_mutability!` -> `assert_modifiable!` & `ImmutableRelation` -> `UnmodifiableRelation`
20
+ * [ref] `Arel::Nodes::Or` is a nary node now - takes its children as a single `Array`
21
+ * [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**
22
+
23
+ ## [2.0.0] - 2026-08-10
24
+ * [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
25
+ * [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)_
26
+ * [add] `QueryMethods#select` raises on metadata fields _(`_id`, `_score`, ...)_ - they are always returned anyway
27
+ * [add] `ElasticsearchRecord::Result` now resolves **tabular** responses _(`sql`: columns + rows, `esql`: columns + values)_, so `find_by_sql` _(String)_ & `find_by_esql` instantiate records - rows are zipped against the **response** columns
28
+ * [add] `CalculationMethods#matrix_stats` raises an `ArgumentError` for less than two columns - the metric quantifies the relationship **between** fields
29
+ * [add] `ResultMethods#meta_only!` to resolve the metadata nodes _(`_id`, `_score`, ...)_ of each hit without transferring the `_source`
30
+ * [add] `Query::COLUMNS_NONE` constant for the `'!'` projection marker - replaces the bare literal in `visit_Selects` & `meta_only!`
31
+ * [add] specs for `ElasticsearchRecord::Persistence`, `ModelApi`, `SchemaMigration`, `Querying::ClassMethods`, the `Relation` methods _(Core, Query, Calculation & Result)_, the `Elasticsearch::SchemaStatements`, `TableStatements` & `SchemaDumper`, every class of the `Elasticsearch::Type` namespace _(incl. its `TYPE_MAP` registration)_, `Arel::Collectors::ElasticsearchQuery` and all three `Arel::Visitors::Elasticsearch*` visitors - incl. the 1.8.1 "only ten migrations" and 1.8.2 nested-reset regressions and a `TestIndexWithAutoIncrement` support that builds a model against a real `auto_increment` schema _(destructive calls still run through `TestIndex` and its name guard)_. Behaviours pinned as-is:
32
+ * `#or` compiles into a **failed** query _(the visitor fails every `Arel::Nodes::Grouping`)_, and AR's blank-arg stripping silently drops `configure(key, nil)` _(only the Hash form removes a key)_
33
+ * an index **alias** is dumped as a mapping of the elasticsearch 'alias' field type, which makes the `t.alias` branch of the dumper unreachable
34
+ * opposed nil-handling of metas _(deleted)_ and settings _(kept through `:__force__`)_
35
+ * a failed `create` has no `FAILED_BODIES` entry and falls back to an empty body; a multi-column `order` keeps only the last sort
36
+ * `SchemaMigration#count` and `#delete_version` / `#delete_all_versions` cannot be served by elasticsearch
37
+ * `_update_record` / `_delete_record` always return `0` _(a document-API response carries no `total`)_, and an auto-incremented id is only written as document `_id`, never into the mapped primary-key field
38
+ * `meta_only!` **spawns** instead of mutating the receiver _(unlike the other bang methods)_ and pins the `COLUMNS_NONE` projection; `pit_results` pins its infinite-loop guard
39
+ * [ref] **BREAKING**: every table statement resolves its table (index) name through `#_env_table_name` **by default** - the `table_name_prefix` / `table_name_suffix` no longer have to be applied by hand, which silently wrote into another environment's index. A new `decorate:`-argument _(default: `true`)_ switches it off per call for already resolved names. Only TABLE names are decorated; schema statements stay undecorated
40
+ * [ref] `SchemaDumper#table` dumps the **base** name instead of a `_env_table_name(...)` call - if that name would not resolve back, the full name is dumped with an explicit `decorate: false`
41
+ * [ref] **BREAKING**: `#open_tables`, `#close_tables`, `#refresh_tables` & `#truncate_tables` no longer subtract the AR-internal indices _(an explicitly named index was silently dropped)_ - plain loops now, forwarding `decorate:` to their singular statement and returning an `Array` for every provided name
42
+ * [ref] `TableStatements` drops the stale `:rename_table` entry from `define_unsupported_method` - the real implementation overwrote it anyway
43
+ * [ref] `#restore_table` replaces its `open`-argument with `unblock:` _(default: true)_ - a restore clones and inherits the 'write'-block, so the table was open but **read-only**. `ModelApi#restore!` follows along
44
+ * [ref] `ResultMethods#pit_results` resolves through `ElasticsearchRecord::Result` and respects the current projection _(`ids_only` removed in favour of `meta_only!`)_
45
+ * [ref] `TestIndex` spec support to optionally create/drop a second index _(still guarded by the `ALLOWED` name check)_
46
+ * [fix] `#rename_table` to resolve both names **itself** - the `clear_data_source_cache!` and `cluster_health` calls in between were left with the undecorated name
47
+ * [fix] `#backup_table` to build the auto-generated target from the **already resolved** name _(the suffix landed behind the `-snapshot-` part)_
48
+ * [fix] `#_env_table_name` to cast prefix & suffix - an empty `table_name_prefix:` yml entry resolves to `nil` and raised a `TypeError`
49
+ * [fix] `Result#cast_values` to resolve values from the `_source` node, metadata fields from the document level
50
+ * [fix] `visit_Selects` to no longer provide metadata fields to the `_source`-filter _(never matched)_ - projects `_source: false` if only metadata is selected
51
+ * [fix] `Querying::ClassMethods#esql` & `#msearch` to dispatch through the public `exec_query` _(rails 7.1 made `internal_exec_query` private)_ - the `async:`-argument was dropped along with it
52
+ * [fix] `Querying::ClassMethods#find_by_sql` to reference the provided `sql` for `String` queries _(undefined `query_or_sql` raised a `NameError`)_
53
+ * [fix] `SchemaStatements#max_result_window` to resolve the **flat** setting key and cast it to an `Integer` _(always fell back to 10000 and broke the batch_size guards)_
54
+ * [fix] `SchemaStatements#primary_keys` to always return an `Array` _(the `_meta` branch returned a raw String)_
55
+ * [fix] `CalculationMethods#count` to apply the SQL `LIMIT n OFFSET m` semantic on the resolved total - `terminate_after` alone acts **per shard** and never fires on a count query. `#size` on an unloaded relation was affected as well
56
+ * [fix] `UpdateTableDefinition#change_mapping_attributes` to resolve the current mapping type with a **String** key _(fell back to `:object` / `:nested`, so elasticsearch rejected every mapping parameter)_
57
+ * [fix] `#restore_table` to no longer touch the backup after a `drop_backup: true` _(it no longer exists at that point)_
58
+ * [fix] `_insert_with_auto_increment` to write the **plain, integer** id into `_meta.auto_increment` - the block returns an Array of the `returning` values, which was stored as-is and raised on the NEXT insert. Now unwraps Array / Hash / plain and casts to `Integer`
59
+ * [fix] `ResultMethods#pit_delete` to no longer `select('_id')` _(rejected by the new metadata guard)_ - resolves the ids through `meta_only!`
60
+
3
61
  ## [1.8.2] - 2024-11-26
4
62
  * [fix] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to raise an exception on `nil` assignments
5
63
  * [fix] `Arel::Visitors::ElasticsearchBase#compile` to always reset temporary assignments _(causes missing assignments after a query-build-exception)_
@@ -18,6 +76,14 @@
18
76
  * [add] new repository branch `rails-7-1-stable` to support different rails version
19
77
  * [ref] gemspec to lock on rails 7.1
20
78
 
79
+ ## [1.7.5] - 2024-11-26 _(no gem release)_
80
+ * [ref] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to raise an exception instead of building an empty `QueryClause`
81
+
82
+ ## [1.7.4] - 2024-11-25 _(no gem release)_
83
+ * [fix] `Arel::Visitors::ElasticsearchBase#compile` to always reset temporary assignments _(causes missing assignments after a query-build-exception)_
84
+ * [fix] `Arel::Nodes::SelectAgg` to not merge nil-values
85
+ * [fix] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to prevent nil-Array assignment _(e.g. `[nil]` causes q query exception)_
86
+
21
87
  ## [1.7.3] - 2024-05-07 _(no gem release)_
22
88
  * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
23
89
  * [ref] `ElasticsearchRecord::Relation#limit` to detect `Float::INFINITY` to also set the **max_result_window**
File without changes
data/docs/LICENSE CHANGED
File without changes
@@ -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,10 +32,10 @@ DESC
32
32
 
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency 'activerecord', '~> 7.1.0'
35
+ spec.add_dependency 'activerecord', '~> 7.2.0'
36
36
  spec.add_dependency 'elasticsearch', '>= 7.17'
37
37
 
38
- #spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
38
+ spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
39
39
  spec.add_development_dependency 'rspec', '~> 3.0'
40
40
  spec.add_development_dependency 'rake', "~> 13.0"
41
41
  spec.add_development_dependency 'yard', '~> 0.9'
@@ -131,14 +131,30 @@ module ActiveRecord
131
131
 
132
132
  private
133
133
 
134
+ # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
135
+ # @param [ElasticsearchRecord::Query] query
136
+ # @param [String (frozen),nil] name
137
+ # @param [Array] binds - not supported on the top-level and therefore ignored!
138
+ # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
139
+ # @param [Boolean] async
140
+ # @param [Boolean] allow_retry
141
+ # @return [ElasticsearchRecord::Result]
142
+ def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false, allow_retry: false)
143
+ build_result(
144
+ internal_execute(query, name, async: async, allow_retry: allow_retry),
145
+ columns: query.columns
146
+ )
147
+ end
148
+
134
149
  # Executes the query object in the context of this connection and returns the raw result
135
150
  # from the connection adapter.
136
151
  # @param [ElasticsearchRecord::Query] query
137
152
  # @param [String (frozen),nil] name
138
- # @param [Boolean] async (default: false)
153
+ # @param [Boolean] async (default: false) - NOT supported!
139
154
  # @param [Boolean] allow_retry (default: false)
155
+ # @param [Boolean] materialize_transactions (default: false) - NOT supported!
140
156
  # @return [ElasticsearchRecord::Result]
141
- def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: nil)
157
+ def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: false)
142
158
  # validate the query
143
159
  raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
144
160
  raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
@@ -146,21 +162,7 @@ module ActiveRecord
146
162
  # checks for write query - raises an exception if connection is locked to readonly ...
147
163
  check_if_write_query(query)
148
164
 
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
- )
165
+ api(query.gate, query.query_arguments, name, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
164
166
  end
165
167
  end
166
168
  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
@@ -14,6 +14,12 @@ module ActiveRecord
14
14
  @aliases = HashWithIndifferentAccess.new
15
15
  @metas = HashWithIndifferentAccess.new
16
16
 
17
+ # HINT: Currently there is only one attribute, that cannot be assigned to any of the top hash kind, and also not opts:
18
+ # * dynamic
19
+ # This attribute must be assigned below the *mappings* node but as sibling to *properties*.
20
+ # This can only be done to create a special kind of options (@flags ???) and assign & fetch them within the required arel.
21
+ # Since this only setting can also be assigned to each individual mapping, there is currently no need to build this ...
22
+
17
23
  transform_settings!(settings) if settings.present?
18
24
  transform_mappings!(mappings) if mappings.present?
19
25
  transform_aliases!(aliases) if aliases.present?
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  include ActiveModel::Validations
11
11
 
12
12
  # exclude settings, that are provided through the API but are not part of the index-settings API
13
- IGNORE_NAMES = ['provided_name', 'creation_date', 'uuid', 'version','routing.allocation.initial_recovery','resize'].freeze
13
+ IGNORE_NAMES = ['provided_name', 'creation_date', 'uuid', 'version', 'routing.allocation.initial_recovery', 'resize'].freeze
14
14
 
15
15
  # available setting names
16
16
  # - see @ https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings
@@ -19,13 +19,12 @@ module ActiveRecord
19
19
  FINAL_NAMES = ['number_of_shards', 'routing_partition_size', 'soft_deletes.enabled'].freeze
20
20
 
21
21
  # static names can only be set during index creation or closed
22
- STATIC_NAMES = ['number_of_routing_shards', 'codec',
22
+ STATIC_NAMES = ['number_of_routing_shards', 'codec', 'mode',
23
23
  'soft_deletes.retention_lease.period',
24
24
  'load_fixed_bitset_filters_eagerly', 'shard.check_on_startup',
25
25
 
26
26
  # modules
27
- 'analysis', 'routing', 'unassigned', 'merge', 'similarity', 'search', 'store', 'translog',
28
- 'indexing_pressure'].freeze
27
+ 'analysis', 'routing', 'unassigned', 'merge', 'similarity', 'search', 'store', 'indexing_pressure'].freeze
29
28
 
30
29
  # dynamic names can always be changed
31
30
  DYNAMIC_NAMES = ['number_of_replicas', 'auto_expand_replicas', "search.idle.after", 'refresh_interval',
@@ -34,7 +33,10 @@ module ActiveRecord
34
33
  'max_refresh_listeners', 'analyze.max_token_count', 'highlight.max_analyzed_offset',
35
34
  'max_terms_count', 'max_regex_length', 'query.default_field', 'routing.allocation.enable',
36
35
  'routing.rebalance.enable', 'gc_deletes', 'default_pipeline', 'final_pipeline',
37
- 'hidden', 'blocks'].freeze
36
+ 'hidden', 'blocks',
37
+
38
+ # modules
39
+ 'translog'].freeze
38
40
 
39
41
  VALID_NAMES = (FINAL_NAMES + STATIC_NAMES + DYNAMIC_NAMES).freeze
40
42
 
@@ -66,7 +66,9 @@ module ActiveRecord
66
66
  raise ArgumentError, "you cannot change an unknown mapping '#{name}'" if current_mapping.blank?
67
67
 
68
68
  # build new mapping
69
- mapping = new_mapping_definition(name, current_mapping[:type], **options, &block)
69
+ # IMPORTANT: the received mapping is a String-keyed Hash - a symbol access would resolve
70
+ # nil here, which +TableMappingDefinition+ then falls back to :object / :nested
71
+ mapping = new_mapping_definition(name, current_mapping['type'], **options, &block)
70
72
  define! ChangeMappingDefinition, mapping
71
73
 
72
74
  # check if the mapping is assigned as new primary_key