This article explores the possibilities opened by the schema conversion workflow. It is an early evaluation: I used HorizonDB as a target, which is in preview and not a supported target yet, and Oracle Database 26ai as a source which has not been validated yet. Early tests and feedbacks are welcome. If you encounter any issue, please open an issue (https://github.com/microsoft/vscode-pgsql/issues) or a discussion (https://github.com/Azure-Samples/postgres-hub/discussions)
The goal is to evaluate how AI assistance changes the first phases of a migration: assessing the effort, creating a first working prototype, and making the remaining technical gaps visible. This is where an integrated development environment is particularly effective. Source artifacts, generated PostgreSQL code, reports, database connections, tests, findings, and repairs can remain in one reproducible VS Code workspace.
Migrating a database is not finished when the tables compile. A useful test must also answer three harder questions:
- Was the reference data copied correctly?
- Were the application-facing stored routines actually deployed?
- Do those routines produce the same business effects under a realistic load?
This is particularly important when business logic is embedded in proprietary database constructs such as PL/SQL packages, which are in scope for this migration. Swingbench Order Entry is a good example. It has two versions: one uses pure JDBC, with the business logic in Java, and the other places the logic in a PL/SQL package that uses advanced Oracle Database features. The server-side version illustrates the migration challenges found in many critical legacy applications.
In this article, I walk through those questions using Swingbench's Sales Order Entry (SOE) schema. I ran Oracle Database and Swingbench in Docker containers and used the PostgreSQL extension for Visual Studio Code to convert the schema against an Azure HorizonDB (preview) scratch database. I then used GitHub Copilot to build the data-copy, workload-emulation, and validation programs needed to test PostgreSQL.
This article follows the migration end to end. Readers can use individual sections independently, but I keep the complete sequence because each stage exposed issues that schema conversion alone could not reveal.
Throughout this project, I made the architectural decisions, directed the investigation, and validated results. The migration extension performed the model-assisted schema and PL/SQL conversion, while GitHub Copilot generated supporting code and tests under my direction. PostgreSQL compilation and the executable tests provided the evidence. Instead of replacing deterministic extraction, dependency analysis, and type mappings, AI participated in the iterations that traditionally follow them: contextual translation, review, diagnosis, repair, and validation. GitHub Copilot also produced the first draft of this article, which I reviewed and refined so it accurately describes what I ran, observed, and concluded.
What was migrated
Swingbench SOE is a compact but realistic migration target. It includes:
- customers, addresses, cards, products, inventory, orders, and order items
- primary keys, foreign keys, checks, indexes, and sequences
- views and analytical queries
- the Oracle
SOE.ORDERENTRYPL/SQL package used by Swingbench, with all the application logic, leaving only the presentation layer to the frontend - package state, collection types, helper routines, random choices, sleeps, transaction control, and business transactions
The test environment was:
| Component | Version or image |
|---|---|
| Oracle Database 26ai | gvenzl/oracle-free:slim (23.26.2) |
| Swingbench | domgiles/swingbench:latest (2.6.1118) |
| PostgreSQL | Azure HorizonDB (PostgreSQL 17.9) |
| PostgreSQL extension for Visual Studio Code | 1.27.3 |
| Foundry model | gpt-5.2 |
1. Build the Oracle source environment
I used the following docker-compose.yaml to start Oracle and keep a Swingbench container available for schema creation and testing:
services:
oracle:
image: gvenzl/oracle-free:slim
environment:
ORACLE_PASSWORD: "<oracle-system-password>"
ports:
- "1521:1521"
healthcheck:
test: ["CMD", "healthcheck.sh"]
interval: 10s
timeout: 5s
retries: 30
swingbench:
image: domgiles/swingbench:latest
platform: linux/amd64
depends_on:
oracle:
condition: service_healthy
entrypoint: ["sleep", "infinity"]
I started both containers with:
docker compose up -d
Once Oracle was healthy, I created a small SOE database with Swingbench's command-line wizard. I chose scale 0.1: large enough to exercise the relationships while remaining convenient for repeated migrations.
docker compose exec swingbench oewizard -cl -create \
-cs //oracle:1521/FREEPDB1 \
-dba "sys as sysdba" -dbap '<oracle-system-password>' \
-df /opt/oracle/oradata/FREE/FREEPDB1/soe01.dbf \
-u soe -p '"<soe-password>"' \
-scale 0.1 -tc 1 -v
Swingbench substitutes the SOE password into CREATE USER ... IDENTIFIED BY. A password containing characters that require Oracle's quoted-password syntax must be passed with the literal double quotes expected by that SQL statement.

Before migrating anything, I compiled the source package and checked for Oracle errors:
docker compose exec -T oracle \
sqlplus -s / as sysdba <<'SQL'
alter session set container=FREEPDB1;
alter package SOE.ORDERENTRY compile;
alter package SOE.ORDERENTRY compile body;
show errors package body SOE.ORDERENTRY
SQL
I then tested the environment to be migrated by running the original Swingbench application for one minute:
docker compose exec -it swingbench charbench \
-cs //oracle:1521/FREEPDB1 \
-dbau system -dbap '<oracle-system-password>' \
-u soe -p '<soe-password>' \
-rt 00:01:00 -c ../configs/SOE_Server_Side_V2.xml

2. Prepare VS Code, HorizonDB, and Foundry
I installed VS Code and the PostgreSQL extension for Visual Studio Code, authored by Microsoft. The migration workflow is exposed under Migrations (Preview) in the PostgreSQL view:

I opened Migrations (Preview) and created a project.
The first step was to name the migration project. I generated all screenshots while reviewing this article and replaying the walkthrough, so the project identifier for this run was walkthrough-validation:

The second step was to connect to the source Oracle Database as a user with the DBA role and select the schema:

The third step was to define the destination PostgreSQL database:

The migration tool executes generated DDL there, so this must not be an application or production database.
At the time of this test, the migration workflow expected an Azure PostgreSQL connection for compilation, either Flexible Server or HorizonDB. I used an Azure HorizonDB (preview) database as a dedicated scratch target. This was an exploratory use of the preview workflow, not a statement of official HorizonDB support. The generated code targets PostgreSQL rather than HorizonDB-specific APIs; portability to another PostgreSQL deployment still depends on its PostgreSQL version, available features, and managed-service restrictions.
I created the database service beforehand from VS Code:

The conversion also needed a Microsoft Foundry resource and a deployed model. I created the resource, opened Foundry, deployed gpt-5.2, and recorded the resource endpoint and deployment name.
I created the resource from the Azure portal:

I noted its endpoint and key:

From VS Code, I opened the Foundry portal:

I selected GPT-5.2:

I chose to use the selected model:

I deployed the model (I used the model name as the deployment name here):

The fourth configuration step was to select the model deployment using its name, endpoint, and API key:

The migration project was created: 
All files, configuration, logs, and reports were stored under .github/postgres-migrations.
3. Run the migration project
The migration workflow extracted Oracle metadata, created a dependency graph, divided the objects into dependency-aware chunks, converted each chunk, reviewed the generated SQL, compiled it against PostgreSQL, and assembled deployment artifacts.

The process resembles a compiler pipeline more than a text translator:
It started by extracting the metadata into artifacts/oracle/SOE/extract/ddl:

A report provides all details when completed:

It then started the conversion, one chunk at a time:

The conversion log shows the cycle of conversion, compilation, review, test, and validation using the LLM.
Here is an example showing the level of detail the log provides:
16:50:16 [INFO] ossdbtoolsservice.conversion_v2.pipeline.chunk_converter.package_converter: Package ORDERENTRY: member processOrders -> converted in 80.0s (tokens=5232, notes=Oracle ROWNUM < 10 translated to LIMIT 9 (no ORDER BY in source, so row choice remains arbitrary).; Oracle (+) outer joi)
This is a good example because it goes beyond syntax, where ROWNUM < 10 translates to LIMIT 9. It warns that a LIMIT without ORDER BY has a nondeterministic result that can differ between Oracle and PostgreSQL. Applications should not rely on physical row order, but a migration must still consider behavior validated by years of production use, even when that behavior originated as a side effect of an application bug.
The PostgreSQL DDL goes into artifacts/oracle/SOE/convert/sessions.
A report provides all details:

Simple objects can follow deterministic paths, while complex PL/SQL receives model-assisted conversion and review. PostgreSQL compilation, rather than the model, is intended to be the final syntax and dependency check. Once the migration is complete, it lists the review tasks. The counts and classifications shown here are observations from version 1.27.3 of an evolving preview workflow. Future versions may present fewer review tasks as their focus evolves in response to feedback, including a stronger emphasis on syntax and conversion completeness:

The summary also lists extensions that should be installed in the target database:

The orafce extension implements many functions familiar to Oracle users, including functions from Oracle DBMS packages. It can reduce the amount of code that must change during a migration. Extensions such as orafce and plpgsql_check can improve conversion and validation when they are available. For this experiment, however, I deliberately used a vanilla PostgreSQL target without them. I wanted to see what the workflow could produce without tying the converted application to extensions that may not be available from every managed PostgreSQL service. This makes portability a design goal of the prototype, not a claim that every PostgreSQL deployment is identical or that omitting extensions always produces the most accurate conversion.
Before proceeding to the manual review, I inspected the fresh conversion log to separate what the pipeline had repaired automatically from what it had only flagged for attention.
4. What the migration workflow corrected, and what it could not prove
I used GitHub Copilot to parse the conversion log and distinguish initial conversion, model review, compiler-driven repair, and warnings left for application review. The analysis found 37 automatic correction events: 33 during the second-pass review and four after PostgreSQL rejected generated SQL during compilation. The run finished with zero failed objects and zero fallback objects.
The second-pass reviewer corrected 20 of the 35 generated package artifacts. The interesting changes included:
- guarding
current_setting()results against missing or empty values before casting them - changing the DML counter helpers from transaction-local to session-level GUC writes so their state can survive a commit on the same connection
- replacing invalid Oracle
%TYPEreferences in PostgreSQL signatures and expressions - replacing invalid
PERFORM * FROMand Oracle cursor syntax - schema-qualifying table references and limiting a
SELECT INTOthat could otherwise return multiple rows - repairing an incomplete generated type-creation block
These were code changes, not only warnings. This is where model-assisted conversion goes beyond a fixed syntax rulebook: it reviews generated code in context and revises interactions between types, state, queries, and control flow. PostgreSQL compilation then found four remaining defects. The repair loop removed unsupported DEFERRABLE clauses from two check constraints and repaired two routines before recompiling them. This is an important distinction: model review can anticipate defects, while the target database provides the exact SQLSTATE and failing statement. The pipeline then fixes and retries, much like a developer working until the code compiles.
Chunking did not leave dependencies to the user. When a referenced table, index, or routine was produced by another chunk, the pipeline deferred the dependent operation and retried it after the required object existed. All such operations completed automatically in this run.
Structural translations
The conversion itself also made deliberate structural changes that were not compiler repairs:
| Oracle behavior | PostgreSQL translation |
|---|---|
| Package members | Schema routines named orderentry$<member> (PostgreSQL has no packages) |
| Package records and collections | Composite types, array domains, and set-returning functions |
| Package variables | Custom settings accessed through set_config() and current_setting() |
DBMS_RANDOM.VALUE | random() with range arithmetic (without the DBMS_RANDOM emulation provided by orafce) |
DBMS_LOCK.SLEEP | pg_sleep() |
DBMS_APPLICATION_INFO | application_name or custom settings |
BULK COLLECT | RETURNS TABLE, SETOF, or RETURN QUERY |
ROWNUM < n | LIMIT n - 1 or row_number() |
CONNECT BY row generation | Recursive CTE |
FORALL inventory updates | Row-by-row PL/pgSQL loop (PostgreSQL runs PL and SQL in the same engine) |
SYSDATE and SYSTIMESTAMP | CURRENT_TIMESTAMP, clock_timestamp(), and date_trunc() |
| Package procedures | Functions returning void |
| Package overloads | PostgreSQL overloads with the same flattened name |
What still requires attention
Successful compilation proves syntax and dependency consistency, not behavioral equivalence. The fresh review report classifies 34 of 78 objects as auto-approved and leaves 44 non-blocking behavioral divergences for application review. The most important are:
- Oracle
ENABLE NOVALIDATEprimary keys do not validate existing rows, while PostgreSQL primary-key creation does. Eight tables therefore require clean source data before deployment. - Five Oracle reverse-key indexes became normal PostgreSQL btree indexes, which can change insertion hot spots and access behavior.
- Package state is stored as text in custom GUCs. Although the reviewer changed the counter helpers to session-level writes, the emitted routines still use different key prefixes and a mixture of session-level and transaction-local settings. Initialization, pooling, commits, and casts need runtime tests.
- PostgreSQL functions cannot reproduce Oracle package-side
COMMIT. The caller must own transaction boundaries. ROWNUMconversions withoutORDER BYpreserve nondeterministic selection, but they do not guarantee that Oracle and PostgreSQL choose the same rows.BULK COLLECTcollections became row sets, andFORALLbecame a loop. Empty collection behavior, ordering, locking, and performance can differ.- Random-number boundaries, numeric casts, exception behavior, and
SYSDATE/SYSTIMESTAMPtiming and timezone semantics remain database-specific.
None of these 44 findings blocked deployment in this run. They define the work for the runtime validation in the following sections rather than automatic fixes that the migration report can prove.
5. Examples from the generated ORDERENTRY package
The package conversion shows why this is more than syntax replacement. The pipeline emitted all 34 executable package-body members, including private helpers, and represented Oracle procedures as PostgreSQL functions returning void. Because PostgreSQL has no equivalent of package visibility, every member became a schema routine named orderentry$<member>. Applications should keep calling only the former public API, with privileges used to preserve the old visibility boundary.
The harder transformations preserved the shape of the business logic without pretending that Oracle and PostgreSQL use the same programming model. For example, BULK COLLECT collections became set-returning functions. Indexed collection access became operations over those row sets. The FORALL inventory update in orderentry$neworder became a PL/pgSQL loop, and the CONNECT BY row generator used for warehouse activity became a recursive CTE. Package helpers using DBMS_RANDOM, DBMS_LOCK.SLEEP, and DBMS_APPLICATION_INFO became calls to random(), pg_sleep(), and PostgreSQL settings.
A small conversion with a large consequence
I verified that from_mills_to_secs was already a private Oracle function rather than a helper invented during migration:
function from_mills_to_secs(value integer) return float is
real_value float := 0;
begin
real_value := value/1000;
return real_value;
exception
when zero_divide then
real_value := 0;
return real_value;
end from_mills_to_secs;
Despite the shortened name, “mills” means milliseconds here. The ZERO_DIVIDE branch is defensive but cannot normally be reached because the divisor is the constant 1000.
I compared it with the generated PostgreSQL function, which keeps the operation but explicitly converts to floating point to avoid integer division:
CREATE OR REPLACE FUNCTION soe.orderentry$from_mills_to_secs(value integer)
RETURNS double precision
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
real_value double precision := 0;
BEGIN
real_value := value::double precision / 1000.0;
RETURN real_value;
EXCEPTION
WHEN division_by_zero THEN
real_value := 0;
RETURN real_value;
END;
$$;
orderentry$sleep calls this helper for the fixed or randomly selected delay, maps DBMS_LOCK.SLEEP to pg_sleep, and updates a package counter. Here the review found a subtle semantic issue: the generated helper calculates elapsed milliseconds but adds the selected delay in seconds to the counter. The supplied workload uses zero delays, so it did not affect this run. With nonzero delays, the code could compile and execute while returning the wrong value. This is the kind of small conversion detail that is easy to miss and appears much later as an application error.
Transaction control changes ownership
The largest architectural difference is transaction control. In Oracle Database, oecommit can issue COMMIT from inside the package when the session-level PLSQLCOMMIT flag is true. The package therefore decides when its changes become durable and increments its own commit counter at that point.
The converter represented the package procedures as PostgreSQL functions to preserve their callable API, but a PostgreSQL function cannot commit or roll back its surrounding transaction. The generated oecommit can preserve the instrumentation, not the transaction boundary. PostgreSQL procedures invoked with CALL can perform transaction control in specific contexts, but changing these package entry points into procedures would also change how the application calls them and how return values are handled.
For this migration, I chose to disable package-side commits. The Python driver calls orderentry$setplsqlcommit('false'), commits each successful mutating call, and rolls back a failed call. Transaction ownership therefore moved from the stored package to the application. That is an intentional adaptation, not an equivalent syntax translation. Multi-call atomicity, retries, error handling, and connection-pool behavior must be reviewed with that new boundary in mind.
6. Copy the reference data separately
Without leaving VS Code, I first checked the migrated schema, including its tables, views, and converted ORDERENTRY routines:

The migration extension converted the schema and code but did not copy the Swingbench data. For this small, specific task, it was faster to guide GitHub Copilot to generate a Python program than to switch to another migration tool. The script stayed in the repository with the connection setup and commands, making the copy reproducible and documented rather than a one-time manual operation.
I installed its dependencies (Python 3.10 or later required) and exposed connection information through environment variables rather than embedding credentials:
python3 -m pip install -r scripts-postgres/requirements.txt
export ORACLE_DSN='localhost:1521/FREEPDB1'
export ORACLE_USER='soe'
export ORACLE_PASSWORD='<soe-password>'
export PG_DSN='host=<host> port=5432 dbname=<db> user=<user> sslmode=require'
export PG_SCHEMA='_mig_scratch_soe'
The PG_SCHEMA variable names the PostgreSQL schema created by the migration extension during validation. The scripts set search_path to this schema automatically before executing any SQL.
I ran a clean copy with:
bash scripts-postgres/copy_soe_data.sh --truncate

The program uses dependency order and PostgreSQL binary COPY. It also handles Oracle intervals and numeric values, preserves the intent of ENABLE NOVALIDATE checks, and advances the PostgreSQL sequences after loading.
I then verified that the Oracle and PostgreSQL row counts matched:

The complete implementation is available in copy_soe_data.py.
7. Recover and reproduce the Swingbench workload
Swingbench is a free load generator, but it is not open source. It is distributed as compiled Java, so porting its client to PostgreSQL was neither practical nor the goal. For this server-side workload, the business logic is in the SOE.ORDERENTRY package. The Java client primarily chooses transactions, generates parameters, and calls the package.
Calling the migrated routines with arbitrary values would test invocation, but not the real workload. I guided GitHub Copilot to inspect the files in the Swingbench container. The XML configuration provided the enabled transactions, weights, user count, delays, and timeout. I used javap to confirm the externally observable call signatures and runtime behavior needed to build a compatible validation workload. GitHub Copilot documented those findings under my direction in scripts-postgres/swingbench_calls.md.
I then guided GitHub Copilot to create run_swingbench_workload.py. It follows the same weighted transaction selection with four persistent user connections, a start barrier, copied customer data, and the configured timeout. It reports throughput, latency, and errors without trying to reproduce the Swingbench GUI or Oracle-specific connection pool.
As described in Section 5, transaction ownership had to change. The driver disables package-side commits, commits successful mutating calls through Psycopg, and rolls back failed calls. I ran it with:
export PG_DSN='host=<host> port=5432 dbname=<db> user=<user> sslmode=require'
export PG_SCHEMA='_mig_scratch_soe'
bash scripts-postgres/run_swingbench_workload.sh --duration 300
Before creating load, the program verifies that the public workload API is complete. It exits early rather than report misleading performance results for an incomplete deployment.
8. Validate in layers
Validation is part of the migration, not a final smoke test. Compilation proves that PostgreSQL accepts the generated objects, but not that they return the right results or preserve business effects. I directed GitHub Copilot to build four validation layers so each failure could be isolated before adding more workload complexity.
Layer 1: catalog and query validation
The rollback-safe test harness begins with a preflight that checks the PostgreSQL version, required tables, five sequences, and all public workload routines. It also submits the two analytical queries to PostgreSQL with EXPLAIN before proceeding to routine calls. I separately ran those two analytical queries with EXPLAIN ANALYZE against 106,583 customers, 160,684 orders, and 482,752 order items. Both executed successfully.
The top-customer query completed in 424 ms. Its sequential scans were reasonable because it consumed almost all orders and items, although the aggregate spilled to temporary disk:
Sort (actual time=423.274..423.277 rows=20 loops=1)
Sort Key: (rank() OVER (?))
Sort Method: quicksort Memory: 26kB
Buffers: shared hit=11220, temp read=1792 written=1953
-> WindowAgg (actual time=423.242..423.255 rows=20 loops=1)
Run Condition: (rank() OVER (?) <= 20)
-> Sort (actual time=423.236..423.240 rows=21 loops=1)
Sort Key: sum(oi.quantity * oi.unit_price) DESC
Sort Method: quicksort Memory: 1929kB
-> HashAggregate (actual time=402.740..415.533 rows=24968 loops=1)
Group Key: c.customer_id
Batches: 9 Memory Usage: 8273kB Disk Usage: 1480kB
-> Hash Join (actual time=60.363..279.582 rows=482718 loops=1)
Hash Cond: (o.customer_id = c.customer_id)
-> Hash Join (actual time=34.192..176.799 rows=482752 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_items oi (rows=482752 loops=1)
-> Hash
-> Seq Scan on orders o (rows=160684 loops=1)
-> Hash
-> Seq Scan on customers c (rows=106583 loops=1)
Planning Time: 0.742 ms
Execution Time: 423.895 ms
VS Code with the PostgreSQL extension can visualize the plan, including buffers read by each node and the flow of rows between nodes: 
The monthly revenue query completed in 627 ms. Its aggregate spilled into 33 batches before two in-memory window sorts, making work_mem the first tuning candidate rather than an index:
WindowAgg (actual time=615.702..626.031 rows=27472 loops=1)
Buffers: shared hit=11211, temp read=1965 written=2208
-> Sort (actual time=615.698..617.253 rows=27472 loops=1)
Sort Key: customer_sales.customer_id, customer_sales.month_start
Sort Method: quicksort Memory: 2352kB
-> WindowAgg (actual time=600.739..610.404 rows=27472 loops=1)
-> Sort (actual time=600.708..602.149 rows=27472 loops=1)
Sort Key: customer_sales.month_start, customer_sales.revenue
Sort Method: quicksort Memory: 2137kB
-> Subquery Scan on customer_sales
(actual time=570.611..587.861 rows=27472 loops=1)
-> HashAggregate
(actual time=570.610..586.090 rows=27472 loops=1)
Group Key: c.customer_id,
date_trunc('month', o.order_date)
Batches: 33 Memory Usage: 8209kB Disk Usage: 2040kB
-> Hash Join
(actual time=54.823..392.870 rows=482718 loops=1)
Hash Cond: (o.customer_id = c.customer_id)
-> Hash Join
(actual time=31.672..208.840 rows=482752 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_items oi (rows=482752)
-> Hash
-> Seq Scan on orders o (rows=160684)
-> Hash
-> Seq Scan on customers c (rows=106583)
Planning Time: 0.266 ms
Execution Time: 627.065 ms
These execution plans establish a PostgreSQL baseline and identify possible tuning work, such as aggregate spills and work_mem pressure. I kept those observations for the physical-design review after correctness and concurrent workload validation rather than tuning from isolated queries too early.
Layer 2: call every public workload routine
After the preflight, the same harness discovers viable values and calls all nine workload transactions plus setPLSQLCOMMIT:
bash scripts-postgres/run_orderentry_tests.sh
Each call is isolated and reports its SQLSTATE on failure. The first run exposed defects that compilation could not: composite return types did not match some rows, internal and public customer-ID types differed, package settings lacked defaults, and newOrder retained an Oracle %TYPE cast. GitHub Copilot generated schema-relative repairs, which I reviewed and applied. The implementation is available in apply_orderentry_runtime_fixes.py:
bash scripts-postgres/apply_orderentry_runtime_fixes.sh
I then ran the harness again, and all ten checks passed.
Layer 3: verify business effects
A successful call does not prove the correct business effect. GitHub Copilot generated a rollback-safe order workflow:
bash scripts-postgres/run_swingbench_workload.sh --verify-only
It creates two orders for a copied customer, verifies their fields and item rows, then checks that browseandupdateorders changes one item quantity and the matching order total together. It rolls back, reconnects, and proves that both orders are absent. Only sequence values may advance because PostgreSQL sequences are nontransactional.
Layer 4: weighted concurrent load
Only after the first three layers passed did I run concurrent load. Four users executed 287 calls across all nine transaction types with zero errors. This ordering separated migration defects from performance observations and avoided assuming that copied customer IDs were contiguous.
After validation: review physical design
After validating the migration and ensuring data accuracy, it is time to look at the physical layer, using the execution plans captured in Layer 1 together with observations from the concurrent workload. Oracle and PostgreSQL handle data patterns differently, so blindly copying Oracle optimizations can degrade performance. PostgreSQL's MVCC architecture adds write overhead to updates, so the table FILLFACTOR strategy needs a fresh look compared with Oracle's PCTFREE. PostgreSQL also provides different tuning options, including partial indexes, specialized index types, and incremental sorting.
Reproduce the complete validation
At the end of my run, the test setup contained:
- all ten workload tables populated
- all five required sequences deployed and advanced
- two analytical queries accepted by PostgreSQL under
EXPLAIN - a complete invocation harness
- a weighted Swingbench-like concurrent driver
- a rollback-safe semantic order test
After deploying the converted schema and its ORDERENTRY routines, I applied the reviewed runtime fixes and ran the checks in increasing order of scope:
bash scripts-postgres/apply_orderentry_runtime_fixes.sh
bash scripts-postgres/run_orderentry_tests.sh
bash scripts-postgres/run_swingbench_workload.sh --verify-only
bash scripts-postgres/run_swingbench_workload.sh --duration 300
Conclusion
AI did not make this migration a one-click operation. The Oracle application combined packages, private helpers, collections, package state, bulk operations, transaction control, and behavior that only appears under realistic calls. A credible migration had to understand those interactions rather than only produce PostgreSQL syntax.
The useful change is that the migration can be approached as an integrated project from one place. In VS Code, the migration extension converted and compiled the schema. I inspected the generated code and reports, directed GitHub Copilot to generate the supporting data-copy and workload programs, reviewed and applied runtime fixes, and validated data, API calls, business effects, and concurrent load. The model accelerated code conversion and the creation of focused tools, while PostgreSQL, executable tests, and my knowledge of the application provided the evidence.
This is the method I would carry from assessment into a real migration: use AI to drive more of the conversion, diagnosis, repair, and validation iterations, not to hide complexity. Keep source behavior, converted code, data movement, test programs, findings, and fixes together in a reproducible workspace. This makes the first prototype useful evidence for estimating the remaining effort. A migration report measures one pipeline stage; the application is migrated only when its data, transactions, business effects, and workload work together on PostgreSQL.
This validation is necessary but not sufficient for production. A real go-live also requires review of edge cases, collation, NULL handling, error paths, security, and sustained performance under representative data volumes.
Because this walkthrough exercises preview functionality, experience reports are especially valuable. Readers trying it on other Oracle applications or PostgreSQL targets can share what converts well, what requires intervention, and which findings would make the assessment more useful in the PostgreSQL Hub Developer Forum