A best-effort, portable Bash script that transforms a MySQL SQL dump into a PostgreSQL-compatible SQL file using text-based rules (sed/awk).
Note: SQL dialects differ significantly. This tool covers the most common cases but cannot fully automate complex migrations. Always review and test the output.
- Converts MySQL-specific DDL to PostgreSQL-friendly syntax (best effort)
- Replaces MySQL types with PostgreSQL equivalents (e.g., TINYINT(1) -> boolean, DATETIME -> timestamp)
- Strips MySQL table options (ENGINE, DEFAULT CHARSET, COLLATE, ROW_FORMAT)
- Converts AUTO_INCREMENT to PostgreSQL identity columns
- Converts
UNIQUE KEY
to table-levelUNIQUE (cols)
constraints - Extracts
KEY
/INDEX
into standaloneCREATE INDEX
statements - Removes MySQL-only pragmas and meta statements (SET, LOCK/UNLOCK TABLES, DELIMITER, etc.)
- Handles backticks -> double quotes
- Removes inline column comments (COMMENT '...')
- Normalizes boolean DEFAULT values 0/1 to false/true when a column becomes
boolean
- Attempts to fix trailing commas before closing parentheses
- Not a full SQL parser. Complex constructs (stored procedures, triggers, functions, views, events, advanced collations, generated columns, spatial types, partial indexes, enum constraints, partitioning, foreign key ON UPDATE/DELETE nuances) may need manual editing.
- ENUM types are mapped to
text
. Consider creating properCHECK
constraints or domains manually. - UNSIGNED numeric semantics are dropped. Validate ranges if you relied on UNSIGNED.
- TIME/TIMESTAMP timezone semantics may require review.
- Requires GNU sed/awk for full compatibility. On macOS, install
gsed
via Homebrew or adapt the script.
- bash
- sed, awk, mktemp
- GNU sed/awk recommended (macOS:
brew install gnu-sed gawk
and run withgsed
/gawk
or adjust PATH)
Basic usage with file input:
./mysql_to_postgres.sh input_mysql_dump.sql > output_postgres.sql
Or via stdin:
cat input_mysql_dump.sql | ./mysql_to_postgres.sh > output_postgres.sql
If you are on macOS and installed GNU sed/awk as gsed
/gawk
, edit the script to replace sed
/awk
with gsed
/gawk
, or prepend them in PATH:
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:/usr/local/opt/gawk/libexec/gnubin:$PATH"
./mysql_to_postgres.sh input.sql > output.sql
- Removes MySQL-specific statements and comments (
SET
,LOCK TABLES
,DELIMITER
,/*! ... */
, etc.) - Normalizes identifiers by replacing backticks with double quotes
- Maps common data types and attributes:
TINYINT(1)
->boolean
TINYINT(n)
->smallint
MEDIUMINT(n)
->integer
INT(n)
->integer
BIGINT(n)
->bigint
FLOAT
->real
DOUBLE
->double precision
DATETIME
/TIMESTAMP
->timestamp without time zone
TEXT
/MEDIUMTEXT
/LONGTEXT
/TINYTEXT
->text
BLOB
/MEDIUMBLOB
/LONGBLOB
/TINYBLOB
->bytea
ENUM(...)
->text
AUTO_INCREMENT
->GENERATED BY DEFAULT AS IDENTITY
- Drops
UNSIGNED
- Strips table options:
ENGINE=...
,DEFAULT CHARSET=...
,COLLATE=...
,ROW_FORMAT=...
- Moves inline
KEY
/INDEX
definitions to standaloneCREATE INDEX
after the table - Converts
UNIQUE KEY
toUNIQUE (...)
table constraints - Removes inline column
COMMENT '...'
- Fixes trailing commas before
);
- Load the converted schema into an empty PostgreSQL database first:
psql -d yourdb -f output_postgres.sql
- If you have data inserts in the dump, they should generally work after type mapping. Watch out for:
- Zero dates like
'0000-00-00'
or'0000-00-00 00:00:00'
converted toNULL
- Byte sequences in BLOBs ->
bytea
formatting
- Zero dates like
- Review constraints, foreign keys, and indexes manually.
- Replace ENUMs with domains or
CHECK
constraints if you need stricter typing. - Validate numeric ranges if you depended on
UNSIGNED
.
Given a MySQL table:
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT 1,
UNIQUE KEY `idx_users_email` (`email`),
KEY `idx_users_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
The converted output will be similar to:
-- Converted by mysql_to_postgres.sh
SET standard_conforming_strings = on;
SET client_encoding = 'UTF8';
CREATE TABLE "users" (
"id" integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"email" varchar(255) NOT NULL,
"is_active" boolean NOT NULL DEFAULT true,
UNIQUE ("email")
);
CREATE INDEX "idx_users_active" ON "users" ("is_active");
- Default boolean values: MySQL often uses
0/1
. PostgreSQL acceptstrue/false
or0/1
for boolean literals in inserts, but you may prefer to normalize defaults totrue/false
. - Quoted identifiers: The script converts backticks to double quotes. Consider removing quotes if you prefer unquoted identifiers.
- Foreign keys: Verify
ON DELETE/UPDATE
rules and naming conventions. - Views/Procedures/Triggers: Usually require manual translation.
Issues and PRs are welcome. Please attach minimal reproducible examples and describe expected vs. actual output.
MIT