inventory_refresh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +4 -0
  5. data/.rspec_ci +4 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +5 -0
  8. data/.rubocop_local.yml +2 -0
  9. data/.travis.yml +12 -0
  10. data/.yamllint +12 -0
  11. data/CHANGELOG.md +0 -0
  12. data/Gemfile +6 -0
  13. data/LICENSE +202 -0
  14. data/README.md +35 -0
  15. data/Rakefile +47 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/inventory_refresh.gemspec +34 -0
  19. data/lib/inventory_refresh.rb +11 -0
  20. data/lib/inventory_refresh/application_record_iterator.rb +56 -0
  21. data/lib/inventory_refresh/application_record_reference.rb +15 -0
  22. data/lib/inventory_refresh/graph.rb +157 -0
  23. data/lib/inventory_refresh/graph/topological_sort.rb +66 -0
  24. data/lib/inventory_refresh/inventory_collection.rb +1175 -0
  25. data/lib/inventory_refresh/inventory_collection/data_storage.rb +178 -0
  26. data/lib/inventory_refresh/inventory_collection/graph.rb +170 -0
  27. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +230 -0
  28. data/lib/inventory_refresh/inventory_collection/index/type/base.rb +80 -0
  29. data/lib/inventory_refresh/inventory_collection/index/type/data.rb +26 -0
  30. data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +286 -0
  31. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +116 -0
  32. data/lib/inventory_refresh/inventory_collection/reference.rb +96 -0
  33. data/lib/inventory_refresh/inventory_collection/references_storage.rb +106 -0
  34. data/lib/inventory_refresh/inventory_collection/scanner.rb +117 -0
  35. data/lib/inventory_refresh/inventory_collection/serialization.rb +140 -0
  36. data/lib/inventory_refresh/inventory_object.rb +303 -0
  37. data/lib/inventory_refresh/inventory_object_lazy.rb +151 -0
  38. data/lib/inventory_refresh/save_collection/base.rb +38 -0
  39. data/lib/inventory_refresh/save_collection/recursive.rb +52 -0
  40. data/lib/inventory_refresh/save_collection/saver/base.rb +390 -0
  41. data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
  42. data/lib/inventory_refresh/save_collection/saver/concurrent_safe.rb +71 -0
  43. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +632 -0
  44. data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
  45. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +85 -0
  46. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +120 -0
  47. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +196 -0
  48. data/lib/inventory_refresh/save_collection/topological_sort.rb +38 -0
  49. data/lib/inventory_refresh/save_inventory.rb +38 -0
  50. data/lib/inventory_refresh/target.rb +73 -0
  51. data/lib/inventory_refresh/target_collection.rb +80 -0
  52. data/lib/inventory_refresh/version.rb +3 -0
  53. data/tools/ci/create_db_user.sh +3 -0
  54. metadata +207 -0
@@ -0,0 +1,38 @@
1
+ require "inventory_refresh/save_collection/saver/batch"
2
+ require "inventory_refresh/save_collection/saver/concurrent_safe"
3
+ require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
4
+ require "inventory_refresh/save_collection/saver/default"
5
+
6
+ module InventoryRefresh::SaveCollection
7
+ class Base
8
+ class << self
9
+ # Saves one InventoryCollection object into the DB.
10
+ #
11
+ # @param ems [ExtManagementSystem] manger owning the InventoryCollection object
12
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want to save
13
+ def save_inventory_object_inventory(ems, inventory_collection)
14
+ #_log.debug("Saving collection #{inventory_collection} of size #{inventory_collection.size} to"\
15
+ # " the database, for the manager: '#{ems.name}'...")
16
+
17
+ if inventory_collection.custom_save_block.present?
18
+ #_log.debug("Saving collection #{inventory_collection} using a custom save block")
19
+ inventory_collection.custom_save_block.call(ems, inventory_collection)
20
+ else
21
+ save_inventory(inventory_collection)
22
+ end
23
+ #_log.debug("Saving collection #{inventory_collection}, for the manager: '#{ems.name}'...Complete")
24
+ inventory_collection.saved = true
25
+ end
26
+
27
+ private
28
+
29
+ # Saves one InventoryCollection object into the DB using a configured saver_strategy class.
30
+ #
31
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we want to save
32
+ def save_inventory(inventory_collection)
33
+ saver_class = "InventoryRefresh::SaveCollection::Saver::#{inventory_collection.saver_strategy.to_s.camelize}"
34
+ saver_class.constantize.new(inventory_collection).save_inventory_collection!
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ require "inventory_refresh/save_collection/base"
2
+
3
+ module InventoryRefresh::SaveCollection
4
+ class Recursive < InventoryRefresh::SaveCollection::Base
5
+ class << self
6
+ # Saves the passed InventoryCollection objects by recursively passing the graph
7
+ #
8
+ # @param ems [ExtManagementSystem] manager owning the inventory_collections
9
+ # @param inventory_collections [Array<InventoryRefresh::InventoryCollection>] array of InventoryCollection objects
10
+ # for saving
11
+ def save_collections(ems, inventory_collections)
12
+ graph = InventoryRefresh::InventoryCollection::Graph.new(inventory_collections)
13
+ graph.build_directed_acyclic_graph!
14
+
15
+ graph.nodes.each do |inventory_collection|
16
+ save_collection(ems, inventory_collection, [])
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # Saves the one passed InventoryCollection object
23
+ #
24
+ # @param ems [ExtManagementSystem] manager owning the inventory_collections
25
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object for saving
26
+ # @param traversed_collections [Array<InventoryRefresh::InventoryCollection>] array of traversed InventoryCollection
27
+ # objects, that we use for detecting possible cycle
28
+ def save_collection(ems, inventory_collection, traversed_collections)
29
+ unless inventory_collection.kind_of?(::InventoryRefresh::InventoryCollection)
30
+ raise "A InventoryRefresh::SaveInventory needs a InventoryCollection object, it got: #{inventory_collection.inspect}"
31
+ end
32
+
33
+ return if inventory_collection.saved?
34
+
35
+ traversed_collections << inventory_collection
36
+
37
+ unless inventory_collection.saveable?
38
+ inventory_collection.dependencies.each do |dependency|
39
+ next if dependency.saved?
40
+ if traversed_collections.include?(dependency)
41
+ raise "Edge from #{inventory_collection} to #{dependency} creates a cycle"
42
+ end
43
+ save_collection(ems, dependency, traversed_collections)
44
+ end
45
+ end
46
+
47
+ #_log.debug("Saving #{inventory_collection} of size #{inventory_collection.size}")
48
+ save_inventory_object_inventory(ems, inventory_collection)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,390 @@
1
+ require "inventory_refresh/application_record_iterator"
2
+ require "inventory_refresh/save_collection/saver/sql_helper"
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ module InventoryRefresh::SaveCollection
6
+ module Saver
7
+ class Base
8
+ include InventoryRefresh::SaveCollection::Saver::SqlHelper
9
+
10
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we will be saving
11
+ def initialize(inventory_collection)
12
+ @inventory_collection = inventory_collection
13
+ # TODO(lsmola) do I need to reload every time? Also it should be enough to clear the associations.
14
+ inventory_collection.parent.reload if inventory_collection.parent
15
+ @association = inventory_collection.db_collection_for_comparison
16
+
17
+ # Private attrs
18
+ @model_class = inventory_collection.model_class
19
+ @table_name = @model_class.table_name
20
+ @q_table_name = get_connection.quote_table_name(@table_name)
21
+ @primary_key = @model_class.primary_key
22
+ @arel_primary_key = @model_class.arel_attribute(@primary_key)
23
+ @unique_index_keys = inventory_collection.unique_index_keys
24
+ @unique_index_keys_to_s = inventory_collection.manager_ref_to_cols.map(&:to_s)
25
+ @select_keys = [@primary_key] + @unique_index_keys_to_s
26
+ @unique_db_primary_keys = Set.new
27
+ @unique_db_indexes = Set.new
28
+
29
+ # Right now ApplicationRecordIterator in association is used for targeted refresh. Given the small amount of
30
+ # records flowing through there, we probably don't need to optimize that association to fetch a pure SQL.
31
+ @pure_sql_records_fetching = !inventory_collection.use_ar_object? && !@association.kind_of?(InventoryRefresh::ApplicationRecordIterator)
32
+
33
+ @batch_size_for_persisting = inventory_collection.batch_size_pure_sql
34
+
35
+ @batch_size = @pure_sql_records_fetching ? @batch_size_for_persisting : inventory_collection.batch_size
36
+ @record_key_method = @pure_sql_records_fetching ? :pure_sql_record_key : :ar_record_key
37
+ @select_keys_indexes = @select_keys.each_with_object({}).with_index { |(key, obj), index| obj[key.to_s] = index }
38
+ @pg_types = @model_class.attribute_names.each_with_object({}) do |key, obj|
39
+ obj[key.to_sym] = inventory_collection.model_class.columns_hash[key]
40
+ .try(:sql_type_metadata)
41
+ .try(:instance_values)
42
+ .try(:[], "sql_type")
43
+ end
44
+
45
+ @serializable_keys = {}
46
+ @deserializable_keys = {}
47
+ @model_class.attribute_names.each do |key|
48
+ attribute_type = @model_class.type_for_attribute(key.to_s)
49
+ pg_type = @pg_types[key.to_sym]
50
+
51
+ if inventory_collection.use_ar_object?
52
+ # When using AR object, lets make sure we type.serialize(value) every value, so we have a slow but always
53
+ # working way driven by a configuration
54
+ @serializable_keys[key.to_sym] = attribute_type
55
+ @deserializable_keys[key.to_sym] = attribute_type
56
+ elsif attribute_type.respond_to?(:coder) ||
57
+ attribute_type.type == :int4range ||
58
+ attribute_type.type == :jsonb ||
59
+ pg_type == "text[]" ||
60
+ pg_type == "character varying[]"
61
+ # Identify columns that needs to be encoded by type.serialize(value), it's a costy operations so lets do
62
+ # do it only for columns we need it for.
63
+ # TODO: should these set @deserializable_keys too?
64
+ @serializable_keys[key.to_sym] = attribute_type
65
+ elsif attribute_type.type == :decimal
66
+ # Postgres formats decimal columns with fixed number of digits e.g. '0.100'
67
+ # Need to parse and let Ruby format the value to have a comparable string.
68
+ @serializable_keys[key.to_sym] = attribute_type
69
+ @deserializable_keys[key.to_sym] = attribute_type
70
+ end
71
+ end
72
+ end
73
+
74
+ # Saves the InventoryCollection
75
+ def save_inventory_collection!
76
+ # If we have a targeted InventoryCollection that wouldn't do anything, quickly skip it
77
+ return if inventory_collection.noop?
78
+ # If we want to use delete_complement strategy using :all_manager_uuids attribute, we are skipping any other
79
+ # job. We want to do 1 :delete_complement job at 1 time, to keep to memory down.
80
+ return delete_complement if inventory_collection.all_manager_uuids.present?
81
+
82
+ save!(association)
83
+ end
84
+
85
+ protected
86
+
87
+ attr_reader :inventory_collection, :association
88
+
89
+ delegate :build_stringified_reference, :build_stringified_reference_for_record, :to => :inventory_collection
90
+
91
+ # Applies serialize method for each relevant attribute, which will cast the value to the right type.
92
+ #
93
+ # @param all_attribute_keys [Symbol] attribute keys we want to process
94
+ # @param attributes [Hash] attributes hash
95
+ # @return [Hash] modified hash from parameter attributes with casted values
96
+ def values_for_database!(all_attribute_keys, attributes)
97
+ all_attribute_keys.each do |key|
98
+ next unless attributes.key?(key)
99
+
100
+ if (type = serializable_keys[key])
101
+ attributes[key] = type.serialize(attributes[key])
102
+ end
103
+ end
104
+ attributes
105
+ end
106
+
107
+ def transform_to_hash!(all_attribute_keys, hash)
108
+ if inventory_collection.use_ar_object?
109
+ record = inventory_collection.model_class.new(hash)
110
+ values_for_database!(all_attribute_keys,
111
+ record.attributes.slice(*record.changed_attributes.keys).symbolize_keys)
112
+ elsif serializable_keys?
113
+ values_for_database!(all_attribute_keys,
114
+ hash)
115
+ else
116
+ hash
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ attr_reader :unique_index_keys, :unique_index_keys_to_s, :select_keys, :unique_db_primary_keys, :unique_db_indexes,
123
+ :primary_key, :arel_primary_key, :record_key_method, :pure_sql_records_fetching, :select_keys_indexes,
124
+ :batch_size, :batch_size_for_persisting, :model_class, :serializable_keys, :deserializable_keys, :pg_types, :table_name,
125
+ :q_table_name
126
+
127
+ # Saves the InventoryCollection
128
+ #
129
+ # @param association [Symbol] An existing association on manager
130
+ def save!(association)
131
+ attributes_index = {}
132
+ inventory_objects_index = {}
133
+ inventory_collection.each do |inventory_object|
134
+ attributes = inventory_object.attributes(inventory_collection)
135
+ index = build_stringified_reference(attributes, unique_index_keys)
136
+
137
+ attributes_index[index] = attributes
138
+ inventory_objects_index[index] = inventory_object
139
+ end
140
+
141
+ #_log.debug("Processing #{inventory_collection} of size #{inventory_collection.size}...")
142
+ # Records that are in the DB, we will be updating or deleting them.
143
+ ActiveRecord::Base.transaction do
144
+ association.find_each do |record|
145
+ index = build_stringified_reference_for_record(record, unique_index_keys)
146
+
147
+ next unless assert_distinct_relation(record.id)
148
+ next unless assert_unique_record(record, index)
149
+
150
+ inventory_object = inventory_objects_index.delete(index)
151
+ hash = attributes_index.delete(index)
152
+
153
+ if inventory_object.nil?
154
+ # Record was found in the DB but not sent for saving, that means it doesn't exist anymore and we should
155
+ # delete it from the DB.
156
+ delete_record!(record) if inventory_collection.delete_allowed?
157
+ else
158
+ # Record was found in the DB and sent for saving, we will be updating the DB.
159
+ update_record!(record, hash, inventory_object) if assert_referential_integrity(hash)
160
+ end
161
+ end
162
+ end
163
+
164
+ unless inventory_collection.custom_reconnect_block.nil?
165
+ inventory_collection.custom_reconnect_block.call(inventory_collection, inventory_objects_index, attributes_index)
166
+ end
167
+
168
+ # Records that were not found in the DB but sent for saving, we will be creating these in the DB.
169
+ if inventory_collection.create_allowed?
170
+ ActiveRecord::Base.transaction do
171
+ inventory_objects_index.each do |index, inventory_object|
172
+ hash = attributes_index.delete(index)
173
+
174
+ create_record!(hash, inventory_object) if assert_referential_integrity(hash)
175
+ end
176
+ end
177
+ end
178
+ #_log.debug("Processing #{inventory_collection}, "\
179
+ # "created=#{inventory_collection.created_records.count}, "\
180
+ # "updated=#{inventory_collection.updated_records.count}, "\
181
+ # "deleted=#{inventory_collection.deleted_records.count}...Complete")
182
+ rescue => e
183
+ #_log.error("Error when saving #{inventory_collection} with #{inventory_collection_details}. Message: #{e.message}")
184
+ raise e
185
+ end
186
+
187
+ # @return [String] a string for logging purposes
188
+ def inventory_collection_details
189
+ "strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}, targeted: #{inventory_collection.targeted?}"
190
+ end
191
+
192
+ # @param record [ApplicationRecord] ApplicationRecord object
193
+ # @param key [Symbol] A key that is an attribute of the AR object
194
+ # @return [Object] Value of attribute name :key on the :record
195
+ def record_key(record, key)
196
+ record.public_send(key)
197
+ end
198
+
199
+ # Deletes a complement of referenced data
200
+ def delete_complement
201
+ return unless inventory_collection.delete_allowed?
202
+
203
+ all_manager_uuids_size = inventory_collection.all_manager_uuids.size
204
+
205
+ #_log.debug("Processing :delete_complement of #{inventory_collection} of size "\
206
+ # "#{all_manager_uuids_size}...")
207
+ deleted_counter = 0
208
+
209
+ inventory_collection.db_collection_for_comparison_for_complement_of(
210
+ inventory_collection.all_manager_uuids
211
+ ).find_in_batches do |batch|
212
+ ActiveRecord::Base.transaction do
213
+ batch.each do |record|
214
+ record.public_send(inventory_collection.delete_method)
215
+ deleted_counter += 1
216
+ end
217
+ end
218
+ end
219
+
220
+ #_log.debug("Processing :delete_complement of #{inventory_collection} of size "\
221
+ # "#{all_manager_uuids_size}, deleted=#{deleted_counter}...Complete")
222
+ end
223
+
224
+ # Deletes/soft-deletes a given record
225
+ #
226
+ # @param [ApplicationRecord] record we want to delete
227
+ def delete_record!(record)
228
+ record.public_send(inventory_collection.delete_method)
229
+ inventory_collection.store_deleted_records(record)
230
+ end
231
+
232
+ # @return [TrueClass] always return true, this method is redefined in default saver
233
+ def assert_unique_record(_record, _index)
234
+ # TODO(lsmola) can go away once we indexed our DB with unique indexes
235
+ true
236
+ end
237
+
238
+ # Check if relation provided is distinct, i.e. the relation should not return the same primary key value twice.
239
+ #
240
+ # @param primary_key_value [Bigint] primary key value
241
+ # @raise [Exception] if env is not production and relation is not distinct
242
+ # @return [Boolean] false if env is production and relation is not distinct
243
+ def assert_distinct_relation(primary_key_value)
244
+ if unique_db_primary_keys.include?(primary_key_value) # Include on Set is O(1)
245
+ # Change the InventoryCollection's :association or :arel parameter to return distinct results. The :through
246
+ # relations can return the same record multiple times. We don't want to do SELECT DISTINCT by default, since
247
+ # it can be very slow.
248
+ if false # TODO: Rails.env.production?
249
+ #_log.warn("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. "\
250
+ # " The duplicate value is being ignored.")
251
+ return false
252
+ else
253
+ raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ")
254
+ end
255
+ else
256
+ unique_db_primary_keys << primary_key_value
257
+ end
258
+ true
259
+ end
260
+
261
+ # Check that the needed foreign key leads to real value. This check simulates NOT NULL and FOREIGN KEY constraints
262
+ # we should have in the DB. The needed foreign keys are identified as fixed_foreign_keys, which are the foreign
263
+ # keys needed for saving of the record.
264
+ #
265
+ # @param hash [Hash] data we want to save
266
+ # @raise [Exception] if env is not production and a foreign_key is missing
267
+ # @return [Boolean] false if env is production and a foreign_key is missing
268
+ def assert_referential_integrity(hash)
269
+ inventory_collection.fixed_foreign_keys.each do |x|
270
+ next unless hash[x].nil?
271
+ subject = "#{hash} of #{inventory_collection} because of missing foreign key #{x} for "\
272
+ "#{inventory_collection.parent.class.name}:"\
273
+ "#{inventory_collection.parent.try(:id)}"
274
+ if false # TODO: Rails.env.production?
275
+ #_log.warn("Referential integrity check violated, ignoring #{subject}")
276
+ return false
277
+ else
278
+ raise("Referential integrity check violated for #{subject}")
279
+ end
280
+ end
281
+ true
282
+ end
283
+
284
+ # @return [Time] A rails friendly time getting config from ActiveRecord::Base.default_timezone (can be :local
285
+ # or :utc)
286
+ def time_now
287
+ if ActiveRecord::Base.default_timezone == :utc
288
+ Time.now.utc
289
+ else
290
+ Time.zone.now
291
+ end
292
+ end
293
+
294
+ # Enriches data hash with timestamp columns
295
+ #
296
+ # @param hash [Hash] data hash
297
+ # @param update_time [Time] data hash
298
+ def assign_attributes_for_update!(hash, update_time)
299
+ hash[:type] = model_class.name if supports_sti? && hash[:type].nil?
300
+ hash[:updated_on] = update_time if supports_updated_on?
301
+ hash[:updated_at] = update_time if supports_updated_at?
302
+ end
303
+
304
+ # Enriches data hash with timestamp and type columns
305
+ #
306
+ # @param hash [Hash] data hash
307
+ # @param create_time [Time] data hash
308
+ def assign_attributes_for_create!(hash, create_time)
309
+ hash[:created_on] = create_time if supports_created_on?
310
+ hash[:created_at] = create_time if supports_created_at?
311
+ assign_attributes_for_update!(hash, create_time)
312
+ end
313
+
314
+ def internal_columns
315
+ @internal_columns ||= inventory_collection.internal_columns
316
+ end
317
+
318
+ # Finds an index that fits the list of columns (keys) the best
319
+ #
320
+ # @param keys [Array<Symbol>]
321
+ # @raise [Exception] if the unique index for the columns was not found
322
+ # @return [ActiveRecord::ConnectionAdapters::IndexDefinition] unique index fitting the keys
323
+ def unique_index_for(keys)
324
+ inventory_collection.unique_index_for(keys)
325
+ end
326
+
327
+ # @return [Array<Symbol>] all columns that are part of the best fit unique index
328
+ def unique_index_columns
329
+ @unique_index_columns ||= inventory_collection.unique_index_columns
330
+ end
331
+
332
+ # @return [Array<String>] all columns that are part of the best fit unique index
333
+ def unique_index_columns_to_s
334
+ return @unique_index_columns_to_s if @unique_index_columns_to_s
335
+
336
+ @unique_index_columns_to_s = unique_index_columns.map(&:to_s)
337
+ end
338
+
339
+ # @return [Boolean] true if the model_class supports STI
340
+ def supports_sti?
341
+ @supports_sti_cache ||= inventory_collection.supports_sti?
342
+ end
343
+
344
+ # @return [Boolean] true if the model_class has created_on column
345
+ def supports_created_on?
346
+ @supports_created_on_cache ||= inventory_collection.supports_created_on?
347
+ end
348
+
349
+ # @return [Boolean] true if the model_class has updated_on column
350
+ def supports_updated_on?
351
+ @supports_updated_on_cache ||= inventory_collection.supports_updated_on?
352
+ end
353
+
354
+ # @return [Boolean] true if the model_class has created_at column
355
+ def supports_created_at?
356
+ @supports_created_at_cache ||= inventory_collection.supports_created_at?
357
+ end
358
+
359
+ # @return [Boolean] true if the model_class has updated_at column
360
+ def supports_updated_at?
361
+ @supports_updated_at_cache ||= inventory_collection.supports_updated_at?
362
+ end
363
+
364
+ # @return [Boolean] true if any serializable keys are present
365
+ def serializable_keys?
366
+ @serializable_keys_bool_cache ||= serializable_keys.present?
367
+ end
368
+
369
+ # @return [Boolean] true if the model_class has resource_timestamp column
370
+ def supports_remote_data_timestamp?(all_attribute_keys)
371
+ all_attribute_keys.include?(:resource_timestamp) # include? on Set is O(1)
372
+ end
373
+
374
+ # @return [Boolean] true if the model_class has resource_version column
375
+ def supports_remote_data_version?(all_attribute_keys)
376
+ all_attribute_keys.include?(:resource_version) # include? on Set is O(1)
377
+ end
378
+
379
+ # @return [Boolean] true if the model_class has resource_timestamps column
380
+ def supports_resource_timestamps_max?
381
+ @supports_resource_timestamps_max_cache ||= inventory_collection.supports_resource_timestamps_max?
382
+ end
383
+
384
+ # @return [Boolean] true if the model_class has resource_versions column
385
+ def supports_resource_versions_max?
386
+ @supports_resource_versions_max_cache ||= inventory_collection.supports_resource_versions_max?
387
+ end
388
+ end
389
+ end
390
+ end