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
@@ -4,6 +4,40 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Elasticsearch
6
6
  # extend adapter with table-related statements
7
+ #
8
+ # == Table name decoration
9
+ #
10
+ # Every statement below resolves its provided table name(s) through +#_env_table_name+, which
11
+ # recaps them with the +table_name_prefix+ & +table_name_suffix+ of the connection config.
12
+ # This happens by *default* - so a migration only ever has to name the *base* table (index):
13
+ #
14
+ # create_table 'settings' # => creates 'settings-dev' on a '-dev' suffixed connection
15
+ #
16
+ # Provide +decorate: false+ to address an index by its *literal* name instead. This is
17
+ # required for names that are already resolved and for base names that happen to start with
18
+ # the prefix (or end with the suffix), which +#_env_table_name+ cannot tell apart:
19
+ #
20
+ # drop_table 'settings-pro', decorate: false
21
+ #
22
+ # The default of a NOT explicitly provided +decorate:+ argument is resolved from
23
+ # +ElasticsearchRecord.decorate_table_names+ - setting it to false restores the former,
24
+ # opt-in behaviour, where the decoration had to be applied by hand through +#_env_table_name+.
25
+ # A single statement can still opt in or out on its own.
26
+ #
27
+ # PLEASE NOTE: the decoration only applies to table (index) names - +alias+, +mapping+,
28
+ # +setting+ & +meta+ names are never touched.
29
+ #
30
+ # == Internal tables
31
+ #
32
+ # +schema_migrations+ & +ar_internal_metadata+ carry the migration state of the connection.
33
+ # Only +#truncate_table+ guards them - it raises instead of wiping the state of a whole
34
+ # environment, which in Elasticsearch means a +drop+ & +create+ of the index.
35
+ #
36
+ # Every other statement passes them through on purpose. +#drop_table+ especially MUST stay
37
+ # open: ActiveRecord resets both tables through it
38
+ # (+ActiveRecord::SchemaMigration#drop_table+ & +ActiveRecord::InternalMetadata#drop_table+
39
+ # both call +connection.drop_table(table_name, if_exists: true)+), so a guard there would
40
+ # break that API without an escape hatch.
7
41
  module TableStatements
8
42
  extend ActiveSupport::Concern
9
43
 
@@ -28,42 +62,50 @@ module ActiveRecord
28
62
  # - rename_column
29
63
 
30
64
  define_unsupported_method :create_join_table, :drop_join_table, :create_alter_table,
31
- :change_column_default, :change_column_null, :rename_column, :rename_table
65
+ :change_column_default, :change_column_null, :rename_column
32
66
 
33
67
  # Opens a closed index.
34
68
  # @param [String] table_name
69
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
35
70
  # @return [Boolean] acknowledged status
36
- def open_table(table_name)
71
+ def open_table(table_name, decorate: nil)
72
+ table_name = _decorate_table_name(table_name, decorate: decorate)
73
+
74
+ # IMPORTANT: Clears out internal caches for the *table_name*
37
75
  schema_cache.clear_data_source_cache!(table_name)
38
- api(:indices, :open, { index: table_name }, 'OPEN TABLE').dig('acknowledged')
76
+
77
+ # call the API
78
+ api('indices.open', { index: table_name }, 'OPEN TABLE').dig('acknowledged')
39
79
  end
40
80
 
41
81
  # Opens closed indices.
42
82
  # @param [Array] table_names
83
+ # @param [Boolean] decorate - resolve the table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
43
84
  # @return [Array] acknowledged status for each provided table
44
- def open_tables(*table_names)
45
- table_names -= [schema_migration.table_name, InternalMetadata.table_name]
46
- return if table_names.empty?
47
-
48
- table_names.map { |table_name| open_table(table_name) }
85
+ def open_tables(*table_names, decorate: nil)
86
+ table_names.map { |table_name| open_table(table_name, decorate: decorate) }
49
87
  end
50
88
 
51
89
  # Closes an index.
52
90
  # @param [String] table_name
91
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
53
92
  # @return [Boolean] acknowledged status
54
- def close_table(table_name)
93
+ def close_table(table_name, decorate: nil)
94
+ table_name = _decorate_table_name(table_name, decorate: decorate)
95
+
96
+ # IMPORTANT: Clears out internal caches for the *table_name*
55
97
  schema_cache.clear_data_source_cache!(table_name)
56
- api(:indices, :close, { index: table_name }, 'CLOSE TABLE').dig('acknowledged')
98
+
99
+ # call the API
100
+ api('indices.close', { index: table_name }, 'CLOSE TABLE').dig('acknowledged')
57
101
  end
58
102
 
59
103
  # Closes indices by provided names.
60
104
  # @param [Array] table_names
105
+ # @param [Boolean] decorate - resolve the table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
61
106
  # @return [Array] acknowledged status for each provided table
62
- def close_tables(*table_names)
63
- table_names -= [schema_migration.table_name, InternalMetadata.table_name]
64
- return if table_names.empty?
65
-
66
- table_names.map { |table_name| close_table(table_name) }
107
+ def close_tables(*table_names, decorate: nil)
108
+ table_names.map { |table_name| close_table(table_name, decorate: decorate) }
67
109
  end
68
110
 
69
111
  # refresh an index.
@@ -71,19 +113,21 @@ module ActiveRecord
71
113
  # raises an exception if the index could not be found.
72
114
  #
73
115
  # @param [String] table_name
116
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
74
117
  # @return [Boolean] result state (returns false if refreshing failed)
75
- def refresh_table(table_name)
76
- api(:indices, :refresh, { index: table_name }, 'REFRESH TABLE').dig('_shards', 'failed') == 0
118
+ def refresh_table(table_name, decorate: nil)
119
+ table_name = _decorate_table_name(table_name, decorate: decorate)
120
+
121
+ # call the API
122
+ api('indices.refresh', { index: table_name }, 'REFRESH TABLE').dig('_shards', 'failed') == 0
77
123
  end
78
124
 
79
125
  # refresh indices by provided names.
80
126
  # @param [Array] table_names
127
+ # @param [Boolean] decorate - resolve the table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
81
128
  # @return [Array] result state (returns false if refreshing failed)
82
- def refresh_tables(*table_names)
83
- table_names -= [schema_migration.table_name, InternalMetadata.table_name]
84
- return if table_names.empty?
85
-
86
- table_names.map { |table_name| refresh_table(table_name) }
129
+ def refresh_tables(*table_names, decorate: nil)
130
+ table_names.map { |table_name| refresh_table(table_name, decorate: decorate) }
87
131
  end
88
132
 
89
133
  # truncates index by provided name.
@@ -91,60 +135,90 @@ module ActiveRecord
91
135
  # - so we have to store the current index' schema
92
136
  # - drop the index
93
137
  # - and create it again
138
+ #
139
+ # PLEASE NOTE: an AR-internal index (+schema_migrations+ / +ar_internal_metadata+) raises
140
+ # instead - a truncate would drop the migration state of the whole environment. The check
141
+ # runs on the ALREADY resolved name and +#_internal_table_names+ holds both forms, so
142
+ # neither a base nor a resolved name slips through.
143
+ #
94
144
  # @param [String] table_name
145
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
146
+ # @raise [ArgumentError] if the resolved name is an AR-internal index
95
147
  # @return [Boolean] acknowledged status
96
- def truncate_table(table_name)
148
+ def truncate_table(table_name, decorate: nil)
149
+ table_name = _decorate_table_name(table_name, decorate: decorate)
150
+
151
+ # ensure the provided *table_name* is NOT an internal table_name
152
+ raise ArgumentError, "Cannot truncate internal table '#{table_name}'!" if _internal_table_names.include?(table_name)
153
+
97
154
  # force: automatically drops an existing index
98
- create_table(table_name, force: true, **table_schema(table_name))
155
+ create_table(table_name, force: true, decorate: false, **table_schema(table_name))
99
156
  end
100
157
 
101
158
  alias :truncate :truncate_table
102
159
 
103
160
  # truncate indices by provided names.
161
+ # PLEASE NOTE: a single AR-internal index raises through +#truncate_table+ and aborts the
162
+ # whole call - the tables before it are already truncated at that point.
104
163
  # @param [Array] table_names
164
+ # @param [Boolean] decorate - resolve the table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
165
+ # @raise [ArgumentError] if one of the resolved names is an AR-internal index
105
166
  # @return [Array] acknowledged status for each provided table
106
- def truncate_tables(*table_names)
107
- table_names -= [schema_migration.table_name, InternalMetadata.table_name]
108
- return if table_names.empty?
109
-
110
- table_names.map { |table_name| truncate_table(table_name) }
167
+ def truncate_tables(*table_names, decorate: nil)
168
+ table_names.map { |table_name| truncate_table(table_name, decorate: decorate) }
111
169
  end
112
170
 
113
171
  # drops an index
114
172
  # [<tt>:if_exists</tt>]
115
173
  # Set to +true+ to only drop the table if it exists.
116
174
  # Defaults to false.
175
+ #
176
+ # PLEASE NOTE: unlike +#truncate_table+ this does NOT guard the AR-internal indices -
177
+ # ActiveRecord resets them through exactly this statement
178
+ # (+ActiveRecord::SchemaMigration#drop_table+ & +ActiveRecord::InternalMetadata#drop_table+
179
+ # both call +connection.drop_table(table_name, if_exists: true)+).
180
+ #
117
181
  # @param [String] table_name
118
182
  # @param [Boolean] if_exists
183
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
119
184
  # @return [Boolean] acknowledged status
120
- def drop_table(table_name, if_exists: false, **)
185
+ def drop_table(table_name, if_exists: false, decorate: nil, **)
186
+ table_name = _decorate_table_name(table_name, decorate: decorate)
187
+
188
+ # IMPORTANT: Clears out internal caches for the *table_name*
121
189
  schema_cache.clear_data_source_cache!(table_name)
122
- api(:indices, :delete, { index: table_name, ignore: (if_exists ? 404 : nil) }, 'DROP TABLE').dig('acknowledged')
190
+
191
+ # call the API
192
+ api('indices.delete', { index: table_name, ignore: (if_exists ? 404 : nil) }, 'DROP TABLE').dig('acknowledged')
123
193
  end
124
194
 
125
195
  # blocks access to the provided table (index) and +block+ name.
126
196
  # @param [String] table_name
127
197
  # @param [Symbol] block_name The block to add (one of :read, :write, :read_only or :metadata)
198
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
128
199
  # @return [Boolean] acknowledged status
129
- def block_table(table_name, block_name = :write)
130
- api(:indices, :add_block, { index: table_name, block: block_name }, "BLOCK #{block_name.to_s.upcase} TABLE").dig('acknowledged')
200
+ def block_table(table_name, block_name = :write, decorate: nil)
201
+ table_name = _decorate_table_name(table_name, decorate: decorate)
202
+
203
+ api('indices.add_block', { index: table_name, block: block_name }, "BLOCK #{block_name.to_s.upcase} TABLE").dig('acknowledged')
131
204
  end
132
205
 
133
206
  # unblocks access to the provided table (index) and +block+ name.
134
207
  # provide a nil-value to unblock all blocks, otherwise provide the blocked name.
135
208
  # @param [String] table_name
136
209
  # @param [Symbol] block_name The block to add (one of :read, :write, :read_only or :metadata)
210
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
137
211
  # @return [Boolean] acknowledged status
138
- def unblock_table(table_name, block_name = nil)
212
+ def unblock_table(table_name, block_name = nil, decorate: nil)
139
213
  if block_name.nil?
140
- change_table(table_name) do |t|
214
+ change_table(table_name, decorate: decorate) do |t|
141
215
  t.change_setting('index.blocks.read', nil)
142
216
  t.change_setting('index.blocks.write', nil)
143
217
  t.change_setting('index.blocks.read_only', nil)
144
218
  t.change_setting('index.blocks.metadata', nil)
145
219
  end
146
220
  else
147
- change_setting(table_name, "index.blocks.#{block_name}", nil)
221
+ change_setting(table_name, "index.blocks.#{block_name}", nil, decorate: decorate)
148
222
  end
149
223
  end
150
224
 
@@ -152,9 +226,13 @@ module ActiveRecord
152
226
  # During cloning, the table will be automatically 'write'-blocked.
153
227
  # @param [String] table_name
154
228
  # @param [String] target_name
229
+ # @param [Boolean] decorate - resolve both table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
155
230
  # @param [Hash] options
156
231
  # @return [Boolean] acknowledged status
157
- def clone_table(table_name, target_name, **options)
232
+ def clone_table(table_name, target_name, decorate: nil, **options)
233
+ table_name = _decorate_table_name(table_name, decorate: decorate)
234
+ target_name = _decorate_table_name(target_name, decorate: decorate)
235
+
158
236
  # create new definition
159
237
  definition = clone_table_definition(table_name, target_name, **extract_table_options!(options))
160
238
 
@@ -179,13 +257,20 @@ module ActiveRecord
179
257
  # @param [String] table_name
180
258
  # @param [String] to - target_name
181
259
  # @param [Boolean] close - closes backup after creation (default: true)
260
+ # @param [Boolean] decorate - resolve both table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
182
261
  # @return [String] backup_name
183
- def backup_table(table_name, to: nil, close: true)
184
- to ||= "#{table_name}-snapshot-#{Time.now.strftime('%s%3N')}"
262
+ def backup_table(table_name, to: nil, close: true, decorate: nil)
263
+ table_name = _decorate_table_name(table_name, decorate: decorate)
264
+
265
+ # IMPORTANT: the auto-generated name is built from the ALREADY resolved +table_name+, so
266
+ # it stays within the current environment without being decorated a second time (which
267
+ # would append the suffix BEHIND the '-snapshot-' part).
268
+ to = to.nil? ? "#{table_name}-snapshot-#{Time.now.strftime('%s%3N')}" : _decorate_table_name(to, decorate: decorate)
269
+
185
270
  raise ArgumentError, "unable to backup '#{table_name}' to already existing target '#{to}'!" if table_exists?(to)
186
271
 
187
- clone_table(table_name, to)
188
- close_table(to) if close
272
+ clone_table(table_name, to, decorate: false)
273
+ close_table(to, decorate: false) if close
189
274
 
190
275
  to
191
276
  end
@@ -194,27 +279,43 @@ module ActiveRecord
194
279
  # The +table_name+ will be dropped, if exists.
195
280
  # The +from+ will persist, if not provided +drop_backup:true+.
196
281
  #
282
+ # IMPORTANT: both strategies restore through a +clone+, which inherits the settings of
283
+ # its source - including the 'write'-block that is required to clone at all.
284
+ # The restored table is therefore *open* but *read-only* until that block is released,
285
+ # which is what the +unblock+ flag is for.
286
+ # (there is no +open+ flag: a clone is always created open - even from a closed source)
287
+ # see @ ActiveRecord::ConnectionAdapters::Elasticsearch::CloneTableDefinition#_before_exec
288
+ #
197
289
  # @example
198
290
  # restore_table('screenshots', from: 'screenshots-backup-v1')
199
291
  #
292
+ # @example
293
+ # # keep the restored table read-only
294
+ # restore_table('screenshots', from: 'screenshots-backup-v1', unblock: false)
295
+ #
200
296
  # @param [String] table_name
201
297
  # @param [String] from
202
- # @param [String (frozen)] timeout - renaming timout (default: '30s')
203
- # @param [Boolean] open - opens restored backup after creation (default: true)
204
- # @return [Boolean] acknowledged status
205
- def restore_table(table_name, from:, timeout: nil, open: true, drop_backup: false)
298
+ # @param [String (frozen)] timeout - renaming timout (default: '1m')
299
+ # @param [Boolean] unblock - releases the inherited 'write'-block on the restored table (default: true)
300
+ # @param [Boolean] drop_backup - renames instead of clones, which removes the +from+ (default: false)
301
+ # @param [Boolean] decorate - resolve both table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
302
+ # @return [nil] - every failing step raises instead
303
+ def restore_table(table_name, from:, timeout: '1m', unblock: true, drop_backup: false, decorate: nil)
304
+ table_name = _decorate_table_name(table_name, decorate: decorate)
305
+ from = _decorate_table_name(from, decorate: decorate)
306
+
206
307
  raise ArgumentError, "unable to restore from missing target '#{from}'!" unless table_exists?(from)
207
- drop_table(table_name, if_exists: true)
308
+ drop_table(table_name, if_exists: true, decorate: false)
208
309
 
209
310
  # choose best strategy
210
311
  if drop_backup
211
- rename_table(from, table_name, timeout: timeout)
312
+ rename_table(from, table_name, timeout: timeout, decorate: false)
212
313
  else
213
- clone_table(from, table_name)
314
+ clone_table(from, table_name, decorate: false)
214
315
  end
215
316
 
216
- # open, if provided
217
- open_table(from) if open
317
+ # release the inherited 'write'-block, if provided
318
+ unblock_table(table_name, :write, decorate: false) if unblock
218
319
  end
219
320
 
220
321
  # renames a table (index) by executing multiple steps:
@@ -225,35 +326,47 @@ module ActiveRecord
225
326
  #
226
327
  # @param [String] table_name
227
328
  # @param [String] target_name
228
- # @param [String (frozen)] timeout (default: '30s')
329
+ # @param [String (frozen)] timeout (default: '1m')
330
+ # @param [Boolean] decorate - resolve both table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
229
331
  # @param [Hash] options - additional 'clone' options (like settings, alias, ...)
230
- def rename_table(table_name, target_name, timeout: nil, **options)
332
+ def rename_table(table_name, target_name, timeout: '1m', decorate: nil, **options)
333
+ # IMPORTANT: both names must be resolved HERE and not only forwarded to the statements
334
+ # below - the +schema_cache+ and the +cluster_health+ call in between address the index
335
+ # directly and would otherwise miss the decorated one
336
+ table_name = _decorate_table_name(table_name, decorate: decorate)
337
+ target_name = _decorate_table_name(target_name, decorate: decorate)
338
+
339
+ # IMPORTANT: Clears out internal caches
231
340
  schema_cache.clear_data_source_cache!(table_name)
232
341
 
233
- clone_table(table_name, target_name, **options)
234
- cluster_health(index: target_name, wait_for_status: 'green', timeout: timeout.presence || '30s')
235
- drop_table(table_name)
342
+ clone_table(table_name, target_name, decorate: false, **options)
343
+ cluster_health(index: target_name, wait_for_status: 'green', timeout: timeout)
344
+ drop_table(table_name, decorate: false)
236
345
  end
237
346
 
238
347
  # creates a new table (index).
239
348
  # [<tt>:force</tt>]
240
- # Set to +true+ to drop an existing table
349
+ # Set to +true+ to drop an existing index
241
350
  # Defaults to false.
242
351
  # [<tt>:copy_from</tt>]
243
352
  # Set to an existing index, to copy it's schema.
244
353
  # [<tt>:if_not_exists</tt>]
245
- # Set to +true+ to skip creation if table already exists.
354
+ # Set to +true+ to skip creation if index already exists.
246
355
  # Defaults to false.
247
356
  # @param [String] table_name
248
- # @param [Boolean] force - force a drop on the existing table (default: false)
249
- # @param [nil, String] copy_from - copy schema from existing table
357
+ # @param [Boolean] force - force a drop on the existing index (default: false)
358
+ # @param [nil, String] copy_from - copy schema from existing index
359
+ # @param [Boolean] if_not_exists - skip the creation if the index already exists (default: false)
360
+ # @param [Boolean] decorate - resolve the table name (and +copy_from+) with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
250
361
  # @param [Hash] options
251
362
  # @return [Boolean] acknowledged status
252
- def create_table(table_name, force: false, copy_from: nil, if_not_exists: false, **options)
363
+ def create_table(table_name, force: false, copy_from: nil, if_not_exists: false, decorate: nil, **options)
364
+ table_name = _decorate_table_name(table_name, decorate: decorate)
365
+
253
366
  return if if_not_exists && table_exists?(table_name)
254
367
 
255
368
  # copy schema from existing table
256
- options.merge!(table_schema(copy_from)) if copy_from
369
+ options.merge!(table_schema(_decorate_table_name(copy_from, decorate: decorate))) if copy_from
257
370
 
258
371
  # create new definition
259
372
  definition = create_table_definition(table_name, **extract_table_options!(options))
@@ -267,9 +380,10 @@ module ActiveRecord
267
380
 
268
381
  # force drop existing table
269
382
  if force
270
- drop_table(table_name, if_exists: true)
383
+ drop_table(table_name, if_exists: true, decorate: false)
271
384
  else
272
- schema_cache.clear_data_source_cache!(table_name.to_s)
385
+ # IMPORTANT: Clears out internal caches
386
+ schema_cache.clear_data_source_cache!(table_name)
273
387
  end
274
388
 
275
389
  # execute definition query(ies)
@@ -283,12 +397,20 @@ module ActiveRecord
283
397
  # t.mapping :name, :string
284
398
  # # Other column alterations here
285
399
  # end
286
- def change_table(table_name, if_exists: false, recreate: false, **options, &block)
400
+ #
401
+ # @param [String] table_name
402
+ # @param [Boolean] if_exists - skip if the index does not exist (default: false)
403
+ # @param [Boolean] recreate - recreate the index from a copy of the current one (default: false)
404
+ # @param [Boolean] decorate - resolve the table name with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
405
+ # @param [Hash] options
406
+ def change_table(table_name, if_exists: false, recreate: false, decorate: nil, **options, &block)
407
+ table_name = _decorate_table_name(table_name, decorate: decorate)
408
+
287
409
  return if if_exists && !table_exists?(table_name)
288
410
 
289
411
  # check 'recreate' flag.
290
412
  # If true, a 'create_table' with copy of the current will be executed
291
- return create_table(table_name, force: true, copy_from: table_name, **options, &block) if recreate
413
+ return create_table(table_name, force: true, copy_from: table_name, decorate: false, **options, &block) if recreate
292
414
 
293
415
  # build new update definition
294
416
  definition = update_table_definition(table_name, self, **options)
@@ -307,13 +429,21 @@ module ActiveRecord
307
429
  # Copies documents from a source to a destination.
308
430
  # @param [String] table_name
309
431
  # @param [String] target_name
432
+ # @param [Boolean] decorate - resolve both table names with the configured prefix & suffix (default: +ElasticsearchRecord.decorate_table_names+)
310
433
  # @param [Hash] options
311
434
  # @return [Hash] reindex stats
312
- def reindex_table(table_name, target_name, **options)
313
- api(:core, :reindex, { body: { source: { index: table_name }, dest: { index: target_name } } }.merge(options), 'REINDEX TABLE')
435
+ def reindex_table(table_name, target_name, decorate: nil, **options)
436
+ table_name = _decorate_table_name(table_name, decorate: decorate)
437
+ target_name = _decorate_table_name(target_name, decorate: decorate)
438
+
439
+ api(:reindex, { body: { source: { index: table_name }, dest: { index: target_name } } }.merge(options), 'REINDEX TABLE')
314
440
  end
315
441
 
316
442
  # -- mapping -------------------------------------------------------------------------------------------------
443
+ #
444
+ # PLEASE NOTE: every statement below reaches +#change_table+ through
445
+ # +#_exec_change_table_with+, which forwards a provided +decorate:+ flag - only the TABLE
446
+ # name is ever decorated, never the mapping / meta / setting / alias name.
317
447
 
318
448
  def add_mapping(table_name, name, type, **options, &block)
319
449
  _exec_change_table_with(:add_mapping, table_name, name, type, **options, &block)
@@ -381,24 +511,73 @@ module ActiveRecord
381
511
  # recaps a provided +table_name+ with optionally configured +table_name_prefix+ & +table_name_suffix+.
382
512
  # This depends on the connection config of the current environment.
383
513
  #
514
+ # PLEASE NOTE: the method is idempotent through a +start_with?+ / +end_with?+ check, so it
515
+ # can safely be called on an already resolved name. That check cannot tell a resolved name
516
+ # apart from a base name that legitimately starts with the prefix (or ends with the
517
+ # suffix) - use +decorate: false+ on the statement to address such an index literally.
518
+ #
384
519
  # @param [String] table_name
385
520
  # @return [String]
386
521
  def _env_table_name(table_name)
522
+ # ensure *table_name* is a string
387
523
  table_name = table_name.to_s
388
524
 
525
+ # ensure *prefix* and *suffix* are strings
526
+ prefix = table_name_prefix.to_s
527
+ suffix = table_name_suffix.to_s
528
+
389
529
  # HINT: +"" creates a new +unfrozen+ string!
390
530
  name = +""
391
- name << table_name_prefix unless table_name.start_with?(table_name_prefix)
531
+ name << prefix unless table_name.start_with?(prefix)
392
532
  name << table_name
393
- name << table_name_suffix unless table_name.end_with?(table_name_suffix)
533
+ name << suffix unless table_name.end_with?(suffix)
394
534
 
395
535
  name
396
536
  end
397
537
 
398
538
  private
399
539
 
400
- def _exec_change_table_with(method, table_name, *args, recreate: false, **kwargs, &block)
401
- change_table(table_name, recreate: recreate) do |t|
540
+ # resolves the provided +table_name+ through +#_env_table_name+, unless the decoration was
541
+ # disabled - either for this call (+decorate: false+) or globally.
542
+ # @param [String, Symbol] table_name
543
+ # @param [nil, Boolean] decorate - a nil-value resolves the global default
544
+ # @return [String]
545
+ def _decorate_table_name(table_name, decorate:)
546
+ # only a NOT explicitly provided flag falls back to the global default, so a single
547
+ # statement can always opt in or out on its own
548
+ decorate = ElasticsearchRecord.decorate_table_names if decorate.nil?
549
+
550
+ decorate ? _env_table_name(table_name) : table_name.to_s
551
+ end
552
+
553
+ # returns the AR-internal indices, which carry the migration state of the connection.
554
+ # Both the resolved AND the decorated name are returned, so the check also holds if those
555
+ # internal table names do not carry the prefix & suffix of the connection
556
+ # (+ActiveRecord::InternalMetadata+ resolves through +ActiveRecord::Base+, so it may well
557
+ # be undecorated while +ElasticsearchRecord::SchemaMigration+ is not).
558
+ #
559
+ # Used by +#truncate_table+ only - see the 'Internal tables' section of this module for
560
+ # why +#drop_table+ deliberately passes them through.
561
+ #
562
+ # @return [Array<String>]
563
+ def _internal_table_names
564
+ names = [schema_migration.table_name, internal_metadata.table_name]
565
+
566
+ names | names.map { |name| _env_table_name(name) }
567
+ end
568
+
569
+ # Executes a given table operation method within the context of a `change_table` block.
570
+ #
571
+ # This method wraps the provided `method` call in a `change_table` transaction. It allows performing
572
+ # modifications such as adding, changing, or removing mappings, settings, or metadata on the specified table.
573
+ #
574
+ # @param [Symbol] method - The operation to perform (e.g., :add_mapping, :remove_mapping).
575
+ # @param [String] table_name - The name of the table to modify.
576
+ # @param [Array<Object>] args - Additional arguments to pass to the operation method.
577
+ # @param [Boolean] recreate - Whether to recreate the table before applying changes (default: false).
578
+ # @param [Boolean, nil] decorate - Whether to resolve the table name with a configured prefix and suffix (default: nil
579
+ def _exec_change_table_with(method, table_name, *args, recreate: false, decorate: nil, **kwargs, &block)
580
+ change_table(table_name, recreate: recreate, decorate: decorate) do |t|
402
581
  t.send(method, *args, **kwargs, &block)
403
582
  end
404
583
  end
@@ -406,4 +585,4 @@ module ActiveRecord
406
585
  end
407
586
  end
408
587
  end
409
- end
588
+ end