inventory_refresh 0.1.1 → 0.2.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.gitignore +6 -0
  4. data/.travis.yml +3 -3
  5. data/Gemfile +4 -0
  6. data/inventory_refresh.gemspec +7 -5
  7. data/lib/inventory_refresh.rb +1 -0
  8. data/lib/inventory_refresh/inventory_collection.rb +115 -646
  9. data/lib/inventory_refresh/inventory_collection/builder.rb +249 -0
  10. data/lib/inventory_refresh/inventory_collection/graph.rb +0 -15
  11. data/lib/inventory_refresh/inventory_collection/helpers.rb +6 -0
  12. data/lib/inventory_refresh/inventory_collection/helpers/associations_helper.rb +80 -0
  13. data/lib/inventory_refresh/inventory_collection/helpers/initialize_helper.rb +456 -0
  14. data/lib/inventory_refresh/inventory_collection/helpers/questions_helper.rb +132 -0
  15. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +1 -1
  16. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +5 -5
  17. data/lib/inventory_refresh/inventory_collection/reference.rb +4 -0
  18. data/lib/inventory_refresh/inventory_collection/scanner.rb +111 -18
  19. data/lib/inventory_refresh/inventory_collection/serialization.rb +7 -7
  20. data/lib/inventory_refresh/inventory_collection/unconnected_edge.rb +19 -0
  21. data/lib/inventory_refresh/inventory_object.rb +17 -11
  22. data/lib/inventory_refresh/inventory_object_lazy.rb +20 -10
  23. data/lib/inventory_refresh/persister.rb +212 -0
  24. data/lib/inventory_refresh/save_collection/base.rb +18 -3
  25. data/lib/inventory_refresh/save_collection/saver/base.rb +27 -64
  26. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +73 -225
  27. data/lib/inventory_refresh/save_collection/saver/partial_upsert_helper.rb +226 -0
  28. data/lib/inventory_refresh/save_collection/saver/retention_helper.rb +115 -0
  29. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +122 -0
  30. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +24 -5
  31. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +6 -6
  32. data/lib/inventory_refresh/save_collection/sweeper.rb +69 -0
  33. data/lib/inventory_refresh/save_inventory.rb +18 -8
  34. data/lib/inventory_refresh/target_collection.rb +12 -0
  35. data/lib/inventory_refresh/version.rb +1 -1
  36. metadata +61 -19
  37. data/lib/inventory_refresh/save_collection/recursive.rb +0 -52
  38. data/lib/inventory_refresh/save_collection/saver/concurrent_safe.rb +0 -71
@@ -42,12 +42,14 @@ module InventoryRefresh
42
42
  "InventoryObjectLazy:('#{self}', #{inventory_collection}#{suffix})"
43
43
  end
44
44
 
45
+ # @param inventory_object [InventoryRefresh::InventoryObject] InventoryObject object owning this relation
46
+ # @param inventory_object_key [Symbol] InventoryObject object's attribute pointing to this relation
45
47
  # @return [InventoryRefresh::InventoryObject, Object] InventoryRefresh::InventoryObject instance or an attribute
46
48
  # on key
47
- def load
49
+ def load(inventory_object = nil, inventory_object_key = nil)
48
50
  transform_nested_secondary_indexes! if transform_nested_lazy_finds && nested_secondary_index?
49
51
 
50
- key ? load_object_with_key : load_object
52
+ load_object(inventory_object, inventory_object_key)
51
53
  end
52
54
 
53
55
  # return [Boolean] true if the Lazy object is causing a dependency, Lazy link is always a dependency if no :key
@@ -128,15 +130,15 @@ module InventoryRefresh
128
130
  skeletal_primary_index.build(full_reference)
129
131
  end
130
132
 
133
+ # @param loaded_object [InventoryRefresh::InventoryObject, NilClass] Loaded object or nil if object wasn't found
131
134
  # @return [Object] value found or :key or default value if the value is nil
132
- def load_object_with_key
135
+ def load_object_with_key(loaded_object)
133
136
  # TODO(lsmola) Log error if we are accessing path that is present in blacklist or not present in whitelist
134
- found = inventory_collection.find(reference)
135
- if found.present?
136
- if found.try(:data).present?
137
- found.data[key] || default
137
+ if loaded_object.present?
138
+ if loaded_object.try(:data).present?
139
+ loaded_object.data[key] || default
138
140
  else
139
- found.public_send(key) || default
141
+ loaded_object.public_send(key) || default
140
142
  end
141
143
  else
142
144
  default
@@ -144,8 +146,16 @@ module InventoryRefresh
144
146
  end
145
147
 
146
148
  # @return [InventoryRefresh::InventoryObject, NilClass] InventoryRefresh::InventoryObject instance or nil if not found
147
- def load_object
148
- inventory_collection.find(reference)
149
+ def load_object(inventory_object = nil, inventory_object_key = nil)
150
+ loaded_object = inventory_collection.find(reference)
151
+
152
+ if inventory_object && inventory_object_key && !loaded_object && reference.loadable?
153
+ # Object was not loaded, but the reference is pointing to something, lets return it as edge that should've
154
+ # been loaded.
155
+ inventory_object.inventory_collection.store_unconnected_edges(inventory_object, inventory_object_key, self)
156
+ end
157
+
158
+ key ? load_object_with_key(loaded_object) : loaded_object
149
159
  end
150
160
  end
151
161
  end
@@ -0,0 +1,212 @@
1
+ module InventoryRefresh
2
+ class Persister
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ attr_reader :manager, :target, :collections
7
+
8
+ attr_accessor :refresh_state_uuid, :refresh_state_part_uuid, :total_parts, :sweep_scope, :retry_count, :retry_max
9
+
10
+ # @param manager [ManageIQ::Providers::BaseManager] A manager object
11
+ # @param target [Object] A refresh Target object
12
+ def initialize(manager, target = nil)
13
+ @manager = manager
14
+ @target = target
15
+
16
+ @collections = {}
17
+
18
+ initialize_inventory_collections
19
+ end
20
+
21
+ # Interface for creating InventoryCollection under @collections
22
+ #
23
+ # @param builder_class [ManageIQ::Providers::Inventory::Persister::Builder] or subclasses
24
+ # @param collection_name [Symbol || Array] used as InventoryCollection:association
25
+ # @param extra_properties [Hash] props from InventoryCollection.initialize list
26
+ # - adds/overwrites properties added by builder
27
+ #
28
+ # @param settings [Hash] builder settings
29
+ # - @see ManageIQ::Providers::Inventory::Persister::Builder.default_options
30
+ # - @see make_builder_settings()
31
+ #
32
+ # @example
33
+ # add_collection(:vms, ManageIQ::Providers::Inventory::Persister::Builder::CloudManager) do |builder|
34
+ # builder.add_properties(
35
+ # :strategy => :local_db_cache_all,
36
+ # )
37
+ # )
38
+ #
39
+ # @see documentation https://github.com/ManageIQ/guides/tree/master/providers/persister/inventory_collections.md
40
+ #
41
+ def add_collection(collection_name, builder_class = inventory_collection_builder, extra_properties = {}, settings = {}, &block)
42
+ builder = builder_class.prepare_data(collection_name,
43
+ self.class,
44
+ builder_settings(settings),
45
+ &block)
46
+
47
+ builder.add_properties(extra_properties) if extra_properties.present?
48
+
49
+ builder.add_properties({:manager_uuids => target.try(:references, collection_name) || []}, :if_missing) if targeted?
50
+
51
+ builder.evaluate_lambdas!(self)
52
+
53
+ collections[collection_name] = builder.to_inventory_collection
54
+ end
55
+
56
+ # @return [Array<InventoryRefresh::InventoryCollection>] array of InventoryCollection objects of the persister
57
+ def inventory_collections
58
+ collections.values
59
+ end
60
+
61
+ # @return [Array<Symbol>] array of InventoryCollection object names of the persister
62
+ def inventory_collections_names
63
+ collections.keys
64
+ end
65
+
66
+ # @return [InventoryRefresh::InventoryCollection] returns a defined InventoryCollection or undefined method
67
+ def method_missing(method_name, *arguments, &block)
68
+ if inventory_collections_names.include?(method_name)
69
+ self.define_collections_reader(method_name)
70
+ send(method_name)
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ # @return [Boolean] true if InventoryCollection with passed method_name name is defined
77
+ def respond_to_missing?(method_name, _include_private = false)
78
+ inventory_collections_names.include?(method_name) || super
79
+ end
80
+
81
+ # Defines a new attr reader returning InventoryCollection object
82
+ def define_collections_reader(collection_key)
83
+ define_singleton_method(collection_key) do
84
+ collections[collection_key]
85
+ end
86
+ end
87
+
88
+ def inventory_collection_builder
89
+ ::InventoryRefresh::InventoryCollection::Builder
90
+ end
91
+
92
+ # Persists InventoryCollection objects into the DB
93
+ def persist!
94
+ InventoryRefresh::SaveInventory.save_inventory(manager, inventory_collections)
95
+ end
96
+
97
+ # Returns serialized Persisted object to JSON
98
+ # @return [String] serialized Persisted object to JSON
99
+ def to_json
100
+ JSON.dump(to_hash)
101
+ end
102
+
103
+ # @return [Hash] entire Persister object serialized to hash
104
+ def to_hash
105
+ collections_data = collections.map do |_, collection|
106
+ next if collection.data.blank? &&
107
+ collection.targeted_scope.primary_references.blank? &&
108
+ collection.all_manager_uuids.nil? &&
109
+ collection.skeletal_primary_index.index_data.blank?
110
+
111
+ collection.to_hash
112
+ end.compact
113
+
114
+ {
115
+ :refresh_state_uuid => refresh_state_uuid,
116
+ :refresh_state_part_uuid => refresh_state_part_uuid,
117
+ :retry_count => retry_count,
118
+ :retry_max => retry_max,
119
+ :total_parts => total_parts,
120
+ :sweep_scope => sweep_scope,
121
+ :collections => collections_data,
122
+ }
123
+ end
124
+
125
+ class << self
126
+ # Returns Persister object loaded from a passed JSON
127
+ #
128
+ # @param json_data [String] input JSON data
129
+ # @return [ManageIQ::Providers::Inventory::Persister] Persister object loaded from a passed JSON
130
+ def from_json(json_data, manager, target = nil)
131
+ from_hash(JSON.parse(json_data), manager, target)
132
+ end
133
+
134
+ # Returns Persister object built from serialized data
135
+ #
136
+ # @param persister_data [Hash] serialized Persister object in hash
137
+ # @return [ManageIQ::Providers::Inventory::Persister] Persister object built from serialized data
138
+ def from_hash(persister_data, manager, target = nil)
139
+ # TODO(lsmola) we need to pass serialized targeted scope here
140
+ target ||= InventoryRefresh::TargetCollection.new(:manager => manager)
141
+
142
+ new(manager, target).tap do |persister|
143
+ persister_data['collections'].each do |collection|
144
+ inventory_collection = persister.collections[collection['name'].try(:to_sym)]
145
+ raise "Unrecognized InventoryCollection name: #{inventory_collection}" if inventory_collection.blank?
146
+
147
+ inventory_collection.from_hash(collection, persister.collections)
148
+ end
149
+
150
+ persister.refresh_state_uuid = persister_data['refresh_state_uuid']
151
+ persister.refresh_state_part_uuid = persister_data['refresh_state_part_uuid']
152
+ persister.retry_count = persister_data['retry_count']
153
+ persister.retry_max = persister_data['retry_max']
154
+ persister.total_parts = persister_data['total_parts']
155
+ persister.sweep_scope = persister_data['sweep_scope']
156
+ end
157
+ end
158
+ end
159
+
160
+ protected
161
+
162
+ def initialize_inventory_collections
163
+ # can be implemented in a subclass
164
+ end
165
+
166
+ # @param extra_settings [Hash]
167
+ # :auto_inventory_attributes
168
+ # - auto creates inventory_object_attributes from target model_class setters
169
+ # - attributes used in InventoryObject.add_attributes
170
+ # :without_model_class
171
+ # - if false and no model_class derived or specified, throws exception
172
+ # - doesn't try to derive model class automatically
173
+ # - @see method ManageIQ::Providers::Inventory::Persister::Builder.auto_model_class
174
+ def builder_settings(extra_settings = {})
175
+ opts = inventory_collection_builder.default_options
176
+
177
+ opts[:shared_properties] = shared_options
178
+ opts[:auto_inventory_attributes] = true
179
+ opts[:without_model_class] = false
180
+
181
+ opts.merge(extra_settings)
182
+ end
183
+
184
+ def strategy
185
+ nil
186
+ end
187
+
188
+ def saver_strategy
189
+ :default
190
+ end
191
+
192
+ # Persisters for targeted refresh can override to true
193
+ def targeted?
194
+ false
195
+ end
196
+
197
+ def assert_graph_integrity?
198
+ false
199
+ end
200
+
201
+ # @return [Hash] kwargs shared for all InventoryCollection objects
202
+ def shared_options
203
+ {
204
+ :saver_strategy => saver_strategy,
205
+ :strategy => strategy,
206
+ :targeted => targeted?,
207
+ :parent => manager.presence,
208
+ :assert_graph_integrity => assert_graph_integrity?,
209
+ }
210
+ end
211
+ end
212
+ end
@@ -1,6 +1,5 @@
1
1
  require "inventory_refresh/logging"
2
2
  require "inventory_refresh/save_collection/saver/batch"
3
- require "inventory_refresh/save_collection/saver/concurrent_safe"
4
3
  require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
5
4
  require "inventory_refresh/save_collection/saver/default"
6
5
 
@@ -14,7 +13,9 @@ module InventoryRefresh::SaveCollection
14
13
  # @param ems [ExtManagementSystem] manger owning the InventoryCollection object
15
14
  # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want to save
16
15
  def save_inventory_object_inventory(ems, inventory_collection)
17
- logger.debug("Saving collection #{inventory_collection} of size #{inventory_collection.size} to"\
16
+ return if skip?(inventory_collection)
17
+
18
+ logger.debug("----- BEGIN ----- Saving collection #{inventory_collection} of size #{inventory_collection.size} to"\
18
19
  " the database, for the manager: '#{ems.name}'...")
19
20
 
20
21
  if inventory_collection.custom_save_block.present?
@@ -23,12 +24,26 @@ module InventoryRefresh::SaveCollection
23
24
  else
24
25
  save_inventory(inventory_collection)
25
26
  end
26
- logger.debug("Saving collection #{inventory_collection}, for the manager: '#{ems.name}'...Complete")
27
+ logger.debug("----- END ----- Saving collection #{inventory_collection}, for the manager: '#{ems.name}'...Complete")
27
28
  inventory_collection.saved = true
28
29
  end
29
30
 
30
31
  private
31
32
 
33
+ # Returns true and sets collection as saved, if the collection should be skipped.
34
+ #
35
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want to save
36
+ # @return [Boolean] True if processing of the collection should be skipped
37
+ def skip?(inventory_collection)
38
+ if inventory_collection.noop?
39
+ logger.debug("Skipping #{inventory_collection} processing because it will do no operation.")
40
+ inventory_collection.saved = true
41
+ return true
42
+ end
43
+
44
+ false
45
+ end
46
+
32
47
  # Saves one InventoryCollection object into the DB using a configured saver_strategy class.
33
48
  #
34
49
  # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want to save
@@ -24,7 +24,7 @@ module InventoryRefresh::SaveCollection
24
24
  @arel_primary_key = @model_class.arel_attribute(@primary_key)
25
25
  @unique_index_keys = inventory_collection.unique_index_keys
26
26
  @unique_index_keys_to_s = inventory_collection.manager_ref_to_cols.map(&:to_s)
27
- @select_keys = [@primary_key] + @unique_index_keys_to_s
27
+ @select_keys = [@primary_key] + @unique_index_keys_to_s + internal_columns.map(&:to_s)
28
28
  @unique_db_primary_keys = Set.new
29
29
  @unique_db_indexes = Set.new
30
30
 
@@ -77,18 +77,23 @@ module InventoryRefresh::SaveCollection
77
77
  def save_inventory_collection!
78
78
  # If we have a targeted InventoryCollection that wouldn't do anything, quickly skip it
79
79
  return if inventory_collection.noop?
80
- # If we want to use delete_complement strategy using :all_manager_uuids attribute, we are skipping any other
81
- # job. We want to do 1 :delete_complement job at 1 time, to keep to memory down.
82
- return delete_complement if inventory_collection.all_manager_uuids.present?
83
80
 
84
- save!(association)
81
+ # Delete_complement strategy using :all_manager_uuids attribute
82
+ delete_complement unless inventory_collection.delete_complement_noop?
83
+
84
+ # Create/Update/Archive/Delete records based on InventoryCollection data and scope
85
+ save!(association) unless inventory_collection.saving_noop?
85
86
  end
86
87
 
87
88
  protected
88
89
 
89
90
  attr_reader :inventory_collection, :association
90
91
 
91
- delegate :build_stringified_reference, :build_stringified_reference_for_record, :to => :inventory_collection
92
+ delegate :build_stringified_reference,
93
+ :build_stringified_reference_for_record,
94
+ :resource_version_column,
95
+ :internal_columns,
96
+ :to => :inventory_collection
92
97
 
93
98
  # Applies serialize method for each relevant attribute, which will cast the value to the right type.
94
99
  #
@@ -126,6 +131,8 @@ module InventoryRefresh::SaveCollection
126
131
  :batch_size, :batch_size_for_persisting, :model_class, :serializable_keys, :deserializable_keys, :pg_types, :table_name,
127
132
  :q_table_name
128
133
 
134
+ delegate :supports_column?, :to => :inventory_collection
135
+
129
136
  # Saves the InventoryCollection
130
137
  #
131
138
  # @param association [Symbol] An existing association on manager
@@ -200,27 +207,7 @@ module InventoryRefresh::SaveCollection
200
207
 
201
208
  # Deletes a complement of referenced data
202
209
  def delete_complement
203
- return unless inventory_collection.delete_allowed?
204
-
205
- all_manager_uuids_size = inventory_collection.all_manager_uuids.size
206
-
207
- logger.debug("Processing :delete_complement of #{inventory_collection} of size "\
208
- "#{all_manager_uuids_size}...")
209
- deleted_counter = 0
210
-
211
- inventory_collection.db_collection_for_comparison_for_complement_of(
212
- inventory_collection.all_manager_uuids
213
- ).find_in_batches do |batch|
214
- ActiveRecord::Base.transaction do
215
- batch.each do |record|
216
- record.public_send(inventory_collection.delete_method)
217
- deleted_counter += 1
218
- end
219
- end
220
- end
221
-
222
- logger.debug("Processing :delete_complement of #{inventory_collection} of size "\
223
- "#{all_manager_uuids_size}, deleted=#{deleted_counter}...Complete")
210
+ raise(":delete_complement method is supported only for :saver_strategy => [:batch, :concurrent_safe_batch]")
224
211
  end
225
212
 
226
213
  # Deletes/soft-deletes a given record
@@ -247,7 +234,7 @@ module InventoryRefresh::SaveCollection
247
234
  # Change the InventoryCollection's :association or :arel parameter to return distinct results. The :through
248
235
  # relations can return the same record multiple times. We don't want to do SELECT DISTINCT by default, since
249
236
  # it can be very slow.
250
- if false # TODO: Rails.env.production?
237
+ unless inventory_collection.assert_graph_integrity
251
238
  logger.warn("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. "\
252
239
  " The duplicate value is being ignored.")
253
240
  return false
@@ -273,7 +260,7 @@ module InventoryRefresh::SaveCollection
273
260
  subject = "#{hash} of #{inventory_collection} because of missing foreign key #{x} for "\
274
261
  "#{inventory_collection.parent.class.name}:"\
275
262
  "#{inventory_collection.parent.try(:id)}"
276
- if false # TODO: Rails.env.production?
263
+ unless inventory_collection.assert_graph_integrity
277
264
  logger.warn("Referential integrity check violated, ignoring #{subject}")
278
265
  return false
279
266
  else
@@ -299,8 +286,8 @@ module InventoryRefresh::SaveCollection
299
286
  # @param update_time [Time] data hash
300
287
  def assign_attributes_for_update!(hash, update_time)
301
288
  hash[:type] = model_class.name if supports_sti? && hash[:type].nil?
302
- hash[:updated_on] = update_time if supports_updated_on?
303
- hash[:updated_at] = update_time if supports_updated_at?
289
+ hash[:updated_on] = update_time if supports_column?(:updated_on)
290
+ hash[:updated_at] = update_time if supports_column?(:updated_at)
304
291
  end
305
292
 
306
293
  # Enriches data hash with timestamp and type columns
@@ -308,8 +295,8 @@ module InventoryRefresh::SaveCollection
308
295
  # @param hash [Hash] data hash
309
296
  # @param create_time [Time] data hash
310
297
  def assign_attributes_for_create!(hash, create_time)
311
- hash[:created_on] = create_time if supports_created_on?
312
- hash[:created_at] = create_time if supports_created_at?
298
+ hash[:created_on] = create_time if supports_column?(:created_on)
299
+ hash[:created_at] = create_time if supports_column?(:created_at)
313
300
  assign_attributes_for_update!(hash, create_time)
314
301
  end
315
302
 
@@ -343,49 +330,25 @@ module InventoryRefresh::SaveCollection
343
330
  @supports_sti_cache ||= inventory_collection.supports_sti?
344
331
  end
345
332
 
346
- # @return [Boolean] true if the model_class has created_on column
347
- def supports_created_on?
348
- @supports_created_on_cache ||= inventory_collection.supports_created_on?
349
- end
350
-
351
- # @return [Boolean] true if the model_class has updated_on column
352
- def supports_updated_on?
353
- @supports_updated_on_cache ||= inventory_collection.supports_updated_on?
354
- end
355
-
356
- # @return [Boolean] true if the model_class has created_at column
357
- def supports_created_at?
358
- @supports_created_at_cache ||= inventory_collection.supports_created_at?
359
- end
360
-
361
- # @return [Boolean] true if the model_class has updated_at column
362
- def supports_updated_at?
363
- @supports_updated_at_cache ||= inventory_collection.supports_updated_at?
364
- end
365
-
366
333
  # @return [Boolean] true if any serializable keys are present
367
334
  def serializable_keys?
368
335
  @serializable_keys_bool_cache ||= serializable_keys.present?
369
336
  end
370
337
 
371
- # @return [Boolean] true if the model_class has resource_timestamp column
338
+ # @return [Boolean] true if the keys we are saving have resource_timestamp column
372
339
  def supports_remote_data_timestamp?(all_attribute_keys)
373
340
  all_attribute_keys.include?(:resource_timestamp) # include? on Set is O(1)
374
341
  end
375
342
 
376
- # @return [Boolean] true if the model_class has resource_version column
343
+ # @return [Boolean] true if the keys we are saving have resource_counter column
377
344
  def supports_remote_data_version?(all_attribute_keys)
378
- all_attribute_keys.include?(:resource_version) # include? on Set is O(1)
379
- end
380
-
381
- # @return [Boolean] true if the model_class has resource_timestamps column
382
- def supports_resource_timestamps_max?
383
- @supports_resource_timestamps_max_cache ||= inventory_collection.supports_resource_timestamps_max?
345
+ all_attribute_keys.include?(:resource_counter) # include? on Set is O(1)
384
346
  end
385
347
 
386
- # @return [Boolean] true if the model_class has resource_versions column
387
- def supports_resource_versions_max?
388
- @supports_resource_versions_max_cache ||= inventory_collection.supports_resource_versions_max?
348
+ # @return [Boolean] true if the keys we are saving have resource_version column, which solves for a quick check
349
+ # if the record was modified
350
+ def supports_resource_version?(all_attribute_keys)
351
+ all_attribute_keys.include?(resource_version_column) # include? on Set is O(1)
389
352
  end
390
353
  end
391
354
  end