How to migrate a Laravel app from MySQL to PostgreSQL: 10-step checklist
Laravel (tested on Laravel 13), MySQL or MariaDB to PostgreSQL. Do the steps in order; each one links a fix that was reproduced on Postgres 17.11 (MySQL side on MariaDB 11.8). Markdown version for AI agents: laravel-mysql-to-postgres.md
1. Scan the MySQL schema before you switch
Paste SHOW CREATE TABLE output (or a mysqldump --no-data file, or Laravel migrations) into the free schema check. It flags what breaks on Postgres: unsigned integers, zero-date defaults, tinyint(1) columns that become boolean, _ci collations, ENUMs and MySQL-only defaults.
2. Clean the data on MySQL first
Postgres rejects two things MySQL happily stores: zero dates ('0000-00-00') and NUL bytes (0x00) in text. Fix them on the MySQL side, before copying. Through pdo_pgsql a NUL in a bound value silently cuts the string, so this is not optional.
See: 0000-00-00 date out of range (22008) · invalid byte sequence 0x00 (22021)
3. Convert the schema
Postgres has no unsigned integers, so an unsigned int column's ceiling drops from 4,294,967,295 to 2,147,483,647. Use bigint (unsignedBigInteger() / bigIncrements() in Laravel migrations). The free schema converter turns CREATE TABLE statements into Postgres DDL and prints the setval lines for step 5.
See: Free schema converter · out of range for type integer (22003)
4. Copy the data
Use pgloader, or CSV export plus COPY. Copy into a scratch database first and compare row counts per table.
5. Reset every sequence
Rows copied with their ids leave each Postgres sequence at 1, so the first new insert fails with a duplicate key error. Run setval once per table with an id column. It is safe to run again.
6. Fix raw SQL syntax
Laravel's query builder quotes and binds for you; raw SQL (DB::statement, whereRaw, selectRaw, old migrations) does not. Remove backticks, use single quotes for strings, and replace MySQL-only functions: IFNULL to COALESCE, GROUP_CONCAT to string_agg, DATE_FORMAT to to_char, IF() to CASE. The free translator rewrites MySQL SQL to Postgres and explains each change.
See: Free SQL translator · syntax error at or near "`" (42601) · column does not exist, double-quoted strings (42703) · function ifnull does not exist (42883)
7. Fix queries that MySQL let slide
Postgres refuses non-grouped columns in GROUP BY queries, and won't compare a boolean or a string column with a bare number written into the SQL. Bound values (->where('active', 1)) still work; literals in raw SQL fail.
See: must appear in the GROUP BY clause (42803) · operator does not exist: boolean = integer (42883) · operator does not exist: character varying = integer (42883)
8. Check case-sensitive search and logins
No error here, just missing rows. MySQL _ci collations make = and LIKE case-insensitive; Postgres is case-sensitive. Use ILIKE, lower() on both sides, or citext for email columns. Unique indexes change too: two emails that differ only in case are allowed on Postgres.
9. Handle failed statements inside transactions
After one failed statement, Postgres rejects every later statement in the same transaction with 25P02 until rollback. Find the first error in the log. If code must try and continue, use a savepoint (a nested DB::transaction() in Laravel).
10. Switch the connection and run the tests on Postgres
Set DB_CONNECTION=pgsql and the pgsql settings in .env, then run the full test suite against Postgres, not SQLite or MySQL. Paste any errors into the error matcher to get the matching fix.
Want it done for you?
The Laravel Postgres Error Fix Pack ($19) has these fixes as tested scripts and a Laravel command. The TuskShift Migration Kit ($49, $34.30 with LAUNCH30 until Nov 5 2026) adds the checker, runbook and cutover plan. Everything above is free.