Create a new default schema in Oracle, customize it and push the data directly from PostgreSQL without intermediate export files by using oracle_fdw (foreign data wrapper).

Current state of this solution

Works as expected, but very slow because one of the biggest tables asset_feature have a CLOB column and OCI library doesn't allow PostgreSQL to prefetch the rows. For example, a pull of a 40 Million rows table takes 3 hours. There are different workarounds that are described in our internal PostgreSQL mailing list.

Prerequisites

  • installed, running and empty Oracle database.

  • access as user oracle to the Oracle DB host, as postgres to the PostgresSQL host, as corpus to the censhare Server host.

  • port 1521 on Oracle host should be open for PostgreSQL host

Create the default Oracle schema

We use the standard create-user.sql and create-db.sql scripts from the same censhare Server release whose schema we have to migrate

# get create-db.sql from /opt/corpus/css/database/ on the censhare server
# we have to separate tables from everything else
perl -ne 'print if /^CREATE TABLE/../^\);/ and /^CREATE TABLE/../^ROWDEPENDENCIES;/ ' 
create-db.sql 
> create-db-TABLES.sql
perl -ne 'print if not ( /^CREATE TABLE/../^\);/ and /^CREATE TABLE/../^ROWDEPENDENCIES;/ ) ' create-db.sql > create-db-REST.sql

-- create user and tables 
sqlplus / as sysdba
drop user corpus cascade;
@create-user.sql
@create-db-TABLES.sql

-- create helper tables for sequences and custom columns
create table seqtmp(sequence_name varchar(100), last_value number(19), start_value number(19), increment_by number(19));

create table columnstmp (
table_name varchar2(100), 
column_name varchar2(100), 
data_type varchar2(100), 
is_nullable varchar2(100),
character_maximum_length number(10),
numeric_precision number(10)
);

Stop censhare process and prepare the PostgreSQL database

We create a new temporary table to hold sequence values.

# stop censhare process at the censhare Server host.

# create a temporary table to transfer sequence values to Oracle
psql -d corpus -U corpus
create table public.seqtmp(sequence_name varchar(100), last_value bigint, start_value bigint, increment_by bigint);

DO $$
DECLARE 
c record;
BEGIN
FOR c IN SELECT * FROM information_schema.sequences where sequence_schema='public' 
and sequence_name not like '%rowid%' 
LOOP
   EXECUTE 'insert into public.seqtmp select '''||c.sequence_name||''', last_value , start_value, increment_by FROM '||c.sequence_name;
END LOOP;
END$$;

commit;

# Help function to help us with customized database schema
CREATE OR REPLACE FUNCTION get_columns(varchar, varchar) RETURNS varchar AS $$
 select string_agg('"'||column_name||'"',',') FROM information_schema.columns  
 WHERE table_name = $1  and table_schema= $2 AND  column_name NOT IN('rowid');
$$ LANGUAGE SQL;

Set up and test Oracle Instant Client

Follow the instructions in the corresponding section in Migration Oracle → PostgreSQL.

Set up oracle_fdw and connect PostgreSQL to Oracle

Follow the instructions in the corresponding section in Migration Oracle → PostgreSQL.

Check PostgreSQL for custom TABLES and create them in Oracle

We compare the custom PostgreSQL schema with default Oracle schema. You can exclude other tables in the list:

psql -d corpus -U corpus
SELECT table_name FROM information_schema.tables WHERE table_schema='public' and table_name 
not in ('pg_stat_statements') 
EXCEPT
SELECT table_name FROM information_schema.tables WHERE table_schema='corpus';

.. create them manually in Oracle. Then check again.

Collect custom COLUMNS in PostgreSQL

Execute the following commands:

psql -d corpus -U corpus
drop table public.columnstmp;
create table public.columnstmp as (
SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' 
and column_name <> 'rowid' and table_name not in ('pg_stat_statements')
EXCEPT
SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='corpus' 
order by 1,2
);

# Get also the data types for these columns
alter table public.columnstmp add data_type varchar;
alter table public.columnstmp add is_nullable varchar;
alter table public.columnstmp add character_maximum_length integer;
alter table public.columnstmp add numeric_precision integer;

UPDATE public.columnstmp SET 
data_type = c.data_type, 
is_nullable=c.is_nullable,
character_maximum_length=c.character_maximum_length,
numeric_precision=c.numeric_precision
FROM information_schema.columns c
WHERE c.table_name = columnstmp.table_name and c.column_name = columnstmp.column_name;

# transfer the custom collumns to Oracle
insert into corpus.columnstmp select * from public.columnstmp;

Create custom COLUMNS in Oracle

Execute the following commands: 

# create the missing columns in Oracle

sqlplus corpus/corpus
-- set serveroutput on size 30000;
BEGIN
FOR lst IN (select * from columnstmp order by 1)
LOOP
execute immediate 'alter table '|| lst.table_name ||' ADD ('|| 
lst.column_name||' '|| 
CASE lst.data_type 
WHEN 'smallint' THEN 'NUMBER(6)' 
WHEN 'numeric' THEN 'NUMBER('||COALESCE(lst.numeric_precision,38)||')' 
WHEN 'character varying' THEN 'varchar2('||COALESCE(lst.character_maximum_length,4000)||')' 
WHEN 'timestamp without time zone' THEN 'TIMESTAMP(6)' 
END ||' '||
CASE lst.is_nullable 
WHEN 'YES' THEN 'NULL' 
WHEN 'NO' THEN 'NOT NULL' 
END ||')';
END LOOP;
END;
/


# recreate and import the schema again
psql -d corpus -U corpus
drop schema corpus cascade;
CREATE SCHEMA corpus;
IMPORT FOREIGN SCHEMA "CORPUS" FROM SERVER oradb INTO CORPUS;


# check again for differences
SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' 
and column_name <> 'rowid' and table_name not in ('pg_stat_statements')
EXCEPT
SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='corpus' 
order by 1,2;

Compare the Oracle database schemas (optional)

Do this only if this database was running on Oracle before the migration to PostgreSQL and if you still have access to this database.

# get the BEFORE migration to Postgres state by executing this the OLD oracle DB
sqlplus corpus/corpus
set lines 200 pages 3000
col table_name format a30
col column_name format a30
col nullable format a1
col data_type format a15
spool BEFORE-db.txt
select table_name,column_name, nullable, data_type,data_length,data_precision from user_tab_cols order by 1,2;
spool off

# get the AFTER migration to Postgres state by executing this the NEW oracle DB
.. do the same, but change this line
spool AFTER-db.txt



# copy to some linux VM, make a fullscreen and compare
diff -y -W 230 AFTER-db.txt BEFORE-db.txt | less -S

Push the data from PostgreSQL to Oracle

Execute the following commands: 

-- set the prefetch size 
DO $$
DECLARE 
c record;
BEGIN
FOR c IN SELECT table_name FROM information_schema.tables WHERE table_schema='public' and 
table_name not in ('pg_stat_statements') 
LOOP
EXECUTE 'alter foreign table corpus.' || c.table_name || ' OPTIONS (prefetch ''10240'');';
END LOOP;
END$$;

-- create a help table
create table public.columns_list as 
select table_name, get_columns(table_name,'public') FROM information_schema.tables WHERE table_schema='public';

DO $$
DECLARE 
c record;
v timestamp;
BEGIN
FOR c IN SELECT * FROM columns_list where table_name not in ('pg_stat_statements') order by table_name desc
LOOP
select timeofday() into v;
RAISE NOTICE '% BEGIN processing table %', v, c.table_name;
EXECUTE 'insert into corpus.'|| c.table_name ||' (' || c.get_columns || ') select '|| 
c.get_columns || ' FROM public.'|| c.table_name;
END LOOP;
END$$;

-- collect statistics on Oracle
exec dbms_stats.gather_schema_stats('CORPUS',no_invalidate=>false,cascade=>true,degree=>4);

--- create indexes, constraints etc.
sqlplus corpus/corpus
@create-db-REST.sql