Meanwhile this alternative query seems to do the job: ==== select name || ' ::=' || group_concat( case when hasOr == 1 then ' (' || trim(rtxt) || ')' else case when length(rtxt) == 0 then ' /* empty */' else rtxt end end, ' |') from ( select lhs, name, substr(txt, instr(txt, tbl.sep) + length(tbl.sep)) rtxt, (instr(txt, '|') > 0) hasOr from rule left join symbol on lhs=id, (select '::=' as sep) tbl ) as t group by lhs; ==== Partial output: ==== input ::= cmdlist cmdlist ::= cmdlist ecmd | ecmd ecmd ::= SEMI | cmdx SEMI | explain cmdx SEMI cmdx ::= cmd explain ::= EXPLAIN | EXPLAIN QUERY PLAN cmd ::= BEGIN transtype trans_opt | (COMMIT|END trans_opt) | ROLLBACK trans_opt | SAVEPOINT nm ====