How to Use the PostgreSQL Migrator Tool to Migrate a Database From MySQL to Postgres

Migrating a database from MySQL to PostgreSQL is a common task when moving to a more advanced database platform or starting a new project. A successful migration requires careful planning to ensure that database objects, data, indexes, and relationships are transferred correctly.

Postgresql Migrator is a command-line tool that simplifies this process by inspecting the source database, converting the schema, generating a migration script, and importing the data into PostgreSQL.

In this guide, we will learn how to migrate the classicmodels sample database from mysql to postgres using postgreql migrator, verify the migrated data, and confirm that the migration completed successfully.

Download the debian file of the pg_migrate tool.

After downloading the file, install it using the sudo apt install command like this.

sudo apt install ./pg-migrate_linux_amd64.deb

Reload your shell to enable the command line completion.

exec /bin/bash

Check installation by requesting the version:

pg_migrate --version

Result :

pg_migrate (PostgreSQL Migrator) v1.0.0
transqlate v0.8.0
cybrosys@cybrosys:~$ pg_migrate --help
PostgreSQL Migrator moves your database to PostgreSQL.
Usage:  pg_migrate [OPTIONS] COMMAND
Commands:
  convert                  Convert source catalog for PostgreSQL
  dump                     Dumps a source database as text files or to stdin
  init                     Initialize a new migration project
  inspect                  Fetch and analyse source database catalog
  status                   Describe a migration project
  ui                       Interactive audit web interface
General Options:
  -C, --directory string    Change to directory before doing anything
  -?, --help                Print help and exit
      --logfile string      Path to save log messages
      --offline             Prevent source database access
      --plain               Disable log coloration
      --profile             Write profiling data for debugging
      --skip-date-warning   Suppress the warning about outdated build
  -v, --verbose             Show debug log messages
  -V, --version             Print version and exit
Environment variables:
  PGMDIRECTORY    Change to directory before doing anything.
  PGMOFFLINE    Set to true prevent source database access.
Report bugs to https://gitlab.com/dalibo/pg_migrate/-/issues.

This is the current database in mysql.

List the databases.

mysql> show databases;

Result :

+--------------------+
| Database           |
+--------------------+
| classicmodels      |
| demo               |
| information_schema |
| mydb               |
| mysql              |
| performance_schema |
| sys                |
| test_db            |
| testdb             |
+--------------------+
9 rows in set (0.00 sec)

Connect to the database named classicmodelsa and list the table names, and check the results.

mysql> use classicmodels;

Result :

Database changed

Now check the tables. 

mysql> show tables;
+-------------------------+
| Tables_in_classicmodels |
+-------------------------+
| customers               |
| employees               |
| offices                 |
| orderdetails            |
| orders                  |
| payments                |
| productlines            |
| products                |
+-------------------------+
8 rows in set (0.00 sec)

Check any of the data from the tables before migrating to postgresql.

mysql> select * from customers limit 5;

Result :

+----------------+----------------------------+-----------------+------------------+--------------+------------------------------+--------------+-----------+----------+------------+-----------+------------------------+-------------+
| customerNumber | customerName               | contactLastName | contactFirstName | phone        | addressLine1                 | addressLine2 | city      | state    | postalCode | country   | salesRepEmployeeNumber | creditLimit |
+----------------+----------------------------+-----------------+------------------+--------------+------------------------------+--------------+-----------+----------+------------+-----------+------------------------+-------------+
|            103 | Atelier graphique          | Schmitt         | Carine           | 40.32.2555   | 54, rue Royale               | NULL         | Nantes    | NULL     | 44000      | France    |                   1370 |    21000.00 |
|            112 | Signal Gift Stores         | King            | Jean             | 7025551838   | 8489 Strong St.              | NULL         | Las Vegas | NV       | 83030      | USA       |                   1166 |    71800.00 |
|            114 | Australian Collectors, Co. | Ferguson        | Peter            | 03 9520 4555 | 636 St Kilda Road            | Level 3      | Melbourne | Victoria | 3004       | Australia |                   1611 |   117300.00 |
|            119 | La Rochelle Gifts          | Labrune         | Janine           | 40.67.8555   | 67, rue des Cinquante Otages | NULL         | Nantes    | NULL     | 44000      | France    |                   1370 |   118200.00 |
|            121 | Baane Mini Imports         | Bergulfsen      | Jonas            | 07-98 9555   | Erling Skakkes gate 78       | NULL         | Stavern   | NULL     | 4110       | Norway    |                   1504 |    81700.00 |
+----------------+----------------------------+-----------------+------------------+--------------+------------------------------+--------------+-----------+----------+------------+-----------+------------------------+-------------+
5 rows in set (0.00 sec)
mysql> 

Create a folder for the migration purpose and change the directory to that folder.

mkdir ~/classicmodels_migrationcd classicmodels_migration/

Check the running postgres clusters.

pg_lsclusters 

Result:

Ver Cluster Port Status Owner    Data directory               Log file
14  main    5435 online postgres /var/lib/postgresql/14/main  /var/log/postgresql/postgresql-14-main.log
18  main2   5433 online postgres /var/lib/postgresql/18/main2 /var/log/postgresql/postgresql-18-main2.log
19  main    5436 online postgres /var/lib/postgresql/19/main  /var/log/postgresql/postgresql-19-main.log

Now create a database in MySQL

Log into MySQL.

sudo mysql

Create a database named classicmodels_pg.

create database classicmodels_pg;

Create a user and set a password like this.

mysql> CREATE USER 'migrator'@'localhost' IDENTIFIED BY 'mypassword';Query OK, 0 rows affected (0.01 sec)

Now grant all privileges of the database to the newly created user like this.

mysql> GRANT ALL PRIVILEGES ON classicmodels.* TO 'migrator'@'localhost';Query OK, 0 rows affected (0.00 sec)

Now flush the privileges.

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)

Exit from the mysql like this.

mysql> EXIT;

Now go to the directory named classicmodels_migration and execute the command below to initialise the migration project by specifying the –source and –target like this.

cybrosys@cybrosys:~/classicmodels_migration$ pg_migrate init \
  --source "mysql://migrator:mypassword@localhost:3306/classicmodels" \
  --target "postgres://postgres@localhost:5436/classicmodels_pg"

Result :

16:52:29 INFO   Databases connections closed.    source=1 target=1
16:52:29 INFO   Initialized migration project.   source=(Ubuntu) version=8.0.46-0ubuntu0.22.04.4 path=/home/cybrosys/classicmodels_migration
16:52:29 INFO   Inspect source catalog using pg_migrate inspect.

Now check the contents inside this folder.

cybrosys@cybrosys:~/classicmodels_migration$ ls

Result :

pg_migrate.toml

Now read this file content using the cat command.

cybrosys@cybrosys:~/classicmodels_migration$ cat pg_migrate.toml 

Result :

# PostgreSQL Migrator mysql Configuration
#
# See documentation for toml reference
# https://postgresql-migrator.readthedocs.io/en/latest/references/toml/
# Restrict the scope of the migration to schemas matching one of the pattern.
#Schemas = ["%"]
[Scores]
# Attach migration issue id to complexity score.
#"type: Column" = 0.1
[Convert]
# Enable identifier lowering.
#PreserveCase = false
# Enable automatic renaming of all constraints.
#RenameConstraints = false
# Enable automatic renaming of all indexes.
#RenameIndexes = false
[Dump]
# Ignore annotations and perform partial dump.
#Force = false
# Enable silent drop of zero bytes from text data.
#StripZeros = false

This is the configuration file we used during the migration process.

Now log in to MySQL and grant the select privilege on mysql.servers to the newly created role for the migration process.

mysql> GRANT SELECT ON mysql.servers TO 'migrator'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

Here we grant the migrator user permission to read the mysql.servers system table. This is because the Postgres migrator doesn't only read the application tables (such as customers, orders, and products). It also queries several MySQL system tables to collect metadata about the database.

The purpose of the pg_migrate inspect command is to analyze the source database and build an internal migration catalog. It does not migrate any data or create objects in the postgres.

Now execute the pg_migrate inspect command like this.

cybrosys@cybrosys:~/classicmodels_migration$ pg_migrate inspect 

Result :

16:55:25 INFO   Inspecting source database.      driver=mysql
16:55:25 INFO   Inspected metadata.              software=(Ubuntu)
16:55:25 INFO   Found tables.                    count=8
16:55:25 INFO   Found columns.                   count=59
16:55:25 INFO   Found tables keys.               count=8
16:55:25 INFO   Found tables foreign keys.       count=8
16:55:25 INFO   Found indexes.                   count=6
16:55:25 INFO   Collected tables statistics.     count=8 missing=0
16:55:25 INFO   Collected partitions statistics. count=0
16:55:25 INFO   Collected subpartitions statistics. count=0
16:55:25 INFO   All statistics collected.        size=544KB
16:55:25 INFO   Auditing catalog.                driver=mysql catalog=MySQL
16:55:25 INFO   Catalog audited.                
16:55:25 INFO   Converted catalog for PostgreSQL.
16:55:25 INFO   Auditing catalog.                driver=mysql catalog=PostgreSQL
16:55:25 WARN   Target catalog has pending annotations. count=57
16:55:25 INFO   Databases connections closed.    source=2 target=0
16:55:25 INFO   Inspection terminated.           errs=0 source=(Ubuntu) version=8.0.46-0ubuntu0.22.04.4 online=true
16:55:25 INFO   Execute pg_migrate ui to browse project.
16:55:25 INFO   Execute pg_migrate dump to migrate for PostgreSQL.

Now check the contents inside this folder.

cybrosys@cybrosys:~/classicmodels_migration$ ls

Result :

inspect.log  pg_migrate.toml

Now read the contents of the file named inspect.log using the cat command.

cat inspect.log

Result :

How to Use the PostgreSQL Migrator Tool to Migrate a Database From MySQL to Postgres-cybrosys

Now use the pg_migrate convert command like this.

The purpose of pg_migrate convert is to convert the inspected mysql database catalog into a Postgresql compatible catalog. It translates the source database metadata so that the postgres can easily understand it.

cybrosys@cybrosys:~/classicmodels_migration$ pg_migrate convert

Result :

16:56:53 INFO   Converted catalog for PostgreSQL.
16:56:53 INFO   Auditing catalog.                driver=mysql catalog=PostgreSQL
16:56:53 WARN   Target catalog has pending annotations. count=57
16:56:53 INFO   Run pg_migrate dump to migrate. 

Now check the contents inside this folder again.

cybrosys@cybrosys:~/classicmodels_migration$ ls

Result :

convert.log  inspect.log  pg_migrate.toml

Check the flags available with the pg_migrate convert tool like this.

cybrosys@cybrosys:~/classicmodels_migration$ pg_migrate convert --help

Result :

Usage: pg_migrate [OPTIONS] convert [OPTIONS]
Converts source catalog for PostgreSQL.
Transpiles code. Renames identifiers. Converts column types.
Uses dump to generate DDL.
Options:
  -?, --help      Show help
      --refresh   Refresh target catalog
See pg_migrate --help for more informations.

The purpose of the pg_migrate dump command is to generate the postgresql migration script and export the mysql data into that script. This is the step where the actual migration SQL is created.

Now use the pg_migrate dump command like this.

cybrosys@cybrosys:~/classicmodels_migration$ pg_migrate dump \
    --force \
    --file migration.sql

Result :

16:59:20 INFO   Auditing catalog.                driver=mysql catalog=PostgreSQL
16:59:20 WARN   Ignoring unhandled annotations in target model. len=57
16:59:20 INFO   Collected tables statistics.     count=8 missing=0
16:59:20 INFO   Collected partitions statistics. count=0
16:59:20 INFO   Collected subpartitions statistics. count=0
16:59:20 INFO   All statistics collected.        size=544KB
16:59:20 INFO   Dumping to file.                 path=migration.sql format=plain
16:59:20 INFO   Create schema.                   path=Schemas/classicmodels
16:59:20 INFO   Create table.                    path=Tables/classicmodels.productlines
16:59:20 INFO   Create table.                    path=Tables/classicmodels.orderdetails
16:59:20 INFO   Create table.                    path=Tables/classicmodels.offices
16:59:20 INFO   Create table.                    path=Tables/classicmodels.orders
16:59:20 INFO   Create table.                    path=Tables/classicmodels.payments
16:59:20 INFO   Create table.                    path=Tables/classicmodels.products
16:59:20 INFO   Table copied.                    table=classicmodels.productlines elapsed=0s data=3.3KB batches=1 rows=7 rate="8  rows/s" sn=10007
16:59:20 INFO   Table copied.                    table=classicmodels.orderdetails elapsed=30ms data=78.2KB batches=1 rows=2996 rate="99.9K rows/s" sn=10004
16:59:20 INFO   Table copied.                    table=classicmodels.offices elapsed=30ms data=515B batches=1 rows=7 rate="233 rows/s" sn=10003
16:59:20 INFO   Table copied.                    table=classicmodels.orders elapsed=40ms data=32.8KB batches=1 rows=326 rate="8.2K rows/s" sn=10005
16:59:20 INFO   Table copied.                    table=classicmodels.payments elapsed=40ms data=11.4KB batches=1 rows=273 rate="6.8K rows/s" sn=10006
16:59:20 INFO   Create table key.                path=Tables/classicmodels.offices/Keys/offices_pkey
16:59:20 INFO   Create table key.                path=Tables/classicmodels.productlines/Keys/productlines_pkey
16:59:20 INFO   Create table key.                path=Tables/classicmodels.orderdetails/Keys/orderdetails_pkey
16:59:20 INFO   Create table key.                path=Tables/classicmodels.orders/Keys/orders_pkey
16:59:20 INFO   Table copied.                    table=classicmodels.products elapsed=10ms data=28.4KB batches=1 rows=110 rate="11K rows/s" sn=10008
16:59:20 INFO   Create table key.                path=Tables/classicmodels.payments/Keys/payments_pkey
16:59:20 INFO   Create table index.              path=Tables/classicmodels.orderdetails/Indexes/orderdetails_productCode_idx
16:59:20 INFO   Create table.                    path=Tables/classicmodels.customers
16:59:20 INFO   Create table index.              path=Tables/classicmodels.orders/Indexes/orders_customerNumber_idx
16:59:20 INFO   Create table.                    path=Tables/classicmodels.employees
16:59:20 INFO   Create table key.                path=Tables/classicmodels.products/Keys/products_pkey
16:59:20 INFO   Create table index.              path=Tables/classicmodels.products/Indexes/products_productLine_idx
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.orderdetails/ForeignKeys/orderdetails_orderNumber_fkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.products/ForeignKeys/products_productLine_fkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.orderdetails/ForeignKeys/orderdetails_productCode_fkey
16:59:20 INFO   Table copied.                    table=classicmodels.employees elapsed=0s data=1.6KB batches=1 rows=23 rate="8  rows/s" sn=10002
16:59:20 INFO   Table copied.                    table=classicmodels.customers elapsed=10ms data=13.7KB batches=1 rows=122 rate="12.2K rows/s" sn=10001
16:59:20 INFO   Create table key.                path=Tables/classicmodels.employees/Keys/employees_pkey
16:59:20 INFO   Create table index.              path=Tables/classicmodels.employees/Indexes/employees_officeCode_idx
16:59:20 INFO   Create table index.              path=Tables/classicmodels.employees/Indexes/employees_reportsTo_idx
16:59:20 INFO   Create table key.                path=Tables/classicmodels.customers/Keys/customers_pkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.employees/ForeignKeys/employees_reportsTo_fkey
16:59:20 INFO   Create table index.              path=Tables/classicmodels.customers/Indexes/customers_salesRepEmployeeNumber_idx
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.employees/ForeignKeys/employees_officeCode_fkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.customers/ForeignKeys/customers_salesRepEmployeeNumber_fkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.payments/ForeignKeys/payments_customerNumber_fkey
16:59:20 INFO   Create foreign key.              path=Tables/classicmodels.orders/ForeignKeys/orders_customerNumber_fkey
16:59:20 INFO   Dump completed.                  elapsed=50ms tasks=57 jobs=4 mem=61.5MB tables=8 copied=169.9KB throughput=3.3MB/s sections=pre,data,post annotations=57
16:59:20 INFO   Databases connections closed.    source=4 target=0

Now check the contents again.

cybrosys@cybrosys:~/classicmodels_migration$ ls

Result :

convert.log  dump.log  inspect.log  migration.sql  pg_migrate.toml

Now we can see the migration.sql file created by the pg_migrate dump command.

So now restore this file into our created database named classicmodels_pg like this.

cybrosys@cybrosys:~/classicmodels_migration$ psql -h localhost -p 5436 -U postgres -d classicmodels_pg -f migration.sql

Result :

CREATE SCHEMA
CREATE TABLE
ALTER TABLE
CREATE TABLE
ALTER TABLE
CREATE TABLE
ALTER TABLE
CREATE TABLE
ALTER TABLE
CREATE TABLE
ALTER TABLE
CREATE TABLE
ALTER TABLE
COPY 7
COPY 2996
COPY 7
COPY 326
COPY 273
ALTER TABLE
ALTER TABLE
COPY 110
ALTER TABLE
ALTER TABLE
ALTER TABLE
CREATE INDEX
CREATE TABLE
ALTER TABLE
CREATE INDEX
CREATE TABLE
ALTER TABLE
ALTER TABLE
CREATE INDEX
ALTER TABLE
ALTER TABLE
ALTER TABLE
COPY 23
COPY 122
ALTER TABLE
CREATE INDEX
CREATE INDEX
ALTER TABLE
ALTER TABLE
CREATE INDEX
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

Now log into the psql on port 5436 and check the contents, and verify whether the migration process happens correctly or not.

cybrosys@cybrosys:~/classicmodels_migration$ sudo su postgres
postgres@cybrosys:/home/cybrosys/classicmodels_migration$ psql -p 5436
psql (19beta2)
Type "help" for help.
postgres=# \c classicmodels_pg 
You are now connected to database "classicmodels_pg" as user "postgres".
classicmodels_pg=# select * from classicmodels.
classicmodels.customers     classicmodels.offices       classicmodels.orders        classicmodels.productlines  
classicmodels_pg=# select * from classicmodels.customers limit 5;
 customerNumber |        customerName        | contactLastName | contactFirstName |    phone     |         addressLine1         | addressLine2 |   city    |  state   | postalCode |  country  | salesRepEmp
loyeeNumber | creditLimit 
----------------+----------------------------+-----------------+------------------+--------------+------------------------------+--------------+-----------+----------+------------+-----------+------------
------------+-------------
            103 | Atelier graphique          | Schmitt         | Carine           | 40.32.2555   | 54, rue Royale               |              | Nantes    |          | 44000      | France    |            
       1370 |    21000.00
            112 | Signal Gift Stores         | King            | Jean             | 7025551838   | 8489 Strong St.              |              | Las Vegas | NV       | 83030      | USA       |            
       1166 |    71800.00
            114 | Australian Collectors, Co. | Ferguson        | Peter            | 03 9520 4555 | 636 St Kilda Road            | Level 3      | Melbourne | Victoria | 3004       | Australia |            
       1611 |   117300.00
            119 | La Rochelle Gifts          | Labrune         | Janine           | 40.67.8555   | 67, rue des Cinquante Otages |              | Nantes    |          | 44000      | France    |            
       1370 |   118200.00
            121 | Baane Mini Imports         | Bergulfsen      | Jonas            | 07-98 9555   | Erling Skakkes gate 78       |              | Stavern   |          | 4110       | Norway    |            
       1504 |    81700.00
(5 rows)

Now the data is the same.

Postgresql migrator provides a straightforward way to move databases from mysql database to Postgres. By following the steps in this guide, we can successfully inspect the source database, convert the schema, generate the migration script, import the data into Postgresql, and verify the migrated records.

Performing these validation steps after the migration helps ensure that the database structure and data have been transferred correctly, making the PostgreSQL database ready for further development and use.

WhatsApp