I don't really know what I am talking about, But I thought that postgres function volatility was introduced for this exact reason, that is, As a clue to the optimizer where it can and cannot push stuff down.
https://www.postgresql.org/docs/15/xfunc-volatility.html
I do note that the postgres explain appears to keep the join filter on the top level.
explain select count(*) from
generate_series(0,999) a
inner join
generate_series(0,999)
on random() < 0.5
;
QUERY PLAN
--------------------------------------------------------------------------------------
Aggregate (cost=25843.34..25843.35 rows=1 width=8)
-> Nested Loop (cost=0.01..25010.01 rows=333333 width=0)
Join Filter: (random() < '0.5'::double precision)
-> Function Scan on generate_series a (cost=0.00..10.00 rows=1000 width=0)
-> Function Scan on generate_series (cost=0.00..10.00 rows=1000 width=0)