Skip to content
IntoSQL home

Move a PostgreSQL dump to an older or newer PostgreSQL

Drop a plain dump from pg_dump or pg_dumpall, as .sql or .sql.gz, and download one script that loads on PostgreSQL 13 to 18, whichever version wrote the dump. Identities, enums, arrays and jsonb stay as they are, types an extension brings become text, and every change is listed in the report.

Your file
sqlshop_2024_postgres.sqlSample file, 6.2 kB
Your database

18 rows converted, no fixes, none rejected

Show

Tables in shop_2024_postgres.sqlPostgreSQL 18.6 dump · written by pg_dump · utf8mb4

  • customers8 rows
    8 columns
    idinteger not null
    nametext not null
    emailtext not null
    countryvarchar(2) not null
    is_activeboolean not null
    tagstext[] not null
    signed_update
    created_attimestamptz(6) not null
  • orders10 rows
    10 columns
    idinteger not null
    customer_idinteger not null
    statusorder_status not null
    totalnumeric(10,2) not null
    itemssmallint not null
    ordered_attimestamp(6) not null
    shipped_attimestamp(6)
    notetext
    detailsjsonb
    deliveryinterval

Not converted

  • View paid_orders

Each database writes these in its own language. Rewrite them for PostgreSQL.

-- Written by IntoSQL from a PostgreSQL dump. Load it with: psql -v ON_ERROR_STOP=1 -f <file>
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET synchronous_commit = off;
SET maintenance_work_mem = '512MB';

CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
CREATE TABLE customers (
    id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name text NOT NULL,
    email text NOT NULL,
    country varchar(2) DEFAULT 'US' NOT NULL,
    is_active boolean DEFAULT true NOT NULL,
    tags text[] DEFAULT '{}' NOT NULL,
    signed_up date,
    created_at timestamptz(6) DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TABLE orders (
    id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    customer_id integer NOT NULL,
    status order_status DEFAULT 'pending' NOT NULL,
    total numeric(10,2) NOT NULL,
    items smallint DEFAULT 1 NOT NULL,
    ordered_at timestamp(6) NOT NULL,
    shipped_at timestamp(6),
    note text,
    details jsonb,
    delivery interval,
    CONSTRAINT orders_total_check CHECK (total >= (0))
);

COPY customers (id, name, email, country, is_active, tags, signed_up, created_at) FROM stdin;
1	Ana Martínez	ana@example.com	MX	t	{vip}	2023-01-15	2023-01-15 10:24:00+00
2	Zoë Müller	zoe@example.de	DE	t	{}	2023-02-03	2023-02-03 08:11:42+00
3	José Peña	jose@example.es	ES	f	{wholesale,"late payer"}	2023-03-21	2023-03-21 19:05:17+00
4	李雷	lilei@example.cn	CN	t	{}	2023-05-09	2023-05-09 02:40:00+00
5	Noah O'Brien	noah@example.ie	IE	t	{}	\N	2023-06-30 14:00:00+00
6	Amélie Laurent	amelie@example.fr	FR	t	{vip}	2024-01-02	2024-01-02 09:30:00+00
7	Ravi Patel	ravi@example.in	IN	f	{}	2024-02-29	2024-02-29 23:59:59+00
8	Sofia Rossi 🌻	sofia@example.it	IT	t	{}	2024-04-12	2024-04-12 12:12:12+00
\.

COPY orders (id, customer_id, status, total, items, ordered_at, shipped_at, note, details, delivery) FROM stdin;
1	1	shipped	129.90	3	2024-03-01 10:00:00	2024-03-02 16:30:00	\N	{"gift": false}	1 day 06:30:00
2	2	paid	54.00	1	2024-03-02 11:15:00	\N	Gift wrap 🎁	{"gift": true, "wrap": "red"}	\N
3	1	cancelled	12.50	1	2024-03-03 09:45:00	\N	Customer changed their mind	\N	\N
4	4	shipped	980.00	12	2024-03-05 02:10:00	2024-03-07 08:00:00	请小心轻放	{"fragile": true}	2 days 05:50:00
5	5	pending	45.75	2	2024-03-08 17:20:00	\N	Leave at the door; ring twice	\N	\N
6	6	shipped	310.40	5	2024-03-10 13:00:00	2024-03-11 09:00:00	Fragile: "glass"	{"fragile": true}	20:00:00
7	8	paid	76.20	2	2024-03-11 18:45:00	\N	\N	\N	\N
8	2	shipped	19.99	1	2024-03-12 08:00:00	infinity	Shipped date set to infinity by an old import	\N	\N
9	3	shipped	64.00	2	2024-03-14 15:30:00	2024-03-15 10:00:00	Two parcels	{"parcels": [1, 2]}	18:30:00
10	7	paid	1499.00	1	2024-03-15 21:05:00	\N	C:\\Users\\ravi\\invoice.pdf	\N	\N
\.


-- Keys, indexes and constraints, after the rows so each builds once.
CREATE UNIQUE INDEX customers_email_key ON customers (email);
CREATE UNIQUE INDEX customers_pkey ON customers (id);
CREATE UNIQUE INDEX orders_pkey ON orders (id);
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status_date ON orders (status, ordered_at);
ALTER TABLE customers ADD CONSTRAINT customers_email_key UNIQUE USING INDEX customers_email_key;
ALTER TABLE customers ADD CONSTRAINT customers_pkey PRIMARY KEY USING INDEX customers_pkey;
ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY USING INDEX orders_pkey;
ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID;
DO $f$ BEGIN
    ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_id_fkey;
EXCEPTION WHEN foreign_key_violation THEN
    RAISE WARNING 'foreign key % has rows without a parent: it stays NOT VALID, and new rows are checked', 'orders_customer_id_fkey';
END $f$;
SELECT setval(pg_get_serial_sequence('customers', 'id'), LEAST(GREATEST(COALESCE((SELECT max(id) FROM customers)::numeric, 0) + 1, 1001), 2147483647)::bigint, false);
COMMENT ON TABLE customers IS 'People who bought at least once';
SELECT setval(pg_get_serial_sequence('orders', 'id'), LEAST(GREATEST(COALESCE((SELECT max(id) FROM orders)::numeric, 0) + 1, 5001), 2147483647)::bigint, false);

Fixes

Nothing needed fixing.

Needs a look

  • 1 view, trigger, routine or event isn't converted; the report lists each one. Recreate them by hand in PostgreSQL.
  • The index idx_orders_details on orders uses an expression that can't be written for PostgreSQL (a gin index), so it isn't created. Create the index by hand if queries need it.

Your file is converted on this computer and never uploaded.

Other samples:

What changes between the file and the script

Each row is a rule the converter applies, with the report line it writes when the rule changes something in your data.

  • varchar(n), char(n), text

    the same types

    The same types, with the same lengths.

  • numeric(p,s), numeric without a precision, NaN and Infinity

    the same types and values

    The same type: every digit is copied as the dump wrote it.

  • money

    numeric(19,2)

    How money is written depends on the server's lc_monetary, so the amount is kept as a number without the currency symbol, and it loads whatever the target's locale.

    Report line: type_replaced

  • enum types

    the same enum types, created before the tables

    The labels keep their order.

  • enum columns holding values outside their labels

    text without a check

    The type would reject those values, so the column is text and keeps all of them.

    Report line: enum_check_skipped

  • composite and range types the dump defines, extension types (citext, hstore, ltree, PostGIS) and multiranges

    text with PostgreSQL's text

    The script creates neither extensions nor composite types, so each value keeps the text PostgreSQL wrote; create the type and alter the column after loading.

    Report line: type_replaced

  • citext

    text, compared exactly

    The script doesn't create the citext extension, so these columns compare text exactly; create it and change the columns back after loading.

    Report line: case_insensitive_collation

  • json, jsonb

    the same types

    json keeps the text as written; jsonb keeps the values as the server normalized them.

  • timestamp with time zone

    timestamptz, the same instants

    Each value keeps the offset the dump wrote, so the instant is the same in any time zone.

  • time, time with time zone

    the same types

    The same types, 24:00:00 included.

  • infinity, -infinity and BC dates

    kept

    PostgreSQL holds them as they are.

  • NaN and Infinity in numeric, real and double precision

    kept

    PostgreSQL holds them as they are.

  • generated columns

    the same generated columns

    The same formula, computed by the target.

  • generated columns with functions IntoSQL doesn't write

    a plain column with the values

    A formula IntoSQL can't write leaves a plain column with the dump's values and a report line.

    Report line: generated_not_translated

  • defaults with functions IntoSQL doesn't write

    no default

    A default IntoSQL can't write is left out and reported.

    Report line: default_not_translated

  • CHECK constraints with functions IntoSQL doesn't write

    not created

    A check IntoSQL can't write is left out and reported.

    Report line: check_skipped

  • CHECK ... NOT VALID

    added NOT VALID after the rows

    The rows that were there before were never checked, so the check is added after them, still not valid.

  • gin, gist, brin and hash indexes, partial and expression indexes

    not created

    IntoSQL writes b-tree indexes on columns; create the others again after loading.

    Report line: index_skipped

  • foreign keys to tables the dump doesn't create

    not created

    A key can't name a table that isn't there.

    Report line: foreign_key_skipped

  • NOT NULL columns with values that become NULL

    NOT NULL dropped

    A column whose values had to become NULL can't keep NOT NULL, or the load would stop.

    Report line: not_null_relaxed

  • partitioned tables

    one table with every partition's rows

    The rows are the same; partition the table again if it needs it.

    Report line: partitions_dropped

  • views, functions, procedures, triggers, rules, policies, extensions and types other than enums

    listed in the report

    IntoSQL converts tables and their rows; the rest is listed for you to recreate.

    Report line: not_converted_objects

  • dumps without CREATE TABLE (--data-only)

    types read from the values

    A data-only dump has only rows, so the types come from the values.

    Report line: no_schema

  • expressions in INSERT values

    NULL

    A converter that doesn't run a database can't evaluate them.

    Report line: expression_value

  • types IntoSQL doesn't know

    text

    Text holds any value; change the type after loading.

    Report line: unknown_type

  • text that isn't valid in its encoding

    the bad bytes as the replacement character

    PostgreSQL text must be valid UTF-8.

    Report line: invalid_utf8_replaced

  • text decoded twice (ñ)

    copied as it is

    The dump holds exactly these characters, so they are copied; the report points them out.

    Report line: mojibake_suspected

In the fileIn the scriptReport line
varchar(n), char(n), textthe same typesThe same types, with the same lengths.
numeric(p,s), numeric without a precision, NaN and Infinitythe same types and valuesThe same type: every digit is copied as the dump wrote it.
moneynumeric(19,2)How money is written depends on the server's lc_monetary, so the amount is kept as a number without the currency symbol, and it loads whatever the target's locale.type_replaced
enum typesthe same enum types, created before the tablesThe labels keep their order.
enum columns holding values outside their labelstext without a checkThe type would reject those values, so the column is text and keeps all of them.enum_check_skipped
composite and range types the dump defines, extension types (citext, hstore, ltree, PostGIS) and multirangestext with PostgreSQL's textThe script creates neither extensions nor composite types, so each value keeps the text PostgreSQL wrote; create the type and alter the column after loading.type_replaced
citexttext, compared exactlyThe script doesn't create the citext extension, so these columns compare text exactly; create it and change the columns back after loading.case_insensitive_collation
json, jsonbthe same typesjson keeps the text as written; jsonb keeps the values as the server normalized them.
timestamp with time zonetimestamptz, the same instantsEach value keeps the offset the dump wrote, so the instant is the same in any time zone.
time, time with time zonethe same typesThe same types, 24:00:00 included.
infinity, -infinity and BC dateskeptPostgreSQL holds them as they are.
NaN and Infinity in numeric, real and double precisionkeptPostgreSQL holds them as they are.
generated columnsthe same generated columnsThe same formula, computed by the target.
generated columns with functions IntoSQL doesn't writea plain column with the valuesA formula IntoSQL can't write leaves a plain column with the dump's values and a report line.generated_not_translated
defaults with functions IntoSQL doesn't writeno defaultA default IntoSQL can't write is left out and reported.default_not_translated
CHECK constraints with functions IntoSQL doesn't writenot createdA check IntoSQL can't write is left out and reported.check_skipped
CHECK ... NOT VALIDadded NOT VALID after the rowsThe rows that were there before were never checked, so the check is added after them, still not valid.
gin, gist, brin and hash indexes, partial and expression indexesnot createdIntoSQL writes b-tree indexes on columns; create the others again after loading.index_skipped
foreign keys to tables the dump doesn't createnot createdA key can't name a table that isn't there.foreign_key_skipped
NOT NULL columns with values that become NULLNOT NULL droppedA column whose values had to become NULL can't keep NOT NULL, or the load would stop.not_null_relaxed
partitioned tablesone table with every partition's rowsThe rows are the same; partition the table again if it needs it.partitions_dropped
views, functions, procedures, triggers, rules, policies, extensions and types other than enumslisted in the reportIntoSQL converts tables and their rows; the rest is listed for you to recreate.not_converted_objects
dumps without CREATE TABLE (--data-only)types read from the valuesA data-only dump has only rows, so the types come from the values.no_schema
expressions in INSERT valuesNULLA converter that doesn't run a database can't evaluate them.expression_value
types IntoSQL doesn't knowtextText holds any value; change the type after loading.unknown_type
text that isn't valid in its encodingthe bad bytes as the replacement characterPostgreSQL text must be valid UTF-8.invalid_utf8_replaced
text decoded twice (ñ)copied as it isThe dump holds exactly these characters, so they are copied; the report points them out.mojibake_suspected

Load the script into PostgreSQL

The script creates the tables, copies the rows, then adds keys, indexes and foreign keys, so it loads fast and stops at the first error.

psql -v ON_ERROR_STOP=1 -d mydb -f postgresql.postgresql.sql

Foreign keys are added as NOT VALID and then validated. If a row points to a row that doesn't exist, the script tells you which constraint, instead of failing half way.

For a large dump, create the database first and run the script with psql from the same machine as the server.

IntoSQL Pro loads the file straight into PostgreSQL from the desktop app or the command line, without writing a script.

Dump PostgreSQL for the best result

The converter reads what the dump holds. These options keep more of your data in it:

  • --format=plain, pg_dump's default, writes SQL. A custom or directory archive (-Fc, -Fd) becomes SQL with pg_restore -f dump.sql, without a database server.
  • --no-owner --no-privileges leave out roles and grants, which the converter skips anyway, so the dump is smaller.
  • pg_dumpall dumps every database in one file; each one's tables are named after it, so nothing collides.

What isn't converted

Views, triggers, procedures, functions and other objects written in PostgreSQL's own language are listed in the report by name, never dropped silently, so you can check each one against the PostgreSQL version you load into before recreating it.

Questions about moving PostgreSQL between versions

Not answered here? Write to us from the contact page and a person replies. Contact page

Is my dump uploaded?

No. The converter runs in your browser and reads the dump from your disk in pieces, so it never leaves your computer. The desktop app and the command line convert on your computer too.

How large can the dump be?

The converter never holds the whole dump in memory: it reads the file twice, by ranges, and writes the script as it goes. Multi-gigabyte dumps convert in the browser, in the desktop app and with the command line.

Does it read pg_dump -Fc archives?

Not directly: a custom archive is a binary format. pg_restore -f dump.sql dump.backup turns it into the plain SQL the converter reads, without connecting to a server. The converter says so when it gets an archive.

Which PostgreSQL versions can the dump come from?

Dumps written by pg_dump and pg_dumpall from PostgreSQL 10 on, with COPY or INSERT rows. Every test dump comes from PostgreSQL 13 and 18 and is compared with its source cell by cell.

Are serial and identity values kept?

Yes. Serial and identity columns stay identity columns, every row keeps its id, and each counter continues from the value the dump's sequence had reached.

What happens to arrays, enums and other PostgreSQL types?

They stay as they are: arrays, ranges, json and jsonb, bit strings, money aside. Enum types are created again with their labels. Types an extension brings (citext, hstore, PostGIS) and composite types become text, and the report says which columns.

What about functions, triggers and views?

They are listed in the report by name and not converted, so you can recreate them once the tables are in. NOT VALID checks are added after the rows, still not valid.

Which PostgreSQL versions does the script load on?

PostgreSQL 13 to 18, whichever version wrote the dump. The same script loads on each of them, stopping at the first error if anything goes wrong.