Module: Autonoma::Graph
- Defined in:
- lib/autonoma/graph.rb
Class Method Summary collapse
-
.find_deferrable_edge(cycle, edges) ⇒ Object
Find a nullable FK edge in a cycle that can be deferred.
-
.topo_sort(nodes, edges) ⇒ Object
Topological sort of nodes by FK dependency edges.
Class Method Details
.find_deferrable_edge(cycle, edges) ⇒ Object
Find a nullable FK edge in a cycle that can be deferred.
82 83 84 85 86 87 88 89 90 |
# File 'lib/autonoma/graph.rb', line 82 def self.find_deferrable_edge(cycle, edges) cycle_set = cycle.to_set edges.find do |e| cycle_set.include?(e["from"]) && cycle_set.include?(e["to"]) && e["from"] != e["to"] && e["nullable"] == true end end |
.topo_sort(nodes, edges) ⇒ Object
Topological sort of nodes by FK dependency edges. Returns => […], “cycles” => [[…]].
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/autonoma/graph.rb', line 9 def self.topo_sort(nodes, edges) node_set = nodes.to_set # Filter to only edges between known nodes, skip self-referential relevant_edges = edges.select do |e| e["from"] != e["to"] && node_set.include?(e["from"]) && node_set.include?(e["to"]) end # Build in-degree map and adjacency list in_degree = nodes.each_with_object({}) { |n, h| h[n] = 0 } adj = Hash.new { |h, k| h[k] = [] } relevant_edges.each do |e| # e["from"] depends on e["to"] (from has the FK pointing to to) adj[e["to"]] << e["from"] in_degree[e["from"]] = (in_degree[e["from"]] || 0) + 1 end # First pass: standard Kahn's queue = in_degree.select { |_, d| d == 0 }.keys.sort sorted_nodes = [] until queue.empty? node = queue.shift sorted_nodes << node (adj[node] || []).each do |neighbor| in_degree[neighbor] -= 1 queue << neighbor if in_degree[neighbor] == 0 end queue.sort! end # Find unsorted nodes unsorted = nodes.reject { |n| sorted_nodes.include?(n) } return { "sorted" => sorted_nodes, "cycles" => [] } if unsorted.empty? cycles = find_sccs(unsorted, relevant_edges) return { "sorted" => sorted_nodes, "cycles" => [] } if cycles.empty? # Second pass: treat cycle nodes as resolved and re-sort remaining cycle_nodes = cycles.flatten.to_set still_unsorted = unsorted.reject { |n| cycle_nodes.include?(n) } if still_unsorted.any? still_set = still_unsorted.to_set in_deg2 = still_unsorted.each_with_object({}) { |n, h| h[n] = 0 } adj2 = Hash.new { |h, k| h[k] = [] } relevant_edges.each do |e| if still_set.include?(e["from"]) && still_set.include?(e["to"]) adj2[e["to"]] << e["from"] in_deg2[e["from"]] = (in_deg2[e["from"]] || 0) + 1 end end queue2 = in_deg2.select { |_, d| d == 0 }.keys.sort until queue2.empty? node = queue2.shift sorted_nodes << node (adj2[node] || []).each do |neighbor| in_deg2[neighbor] -= 1 queue2 << neighbor if in_deg2[neighbor] == 0 end queue2.sort! end end { "sorted" => sorted_nodes, "cycles" => cycles } end |