Short version for the impatient: PostgreSQL Migrator 1.0 is the best MySQL to PostgreSQL inspection tool I've used, and it still can't do the one thing that keeps me up the night before a cutover. If you want to know why I'm running it anyway, read on.
I have a client on a MariaDB 10.6 database that was born in 2014 as a WordPress plugin's side table and grew into the system of record for a small logistics company. About 140 tables, a few hundred million rows in the two big ones, and a pile of stored routines nobody has opened since the person who wrote them left. They want it on Postgres. I want it on Postgres. The question has always been how many weekends that costs, and the honest answer for the last three years has been "I don't know, and the tool that would tell me doesn't exist."
Then on 7 September Dalibo shipped PostgreSQL Migrator 1.0, after a year of betas and release candidates. I spent Tuesday evening pointing it at a copy of that database. Here's what it told me, what it couldn't tell me, and where pgloader still lives in my toolbox.
What the tool actually is
It's a single Go binary, no JVM, no Python environment, no Oracle client libraries unless you're migrating from Oracle. You run pg_migrate init with a source DSN, it creates a project directory with a pg_migrate.toml and a .env for secrets, and from there the workflow is inspect, convert, dump, verify. The CLI reference is short enough to read in one sitting, which I appreciated after years of squinting at ora2pg's config file.
The part I didn't expect is the offline mode. pg_migrate inspect connects to the source once, pulls the entire catalog (tables, columns, constraints, indexes, routines, triggers, even scheduled jobs) into JSON, and then everything after that runs against the JSON. You can tweak conversion rules, re-run convert, and browse the result in a local web UI without touching production again. For a client who gets nervous every time I open a connection to their live database, that's a real selling point. One inspection window, agreed in advance, and then I'm working on a file.
The complexity scoring is the other thing that's new to me. Every object gets a score and a list of annotated compatibility issues, and the project as a whole gets a number you can compare across databases. I ran it against the logistics database and against a smaller Laravel app's MySQL schema I had lying around, and the scores landed roughly where my gut said they would. The logistics one was about four times harder. Having that as a number I can put in a proposal, instead of "trust me, this one's worse," is worth the install on its own.
Where the 1.0 label is doing a lot of work
Now the part where I push back a bit.
The announcement says inspection is complete. It also says, in the next sentence, that conversion and migration are limited to roles, schemas, sequences, tables, virtual columns, constraints and indexes. Views are on the roadmap. Procedural code goes through the companion transqlate tool for analysis and transpilation, but the migrator itself doesn't convert it end to end yet.
I want to be fair here: that's a sensible scope for a 1.0, and the features matrix is unusually honest about what's implemented, partial, and not started. But two rows in that matrix matter more than the rest for anyone doing a real cutover.
The first is "Consistent dump using snapshot mechanisms: not implemented." That means the data copy is not a point-in-time snapshot of the source. If writes are still happening while the dump runs, table A and table B can come from different moments. For a read-mostly reporting database that's fine. For the logistics system, where a shipment row and its status-history rows have to agree, it means I have to stop writes for the duration of the copy. I was hoping 1.0 would let me skip the maintenance window. It doesn't.
The second is "Post-dump data validation: not implemented." The tool copies the data and doesn't count it afterwards. So the verify step in the getting-started guide is, for now, you and psql and a row-count script. That's what I did with pgloader in 2022 too, so it's not a regression, but I'd assumed a tool with this much inspection machinery would close the loop.
Neither of these is hidden. They're on the features page in a red circle. My complaint is only that the phrase "stable release" pulled my expectations somewhere the documentation quietly walks them back from.
The COPY path is fast, and I checked
The announcement claims "significantly higher throughput than traditional tools in our tests." I've read enough vendor benchmarks to treat that sentence as decoration, so I ran my own on the two big tables.
Source was the MariaDB copy on one Hetzner box, target was Postgres 17 on another in the same region, both on the kind of VPS I describe in my Hetzner vs DigitalOcean writeup. I used the migrator's streamed bulk COPY with the multi-thread dump, then repeated the load of the same tables with pgloader using its default MySQL settings.
The migrator finished the larger table (about 210 million narrow rows) in a bit under 19 minutes. pgloader took 31 on the same hardware. Memory on the migrator process stayed flat the whole time, which matches the "low memory usage" claim; pgloader climbed and then plateaued, which is what I remembered from last time. I ran each once, so this is an anecdote with a stopwatch, not a benchmark. But a 40 percent gap on a single run is bigger than noise, and the flat memory line is the thing I'd actually care about on a box that's also serving traffic.
One wrinkle: the dump can also write to files or a pipe instead of straight into Postgres. I didn't use that mode, but for a client whose Postgres target is behind a bastion I can't tunnel a MySQL connection through, dumping to files on the source side and shipping them over is a workflow pgloader never gave me cleanly.
What I'd do differently from the getting-started guide
The guide walks you through the sakila sample database, which is a fine way to learn the commands and a terrible way to learn what your migration will cost. Sakila has no TINYINT(1) columns masquerading as booleans, no ENUM columns with 14 values that map to nothing in Postgres, and no zero-dates. Real MySQL schemas have all three.
So my suggestion is to skip sakila and run pg_migrate inspect against a copy of the real thing on day one, then spend the first hour in pg_migrate ui reading the annotated issues rather than trying to convert anything. On the logistics database, the inspection flagged 23 issues I'd have hit eventually. Nine were the boolean thing. Four were ENUM columns. One was a DATETIME column with '0000-00-00 00:00:00' as a default, which Postgres rejects outright and which I'd forgotten about. The tool's "convert column to boolean" rule handles the first group with one line of TOML; the other two were manual decisions, but at least I made them on a Tuesday evening instead of at 2am during the cutover.
The other thing I'd do is keep the inspection JSON in version control alongside the TOML. The catalog is the input to every later step, and being able to diff two inspections a month apart (the client's team does still add columns) turned out to be the most useful artifact of the whole exercise. I do the same with pg_stat_statements snapshots, which I wrote up in four queries I run before adding an index, and the habit transfers.
Where pgloader still wins
I'm not deleting pgloader. Here's where I'd still reach for it.
If the source is not MySQL, MariaDB or Oracle, the migrator can't help you. SQL Server is listed as planned; SQLite, CSV and the MS Access nightmare I once got paid to deal with are pgloader territory and will stay that way.
If the whole job is "get the rows across tonight," pgloader's one-command load is still less ceremony than init, inspect, convert, dump. The migrator earns its extra steps on a schema you need to understand before you move it. On a 12-table side project I'd not bother.
And if you need transformations during load (casting rules, column drops, renames applied inline), pgloader's command language is more mature than the migrator's TOML rules today. I expect that to flip within a year, given the pace of the changelog, but today it's true.
What I'm doing with the logistics client
I'm sending them the complexity score and the 23-issue list this week, with a proposal that includes a write freeze of roughly 45 minutes for the copy, because the snapshot row in the features matrix says I have to. I'll run the migrator for inspection and the data copy, transqlate for the routines nobody understands, and a row-count script I've already written for the verify step. If Dalibo ships consistent snapshots in a 1.x release before we cut over, I'll drop the freeze and be pleased about it.
If you have a MySQL database you've been meaning to move, here's the one thing to do this week: install the binary, run pg_migrate init and pg_migrate inspect against a copy, and open the UI. Don't convert anything. Just read the issue list. It took me 40 minutes and answered a question I'd been guessing at for three years. You can see the kind of migration and infrastructure work I take on at abrarqasim.com if you'd rather hand the weekends to someone else.
Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.
Top comments (0)