# Migrate from MySQL

> Move a MySQL or MariaDB database to Postgres on SupaHost with pgloader: type differences to expect, loading over the SSH tunnel or via a local Postgres, and checking the result.

Source: https://supahost.app/kb/migrate-from-mysql · Updated 2026-11-11

The usual tool for MySQL to Postgres is [pgloader](https://pgloader.io): it reads the MySQL schema, converts types, creates tables and copies the data in one run. pgloader needs a direct Postgres connection as its target. On Business and Enterprise that's your project through the SSH tunnel. On other plans you run pgloader into a local Postgres 17 first, then move the result to SupaHost.

## Plan

- **Size.** The converted database must fit your plan's shared quota (8 GB Builder, 25 GB Pro, 50 GB Business).
- **Read the type differences** below and grep your app for MySQL-only SQL (backticks, `LIMIT x, y`, `ON DUPLICATE KEY UPDATE`, `IFNULL`, `GROUP_CONCAT`).
- **Downtime.** Stop writes to MySQL before the final run.

## Type and behaviour differences

| MySQL | Postgres on SupaHost |
|---|---|
| `tinyint(1)` | `boolean` (pgloader converts by default) |
| `int unsigned` | `bigint` (Postgres has no unsigned types) |
| `AUTO_INCREMENT` | `generated ... as identity` or `serial` |
| `datetime` | `timestamp`; consider `timestamptz` |
| `0000-00-00` dates | not valid; pgloader turns them into `null` |
| `enum('a','b')` | a Postgres enum type or `text` with a check constraint |
| case-insensitive `=` on strings | case-sensitive; use `citext` or `lower()` indexes |
| `ON DUPLICATE KEY UPDATE` | `insert ... on conflict (...) do update` |

## Export and convert

Write a pgloader command file, `bookings.load`:

```
LOAD DATABASE
  FROM mysql://app:secret@mysql.internal:3306/bookings
  INTO postgresql://postgres:<password>@127.0.0.1:6543/p_app_one

WITH include drop, create tables, create indexes, reset sequences, foreign keys

ALTER SCHEMA 'bookings' RENAME TO 'public';
```

The `ALTER SCHEMA` line matters: pgloader otherwise creates a schema named after the MySQL database, and the Data API only exposes `public`, `storage`, `graphql_public` and `pgmq_public`.

## Load

**Business and Enterprise.** Open the tunnel, then run pgloader against it:

```bash
ssh -N -L 6543:127.0.0.1:5432 supahost@acme.supahost.space &
pgloader bookings.load
```

Get the password from Dashboard → your project → **Connect** → **Database** → **Show connection string**.

**Builder and Pro.** Point `INTO` at a local Postgres 17 (for example `docker run -p 6543:5432 -e POSTGRES_PASSWORD=dev postgres:17`), run pgloader, check the result, then:

- dump it (`pg_dump -Fc --no-owner --no-privileges -n public`), restore it into a temporary Supabase Cloud project and use the [one-click import](/kb/migrate-from-supabase-cloud), or
- for small databases, run the schema from `pg_dump --schema-only` in Studio's SQL editor and import each table as CSV in the table editor.

## After loading

```sql
-- make the new tables safe to expose, then refresh planner stats
alter table public.reservations enable row level security;
alter table public.rooms enable row level security;
analyze;
```

Add policies for each table. If you didn't use `reset sequences`, see [duplicate key after importing data](/kb/duplicate-key-sequence).

## Switch the app

Replace the MySQL driver with supabase-js:

```ts
const { data, error } = await supabase
  .from('reservations')
  .upsert({ room_id: 12, night: '2026-12-24', guest_id: userId }, { onConflict: 'room_id,night' })
  .select()
```

Logic that used stored procedures becomes Postgres functions in PL/pgSQL, called with `supabase.rpc()`.

## Verify

Compare `SELECT COUNT(*)` per table on both sides, compare a few sums or max IDs, and check that text with accents and emoji came through intact (Postgres here is UTF-8; MySQL tables in `latin1` sometimes hide double-encoded text).

## Related

- [Tables, columns and data](/docs/database/tables)
- [Database functions](/docs/database/functions)
- [camelCase table or column names not working](/kb/camelcase-identifiers)
- [SSH access and direct Postgres](/docs/platform/ssh)
