inventory_refresh 0.3.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +25 -30
  3. data/.github/workflows/ci.yaml +47 -0
  4. data/.rubocop.yml +3 -3
  5. data/.rubocop_cc.yml +3 -4
  6. data/.rubocop_local.yml +5 -2
  7. data/.whitesource +3 -0
  8. data/CHANGELOG.md +19 -0
  9. data/Gemfile +10 -4
  10. data/README.md +1 -2
  11. data/Rakefile +2 -2
  12. data/inventory_refresh.gemspec +9 -10
  13. data/lib/inventory_refresh/application_record_iterator.rb +25 -12
  14. data/lib/inventory_refresh/graph/topological_sort.rb +24 -26
  15. data/lib/inventory_refresh/graph.rb +2 -2
  16. data/lib/inventory_refresh/inventory_collection/builder.rb +37 -15
  17. data/lib/inventory_refresh/inventory_collection/data_storage.rb +9 -0
  18. data/lib/inventory_refresh/inventory_collection/helpers/initialize_helper.rb +147 -38
  19. data/lib/inventory_refresh/inventory_collection/helpers/questions_helper.rb +49 -5
  20. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +35 -3
  21. data/lib/inventory_refresh/inventory_collection/index/type/base.rb +8 -0
  22. data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +2 -0
  23. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +1 -0
  24. data/lib/inventory_refresh/inventory_collection/reference.rb +1 -0
  25. data/lib/inventory_refresh/inventory_collection/references_storage.rb +17 -0
  26. data/lib/inventory_refresh/inventory_collection/scanner.rb +91 -3
  27. data/lib/inventory_refresh/inventory_collection/serialization.rb +16 -10
  28. data/lib/inventory_refresh/inventory_collection.rb +122 -64
  29. data/lib/inventory_refresh/inventory_object.rb +74 -40
  30. data/lib/inventory_refresh/inventory_object_lazy.rb +17 -10
  31. data/lib/inventory_refresh/null_logger.rb +2 -2
  32. data/lib/inventory_refresh/persister.rb +31 -65
  33. data/lib/inventory_refresh/save_collection/base.rb +4 -2
  34. data/lib/inventory_refresh/save_collection/saver/base.rb +114 -15
  35. data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
  36. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +129 -51
  37. data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
  38. data/lib/inventory_refresh/save_collection/saver/partial_upsert_helper.rb +2 -19
  39. data/lib/inventory_refresh/save_collection/saver/retention_helper.rb +68 -3
  40. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +125 -0
  41. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +10 -6
  42. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +28 -16
  43. data/lib/inventory_refresh/save_collection/sweeper.rb +17 -93
  44. data/lib/inventory_refresh/save_collection/topological_sort.rb +5 -5
  45. data/lib/inventory_refresh/save_inventory.rb +5 -12
  46. data/lib/inventory_refresh/target.rb +73 -0
  47. data/lib/inventory_refresh/target_collection.rb +92 -0
  48. data/lib/inventory_refresh/version.rb +1 -1
  49. data/lib/inventory_refresh.rb +2 -0
  50. metadata +42 -39
  51. data/.travis.yml +0 -23
  52. data/lib/inventory_refresh/exception.rb +0 -8
@@ -42,12 +42,64 @@ module InventoryRefresh
42
42
  # Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
43
43
  # values of the foreign keys (even the polymorphic ones)
44
44
  #
45
- # @param data [Array<Object>] Array of objects that we want to map to DB table columns
45
+ # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
46
+ # @return [Hash] Data in DB format
47
+ def attributes(inventory_collection_scope = nil)
48
+ # We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
49
+ # different blacklist and whitelist. The generic code always passes a scope.
50
+ inventory_collection_scope ||= inventory_collection
51
+
52
+ attributes_for_saving = {}
53
+ # First transform the values
54
+ data.each do |key, value|
55
+ if !allowed?(inventory_collection_scope, key)
56
+ next
57
+ elsif value.kind_of?(Array) && value.any? { |x| loadable?(x) }
58
+ # Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
59
+ # result
60
+ data[key] = value.compact.map(&:load).compact
61
+ # We can use built in _ids methods to assign array of ids into has_many relations. So e.g. the :key_pairs=
62
+ # relation setter will become :key_pair_ids=
63
+ attributes_for_saving["#{key.to_s.singularize}_ids".to_sym] = data[key].map(&:id).compact.uniq
64
+ elsif loadable?(value) || inventory_collection_scope.association_to_foreign_key_mapping[key]
65
+ # Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
66
+ # result
67
+ data[key] = value.load if value.respond_to?(:load)
68
+ if (foreign_key = inventory_collection_scope.association_to_foreign_key_mapping[key])
69
+ # We have an association to fill, lets fill also the :key, cause some other InventoryObject can refer to it
70
+ record_id = data[key].try(:id)
71
+ attributes_for_saving[foreign_key.to_sym] = record_id
72
+
73
+ if (foreign_type = inventory_collection_scope.association_to_foreign_type_mapping[key])
74
+ # If we have a polymorphic association, we need to also fill a base class name, but we want to nullify it
75
+ # if record_id is missing
76
+ base_class = data[key].try(:base_class_name) || data[key].class.try(:base_class).try(:name)
77
+ attributes_for_saving[foreign_type.to_sym] = record_id ? base_class : nil
78
+ end
79
+ elsif data[key].kind_of?(::InventoryRefresh::InventoryObject)
80
+ # We have an association to fill but not an Activerecord association, so e.g. Ancestry, lets just load
81
+ # it here. This way of storing ancestry is ineffective in DB call count, but RAM friendly
82
+ attributes_for_saving[key.to_sym] = data[key].base_class_name.constantize.find_by(:id => data[key].id)
83
+ else
84
+ # We have a normal attribute to fill
85
+ attributes_for_saving[key.to_sym] = data[key]
86
+ end
87
+ else
88
+ attributes_for_saving[key.to_sym] = value
89
+ end
90
+ end
91
+
92
+ attributes_for_saving
93
+ end
94
+
95
+ # Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
96
+ # values of the foreign keys (even the polymorphic ones)
97
+ #
46
98
  # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
47
99
  # @param all_attribute_keys [Array<Symbol>] Attribute keys we will modify based on object's data
48
100
  # @param inventory_object [InventoryRefresh::InventoryObject] InventoryObject object owning these attributes
49
101
  # @return [Hash] Data in DB format
50
- def self.attributes_with_keys(data, inventory_collection_scope = nil, all_attribute_keys = [], inventory_object = nil)
102
+ def attributes_with_keys(inventory_collection_scope = nil, all_attribute_keys = [], inventory_object = nil)
51
103
  # We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
52
104
  # different blacklist and whitelist. The generic code always passes a scope.
53
105
  inventory_collection_scope ||= inventory_collection
@@ -96,8 +148,8 @@ module InventoryRefresh
96
148
  def assign_attributes(attributes)
97
149
  attributes.each do |k, v|
98
150
  # We don't want timestamps or resource versions to be overwritten here, since those are driving the conditions
99
- next if %i(resource_timestamps resource_timestamps_max resource_timestamp).include?(k)
100
- next if %i(resource_counters resource_counters_max resource_counter).include?(k)
151
+ next if %i[resource_timestamps resource_timestamps_max resource_timestamp].include?(k)
152
+ next if %i[resource_counters resource_counters_max resource_counter].include?(k)
101
153
 
102
154
  if data[:resource_timestamp] && attributes[:resource_timestamp]
103
155
  assign_only_newest(:resource_timestamp, :resource_timestamps, attributes, data, k, v)
@@ -146,44 +198,14 @@ module InventoryRefresh
146
198
  end
147
199
  end
148
200
 
149
- unless defined_methods.include?(attr.to_sym)
150
- define_method(attr) do
151
- data[attr]
152
- end
201
+ next if defined_methods.include?(attr.to_sym)
202
+
203
+ define_method(attr) do
204
+ data[attr]
153
205
  end
154
206
  end
155
207
  end
156
208
 
157
- # Return true if the attribute is allowed to be saved into the DB
158
- #
159
- # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
160
- # attribute
161
- # @param key [Symbol] attribute name
162
- # @return true if the attribute is allowed to be saved into the DB
163
- def self.allowed?(inventory_collection_scope, key)
164
- foreign_to_association = (inventory_collection_scope.foreign_key_to_association_mapping[key] ||
165
- inventory_collection_scope.foreign_type_to_association_mapping[key])
166
-
167
- return false if inventory_collection_scope.attributes_blacklist.present? &&
168
- (inventory_collection_scope.attributes_blacklist.include?(key) ||
169
- (foreign_to_association && inventory_collection_scope.attributes_blacklist.include?(foreign_to_association)))
170
-
171
- return false if inventory_collection_scope.attributes_whitelist.present? &&
172
- (!inventory_collection_scope.attributes_whitelist.include?(key) &&
173
- (!foreign_to_association || (foreign_to_association && inventory_collection_scope.attributes_whitelist.include?(foreign_to_association))))
174
-
175
- true
176
- end
177
-
178
- # Return true if the object is loadable, which we determine by a list of loadable classes.
179
- #
180
- # @param value [Object] object we test
181
- # @return true if the object is loadable
182
- def self.loadable?(value)
183
- value.kind_of?(::InventoryRefresh::InventoryObjectLazy) || value.kind_of?(::InventoryRefresh::InventoryObject) ||
184
- value.kind_of?(::InventoryRefresh::ApplicationRecordReference)
185
- end
186
-
187
209
  private
188
210
 
189
211
  # Assigns value based on the version attributes. If versions are specified, it asigns attribute only if it's
@@ -261,7 +283,18 @@ module InventoryRefresh
261
283
  # @param key [Symbol] attribute name
262
284
  # @return true if the attribute is allowed to be saved into the DB
263
285
  def allowed?(inventory_collection_scope, key)
264
- self.class.allowed?(inventory_collection_scope, key)
286
+ foreign_to_association = inventory_collection_scope.foreign_key_to_association_mapping[key] ||
287
+ inventory_collection_scope.foreign_type_to_association_mapping[key]
288
+
289
+ return false if inventory_collection_scope.attributes_blacklist.present? &&
290
+ (inventory_collection_scope.attributes_blacklist.include?(key) ||
291
+ (foreign_to_association && inventory_collection_scope.attributes_blacklist.include?(foreign_to_association)))
292
+
293
+ return false if inventory_collection_scope.attributes_whitelist.present? &&
294
+ (!inventory_collection_scope.attributes_whitelist.include?(key) &&
295
+ (!foreign_to_association || (foreign_to_association && inventory_collection_scope.attributes_whitelist.include?(foreign_to_association))))
296
+
297
+ true
265
298
  end
266
299
 
267
300
  # Return true if the object is loadable, which we determine by a list of loadable classes.
@@ -269,7 +302,8 @@ module InventoryRefresh
269
302
  # @param value [Object] object we test
270
303
  # @return true if the object is loadable
271
304
  def loadable?(value)
272
- self.class.loadable?(value)
305
+ value.kind_of?(::InventoryRefresh::InventoryObjectLazy) || value.kind_of?(::InventoryRefresh::InventoryObject) ||
306
+ value.kind_of?(::InventoryRefresh::ApplicationRecordReference)
273
307
  end
274
308
  end
275
309
  end
@@ -30,6 +30,7 @@ module InventoryRefresh
30
30
 
31
31
  # @return [String] stringified reference
32
32
  def to_s
33
+ # TODO(lsmola) do we need this method?
33
34
  stringified_reference
34
35
  end
35
36
 
@@ -70,6 +71,10 @@ module InventoryRefresh
70
71
 
71
72
  # @return [Boolean] true if the key is an association on inventory_collection_scope model class
72
73
  def association?(key)
74
+ # TODO(lsmola) remove this if there will be better dependency scan, probably with transitive dependencies filled
75
+ # in a second pass, then we can get rid of this hardcoded symbols. Right now we are not able to introspect these.
76
+ return true if [:parent, :genealogy_parent].include?(key)
77
+
73
78
  inventory_collection.dependency_attributes.key?(key) ||
74
79
  !inventory_collection.association_to_foreign_key_mapping[key].nil?
75
80
  end
@@ -95,7 +100,7 @@ module InventoryRefresh
95
100
 
96
101
  private
97
102
 
98
- delegate :saved?, :saver_strategy, :skeletal_primary_index, :to => :inventory_collection
103
+ delegate :parallel_safe?, :saved?, :saver_strategy, :skeletal_primary_index, :targeted?, :to => :inventory_collection
99
104
  delegate :nested_secondary_index?, :primary?, :full_reference, :keys, :primary?, :to => :reference
100
105
 
101
106
  attr_writer :reference
@@ -107,18 +112,20 @@ module InventoryRefresh
107
112
  #
108
113
  # @return [InventoryRefresh::InventoryObject, NilClass] Returns pre-created InventoryObject or nil
109
114
  def skeletal_precreate!
115
+ # We can do skeletal pre-create only for strategies using unique indexes. Since this can build records out of
116
+ # the given :arel scope, we will always attempt to create the recod, so we need unique index to avoid duplication
117
+ # of records.
118
+ return unless parallel_safe?
110
119
  # Pre-create only for strategies that will be persisting data, i.e. are not saved already
111
120
  return if saved?
112
121
  # We can only do skeletal pre-create for primary index reference, since that is needed to create DB unique index
113
- # and full reference must be present
114
- return if !primary? || full_reference.blank?
115
-
116
- # To avoid pre-creating invalid records all fields of a primary key must have non null value
117
- # TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this. We
118
- # will need to scan the DB for NOT NULL constraint and allow it based on that. So we would move this check to
119
- # saving code, but this will require bigger change, since having the column nil means we will have to batch it
120
- # smartly, since having nil means we will need to use different unique index for the upsert/update query.
121
- return if keys.any? { |x| full_reference[x].nil? }
122
+ return unless primary?
123
+ # Full reference must be present
124
+ return if full_reference.blank?
125
+
126
+ # To avoid pre-creating invalid records all fields of a primary key must have value
127
+ # TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this
128
+ return if keys.any? { |x| full_reference[x].blank? }
122
129
 
123
130
  skeletal_primary_index.build(full_reference)
124
131
  end
@@ -3,10 +3,10 @@ require "logger"
3
3
  module InventoryRefresh
4
4
  class NullLogger < Logger
5
5
  def initialize(*_args)
6
- end
6
+ end
7
7
 
8
8
  def add(*_args, &_block)
9
- end
9
+ end
10
10
 
11
11
  def debug?
12
12
  false
@@ -3,13 +3,15 @@ module InventoryRefresh
3
3
  require 'json'
4
4
  require 'yaml'
5
5
 
6
- attr_reader :manager, :collections
6
+ attr_reader :manager, :target, :collections
7
7
 
8
8
  attr_accessor :refresh_state_uuid, :refresh_state_part_uuid, :total_parts, :sweep_scope, :retry_count, :retry_max
9
9
 
10
10
  # @param manager [ManageIQ::Providers::BaseManager] A manager object
11
- def initialize(manager)
11
+ # @param target [Object] A refresh Target object
12
+ def initialize(manager, target = nil)
12
13
  @manager = manager
14
+ @target = target
13
15
 
14
16
  @collections = {}
15
17
 
@@ -43,6 +45,9 @@ module InventoryRefresh
43
45
  &block)
44
46
 
45
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
+
46
51
  builder.evaluate_lambdas!(self)
47
52
 
48
53
  collections[collection_name] = builder.to_inventory_collection
@@ -61,7 +66,7 @@ module InventoryRefresh
61
66
  # @return [InventoryRefresh::InventoryCollection] returns a defined InventoryCollection or undefined method
62
67
  def method_missing(method_name, *arguments, &block)
63
68
  if inventory_collections_names.include?(method_name)
64
- self.define_collections_reader(method_name)
69
+ define_collections_reader(method_name)
65
70
  send(method_name)
66
71
  else
67
72
  super
@@ -91,7 +96,7 @@ module InventoryRefresh
91
96
 
92
97
  # Returns serialized Persisted object to JSON
93
98
  # @return [String] serialized Persisted object to JSON
94
- def to_json
99
+ def to_json(*_args)
95
100
  JSON.dump(to_hash)
96
101
  end
97
102
 
@@ -99,6 +104,8 @@ module InventoryRefresh
99
104
  def to_hash
100
105
  collections_data = collections.map do |_, collection|
101
106
  next if collection.data.blank? &&
107
+ collection.targeted_scope.primary_references.blank? &&
108
+ collection.all_manager_uuids.nil? &&
102
109
  collection.skeletal_primary_index.index_data.blank?
103
110
 
104
111
  collection.to_hash
@@ -110,7 +117,7 @@ module InventoryRefresh
110
117
  :retry_count => retry_count,
111
118
  :retry_max => retry_max,
112
119
  :total_parts => total_parts,
113
- :sweep_scope => sweep_scope_to_hash(sweep_scope),
120
+ :sweep_scope => sweep_scope,
114
121
  :collections => collections_data,
115
122
  }
116
123
  end
@@ -120,19 +127,22 @@ module InventoryRefresh
120
127
  #
121
128
  # @param json_data [String] input JSON data
122
129
  # @return [ManageIQ::Providers::Inventory::Persister] Persister object loaded from a passed JSON
123
- def from_json(json_data, manager)
124
- from_hash(JSON.parse(json_data), manager)
130
+ def from_json(json_data, manager, target = nil)
131
+ from_hash(JSON.parse(json_data), manager, target)
125
132
  end
126
133
 
127
134
  # Returns Persister object built from serialized data
128
135
  #
129
136
  # @param persister_data [Hash] serialized Persister object in hash
130
137
  # @return [ManageIQ::Providers::Inventory::Persister] Persister object built from serialized data
131
- def from_hash(persister_data, manager)
132
- new(manager).tap do |persister|
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|
133
143
  persister_data['collections'].each do |collection|
134
144
  inventory_collection = persister.collections[collection['name'].try(:to_sym)]
135
- raise "Unrecognized InventoryCollection name: #{collection['name']}" if inventory_collection.blank?
145
+ raise "Unrecognized InventoryCollection name: #{inventory_collection}" if inventory_collection.blank?
136
146
 
137
147
  inventory_collection.from_hash(collection, persister.collections)
138
148
  end
@@ -142,44 +152,9 @@ module InventoryRefresh
142
152
  persister.retry_count = persister_data['retry_count']
143
153
  persister.retry_max = persister_data['retry_max']
144
154
  persister.total_parts = persister_data['total_parts']
145
- persister.sweep_scope = sweep_scope_from_hash(persister_data['sweep_scope'], persister.collections)
155
+ persister.sweep_scope = persister_data['sweep_scope']
146
156
  end
147
157
  end
148
-
149
- private
150
-
151
- def assert_sweep_scope!(sweep_scope)
152
- return unless sweep_scope
153
-
154
- allowed_format_message = "Allowed format of sweep scope is Array<String> or Hash{String => Hash}, got #{sweep_scope}"
155
-
156
- if sweep_scope.kind_of?(Array)
157
- return if sweep_scope.all? { |x| x.kind_of?(String) || x.kind_of?(Symbol) }
158
-
159
- raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
160
- elsif sweep_scope.kind_of?(Hash)
161
- return if sweep_scope.values.all? { |x| x.kind_of?(Array) }
162
-
163
- raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
164
- end
165
-
166
- raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
167
- end
168
-
169
- def sweep_scope_from_hash(sweep_scope, available_inventory_collections)
170
- assert_sweep_scope!(sweep_scope)
171
-
172
- return sweep_scope unless sweep_scope.kind_of?(Hash)
173
-
174
- sweep_scope.each_with_object({}) do |(k, v), obj|
175
- inventory_collection = available_inventory_collections[k.try(:to_sym)]
176
- raise "Unrecognized InventoryCollection name: #{k}" if inventory_collection.blank?
177
-
178
- serializer = InventoryRefresh::InventoryCollection::Serialization.new(inventory_collection)
179
-
180
- obj[k] = serializer.sweep_scope_from_hash(v, available_inventory_collections)
181
- end.symbolize_keys!
182
- end
183
158
  end
184
159
 
185
160
  protected
@@ -210,37 +185,28 @@ module InventoryRefresh
210
185
  nil
211
186
  end
212
187
 
213
- def assert_graph_integrity?
188
+ def saver_strategy
189
+ :default
190
+ end
191
+
192
+ # Persisters for targeted refresh can override to true
193
+ def targeted?
214
194
  false
215
195
  end
216
196
 
217
- def use_ar_object?
218
- true
197
+ def assert_graph_integrity?
198
+ false
219
199
  end
220
200
 
221
201
  # @return [Hash] kwargs shared for all InventoryCollection objects
222
202
  def shared_options
223
203
  {
204
+ :saver_strategy => saver_strategy,
224
205
  :strategy => strategy,
206
+ :targeted => targeted?,
225
207
  :parent => manager.presence,
226
208
  :assert_graph_integrity => assert_graph_integrity?,
227
- :use_ar_object => use_ar_object?,
228
209
  }
229
210
  end
230
-
231
- private
232
-
233
- def sweep_scope_to_hash(sweep_scope)
234
- return sweep_scope unless sweep_scope.kind_of?(Hash)
235
-
236
- sweep_scope.each_with_object({}) do |(k, v), obj|
237
- inventory_collection = collections[k.try(:to_sym)]
238
- raise "Unrecognized InventoryCollection name: #{k}" if inventory_collection.blank?
239
-
240
- serializer = InventoryRefresh::InventoryCollection::Serialization.new(inventory_collection)
241
-
242
- obj[k] = serializer.sweep_scope_to_hash(v)
243
- end
244
- end
245
211
  end
246
212
  end
@@ -1,5 +1,7 @@
1
1
  require "inventory_refresh/logging"
2
+ require "inventory_refresh/save_collection/saver/batch"
2
3
  require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
4
+ require "inventory_refresh/save_collection/saver/default"
3
5
 
4
6
  module InventoryRefresh::SaveCollection
5
7
  class Base
@@ -14,7 +16,7 @@ module InventoryRefresh::SaveCollection
14
16
  return if skip?(inventory_collection)
15
17
 
16
18
  logger.debug("----- BEGIN ----- Saving collection #{inventory_collection} of size #{inventory_collection.size} to"\
17
- " the database, for the manager: '#{ems.id}'...")
19
+ " the database, for the manager: '#{ems.name}'...")
18
20
 
19
21
  if inventory_collection.custom_save_block.present?
20
22
  logger.debug("Saving collection #{inventory_collection} using a custom save block")
@@ -22,7 +24,7 @@ module InventoryRefresh::SaveCollection
22
24
  else
23
25
  save_inventory(inventory_collection)
24
26
  end
25
- logger.debug("----- END ----- Saving collection #{inventory_collection}, for the manager: '#{ems.id}'...Complete")
27
+ logger.debug("----- END ----- Saving collection #{inventory_collection}, for the manager: '#{ems.name}'...Complete")
26
28
  inventory_collection.saved = true
27
29
  end
28
30
 
@@ -12,6 +12,8 @@ module InventoryRefresh::SaveCollection
12
12
  # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we will be saving
13
13
  def initialize(inventory_collection)
14
14
  @inventory_collection = inventory_collection
15
+ # TODO(lsmola) do I need to reload every time? Also it should be enough to clear the associations.
16
+ inventory_collection.parent&.reload
15
17
  @association = inventory_collection.db_collection_for_comparison
16
18
 
17
19
  # Private attrs
@@ -19,17 +21,21 @@ module InventoryRefresh::SaveCollection
19
21
  @table_name = @model_class.table_name
20
22
  @q_table_name = get_connection.quote_table_name(@table_name)
21
23
  @primary_key = @model_class.primary_key
22
- @arel_primary_key = @model_class.arel_attribute(@primary_key)
24
+ @arel_primary_key = @model_class.arel_table[@primary_key]
23
25
  @unique_index_keys = inventory_collection.unique_index_keys
24
26
  @unique_index_keys_to_s = inventory_collection.manager_ref_to_cols.map(&:to_s)
25
27
  @select_keys = [@primary_key] + @unique_index_keys_to_s + internal_columns.map(&:to_s)
26
28
  @unique_db_primary_keys = Set.new
27
29
  @unique_db_indexes = Set.new
28
30
 
31
+ # Right now ApplicationRecordIterator in association is used for targeted refresh. Given the small amount of
32
+ # records flowing through there, we probably don't need to optimize that association to fetch a pure SQL.
33
+ @pure_sql_records_fetching = !inventory_collection.use_ar_object? && !@association.kind_of?(InventoryRefresh::ApplicationRecordIterator)
34
+
29
35
  @batch_size_for_persisting = inventory_collection.batch_size_pure_sql
30
- @batch_size = inventory_collection.use_ar_object? ? @batch_size_for_persisting : inventory_collection.batch_size
31
36
 
32
- @record_key_method = inventory_collection.pure_sql_record_fetching? ? :pure_sql_record_key : :ar_record_key
37
+ @batch_size = @pure_sql_records_fetching ? @batch_size_for_persisting : inventory_collection.batch_size
38
+ @record_key_method = @pure_sql_records_fetching ? :pure_sql_record_key : :ar_record_key
33
39
  @select_keys_indexes = @select_keys.each_with_object({}).with_index { |(key, obj), index| obj[key.to_s] = index }
34
40
  @pg_types = @model_class.attribute_names.each_with_object({}) do |key, obj|
35
41
  obj[key.to_sym] = inventory_collection.model_class.columns_hash[key]
@@ -69,8 +75,14 @@ module InventoryRefresh::SaveCollection
69
75
 
70
76
  # Saves the InventoryCollection
71
77
  def save_inventory_collection!
78
+ # If we have a targeted InventoryCollection that wouldn't do anything, quickly skip it
79
+ return if inventory_collection.noop?
80
+
81
+ # Delete_complement strategy using :all_manager_uuids attribute
82
+ delete_complement unless inventory_collection.delete_complement_noop?
83
+
72
84
  # Create/Update/Archive/Delete records based on InventoryCollection data and scope
73
- save!(association)
85
+ save!(association) unless inventory_collection.saving_noop?
74
86
  end
75
87
 
76
88
  protected
@@ -89,8 +101,6 @@ module InventoryRefresh::SaveCollection
89
101
  # @param attributes [Hash] attributes hash
90
102
  # @return [Hash] modified hash from parameter attributes with casted values
91
103
  def values_for_database!(all_attribute_keys, attributes)
92
- # TODO(lsmola) we'll need to fill default value from the DB to the NOT_NULL columns here, since sending NULL
93
- # to column with NOT_NULL constraint always fails, even if there is a default value
94
104
  all_attribute_keys.each do |key|
95
105
  next unless attributes.key?(key)
96
106
 
@@ -102,7 +112,11 @@ module InventoryRefresh::SaveCollection
102
112
  end
103
113
 
104
114
  def transform_to_hash!(all_attribute_keys, hash)
105
- if serializable_keys?
115
+ if inventory_collection.use_ar_object?
116
+ record = inventory_collection.model_class.new(hash)
117
+ values_for_database!(all_attribute_keys,
118
+ record.attributes.slice(*record.changed_attributes.keys).symbolize_keys)
119
+ elsif serializable_keys?
106
120
  values_for_database!(all_attribute_keys,
107
121
  hash)
108
122
  else
@@ -113,15 +127,99 @@ module InventoryRefresh::SaveCollection
113
127
  private
114
128
 
115
129
  attr_reader :unique_index_keys, :unique_index_keys_to_s, :select_keys, :unique_db_primary_keys, :unique_db_indexes,
116
- :primary_key, :arel_primary_key, :record_key_method, :select_keys_indexes,
130
+ :primary_key, :arel_primary_key, :record_key_method, :pure_sql_records_fetching, :select_keys_indexes,
117
131
  :batch_size, :batch_size_for_persisting, :model_class, :serializable_keys, :deserializable_keys, :pg_types, :table_name,
118
132
  :q_table_name
119
133
 
120
134
  delegate :supports_column?, :to => :inventory_collection
121
135
 
136
+ # Saves the InventoryCollection
137
+ #
138
+ # @param association [Symbol] An existing association on manager
139
+ def save!(association)
140
+ attributes_index = {}
141
+ inventory_objects_index = {}
142
+ inventory_collection.each do |inventory_object|
143
+ attributes = inventory_object.attributes(inventory_collection)
144
+ index = build_stringified_reference(attributes, unique_index_keys)
145
+
146
+ attributes_index[index] = attributes
147
+ inventory_objects_index[index] = inventory_object
148
+ end
149
+
150
+ logger.debug("Processing #{inventory_collection} of size #{inventory_collection.size}...")
151
+ # Records that are in the DB, we will be updating or deleting them.
152
+ ActiveRecord::Base.transaction do
153
+ association.find_each do |record|
154
+ index = build_stringified_reference_for_record(record, unique_index_keys)
155
+
156
+ next unless assert_distinct_relation(record.id)
157
+ next unless assert_unique_record(record, index)
158
+
159
+ inventory_object = inventory_objects_index.delete(index)
160
+ hash = attributes_index.delete(index)
161
+
162
+ if inventory_object.nil?
163
+ # Record was found in the DB but not sent for saving, that means it doesn't exist anymore and we should
164
+ # delete it from the DB.
165
+ delete_record!(record) if inventory_collection.delete_allowed?
166
+ elsif assert_referential_integrity(hash)
167
+ # Record was found in the DB and sent for saving, we will be updating the DB.
168
+ update_record!(record, hash, inventory_object)
169
+ end
170
+ end
171
+ end
172
+
173
+ inventory_collection.custom_reconnect_block&.call(inventory_collection, inventory_objects_index, attributes_index)
174
+
175
+ # Records that were not found in the DB but sent for saving, we will be creating these in the DB.
176
+ if inventory_collection.create_allowed?
177
+ ActiveRecord::Base.transaction do
178
+ inventory_objects_index.each do |index, inventory_object|
179
+ hash = attributes_index.delete(index)
180
+
181
+ create_record!(hash, inventory_object) if assert_referential_integrity(hash)
182
+ end
183
+ end
184
+ end
185
+ logger.debug("Processing #{inventory_collection}, "\
186
+ "created=#{inventory_collection.created_records.count}, "\
187
+ "updated=#{inventory_collection.updated_records.count}, "\
188
+ "deleted=#{inventory_collection.deleted_records.count}...Complete")
189
+ rescue => e
190
+ logger.error("Error when saving #{inventory_collection} with #{inventory_collection_details}. Message: #{e.message}")
191
+ raise e
192
+ end
193
+
122
194
  # @return [String] a string for logging purposes
123
195
  def inventory_collection_details
124
- "strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}"
196
+ "strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}, targeted: #{inventory_collection.targeted?}"
197
+ end
198
+
199
+ # @param record [ApplicationRecord] ApplicationRecord object
200
+ # @param key [Symbol] A key that is an attribute of the AR object
201
+ # @return [Object] Value of attribute name :key on the :record
202
+ def record_key(record, key)
203
+ record.public_send(key)
204
+ end
205
+
206
+ # Deletes a complement of referenced data
207
+ def delete_complement
208
+ raise(":delete_complement method is supported only for :saver_strategy => [:batch, :concurrent_safe_batch]")
209
+ end
210
+
211
+ # Deletes/soft-deletes a given record
212
+ #
213
+ # @param [ApplicationRecord] record we want to delete
214
+ def delete_record!(record)
215
+ record.public_send(inventory_collection.delete_method)
216
+ inventory_collection.store_deleted_records(record)
217
+ end
218
+
219
+ # @return [TrueClass] always return true, this method is redefined in default saver
220
+ def assert_unique_record(_record, _index)
221
+ # TODO(lsmola) can go away once we indexed our DB with unique indexes
222
+ true
125
223
  end
126
224
 
127
225
  # Check if relation provided is distinct, i.e. the relation should not return the same primary key value twice.
@@ -134,12 +232,12 @@ module InventoryRefresh::SaveCollection
134
232
  # Change the InventoryCollection's :association or :arel parameter to return distinct results. The :through
135
233
  # relations can return the same record multiple times. We don't want to do SELECT DISTINCT by default, since
136
234
  # it can be very slow.
137
- unless inventory_collection.assert_graph_integrity
235
+ if inventory_collection.assert_graph_integrity
236
+ raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ")
237
+ else
138
238
  logger.warn("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. "\
139
239
  " The duplicate value is being ignored.")
140
240
  return false
141
- else
142
- raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ")
143
241
  end
144
242
  else
145
243
  unique_db_primary_keys << primary_key_value
@@ -157,14 +255,15 @@ module InventoryRefresh::SaveCollection
157
255
  def assert_referential_integrity(hash)
158
256
  inventory_collection.fixed_foreign_keys.each do |x|
159
257
  next unless hash[x].nil?
258
+
160
259
  subject = "#{hash} of #{inventory_collection} because of missing foreign key #{x} for "\
161
260
  "#{inventory_collection.parent.class.name}:"\
162
261
  "#{inventory_collection.parent.try(:id)}"
163
- unless inventory_collection.assert_graph_integrity
262
+ if inventory_collection.assert_graph_integrity
263
+ raise("Referential integrity check violated for #{subject}")
264
+ else
164
265
  logger.warn("Referential integrity check violated, ignoring #{subject}")
165
266
  return false
166
- else
167
- raise("Referential integrity check violated for #{subject}")
168
267
  end
169
268
  end
170
269
  true
@@ -0,0 +1,17 @@
1
+ require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
2
+
3
+ module InventoryRefresh::SaveCollection
4
+ module Saver
5
+ class Batch < InventoryRefresh::SaveCollection::Saver::ConcurrentSafeBatch
6
+ private
7
+
8
+ # Just returning manager ref transformed to column names, for strategies that do not expect to have unique DB
9
+ # indexes.
10
+ #
11
+ # @return [Array<Symbol>] manager ref transformed to column names
12
+ def unique_index_columns
13
+ inventory_collection.manager_ref_to_cols.map(&:to_sym)
14
+ end
15
+ end
16
+ end
17
+ end