DEV Community

Cover image for Oracle to PostgreSQL Migration: The Residue Report I'd Price From
Qasim Parray
Qasim Parray

Posted on Originally published at abrarqasim.com

Oracle to PostgreSQL Migration: The Residue Report I'd Price From

Short version for the impatient: before you pick an Oracle to PostgreSQL migration tool, ask it for the list of things it refuses to convert. That list is your quote. The converted DDL is the cheap part.

I read the pgrecon announcement on the PostgreSQL news page twice this week. The first read was for the feature list. The second was because the benchmark table made me suspicious, and I wanted to work out whether the suspicion was fair. I haven't run pgrecon against a real Oracle estate, so treat everything below as a reading of what the project claims plus my own habits from client migrations, not a test report.

If you quote fixed-price database moves for clients, or you're the person who gets handed one, this is the part of the problem I'd look at first.

Why the schema script is the cheap part

Every Oracle migration pitch starts with a demo where tables go in one side and come out the other. Tables are easy. A table is columns, types and constraints, and the mapping is mostly mechanical. The money disappears somewhere else: PL/SQL packages that keep state between calls, triggers that write audit rows in their own transaction, jobs scheduled inside the database, and application code that quietly depends on how Oracle treats empty strings.

That last one deserves a paragraph because it bites people who think they've finished. Oracle treats an empty string as NULL. PostgreSQL does not. A column that has always been "optional" in practice, with the application sending an empty string, behaves one way on Oracle and another on PostgreSQL. Your WHERE col IS NULL filters, your NOT NULL constraints and your unique indexes all change meaning without a single syntax error. No DDL converter can fix that. Only an assessment that goes looking for it can.

So the useful question for any tool is not "how much did it convert?" It's "how clearly did it tell me what it left behind?"

What pgrecon says it does

The project describes three stages. Your DBA runs one read-only SQL*Plus script and sends back the spool files. The tool loads them into a local SQLite inventory and never connects to either database. Then a rule engine of 80 rules runs over the inventory, and a converter emits PostgreSQL DDL for what it can prove equivalent.

Two design choices stood out to me.

The first is the offline dump. I like this more than I expected to. A lot of Oracle estates belong to companies where nobody will hand a contractor a connection string in week one. A short, auditable script that a DBA reads line by line and runs with a read-only account is a much easier yes. The project page says the script is short enough to audit, and that's the right claim to make, because it's the thing a security reviewer will actually check.

The second is the residue report. The page says everything the converter can't carry faithfully becomes a named line instead of emitted DDL. Their examples are the sort of thing I'd want to see: a view using JSON_TABLE that needs a hand rewrite because PostgreSQL 17 added it with different clause syntax, a trigger that runs an autonomous transaction, a compound trigger, an external file locator column with no counterpart.

Read those four lines again. Each one is a different kind of work. The view is an afternoon. The autonomous transaction is a design decision, because you're choosing between dblink, a queue, or changing what the audit table means. The compound trigger is a restructure. The file locator is a conversation with whoever owns the files. A flat list with reasons attached is how you turn "it's about six months" into a number you can defend.

What the conversions actually look like

Here's the flavour of what a converter has to get right. Take the classic Oracle top-N pattern:

-- Oracle
SELECT * FROM (
  SELECT employee_id, last_name
  FROM employees
  ORDER BY salary DESC
)
WHERE ROWNUM <= 10;
Enter fullscreen mode Exit fullscreen mode
-- PostgreSQL
SELECT employee_id, last_name
FROM employees
ORDER BY salary DESC
LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

The announcement says the converter now turns ROWNUM top-N bounds into LIMIT. Fine, but notice what the tool has to prove. If somebody wrote the lazy version, WHERE ROWNUM <= 10 ORDER BY salary DESC with no subquery, Oracle applies the row limit before the sort and returns ten arbitrary rows. A mechanical conversion to LIMIT would silently start returning the real top ten. That's arguably the behaviour the author wanted, and it's still a change that someone should sign off on. I'd want to know whether the tool flags that shape or just rewrites it.

Hierarchies are the other one I always check, and the announcement says CONNECT BY becomes WITH RECURSIVE:

-- Oracle
SELECT employee_id, manager_id, LEVEL
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;
Enter fullscreen mode Exit fullscreen mode
-- PostgreSQL
WITH RECURSIVE tree AS (
  SELECT employee_id, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL
  UNION ALL
  SELECT e.employee_id, e.manager_id, t.level + 1
  FROM employees e
  JOIN tree t ON e.manager_id = t.employee_id
)
SELECT employee_id, manager_id, level FROM tree;
Enter fullscreen mode Exit fullscreen mode

That one is mechanical for simple cases. It gets uglier with CONNECT_BY_ISCYCLE, SYS_CONNECT_BY_PATH and ORDER SIBLINGS BY, none of which the announcement mentions. Absence from a press release isn't evidence of absence from the tool. I'd test exactly those on a sample schema before believing any summary.

There's also a quieter item in the announcement: both extraction tiers capture character versus byte length semantics, so VARCHAR2(30 CHAR) maps to varchar(30). This matters because PostgreSQL's varchar(n) counts characters. A VARCHAR2(30 BYTE) column on a UTF-8 database holds fewer characters than the same number in PostgreSQL. Moving to the looser limit is usually harmless. Moving data the other direction later, if you keep Oracle as a fallback during cutover, is where it hurts.

Reading a benchmark the vendor ran

Now the part that made me suspicious. The project page has a table of apply errors against a live PostgreSQL 16 after converting nine schemas. pgrecon shows zero. The other tools show 40, 58, 77, 135 and 481, depending on the tool, and the page adds notes such as "PL/SQL out of scope by design" and "tables, indexes and sequences only".

I'm not saying the numbers are wrong. The page says the benchmark workflow lives in the repository and is re-run in CI, which is exactly how this should be published, and I'd rather see a reproducible vendor benchmark than none at all. But I'd hold three reservations.

One, the author picked the schemas. Oracle's HR, OE and CO samples plus a handful of open-source PL/SQL libraries are a sensible public set, and they're also a set the author could tune against for months. Your estate isn't in it.

Two, "apply errors" counts statements PostgreSQL rejected. It doesn't count statements PostgreSQL accepted that now behave differently. A tool that declines hard cases gets a perfect score on this metric by design, and the page is open about that. Declining is the honest behaviour, and it still isn't the same thing as converting.

Three, the comparison tools are measured at specific versions on specific dates, and some of the notes describe scope choices, not defects. The CYBERTEC and Dalibo notes say what they cover. Reading "40 errors" next to "0 errors" without those notes would be unfair to both.

The version of this that I do trust is the project's own statement of what it declines. A tool that publishes its residue is easier to audit than one that publishes a score.

How I'd run an assessment this week

If I had an Oracle estate on my desk tomorrow, this is the order I'd go in. It's the order that spends the least client money before the first real decision.

First, get the DBA to run the extraction script on a non-production copy with a read-only account, and read the script yourself before they do. If the client's security team balks at that, you've learned something about the schedule.

Second, install the tool with pip install pgrecon and run the report before the converter. Sort findings by severity and read the evidence lines. Don't skim the summary. A line-level hit on package-level state or on database links is worth more than a hundred green rows.

Third, take the residue report and price each line separately, in days, with a name next to it. If a line says "redesign with a queue", that's a design meeting and a test plan, not an hour.

Fourth, check the empty-string behaviour by hand. Grep the application code for comparisons against '' and for string concatenation over nullable columns. Oracle's || treats NULL as an empty string, and PostgreSQL's returns NULL, so first_name || ' ' || middle_name quietly starts blanking whole names. A schema dump can't show you that.

Fifth, apply the converted DDL to a scratch PostgreSQL instance yourself. Don't take the apply-error count from anyone's table, including mine. If you want the data-movement side of a database move, I wrote about a MySQL to PostgreSQL one in the row that kept my write freeze.

If you take on this kind of work, my portfolio lists the database and backend projects I do for clients. For partitioned tables in particular, read the PostgreSQL partitioning chapter before you accept any automatic mapping of Oracle interval partitions, because PostgreSQL's declarative partitioning needs partitions to exist up front.

This week, do one thing: ask whichever tool you're considering to produce its refusal list for a schema you own, and put a day estimate beside every line. If it can't produce the list, you have your answer about the quote.


Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.

Top comments (0)