ergon_graph (ergon v0.5.0)
View SourceWorkflow dependency resolution over PostgreSQL 19's SQL/PGQ property graph.
The ergon.workflow graph maps jobs and edges into job vertices connected by
triggers edges, so "which jobs are ready to run?" is a single GRAPH_TABLE
match rather than a hand-rolled join. Its vertex table is ergon.jobs_current,
not ergon.jobs: under the temporal primary key a job owns one row per
valid-time version, and using the base table gave a completed parent one vertex
per version, which made ready_children/0 return nothing for any parent that had
ever run. See the property graph comment in priv/migrations/schema.
These are observability queries, not the scheduler
Dependencies are enforced in checkout.sql through ergon.jobs.pending_parents,
so a blocked job is simply not checked out and nothing here needs to be consulted
for correctness. ready_children/0 and direct_children/1 answer "what is the
workflow doing?", which is what a dashboard or an operator wants.
Multi-hop reachability uses a recursive CTE rather than a graph walk. PG19's
SQL/PGQ has no path quantifiers, so variable-length reachability cannot be
expressed in MATCH at all.
Summary
Functions
Every job id reachable from AncestorId through triggers edges: its transitive
closure, and the set a cascade would touch.
The ids of the available jobs a completed job directly unblocks: everything one
triggers hop away.
The ids of every available job whose workflow parents have all completed.
Whether adding ParentId -> ChildId would introduce a cycle, including a
self-loop.
Types
-type db_error() :: empty_result | would_create_cycle | {job_not_found, ergon_job:job_id()} | {pgo_error, map()} | term().
Functions
-spec descendants(ergon_job:job_id()) -> {ok, [ergon_job:job_id()]} | {error, db_error()}.
Every job id reachable from AncestorId through triggers edges: its transitive
closure, and the set a cascade would touch.
-spec direct_children(ergon_job:job_id()) -> {ok, [ergon_job:job_id()]} | {error, db_error()}.
The ids of the available jobs a completed job directly unblocks: everything one
triggers hop away.
-spec ready_children() -> {ok, [ergon_job:job_id()]} | {error, db_error()}.
The ids of every available job whose workflow parents have all completed.
Equivalently: the jobs whose pending_parents has just reached zero. The two
are maintained independently (one by triggers on the row, one by a graph match),
so a disagreement between them means the counter has drifted, which makes this a
useful cross-check for the reconciler as well as for a dashboard.
-spec would_create_cycle(ergon_job:job_id(), ergon_job:job_id()) -> {ok, boolean()} | {error, db_error()}.
Whether adding ParentId -> ChildId would introduce a cycle, including a
self-loop.
ergon_db:link/3 runs this itself, inside a transaction and behind an advisory
lock, so calling it beforehand is advisory only: between this answer and a later
link/3 another process may have added the edge that closes the loop.