interesting challenge, and we will be working on something similar in coming months. I think the way out is to not model the deletions as events. They are not events arriving from a source, they are a property of the dataset ie, the table should only contain the last two years. There are two possible approaches:
(a) If queries just need to see the window, make retention part of the definition. Keep bronze append-only and define silver as a materialized view:
```sql
CREATE MATERIALIZED VIEW silver_history AS
SELECT * FROM bronze_history
WHERE yyyymm >= cast(date_format(add_months(current_date(), -24), 'yyyyMM') as int)
```
That removes the delete events, the circular dependency, and the support table, and ordering is handled by the pipeline graph. The caveat is cost. A predicate that moves with current_date() is non-deterministic, which tends to prevent incremental refresh, so on a large table each refresh can recompute the whole window.
(b) If the rows must physically leave the table, or it is too large to keep recomputing, keep the pipeline append-only and delete outside it using a scheduled `DELETE FROM silver_history WHERE yyyymm < :cutoff`, or a SQL task after the pipeline task in the same job. Cluster the table by yyyymm and the monthly delete mostly drops whole files, so it stays cheap as the table grows.