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.
18 rows converted, 2 fixes, none rejected
Tables in shop_2024_postgres.sqlPostgreSQL 18.6 dump · written by pg_dump · utf8mb4
customerswritten as CUSTOMERS8 rows1
8 columns ID NUMBER(10) not null NAME VARCHAR2(4000 CHAR) not null EMAIL VARCHAR2(750 CHAR) not null COUNTRY VARCHAR2(2 CHAR) not null IS_ACTIVE NUMBER(1) not null TAGS CLOB not null SIGNED_UP DATE CREATED_AT TIMESTAMP(6) WITH TIME ZONE not null orderswritten as ORDERS10 rows1
10 columns ID NUMBER(10) not null CUSTOMER_ID NUMBER(10) not null STATUS VARCHAR2(9 CHAR) not null TOTAL NUMBER(10,2) not null ITEMS NUMBER(5) not null ORDERED_AT TIMESTAMP(6) not null SHIPPED_AT TIMESTAMP(6) NOTE VARCHAR2(4000 CHAR) DETAILS CLOB DELIVERY VARCHAR2(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
- "tags" of customers is text[], which Oracle doesn't have, so it is CLOB with the same values.
- "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 PostgreSQL | In Oracle | Report line |
|---|---|---|
| boolean | NUMBER(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. | |
| text | VARCHAR2(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 values | NULLOracle stores an empty string as NULL, so a value that was ' reads back as NULL. The report counts them per column. | empty_string_null |
| citext | exact 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 precision | NUMBER 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 |
| 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. | type_replaced |
| interval | VARCHAR2(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 types | VARCHAR2 or CLOB with PostgreSQL's textOracle has no equivalent type, so each value keeps the text PostgreSQL wrote. | type_replaced |
| json, jsonb | CLOBOracle's JSON functions read JSON from text on every version, and a CLOB keeps values of any size. | |
| timestamp | TIMESTAMPA 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 UTCEach value is written in UTC, the same instant whatever offset the dump wrote. | |
| time | INTERVAL 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 9999 | NULLThe 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 numeric | NULLOracle'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 NULL | NOT NULL droppedA column whose values had to become NULL can't keep NOT NULL, or the load would stop. | not_null_relaxed |
| enum types | VARCHAR2 with a CHECK of its labelsText with a check keeps the same values. | |
| enum columns holding values outside their labels | VARCHAR2 without a checkValues the labels don't name would fail the check, so the check isn't created. | enum_check_skipped |
| uuid, inet, cidr, macaddr | VARCHAR2Oracle has no such types; the text keeps every value. | |
| bytea | BLOBBytes stay bytes. | |
| bit(n), bit varying | NUMBER with the bits' value up to 63 bits; text digits beyondBit strings are stored as the number their bits make. | |
| oid, xid, cid | NUMBER(10) with a CHECK that it isn't negativeNUMBER holds every value. | |
| serial, identity | GENERATED 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 table | a plain integerOracle counts one identity column per table; any other keeps its values without a counter. | counter_dropped |
| sequences not owned by a column | CREATE SEQUENCE, at the value it had reachedOracle sequences work the same way. | |
| generated columns | virtual columnsOracle computes generated columns on read only; STORED columns become virtual with the same values. | stored_as_virtual |
| generated columns with functions Oracle lacks | a 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 sequence | no defaultA default that can't be written for Oracle is left out and reported. | default_not_translated |
| CHECK constraints | translated, inline in CREATE TABLEInline checks are tested as each row loads. | |
| CHECK constraints with functions Oracle lacks | not createdA check that can't be written for Oracle is left out and reported. | check_skipped |
| CHECK ... NOT VALID | not 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 columns | not createdIntoSQL writes b-tree indexes on columns for Oracle, which can't index a CLOB; create the others again after loading. | index_skipped |
| foreign keys | after 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 DEFAULT | droppedOracle's foreign keys act on delete only, with CASCADE or SET NULL. | foreign_key_action_unsupported |
| foreign keys to tables the dump doesn't create | not createdA key can't name a table that isn't there. | foreign_key_skipped |
| names over 128 bytes | shortenedOracle 12.2 and later names hold 128 bytes, more than PostgreSQL's 63. | name_shortened |
| partitioned tables | one table with every partition's rowsThe rows are the same; partition the table again in Oracle if it needs it. | partitions_dropped |
| schemas | one 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 types | listed 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 values | NULLA converter that doesn't run a database can't evaluate them. | expression_value |
| types IntoSQL doesn't know | CLOBText holds any value; change the type after loading. | unknown_type |
| text that isn't valid in its encoding | the 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.sqlForeign 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.