Skip to content
IntoSQL home

Convert PostgreSQL to Oracle

Drop a plain dump from pg_dump or pg_dumpall, as .sql or .sql.gz, and download a script for Oracle with the same tables, keys, indexes and data. Every value Oracle can't hold as it is, and every object it doesn't carry, is listed in the report.

Your file
sqlshop_2024_postgres.sqlSample file, 6.2 kB
Your database

18 rows converted, 2 fixes, none rejected

Show

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

  • customerswritten as CUSTOMERS8 rows1
    8 columns
    IDNUMBER(10) not null
    NAMEVARCHAR2(4000 CHAR) not null
    EMAILVARCHAR2(750 CHAR) not null
    COUNTRYVARCHAR2(2 CHAR) not null
    IS_ACTIVENUMBER(1) not null
    TAGSCLOB not null
    SIGNED_UPDATE
    CREATED_ATTIMESTAMP(6) WITH TIME ZONE not null
  • orderswritten as ORDERS10 rows1
    10 columns
    IDNUMBER(10) not null
    CUSTOMER_IDNUMBER(10) not null
    STATUSVARCHAR2(9 CHAR) not null
    TOTALNUMBER(10,2) not null
    ITEMSNUMBER(5) not null
    ORDERED_ATTIMESTAMP(6) not null
    SHIPPED_ATTIMESTAMP(6)
    NOTEVARCHAR2(4000 CHAR)
    DETAILSCLOB
    DELIVERYVARCHAR2(100 CHAR)

Not converted

  • View paid_orders

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

-- Written by IntoSQL from a PostgreSQL dump. Load it with: NLS_LANG=.AL32UTF8 sqlplus -S -L <user>/<password>@<database> @<file>
WHENEVER SQLERROR EXIT FAILURE ROLLBACK;
SET DEFINE OFF;
SET SQLBLANKLINES ON;
SET FEEDBACK OFF;
SET SERVEROUTPUT ON;
CREATE OR REPLACE FUNCTION "INTOSQL_CLOB" (p SYS.ODCIVARCHAR2LIST) RETURN CLOB IS
    r CLOB;
BEGIN
    DBMS_LOB.CREATETEMPORARY(r, TRUE);
    FOR i IN 1 .. p.COUNT LOOP
        IF p(i) IS NOT NULL THEN
            DBMS_LOB.WRITEAPPEND(r, LENGTH2(p(i)), p(i));
        END IF;
    END LOOP;
    RETURN r;
END;
/
CREATE OR REPLACE FUNCTION "INTOSQL_BLOB" (p SYS.ODCIVARCHAR2LIST) RETURN BLOB IS
    r BLOB;
    piece RAW(2000);
BEGIN
    DBMS_LOB.CREATETEMPORARY(r, TRUE);
    FOR i IN 1 .. p.COUNT LOOP
        piece := HEXTORAW(p(i));
        DBMS_LOB.WRITEAPPEND(r, UTL_RAW.LENGTH(piece), piece);
    END LOOP;
    RETURN r;
END;
/

CREATE TABLE "CUSTOMERS" (
    "ID" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH 1001),
    "NAME" VARCHAR2(4000 CHAR) NOT NULL,
    "EMAIL" VARCHAR2(750 CHAR) NOT NULL,
    "COUNTRY" VARCHAR2(2 CHAR) DEFAULT 'US' NOT NULL,
    "IS_ACTIVE" NUMBER(1) DEFAULT 1 NOT NULL,
    "TAGS" CLOB NOT NULL,
    "SIGNED_UP" DATE,
    "CREATED_AT" TIMESTAMP(6) WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
    CONSTRAINT "CK_CUSTOMERS_IS_ACTIVE" CHECK ("IS_ACTIVE" IN (0, 1))
);

CREATE TABLE "ORDERS" (
    "ID" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH 5001),
    "CUSTOMER_ID" NUMBER(10) NOT NULL,
    "STATUS" VARCHAR2(9 CHAR) DEFAULT 'pending' NOT NULL,
    "TOTAL" NUMBER(10,2) NOT NULL,
    "ITEMS" NUMBER(5) DEFAULT 1 NOT NULL,
    "ORDERED_AT" TIMESTAMP(6) NOT NULL,
    "SHIPPED_AT" TIMESTAMP(6),
    "NOTE" VARCHAR2(4000 CHAR),
    "DETAILS" CLOB,
    "DELIVERY" VARCHAR2(100 CHAR),
    CONSTRAINT "CK_ORDERS_STATUS" CHECK ("STATUS" IN ('pending', 'paid', 'shipped', 'cancelled')),
    CONSTRAINT "ORDERS_TOTAL_CHECK" CHECK ("TOTAL" >= (0))
);

BEGIN
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
1,
'Ana Martínez',
'ana@example.com',
'MX',
1,
'["vip"]',
DATE '2023-01-15',
TIMESTAMP '2023-01-15 10:24:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
2,
'Zoë Müller',
'zoe@example.de',
'DE',
1,
'[]',
DATE '2023-02-03',
TIMESTAMP '2023-02-03 08:11:42 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
3,
'José Peña',
'jose@example.es',
'ES',
0,
'["wholesale","late payer"]',
DATE '2023-03-21',
TIMESTAMP '2023-03-21 19:05:17 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
4,
'李雷',
'lilei@example.cn',
'CN',
1,
'[]',
DATE '2023-05-09',
TIMESTAMP '2023-05-09 02:40:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
5,
'Noah O''Brien',
'noah@example.ie',
'IE',
1,
'[]',
NULL,
TIMESTAMP '2023-06-30 14:00:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
6,
'Amélie Laurent',
'amelie@example.fr',
'FR',
1,
'["vip"]',
DATE '2024-01-02',
TIMESTAMP '2024-01-02 09:30:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
7,
'Ravi Patel',
'ravi@example.in',
'IN',
0,
'[]',
DATE '2024-02-29',
TIMESTAMP '2024-02-29 23:59:59 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
8,
'Sofia Rossi 🌻',
'sofia@example.it',
'IT',
1,
'[]',
DATE '2024-04-12',
TIMESTAMP '2024-04-12 12:12:12 +00:00'
);
COMMIT;
END;
/
BEGIN
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
1,
1,
'shipped',
129.90,
3,
TIMESTAMP '2024-03-01 10:00:00',
TIMESTAMP '2024-03-02 16:30:00',
NULL,
'{"gift": false}',
'1 day 06:30:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
2,
2,
'paid',
54.00,
1,
TIMESTAMP '2024-03-02 11:15:00',
NULL,
'Gift wrap 🎁',
'{"gift": true, "wrap": "red"}',
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
3,
1,
'cancelled',
12.50,
1,
TIMESTAMP '2024-03-03 09:45:00',
NULL,
'Customer changed their mind',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
4,
4,
'shipped',
980.00,
12,
TIMESTAMP '2024-03-05 02:10:00',
TIMESTAMP '2024-03-07 08:00:00',
'请小心轻放',
'{"fragile": true}',
'2 days 05:50:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
5,
5,
'pending',
45.75,
2,
TIMESTAMP '2024-03-08 17:20:00',
NULL,
'Leave at the door; ring twice',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
6,
6,
'shipped',
310.40,
5,
TIMESTAMP '2024-03-10 13:00:00',
TIMESTAMP '2024-03-11 09:00:00',
'Fragile: "glass"',
'{"fragile": true}',
'20:00:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
7,
8,
'paid',
76.20,
2,
TIMESTAMP '2024-03-11 18:45:00',
NULL,
NULL,
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
8,
2,
'shipped',
19.99,
1,
TIMESTAMP '2024-03-12 08:00:00',
NULL,
'Shipped date set to infinity by an old import',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
9,
3,
'shipped',
64.00,
2,
TIMESTAMP '2024-03-14 15:30:00',
TIMESTAMP '2024-03-15 10:00:00',
'Two parcels',
'{"parcels": [1, 2]}',
'18:30:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
10,
7,
'paid',
1499.00,
1,
TIMESTAMP '2024-03-15 21:05:00',
NULL,
'C:\Users\ravi\invoice.pdf',
NULL,
NULL
);
COMMIT;
END;
/

-- Keys, indexes, foreign keys, counters and triggers, after the rows.
ALTER TABLE "CUSTOMERS" ADD CONSTRAINT "UQ_CUSTOMERS_CUSTOMERS_EMAIL_KEY" UNIQUE ("EMAIL");
ALTER TABLE "CUSTOMERS" ADD CONSTRAINT "PK_CUSTOMERS" PRIMARY KEY ("ID");
ALTER TABLE "ORDERS" ADD CONSTRAINT "PK_ORDERS" PRIMARY KEY ("ID");
CREATE INDEX "IX_ORDERS_IDX_ORDERS_CUSTOMER" ON "ORDERS" ("CUSTOMER_ID");
CREATE INDEX "IX_ORDERS_IDX_ORDERS_STATUS_DATE" ON "ORDERS" ("STATUS", "ORDERED_AT");
ALTER TABLE "ORDERS" ADD CONSTRAINT "ORDERS_CUSTOMER_ID_FKEY" FOREIGN KEY ("CUSTOMER_ID") REFERENCES "CUSTOMERS" ("ID") ENABLE NOVALIDATE;
BEGIN
    EXECUTE IMMEDIATE 'ALTER TABLE "ORDERS" ENABLE VALIDATE CONSTRAINT "ORDERS_CUSTOMER_ID_FKEY"';
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Foreign key ' || '"ORDERS_CUSTOMER_ID_FKEY"' || ' has rows without a parent: it stays unvalidated, and new rows are checked.');
END;
/
DECLARE
    next_id NUMBER;
BEGIN
    SELECT NVL(MAX("ID"), 0) + 1 INTO next_id FROM "CUSTOMERS";
    IF next_id < 1001 THEN
        next_id := 1001;
    END IF;
    EXECUTE IMMEDIATE 'ALTER TABLE "CUSTOMERS" MODIFY ("ID" GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH ' || next_id || '))';
END;
/
COMMENT ON TABLE "CUSTOMERS" IS 'People who bought at least once';
DECLARE
    next_id NUMBER;
BEGIN
    SELECT NVL(MAX("ID"), 0) + 1 INTO next_id FROM "ORDERS";
    IF next_id < 5001 THEN
        next_id := 5001;
    END IF;
    EXECUTE IMMEDIATE 'ALTER TABLE "ORDERS" MODIFY ("ID" GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH ' || next_id || '))';
END;
/
DROP FUNCTION "INTOSQL_CLOB";
DROP FUNCTION "INTOSQL_BLOB";
COMMIT;
EXIT;

Fixes

  1. "tags" of customers is text[], which Oracle doesn't have, so it is CLOB with the same values.
  2. "delivery" of orders is interval, which Oracle doesn't have, so it is VARCHAR2(100 CHAR) with the same values.

Needs a look

  • 1 view, trigger, routine or event isn't converted; the report lists each one. Recreate them by hand in Oracle.
  • The default of "tags" in customers can't be written for Oracle (a value its type can't hold), so the column has no default. Add the default by hand if new rows need it.
  • The index idx_orders_details on orders uses an expression that can't be written for Oracle (a gin index), so it isn't created. Create the index by hand if queries need it.
  • 1 value in "shipped_at" of orders is a date Oracle can't store, such as infinity, so it is NULL. Oracle stores years 1 to 9999 without infinity; replace those dates in the source if they matter.

Your file is converted on this computer and never uploaded.

Other samples:

What changes from PostgreSQL to Oracle

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

  • boolean

    NUMBER(1) with a CHECK of 0 and 1

    Oracle 21c has no boolean column type; 1 and 0 keep true and false on every version.

  • varchar(n), char(n)

    VARCHAR2(n CHAR), counting characters

    Lengths count characters as PostgreSQL's do.

  • text

    VARCHAR2(4000 CHAR) when every value fits 4000 bytes (750 characters in a key), else CLOB

    A VARCHAR2 holds 4000 bytes by default and Oracle can't index a CLOB, so text keeps VARCHAR2 when its values fit and is a CLOB when they don't.

    Report line: text_as_lob

  • empty strings and empty bytea values

    NULL

    Oracle stores an empty string as NULL, so a value that was ' reads back as NULL. The report counts them per column.

    Report line: empty_string_null

  • citext

    exact comparison

    Oracle compares text exactly unless told otherwise, as PostgreSQL's text does; citext columns lose ignoring case.

    Report line: case_insensitive_collation

  • numeric(p,s) past 38 digits; numeric without a precision

    NUMBER when its values fit 38 digits, else VARCHAR2

    Oracle's NUMBER holds 38 digits at any scale. Wider numbers are kept as text so every digit stays exact.

    Report line: decimal_as_text

  • money

    NUMBER(19,2)

    Oracle has no money type. The amount is kept as a number, without the currency symbol the server's locale added.

    Report line: type_replaced

  • interval

    VARCHAR2(100 CHAR) with PostgreSQL's text

    An interval mixes months with days and seconds, which Oracle keeps in two different types; the text keeps every part.

    Report line: type_replaced

  • arrays (integer[], text[]...)

    CLOB with a JSON array

    Oracle has no arrays. A JSON array keeps every item and NULL, and JSON_TABLE reads it.

    Report line: type_replaced

  • ranges, geometric types, tsvector, xml, hstore, PostGIS, composite and extension types

    VARCHAR2 or CLOB with PostgreSQL's text

    Oracle has no equivalent type, so each value keeps the text PostgreSQL wrote.

    Report line: type_replaced

  • json, jsonb

    CLOB

    Oracle's JSON functions read JSON from text on every version, and a CLOB keeps values of any size.

  • timestamp

    TIMESTAMP

    A timestamp without time zone is a wall-clock reading, like Oracle's TIMESTAMP. date becomes DATE, which also holds a time of day that stays at midnight.

  • timestamp with time zone

    TIMESTAMP WITH TIME ZONE, written in UTC

    Each value is written in UTC, the same instant whatever offset the dump wrote.

  • time

    INTERVAL DAY TO SECOND from midnight

    Oracle has no time-of-day type; an interval from midnight holds every value, 24:00:00 included.

  • infinity, -infinity, BC dates and years past 9999

    NULL

    The script writes years 1 to 9999, and Oracle has no infinity. Those values become NULL and the report counts them.

    Report line: date_out_of_range

  • NaN and Infinity in numeric

    NULL

    Oracle's NUMBER has no NaN or Infinity, so those values become NULL and the report counts them.

    Report line: invalid_value_null

  • 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

  • enum types

    VARCHAR2 with a CHECK of its labels

    Text with a check keeps the same values.

  • enum columns holding values outside their labels

    VARCHAR2 without a check

    Values the labels don't name would fail the check, so the check isn't created.

    Report line: enum_check_skipped

  • uuid, inet, cidr, macaddr

    VARCHAR2

    Oracle has no such types; the text keeps every value.

  • bytea

    BLOB

    Bytes stay bytes.

  • bit(n), bit varying

    NUMBER with the bits' value up to 63 bits; text digits beyond

    Bit strings are stored as the number their bits make.

  • oid, xid, cid

    NUMBER(10) with a CHECK that it isn't negative

    NUMBER holds every value.

  • serial, identity

    GENERATED BY DEFAULT ON NULL AS IDENTITY, continuing after the highest id

    The copied ids load as they are, and the counter continues after the highest one or the sequence's next value, whichever is higher.

  • a second serial or identity column in a table

    a plain integer

    Oracle counts one identity column per table; any other keeps its values without a counter.

    Report line: counter_dropped

  • sequences not owned by a column

    CREATE SEQUENCE, at the value it had reached

    Oracle sequences work the same way.

  • generated columns

    virtual columns

    Oracle computes generated columns on read only; STORED columns become virtual with the same values.

    Report line: stored_as_virtual

  • generated columns with functions Oracle lacks

    a plain column with the values

    A formula that can't be written for Oracle leaves a plain column with the dump's values and a report line.

    Report line: generated_not_translated

  • defaults with functions Oracle lacks, and defaults from a sequence

    no default

    A default that can't be written for Oracle is left out and reported.

    Report line: default_not_translated

  • CHECK constraints

    translated, inline in CREATE TABLE

    Inline checks are tested as each row loads.

  • CHECK constraints with functions Oracle lacks

    not created

    A check that can't be written for Oracle is left out and reported.

    Report line: check_skipped

  • CHECK ... NOT VALID

    not created

    PostgreSQL never checked the rows that were there before, so they may break it.

    Report line: check_not_valid

  • gin, gist, brin and hash indexes, partial and expression indexes, and indexes on CLOB columns

    not created

    IntoSQL writes b-tree indexes on columns for Oracle, which can't index a CLOB; create the others again after loading.

    Report line: index_skipped

  • foreign keys

    after the data ENABLE NOVALIDATE, then validated with a warning on orphans

    The key checks new rows at once and is validated last; rows without a parent leave it unvalidated with a warning instead of stopping the load.

  • ON UPDATE CASCADE, SET NULL or SET DEFAULT; ON DELETE SET DEFAULT

    dropped

    Oracle's foreign keys act on delete only, with CASCADE or SET NULL.

    Report line: foreign_key_action_unsupported

  • 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

  • names over 128 bytes

    shortened

    Oracle 12.2 and later names hold 128 bytes, more than PostgreSQL's 63.

    Report line: name_shortened

  • partitioned tables

    one table with every partition's rows

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

    Report line: partitions_dropped

  • schemas

    one schema, tables named after their schema first

    The script loads into the schema of the user who runs it, so tables of different schemas get their schema's name in front; tables in public keep their names.

    Report line: schemas_merged

  • views, functions, procedures, triggers, rules, policies, extensions and types

    listed in the report

    Their code is written in PostgreSQL's language, so they are 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

    CLOB

    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

    The script is UTF-8 and loads with NLS_LANG=.AL32UTF8.

    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 PostgreSQLIn OracleReport line
booleanNUMBER(1) with a CHECK of 0 and 1Oracle 21c has no boolean column type; 1 and 0 keep true and false on every version.
varchar(n), char(n)VARCHAR2(n CHAR), counting charactersLengths count characters as PostgreSQL's do.
textVARCHAR2(4000 CHAR) when every value fits 4000 bytes (750 characters in a key), else CLOBA VARCHAR2 holds 4000 bytes by default and Oracle can't index a CLOB, so text keeps VARCHAR2 when its values fit and is a CLOB when they don't.text_as_lob
empty strings and empty bytea valuesNULLOracle stores an empty string as NULL, so a value that was ' reads back as NULL. The report counts them per column.empty_string_null
citextexact comparisonOracle compares text exactly unless told otherwise, as PostgreSQL's text does; citext columns lose ignoring case.case_insensitive_collation
numeric(p,s) past 38 digits; numeric without a precisionNUMBER when its values fit 38 digits, else VARCHAR2Oracle's NUMBER holds 38 digits at any scale. Wider numbers are kept as text so every digit stays exact.decimal_as_text
moneyNUMBER(19,2)Oracle has no money type. The amount is kept as a number, without the currency symbol the server's locale added.type_replaced
intervalVARCHAR2(100 CHAR) with PostgreSQL's textAn interval mixes months with days and seconds, which Oracle keeps in two different types; the text keeps every part.type_replaced
arrays (integer[], text[]...)CLOB with a JSON arrayOracle has no arrays. A JSON array keeps every item and NULL, and JSON_TABLE reads it.type_replaced
ranges, geometric types, tsvector, xml, hstore, PostGIS, composite and extension typesVARCHAR2 or CLOB with PostgreSQL's textOracle has no equivalent type, so each value keeps the text PostgreSQL wrote.type_replaced
json, jsonbCLOBOracle's JSON functions read JSON from text on every version, and a CLOB keeps values of any size.
timestampTIMESTAMPA timestamp without time zone is a wall-clock reading, like Oracle's TIMESTAMP. date becomes DATE, which also holds a time of day that stays at midnight.
timestamp with time zoneTIMESTAMP WITH TIME ZONE, written in UTCEach value is written in UTC, the same instant whatever offset the dump wrote.
timeINTERVAL DAY TO SECOND from midnightOracle has no time-of-day type; an interval from midnight holds every value, 24:00:00 included.
infinity, -infinity, BC dates and years past 9999NULLThe script writes years 1 to 9999, and Oracle has no infinity. Those values become NULL and the report counts them.date_out_of_range
NaN and Infinity in numericNULLOracle's NUMBER has no NaN or Infinity, so those values become NULL and the report counts them.invalid_value_null
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
enum typesVARCHAR2 with a CHECK of its labelsText with a check keeps the same values.
enum columns holding values outside their labelsVARCHAR2 without a checkValues the labels don't name would fail the check, so the check isn't created.enum_check_skipped
uuid, inet, cidr, macaddrVARCHAR2Oracle has no such types; the text keeps every value.
byteaBLOBBytes stay bytes.
bit(n), bit varyingNUMBER with the bits' value up to 63 bits; text digits beyondBit strings are stored as the number their bits make.
oid, xid, cidNUMBER(10) with a CHECK that it isn't negativeNUMBER holds every value.
serial, identityGENERATED BY DEFAULT ON NULL AS IDENTITY, continuing after the highest idThe copied ids load as they are, and the counter continues after the highest one or the sequence's next value, whichever is higher.
a second serial or identity column in a tablea plain integerOracle counts one identity column per table; any other keeps its values without a counter.counter_dropped
sequences not owned by a columnCREATE SEQUENCE, at the value it had reachedOracle sequences work the same way.
generated columnsvirtual columnsOracle computes generated columns on read only; STORED columns become virtual with the same values.stored_as_virtual
generated columns with functions Oracle lacksa plain column with the valuesA formula that can't be written for Oracle leaves a plain column with the dump's values and a report line.generated_not_translated
defaults with functions Oracle lacks, and defaults from a sequenceno defaultA default that can't be written for Oracle is left out and reported.default_not_translated
CHECK constraintstranslated, inline in CREATE TABLEInline checks are tested as each row loads.
CHECK constraints with functions Oracle lacksnot createdA check that can't be written for Oracle is left out and reported.check_skipped
CHECK ... NOT VALIDnot createdPostgreSQL never checked the rows that were there before, so they may break it.check_not_valid
gin, gist, brin and hash indexes, partial and expression indexes, and indexes on CLOB columnsnot createdIntoSQL writes b-tree indexes on columns for Oracle, which can't index a CLOB; create the others again after loading.index_skipped
foreign keysafter the data ENABLE NOVALIDATE, then validated with a warning on orphansThe key checks new rows at once and is validated last; rows without a parent leave it unvalidated with a warning instead of stopping the load.
ON UPDATE CASCADE, SET NULL or SET DEFAULT; ON DELETE SET DEFAULTdroppedOracle's foreign keys act on delete only, with CASCADE or SET NULL.foreign_key_action_unsupported
foreign keys to tables the dump doesn't createnot createdA key can't name a table that isn't there.foreign_key_skipped
names over 128 bytesshortenedOracle 12.2 and later names hold 128 bytes, more than PostgreSQL's 63.name_shortened
partitioned tablesone table with every partition's rowsThe rows are the same; partition the table again in Oracle if it needs it.partitions_dropped
schemasone schema, tables named after their schema firstThe script loads into the schema of the user who runs it, so tables of different schemas get their schema's name in front; tables in public keep their names.schemas_merged
views, functions, procedures, triggers, rules, policies, extensions and typeslisted in the reportTheir code is written in PostgreSQL's language, so they are 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 knowCLOBText holds any value; change the type after loading.unknown_type
text that isn't valid in its encodingthe bad bytes as the replacement characterThe script is UTF-8 and loads with NLS_LANG=.AL32UTF8.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 Oracle

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.

NLS_LANG=.AL32UTF8 sqlplus -S -L user@//localhost/mydb @postgresql.oracle.sql

Foreign keys are added ENABLE NOVALIDATE, so new rows are checked at once, and then validated. If an old row points to a row that doesn't exist, the script says which constraint and leaves it unvalidated, instead of failing half way.

For a large dump, run SQL*Plus on the same machine as the database. The script commits every 100 rows. The user that runs it needs CREATE TABLE, CREATE SEQUENCE, CREATE TRIGGER and CREATE PROCEDURE, and quota on its tablespace.

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, materialized views, functions, procedures, triggers, rules, row security policies, extensions and types are written in PostgreSQL's own language. They are listed in the report by name, never dropped silently, so you can rewrite them for Oracle.

Questions about PostgreSQL to Oracle

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. Each table's serial or identity column becomes an identity column, every row keeps its id, and the next id continues after the highest one. Sequences not owned by a column become Oracle sequences at the value they had reached.

What happens to arrays, enums and other PostgreSQL types?

Arrays become JSON arrays, booleans NUMBER(1), timestamps with a time zone TIMESTAMP WITH TIME ZONE, and text VARCHAR2 when its values fit, else CLOB. Empty strings become NULL, as Oracle stores them, and the report counts them.

What about functions, triggers and views?

They are listed in the report by name and not converted, because each database writes them in its own language.

Which Oracle versions does the script load on?

Oracle 19c and later. Every test dump loads on 21c and 23ai, stopping at the first error if anything goes wrong.