Recursive CTE rows can carry derived text such as the path taken through a dependency graph.

Program

Play the script to choose a starting node and see the routes reachable from it.

dependency_paths.sql
CREATE TABLE edges (parent TEXT, child TEXT);
INSERT INTO edges VALUES ('app', 'api'), ('app', 'worker'), ('api', 'db'), ('api', 'cache'), ('worker', 'queue'), ('queue', 'db');
WITH RECURSIVE params(start_node) AS (VALUES ()), walk(node, route, depth) AS (SELECT (SELECT start_node FROM params), (SELECT start_node FROM params), 0 UNION ALL SELECT edges.child, walk.route || ' > ' || edges.child, walk.depth + 1 FROM edges JOIN walk ON edges.parent = walk.node) SELECT node, route, depth FROM walk ORDER BY depth, node;
CREATE TABLE edges (parent TEXT, child TEXT);
INSERT INTO edges VALUES ('app', 'api'), ('app', 'worker'), ('api', 'db'), ('api', 'cache'), ('worker', 'queue'), ('queue', 'db');
WITH RECURSIVE params(start_node) AS (VALUES ()), walk(node, route, depth) AS (SELECT (SELECT start_node FROM params), (SELECT start_node FROM params), 0 UNION ALL SELECT edges.child, walk.route || ' > ' || edges.child, walk.depth + 1 FROM edges JOIN walk ON edges.parent = walk.node) SELECT node, route, depth FROM walk ORDER BY depth, node;
CREATE TABLE edges (parent TEXT, child TEXT);
INSERT INTO edges VALUES ('app', 'api'), ('app', 'worker'), ('api', 'db'), ('api', 'cache'), ('worker', 'queue'), ('queue', 'db');
WITH RECURSIVE params(start_node) AS (VALUES ()), walk(node, route, depth) AS (SELECT (SELECT start_node FROM params), (SELECT start_node FROM params), 0 UNION ALL SELECT edges.child, walk.route || ' > ' || edges.child, walk.depth + 1 FROM edges JOIN walk ON edges.parent = walk.node) SELECT node, route, depth FROM walk ORDER BY depth, node;
graph edges Each `edges` row points from one node to a dependent node.
carried route `walk.route || ' > ' || edges.child` records the path as recursion advances.
bounded data The fixed acyclic data keeps the replay deterministic and finite.