inventory_refresh 0.0.1

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 (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