Laravel on Postgres after MySQL: paste your error

Paste the error (the whole SQLSTATE line is fine). Matching entries open below. Everything runs in your browser.

duplicate key value violates unique constraint "users_pkey" SQLSTATE 23505

You copied rows from MySQL with their ids, but the Postgres sequence behind each id column still starts at 1. The first new insert reuses id 1, 2, 3... If your ids have gaps, it fails some inserts and not others, so it looks random.

-- once per table that has a serial/identity id: SELECT setval(pg_get_serial_sequence('users','id'), COALESCE((SELECT MAX(id) FROM users), 0) + 1, false);

Tested on Postgres 17.11: after the setval, the next insert gets MAX(id)+1. Running it again is safe. If the key column is named differently, change 'id'.

Full tested fix (scripts for every table, Laravel command, checks): Error Fix Pack ($19), item 01.

Full page for this error

column "users.name" must appear in the GROUP BY clause or be used in an aggregate function SQLSTATE 42803

MySQL (with ONLY_FULL_GROUP_BY off) lets you select columns that are not grouped and returns a value from an arbitrary row. Postgres refuses. Grouping by the table's primary key is allowed, so groupBy('users.id') with users.* works.

-- pick one: SELECT team_id, MAX(name) FROM users GROUP BY team_id; -- aggregate it SELECT DISTINCT ON (team_id) team_id, name FROM users ORDER BY team_id, id DESC; -- latest row per group

Worth knowing: the old MySQL query may have been returning the wrong row the whole time. In our test, select * ... group by user_id on non-strict MariaDB 11.8 returned the oldest row, not the latest.

Full tested fix (scripts for every table, Laravel command, checks): Error Fix Pack ($19), item 03.

Full page for this error

value "3000000000" is out of range for type integer SQLSTATE 22003

Postgres has no unsigned integers. Laravel's unsignedInteger() and increments() become a plain integer on pgsql, so the ceiling drops from 4,294,967,295 to 2,147,483,647. Values above that loaded fine on MySQL and fail on Postgres.

ALTER TABLE events ALTER COLUMN hits TYPE bigint; -- in Laravel migrations: unsignedBigInteger() / bigIncrements() instead of -- unsignedInteger() / increments()

Tested with Laravel 13 on MariaDB 11.8 + Postgres 17: inserting 3000000000 works on MariaDB and fails on Postgres with SQLSTATE[22003]; after ALTER ... TYPE bigint it works.

Full tested fix (scripts for every table, Laravel command, checks): Error Fix Pack ($19), item 06.

Full page for this error

operator does not exist: boolean = integer SQLSTATE 42883

tinyint(1) columns usually become boolean on Postgres. MySQL compares them to 1/0 freely; Postgres does not when the 1 is written into the SQL text (whereRaw, DB::select with a literal, raw joins).

-- raw SQL: WHERE active = true -- or let Laravel bind it (this works on pgsql): ->where('active', 1)

Tested with pdo_pgsql: a bound 1 is accepted for a boolean column; only a literal 1 in the SQL fails.

Full tested fix (scripts for every table, Laravel command, checks): Error Fix Pack ($19), item 04.

Full page for this error

operator does not exist: character varying = integer SQLSTATE 42883

A string column (phone, code, zip, external_id) is compared with a bare number in raw SQL. MySQL casts silently; Postgres won't guess.

WHERE phone = '5551234' -- quote it WHERE phone = ? -- or bind it

Tested with pdo_pgsql: a bound value works; only the literal number fails.

Full page for this error

date/time field value out of range: "0000-00-00 00:00:00" SQLSTATE 22008

MySQL stores zero dates ('0000-00-00') when strict mode is off. Postgres has no such date, so the copy or the insert fails.

-- on MySQL, before copying (column must allow NULL): UPDATE orders SET shipped_at = NULL WHERE shipped_at = '0000-00-00 00:00:00'; ALTER TABLE orders MODIFY due_on date NULL DEFAULT NULL; UPDATE orders SET due_on = NULL WHERE due_on = '0000-00-00';

Tested on MariaDB 11.8. Also remove DEFAULT '0000-00-00' from the schema, or new rows bring the problem back.

Full tested fix (scripts for every table, Laravel command, checks): Error Fix Pack ($19), item 05.

Full page for this error

column "a@x.com" does not exist SQLSTATE 42703

If the 'column' in the error is actually a value (an email, a status word), your raw SQL uses double quotes for a string. MySQL treats "text" as a string; Postgres treats it as a column name.

WHERE email = 'a@x.com' -- single quotes for strings WHERE email = ? -- or bind it

If the name really is a column, check its case: Postgres folds unquoted names to lower case, so a quoted "userId" and userid are different.

Full page for this error

syntax error at or near "`" SQLSTATE 42601

Backticks are MySQL-only identifier quotes. Laravel's query builder quotes for you, so these come from raw SQL: DB::statement, whereRaw, selectRaw, old migrations.

SELECT * FROM users -- no quotes SELECT * FROM "users" -- Postgres identifier quotes

Tested on Postgres 17. Backticks next to a comma or keyword can also show up as: syntax error at or near "," or near "from". Search the codebase for ` in .php files.

Full page for this error

function ifnull(text, unknown) does not exist SQLSTATE 42883

A MySQL-only function in raw SQL. Common ones and their Postgres versions are below.

IFNULL(a, b) -> COALESCE(a, b) GROUP_CONCAT(email) -> string_agg(email, ',' ORDER BY id) DATE_FORMAT(d, '%Y') -> to_char(d, 'YYYY') IF(cond, a, b) -> CASE WHEN cond THEN a ELSE b END UNIX_TIMESTAMP(d) -> extract(epoch FROM d) CURDATE() -> current_date

COALESCE, string_agg and to_char tested on Postgres 17.11.

Full page for this error

invalid byte sequence for encoding "UTF8": 0x00 SQLSTATE 22021

Some MySQL text values contain NUL bytes (often from binary data or bad imports). Postgres text can't store them, so the load fails.

-- on MySQL, find and strip them before copying: SELECT id FROM t WHERE INSTR(name, CHAR(0)) > 0; UPDATE t SET name = REPLACE(name, CHAR(0), '') WHERE INSTR(name, CHAR(0)) > 0;

Silent variant: through PHP's pdo_pgsql (PHP 8.4, Postgres 17), a bound value 'a\0b' was stored as 'a' with no error. Everything after the NUL is lost. Check before you copy. Tested on MariaDB 11.8 and Postgres 17.

Full page for this error

current transaction is aborted, commands ignored until end of transaction block SQLSTATE 25P02

This is a symptom. An earlier statement in the same transaction failed; MySQL would carry on, but Postgres refuses every statement until rollback. Code that catches an exception and keeps using the same transaction hits this.

-- find the FIRST error in the log, fix that one. -- if you must try-and-continue inside a transaction, wrap the risky step -- in a savepoint (nested DB::transaction() does this in Laravel).

Tested on Postgres 17: after a failing statement, the next one in the block returns 25P02.

Full page for this error

(no error) search or login finds nothing: WHERE email LIKE ... returns 0 rows

Not an error, which makes it worse. MySQL's _ci collations make = and LIKE case-insensitive. Postgres is case-sensitive, so Name@Example.com no longer matches name@example.com.

WHERE email ILIKE 'a@%' WHERE lower(email) = lower(?) -- or the citext extension for email columns

Tested on Postgres 17.11. Also check unique indexes: two rows differing only in case were blocked on MySQL and are allowed on Postgres.

Full page for this error

Not moved yet? Check before you switch: the free schema risk check (paste SHOW CREATE TABLE) and the free read-only data checker (finds the rows that will fail). Planning the whole move: Migration Kit ($49, $34.30 with LAUNCH30 until Nov 5).

Each message above was reproduced on Postgres 17.11 (and the MySQL side on MariaDB 11.8) before it was written down. Error text is from Postgres 17; older versions word a few of them slightly differently.

Published with deed.page