How do people deploy this stuff declaratively, especially cleaning up old functions? I'm still surprised something terraform-esque doesn't exist.

Bunch of SQL scripts with "CREATE OR REPLACE" and cross your fingers? Make a schema that a script can wholly own, then blow the whole thing away and reinitialize in a transaction?

I use this https://github.com/golang-migrate/migrate in a deploy step to each environment with the upgrade / downgrade scripts committed to the repo alongside the code. The scripts can do pretty much anything you need to do in PG including defining and executing functions.