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,80 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ module InventoryRefresh
4
+ class InventoryCollection
5
+ module Index
6
+ module Type
7
+ class Base
8
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection owning the index
9
+ # @param index_name
10
+ def initialize(inventory_collection, index_name, attribute_names, *_args)
11
+ @index = {}
12
+
13
+ @inventory_collection = inventory_collection
14
+ @index_name = index_name
15
+ @attribute_names = attribute_names
16
+
17
+ assert_attribute_names!
18
+ end
19
+
20
+ delegate :keys, :to => :index
21
+
22
+ # Indexes passed InventoryObject
23
+ #
24
+ # @param inventory_object [InventoryRefresh::InventoryObject] InventoryObject we want to index
25
+ # @return [InventoryRefresh::InventoryObject] InventoryObject object
26
+ def store_index_for(inventory_object)
27
+ index[build_stringified_reference(inventory_object.data, attribute_names)] = inventory_object
28
+ end
29
+
30
+ # Rebuilds the indexes for all InventoryObject objects
31
+ def reindex!
32
+ self.index = {}
33
+ data.each do |inventory_object|
34
+ store_index_for(inventory_object)
35
+ end
36
+ end
37
+
38
+ # @return [Array] Returns index data
39
+ def index_data
40
+ index.values
41
+ end
42
+
43
+ # Find value based on index_value
44
+ #
45
+ # @param _index_value [String] a index_value of the InventoryObject we search for
46
+ def find(_index_value)
47
+ raise "Implement in subclass"
48
+ end
49
+
50
+ protected
51
+
52
+ attr_reader :attribute_names, :index, :index_name, :inventory_collection
53
+
54
+ private
55
+
56
+ attr_writer :index
57
+
58
+ delegate :build_stringified_reference, :data, :model_class, :custom_save_block, :to => :inventory_collection
59
+
60
+ # Asserts that InventoryCollection model has attributes specified for index
61
+ def assert_attribute_names!
62
+ # Skip for manually defined nodes
63
+ return if model_class.nil?
64
+ # When we do custom saving, we allow any indexes to be passed, to no limit the user
65
+ return unless custom_save_block.nil?
66
+
67
+ # We cannot simply do model_class.method_defined?(attribute_name.to_sym), because e.g. db attributes seems
68
+ # to be create lazily
69
+ test_model_object = model_class.new
70
+ attribute_names.each do |attribute_name|
71
+ unless test_model_object.respond_to?(attribute_name.to_sym)
72
+ raise "Invalid definition of index :#{index_name}, there is no attribute :#{attribute_name} on model #{model_class}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ require "inventory_refresh/inventory_collection/index/type/base"
2
+
3
+ module InventoryRefresh
4
+ class InventoryCollection
5
+ module Index
6
+ module Type
7
+ class Data < InventoryRefresh::InventoryCollection::Index::Type::Base
8
+ # Find value based on index_value
9
+ #
10
+ # @param index_value [String] a index_value of the InventoryObject we search in data
11
+ def find(index_value)
12
+ index[index_value]
13
+ end
14
+
15
+ # Deletes and returns the value on the index_value
16
+ #
17
+ # @param index_value [String] a index_value of the InventoryObject we search for
18
+ # @return [InventoryObject|nil] Returns found value or nil
19
+ def delete(index_value)
20
+ index.delete(index_value)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,286 @@
1
+ require "inventory_refresh/inventory_collection/index/type/base"
2
+ require "inventory_refresh/application_record_reference"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "more_core_extensions/core_ext/hash"
5
+
6
+ module InventoryRefresh
7
+ class InventoryCollection
8
+ module Index
9
+ module Type
10
+ class LocalDb < InventoryRefresh::InventoryCollection::Index::Type::Base
11
+ # (see InventoryRefresh::InventoryCollection::Index::Type::Base#initialize)
12
+ # @param data_index[InventoryRefresh::InventoryCollection::Index::Type::Data] Related data index, so we can
13
+ # figure out what data we are building vs. what we need to fetch from the DB
14
+ def initialize(inventory_collection, index_name, attribute_names, data_index)
15
+ super
16
+
17
+ @index = nil
18
+ @loaded_references = Set.new
19
+ @data_index = data_index
20
+ @all_references_loaded = false
21
+ end
22
+
23
+ # Finds reference in the DB. Using a configured strategy we cache obtained data in the index, so the
24
+ # same find will not hit database twice. Also if we use lazy_links and this is called when
25
+ # data_collection_finalized?, we load all data from the DB, referenced by lazy_links, in one query.
26
+ #
27
+ # @param reference [InventoryRefresh::InventoryCollection::Reference] Reference we want to find
28
+ def find(reference)
29
+ # Use the cached index only data_collection_finalized?, meaning no new reference can occur
30
+ if data_collection_finalized? && all_references_loaded? && index
31
+ return index[reference.stringified_reference]
32
+ else
33
+ return index[reference.stringified_reference] if index && index[reference.stringified_reference]
34
+ # We haven't found the reference, lets add it to the list of references and load it
35
+ add_reference(reference)
36
+ end
37
+
38
+ # Put our existing data_index keys into loaded references
39
+ loaded_references.merge(data_index.keys)
40
+ # Load the rest of the references from the DB
41
+ populate_index!
42
+
43
+ self.all_references_loaded = true if data_collection_finalized?
44
+
45
+ index[reference.stringified_reference]
46
+ end
47
+
48
+ private
49
+
50
+ attr_accessor :all_references_loaded, :schema
51
+ attr_reader :data_index, :loaded_references
52
+ attr_writer :index
53
+
54
+ delegate :add_reference,
55
+ :arel,
56
+ :association,
57
+ :association_to_base_class_mapping,
58
+ :association_to_foreign_key_mapping,
59
+ :association_to_foreign_type_mapping,
60
+ :attribute_references,
61
+ :data_collection_finalized?,
62
+ :db_relation,
63
+ :inventory_object?,
64
+ :inventory_object_lazy?,
65
+ :model_class,
66
+ :new_inventory_object,
67
+ :parent,
68
+ :references,
69
+ :strategy,
70
+ :stringify_joiner,
71
+ :table_name,
72
+ :to => :inventory_collection
73
+
74
+ alias all_references_loaded? all_references_loaded
75
+
76
+ # Fills index with InventoryObjects obtained from the DB
77
+ def populate_index!
78
+ # Load only new references from the DB
79
+ new_references = index_references - loaded_references
80
+ # And store which references we've already loaded
81
+ loaded_references.merge(new_references)
82
+
83
+ # Initialize index in nil
84
+ self.index ||= {}
85
+
86
+ return if new_references.blank? # Return if all references are already loaded
87
+
88
+ # TODO(lsmola) selected need to contain also :keys used in other InventoryCollections pointing to this one, once
89
+ # we get list of all keys for each InventoryCollection ,we can uncomnent
90
+ # selected = [:id] + attribute_names.map { |x| model_class.reflect_on_association(x).try(:foreign_key) || x }
91
+ # selected << :type if model_class.new.respond_to? :type
92
+ # load_from_db.select(selected).find_each do |record|
93
+
94
+ full_references = references[index_name].select { |key, _value| new_references.include?(key) } # O(1) include on Set
95
+ full_references = full_references.values.map(&:full_reference)
96
+
97
+ schema = get_schema(full_references)
98
+ paths = schema.keys
99
+ rails_friendly_includes_schema = get_rails_friendly_includes_schema(paths)
100
+
101
+ all_values = full_references.map do |ref|
102
+ schema.map do |schema_item_path, arel_column|
103
+ arel_column.eq(fetch_hash_path(schema_item_path, ref))
104
+ end.inject(:and)
105
+ end
106
+
107
+ # Return the the correct relation based on strategy and selection&projection
108
+ projection = nil
109
+
110
+ db_relation(rails_friendly_includes_schema, all_values, projection).find_each do |record|
111
+ process_db_record!(record, paths)
112
+ end
113
+ end
114
+
115
+ # Builds a multiselection conditions like (table1.a = a1 AND table2.b = b1) OR (table1.a = a2 AND table2.b = b2)
116
+ # @param all_values [Array<Arel::Nodes::And>] nested array of arel nodes
117
+ # @return [String] A condition usable in .where of an ActiveRecord relation
118
+ def build_multi_selection_condition(all_values)
119
+ # We do pure SQL OR, since Arel is nesting every .or into another parentheses, otherwise this would be just
120
+ # all_values.inject(:or)
121
+ all_values.map { |value| "(#{value.to_sql})" }.join(" OR ")
122
+ end
123
+
124
+ # Traverses the schema_item_path e.g. [:hardware, :vm_or_template, :ems_ref] and gets the value on the path
125
+ # in hash.
126
+ # @param path [Array] path for traversing hash e.g. [:hardware, :vm_or_template, :ems_ref]
127
+ # @param hash [Hash] nested hash with data e.g. {:hardware => {:vm_or_template => {:ems_ref => "value"}}}
128
+ # @return [Object] value in the Hash on the path, in the example that would be "value"
129
+ def fetch_hash_path(path, hash)
130
+ path.inject(hash) { |x, r| x.try(:[], r) }
131
+ end
132
+
133
+ # Traverses the schema_item_path e.g. [:hardware, :vm_or_template, :ems_ref] and gets the value on the path
134
+ # in object.
135
+ # @param path [Array] path for traversing hash e.g. [:hardware, :vm_or_template, :ems_ref]
136
+ # @param object [ApplicationRecord] an ApplicationRecord fetched from the DB
137
+ # @return [Object] value in the Hash on the path, in the example that would be "value"
138
+ def fetch_object_path(path, object)
139
+ path.inject(object) { |x, r| x.public_send(r) }
140
+ end
141
+
142
+ # For full_reference {:hardware => lazy_find_hardware(lazy_find_vm_or_template(:ems_ref))}
143
+ # we get schema of
144
+ # {[:hardware, :vm_or_template, :ems_ref] => VmOrTemplate.arel_table[:ems_ref]]
145
+ #
146
+ # @param full_references [Hash] InventoryRefresh::InventoryCollection::Reference object full_reference method
147
+ # containing full reference to InventoryObject
148
+ # @return [Hash] Hash containing key representing path to record's attribute and value representing arel
149
+ # definition of column
150
+ def get_schema(full_references)
151
+ @schema ||= get_schema_recursive(attribute_names, model_class.arel_table, full_references.first, {}, [], 0)
152
+ end
153
+
154
+ # Converts an array of paths to attributes in different DB tables into rails friendly format, that can be used
155
+ # for correct DB JOIN of those tables (references&includes methods)
156
+ # @param [Array[Array]] Nested array with paths e.g. [[:hardware, :vm_or_template, :ems_ref], [:description]
157
+ # @return [Array] A rails friendly format for ActiveRecord relation .includes and .references
158
+ def get_rails_friendly_includes_schema(paths)
159
+ return @rails_friendly_includes_schema if @rails_friendly_includes_schema
160
+
161
+ nested_hashes_schema = {}
162
+ # Ignore last value in path and build nested hash from paths, e.g. paths
163
+ # [[:hardware, :vm_or_template, :ems_ref], [:description] will be transformed to
164
+ # [[:hardware, :vm_or_template]], so only relations we need for DB join and then to nested hash
165
+ # {:hardware => {:vm_or_template => {}}}
166
+ paths.map { |x| x[0..-2] }.select(&:present?).each { |x| nested_hashes_schema.store_path(x, {}) }
167
+ # Convert nested Hash to Rails friendly format, e.g. {:hardware => {:vm_or_template => {}}} will be
168
+ # transformed to [:hardware => :vm_or_template]
169
+ @rails_friendly_includes_schema = transform_hash_to_rails_friendly_array_recursive(nested_hashes_schema, [])
170
+ end
171
+
172
+ # @param hash [Hash] Nested hash representing join schema e.g. {:hardware => {:vm_or_template => {}}}
173
+ # @param current_layer [Array] One layer of the joins schema
174
+ # @return [Array] Transformed hash applicable for Rails .joins, e.g. [:hardware => [:vm_or_template]]
175
+ def transform_hash_to_rails_friendly_array_recursive(hash, current_layer)
176
+ array_attributes, hash_attributes = hash.partition { |_key, value| value.blank? }
177
+
178
+ array_attributes = array_attributes.map(&:first)
179
+ current_layer.concat(array_attributes)
180
+ # current_array.concat(array_attributes)
181
+ if hash_attributes.present?
182
+ last_hash_attr = hash_attributes.each_with_object({}) do |(key, value), obj|
183
+ obj[key] = transform_hash_to_rails_friendly_array_recursive(value, [])
184
+ end
185
+ current_layer << last_hash_attr
186
+ end
187
+
188
+ current_layer
189
+ end
190
+
191
+ # A recursive method for getting a schema out of full_reference (see #get_schema)
192
+ #
193
+ # @param attribute_names [Array<Symbol>] Array of attribute names
194
+ # @param arel_table [Arel::Table]
195
+ # @param data [Hash] The full reference layer
196
+ # @param schema [Hash] Recursively built schema
197
+ # @param path [Array] Recursively build path
198
+ # @param total_level [Integer] Guard for max recursive nesting
199
+ # @return [Hash] Recursively built schema
200
+ def get_schema_recursive(attribute_names, arel_table, data, schema, path, total_level)
201
+ raise "Nested too deep" if total_level > 100
202
+
203
+ attribute_names.each do |key|
204
+ new_path = path + [key]
205
+
206
+ value = data[key]
207
+
208
+ if inventory_object?(value)
209
+ get_schema_recursive(value.inventory_collection.manager_ref,
210
+ value.inventory_collection.model_class.arel_table,
211
+ value,
212
+ schema,
213
+ new_path,
214
+ total_level + 1)
215
+ elsif inventory_object_lazy?(value)
216
+ get_schema_recursive(value.inventory_collection.index_proxy.named_ref(value.ref),
217
+ value.inventory_collection.model_class.arel_table,
218
+ value.reference.full_reference,
219
+ schema,
220
+ new_path,
221
+ total_level + 1)
222
+ else
223
+ schema[new_path] = arel_table[key]
224
+ end
225
+ end
226
+
227
+ schema
228
+ end
229
+
230
+ # Returns keys of the reference
231
+ #
232
+ # @return [Array] Keys of the reference
233
+ def index_references
234
+ Set.new(references[index_name].try(:keys) || [])
235
+ end
236
+
237
+ # Return a Rails relation that will be used to obtain the records we need to load from the DB
238
+ #
239
+ # @param rails_friendly_includes_schema [Array] Schema usable in .includes and .references methods of
240
+ # ActiveRecord relation object
241
+ # @param all_values [Array<Array>] nested array of values in format [[a1, b1], [a2, b2]] the nested array
242
+ # values must have the order of column_names
243
+ # @param projection [Array] A projection array resulting in Project operation (in Relation algebra terms)
244
+ # @return [ActiveRecord::AssociationRelation] relation object having filtered data
245
+ def db_relation(rails_friendly_includes_schema, all_values = nil, projection = nil)
246
+ relation = if !parent.nil? && !association.nil?
247
+ parent.send(association)
248
+ elsif !arel.nil?
249
+ arel
250
+ end
251
+ relation = relation.where(build_multi_selection_condition(all_values)) if relation && all_values
252
+ relation = relation.select(projection) if relation && projection
253
+ relation = relation.includes(rails_friendly_includes_schema).references(rails_friendly_includes_schema) if rails_friendly_includes_schema.present?
254
+ relation || model_class.none
255
+ end
256
+
257
+ # Takes ApplicationRecord record, converts it to the InventoryObject and places it to index
258
+ #
259
+ # @param record [ApplicationRecord] ApplicationRecord record we want to place to the index
260
+ def process_db_record!(record, paths)
261
+ # Important fact is that the path was added as .includes in the query, so this doesn't generate n+1 queries
262
+ index_value = InventoryRefresh::InventoryCollection::Reference.stringify_reference(
263
+ paths.map { |path| fetch_object_path(path, record) }
264
+ )
265
+
266
+ attributes = record.attributes.symbolize_keys
267
+ attribute_references.each do |ref|
268
+ # We need to fill all references that are relations, we will use a InventoryRefresh::ApplicationRecordReference which
269
+ # can be used for filling a relation and we don't need to do any query here.
270
+ # TODO(lsmola) maybe loading all, not just referenced here? Otherwise this will have issue for db_cache_all
271
+ # and find used in parser
272
+ # TODO(lsmola) the last usage of this should be lazy_find_by with :key specified, maybe we can get rid of this?
273
+ next unless (foreign_key = association_to_foreign_key_mapping[ref])
274
+ base_class_name = attributes[association_to_foreign_type_mapping[ref].try(:to_sym)] || association_to_base_class_mapping[ref]
275
+ id = attributes[foreign_key.to_sym]
276
+ attributes[ref] = InventoryRefresh::ApplicationRecordReference.new(base_class_name, id)
277
+ end
278
+
279
+ index[index_value] = new_inventory_object(attributes)
280
+ index[index_value].id = record.id
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,116 @@
1
+ require "inventory_refresh/inventory_collection/index/type/base"
2
+ require "active_support/core_ext/module/delegation"
3
+
4
+ module InventoryRefresh
5
+ class InventoryCollection
6
+ module Index
7
+ module Type
8
+ class Skeletal < InventoryRefresh::InventoryCollection::Index::Type::Base
9
+ # (see InventoryRefresh::InventoryCollection::Index::Type::Base#initialize)
10
+ # @param primary_index [InventoryRefresh::InventoryCollection::Index::Type::Data] Data index of primary_index
11
+ def initialize(inventory_collection, index_name, attribute_names, primary_index)
12
+ super
13
+
14
+ @primary_index = primary_index
15
+ end
16
+
17
+ delegate :default_values,
18
+ :new_inventory_object,
19
+ :named_ref,
20
+ :to => :inventory_collection
21
+
22
+ delegate :blank?,
23
+ :each,
24
+ :each_value,
25
+ :to => :index
26
+
27
+ # Find value based on index_value
28
+ #
29
+ # @param index_value [String] a index_value of the InventoryObject we search for
30
+ # @return [InventoryObject|nil] Returns found value or nil
31
+ def find(index_value)
32
+ index[index_value]
33
+ end
34
+
35
+ # Deletes and returns the value on the index_value
36
+ #
37
+ # @param index_value [String] a index_value of the InventoryObject we search for
38
+ # @return [InventoryObject|nil] Returns found value or nil
39
+ def delete(index_value)
40
+ index.delete(index_value)
41
+ end
42
+
43
+ # Takes value from primary_index and inserts it to skeletal index
44
+ #
45
+ # @param index_value [String] a index_value of the InventoryObject we search for
46
+ # @return [InventoryObject|nil] Returns found value or nil
47
+ def skeletonize_primary_index(index_value)
48
+ inventory_object = primary_index.delete(index_value)
49
+ return unless inventory_object
50
+ fill_versions!(inventory_object.data)
51
+
52
+ index[index_value] = inventory_object
53
+ end
54
+
55
+ # Builds index record with skeletal InventoryObject and returns it. Or it returns existing InventoryObject
56
+ # that is found in primary_index or skeletal_primary_index.
57
+ #
58
+ # @param attributes [Hash] Skeletal data of the index, must contain unique index keys and everything else
59
+ # needed for creating the record in the Database
60
+ # @return [InventoryObject] Returns built InventoryObject or existing InventoryObject with new attributes
61
+ # assigned
62
+ def build(attributes)
63
+ attributes = {}.merge!(default_values).merge!(attributes)
64
+ fill_versions!(attributes)
65
+
66
+ # If the primary index is already filled, we don't want populate skeletal index
67
+ uuid = ::InventoryRefresh::InventoryCollection::Reference.build_stringified_reference(attributes, named_ref)
68
+ if (inventory_object = primary_index.find(uuid))
69
+ return inventory_object.assign_attributes(attributes)
70
+ end
71
+
72
+ # Return if skeletal index already exists
73
+ if (inventory_object = index[uuid])
74
+ return inventory_object.assign_attributes(attributes)
75
+ end
76
+
77
+ # We want to populate a new skeletal index
78
+ inventory_object = new_inventory_object(attributes)
79
+ index[inventory_object.manager_uuid] = inventory_object
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :primary_index
85
+
86
+ # Add versions columns into the passed attributes
87
+ #
88
+ # @param attributes [Hash] Attributes we want to extend with version related attributes
89
+ def fill_versions!(attributes)
90
+ if inventory_collection.supports_resource_timestamps_max? && attributes[:resource_timestamp]
91
+ fill_specific_version_attr(:resource_timestamps, :resource_timestamp, attributes)
92
+ elsif inventory_collection.supports_resource_versions_max? && attributes[:resource_version]
93
+ fill_specific_version_attr(:resource_versions, :resource_version, attributes)
94
+ end
95
+ end
96
+
97
+ # Add specific versions columns into the passed attributes
98
+ #
99
+ # @param partial_row_version_attr [Symbol] Attr name for partial rows, allowed values are
100
+ # [:resource_timestamps, :resource_versions]
101
+ # @param full_row_version_attr [Symbol] Attr name for full rows, allowed values are
102
+ # [:resource_timestamp, :resource_version]
103
+ # @param attributes [Hash] Attributes we want to extend with version related attributes
104
+ def fill_specific_version_attr(partial_row_version_attr, full_row_version_attr, attributes)
105
+ # We have to symbolize, since serializing persistor makes these strings
106
+ (attributes[partial_row_version_attr] ||= {}).symbolize_keys!
107
+
108
+ (attributes.keys - inventory_collection.base_columns).each do |key|
109
+ attributes[partial_row_version_attr][key] ||= attributes[full_row_version_attr]
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end