I also found that the use-case which most of these migration tools optimize for (being somewhat independent of specific SQL dialects) rarely works in practice, except maybe if you stick to the smallest common denominator of all SQL dialects. And that again is wasteful IMO as not using a lot of the functionality that's present e.g. in Postgres can make your life a lot harder.
A few years back I switched to plain SQL based migrations (using a lightweight library called DbUp to execute them) - I couldn't be happier with it!
What better tool to express database changes with than SQL? I also like the transparency it brings you - the files are right there for you to read. And I'm a stickler for formatting and convention, so it's great being able to write SQL files that include comments and use my preferred formatting.
This has both the pro and con of your app being quite tightly coupled with the database.
I mean, this is the case any time your app talks directly to a database
Personally, I would start writing the abstraction as soon as I plan to have more than two apps.
Which is in most cases desirable, but for example if you want to use database features that SQLAlchemy/Alembic don't (yet) support it can get a bit messy and/or require some manual plumbing to keep getting the migrations you want.