Module: Exwiw::DetermineTableProcessingOrder

Defined in:
lib/exwiw/determine_table_processing_order.rb

Class Method Summary collapse

Class Method Details

.compute_table_dependencies(table) ⇒ Object

The belongs_to target table names of table. A polymorphic belongs_to is expanded into one entry per concrete target by schema generation, so each entry is a plain table name here.



90
91
92
# File 'lib/exwiw/determine_table_processing_order.rb', line 90

def compute_table_dependencies(table)
  table.belongs_tos.map(&:table_name)
end

.run(tables, logger: nil, runtime_reverse_scope: false) ⇒ Array<String>

Returns sorted table names.

Parameters:

  • tables (Array<Exwiw::TableConfig>)

    tables

  • logger (Logger, nil) (defaults to: nil)

    receives a warning when a cycle has to be broken

  • runtime_reverse_scope (Boolean) (defaults to: false)

    when true (mongodb), a table declaring reverse_scope is ordered AFTER its via referencer tables: the adapter builds the reverse filter from ids captured at runtime while the referencers were dumped, so they must be processed first. Each arm's own belongs_to edge back to the reverse-scoped table (the usual referencer.fk -> hub relation) is inverted rather than kept, since the declaration states ids flow referencer -> hub. False (the default) preserves the historical belongs_to-only ordering — the SQL adapters scope via subqueries and need the hub emitted before its referencers so the INSERT output stays loadable in foreign-key order.

Returns:

  • (Array<String>)

    sorted table names



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
80
81
82
83
84
85
# File 'lib/exwiw/determine_table_processing_order.rb', line 22

def run(tables, logger: nil, runtime_reverse_scope: false)
  return tables.map(&:name) if tables.size < 2

  ordered_table_names = []
  ordered = Set.new

  table_by_name = tables.each_with_object({}) do |table, acc|
    acc[table.name] = table
  end

  reverse_scope_deps = runtime_reverse_scope ? compute_reverse_scope_dependencies(tables) : {}
  dependencies_by_name = tables.each_with_object({}) do |table, acc|
    acc[table.name] = compute_dependencies(table, reverse_scope_deps)
  end

  # Only relations whose target is also in this run constrain the order. A
  # dependency pointing at a table that is not being processed here — e.g.
  # an embedded MongoDB collection (masked through its parent, never dumped
  # on its own) or any table excluded from the run — is not something we can
  # or need to order against, so it must never block resolution. Without
  # this, such a dependency would stay unresolved forever and masquerade as
  # a circular dependency, freezing every table that (transitively)
  # references it.
  present_names = table_by_name.keys.to_set

  loop do
    break if table_by_name.empty?

    resolvable = table_by_name.values.select do |table|
      unresolved_dependencies(table.name, dependencies_by_name, present_names, ordered).empty?
    end

    if resolvable.empty?
      # No table has all its (in-run) dependencies satisfied, yet tables
      # remain: the dependency graph has a genuine cycle and no strict
      # topological order exists.
      #
      # When a reverse_scope ordering edge participates in the cycle, there
      # is no safe way out: emitting the reverse-scoped table before an arm
      # would build its filter from missing state (silently dropping rows),
      # so fail loudly instead of guessing.
      detect_reverse_scope_cycle!(table_by_name, dependencies_by_name, reverse_scope_deps, present_names, ordered)

      # Otherwise the cycle is a plain belongs_to cycle. Rather than
      # aborting the whole export, break it by emitting one cycle member;
      # see pick_cycle_victim for how the member is chosen. Warn so the
      # dropped constraint is visible.
      victim = pick_cycle_victim(table_by_name.values, dependencies_by_name, present_names, ordered)
      warn_cycle_break(logger, victim, unresolved_dependencies(victim.name, dependencies_by_name, present_names, ordered))
      resolvable = [victim]
    end

    # In the normal (acyclic) path, emit every currently-resolvable table in
    # insertion order — preserving the historical ordering the snapshot specs
    # depend on. The cycle-break path emits exactly its single chosen victim.
    resolvable.each do |table|
      ordered_table_names << table.name
      ordered << table.name
      table_by_name.delete(table.name)
    end
  end

  ordered_table_names
end