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,34 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "inventory_refresh/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "inventory_refresh"
8
+ spec.version = InventoryRefresh::VERSION
9
+ spec.authors = ["ManageIQ Developers"]
10
+
11
+ spec.summary = %q{Topological Inventory Persister}
12
+ spec.description = %q{Topological Inventory Persister}
13
+ spec.homepage = "https://github.com/ManageIQ/inventory_refresh"
14
+ spec.licenses = ["Apache-2.0"]
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "activerecord", "~> 5.0.6"
26
+ spec.add_dependency "more_core_extensions", "~> 3.5"
27
+ spec.add_dependency "pg", "~> 0.18.2"
28
+
29
+ spec.add_development_dependency "ancestry"
30
+ spec.add_development_dependency "bundler", "~> 1.16"
31
+ spec.add_development_dependency "factory_girl", "~> 4.5.0"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end
@@ -0,0 +1,11 @@
1
+ require "inventory_refresh/graph"
2
+ require "inventory_refresh/inventory_collection"
3
+ require "inventory_refresh/inventory_object"
4
+ require "inventory_refresh/inventory_object_lazy"
5
+ require "inventory_refresh/save_inventory"
6
+ require "inventory_refresh/target"
7
+ require "inventory_refresh/target_collection"
8
+ require "inventory_refresh/version"
9
+
10
+ module InventoryRefresh
11
+ end
@@ -0,0 +1,56 @@
1
+ module InventoryRefresh
2
+ class ApplicationRecordIterator
3
+ attr_reader :inventory_collection, :manager_uuids_set, :iterator, :query
4
+
5
+ # An iterator that can fetch batches of the AR objects based on a set of manager refs, or just mimics AR relation
6
+ # when given an iterator. Or given query, acts as iterator by selecting batches.
7
+ #
8
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] Inventory collection owning the iterator
9
+ # @param manager_uuids_set [Array<InventoryRefresh::InventoryCollection::Reference>] Array of references we want to
10
+ # fetch from the DB
11
+ # @param iterator [Proc] Block based iterator
12
+ # @query query [ActiveRecord::Relation] Existing query we want to use for querying the db
13
+ def initialize(inventory_collection: nil, manager_uuids_set: nil, iterator: nil, query: nil)
14
+ @inventory_collection = inventory_collection
15
+ @manager_uuids_set = manager_uuids_set
16
+ @iterator = iterator
17
+ @query = query
18
+ end
19
+
20
+ # Iterator that mimics find_in_batches of ActiveRecord::Relation. This iterator serves for making more optimized query
21
+ # since e.g. having 1500 ids if objects we want to return. Doing relation.where(:id => 1500ids).find_each would
22
+ # always search for all 1500 ids, then return on limit 1000.
23
+ #
24
+ # With this iterator we build queries using only batch of ids, so find_each will cause relation.where(:id => 1000ids)
25
+ # and relation.where(:id => 500ids)
26
+ #
27
+ # @param batch_size [Integer] A batch size we want to fetch from DB
28
+ # @yield Code processing the batches
29
+ def find_in_batches(batch_size: 1000)
30
+ if iterator
31
+ iterator.call do |batch|
32
+ yield(batch)
33
+ end
34
+ elsif query
35
+ manager_uuids_set.each_slice(batch_size) do |batch|
36
+ yield(query.where(inventory_collection.targeted_selection_for(batch)))
37
+ end
38
+ else
39
+ manager_uuids_set.each_slice(batch_size) do |batch|
40
+ yield(inventory_collection.db_collection_for_comparison_for(batch))
41
+ end
42
+ end
43
+ end
44
+
45
+ # Iterator that mimics find_each of ActiveRecord::Relation using find_in_batches (see #find_in_batches)
46
+ #
47
+ # @yield Code processing the batches
48
+ def find_each
49
+ find_in_batches do |batch|
50
+ batch.each do |item|
51
+ yield(item)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module InventoryRefresh
2
+ class ApplicationRecordReference
3
+ attr_reader :base_class_name, :id
4
+
5
+ # ApplicationRecord is very bloaty in memory, so this class server for storing base_class and primary key
6
+ # of the ApplicationRecord, which is just enough for filling up relationships
7
+ #
8
+ # @param base_class_name [String] A base class of the ApplicationRecord object
9
+ # @param id [Bigint] Primary key value of the ApplicationRecord object
10
+ def initialize(base_class_name, id)
11
+ @base_class_name = base_class_name
12
+ @id = id
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,157 @@
1
+ require "inventory_refresh/graph/topological_sort"
2
+
3
+ module InventoryRefresh
4
+ class Graph
5
+ attr_reader :nodes, :edges, :fixed_edges
6
+
7
+ # @param nodes [Array<InventoryRefresh::InventoryCollection>] List of Inventory collection nodes
8
+ def initialize(nodes)
9
+ @nodes = nodes
10
+ @edges = []
11
+ @fixed_edges = []
12
+
13
+ construct_graph!(@nodes)
14
+ end
15
+
16
+ # Returns graph in GraphViz format, as a string. So it can be displayed.
17
+ #
18
+ # @param layers [Array<Array>] Array of arrays(layers) of InventoryCollection objects
19
+ # @return [String] Graph in GraphViz format
20
+ def to_graphviz(layers: nil)
21
+ node_names = friendly_unique_node_names
22
+ s = []
23
+
24
+ s << "digraph {"
25
+ (layers || [nodes]).each_with_index do |layer_nodes, i|
26
+ s << " subgraph cluster_#{i} { label = \"Layer #{i}\";" unless layers.nil?
27
+
28
+ layer_nodes.each do |n|
29
+ s << " #{node_names[n]}; \t// #{n.inspect}"
30
+ end
31
+
32
+ s << " }" unless layers.nil?
33
+ end
34
+
35
+ s << " // edges:"
36
+ edges.each do |from, to|
37
+ s << " #{node_names[from]} -> #{node_names[to]};"
38
+ end
39
+ s << "}"
40
+ s.join("\n") + "\n"
41
+ end
42
+
43
+ protected
44
+
45
+ attr_writer :nodes, :edges, :fixed_edges
46
+
47
+ # Given array of InventoryCollection objects as nodes, we construct a graph with nodes and edges
48
+ #
49
+ # @param nodes [Array<InventoryRefresh::InventoryCollection>] List of Inventory collection nodes
50
+ # @return [InventoryRefresh::Graph] Constructed graph
51
+ def construct_graph!(nodes)
52
+ self.nodes = nodes
53
+ self.edges, self.fixed_edges = build_edges(nodes)
54
+ self
55
+ end
56
+
57
+ # Checks that there are no cycles in the graph
58
+ #
59
+ # @param fixed_edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
60
+ # fixed edges are those that can't be moved
61
+ def assert_graph!(fixed_edges)
62
+ fixed_edges.each do |edge|
63
+ detect_cycle(edge, fixed_edges - [edge], :exception)
64
+ end
65
+ end
66
+
67
+ # Builds a feedback edge set, which is a set of edges creating a cycle
68
+ #
69
+ # @param edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
70
+ # these are all edges except fixed_edges
71
+ # @param fixed_edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
72
+ # fixed edges are those that can't be moved
73
+ def build_feedback_edge_set(edges, fixed_edges)
74
+ edges = edges.dup
75
+ acyclic_edges = fixed_edges.dup
76
+ feedback_edge_set = []
77
+
78
+ while edges.present?
79
+ edge = edges.shift
80
+ if detect_cycle(edge, acyclic_edges)
81
+ feedback_edge_set << edge
82
+ else
83
+ acyclic_edges << edge
84
+ end
85
+ end
86
+
87
+ feedback_edge_set
88
+ end
89
+
90
+ # Detects a cycle. Based on escalation returns true or raises exception if there is a cycle
91
+ #
92
+ # @param edge [Array(InventoryRefresh::InventoryCollection, InventoryRefresh::InventoryCollection)] Edge we are
93
+ # inspecting for cycle
94
+ # @param acyclic_edges [Array<Array>] Starting with fixed edges that can't have cycle, these are edges without cycle
95
+ # @param escalation [Symbol] If :exception, this method throws exception when it finds a cycle
96
+ # @return [Boolean, Exception] Based on escalation returns true or raises exception if there is a cycle
97
+ def detect_cycle(edge, acyclic_edges, escalation = nil)
98
+ # Test if adding edge creates a cycle, ew will traverse the graph from edge Node, through all it's
99
+ # dependencies
100
+ starting_node = edge.second
101
+ edges = [edge] + acyclic_edges
102
+ traverse_dependecies([], starting_node, starting_node, edges, node_edges(edges, starting_node), escalation)
103
+ end
104
+
105
+ # Recursive method for traversing dependencies and finding a cycle
106
+ #
107
+ # @param traversed_nodes [Array<InventoryRefresh::InventoryCollection> Already traversed nodes
108
+ # @param starting_node [InventoryRefresh::InventoryCollection] Node we've started the traversal on
109
+ # @param current_node [InventoryRefresh::InventoryCollection] Node we are currently on
110
+ # @param edges [Array<Array>] All graph edges
111
+ # @param dependencies [Array<InventoryRefresh::InventoryCollection> Dependencies of the current node
112
+ # @param escalation [Symbol] If :exception, this method throws exception when it finds a cycle
113
+ # @return [Boolean, Exception] Based on escalation returns true or raises exception if there is a cycle
114
+ def traverse_dependecies(traversed_nodes, starting_node, current_node, edges, dependencies, escalation)
115
+ dependencies.each do |node_edge|
116
+ node = node_edge.first
117
+ traversed_nodes << node
118
+ if traversed_nodes.include?(starting_node)
119
+ if escalation == :exception
120
+ raise "Cycle from #{current_node} to #{node}, starting from #{starting_node} passing #{traversed_nodes}"
121
+ else
122
+ return true
123
+ end
124
+ end
125
+ return true if traverse_dependecies(traversed_nodes, starting_node, node, edges, node_edges(edges, node), escalation)
126
+ end
127
+
128
+ false
129
+ end
130
+
131
+ # Returns dependencies of the node, i.e. nodes that are connected by edge
132
+ #
133
+ # @param edges [Array<Array>] All graph edges
134
+ # @param node [InventoryRefresh::InventoryCollection] Node we are inspecting
135
+ # @return [Array<InventoryRefresh::InventoryCollection>] Nodes that are connected to the inspected node
136
+ def node_edges(edges, node)
137
+ edges.select { |e| e.second == node }
138
+ end
139
+
140
+ # Returns Hash of {node => name}, appending numbers if needed to make unique, quoted if needed. Used for the
141
+ # GraphViz format
142
+ #
143
+ # @return [Hash] Hash of {node => name}
144
+ def friendly_unique_node_names
145
+ node_names = {}
146
+ # Try to use shorter .name method that InventoryCollection has.
147
+ nodes.group_by { |n| n.respond_to?(:name) ? n.name.to_s : n.to_s }.each do |base_name, ns|
148
+ ns.each_with_index do |n, i|
149
+ name = ns.size == 1 ? base_name : "#{base_name}_#{i}"
150
+ name = '"' + name.gsub(/["\\]/) { |c| "\\" + c } + '"' unless name =~ /^[A-Za-z0-9_]+$/
151
+ node_names[n] = name
152
+ end
153
+ end
154
+ node_names
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,66 @@
1
+ module InventoryRefresh
2
+ class Graph
3
+ class TopologicalSort
4
+ attr_reader :graph
5
+
6
+ # @param graph [InventoryRefresh::Graph] graph object we want to sort
7
+ def initialize(graph)
8
+ @graph = graph
9
+ end
10
+
11
+ ################################################################################################################
12
+ # Topological sort of the graph of the DTO collections to find the right order of saving DTO collections and
13
+ # identify what DTO collections can be saved in parallel.
14
+ # Does not mutate graph.
15
+ #
16
+ # @return [Array<Array>] Array of arrays(layers) of InventoryCollection objects
17
+ ################################################################################################################
18
+ # The expected input here is the directed acyclic Graph G (inventory_collections), consisting of Vertices(Nodes) V and
19
+ # Edges E:
20
+ # G = (V, E)
21
+ #
22
+ # The directed edge is defined as (u, v), where u is the dependency of v, i.e. arrow comes from u to v:
23
+ # (u, v) ∈ E and u,v ∈ V
24
+ #
25
+ # S0 is a layer that has no dependencies:
26
+ # S0 = { v ∈ V ∣ ∀u ∈ V.(u,v) ∉ E}
27
+ #
28
+ # Si+1 is a layer whose dependencies are in the sum of the previous layers from i to 0, cannot write
29
+ # mathematical sum using U in text editor, so there is an alternative format using _(sum)
30
+ # Si+1 = { v ∈ V ∣ ∀u ∈ V.(u,v) ∈ E → u ∈ _(sum(S0..Si))_ }
31
+ #
32
+ # Then each Si can have their Vertices(DTO collections) processed in parallel. This algorithm cannot
33
+ # identify independent sub-graphs inside of the layers Si, so we can make the processing even more effective.
34
+ ################################################################################################################
35
+ def topological_sort
36
+ nodes = graph.nodes.dup
37
+ edges = graph.edges
38
+ sets = []
39
+ i = 0
40
+ sets[0], nodes = nodes.partition { |v| !edges.detect { |e| e.second == v } }
41
+
42
+ max_depth = 1000
43
+ while nodes.present?
44
+ i += 1
45
+ max_depth -= 1
46
+ if max_depth <= 0
47
+ message = "Max depth reached while doing topological sort, your graph probably contains a cycle"
48
+ #$log.error("#{message}:\n#{graph.to_graphviz}")
49
+ raise "#{message} (see log)"
50
+ end
51
+
52
+ set, nodes = nodes.partition { |v| edges.select { |e| e.second == v }.all? { |e| sets.flatten.include?(e.first) } }
53
+ if set.blank?
54
+ message = "Blank dependency set while doing topological sort, your graph probably contains a cycle"
55
+ #$log.error("#{message}:\n#{graph.to_graphviz}")
56
+ raise "#{message} (see log)"
57
+ end
58
+
59
+ sets[i] = set
60
+ end
61
+
62
+ sets
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,1175 @@
1
+ require "inventory_refresh/inventory_collection/data_storage"
2
+ require "inventory_refresh/inventory_collection/index/proxy"
3
+ require "inventory_refresh/inventory_collection/reference"
4
+ require "inventory_refresh/inventory_collection/references_storage"
5
+ require "inventory_refresh/inventory_collection/scanner"
6
+ require "inventory_refresh/inventory_collection/serialization"
7
+
8
+ require "active_support/core_ext/module/delegation"
9
+
10
+ module InventoryRefresh
11
+ # For more usage examples please follow spec examples in
12
+ # * spec/models/inventory_refresh/save_inventory/single_inventory_collection_spec.rb
13
+ # * spec/models/inventory_refresh/save_inventory/acyclic_graph_of_inventory_collections_spec.rb
14
+ # * spec/models/inventory_refresh/save_inventory/graph_of_inventory_collections_spec.rb
15
+ # * spec/models/inventory_refresh/save_inventory/graph_of_inventory_collections_targeted_refresh_spec.rb
16
+ # * spec/models/inventory_refresh/save_inventory/strategies_and_references_spec.rb
17
+ #
18
+ # @example storing Vm model data into the DB
19
+ #
20
+ # @ems = ManageIQ::Providers::BaseManager.first
21
+ # puts @ems.vms.collect(&:ems_ref) # => []
22
+ #
23
+ # # Init InventoryCollection
24
+ # vms_inventory_collection = ::InventoryRefresh::InventoryCollection.new(
25
+ # :model_class => ManageIQ::Providers::CloudManager::Vm, :parent => @ems, :association => :vms
26
+ # )
27
+ #
28
+ # # Fill InventoryCollection with data
29
+ # # Starting with no vms, lets add vm1 and vm2
30
+ # vms_inventory_collection.build(:ems_ref => "vm1", :name => "vm1")
31
+ # vms_inventory_collection.build(:ems_ref => "vm2", :name => "vm2")
32
+ #
33
+ # # Save InventoryCollection to the db
34
+ # InventoryRefresh::SaveInventory.save_inventory(@ems, [vms_inventory_collection])
35
+ #
36
+ # # The result in the DB is that vm1 and vm2 were created
37
+ # puts @ems.vms.collect(&:ems_ref) # => ["vm1", "vm2"]
38
+ #
39
+ # @example In another refresh, vm1 does not exist anymore and vm3 was added
40
+ # # Init InventoryCollection
41
+ # vms_inventory_collection = ::InventoryRefresh::InventoryCollection.new(
42
+ # :model_class => ManageIQ::Providers::CloudManager::Vm, :parent => @ems, :association => :vms
43
+ # )
44
+ #
45
+ # # Fill InventoryCollection with data
46
+ # vms_inventory_collection.build(:ems_ref => "vm2", :name => "vm2")
47
+ # vms_inventory_collection.build(:ems_ref => "vm3", :name => "vm3")
48
+ #
49
+ # # Save InventoryCollection to the db
50
+ # InventoryRefresh::SaveInventory.save_inventory(@ems, [vms_inventory_collection])
51
+ #
52
+ # # The result in the DB is that vm1 was deleted, vm2 was updated and vm3 was created
53
+ # puts @ems.vms.collect(&:ems_ref) # => ["vm2", "vm3"]
54
+ #
55
+ class InventoryCollection
56
+ # @return [Boolean] A true value marks that we collected all the data of the InventoryCollection,
57
+ # meaning we also collected all the references.
58
+ attr_accessor :data_collection_finalized
59
+
60
+ # @return [InventoryRefresh::InventoryCollection::DataStorage] An InventoryCollection encapsulating all data with
61
+ # indexes
62
+ attr_accessor :data_storage
63
+
64
+ # @return [Boolean] true if this collection is already saved into the DB. E.g. InventoryCollections with
65
+ # DB only strategy are marked as saved. This causes InventoryCollection not being a dependency for any other
66
+ # InventoryCollection, since it is already persisted into the DB.
67
+ attr_accessor :saved
68
+
69
+ # If present, InventoryCollection switches into delete_complement mode, where it will
70
+ # delete every record from the DB, that is not present in this list. This is used for the batch processing,
71
+ # where we don't know which InventoryObject should be deleted, but we know all manager_uuids of all
72
+ # InventoryObject objects that exists in the provider.
73
+ #
74
+ # @return [Array, nil] nil or a list of all :manager_uuids that are present in the Provider's InventoryCollection.
75
+ attr_accessor :all_manager_uuids
76
+
77
+ # @return [Set] A set of InventoryCollection objects that depends on this InventoryCollection object.
78
+ attr_accessor :dependees
79
+
80
+ # @return [Array<Symbol>] @see #parent_inventory_collections documentation of InventoryCollection.new kwargs
81
+ # parameters
82
+ attr_accessor :parent_inventory_collections
83
+
84
+ attr_reader :model_class, :strategy, :attributes_blacklist, :attributes_whitelist, :custom_save_block, :parent,
85
+ :internal_attributes, :delete_method, :dependency_attributes, :manager_ref, :create_only,
86
+ :association, :complete, :update_only, :transitive_dependency_attributes, :check_changed, :arel,
87
+ :inventory_object_attributes, :name, :saver_strategy, :targeted_scope, :default_values,
88
+ :targeted_arel, :targeted, :manager_ref_allowed_nil, :use_ar_object,
89
+ :created_records, :updated_records, :deleted_records,
90
+ :custom_reconnect_block, :batch_extra_attributes, :references_storage
91
+
92
+ delegate :<<,
93
+ :build,
94
+ :build_partial,
95
+ :data,
96
+ :each,
97
+ :find_or_build,
98
+ :find_or_build_by,
99
+ :from_hash,
100
+ :index_proxy,
101
+ :push,
102
+ :size,
103
+ :to_a,
104
+ :to_hash,
105
+ :to => :data_storage
106
+
107
+ delegate :add_reference,
108
+ :attribute_references,
109
+ :build_reference,
110
+ :references,
111
+ :build_stringified_reference,
112
+ :build_stringified_reference_for_record,
113
+ :to => :references_storage
114
+
115
+ delegate :find,
116
+ :find_by,
117
+ :lazy_find,
118
+ :lazy_find_by,
119
+ :named_ref,
120
+ :primary_index,
121
+ :reindex_secondary_indexes!,
122
+ :skeletal_primary_index,
123
+ :to => :index_proxy
124
+
125
+ delegate :table_name,
126
+ :to => :model_class
127
+
128
+ # @param model_class [Class] A class of an ApplicationRecord model, that we want to persist into the DB or load from
129
+ # the DB.
130
+ # @param manager_ref [Array] Array of Symbols, that are keys of the InventoryObject's data, inserted into this
131
+ # InventoryCollection. Using these keys, we need to be able to uniquely identify each of the InventoryObject
132
+ # objects inside.
133
+ # @param association [Symbol] A Rails association callable on a :parent attribute is used for comparing with the
134
+ # objects in the DB, to decide if the InventoryObjects will be created/deleted/updated or used for obtaining
135
+ # the data from a DB, if a DB strategy is used. It returns objects of the :model_class class or its sub STI.
136
+ # @param parent [ApplicationRecord] An ApplicationRecord object that has a callable :association method returning
137
+ # the objects of a :model_class.
138
+ # @param strategy [Symbol] A strategy of the InventoryCollection that will be used for saving/loading of the
139
+ # InventoryObject objects.
140
+ # Allowed strategies are:
141
+ # - nil => InventoryObject objects of the InventoryCollection will be saved to the DB, only these objects
142
+ # will be referable from the other InventoryCollection objects.
143
+ # - :local_db_cache_all => Loads InventoryObject objects from the database, it loads all the objects that
144
+ # are a result of a [<:parent>.<:association>, :arel] taking
145
+ # first defined in this order. This strategy will not save any objects in the DB.
146
+ # - :local_db_find_references => Loads InventoryObject objects from the database, it loads only objects that
147
+ # were referenced by the other InventoryCollections using a filtered result
148
+ # of a [<:parent>.<:association>, :arel] taking first
149
+ # defined in this order. This strategy will not save any objects in the DB.
150
+ # - :local_db_find_missing_references => InventoryObject objects of the InventoryCollection will be saved to
151
+ # the DB. Then if we reference an object that is not present, it will
152
+ # load them from the db using :local_db_find_references strategy.
153
+ # @param custom_save_block [Proc] A custom lambda/proc for persisting in the DB, for cases where it's not enough
154
+ # to just save every InventoryObject inside by the defined rules and default saving algorithm.
155
+ #
156
+ # Example1 - saving SomeModel in my own ineffective way :-) :
157
+ #
158
+ # custom_save = lambda do |_ems, inventory_collection|
159
+ # inventory_collection.each |inventory_object| do
160
+ # hash = inventory_object.attributes # Loads possible dependencies into saveable hash
161
+ # obj = SomeModel.find_by(:attr => hash[:attr]) # Note: doing find_by for many models produces N+1
162
+ # # queries, avoid this, this is just a simple example :-)
163
+ # obj.update_attributes(hash) if obj
164
+ # obj ||= SomeModel.create(hash)
165
+ # inventory_object.id = obj.id # If this InventoryObject is referenced elsewhere, we need to store its
166
+ # primary key back to the InventoryObject
167
+ # end
168
+ #
169
+ # Example2 - saving parent OrchestrationStack in a more effective way, than the default saving algorithm can
170
+ # achieve. Ancestry gem requires an ActiveRecord object for association and is not defined as a proper
171
+ # ActiveRecord association. That leads in N+1 queries in the default saving algorithm, so we can do better
172
+ # with custom saving for now. The InventoryCollection is defined as a custom dependencies processor,
173
+ # without its own :model_class and InventoryObjects inside:
174
+ #
175
+ # InventoryRefresh::InventoryCollection.new({
176
+ # :association => :orchestration_stack_ancestry,
177
+ # :custom_save_block => orchestration_stack_ancestry_save_block,
178
+ # :dependency_attributes => {
179
+ # :orchestration_stacks => [collections[:orchestration_stacks]],
180
+ # :orchestration_stacks_resources => [collections[:orchestration_stacks_resources]]
181
+ # }
182
+ # })
183
+ #
184
+ # And the lambda is defined as:
185
+ #
186
+ # orchestration_stack_ancestry_save_block = lambda do |_ems, inventory_collection|
187
+ # stacks_inventory_collection = inventory_collection.dependency_attributes[:orchestration_stacks].try(:first)
188
+ #
189
+ # return if stacks_inventory_collection.blank?
190
+ #
191
+ # stacks_parents = stacks_inventory_collection.data.each_with_object({}) do |x, obj|
192
+ # parent_id = x.data[:parent].load.try(:id)
193
+ # obj[x.id] = parent_id if parent_id
194
+ # end
195
+ #
196
+ # model_class = stacks_inventory_collection.model_class
197
+ #
198
+ # stacks_parents_indexed = model_class
199
+ # .select([:id, :ancestry])
200
+ # .where(:id => stacks_parents.values).find_each.index_by(&:id)
201
+ #
202
+ # model_class
203
+ # .select([:id, :ancestry])
204
+ # .where(:id => stacks_parents.keys).find_each do |stack|
205
+ # parent = stacks_parents_indexed[stacks_parents[stack.id]]
206
+ # stack.update_attribute(:parent, parent)
207
+ # end
208
+ # end
209
+ # @param custom_reconnect_block [Proc] A custom lambda for reconnect logic of previously disconnected records
210
+ #
211
+ # Example - Reconnect disconnected Vms
212
+ # InventoryRefresh::InventoryCollection.new({
213
+ # :association => :orchestration_stack_ancestry,
214
+ # :custom_reconnect_block => vms_custom_reconnect_block,
215
+ # })
216
+ #
217
+ # And the lambda is defined as:
218
+ #
219
+ # vms_custom_reconnect_block = lambda do |inventory_collection, inventory_objects_index, attributes_index|
220
+ # inventory_objects_index.each_slice(1000) do |batch|
221
+ # Vm.where(:ems_ref => batch.map(&:second).map(&:manager_uuid)).each do |record|
222
+ # index = inventory_collection.object_index_with_keys(inventory_collection.manager_ref_to_cols, record)
223
+ #
224
+ # # We need to delete the record from the inventory_objects_index and attributes_index, otherwise it
225
+ # # would be sent for create.
226
+ # inventory_object = inventory_objects_index.delete(index)
227
+ # hash = attributes_index.delete(index)
228
+ #
229
+ # record.assign_attributes(hash.except(:id, :type))
230
+ # if !inventory_collection.check_changed? || record.changed?
231
+ # record.save!
232
+ # inventory_collection.store_updated_records(record)
233
+ # end
234
+ #
235
+ # inventory_object.id = record.id
236
+ # end
237
+ # end
238
+ # @param delete_method [Symbol] A delete method that will be used for deleting of the InventoryObject, if the
239
+ # object is marked for deletion. A default is :destroy, the instance method must be defined on the
240
+ # :model_class.
241
+ # @param dependency_attributes [Hash] Manually defined dependencies of this InventoryCollection. We can use this
242
+ # by manually place the InventoryCollection into the graph, to make sure the saving is invoked after the
243
+ # dependencies were saved. The dependencies itself are InventoryCollection objects. For a common use-cases
244
+ # we do not need to define dependencies manually, since those are inferred automatically by scanning of the
245
+ # data.
246
+ #
247
+ # Example:
248
+ # :dependency_attributes => {
249
+ # :orchestration_stacks => [collections[:orchestration_stacks]],
250
+ # :orchestration_stacks_resources => [collections[:orchestration_stacks_resources]]
251
+ # }
252
+ # This example is used in Example2 of the <param custom_save_block> and it means that our :custom_save_block
253
+ # will be invoked after the InventoryCollection :orchestration_stacks and :orchestration_stacks_resources
254
+ # are saved.
255
+ # @param attributes_blacklist [Array] Attributes we do not want to include into saving. We cannot blacklist an
256
+ # attribute that is needed for saving of the object.
257
+ # Note: attributes_blacklist is also used for internal resolving of the cycles in the graph.
258
+ #
259
+ # In the Example2 of the <param custom_save_block>, we have a custom saving code, that saves a :parent
260
+ # attribute of the OrchestrationStack. That means we don't want that attribute saved as a part of
261
+ # InventoryCollection for OrchestrationStack, so we would set :attributes_blacklist => [:parent]. Then the
262
+ # :parent will be ignored while saving.
263
+ # @param attributes_whitelist [Array] Same usage as the :attributes_blacklist, but defining full set of attributes
264
+ # that should be saved. Attributes that are part of :manager_ref and needed validations are automatically
265
+ # added.
266
+ # @param complete [Boolean] By default true, :complete is marking we are sending a complete dataset and therefore
267
+ # we can create/update/delete the InventoryObject objects. If :complete is false we will only do
268
+ # create/update without delete.
269
+ # @param update_only [Boolean] By default false. If true we only update the InventoryObject objects, if false we do
270
+ # create/update/delete.
271
+ # @param check_changed [Boolean] By default true. If true, before updating the InventoryObject, we call Rails
272
+ # 'changed?' method. This can optimize speed of updates heavily, but it can fail to recognize the change for
273
+ # e.g. Ancestry and Relationship based columns. If false, we always update the InventoryObject.
274
+ # @param arel [ActiveRecord::Associations::CollectionProxy|Arel::SelectManager] Instead of :parent and :association
275
+ # we can provide Arel directly to say what records should be compared to check if InventoryObject will be
276
+ # doing create/update/delete.
277
+ #
278
+ # Example:
279
+ # for a targeted refresh, we want to delete/update/create only a list of vms specified with a list of
280
+ # ems_refs:
281
+ # :arel => manager.vms.where(:ems_ref => manager_refs)
282
+ # Then we want to do the same for the hardwares of only those vms:
283
+ # :arel => manager.hardwares.joins(:vm_or_template).where(
284
+ # 'vms' => {:ems_ref => manager_refs}
285
+ # )
286
+ # And etc. for the other Vm related records.
287
+ # @param default_values [Hash] A hash of an attributes that will be added to every inventory object created by
288
+ # inventory_collection.build(hash)
289
+ #
290
+ # Example: Given
291
+ # inventory_collection = InventoryCollection.new({
292
+ # :model_class => ::Vm,
293
+ # :arel => @ems.vms,
294
+ # :default_values => {:ems_id => 10}
295
+ # })
296
+ # And building the inventory_object like:
297
+ # inventory_object = inventory_collection.build(:ems_ref => "vm_1", :name => "vm1")
298
+ # The inventory_object.data will look like:
299
+ # {:ems_ref => "vm_1", :name => "vm1", :ems_id => 10}
300
+ # @param inventory_object_attributes [Array] Array of attribute names that will be exposed as readers/writers on the
301
+ # InventoryObject objects inside.
302
+ #
303
+ # Example: Given
304
+ # inventory_collection = InventoryCollection.new({
305
+ # :model_class => ::Vm,
306
+ # :arel => @ems.vms,
307
+ # :inventory_object_attributes => [:name, :label]
308
+ # })
309
+ # And building the inventory_object like:
310
+ # inventory_object = inventory_collection.build(:ems_ref => "vm1", :name => "vm1")
311
+ # We can use inventory_object_attributes as setters and getters:
312
+ # inventory_object.name = "Name"
313
+ # inventory_object.label = inventory_object.name
314
+ # Which would be equivalent to less nicer way:
315
+ # inventory_object[:name] = "Name"
316
+ # inventory_object[:label] = inventory_object[:name]
317
+ # So by using inventory_object_attributes, we will be guarding the allowed attributes and will have an
318
+ # explicit list of allowed attributes, that can be used also for documentation purposes.
319
+ # @param name [Symbol] A unique name of the InventoryCollection under a Persister. If not provided, the :association
320
+ # attribute is used. If :association is nil as well, the :name will be inferred from the :model_class.
321
+ # @param saver_strategy [Symbol] A strategy that will be used for InventoryCollection persisting into the DB.
322
+ # Allowed saver strategies are:
323
+ # - :default => Using Rails saving methods, this way is not safe to run in multiple workers concurrently,
324
+ # since it will lead to non consistent data.
325
+ # - :batch => Using batch SQL queries, this way is not safe to run in multiple workers
326
+ # concurrently, since it will lead to non consistent data.
327
+ # - :concurrent_safe => This method is designed for concurrent saving. It uses atomic upsert to avoid
328
+ # data duplication and it uses timestamp based atomic checks to avoid new data being overwritten by the
329
+ # the old data.
330
+ # - :concurrent_safe_batch => Same as :concurrent_safe, but the upsert/update queries are executed as
331
+ # batched SQL queries, instead of sending 1 query per record.
332
+ # @param parent_inventory_collections [Array] Array of symbols having a name pointing to the
333
+ # InventoryRefresh::InventoryCollection objects, that serve as parents to this InventoryCollection. There are
334
+ # several scenarios to consider, when deciding if InventoryCollection has parent collections, see the example.
335
+ #
336
+ # Example:
337
+ # taking inventory collections :vms and :disks (local disks), if we write that:
338
+ # inventory_collection = InventoryCollection.new({
339
+ # :model_class => ::Disk,
340
+ # :association => :disks,
341
+ # :manager_ref => [:vm, :location]
342
+ # :parent_inventory_collection => [:vms],
343
+ # })
344
+ #
345
+ # Then the decision for having :parent_inventory_collection => [:vms] was probably driven by these
346
+ # points:
347
+ # 1. We can get list of all disks only by doing SQL query through the parent object (so there will be join
348
+ # from vms to disks table).
349
+ # 2. There is no API query for getting all disks from the provider API, we get them inside VM data, or as
350
+ # a Vm subquery
351
+ # 3. Part of the manager_ref of the IC is the VM object (foreign key), so the disk's location is unique
352
+ # only under 1 Vm. (In current models, this modeled going through Hardware model)
353
+ # 4. In targeted refresh, we always expect that each Vm will be saved with all its disks.
354
+ #
355
+ # Then having the above points, adding :parent_inventory_collection => [:vms], will bring these
356
+ # implications:
357
+ # 1. By archiving/deleting Vm, we can no longer see the disk, because those were owned by the Vm. Any
358
+ # archival/deletion of the Disk model, must be then done by cascade delete/hooks logic.
359
+ # 2. Having Vm as a parent ensures we always process it first. So e.g. when providing no Vms for saving
360
+ # we would have no graph dependency (no data --> no edges --> no dependencies) and Disk could be
361
+ # archived/removed before the Vm, while we always want to archive the VM first.
362
+ # 3. For targeted refresh, we always expect that all disks are saved with a VM. So for targeting :disks,
363
+ # we are not using #manager_uuids attribute, since the scope is "all disks of all targeted VMs", so we
364
+ # always use #manager_uuids of the parent. (that is why :parent_inventory_collections and
365
+ # :manager_uuids are mutually exclusive attributes)
366
+ # 4. For automatically building the #targeted_arel query, we need the parent to know what is the root node.
367
+ # While this information can be introspected from the data, it creates a scope for create&update&delete,
368
+ # which means it has to work with no data provided (causing delete all). So with no data we cannot
369
+ # introspect anything.
370
+ # @param manager_uuids [Array|Proc] Array of manager_uuids of the InventoryObjects we want to create/update/delete. Using
371
+ # this attribute, the db_collection_for_comparison will be automatically limited by the manager_uuids, in a
372
+ # case of a simple relation. In a case of a complex relation, we can leverage :manager_uuids in a
373
+ # custom :targeted_arel. We can pass also lambda, for lazy_evaluation.
374
+ # @param all_manager_uuids [Array] Array of all manager_uuids of the InventoryObjects. With the :targeted true,
375
+ # having this parameter defined will invoke only :delete_method on a complement of this set, making sure
376
+ # the DB has only this set of data after. This :attribute serves for deleting of top level
377
+ # InventoryCollections, i.e. InventoryCollections having parent_inventory_collections nil. The deleting of
378
+ # child collections is already handled by the scope of the parent_inventory_collections and using Rails
379
+ # :dependent => :destroy,
380
+ # @param targeted_arel [Proc] A callable block that receives this InventoryCollection as a first argument. In there
381
+ # we can leverage a :parent_inventory_collections or :manager_uuids to limit the query based on the
382
+ # manager_uuids available.
383
+ # Example:
384
+ # targeted_arel = lambda do |inventory_collection|
385
+ # # Getting ems_refs of parent :vms and :miq_templates
386
+ # manager_uuids = inventory_collection.parent_inventory_collections.collect(&:manager_uuids).flatten
387
+ # inventory_collection.db_collection_for_comparison.hardwares.joins(:vm_or_template).where(
388
+ # 'vms' => {:ems_ref => manager_uuids}
389
+ # )
390
+ # end
391
+ #
392
+ # inventory_collection = InventoryCollection.new({
393
+ # :model_class => ::Hardware,
394
+ # :association => :hardwares,
395
+ # :parent_inventory_collection => [:vms, :miq_templates],
396
+ # :targeted_arel => targeted_arel,
397
+ # })
398
+ # @param targeted [Boolean] True if the collection is targeted, in that case it will be leveraging :manager_uuids
399
+ # :parent_inventory_collections and :targeted_arel to save a subgraph of a data.
400
+ # @param manager_ref_allowed_nil [Array] Array of symbols having manager_ref columns, that are a foreign key an can
401
+ # be nil. Given the table are shared by many providers, it can happen, that the table is used only partially.
402
+ # Then it can happen we want to allow certain foreign keys to be nil, while being sure the referential
403
+ # integrity is not broken. Of course the DB Foreign Key can't be created in this case, so we should try to
404
+ # avoid this usecase by a proper modeling.
405
+ # @param use_ar_object [Boolean] True or False. Whether we need to initialize AR object as part of the saving
406
+ # it's needed if the model have special setters, serialize of columns, etc. This setting is relevant only
407
+ # for the batch saver strategy.
408
+ # @param batch_extra_attributes [Array] Array of symbols marking which extra attributes we want to store into the
409
+ # db. These extra attributes might be a product of :use_ar_object assignment and we need to specify them
410
+ # manually, if we want to use a batch saving strategy and we have models that populate attributes as a side
411
+ # effect.
412
+ def initialize(model_class: nil, manager_ref: nil, association: nil, parent: nil, strategy: nil,
413
+ custom_save_block: nil, delete_method: nil, dependency_attributes: nil,
414
+ attributes_blacklist: nil, attributes_whitelist: nil, complete: nil, update_only: nil,
415
+ check_changed: nil, arel: nil, default_values: {}, create_only: nil,
416
+ inventory_object_attributes: nil, name: nil, saver_strategy: nil,
417
+ parent_inventory_collections: nil, manager_uuids: [], all_manager_uuids: nil, targeted_arel: nil,
418
+ targeted: nil, manager_ref_allowed_nil: nil, secondary_refs: {}, use_ar_object: nil,
419
+ custom_reconnect_block: nil, batch_extra_attributes: [])
420
+ @model_class = model_class
421
+ @manager_ref = manager_ref || [:ems_ref]
422
+ @secondary_refs = secondary_refs
423
+ @association = association
424
+ @parent = parent || nil
425
+ @arel = arel
426
+ @dependency_attributes = dependency_attributes || {}
427
+ @strategy = process_strategy(strategy)
428
+ @delete_method = delete_method || :destroy
429
+ @custom_save_block = custom_save_block
430
+ @custom_reconnect_block = custom_reconnect_block
431
+ @check_changed = check_changed.nil? ? true : check_changed
432
+ @internal_attributes = %i(__feedback_edge_set_parent __parent_inventory_collections)
433
+ @complete = complete.nil? ? true : complete
434
+ @update_only = update_only.nil? ? false : update_only
435
+ @create_only = create_only.nil? ? false : create_only
436
+ @default_values = default_values
437
+ @name = name || association || model_class.to_s.demodulize.tableize
438
+ @saver_strategy = process_saver_strategy(saver_strategy)
439
+ @use_ar_object = use_ar_object || false
440
+ @batch_extra_attributes = batch_extra_attributes
441
+
442
+ @manager_ref_allowed_nil = manager_ref_allowed_nil || []
443
+
444
+ # Targeted mode related attributes
445
+ # TODO(lsmola) Should we refactor this to use references too?
446
+ @all_manager_uuids = all_manager_uuids
447
+ @parent_inventory_collections = parent_inventory_collections
448
+ @targeted_arel = targeted_arel
449
+ @targeted = !!targeted
450
+
451
+ @inventory_object_attributes = inventory_object_attributes
452
+
453
+ @saved ||= false
454
+ @attributes_blacklist = Set.new
455
+ @attributes_whitelist = Set.new
456
+ @transitive_dependency_attributes = Set.new
457
+ @dependees = Set.new
458
+ @data_storage = ::InventoryRefresh::InventoryCollection::DataStorage.new(self, secondary_refs)
459
+ @references_storage = ::InventoryRefresh::InventoryCollection::ReferencesStorage.new(index_proxy)
460
+ @targeted_scope = ::InventoryRefresh::InventoryCollection::ReferencesStorage.new(index_proxy).merge!(manager_uuids)
461
+
462
+ @created_records = []
463
+ @updated_records = []
464
+ @deleted_records = []
465
+
466
+ blacklist_attributes!(attributes_blacklist) if attributes_blacklist.present?
467
+ whitelist_attributes!(attributes_whitelist) if attributes_whitelist.present?
468
+ end
469
+
470
+ # Caches what records were created, for later use, e.g. post provision behavior
471
+ #
472
+ # @param records [Array<ApplicationRecord, Hash>] list of stored records
473
+ def store_created_records(records)
474
+ @created_records.concat(records_identities(records))
475
+ end
476
+
477
+ # Caches what records were updated, for later use, e.g. post provision behavior
478
+ #
479
+ # @param records [Array<ApplicationRecord, Hash>] list of stored records
480
+ def store_updated_records(records)
481
+ @updated_records.concat(records_identities(records))
482
+ end
483
+
484
+ # Caches what records were deleted/soft-deleted, for later use, e.g. post provision behavior
485
+ #
486
+ # @param records [Array<ApplicationRecord, Hash>] list of stored records
487
+ def store_deleted_records(records)
488
+ @deleted_records.concat(records_identities(records))
489
+ end
490
+
491
+ # Processes passed saver strategy
492
+ #
493
+ # @param saver_strategy [Symbol] Passed saver strategy
494
+ # @return [Symbol] Returns back the passed strategy if supported, or raises exception
495
+ def process_saver_strategy(saver_strategy)
496
+ return :default unless saver_strategy
497
+
498
+ saver_strategy = saver_strategy.to_sym
499
+ case saver_strategy
500
+ when :default, :batch, :concurrent_safe, :concurrent_safe_batch
501
+ saver_strategy
502
+ else
503
+ raise "Unknown InventoryCollection saver strategy: :#{saver_strategy}, allowed strategies are "\
504
+ ":default, :batch, :concurrent_safe and :concurrent_safe_batch"
505
+ end
506
+ end
507
+
508
+ # Processes passed strategy, modifies :data_collection_finalized and :saved attributes for db only strategies
509
+ #
510
+ # @param strategy_name [Symbol] Passed saver strategy
511
+ # @return [Symbol] Returns back the passed strategy if supported, or raises exception
512
+ def process_strategy(strategy_name)
513
+ self.data_collection_finalized = false
514
+
515
+ return unless strategy_name
516
+
517
+ strategy_name = strategy_name.to_sym
518
+ case strategy_name
519
+ when :local_db_cache_all
520
+ self.data_collection_finalized = true
521
+ self.saved = true
522
+ when :local_db_find_references
523
+ self.saved = true
524
+ when :local_db_find_missing_references
525
+ else
526
+ raise "Unknown InventoryCollection strategy: :#{strategy_name}, allowed strategies are :local_db_cache_all, "\
527
+ ":local_db_find_references and :local_db_find_missing_references."
528
+ end
529
+ strategy_name
530
+ end
531
+
532
+ # @return [Boolean] true means we want to call .changed? on every ActiveRecord object before saving it
533
+ def check_changed?
534
+ check_changed
535
+ end
536
+
537
+ # @return [Boolean] true means we want to use ActiveRecord object for writing attributes and we want to perform
538
+ # casting on all columns
539
+ def use_ar_object?
540
+ use_ar_object
541
+ end
542
+
543
+ # @return [Boolean] true means the data is not complete, leading to only creating and updating data
544
+ def complete?
545
+ complete
546
+ end
547
+
548
+ # @return [Boolean] true means we want to only update data
549
+ def update_only?
550
+ update_only
551
+ end
552
+
553
+ # @return [Boolean] true means we will delete/soft-delete data
554
+ def delete_allowed?
555
+ complete? && !update_only?
556
+ end
557
+
558
+ # @return [Boolean] true means we will delete/soft-delete data
559
+ def create_allowed?
560
+ !update_only?
561
+ end
562
+
563
+ # @return [Boolean] true means that only create of new data is allowed
564
+ def create_only?
565
+ create_only
566
+ end
567
+
568
+ # @return [Boolean] true if the whole InventoryCollection object has all data persisted
569
+ def saved?
570
+ saved
571
+ end
572
+
573
+ # @return [Boolean] true if all dependencies have all data persisted
574
+ def saveable?
575
+ dependencies.all?(&:saved?)
576
+ end
577
+
578
+ # @return [Boolean] true if we are using a saver strategy that allows saving in parallel processes
579
+ def parallel_safe?
580
+ @parallel_safe_cache ||= %i(concurrent_safe concurrent_safe_batch).include?(saver_strategy)
581
+ end
582
+
583
+ # @return [Boolean] true if the model_class supports STI
584
+ def supports_sti?
585
+ @supports_sti_cache = model_class.column_names.include?("type") if @supports_sti_cache.nil?
586
+ @supports_sti_cache
587
+ end
588
+
589
+ # @return [Boolean] true if the model_class has created_on column
590
+ def supports_created_on?
591
+ if @supports_created_on_cache.nil?
592
+ @supports_created_on_cache = (model_class.column_names.include?("created_on") && ActiveRecord::Base.record_timestamps)
593
+ end
594
+ @supports_created_on_cache
595
+ end
596
+
597
+ # @return [Boolean] true if the model_class has updated_on column
598
+ def supports_updated_on?
599
+ if @supports_updated_on_cache.nil?
600
+ @supports_updated_on_cache = (model_class.column_names.include?("updated_on") && ActiveRecord::Base.record_timestamps)
601
+ end
602
+ @supports_updated_on_cache
603
+ end
604
+
605
+ # @return [Boolean] true if the model_class has created_at column
606
+ def supports_created_at?
607
+ if @supports_created_at_cache.nil?
608
+ @supports_created_at_cache = (model_class.column_names.include?("created_at") && ActiveRecord::Base.record_timestamps)
609
+ end
610
+ @supports_created_at_cache
611
+ end
612
+
613
+ # @return [Boolean] true if the model_class has updated_at column
614
+ def supports_updated_at?
615
+ if @supports_updated_at_cache.nil?
616
+ @supports_updated_at_cache = (model_class.column_names.include?("updated_at") && ActiveRecord::Base.record_timestamps)
617
+ end
618
+ @supports_updated_at_cache
619
+ end
620
+
621
+ # @return [Boolean] true if the model_class has resource_timestamps_max column
622
+ def supports_resource_timestamps_max?
623
+ @supports_resource_timestamps_max_cache ||= model_class.column_names.include?("resource_timestamps_max")
624
+ end
625
+
626
+ # @return [Boolean] true if the model_class has resource_timestamps column
627
+ def supports_resource_timestamps?
628
+ @supports_resource_timestamps_cache ||= model_class.column_names.include?("resource_timestamps")
629
+ end
630
+
631
+ # @return [Boolean] true if the model_class has resource_timestamp column
632
+ def supports_resource_timestamp?
633
+ @supports_resource_timestamp_cache ||= model_class.column_names.include?("resource_timestamp")
634
+ end
635
+
636
+ # @return [Boolean] true if the model_class has resource_versions_max column
637
+ def supports_resource_versions_max?
638
+ @supports_resource_versions_max_cache ||= model_class.column_names.include?("resource_versions_max")
639
+ end
640
+
641
+ # @return [Boolean] true if the model_class has resource_versions column
642
+ def supports_resource_versions?
643
+ @supports_resource_versions_cache ||= model_class.column_names.include?("resource_versions")
644
+ end
645
+
646
+ # @return [Boolean] true if the model_class has resource_version column
647
+ def supports_resource_version?
648
+ @supports_resource_version_cache ||= model_class.column_names.include?("resource_version")
649
+ end
650
+
651
+ # @return [Array<Symbol>] all columns that are part of the best fit unique index
652
+ def unique_index_columns
653
+ return @unique_index_columns if @unique_index_columns
654
+
655
+ @unique_index_columns = unique_index_for(unique_index_keys).columns.map(&:to_sym)
656
+ end
657
+
658
+ def unique_index_keys
659
+ @unique_index_keys ||= manager_ref_to_cols.map(&:to_sym)
660
+ end
661
+
662
+ # @return [Array<ActiveRecord::ConnectionAdapters::IndexDefinition>] array of all unique indexes known to model
663
+ def unique_indexes
664
+ @unique_indexes_cache if @unique_indexes_cache
665
+
666
+ @unique_indexes_cache = model_class.connection.indexes(model_class.table_name).select(&:unique)
667
+
668
+ if @unique_indexes_cache.blank?
669
+ raise "#{self} and its table #{model_class.table_name} must have a unique index defined, to"\
670
+ " be able to use saver_strategy :concurrent_safe or :concurrent_safe_batch."
671
+ end
672
+
673
+ @unique_indexes_cache
674
+ end
675
+
676
+ # Finds an index that fits the list of columns (keys) the best
677
+ #
678
+ # @param keys [Array<Symbol>]
679
+ # @raise [Exception] if the unique index for the columns was not found
680
+ # @return [ActiveRecord::ConnectionAdapters::IndexDefinition] unique index fitting the keys
681
+ def unique_index_for(keys)
682
+ @unique_index_for_keys_cache ||= {}
683
+ @unique_index_for_keys_cache[keys] if @unique_index_for_keys_cache[keys]
684
+
685
+ # Find all uniq indexes that that are covering our keys
686
+ uniq_key_candidates = unique_indexes.each_with_object([]) { |i, obj| obj << i if (keys - i.columns.map(&:to_sym)).empty? }
687
+
688
+ if @unique_indexes_cache.blank?
689
+ raise "#{self} and its table #{model_class.table_name} must have a unique index defined "\
690
+ "covering columns #{keys} to be able to use saver_strategy :concurrent_safe or :concurrent_safe_batch."
691
+ end
692
+
693
+ # Take the uniq key having the least number of columns
694
+ @unique_index_for_keys_cache[keys] = uniq_key_candidates.min_by { |x| x.columns.count }
695
+ end
696
+
697
+ def internal_columns
698
+ return @internal_columns if @internal_columns
699
+
700
+ @internal_columns = [] + internal_timestamp_columns
701
+ @internal_columns << :type if supports_sti?
702
+ @internal_columns << :resource_timestamps_max if supports_resource_timestamps_max?
703
+ @internal_columns << :resource_timestamps if supports_resource_timestamps?
704
+ @internal_columns << :resource_timestamp if supports_resource_timestamp?
705
+ @internal_columns << :resource_versions_max if supports_resource_versions_max?
706
+ @internal_columns << :resource_versions if supports_resource_versions?
707
+ @internal_columns << :resource_version if supports_resource_version?
708
+ @internal_columns
709
+ end
710
+
711
+ def internal_timestamp_columns
712
+ return @internal_timestamp_columns if @internal_timestamp_columns
713
+
714
+ @internal_timestamp_columns = []
715
+ @internal_timestamp_columns << :created_on if supports_created_on?
716
+ @internal_timestamp_columns << :created_at if supports_created_at?
717
+ @internal_timestamp_columns << :updated_on if supports_updated_on?
718
+ @internal_timestamp_columns << :updated_at if supports_updated_at?
719
+
720
+ @internal_timestamp_columns
721
+ end
722
+
723
+ def base_columns
724
+ @base_columns ||= unique_index_columns + internal_columns
725
+ end
726
+
727
+ # @return [Boolean] true if no more data will be added to this InventoryCollection object, that usually happens
728
+ # after the parsing step is finished
729
+ def data_collection_finalized?
730
+ data_collection_finalized
731
+ end
732
+
733
+ # @param value [Object] Object we want to test
734
+ # @return [Boolean] true is value is kind of InventoryRefresh::InventoryObject
735
+ def inventory_object?(value)
736
+ value.kind_of?(::InventoryRefresh::InventoryObject)
737
+ end
738
+
739
+ # @param value [Object] Object we want to test
740
+ # @return [Boolean] true is value is kind of InventoryRefresh::InventoryObjectLazy
741
+ def inventory_object_lazy?(value)
742
+ value.kind_of?(::InventoryRefresh::InventoryObjectLazy)
743
+ end
744
+
745
+ # Builds string uuid from passed Object and keys
746
+ #
747
+ # @param keys [Array<Symbol>] Indexes into the Hash data
748
+ # @param record [ApplicationRecord] ActiveRecord record
749
+ # @return [String] Concatenated values on keys from data
750
+ def object_index_with_keys(keys, record)
751
+ # TODO(lsmola) remove, last usage is in k8s reconnect logic
752
+ build_stringified_reference_for_record(record, keys)
753
+ end
754
+
755
+ # True if processing of this InventoryCollection object would lead to no operations. Then we use this marker to
756
+ # stop processing of the InventoryCollector object very soon, to avoid a lot of unnecessary Db queries, etc.
757
+ #
758
+ # @return [Boolean] true if processing of this InventoryCollection object would lead to no operations.
759
+ def noop?
760
+ # If this InventoryCollection doesn't do anything. it can easily happen for targeted/batched strategies.
761
+ if targeted?
762
+ if parent_inventory_collections.nil? && targeted_scope.primary_references.blank? &&
763
+ all_manager_uuids.nil? && parent_inventory_collections.blank? && custom_save_block.nil? &&
764
+ skeletal_primary_index.blank?
765
+ # It's a noop Parent targeted InventoryCollection
766
+ true
767
+ elsif !parent_inventory_collections.nil? && parent_inventory_collections.all? { |x| x.targeted_scope.primary_references.blank? } &&
768
+ skeletal_primary_index.blank?
769
+ # It's a noop Child targeted InventoryCollection
770
+ true
771
+ else
772
+ false
773
+ end
774
+ elsif data.blank? && !delete_allowed? && skeletal_primary_index.blank?
775
+ # If we have no data to save and delete is not allowed, we can just skip
776
+ true
777
+ else
778
+ false
779
+ end
780
+ end
781
+
782
+ # @return [Boolean] true is processing of this InventoryCollection will be in targeted mode
783
+ def targeted?
784
+ targeted
785
+ end
786
+
787
+ # Convert manager_ref list of attributes to list of DB columns
788
+ #
789
+ # @return [Array<String>] true is processing of this InventoryCollection will be in targeted mode
790
+ def manager_ref_to_cols
791
+ # TODO(lsmola) this should contain the polymorphic _type, otherwise the IC with polymorphic unique key will get
792
+ # conflicts
793
+ manager_ref.map do |ref|
794
+ association_to_foreign_key_mapping[ref] || ref
795
+ end
796
+ end
797
+
798
+ # List attributes causing a dependency and filters them by attributes_blacklist and attributes_whitelist
799
+ #
800
+ # @return [Hash{Symbol => Set}] attributes causing a dependency and filtered by blacklist and whitelist
801
+ def filtered_dependency_attributes
802
+ filtered_attributes = dependency_attributes
803
+
804
+ if attributes_blacklist.present?
805
+ filtered_attributes = filtered_attributes.reject { |key, _value| attributes_blacklist.include?(key) }
806
+ end
807
+
808
+ if attributes_whitelist.present?
809
+ filtered_attributes = filtered_attributes.select { |key, _value| attributes_whitelist.include?(key) }
810
+ end
811
+
812
+ filtered_attributes
813
+ end
814
+
815
+ # Attributes that are needed to be able to save the record, i.e. attributes that are part of the unique index
816
+ # and attributes with presence validation or NOT NULL constraint
817
+ #
818
+ # @return [Array<Symbol>] attributes that are needed for saving of the record
819
+ def fixed_attributes
820
+ if model_class
821
+ presence_validators = model_class.validators.detect { |x| x.kind_of?(ActiveRecord::Validations::PresenceValidator) }
822
+ end
823
+ # Attributes that has to be always on the entity, so attributes making unique index of the record + attributes
824
+ # that have presence validation
825
+ fixed_attributes = manager_ref
826
+ fixed_attributes += presence_validators.attributes if presence_validators.present?
827
+ fixed_attributes
828
+ end
829
+
830
+ # Returns fixed dependencies, which are the ones we can't move, because we wouldn't be able to save the data
831
+ #
832
+ # @returns [Set<InventoryRefresh::InventoryCollection>] all unique non saved fixed dependencies
833
+ def fixed_dependencies
834
+ fixed_attrs = fixed_attributes
835
+
836
+ filtered_dependency_attributes.each_with_object(Set.new) do |(key, value), fixed_deps|
837
+ fixed_deps.merge(value) if fixed_attrs.include?(key)
838
+ end.reject(&:saved?)
839
+ end
840
+
841
+ # @return [Array<InventoryRefresh::InventoryCollection>] all unique non saved dependencies
842
+ def dependencies
843
+ filtered_dependency_attributes.values.map(&:to_a).flatten.uniq.reject(&:saved?)
844
+ end
845
+
846
+ # Returns what attributes are causing a dependencies to certain InventoryCollection objects.
847
+ #
848
+ # @param inventory_collections [Array<InventoryRefresh::InventoryCollection>]
849
+ # @return [Array<InventoryRefresh::InventoryCollection>] attributes causing the dependencies to certain
850
+ # InventoryCollection objects
851
+ def dependency_attributes_for(inventory_collections)
852
+ attributes = Set.new
853
+ inventory_collections.each do |inventory_collection|
854
+ attributes += filtered_dependency_attributes.select { |_key, value| value.include?(inventory_collection) }.keys
855
+ end
856
+ attributes
857
+ end
858
+
859
+ # Add passed attributes to blacklist. The manager_ref attributes cannot be blacklisted, otherwise we will not
860
+ # be able to identify the inventory_object. We do not automatically remove attributes causing fixed dependencies,
861
+ # so beware that without them, you won't be able to create the record.
862
+ #
863
+ # @param attributes [Array<Symbol>] Attributes we want to blacklist
864
+ # @return [Array<Symbol>] All blacklisted attributes
865
+ def blacklist_attributes!(attributes)
866
+ self.attributes_blacklist += attributes - (fixed_attributes + internal_attributes)
867
+ end
868
+
869
+ # Add passed attributes to whitelist. The manager_ref attributes always needs to be in the white list, otherwise
870
+ # we will not be able to identify theinventory_object. We do not automatically add attributes causing fixed
871
+ # dependencies, so beware that without them, you won't be able to create the record.
872
+ #
873
+ # @param attributes [Array<Symbol>] Attributes we want to whitelist
874
+ # @return [Array<Symbol>] All whitelisted attributes
875
+ def whitelist_attributes!(attributes)
876
+ self.attributes_whitelist += attributes + (fixed_attributes + internal_attributes)
877
+ end
878
+
879
+ # @return [InventoryCollection] a shallow copy of InventoryCollection, the copy will share data_storage of the
880
+ # original collection, otherwise we would be copying a lot of records in memory.
881
+ def clone
882
+ cloned = self.class.new(:model_class => model_class,
883
+ :manager_ref => manager_ref,
884
+ :association => association,
885
+ :parent => parent,
886
+ :arel => arel,
887
+ :strategy => strategy,
888
+ :saver_strategy => saver_strategy,
889
+ :custom_save_block => custom_save_block,
890
+ # We want cloned IC to be update only, since this is used for cycle resolution
891
+ :update_only => true,
892
+ # Dependency attributes need to be a hard copy, since those will differ for each
893
+ # InventoryCollection
894
+ :dependency_attributes => dependency_attributes.clone)
895
+
896
+ cloned.data_storage = data_storage
897
+ cloned
898
+ end
899
+
900
+ # @return [Array<ActiveRecord::Reflection::BelongsToReflection">] All belongs_to associations
901
+ def belongs_to_associations
902
+ model_class.reflect_on_all_associations.select { |x| x.kind_of?(ActiveRecord::Reflection::BelongsToReflection) }
903
+ end
904
+
905
+ # @return [Hash{Symbol => String}] Hash with association name mapped to foreign key column name
906
+ def association_to_foreign_key_mapping
907
+ return {} unless model_class
908
+
909
+ @association_to_foreign_key_mapping ||= belongs_to_associations.each_with_object({}) do |x, obj|
910
+ obj[x.name] = x.foreign_key
911
+ end
912
+ end
913
+
914
+ # @return [Hash{String => Hash}] Hash with foreign_key column name mapped to association name
915
+ def foreign_key_to_association_mapping
916
+ return {} unless model_class
917
+
918
+ @foreign_key_to_association_mapping ||= belongs_to_associations.each_with_object({}) do |x, obj|
919
+ obj[x.foreign_key] = x.name
920
+ end
921
+ end
922
+
923
+ # @return [Hash{Symbol => String}] Hash with association name mapped to polymorphic foreign key type column name
924
+ def association_to_foreign_type_mapping
925
+ return {} unless model_class
926
+
927
+ @association_to_foreign_type_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
928
+ obj[x.name] = x.foreign_type if x.polymorphic?
929
+ end
930
+ end
931
+
932
+ # @return [Hash{Symbol => String}] Hash with polymorphic foreign key type column name mapped to association name
933
+ def foreign_type_to_association_mapping
934
+ return {} unless model_class
935
+
936
+ @foreign_type_to_association_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
937
+ obj[x.foreign_type] = x.name if x.polymorphic?
938
+ end
939
+ end
940
+
941
+ # @return [Hash{Symbol => String}] Hash with association name mapped to base class of the association
942
+ def association_to_base_class_mapping
943
+ return {} unless model_class
944
+
945
+ @association_to_base_class_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
946
+ obj[x.name] = x.klass.base_class.name unless x.polymorphic?
947
+ end
948
+ end
949
+
950
+ # @return [Array<Symbol>] List of all column names that are foreign keys
951
+ def foreign_keys
952
+ return [] unless model_class
953
+
954
+ @foreign_keys_cache ||= belongs_to_associations.map(&:foreign_key).map!(&:to_sym)
955
+ end
956
+
957
+ # @return [Array<Symbol>] List of all column names that are foreign keys and cannot removed, otherwise we couldn't
958
+ # save the record
959
+ def fixed_foreign_keys
960
+ # Foreign keys that are part of a manager_ref must be present, otherwise the record would get lost. This is a
961
+ # minimum check we can do to not break a referential integrity.
962
+ return @fixed_foreign_keys_cache unless @fixed_foreign_keys_cache.nil?
963
+
964
+ manager_ref_set = (manager_ref - manager_ref_allowed_nil)
965
+ @fixed_foreign_keys_cache = manager_ref_set.map { |x| association_to_foreign_key_mapping[x] }.compact
966
+ @fixed_foreign_keys_cache += foreign_keys & manager_ref
967
+ @fixed_foreign_keys_cache.map!(&:to_sym)
968
+ @fixed_foreign_keys_cache
969
+ end
970
+
971
+ # @return [String] Base class name of the model_class of this InventoryCollection
972
+ def base_class_name
973
+ return "" unless model_class
974
+
975
+ @base_class_name ||= model_class.base_class.name
976
+ end
977
+
978
+ # @return [String] a concise form of the inventoryCollection for easy logging
979
+ def to_s
980
+ whitelist = ", whitelist: [#{attributes_whitelist.to_a.join(", ")}]" if attributes_whitelist.present?
981
+ blacklist = ", blacklist: [#{attributes_blacklist.to_a.join(", ")}]" if attributes_blacklist.present?
982
+
983
+ strategy_name = ", strategy: #{strategy}" if strategy
984
+
985
+ name = model_class || association
986
+
987
+ "InventoryCollection:<#{name}>#{whitelist}#{blacklist}#{strategy_name}"
988
+ end
989
+
990
+ # @return [String] a concise form of the InventoryCollection for easy logging
991
+ def inspect
992
+ to_s
993
+ end
994
+
995
+ # @return [Integer] default batch size for talking to the DB
996
+ def batch_size
997
+ # TODO(lsmola) mode to the settings
998
+ 1000
999
+ end
1000
+
1001
+ # @return [Integer] default batch size for talking to the DB if not using ApplicationRecord objects
1002
+ def batch_size_pure_sql
1003
+ # TODO(lsmola) mode to the settings
1004
+ 10_000
1005
+ end
1006
+
1007
+ # Returns a list of stringified uuids of all scoped InventoryObjects, which is used for scoping in targeted mode
1008
+ #
1009
+ # @return [Array<String>] list of stringified uuids of all scoped InventoryObjects
1010
+ def manager_uuids
1011
+ # TODO(lsmola) LEGACY: this is still being used by :targetel_arel definitions and it expects array of strings
1012
+ raise "This works only for :manager_ref size 1" if manager_ref.size > 1
1013
+ key = manager_ref.first
1014
+ transform_references_to_hashes(targeted_scope.primary_references).map { |x| x[key] }
1015
+ end
1016
+
1017
+ # Builds a multiselection conditions like (table1.a = a1 AND table2.b = b1) OR (table1.a = a2 AND table2.b = b2)
1018
+ #
1019
+ # @param hashes [Array<Hash>] data we want to use for the query
1020
+ # @param keys [Array<Symbol>] keys of attributes involved
1021
+ # @return [String] A condition usable in .where of an ActiveRecord relation
1022
+ def build_multi_selection_condition(hashes, keys = manager_ref)
1023
+ arel_table = model_class.arel_table
1024
+ # We do pure SQL OR, since Arel is nesting every .or into another parentheses, otherwise this would be just
1025
+ # inject(:or) instead of to_sql with .join(" OR ")
1026
+ hashes.map { |hash| "(#{keys.map { |key| arel_table[key].eq(hash[key]) }.inject(:and).to_sql})" }.join(" OR ")
1027
+ end
1028
+
1029
+ # @return [ActiveRecord::Relation] A relation that can fetch all data of this InventoryCollection from the DB
1030
+ def db_collection_for_comparison
1031
+ if targeted?
1032
+ if targeted_arel.respond_to?(:call)
1033
+ targeted_arel.call(self)
1034
+ elsif parent_inventory_collections.present?
1035
+ targeted_arel_default
1036
+ else
1037
+ targeted_iterator_for(targeted_scope.primary_references)
1038
+ end
1039
+ else
1040
+ full_collection_for_comparison
1041
+ end
1042
+ end
1043
+
1044
+ # Builds targeted query limiting the results by the :references defined in parent_inventory_collections
1045
+ #
1046
+ # @return [InventoryRefresh::ApplicationRecordIterator] an iterator for default targeted arel
1047
+ def targeted_arel_default
1048
+ if parent_inventory_collections.collect { |x| x.model_class.base_class }.uniq.count > 1
1049
+ raise "Multiple :parent_inventory_collections with different base class are not supported by default. Write "\
1050
+ ":targeted_arel manually, or separate [#{self}] into 2 InventoryCollection objects."
1051
+ end
1052
+ parent_collection = parent_inventory_collections.first
1053
+ references = parent_inventory_collections.map { |x| x.targeted_scope.primary_references }.reduce({}, :merge!)
1054
+
1055
+ parent_collection.targeted_iterator_for(references, full_collection_for_comparison)
1056
+ end
1057
+
1058
+ # Gets targeted references and transforms them into list of hashes
1059
+ #
1060
+ # @param references [Array, InventoryRefresh::Inventorycollection::TargetedScope] passed references
1061
+ # @return [Array<Hash>] References transformed into the array of hashes
1062
+ def transform_references_to_hashes(references)
1063
+ if references.kind_of?(Array)
1064
+ # Sliced InventoryRefresh::Inventorycollection::TargetedScope
1065
+ references.map { |x| x.second.full_reference }
1066
+ else
1067
+ references.values.map(&:full_reference)
1068
+ end
1069
+ end
1070
+
1071
+ # Builds a multiselection conditions like (table1.a = a1 AND table2.b = b1) OR (table1.a = a2 AND table2.b = b2)
1072
+ # for passed references
1073
+ #
1074
+ # @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] passed references
1075
+ # @return [String] A condition usable in .where of an ActiveRecord relation
1076
+ def targeted_selection_for(references)
1077
+ build_multi_selection_condition(transform_references_to_hashes(references))
1078
+ end
1079
+
1080
+ # Returns iterator for the passed references and a query
1081
+ #
1082
+ # @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] Passed references
1083
+ # @param query [ActiveRecord::Relation] relation that can fetch all data of this InventoryCollection from the DB
1084
+ # @return [InventoryRefresh::ApplicationRecordIterator] Iterator for the references and query
1085
+ def targeted_iterator_for(references, query = nil)
1086
+ InventoryRefresh::ApplicationRecordIterator.new(
1087
+ :inventory_collection => self,
1088
+ :manager_uuids_set => references,
1089
+ :query => query
1090
+ )
1091
+ end
1092
+
1093
+ # Builds an ActiveRecord::Relation that can fetch all the references from the DB
1094
+ #
1095
+ # @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] passed references
1096
+ # @return [ActiveRecord::Relation] relation that can fetch all the references from the DB
1097
+ def db_collection_for_comparison_for(references)
1098
+ full_collection_for_comparison.where(targeted_selection_for(references))
1099
+ end
1100
+
1101
+ # Builds an ActiveRecord::Relation that can fetch complement of all the references from the DB
1102
+ #
1103
+ # @param manager_uuids_set [Array<String>] passed references
1104
+ # @return [ActiveRecord::Relation] relation that can fetch complement of all the references from the DB
1105
+ def db_collection_for_comparison_for_complement_of(manager_uuids_set)
1106
+ # TODO(lsmola) this should have the build_multi_selection_condition, like in the method above
1107
+ # TODO(lsmola) this query will be highly ineffective, we will try approach with updating a timestamp of all
1108
+ # records, then we can get list of all records that were not update. That would be equivalent to result of this
1109
+ # more effective query and without need of all manager_uuids
1110
+ full_collection_for_comparison.where.not(manager_ref.first => manager_uuids_set)
1111
+ end
1112
+
1113
+ # @return [ActiveRecord::Relation] relation that can fetch all the references from the DB
1114
+ def full_collection_for_comparison
1115
+ return arel unless arel.nil?
1116
+ parent.send(association)
1117
+ end
1118
+
1119
+ # Creates InventoryRefresh::InventoryObject object from passed hash data
1120
+ #
1121
+ # @param hash [Hash] Object data
1122
+ # @return [InventoryRefresh::InventoryObject] Instantiated InventoryRefresh::InventoryObject
1123
+ def new_inventory_object(hash)
1124
+ manager_ref.each do |x|
1125
+ # TODO(lsmola) with some effort, we can do this, but it's complex
1126
+ raise "A lazy_find with a :key can't be a part of the manager_uuid" if inventory_object_lazy?(hash[x]) && hash[x].key
1127
+ end
1128
+
1129
+ inventory_object_class.new(self, hash)
1130
+ end
1131
+
1132
+ attr_writer :attributes_blacklist, :attributes_whitelist
1133
+
1134
+ private
1135
+
1136
+ # Creates dynamically a subclass of InventoryRefresh::InventoryObject, that will be used per InventoryCollection
1137
+ # object. This approach is needed because we want different InventoryObject's getters&setters for each
1138
+ # InventoryCollection.
1139
+ #
1140
+ # @return [InventoryRefresh::InventoryObject] new isolated subclass of InventoryRefresh::InventoryObject
1141
+ def inventory_object_class
1142
+ @inventory_object_class ||= begin
1143
+ klass = Class.new(::InventoryRefresh::InventoryObject)
1144
+ klass.add_attributes(inventory_object_attributes) if inventory_object_attributes
1145
+ klass
1146
+ end
1147
+ end
1148
+
1149
+ # Returns array of records identities
1150
+ #
1151
+ # @param records [Array<ApplicationRecord>, Array[Hash]] list of stored records
1152
+ # @return [Array<Hash>] array of records identities
1153
+ def records_identities(records)
1154
+ records = [records] unless records.respond_to?(:map)
1155
+ records.map { |record| record_identity(record) }
1156
+ end
1157
+
1158
+ # Returns a hash with a simple record identity
1159
+ #
1160
+ # @param record [ApplicationRecord, Hash] list of stored records
1161
+ # @return [Hash{Symbol => Bigint}] record identity
1162
+ def record_identity(record)
1163
+ identity = record.try(:[], :id) || record.try(:[], "id") || record.try(:id)
1164
+ raise "Cannot obtain identity of the #{record}" if identity.blank?
1165
+ {
1166
+ :id => identity
1167
+ }
1168
+ end
1169
+
1170
+ # @return [Array<Symbol>] all association attributes and foreign keys of the model class
1171
+ def association_attributes
1172
+ model_class.reflect_on_all_associations.map { |x| [x.name, x.foreign_key] }.flatten.compact.map(&:to_sym)
1173
+ end
1174
+ end
1175
+ end