Each replacement depends on the result of the last. You need a loop of some kind. And you need a deterministic order within replacements. Assuming rule_id
in ascending order. And assuming you want to replace any match, not just whole words (easy to adapt).
You could loop in a plpgsql function. Probably faster. See:
Or, for pure SQL, try this recursive CTE:
WITH RECURSIVE cte AS (
SELECT s.content_id, r.rule_id
, replace(s.content, r.old_string, r.new_string) AS content
FROM some_content s
CROSS JOIN (
SELECT rule_id, old_string, new_string
FROM rewrite
ORDER BY rule_id -- order of rows is relevant!
LIMIT 1
) r
UNION ALL
SELECT c.content_id, r.rule_id
, replace(c.content, r.old_string, r.new_string) AS content
FROM cte c
, LATERAL (
SELECT rule_id, old_string, new_string
FROM rewrite
WHERE rule_id > c.rule_id
ORDER BY rule_id -- order of rows is relevant!
LIMIT 1
) r
)
SELECT DISTINCT ON (content_id) content
FROM cte
ORDER BY content_id, rule_id DESC;
The LATERAL
join to work around "invalid reference to FROM-clause entry for table "c"
you would get with a direct subquery referencing the CTE. Related:
Or, using row_number()
to produces a serial number without gaps like you commented:
WITH RECURSIVE r AS (
SELECT old_string, new_string
, row_number() OVER (ORDER BY rule_id) AS rn -- your ORDER BY expression?
FROM rewrite
)
, cte AS (
SELECT s.content_id, r.rn
, replace(s.content, r.old_string, r.new_string) AS content
FROM some_content s
JOIN r ON r.rn = 1
UNION ALL
SELECT s.content_id, r.rn
, replace(s.content, r.old_string, r.new_string) AS content
FROM cte s
JOIN r ON r.rn = s.rn + 1
)
SELECT DISTINCT ON (content_id) content
FROM cte
ORDER BY content_id, rn DESC;
dbfiddle here
It's often overlooked that plain CTEs can still be added after WITH RECURSIVE
:
About DISTINCT ON
: