Standard Operating Procedure

MySQL 8.0 → PostgreSQL

Step-by-step procedure for migrating a MySQL 8.0 database (on-premises or RDS MySQL) to a PostgreSQL 14+ target using Endrias Bridge, including prerequisite binlog configuration, type conversion guidance, and post-migration sequence resets.

Tool: Endrias Bridge · Source: MySQL 8.0 (on-prem or RDS) · Target: PostgreSQL 14+

📚 Overview & Scope

This SOP covers a full copy (schema + data) from a MySQL 8.0 source into a PostgreSQL 14+ target using Endrias Bridge's cross-engine row-copy mode. Cross-engine migration requires more type-conversion awareness than a same-engine copy — key differences include ENUM handling, AUTO_INCREMENT sequences, zero-date behavior, unsigned integers, and case sensitivity of identifiers.

Bidirectional support: Endrias Bridge also supports PostgreSQL → MySQL. Swap source and target — see Known Issues for reverse type-mapping notes.
Estimated time: 10–20 minutes for setup + assessment, plus data copy time (varies by database size, row count, and JSONB conversion overhead).

Prerequisites — Source (MySQL 8.0)

RequirementNotes / Verification
MigrationUser: SELECT + LOCK TABLES + REPLICATION CLIENT privileges on EndriasBridge Minimum for full-copy mode; REPLICATION CLIENT required for CDC
binlog_format = ROW Check: SHOW VARIABLES LIKE 'binlog_format';
Binlog retention: expire_logs_days=30 (5.7) or binlog_expire_logs_seconds=2592000 (8.0) For RDS MySQL: CALL mysql.rds_set_configuration('binlog retention hours', 720);
default_authentication_plugin=mysql_native_password recommended Avoids caching_sha2_password handshake issues with some connector versions
TCP 3306 reachable from migration host to mysql-source-server Confirm with: Test-NetConnection mysql-source-server -Port 3306

Prerequisites — Target (PostgreSQL 14+)

Endrias Bridge connects directly via postgresql+psycopg2:// — it does not create the database, only tables inside it. Run the following once on the PostgreSQL server (as a superuser, e.g. psql -U postgres) before Step 1:

-- 1. Create the target database
CREATE DATABASE endriasbridge_target;

-- 2. Create the migration user
CREATE USER MigrationUser WITH PASSWORD 'your-password';

-- 3. Grant ownership so CREATE/DROP/INSERT work freely
--    (avoids "permission denied for schema public" on PostgreSQL 15+)
ALTER DATABASE endriasbridge_target OWNER TO MigrationUser;
\connect endriasbridge_target
ALTER SCHEMA public OWNER TO MigrationUser;
💡
psycopg2 is already bundled with Endrias Bridge — no separate Python driver installation is required.
TCP 5432 must be reachable from the migration host to the PostgreSQL server. If using RDS PostgreSQL, add the migration host IP to the instance security group's inbound rules.
1

Configure Connections

Open Endrias Bridge, navigate to Migration Projects → New Project, and fill in both connection blocks. Select MySQL as source type and PostgreSQL as target type.

  Endrias Bridge — New Migration Project: Source & Target

SOURCE DATABASE

MySQL
mysql-source-server
3306
EndriasBridge
MigrationUser
••••••••••••

TARGET DATABASE

PostgreSQL
pg-target-host
5432
EndriasBridgeTarget
MigrationUser
••••••••••••
Test Connections
Save Project
💡
Click Test Connections before proceeding. A MySQL driver failure often indicates a caching_sha2_password mismatch — set default_authentication_plugin=mysql_native_password on the source to resolve it.
2

Run Assessment

Switch to the Assessment tab and click Run Assessment. For cross-engine migrations the assessment panel shows a richer compatibility report — all type conversions that will be applied are listed per column.

  Endrias Bridge
Assessment
Migration
Jobs
Schema Objects
Tools
Database Assessment
Discovers source database objects and checks compatibility with target.
Run Assessment

SOURCE OBJECTS

📁 Tables (38)
  users   28,400 rows
  orders   142,000 rows
  products   4,200 rows
  categories   85 rows
  … (34 more)

COMPATIBILITY ISSUES

• users.active: TINYINT(1) → BOOLEAN
• orders.id: INT AUTO_INCREMENT → SERIAL
• products.metadata: JSON → JSONB
• categories.type: ENUM → VARCHAR(n)
• inventory.qty: INT UNSIGNED → BIGINT
Review all flagged ENUM columns before migration. ENUMs become VARCHAR(n) where n is the length of the longest label. Values are fully preserved as strings — no data loss occurs, but PostgreSQL will not enforce the constraint. See Known Issues.
3

Select Tables

Switch to the Migration tab. Click Load Tables from Source, confirm all tables are checked, then configure migration options.

  Endrias Bridge
Assessment
Migration
Jobs
Schema Objects
Tools
Migrate Schema & Data
Select tables and copy schema / data to target database.
Migrate schema (CREATE TABLE)
Migrate data (INSERT rows)
Sync mode:
Full copy
Incremental (watermark column)
MySQL binlog CDC
Load Tables from Source
Start Migration
Verify Data Integrity
Export Report
users
orders
products
categories
inventory
… (all 38 tables, checked)
All MySQL identifiers (table names, column names) are lowercased during migration. If your application queries use mixed-case identifiers, update those queries after migration — PostgreSQL is case-sensitive for quoted identifiers.
4

Start Migration & Monitor

Click Start Migration. The Migration Log updates in real time. Type conversion lines are printed first, then schema creation, then row-by-row copy progress per table.

  Endrias Bridge — Migration Log
14:02:11 --- Data type conversions (MySQL -> PostgreSQL) ---
14:02:11 EndriasBridge.users.active: TINYINT(1) -> BOOLEAN
14:02:11 EndriasBridge.orders.id: INT AUTO_INCREMENT -> SERIAL
14:02:11 EndriasBridge.products.metadata: JSON -> JSONB
14:02:11 EndriasBridge.categories.type: ENUM('A','B','C') -> VARCHAR(10) [values preserved]
14:02:11 EndriasBridge.inventory.qty: INT UNSIGNED -> BIGINT
14:02:12 Schema: created 38 table(s)
14:02:12 [1/38] users ... 28,400 row(s)
14:02:13 [2/38] orders ... 142,000 row(s)
14:02:45 --- Full copy: 1,100,000+ row(s) in 28s ---
14:02:45 --- Done ---
Zero-date values (0000-00-00 / 0000-00-00 00:00:00) cannot be stored in PostgreSQL DATE / TIMESTAMP columns. Endrias Bridge converts them to NULL and logs a WARNING per affected row. Review the log for zero-date WARNINGs after migration completes.
5

Verify & Export Report

  1. Click Verify Data Integrity — compares source vs. target row counts for every migrated table and logs any mismatches.
  2. Review zero-date warnings: check the Migration Log for WARNING: zero-date converted to NULL lines, then query the target to confirm NULLs are expected.
  3. Click Export Report (enabled once migration finishes) to save an HTML/text summary of tables, row counts, type conversions, and all warnings.
6

Post-Migration Tasks

After a successful full copy the following tasks are required before switching application traffic to the PostgreSQL target.

1. Verify ENUM columns — values are stored as strings in VARCHAR(n) columns. No PostgreSQL ENUM type is created.

-- Spot-check a former ENUM column
SELECT DISTINCT type FROM categories ORDER BY type;

2. Reset sequences — PostgreSQL SERIAL sequences start at 1 after schema creation; they are not seeded from the migrated MAX(id) value. Reset each auto-increment column:

-- Run for each table with an AUTO_INCREMENT / SERIAL primary key
SELECT setval(
    pg_get_serial_sequence('users', 'id'),
    (SELECT MAX(id) FROM users)
);

SELECT setval(
    pg_get_serial_sequence('orders', 'id'),
    (SELECT MAX(id) FROM orders)
);
-- Repeat for every SERIAL column in the schema

3. Zero-date check

-- Confirm expected NULLs where MySQL stored 0000-00-00
SELECT COUNT(*) AS zero_date_rows
FROM orders
WHERE created_at IS NULL;

4. Case sensitivity — PostgreSQL identifiers are case-sensitive. All MySQL identifiers were lowercased during migration. Update any application queries that reference mixed-case table or column names.

5. Full-text indexes — MySQL FULLTEXT indexes are not migrated. Recreate them as GIN indexes with tsvector on PostgreSQL:

-- Example: recreate full-text search index on products.description
ALTER TABLE products ADD COLUMN tsv tsvector
    GENERATED ALWAYS AS (to_tsvector('english', description)) STORED;

CREATE INDEX idx_products_tsv ON products USING GIN (tsv);
💡
Run ANALYZE; on the target database after migration completes to update planner statistics before switching application traffic. PostgreSQL autovacuum will not have gathered statistics on newly populated tables yet.

Known Issues

IssueDetails & Resolution
ENUM / SET types MySQL ENUM becomes VARCHAR(n); SET becomes TEXT (comma-separated values). No PostgreSQL ENUM type is created — this avoids ALTER TYPE table lock issues. Values are fully preserved as strings.
Zero dates MySQL 0000-00-00 / 0000-00-00 00:00:00 are invalid on PostgreSQL — converted to NULL. A WARNING is logged per affected row in the Migration Log.
UNSIGNED integers INT UNSIGNEDBIGINT to avoid overflow above 2,147,483,647. BIGINT UNSIGNEDNUMERIC(20).
Case sensitivity All MySQL identifiers are lowercased during migration. Application queries using mixed-case names must be updated.
AUTO_INCREMENT seed PostgreSQL SERIAL sequences are not seeded from migrated data — they start at 1. Reset each sequence to MAX(id)+1 before going live (see Step 6).
JSON → JSONB MySQL JSON is migrated as PostgreSQL JSONB (binary-indexed). The ->> operator works identically; the @> containment operator behaves differently — review any queries using containment after migration.
Full-text indexes MySQL FULLTEXT indexes are not migrated. Recreate as GIN indexes with tsvector (see Step 6).

📊 Type Conversions (MySQL → PostgreSQL)

Source (MySQL)Target (PostgreSQL)Note
TINYINT(1)BOOLEANMySQL boolean convention
TINYINTSMALLINTNo 1-byte integer on PostgreSQL
SMALLINTSMALLINT
MEDIUMINTINTEGER
INTINTEGER
INT UNSIGNEDBIGINTAvoids overflow above 2,147,483,647
BIGINTBIGINT
BIGINT UNSIGNEDNUMERIC(20)Exceeds BIGINT max range
FLOATREAL
DOUBLEDOUBLE PRECISION
DECIMAL(p,s)NUMERIC(p,s)
CHAR(n) / VARCHAR(n)CHAR(n) / VARCHAR(n)
TINYTEXT / TEXT / MEDIUMTEXT / LONGTEXTTEXT
BINARY(n) / VARBINARY(n)BYTEA
TINYBLOB / BLOB / MEDIUMBLOB / LONGBLOBBYTEA
DATEDATE
TIMETIME
DATETIMETIMESTAMPNo timezone information
TIMESTAMPTIMESTAMPTZMySQL stores as UTC internally
YEARSMALLINT
ENUMVARCHAR(n)n = max label length; values preserved
SETTEXTStored as comma-separated values
JSONJSONBBinary-indexed on PostgreSQL
AUTO_INCREMENT INTSERIALSequence reset required post-migration
AUTO_INCREMENT BIGINTBIGSERIALSequence reset required post-migration