It's a recursive query, but it's basically the simplest recursive query. It's not
that bad.
WITH recursive_traversal(depth, id)
AS (
-- base case: the root has no previous
SELECT 0, id
FROM todo
WHERE previous IS NULL
AND user_id = $1
UNION ALL
-- recursive case: traverse to next row
SELECT depth + 1, todo.id
FROM todo
JOIN recursive_traversal r ON r.next = todo.id
WHERE depth < $2
)
-- we only accumulated the ids, so join one more time to get the rest of the columns
SELECT todo.*
FROM todo
JOIN recursive_traversal USING (id);