Skip to content

Rewrite dependency resolution on algebraic-graphs (fixes exponential non-linear DAG slowdown)#2

Open
ocharles wants to merge 2 commits into
masterfrom
algebraic-graphs-rewrite
Open

Rewrite dependency resolution on algebraic-graphs (fixes exponential non-linear DAG slowdown)#2
ocharles wants to merge 2 commits into
masterfrom
algebraic-graphs-rewrite

Conversation

@ocharles

@ocharles ocharles commented Jul 8, 2026

Copy link
Copy Markdown

Alternative to (and superset of) #1 — this branch contains that PR's commit plus a rewrite of dependency resolution on top of algebraic-graphs, which postdates this library's original design. Merging this makes #1 redundant.

Problem

Same as #1: dependenciesWith enumerated every path through the dependency graph and deduplicated with the quadratic cleanLDups, which is exponential in the depth of a non-linear DAG. upgrade runs this once per missing migration, so any non-linear migration history becomes unusably slow — forcing migration histories to be kept fully serialized.

Fix

Two commits:

  1. The order-preserving linear-time fix from Fix exponential dependency resolution on non-linear DAGs #1, plus the non-linear stress test (test/NonLinearStressTest.hs), which simulates the upgrade command over a "diamond ladder" (two migrations per level, each depending on both migrations of the previous level) and asserts correctness + a 10s time limit.
  2. A rewrite of Database.Schema.Migrations.Dependencies on algebraic-graphs (net −133 lines):
    • The graph is an AdjacencyMap Text keyed by migration name, built with stars; all fgl node-index bookkeeping (depGraphObjectMap/depGraphNameMap) disappears, and fgl is dropped as a dependency.
    • dependencies/reverseDependencies are reachable + induce + topSort (the reverse direction via transpose) — exponential blowup is impossible by construction.
    • CycleDetection (hand-rolled tricolor DFS over assoc lists) is deleted: topSort returns Left with the actual cycle, so mkDepGraph finally reports which migrations form the cycle, resolving the long-standing XXX comment. Its test cases are ported to mkDepGraph-level tests.
    • mkDepGraph now rejects duplicate object identifiers explicitly (previously silently tolerated, with name lookups resolving to the first match while the graph contained both vertices).

Performance

Full simulated upgrade of the diamond ladder:

migrations before this PR
46 (ladder depth 22) 54.9 s 2.3 ms
402 — (hours) 0.23 s
2002 7.4 s
4002 33 s

The residual superlinear growth at implausibly large store sizes is upgrade recomputing an O(n) plan once per missing migration — inherent to the command's shape.

Ordering semantics

topSort returns the lexicographically smallest topological ordering, so apply/revert order is now documented-deterministic rather than an artifact of graph node numbering (i.e. directory listing order). Any result is still a valid topological order, but where independent sibling migrations were previously ordered by node number they are now ordered by name; exactly one DependencyTest expectation changed accordingly (verified by hand to be a valid revert order). Linear migration histories are unaffected — their topological order is unique — so this changes nothing for a fully serialized store like ours.

This ordering change is the difference from #1, which reproduced the legacy ordering exactly. If we want zero observable change, merge #1 alone; if we're happy adopting deterministic lexicographic ordering (recommended), merge this and close #1.

🤖 Generated with Claude Code

ocharles and others added 2 commits July 8, 2026 17:03
dependenciesWith enumerated every path from the start object (each
object's direct successor names followed by the recursive expansion of
each name), then deduplicated the result with the quadratic cleanLDups.
The number of paths is exponential in the depth of a non-linear
dependency graph, so an upgrade over a "diamond ladder" of just 46
migrations took ~55 seconds.

Produce the identical ordering in linear time by scanning the virtual
path enumeration from the end: walk successor names in reverse order,
expand each name's subtree at most once (a repeated expansion occurs
earlier in the enumeration, so it cannot contain a last occurrence),
and emit each name the first time it is seen, i.e. at its last
occurrence. Like the old code, the traversal is name-driven rather
than node-driven, so behaviour is preserved even for graphs with
duplicate object names.

Also replace the assoc-list lookup in dependenciesWith and the
list-membership filters in migrationsToApply/migrationsToRevert with
Map/Set equivalents.

Add a stress test that simulates the upgrade command over a
diamond-ladder graph (two migrations per level, each depending on both
migrations of the previous level). It now runs in ~3ms where it
previously took ~55s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the fgl-based dependency graph and the hand-rolled traversal
and cycle detection with algebraic-graphs, which postdates this
library's original design:

- The graph is now an AdjacencyMap keyed by object name, built
  directly with `stars`, so the node-index bookkeeping
  (depGraphObjectMap/depGraphNameMap) disappears.

- dependencies/reverseDependencies are `reachable` + `induce` +
  `topSort` (reverseDependencies via `transpose`). topSort returns the
  lexicographically smallest topological ordering, so results are
  deterministic by construction rather than dependent on graph node
  numbering (i.e. on directory listing order).

- The CycleDetection module is deleted: cycle rejection now comes from
  `topSort`'s Left result, which contains the actual cycle, so
  mkDepGraph can finally report which objects form the cycle
  (resolving the long-standing XXX comment). Its test cases are ported
  to mkDepGraph-level tests.

- mkDepGraph now rejects duplicate object identifiers explicitly.
  Previously duplicates were silently tolerated with confusing
  behaviour (name lookups resolved to the first match while the graph
  contained both vertices).

Ordering semantics: any result is still a valid topological order, but
the specific order changes where siblings were previously ordered by
node number; one DependencyTest expectation is updated accordingly.
Linear migration histories are unaffected (their topological order is
unique).

The non-linear stress test runs marginally faster than the previous
implementation (46 migrations: 2.3ms; 2002 migrations: 7.4s vs 9.8s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant