Back to blog
Migration Apr 12, 2026 9 min read

Database Triggers, Package State, and the Migration Pitfalls Nobody Mentions

The logic that lives outside the forms

An energy trading firm asked us to estimate a migration last spring. The .fmb inventory came in at 184 files and roughly 340,000 lines of embedded PL/SQL. Reasonable scope. Then we ran the dependency analysis against the database. The forms referenced 412 PL/SQL packages, 1,840 database triggers, and 96 package-state variables shared across sessions. The database side held 2.7 times more code than the forms themselves.

That ratio is typical. The forms are the tip. The iceberg is in the database.

Database triggers are not form triggers

Forms triggers fire in response to UI events. Database triggers fire in response to DML — INSERT, UPDATE, DELETE — and run inside the transaction that issued the statement. They enforce referential rules, populate audit columns, cascade deletes, and run business logic the forms assume will happen automatically.

A representative audit trigger:

CREATE OR REPLACE TRIGGER orders_audit_trg
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
BEGIN
  INSERT INTO orders_audit (order_id, action, changed_by, changed_at,
                            old_amount, new_amount)
  VALUES (COALESCE(:NEW.order_id, :OLD.order_id),
          CASE WHEN INSERTING THEN 'I'
               WHEN UPDATING THEN 'U' ELSE 'D' END,
          USER, SYSDATE, :OLD.amount, :NEW.amount);
END;

Migrations that move to a new application layer often assume they can replace this with application-level audit logging. That assumption breaks the moment a batch job, a SQL*Plus script, or a report writer modifies the same table. The database trigger caught all writers. The application logger catches one.

Package state is a hidden contract

Oracle PL/SQL packages can hold session-scoped state: variables declared at package level that persist across calls within the same database session. Forms applications use this heavily — a user logs in, the session fires a login procedure, and 40 package variables get populated with user ID, role, cost center, fiscal calendar, and approval limits. Every subsequent form call reads from that state without reloading it.

We’ve counted between 20 and 180 package-state variables per Forms application. The median is 58. None of them are visible in the .fmb files. None of them survive a stateless REST architecture without explicit remodeling.

The four pitfalls we see repeatedly

Across 14 migration projects, the same four problems account for most of the schedule slips on the database side:

  • Invisible commit points. Forms issue implicit commits on block navigation. Database triggers assume those commits happen. REST endpoints that batch multiple operations into one transaction break trigger assumptions.
  • Session-state drift. Package variables populated at login become stale when the new architecture pools database connections. The same connection serves different users across requests.
  • Trigger cascades. One UPDATE fires a trigger that issues another UPDATE that fires another trigger. The cascade depth in our sample reaches 7. Application-level replacements miss intermediate steps.
  • REF cursor leaks. Stored procedures return REF cursors that forms consume row-by-row. A REST endpoint has to materialize the full result set, which changes memory characteristics and sometimes trips ORA-04030 on large queries.

Each one is fixable. None of them is obvious from reading the .fmb files alone.

What the dependency analysis has to catch

Before writing a line of migration code, we extract the full dependency graph: every table, view, package, procedure, function, trigger, and sequence the forms touch, plus everything those objects touch in turn. The graph for a mid-sized application runs to 4,000 to 12,000 nodes.

The graph is what tells us whether package state can be replaced with a typed session store, whether database triggers can be left in place, or whether the audit logic has to be rewritten at the application layer. Skipping this step is the most expensive mistake in Oracle Forms migration. We’ve seen it add six to nine months to projects that looked clean on day one.

Leaving database triggers in place

Our default recommendation is to keep database triggers running during and after migration. They’re battle-tested, they catch non-application writers, and they’re already in the auditors’ control matrix. The new TypeScript application calls stored procedures for complex transactions instead of reimplementing the logic.

This isn’t always possible — some triggers call DBMS_ALERT, UTL_HTTP, or other features that have to be rewritten — but when it is, it removes an entire class of migration risk. The database keeps enforcing what it’s always enforced. The application layer changes around it.

The takeaway

Oracle Forms migrations are database migrations wearing a UI disguise. The .fmb files are the visible surface; the real contract is the PL/SQL packages, database triggers, and package state the forms have been leaning on for decades. Every project we’ve shipped on time started with a full dependency analysis of the database side — before anyone opened a form. Every project that slipped skipped that step.