Writing
August 5, 2026 · Migration · 5 min read

I Built a One-Click Oracle to PostgreSQL Migration POC

By Lukman Balunywa, Principal Cloud & AI Solutions Engineer, Microsoft Azure

Most Oracle-to-Postgres conversations stall before anyone converts a single object. So I built the environment as code: one Deploy to Azure button, a sample Oracle source, a Postgres target, and AI-assisted schema conversion.


Every Oracle to PostgreSQL conversation I have follows the same arc.

The customer is interested. The economics are obvious. Then someone asks the only question that matters: how much of our schema actually converts, and what breaks?

Nobody answers that in a meeting. Answering it requires an environment — an Oracle source, a Postgres target, a workstation with the right tooling, network paths between them, and a model endpoint for the AI-assisted conversion. That setup is usually where the momentum dies. Two weeks of tickets for VMs, firewall exceptions, and quota, and by then the sponsor has moved on.

So I stopped hand-building it and turned the environment into code.

Repo: github.com/Balunywa/oracle-to-postgres-poc

One Deploy to Azure button stands up the whole thing. No CLI required.

What it deploys#

Four components, each independently selectable in the deployment form:

  • A Windows migration workstation running desktop Visual Studio Code with the Microsoft PostgreSQL extension — the tool that actually performs the conversion.
  • An Oracle source database (Oracle Database Free 23ai in a container) pre-seeded with the sample HR schema, so you can prove the tooling end to end with zero external dependencies.
  • An Azure Database for PostgreSQL flexible server as the conversion target.
  • An Azure OpenAI (Microsoft Foundry) model deployment that powers the AI-assisted conversion.

Everything sits inside a virtual network. Access is through Azure Bastion only — direct inbound RDP is blocked. Shared networking is created automatically when a selected component needs it.

                        Azure Bastion
                             |
                             v
                    +--------------------+
                    | migration          |
                    | workstation        |
                    | VS Code + pg ext   |
                    +--------------------+
                     /         |        \
                    v          v         v
        +-----------+   +------------+   +--------------------+
        | Oracle    |   | Microsoft  |   | Azure Database for |
        | source VM |   | Foundry    |   | PostgreSQL target  |
        | HR schema |   | model      |   | + scratch DB       |
        +-----------+   +------------+   +--------------------+

        read metadata     convert DDL      validate + deploy

No custom conversion logic lives in the repo. The PostgreSQL extension does the work. The repo's job is to remove the two weeks of environment yak-shaving in front of it.

The design decision that matters: a built-in source#

The obvious version of this tool points at the customer's Oracle database on day one. That is a mistake.

If the first run hits a real Oracle instance, every failure is ambiguous. Is it network? Credentials? Quota? Extension version? Model deployment? You end up debugging the harness instead of learning anything about the schema.

So the POC ships with its own Oracle source and a known schema. The first run is a control: it proves the workstation, the Bastion path, the Postgres target, and your Azure OpenAI quota all work against a schema whose correct output is already known. Only after that do you point the wizard at a real dev/test Oracle.

That sequencing is the whole trick. It converts "the migration tool is broken" into "the migration tool works, and this specific object in your schema does not convert cleanly" — which is the conversation you actually wanted.

Scope: schema, not data#

Worth being blunt about this, because it is the most common misread.

This POC converts and deploys the schema — tables, views, constraints, types, and other DDL. It does not copy rows. The conversion only reads the Oracle source. Your Postgres tables land empty until you load them with a separate tool: Azure Database Migration Service, ora2pg, or pgloader.

That is deliberate. Schema conversion is where the risk lives — packages, custom types, PL/SQL, sequence and identity semantics, date handling. Data movement is a bandwidth and cutover problem with well-understood tooling. Conflating the two is how POCs turn into six-month projects.

The failure modes I hardened against#

Most of the README is not architecture. It is the specific things that broke on real subscriptions, documented, because these are the errors that make a customer conclude "Azure is hard" when the actual problem is a subscription flag.

Unregistered resource providers. On a brand-new subscription, Microsoft.Compute, Microsoft.Network, Microsoft.DBforPostgreSQL, or Microsoft.CognitiveServices may not be registered. The portal auto-registers them during validation; the CLI does not. From the CLI you get MissingSubscriptionRegistration and no useful hint about which one.

The misleading public IP feature flag. Some subscriptions gate all Standard public IP creation behind Microsoft.Network/AllowBringYourOwnPublicIpAddress. The template does not use BYOIP at all — it creates two ordinary Azure-allocated IPs, one for Bastion and one for the workstation. But the error names a feature you are not using, and unlike resource providers, the portal will not register it for you. You register the feature, poll until it reports Registered, re-register the provider so the change propagates, then redeploy.

Governance policies stopping your compute. Plenty of subscriptions run auto-shutdown policies. You come back the next morning, the workstation cannot reach anything, and it looks like a networking bug. It is a stopped VM. Confirm the Postgres flexible server, the workstation, and the Oracle source VM are all running before debugging anything else.

Extensions that install after first logon. The provisioning log writes PROVISION_COMPLETE when the deploy-time steps finish — which is before the VS Code extensions finish installing at first interactive logon. Trusting that line means you open VS Code, find no PostgreSQL extension, and assume the deployment failed.

None of that is glamorous. All of it is the difference between a POC that lands and a POC that gets abandoned in the first hour.

Why I keep building these#

I do a lot of architecture reviews. The pattern I keep hitting: the technical answer is knowable in an afternoon, but nobody can get to the afternoon because the environment is a procurement exercise.

Packaging the environment as an ARM template with a portal form changes the unit of work. Instead of "let's schedule a POC," it becomes "click this, run it against the sample schema today, then point it at your own database tomorrow." The decision gets made on evidence from the customer's own schema instead of on a vendor's compatibility matrix.

That is the entire argument for infrastructure-as-code in a pre-sales context. Not elegance. Time to first real signal.

Try it: github.com/Balunywa/oracle-to-postgres-poc — the README has the full deployment walkthrough with screenshots, plus a teardown script so you are not paying for a Bastion you forgot about.


← Writing

© 2026 Lukman Balunywa · Opinions are my own.