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
@@ -7,8 +7,8 @@ module ElasticsearchRecord
7
7
  include Enumerable
8
8
 
9
9
  # creates an empty response
10
- # @return [ElasticsearchRecord::Result (frozen)]
11
- def self.empty(async: false) # :nodoc:
10
+ # @return [ElasticsearchRecord::Result (frozen), ActiveRecord::FutureResult::Complete (frozen)]
11
+ def self.empty(async: false)
12
12
  if async
13
13
  EMPTY_ASYNC
14
14
  else
@@ -23,7 +23,7 @@ module ElasticsearchRecord
23
23
  # @param [Array] columns
24
24
  # @param [Hash] column_types
25
25
  def initialize(response, columns = [], column_types = {})
26
- # contains either the response or creates a empty hash (if nil)
26
+ # contains either the response or creates an empty hash (if nil)
27
27
  @response = response.presence || {}
28
28
 
29
29
  # used to build computed_results
@@ -39,37 +39,69 @@ module ElasticsearchRecord
39
39
  response['took']
40
40
  end
41
41
 
42
+ # returns the response result string
43
+ # @return [String]
44
+ def result
45
+ response['result'] || ''
46
+ end
47
+
42
48
  # returns the response total value.
43
49
  # either chops the +total+ value directly from response, from hits or aggregations.
44
50
  # @return [Integer]
45
51
  def total
46
- # chop total only
47
- @total ||= _chop_total
52
+ # chop total from response and not from the generated data
53
+ @total ||= _total
48
54
  end
49
55
 
50
- # returns the response RAW hits hash.
51
- # PLEASE NOTE: Does not return the nested hits (+response['hits']['hits']+) array!
52
- # @return [ActiveSupport::HashWithIndifferentAccess, Hash]
53
- def hits
54
- response.key?('hits') ? response['hits'].with_indifferent_access : {}
55
- end
56
-
57
- # Returns the RAW +_source+ data from each hit - aka. +rows+.
56
+ # Returns the RAW +_source+ data from each hit.
58
57
  # PLEASE NOTE: The array will only contain the RAW data from each +_source+ (meta info like '_id' or '_score' are not included)
59
58
  # @return [Array]
60
59
  def results
61
- return [] unless response['hits']
60
+ # IMPORTANT: check against missing hits without any '_source' node.
61
+ # This happens if the Elasticsearch query has the +_source:false+ flag!
62
+ if response['hits']
63
+ response['hits']['hits'].map { |doc| doc['_source'] || {} }
64
+ elsif _tabular?
65
+ # a tabular (+SQL+ / +ES|QL+) response has no '_source' node - the row values are the data
66
+ _results_from_tabular
67
+ else
68
+ []
69
+ end
70
+ end
62
71
 
63
- response['hits']['hits'].map { |result| result['_source'] }
72
+ # returns an array of all rows.
73
+ # => All result values, depending on the provided columns.
74
+ # The +rows+ is used by the ActiveRecord ConnectionAdapters and must not be removed!
75
+ # @return [Array]
76
+ def rows
77
+ # a tabular (+SQL+ / +ES|QL+) response is ALREADY positional - and it is positional to the
78
+ # response's own columns, not to the (requested) +columns+ of the query.
79
+ return _tabular_values if _tabular?
80
+
81
+ # IMPORTANT: without provided +columns+ we cannot build positional rows - mapping over an
82
+ # empty +columns+ array would return an empty array per hit and silently lose all data.
83
+ # In this case we fall back to the raw +_source+ values.
84
+ return results.map(&:values) if columns.blank?
85
+
86
+ results.map { |doc|
87
+ columns.map { |column|
88
+ doc[column]
89
+ }
90
+ }
64
91
  end
65
92
 
66
- # The +rows+ alias is used by the ActiveRecord ConnectionAdapters and must not be removed!
67
- alias_method :rows, :results
93
+ # returns the response RAW hits hash.
94
+ # PLEASE NOTE: Does not return the nested hits (+response['hits']['hits']+) array!
95
+ #
96
+ # @return [ActiveSupport::HashWithIndifferentAccess, Hash]
97
+ def hits
98
+ response['hits']&.with_indifferent_access || {}
99
+ end
68
100
 
69
101
  # returns the response RAW aggregations hash.
70
102
  # @return [ActiveSupport::HashWithIndifferentAccess, Hash]
71
103
  def aggregations
72
- response.key?('aggregations') ? response['aggregations'].with_indifferent_access : {}
104
+ response['aggregations']&.with_indifferent_access || {}
73
105
  end
74
106
 
75
107
  # returns the (nested) bucket values (and aggregated values) from the response aggregations.
@@ -84,13 +116,14 @@ module ElasticsearchRecord
84
116
  end
85
117
 
86
118
  # Returns true if this result set includes the column named +name+.
87
- # used by ActiveRecord
119
+ # used by +ActiveRecord+
88
120
  def includes_column?(name)
89
121
  @columns&.include?(name)
90
122
  end
91
123
 
92
124
  # Returns the number of elements in the response array.
93
- # Either uses the +hits+ length or the +responses+ length _(msearch)_.
125
+ # Either uses the +hits+ length, the +responses+ length _(msearch)_ or the length of the
126
+ # tabular value rows _(SQL / ES|QL)_.
94
127
  # @return [Integer]
95
128
  def length
96
129
  if response.key?('hits')
@@ -98,6 +131,9 @@ module ElasticsearchRecord
98
131
  elsif response.key?('responses')
99
132
  # used by +msearch+
100
133
  response['responses'].length
134
+ elsif _tabular?
135
+ # used by +sql+ & +esql+
136
+ _tabular_values.length
101
137
  else
102
138
  0
103
139
  end
@@ -131,51 +167,56 @@ module ElasticsearchRecord
131
167
  computed_results[idx]
132
168
  end
133
169
 
134
- # Returns the last record from the rows collection.
170
+ # Returns the last record(s) from the *computed_results* collection.
135
171
  def last(n = nil)
136
172
  n ? computed_results.last(n) : computed_results.last
137
173
  end
138
174
 
139
- # returns the response result string
140
- # @return [String]
141
- def result
142
- response['result'] || ''
143
- end
144
-
145
175
  # used by ActiveRecord
146
176
  def cancel # :nodoc:
147
177
  self
148
178
  end
149
179
 
150
- # used by ActiveRecord
180
+ # used by ActiveRecord for "pluck"
151
181
  def cast_values(type_overrides = {})
152
- # :nodoc:
182
+ # fast escape, if no hits are available
183
+ return [] unless response['hits']
184
+
185
+ # HINT: This is separated to avoid allocating a (nested) array per row
153
186
  if columns.one?
154
- # Separated to avoid allocating an array per row
187
+ # resolve the column key
155
188
  key = columns.first
156
189
 
157
- type = if type_overrides.is_a?(Array)
158
- type_overrides.first
159
- else
160
- column_type(columns.first, type_overrides)
161
- end
190
+ # resolve type from overrides or +#column_type+ method
191
+ type = type_overrides.is_a?(Array) ? type_overrides.first : column_type(key, type_overrides)
162
192
 
163
- computed_results.map do |result|
164
- type.deserialize(result[key])
193
+ # EDGE-case for metadata fields
194
+ if ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys.include?(key)
195
+ # directly read from doc
196
+ response['hits']['hits'].map { |doc| type.deserialize(doc[key]) }
197
+ else
198
+ results.map do |result|
199
+ type.deserialize(result[key])
200
+ end
165
201
  end
166
202
  else
167
- types = if type_overrides.is_a?(Array)
168
- type_overrides
169
- else
170
- columns.map { |name| column_type(name, type_overrides) }
171
- end
203
+ # resolve types from overrides or +#column_type+ method
204
+ types = type_overrides.is_a?(Array) ? type_overrides : columns.map { |name| column_type(name, type_overrides) }
172
205
 
173
206
  size = types.size
174
207
 
175
- computed_results.map do |result|
208
+ # EDGE-case for metadata fields - they have to be resolved from the doc, so we merge them into the +_source+
209
+ rows = if (ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys & columns).any?
210
+ response['hits']['hits'].map { |doc|
211
+ (doc['_source'] || {}).merge(doc.slice(*ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys))
212
+ }
213
+ else
214
+ response['hits']['hits'].map { |doc| doc['_source'] || {} }
215
+ end
216
+
217
+ rows.map do |result|
176
218
  Array.new(size) { |i|
177
- key = columns[i]
178
- types[i].deserialize(result[key])
219
+ types[i].deserialize(result[columns[i]])
179
220
  }
180
221
  end
181
222
  end
@@ -185,45 +226,84 @@ module ElasticsearchRecord
185
226
 
186
227
  # used by ActiveRecord
187
228
  def column_type(name, type_overrides = {})
188
- type_overrides.fetch(name, Type.default_value)
229
+ type_overrides.fetch(name, ::ActiveRecord::Type.default_value)
189
230
  end
190
231
 
191
- # chops total value from response
232
+ # resolves total value from response
192
233
  # @return [Integer]
193
- def _chop_total
234
+ def _total
194
235
  return self.response['total'] if self.response.key?('total')
195
236
  return self.response['hits']['total']['value'] if self.response.key?('hits')
196
237
  return self.response['aggregations'].count if self.response.key?('aggregations')
197
- return self.response['_shards']['total'] if self.response.key?('_shards')
238
+ # a tabular response has no total - the transferred rows are all there is
239
+ return _tabular_values.length if _tabular?
198
240
 
199
241
  0
200
242
  end
201
243
 
244
+ # true if the response is TABULAR - which is what the +sql+ & +esql+ APIs return instead of a
245
+ # (nested) 'hits' node: a flat 'columns' definition and positional value rows.
246
+ # @return [Boolean]
247
+ def _tabular?
248
+ response.key?('columns') && (response.key?('rows') || response.key?('values'))
249
+ end
250
+
251
+ # returns the column names of a tabular response.
252
+ # Both APIs describe their columns as a {'name' =>, 'type' =>} pair.
253
+ # @return [Array<String>]
254
+ def _tabular_columns
255
+ response['columns'].map { |column| column['name'] }
256
+ end
257
+
258
+ # returns the positional value rows of a tabular response.
259
+ # PLEASE NOTE: the +sql+ API names this node 'rows', the +esql+ API names it 'values'.
260
+ # @return [Array<Array>]
261
+ def _tabular_values
262
+ response['rows'] || response['values']
263
+ end
264
+
265
+ # used for +sql+ & +esql+ results
266
+ # IMPORTANT: the rows are positional to the RESPONSE columns - not to the (requested) +columns+
267
+ # of the query. A projecting query (e.g. 'SELECT name FROM ...') returns fewer columns, so
268
+ # zipping against the query's columns would shift every value.
269
+ # @return [Array]
270
+ def _results_from_tabular
271
+ # We freeze the strings to prevent them getting duped when
272
+ # used as keys in ActiveRecord::Base's @attributes hash.
273
+ keys = _tabular_columns.map(&:-@)
274
+
275
+ _tabular_values.map { |row| keys.zip(row).to_h }
276
+ end
277
+
202
278
  # used for +msearch+ results
203
279
  # @return [Array]
204
- def _results_for_responses
280
+ def _results_from_responses
205
281
  response['responses'].map { |response| self.class.new(response, self.columns, self.column_types) }
206
282
  end
207
283
 
208
284
  # used for +search+ results
209
285
  # @return [Array]
210
- def _results_for_hits
286
+ def _results_from_hits
211
287
  # PLEASE NOTE: the 'hits' response has multiple nodes: BASE nodes & the +_source+ node.
212
- # The real data is within the source node, but we also want the BASE nodes for possible score & type check
213
- base_fields = ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.base_structure_keys
288
+ # The real data is within the source node, but we also want the METADATA nodes for possible score & type check
289
+ metadata_fields = ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.metadata_keys
214
290
 
215
291
  # check for provided columns
216
292
  if @columns.present?
217
293
  # We freeze the strings to prevent them getting duped when
218
294
  # used as keys in ActiveRecord::Base's @attributes hash.
219
- # ALSO IMPORTANT: remove base_fields from possible provided columns
220
- columns = @columns ? (@columns - base_fields).map(&:-@) : []
295
+ # IMPORTANT: remove *metadata_fields* from possible provided columns ( &:-@ -> freeze strings )
296
+ columns = (@columns - metadata_fields).map(&:-@)
221
297
 
222
298
  # this is the hashed result array
223
299
  response['hits']['hits'].map { |doc|
224
- result = doc.slice(*base_fields)
300
+ # allocate new result hash with all known metadata keys ('_id', ...)
301
+ result = doc.slice(*metadata_fields)
302
+
303
+ # iterate through each requested column
225
304
  columns.each do |column|
226
- result[column] = doc['_source'][column]
305
+ # in case no source was provided, it prevents an exception
306
+ result[column] = doc.dig('_source', column)
227
307
  end
228
308
 
229
309
  result
@@ -234,7 +314,8 @@ module ElasticsearchRecord
234
314
 
235
315
  # this is the hashed result array
236
316
  response['hits']['hits'].map { |doc|
237
- doc.slice(*base_fields).merge(doc['_source'])
317
+ # in case no source was provided, it prevents an exception
318
+ doc.slice(*metadata_fields).merge!(doc['_source'] || {})
238
319
  }
239
320
  end
240
321
  end
@@ -270,10 +351,13 @@ module ElasticsearchRecord
270
351
  # @return [Array]
271
352
  def computed_results
272
353
  @computed_results ||= if response.key?('hits')
273
- _results_for_hits
354
+ _results_from_hits
274
355
  elsif response.key?('responses')
275
356
  # used by +msearch+
276
- _results_for_responses
357
+ _results_from_responses
358
+ elsif _tabular?
359
+ # used by +sql+ & +esql+
360
+ _results_from_tabular
277
361
  else
278
362
  []
279
363
  end
File without changes
File without changes
File without changes
@@ -65,6 +65,21 @@ module ElasticsearchRecord
65
65
  # However enabling this flag will surely fail transactional tests ...
66
66
  singleton_class.attr_accessor :error_on_transaction
67
67
  self.error_on_transaction = false
68
+
69
+ ##
70
+ # :singleton-method:
71
+ # Specifies if the table (index) statements resolve their provided table name(s) with the
72
+ # +table_name_prefix+ & +table_name_suffix+ of the connection config.
73
+ # As default every statement decorates, so a migration only ever names the *base* table (index).
74
+ # Disabling this flag restores the former, opt-in behaviour, where the decoration had to be
75
+ # applied by hand through +#_env_table_name+.
76
+ #
77
+ # HINT: this only provides the DEFAULT for a statement that was not given an explicit
78
+ # +decorate:+ argument - a single call can always opt in or out on its own.
79
+ #
80
+ # see @ ActiveRecord::ConnectionAdapters::Elasticsearch::TableStatements
81
+ singleton_class.attr_accessor :decorate_table_names
82
+ self.decorate_table_names = true
68
83
  end
69
84
 
70
85
  ActiveSupport.on_load(:active_record) do
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '7.17'
40
+ - !ruby/object:Gem::Dependency
41
+ name: coveralls_reborn
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.25'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.25'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: rspec
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -206,7 +219,6 @@ metadata:
206
219
  source_code_uri: https://github.com/ruby-smart/elasticsearch_record
207
220
  documentation_uri: https://rubydoc.info/gems/elasticsearch_record
208
221
  changelog_uri: https://github.com/ruby-smart/elasticsearch_record/blob/main/docs/CHANGELOG.md
209
- post_install_message:
210
222
  rdoc_options: []
211
223
  require_paths:
212
224
  - lib
@@ -221,8 +233,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
233
  - !ruby/object:Gem::Version
222
234
  version: '0'
223
235
  requirements: []
224
- rubygems_version: 3.4.19
225
- signing_key:
236
+ rubygems_version: 3.6.9
226
237
  specification_version: 4
227
238
  summary: ActiveRecord adapter for Elasticsearch
228
239
  test_files: []