inventory_refresh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +4 -0
  5. data/.rspec_ci +4 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +5 -0
  8. data/.rubocop_local.yml +2 -0
  9. data/.travis.yml +12 -0
  10. data/.yamllint +12 -0
  11. data/CHANGELOG.md +0 -0
  12. data/Gemfile +6 -0
  13. data/LICENSE +202 -0
  14. data/README.md +35 -0
  15. data/Rakefile +47 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/inventory_refresh.gemspec +34 -0
  19. data/lib/inventory_refresh.rb +11 -0
  20. data/lib/inventory_refresh/application_record_iterator.rb +56 -0
  21. data/lib/inventory_refresh/application_record_reference.rb +15 -0
  22. data/lib/inventory_refresh/graph.rb +157 -0
  23. data/lib/inventory_refresh/graph/topological_sort.rb +66 -0
  24. data/lib/inventory_refresh/inventory_collection.rb +1175 -0
  25. data/lib/inventory_refresh/inventory_collection/data_storage.rb +178 -0
  26. data/lib/inventory_refresh/inventory_collection/graph.rb +170 -0
  27. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +230 -0
  28. data/lib/inventory_refresh/inventory_collection/index/type/base.rb +80 -0
  29. data/lib/inventory_refresh/inventory_collection/index/type/data.rb +26 -0
  30. data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +286 -0
  31. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +116 -0
  32. data/lib/inventory_refresh/inventory_collection/reference.rb +96 -0
  33. data/lib/inventory_refresh/inventory_collection/references_storage.rb +106 -0
  34. data/lib/inventory_refresh/inventory_collection/scanner.rb +117 -0
  35. data/lib/inventory_refresh/inventory_collection/serialization.rb +140 -0
  36. data/lib/inventory_refresh/inventory_object.rb +303 -0
  37. data/lib/inventory_refresh/inventory_object_lazy.rb +151 -0
  38. data/lib/inventory_refresh/save_collection/base.rb +38 -0
  39. data/lib/inventory_refresh/save_collection/recursive.rb +52 -0
  40. data/lib/inventory_refresh/save_collection/saver/base.rb +390 -0
  41. data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
  42. data/lib/inventory_refresh/save_collection/saver/concurrent_safe.rb +71 -0
  43. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +632 -0
  44. data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
  45. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +85 -0
  46. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +120 -0
  47. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +196 -0
  48. data/lib/inventory_refresh/save_collection/topological_sort.rb +38 -0
  49. data/lib/inventory_refresh/save_inventory.rb +38 -0
  50. data/lib/inventory_refresh/target.rb +73 -0
  51. data/lib/inventory_refresh/target_collection.rb +80 -0
  52. data/lib/inventory_refresh/version.rb +3 -0
  53. data/tools/ci/create_db_user.sh +3 -0
  54. metadata +207 -0
@@ -0,0 +1,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