elasticsearch_record 1.8.2 → 2.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 (77) 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 +75 -9
  6. data/Rakefile +0 -0
  7. data/docs/CHANGELOG.md +46 -0
  8. data/docs/CODE_OF_CONDUCT.md +0 -0
  9. data/docs/LICENSE +0 -0
  10. data/elasticsearch_record.gemspec +1 -1
  11. data/lib/active_record/connection_adapters/elasticsearch/column.rb +0 -0
  12. data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +18 -17
  13. data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +0 -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/column_methods.rb +0 -0
  18. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +6 -0
  19. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +0 -0
  20. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +0 -0
  21. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_meta_definition.rb +0 -0
  22. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +7 -5
  23. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +3 -1
  24. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +0 -0
  25. data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +24 -3
  26. data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +39 -34
  27. data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +251 -72
  28. data/lib/active_record/connection_adapters/elasticsearch/transactions.rb +0 -0
  29. data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +0 -0
  30. data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +0 -0
  31. data/lib/active_record/connection_adapters/elasticsearch/type/nested.rb +0 -0
  32. data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +0 -0
  33. data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +0 -0
  34. data/lib/active_record/connection_adapters/elasticsearch/type.rb +0 -0
  35. data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +0 -0
  36. data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +144 -87
  37. data/lib/arel/collectors/elasticsearch_query.rb +0 -0
  38. data/lib/arel/nodes/select_agg.rb +0 -0
  39. data/lib/arel/nodes/select_configure.rb +0 -0
  40. data/lib/arel/nodes/select_kind.rb +0 -0
  41. data/lib/arel/nodes/select_query.rb +0 -0
  42. data/lib/arel/visitors/elasticsearch.rb +0 -0
  43. data/lib/arel/visitors/elasticsearch_query.rb +51 -9
  44. data/lib/elasticsearch_record/base.rb +0 -0
  45. data/lib/elasticsearch_record/core.rb +0 -0
  46. data/lib/elasticsearch_record/errors.rb +0 -0
  47. data/lib/elasticsearch_record/extensions/relation.rb +0 -0
  48. data/lib/elasticsearch_record/gem_version.rb +3 -3
  49. data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +0 -0
  50. data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +11 -9
  51. data/lib/elasticsearch_record/instrumentation/railtie.rb +0 -0
  52. data/lib/elasticsearch_record/instrumentation.rb +0 -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/relation_merger_patch.rb +0 -0
  56. data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +0 -0
  57. data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +0 -0
  58. data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +0 -0
  59. data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +0 -0
  60. data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +0 -0
  61. data/lib/elasticsearch_record/persistence.rb +35 -18
  62. data/lib/elasticsearch_record/query.rb +53 -33
  63. data/lib/elasticsearch_record/querying.rb +5 -7
  64. data/lib/elasticsearch_record/relation/calculation_methods.rb +38 -2
  65. data/lib/elasticsearch_record/relation/core_methods.rb +0 -0
  66. data/lib/elasticsearch_record/relation/query_clause.rb +0 -0
  67. data/lib/elasticsearch_record/relation/query_clause_tree.rb +0 -0
  68. data/lib/elasticsearch_record/relation/query_methods.rb +30 -0
  69. data/lib/elasticsearch_record/relation/result_methods.rb +47 -43
  70. data/lib/elasticsearch_record/relation/value_methods.rb +0 -0
  71. data/lib/elasticsearch_record/result.rb +144 -60
  72. data/lib/elasticsearch_record/schema_migration.rb +0 -0
  73. data/lib/elasticsearch_record/statement_cache.rb +0 -0
  74. data/lib/elasticsearch_record/tasks/elasticsearch_database_tasks.rb +0 -0
  75. data/lib/elasticsearch_record/version.rb +0 -0
  76. data/lib/elasticsearch_record.rb +15 -0
  77. metadata +17 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbb80c1852041338e02e17314a2a70e852db077e2724383fe05d1990dc5c601e
4
- data.tar.gz: 5d1f2b445e83e602b16ac571b8b3824784490f9c365a304b4be06aaa02620cb2
3
+ metadata.gz: 27f0d74b39e373493b56511cb08e07ed2fc447da5f33cf5a23a8a080072f3d4e
4
+ data.tar.gz: 52f5843b03ea19f3f236910f2268aadb1eba5b7eae48c59734d162fdd2bc6000
5
5
  SHA512:
6
- metadata.gz: 80ac0419ba02c1d7cc563446067caff43f7ee5b1a92c1155b5d3c2871652b06d5a8998aae4da6ee5c44289c7579a87b1346c15ae15630c11ab8cded34412e524
7
- data.tar.gz: 1559d1ab3873e002c867c88726358a4d1c3f875e087f8b37ec58d9c8ee196fc757490c246409c1bc32871e9210b617e6c48473c7fe6fe68e97ab47e66da010ec
6
+ metadata.gz: '07733791c4f29ed331631d6ede8ba83b23ebad2af68afffda6e998eca7d09f4aad4a57a83c87e10dbd866a98bb8f77a6d4fffc905b1dd214d13f48313d603955'
7
+ data.tar.gz: 4f3e9ffb52a06c4db77fb698b7e715eac3ad77aa259df5f66905890f69646b35e4f6ee809c52feae3180544de8e47943f6cff95faeff12c43bfaecc9d0e01cb4
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
@@ -16,8 +16,9 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
16
16
 
17
17
  - This is the `main`-branch, which currently supports rails **7.1** _(see section 'Rails_Versions' for supported versions)_
18
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
+ - added features up to Elasticsearch `8.17.1`
20
+ - _ES|QL_ queries _(`TYPE_ESQL` / the `esql.query` gate)_ require **Elasticsearch >= 8.11**, where the feature became
21
+ generally available. All other features remain available from Elasticsearch `7.17`.
21
22
 
22
23
  -----
23
24
 
@@ -50,7 +51,7 @@ gem 'elasticsearch_record', '~> 1.8'
50
51
 
51
52
  # alternative
52
53
  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'
54
+ gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-0-stable'
54
55
 
55
56
  ```
56
57
 
@@ -616,17 +617,29 @@ end
616
617
  ```
617
618
 
618
619
  ## 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 ...
620
+ Table (index) names are resolved within the current environment, even if the environments share the
621
+ same cluster ...
621
622
 
622
623
  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.
624
+ **Every table statement applies them by default**, so a migration only ever names the table (index)
625
+ _base_ name.
624
626
 
625
627
  **Example:**
626
628
  Production uses a index suffix with '-pro', development uses '-dev' - they share the same cluster, but different indexes.
627
629
 
628
- For the **settings** table:
630
+ ```yml
631
+ production:
632
+ elasticsearch:
633
+ # ...
634
+ table_name_suffix: '-pro'
635
+
636
+ development:
637
+ elasticsearch:
638
+ # ...
639
+ table_name_suffix: '-dev'
640
+ ```
629
641
 
642
+ For the **settings** table / index this results in the following names:
630
643
  * settings-pro
631
644
  * settings-dev
632
645
 
@@ -636,7 +649,8 @@ A single migration can be created to be used within each environment:
636
649
  # Example migration
637
650
  class AddSettings < ActiveRecord::Migration[7.0]
638
651
  def up
639
- create_table _env_table_name("settings"), force: true do |t|
652
+ # creates 'settings-pro' on production & 'settings-dev' on development
653
+ create_table "settings", force: true do |t|
640
654
  t.mapping :created_at, :date
641
655
  t.mapping :key, :integer do |m|
642
656
  m.primary_key = true
@@ -653,11 +667,63 @@ class AddSettings < ActiveRecord::Migration[7.0]
653
667
  end
654
668
 
655
669
  def down
656
- drop_table _env_table_name("settings")
670
+ drop_table "settings"
657
671
  end
658
672
  end
659
673
  ```
660
674
 
675
+ ### opting out with `decorate: false`
676
+
677
+ Provide `decorate: false` to address an index by its **literal** name:
678
+
679
+ ```ruby
680
+ # addresses 'settings-pro' - even from a '-dev' suffixed connection
681
+ drop_table "settings-pro", decorate: false
682
+ ```
683
+
684
+ This is required in two cases:
685
+
686
+ * the name is **already resolved** _(e.g. `Model.table_name`, or a name read back from `#tables`)_
687
+ * the base name itself **starts with the prefix** or **ends with the suffix** - `_env_table_name`
688
+ keeps itself idempotent through a `start_with?` / `end_with?` check and cannot tell such a name
689
+ apart from an already resolved one
690
+
691
+ The flag only ever applies to table (index) names - `alias`, `mapping`, `setting` & `meta` names are
692
+ never touched. Statements taking **two** names _(`clone_table`, `rename_table`, `reindex_table`,
693
+ `restore_table`, `backup_table`, `create_table copy_from:`)_ resolve both.
694
+
695
+ The **schema statements** _(`table_exists?`, `table_schema`, `table_mappings`, `table_settings`,
696
+ `columns`, ...)_ are deliberately **not** decorated - ActiveRecord and the schema dumper call them
697
+ with an already resolved index name.
698
+
699
+ The `_env_table_name`-method itself is still public, so existing migrations keep working - it is now
700
+ redundant, since it resolves the very same name the statement would resolve on its own.
701
+
702
+ ### global kill-switch
703
+
704
+ The default of a **not explicitly provided** `decorate:` argument is resolved from a global flag:
705
+
706
+ ```ruby
707
+ # e.g. in an initializer
708
+ ElasticsearchRecord.decorate_table_names = false
709
+ ```
710
+
711
+ Setting it to `false` restores the former, opt-in behaviour, where the decoration had to be applied
712
+ by hand through `_env_table_name`. A single statement can still opt in or out on its own, so
713
+ `decorate: true` keeps working while the flag is off:
714
+
715
+ ```ruby
716
+ ElasticsearchRecord.decorate_table_names = false
717
+
718
+ drop_table "settings" # => drops 'settings'
719
+ drop_table "settings", decorate: true # => drops 'settings-dev'
720
+ drop_table _env_table_name("settings") # => drops 'settings-dev' (the former syntax)
721
+ ```
722
+
723
+ The schema dumper follows the flag: while the decoration is globally disabled it dumps the **full**
724
+ index name with an explicit `decorate: false`, so a dumped schema stays correct even if the flag is
725
+ flipped back on before it is loaded.
726
+
661
727
  ## Docs
662
728
 
663
729
  [CHANGELOG](docs/CHANGELOG.md)
data/Rakefile CHANGED
File without changes
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [2.0.0] - 2026-08-10
4
+ * [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
+ * [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)_
6
+ * [add] `QueryMethods#select` raises on metadata fields _(`_id`, `_score`, ...)_ - they are always returned anyway
7
+ * [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
8
+ * [add] `CalculationMethods#matrix_stats` raises an `ArgumentError` for less than two columns - the metric quantifies the relationship **between** fields
9
+ * [add] `ResultMethods#meta_only!` to resolve the metadata nodes _(`_id`, `_score`, ...)_ of each hit without transferring the `_source`
10
+ * [add] `Query::COLUMNS_NONE` constant for the `'!'` projection marker - replaces the bare literal in `visit_Selects` & `meta_only!`
11
+ * [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:
12
+ * `#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)_
13
+ * an index **alias** is dumped as a mapping of the elasticsearch 'alias' field type, which makes the `t.alias` branch of the dumper unreachable
14
+ * opposed nil-handling of metas _(deleted)_ and settings _(kept through `:__force__`)_
15
+ * a failed `create` has no `FAILED_BODIES` entry and falls back to an empty body; a multi-column `order` keeps only the last sort
16
+ * `SchemaMigration#count` and `#delete_version` / `#delete_all_versions` cannot be served by elasticsearch
17
+ * `_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
18
+ * `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
19
+ * [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
20
+ * [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`
21
+ * [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
22
+ * [ref] `TableStatements` drops the stale `:rename_table` entry from `define_unsupported_method` - the real implementation overwrote it anyway
23
+ * [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
24
+ * [ref] `ResultMethods#pit_results` resolves through `ElasticsearchRecord::Result` and respects the current projection _(`ids_only` removed in favour of `meta_only!`)_
25
+ * [ref] `TestIndex` spec support to optionally create/drop a second index _(still guarded by the `ALLOWED` name check)_
26
+ * [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
27
+ * [fix] `#backup_table` to build the auto-generated target from the **already resolved** name _(the suffix landed behind the `-snapshot-` part)_
28
+ * [fix] `#_env_table_name` to cast prefix & suffix - an empty `table_name_prefix:` yml entry resolves to `nil` and raised a `TypeError`
29
+ * [fix] `Result#cast_values` to resolve values from the `_source` node, metadata fields from the document level
30
+ * [fix] `visit_Selects` to no longer provide metadata fields to the `_source`-filter _(never matched)_ - projects `_source: false` if only metadata is selected
31
+ * [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
32
+ * [fix] `Querying::ClassMethods#find_by_sql` to reference the provided `sql` for `String` queries _(undefined `query_or_sql` raised a `NameError`)_
33
+ * [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)_
34
+ * [fix] `SchemaStatements#primary_keys` to always return an `Array` _(the `_meta` branch returned a raw String)_
35
+ * [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
36
+ * [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)_
37
+ * [fix] `#restore_table` to no longer touch the backup after a `drop_backup: true` _(it no longer exists at that point)_
38
+ * [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`
39
+ * [fix] `ResultMethods#pit_delete` to no longer `select('_id')` _(rejected by the new metadata guard)_ - resolves the ids through `meta_only!`
40
+
3
41
  ## [1.8.2] - 2024-11-26
4
42
  * [fix] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to raise an exception on `nil` assignments
5
43
  * [fix] `Arel::Visitors::ElasticsearchBase#compile` to always reset temporary assignments _(causes missing assignments after a query-build-exception)_
@@ -18,6 +56,14 @@
18
56
  * [add] new repository branch `rails-7-1-stable` to support different rails version
19
57
  * [ref] gemspec to lock on rails 7.1
20
58
 
59
+ ## [1.7.5] - 2024-11-26 _(no gem release)_
60
+ * [ref] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to raise an exception instead of building an empty `QueryClause`
61
+
62
+ ## [1.7.4] - 2024-11-25 _(no gem release)_
63
+ * [fix] `Arel::Visitors::ElasticsearchBase#compile` to always reset temporary assignments _(causes missing assignments after a query-build-exception)_
64
+ * [fix] `Arel::Nodes::SelectAgg` to not merge nil-values
65
+ * [fix] `ElasticsearchRecord::Relation::QueryMethods#build_query_clause` to prevent nil-Array assignment _(e.g. `[nil]` causes q query exception)_
66
+
21
67
  ## [1.7.3] - 2024-05-07 _(no gem release)_
22
68
  * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
23
69
  * [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
@@ -35,7 +35,7 @@ DESC
35
35
  spec.add_dependency 'activerecord', '~> 7.1.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,29 @@ 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
+ # @return [ElasticsearchRecord::Result]
141
+ def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
142
+ build_result(
143
+ internal_execute(query, name, async: async),
144
+ columns: query.columns
145
+ )
146
+ end
147
+
134
148
  # Executes the query object in the context of this connection and returns the raw result
135
149
  # from the connection adapter.
136
150
  # @param [ElasticsearchRecord::Query] query
137
151
  # @param [String (frozen),nil] name
138
- # @param [Boolean] async (default: false)
152
+ # @param [Boolean] async (default: false) - NOT supported!
139
153
  # @param [Boolean] allow_retry (default: false)
154
+ # @param [Boolean] materialize_transactions (default: false) - NOT supported!
140
155
  # @return [ElasticsearchRecord::Result]
141
- def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: nil)
156
+ def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: false)
142
157
  # validate the query
143
158
  raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
144
159
  raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
@@ -146,21 +161,7 @@ module ActiveRecord
146
161
  # checks for write query - raises an exception if connection is locked to readonly ...
147
162
  check_if_write_query(query)
148
163
 
149
- api(*query.gate, query.query_arguments, name, async: async)
150
- end
151
-
152
- # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
153
- # @param [ElasticsearchRecord::Query] query
154
- # @param [String (frozen),nil] name
155
- # @param [Array] binds - not supported on the top-level and therefore ignored!
156
- # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
157
- # @param [Boolean] async
158
- # @return [ElasticsearchRecord::Result]
159
- def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
160
- build_result(
161
- internal_execute(query, name, async: async),
162
- columns: query.columns
163
- )
164
+ api(query.gate, query.query_arguments, name, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
164
165
  end
165
166
  end
166
167
  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
@@ -45,6 +45,25 @@ module ActiveRecord
45
45
  false
46
46
  end
47
47
 
48
+ # returns true if the BASE name may be dumped, which keeps the dump loadable in another
49
+ # environment (with its own prefix / suffix): every table statement decorates its name on
50
+ # its own, so the base name may only be dumped if that decoration lands on exactly this
51
+ # index again. Otherwise the FULL name is dumped with a +decorate: false+.
52
+ #
53
+ # see @ ActiveRecord::ConnectionAdapters::Elasticsearch::TableStatements#_env_table_name
54
+ def _dumps_base_name?(table, base_name)
55
+ # without a prefix & suffix nothing is ever decorated - the plain name IS the index
56
+ return true unless _has_env_table_names?
57
+
58
+ # a globally disabled decoration would leave the base name untouched while loading
59
+ return false unless ElasticsearchRecord.decorate_table_names
60
+
61
+ # the base name must resolve BACK to the real index. It does not if the base name itself
62
+ # starts with the prefix (or ends with the suffix), and not if the dumper was built with a
63
+ # prefix / suffix that differs from the one of the connection.
64
+ @connection._env_table_name(base_name) == table
65
+ end
66
+
48
67
  def table(table, stream, nested_blocks: false, **)
49
68
  begin
50
69
  self.table_name = table
@@ -57,10 +76,12 @@ module ActiveRecord
57
76
 
58
77
  tbl.print " create_table"
59
78
 
60
- if _has_env_table_names?
61
- tbl.print " _env_table_name(#{remove_prefix_and_suffix(table).inspect})"
79
+ base_name = remove_prefix_and_suffix(table)
80
+
81
+ if _dumps_base_name?(table, base_name)
82
+ tbl.print " #{base_name.inspect}"
62
83
  else
63
- tbl.print " #{remove_prefix_and_suffix(table).inspect}"
84
+ tbl.print " #{table.inspect}, decorate: false"
64
85
  end
65
86
 
66
87
  tbl.print ", force: true do |t|"
@@ -92,13 +92,13 @@ module ActiveRecord
92
92
  :dump_schema_information
93
93
 
94
94
  def assume_migrated_upto_version(version)
95
- version = version.to_i
95
+ version = version.to_i
96
96
  migrated = migration_context.get_all_versions
97
97
  versions = migration_context.migrations.map(&:version)
98
98
 
99
99
  unless migrated.include?(version)
100
- # use a ActiveRecord syntax to create a new version
101
- schema_migration.create(version: version)
100
+ # use Arel syntax to create a new version
101
+ schema_migration.create_version(version)
102
102
  end
103
103
 
104
104
  inserting = (versions - migrated).select { |v| v < version }
@@ -107,8 +107,8 @@ module ActiveRecord
107
107
  raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
108
108
  end
109
109
 
110
- # use a ActiveRecord syntax to create new versions
111
- inserting.each { |iversion| schema_migration.create(version: iversion) }
110
+ # use Arel syntax to create new versions
111
+ inserting.each { |iversion| schema_migration.create_version(iversion) }
112
112
  end
113
113
 
114
114
  true
@@ -119,7 +119,7 @@ module ActiveRecord
119
119
  # @see ActiveRecord::ConnectionAdapters::SchemaStatements#data_sources
120
120
  # @return [Array<String>]
121
121
  def data_sources
122
- api(:indices, :get, { index: :_all, expand_wildcards: [:open, :closed] }, 'SCHEMA').keys
122
+ api('indices.get', { index: :_all, expand_wildcards: [:open, :closed] }, 'SCHEMA').keys
123
123
  end
124
124
 
125
125
  # Returns an array of table names defined in the database.
@@ -134,7 +134,7 @@ module ActiveRecord
134
134
  # @param [String] table_name
135
135
  # @return [Hash]
136
136
  def table_mappings(table_name)
137
- api(:indices, :get_mapping, { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA').dig(table_name, 'mappings')
137
+ api('indices.get_mapping', { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA').dig(table_name, 'mappings')
138
138
  end
139
139
 
140
140
  # returns a hash of all meta data by provided table_name (index).
@@ -150,21 +150,21 @@ module ActiveRecord
150
150
  # @param [Boolean] flat_settings (default: true)
151
151
  # @return [Hash]
152
152
  def table_settings(table_name, flat_settings = true)
153
- api(:indices, :get_settings, { index: table_name, expand_wildcards: [:open, :closed], flat_settings: flat_settings }, 'SCHEMA').dig(table_name, 'settings')
153
+ api('indices.get_settings', { index: table_name, expand_wildcards: [:open, :closed], flat_settings: flat_settings }, 'SCHEMA').dig(table_name, 'settings')
154
154
  end
155
155
 
156
156
  # returns a hash of all aliases by provided table_name (index).
157
157
  # @param [String] table_name
158
158
  # @return [Hash]
159
159
  def table_aliases(table_name)
160
- api(:indices, :get_alias, { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA').dig(table_name, 'aliases')
160
+ api('indices.get_alias', { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA').dig(table_name, 'aliases')
161
161
  end
162
162
 
163
163
  # returns information about number of primaries and replicas, document counts, disk size, ... by provided table_name (index).
164
164
  # @param [String] table_name
165
165
  # @return [Hash]
166
166
  def table_state(table_name)
167
- response = api(:cat, :indices, { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA')
167
+ response = api('cat.indices', { index: table_name, expand_wildcards: [:open, :closed] }, 'SCHEMA')
168
168
 
169
169
  [:health, :status, :name, :uuid, :pri, :rep, :docs_count, :docs_deleted, :store_size, :pri_store_size].zip(
170
170
  response.body.split(' ')
@@ -178,15 +178,15 @@ module ActiveRecord
178
178
  # @return [Hash]
179
179
  def table_schema(table_name, features = [:aliases, :mappings, :settings])
180
180
  if cluster_info[:version] >= '8.5.0'
181
- response = api(:indices, :get, { index: table_name, expand_wildcards: [:open, :closed], features: features, flat_settings: true }, 'SCHEMA')
181
+ response = api('indices.get', { index: table_name, expand_wildcards: [:open, :closed], features: features, flat_settings: true }, 'SCHEMA')
182
182
  else
183
- response = api(:indices, :get, { index: table_name, expand_wildcards: [:open, :closed], flat_settings: true }, 'SCHEMA')
183
+ response = api('indices.get', { index: table_name, expand_wildcards: [:open, :closed], flat_settings: true }, 'SCHEMA')
184
184
  end
185
185
 
186
186
  {
187
187
  settings: response.dig(table_name, 'settings'),
188
188
  mappings: response.dig(table_name, 'mappings'),
189
- aliases: response.dig(table_name, 'aliases')
189
+ aliases: response.dig(table_name, 'aliases')
190
190
  }
191
191
  end
192
192
 
@@ -204,8 +204,8 @@ module ActiveRecord
204
204
  # raise(ActiveRecord::StatementInvalid, "Could not find valid mappings for '#{table_name}'") if mappings.blank? || mappings['properties'].blank?
205
205
 
206
206
  # since the received mappings do not have the "primary" +_id+-column we manually need to add this here
207
- # The BASE_STRUCTURE will also include some meta keys like '_score', '_type', ...
208
- ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::BASE_STRUCTURE + mappings['properties'].map { |key, prop|
207
+ # The METADATA_FIELDS will also include some meta keys like '_score', '_type', ...
208
+ ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::METADATA_FIELDS + mappings['properties'].map { |key, prop|
209
209
  # resolve (nested) fields and properties
210
210
  fields, properties = resolve_fields_and_properties(key, prop, true)
211
211
 
@@ -228,11 +228,11 @@ module ActiveRecord
228
228
  field["name"],
229
229
  field["null_value"],
230
230
  fetch_type_metadata(field["type"]),
231
- meta: field['meta'],
232
- virtual: field['virtual'],
233
- fields: field['fields'],
231
+ meta: field['meta'],
232
+ virtual: field['virtual'],
233
+ fields: field['fields'],
234
234
  properties: field['properties'],
235
- enabled: field['enabled']
235
+ enabled: field['enabled']
236
236
  )
237
237
  end
238
238
 
@@ -250,13 +250,16 @@ module ActiveRecord
250
250
  # The only thing that uniquely identifies a document is the index together with the +_id+.
251
251
  # To support this concept we simulate this through the +_meta+ field (from the index).
252
252
  #
253
- # As a alternative, the primary_key can also be provided through the mappings +meta+ field.
254
- #
255
- # see @ https://www.elastic.co/guide/en/elasticsearch/reference/8.5/mapping-meta-field.html
253
+ # As an alternative, the primary_key can also be provided through the mappings +meta+ field.
254
+ # - see @ https://www.elastic.co/guide/en/elasticsearch/reference/8.5/mapping-meta-field.html
255
+
256
256
  # @see ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#primary_keys
257
257
  # @param [String] table_name
258
+ # @return [Array<String>]
258
259
  def primary_keys(table_name)
259
- table_metas(table_name).dig('primary_key').presence || column_definitions(table_name).
260
+ # IMPORTANT: the +_meta+ node stores the primary_key as a plain String - it must be
261
+ # wrapped, so every branch of this method returns an Array (as the API demands).
262
+ Array.wrap(table_metas(table_name).dig('primary_key')).presence || column_definitions(table_name).
260
263
  select { |f| f['meta'] && f['meta']['primary_key'] == 'true' }.
261
264
  # only take the last found primary key (if no custom primary_key was provided this will return +_id+ )
262
265
  map { |f| f["name"] }[-1..-1]
@@ -270,7 +273,7 @@ module ActiveRecord
270
273
  # @return [Boolean]
271
274
  def data_source_exists?(name)
272
275
  # response returns boolean
273
- api(:indices, :exists?, { index: name, expand_wildcards: [:open, :closed] }, 'SCHEMA')
276
+ api('indices.exists?', { index: name, expand_wildcards: [:open, :closed] }, 'SCHEMA')
274
277
  end
275
278
 
276
279
  # Checks to see if the table +table_name+ exists on the database.
@@ -358,7 +361,9 @@ module ActiveRecord
358
361
  # The query will raise an ActiveRecord::StatementInvalid if the requested limit is above this value.
359
362
  # @return [Integer]
360
363
  def max_result_window(table_name)
361
- table_settings(table_name).dig('index', 'max_result_window').presence || 10000
364
+ # IMPORTANT: the settings API returns every value as a String - without the cast the
365
+ # callers would compare a String against their (Integer) batch sizes.
366
+ table_settings(table_name).dig('index.max_result_window').presence&.to_i || 10_000
362
367
  end
363
368
 
364
369
  # returns true if the cluster option 'id_field_data' is enabled or not configured.
@@ -388,13 +393,13 @@ module ActiveRecord
388
393
  # @return [Hash{Symbol->Unknown}]
389
394
  def cluster_info
390
395
  @cluster_info ||= begin
391
- response = api(:core, :info, {}, 'CLUSTER INFO')
396
+ response = api(:info, {}, 'CLUSTER INFO')
392
397
 
393
398
  {
394
- name: response.dig('name'),
395
- cluster_name: response.dig('cluster_name'),
396
- cluster_uuid: response.dig('cluster_uuid'),
397
- version: Gem::Version.new(response.dig('version', 'number')),
399
+ name: response.dig('name'),
400
+ cluster_name: response.dig('cluster_name'),
401
+ cluster_uuid: response.dig('cluster_uuid'),
402
+ version: Gem::Version.new(response.dig('version', 'number')),
398
403
  lucene_version: response.dig('version', 'lucene_version')
399
404
  }
400
405
  end
@@ -403,14 +408,14 @@ module ActiveRecord
403
408
  # returns a hash of current set, none-default settings in flat
404
409
  # @return [Hash]
405
410
  def cluster_settings
406
- settings = api(:cluster, :get_settings, { flat_settings: true }, 'CLUSTER SETTINGS')
411
+ settings = api('cluster.get_settings', { flat_settings: true }, 'CLUSTER SETTINGS')
407
412
  settings['persistent'].merge(settings['transient'])
408
413
  end
409
414
 
410
415
  # returns the cluster health
411
416
  # @return [Hash]
412
417
  def cluster_health(**options)
413
- api(:cluster, :health, options, 'CLUSTER HEALTH').to_h
418
+ api('cluster.health', options, 'CLUSTER HEALTH').to_h
414
419
  end
415
420
 
416
421
  # transforms provided schema-type to a sql-type
@@ -469,8 +474,8 @@ module ActiveRecord
469
474
  if prop['properties'].present?
470
475
  prop['properties'].each do |nested_key, nested_prop|
471
476
  nested_fields, nested_properties = resolve_fields_and_properties("#{key}.#{nested_key}", nested_prop)
472
- fields |= nested_fields
473
- properties |= nested_properties
477
+ fields |= nested_fields
478
+ properties |= nested_properties
474
479
  end
475
480
  elsif !root # don't add the root property as sub-property
476
481
  properties << { 'name' => key, 'type' => prop['type'] }