STRUMENTA LANGUAGE ENGINES · ORACLE PL/SQL

Oracle PL/SQL, parsed the way it is actually written.

Package bodies, the SQL embedded in them, and the SQL*Plus script wrapped around both. One grammar covers all three: 9,634 lines of it, 508 regression fixtures with committed expected trees, an EMF/Ecore metamodel — and a code generator that writes PL/SQL back out.

It is the one engine in our catalog that closes the loop. Parse, change the model, print PL/SQL again. Everything else we license reads; this one also writes.

The engine at a glance
GRAMMAR9,634 lines of ANTLR
FIXTURES508 files · 19,074 lines
TESTS156 unit + 8 parameterized
OUTPUTAST · JSON · XML · EMF · LionWeb
GENERATORAST → PL/SQL text
RUNTIMEJVM library · CLI

One file, two languages, and a third on top

ExtendedSqlParser.g4 · 7,221 parser + 2,413 lexer lines

PL/SQL does not call SQL. It embeds SQL as a first-class statement form — and the two halves disagree about almost everything. That disagreement is the whole difficulty of the language, and it is what a single-namespace SQL parser cannot represent.

One PL/SQL statement whose identifiers resolve in two different namespaces In the statement SELECT sal INTO v_sal FROM emp WHERE empno = v_id, the names sal, emp and empno are resolved against the database catalog defined by the DDL, while v_sal and v_id are resolved against the declarations of the surrounding PL/SQL block. SELECT sal INTO v_sal FROM emp WHERE empno = v_id; RESOLVED AGAINST THE DATABASE CATALOG — THE DDL, WHICH IS NOT IN THIS FILE sal · emp · empno v_sal · v_id RESOLVED AGAINST THE DECLARATIONS OF THE SURROUNDING PL/SQL BLOCK

In SELECT sal INTO v_sal FROM emp WHERE empno = v_id; the names sal, emp and empno belong to the database catalog. v_sal and v_id belong to the PL/SQL block. Oracle’s capture rule — a PL/SQL variable is shadowed by a column of the same name — is a resolution rule, not a syntax rule, so the parse tree alone will never tell you what empno means. What the parse tree can do is keep the two halves cleanly apart, so that a resolver has something to work with.

Reserved words diverge too: words reserved in SQL are legal identifiers in PL/SQL and the reverse also happens. There is a fixture in our regression corpus called substr_as_regular_id.pkb, and it exists because somebody, somewhere, named a variable after a built-in function.

On top of both sits SQL*Plus, a third language the database never sees. A real installation script starts prompt create_types.sql, ends its units with a bare /, and calls SHOW ERRORS. A parser that chokes on any of those fails on the first file of the deployment, before it has read a single line of business logic.

All three are in one grammar, reachable through three entry points: plsql_script for a whole script, plsql_snippets for a bare executable block, and sql_script for plain SQL. The snippet entry point is what an editor or a rules engine needs — it parses a fragment without asking you to wrap it in a package that does not exist.

Input — PL/SQL package body

hr_util.pkb

-- installation script, not a clean unit source
prompt installing hr_util
CREATE OR REPLACE PACKAGE BODY hr_util AS
  FUNCTION chain(p_root IN employees.employee_id%TYPE)
    RETURN emp_tab PIPELINED IS
    l_msg VARCHAR2(200) := q'{can't walk the tree}';
  BEGIN
    FOR r IN (SELECT e.* FROM employees e
                START WITH e.employee_id = p_root
              CONNECT BY PRIOR e.employee_id = e.manager_id)
    LOOP
      PIPE ROW (r);
    END LOOP;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      RAISE_APPLICATION_ERROR(-20101, l_msg);
  END chain;
END hr_util;
/
SHOW ERRORS
The two marked lines are the ones that are not PL/SQL at all. prompt and the lone / are SQL*Plus — and inside the body, ; ends a statement while only the / ends the unit.

Output — parse tree, by grammar rule

intermediate rules elided

plsql_script
├── sql_plus_command PROMPT_MESSAGE, line 2
├── unit_statement
│   └── package_body hr_util, line 3
│       └── create_function chain, PIPELINED
│           ├── plsql_parameter p_root IN … %TYPE
│           ├── ⋯
│           │   └── connect_by line 9
│           └── exception_handler WHEN NO_DATA_FOUND
├── sql_plus_command SOLIDUS — end of unit, line 19
└── sql_plus_command SHOW ERRORS, line 20
These are the real rule names in the shipped grammar, not a diagram of one. The AST you work against is derived from this tree with re-arrangements and simplifications; every node carries a position, and the test suite fails the build if any node’s position falls outside its parent’s.

What it handles

Four constructs decide whether a PL/SQL parser survives contact with a real Oracle estate. Each one is a place where the lexer, not the parser, has to make the decision — which is why generic SQL grammars fail them.

Q-quoted string literals

The author chooses the delimiter, per literal. The lexer has to read one character to learn how the string terminates — and then keep the apostrophes inside it out of the token stream. Oracle code is full of them precisely because Oracle code is full of English sentences.

l_msg := q'{can't walk the tree}';
l_ddl := q'[ALTER TABLE emp ADD (x NUMBER)]';
l_utf := nq'#il n'y a qu'un#';

The / unit terminator

Inside a package body, ; ends a statement. Only a lone / on its own line ends the unit. Get this wrong and every package body in the estate collapses into one malformed statement. It is modeled as sql_plus_command : SOLIDUS | …, alongside PROMPT, SHOW ERRORS, START, WHENEVER and EXIT.

  END chain;
END hr_util;
/  -- ends the unit, not the statement
SHOW ERRORS

%TYPE and %ROWTYPE anchoring

An anchored declaration gives a variable whose type — or whose entire field list — comes from the DDL. Nothing in the PL/SQL text says what those fields are. This is also why a column type change silently re-types code that no grep will find.

  v_row  employees%ROWTYPE;
  v_id   employees.employee_id%TYPE;
  TYPE emp_tab IS TABLE OF employees%ROWTYPE
    INDEX BY PLS_INTEGER;

Hierarchical queries

CONNECT BY PRIOR with START WITH, LEVEL, CONNECT_BY_ROOT and SYS_CONNECT_BY_PATH is a query shape with no equivalent in any other SQL dialect we license. Fifteen grammar rules reference it, and it is where org charts, bills of materials and account hierarchies actually live.

SELECT LEVEL, SYS_CONNECT_BY_PATH(last_name, '/')
  FROM employees
 START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;

Two more belong on the same list. Conditional compilation — $IF … $THEN … $ELSE … $END and $$name inquiry directives — is a preprocessor handled inside the grammar, so the directives survive into the tree instead of being stripped before anyone can analyze them. And the MODEL clause is a spreadsheet-style calculation language nested inside a SELECT, with its own PARTITION BY, DIMENSION BY and MEASURES.

Coverage, by construct

Every rule name in the middle column is a rule in the grammar we ship. Status reflects the current release and the cases the test suite tracks as unsupported.
Construct Grammar rules Status
Packages — specification and body create_package, package_body Supported
Procedures, functions, pipelined table functions create_procedure, create_function, pipelined Supported
Triggers create_trigger Supported
Object types, collections, VARRAYs create_type Supported
Cursors, cursor FOR loops, anchored types cursor_declaration, %TYPE, %ROWTYPE Supported
Exception handlers and pragmas exception_handler Supported
Bulk operations forall Supported
Oracle-only SQL merge_statement, connect_by, pivot, model_clause, create_materialized, create_synonym, flashback Supported
SQL*Plus script commands sql_plus_command, set_command Supported
Conditional compilation directives Supported
Dynamic SQL execute_immediate Call site only — the string built at run time stays opaque
Exotic DBA DDL, such as ALTER CLUSTER … ALLOCATE EXTENT alter_cluster, tablespace and partition clauses Partial — around 20 cases tracked as unsupported in the suite
Transact-SQL Not offered on this engine

On versions, we would rather be exact than impressive. The grammar’s lineage is the antlr/grammars-v4 Oracle PL/SQL 11g grammar; our extensions go well past it — flashback queries, MODEL, PIVOT, materialized views and blockchain tables (a 21c feature) are all in the grammar and in the shipped example corpus. We do not publish a certified “Oracle X through Y” range, because the number would not answer your question. Send us a representative set of your own sources and we will tell you what parses.

One file that only this engine takes

We license five SQL-family engines. This file parses with exactly one of them, and it is a fair sample of what an Oracle estate looks like from the inside.

Input — package specification and body in one script

hr_util.pks + hr_util.pkb

PROMPT installing hr_util
CREATE OR REPLACE PACKAGE hr_util AUTHID CURRENT_USER AS
    TYPE emp_tab IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
    bad_grade EXCEPTION;
    PRAGMA EXCEPTION_INIT(bad_grade, -20101);
    FUNCTION chain(p_root IN employees.employee_id%TYPE)
      RETURN emp_tab PIPELINED;
END hr_util;
/
CREATE OR REPLACE PACKAGE BODY hr_util AS
    FUNCTION chain(p_root IN employees.employee_id%TYPE)
      RETURN emp_tab PIPELINED IS
        l_msg VARCHAR2(200) := q'{can't walk the tree}';
    BEGIN
        FOR r IN (SELECT e.* FROM employees e
                    START WITH e.employee_id = p_root
                  CONNECT BY PRIOR e.employee_id = e.manager_id)
        LOOP
            PIPE ROW (r);
        END LOOP;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20101, l_msg);
    END chain;
END hr_util;
/
Eight marked constructs, all Oracle-only. Our Teradata SQL engine has no LOOP of this shape; Firebird PSQL uses SUSPEND rather than PIPE ROW; our generic SQL engine accepts SELECT, INSERT and UPDATE and nothing procedural at all. If your files look like this, the PL/SQL engine is the only one on the list that will read them.
THE HEADLINE DIFFERENTIATOR

Parse it, change it, print PL/SQL back out.

Reading a codebase is the easy half. Writing valid PL/SQL back from a modified tree is the half nobody publishes for free — and it is what turns an assessment tool into a migration tool.

The engine ships a printer that walks the AST and emits PL/SQL text. That closes the loop: read the estate, apply a transformation to the model, print the result, and diff it against what you started with. Without a printer you can only report; with one you can rewrite.

It is the reason this engine, alone among the five SQL-family engines we license, can support automated conversion rather than assessment only — whether the target is modernized PL/SQL or the first mechanical pass of a PL/SQL to PL/pgSQL move.

The shipped status note is honest about the boundary: the code generator has good coverage of the syntax the parser supports. Where the parser has a gap, the printer has the same gap. That is a limitation with an edge you can find, not a promise with no edge at all.

Command line

XML by default, JSON on request

# serialize the AST for one file
java -jar ExtendedSqlParser-1.5.2.jar \
     --format json \
     --destination out/ \
     hr_util.pkb

# options
--format xml|json   --language plsql
--destination DIR   --ignorePosition
--verbose
The same jar is the library. In-process you get the tree and issues directly; on the command line you get it serialized, which is what a pipeline step usually wants.
01

Parse

What is in the file?

Tokens, then a parse tree, then an AST derived from it with re-arrangements and simplifications, so you work against a designed model rather than raw ANTLR contexts.

02

Inspect

Where exactly did it come from?

Every node carries a position. Difficult input returns a partial tree plus a positioned issues list instead of an exception, so a batch over ten thousand files finishes and tells you where it struggled.

03

Transform

What do you want it to become?

Your code walks and rewrites the tree — from the JVM directly, or against the EMF/Ecore metamodel, or through the serialized JSON in whatever language your tooling is written in.

04

Print

Is it PL/SQL again?

The printer emits PL/SQL text from the modified AST. Round-trip it through the parser again and you have a check that costs nothing to run.

9,634
LINES OF ANTLR GRAMMAR
508
FIXTURES, EACH WITH A COMMITTED EXPECTED TREE
19,074
LINES OF PL/SQL IN THE REGRESSION CORPUS
164
AUTOMATED TESTS ACROSS 20 CLASSES

What you receive

A jar, the documents that explain the tree it produces, and a person to write to. The engine runs inside your own network, on your own machines, with no connection back to us and none to the database the code came from.

01 The engine

A JVM library and a command-line tool, in one jar

Kotlin on the JVM, built with ANTLR 4.11.1 on Kolasu — the JVM implementation of Starlasu. Use it as a library from Java or Kotlin, or run the jar over a directory and collect serialized trees. Java 11 or later.

library · CLI · same artifact
02 The model

An AST, a metamodel, and four ways to serialize it

A typed AST rooted at a compilation unit, with positions on every node. Serialize to JSON or XML. An EMF/Ecore metamodel ships with the product, so you can generate code against the tree rather than pattern-matching a shape you guessed. The engine also supports LionWeb, so the model travels to tooling we did not write.

JSON · XML · EMF/Ecore · LionWeb
03 The documents

A manual, and a generated map of the AST

The delivery package is the jar, a PDF manual, an HTML document describing the AST structure generated from the grammar itself, a README, a folder of worked examples, and a sample serialized tree to check your reader against on day one.

PDF manual · AST document · examples
04 The license

Standard, Distribution or Service — with support included

Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. Support is part of the license, not an upsell. The guarantee runs a year and is extendable, or can be taken pay-as-you-go.

three tiers · support included
Onboarding

The first week is reading the AST document and running the jar over your own files — not waiting for an environment.

There is nothing to install on the database side and nothing to provision. If the integration needs help, that is what the support in the license is for; if it needs more than help, we do that work too, as training, architectural design or coaching alongside your team. Write to products@strumenta.com.

THE REST OF THE ESTATE

Oracle PL/SQL rarely travels alone.

The packages hold the rules. The application that calls them is COBOL on z/OS, RPG on IBM i, Java, or all three — and the question you are actually asking usually crosses that boundary.

An inventory that stops at the database edge answers half the question. Which batch program calls which package; which package writes the column that the screen reads; what breaks if this table changes — none of those live entirely inside Oracle.

Every Strumenta engine is built on Starlasu, so a PL/SQL tree, a COBOL tree and an RPG tree have the same shape, the same traversal model and the same API. One tool walks all of them. That is the thing you cannot assemble by gluing unrelated open-source parsers together: five parsers means five models, five idioms and five sets of edge cases to reconcile before you have written a line of analysis.

Every engine also supports LionWeb, so the models interchange with LionWeb-compliant tooling instead of being locked inside one process — and every engine is available through bindings from Java, Kotlin, Python, TypeScript and C#. The host application picks the language; the parser does not.

Engines in the same estate

one model, one traversal

COBOL engine →
The batch that calls the package.

RPG & DDS engine →
The IBM i application on the other side of the link.

Java engine →
The service layer that issues the calls.

The whole catalog →
15+ engines, one AST framework underneath.

What people build with it

Five kinds of work, in the order customers usually reach them. The first three need only the parser; the last two are the reason the code generator exists.

Analysis, standards and inventory

PL/SQL carries business-critical logic, and in an Oracle ERP estate there are tens of thousands of custom packages in XX schemas that nobody has read in a decade. Parse the .pks and .pkb pairs to check syntax, enforce coding standards across the whole codebase, and build the inventory of which custom package touches which seeded table — the artifact every Fusion or OCI move asks for first.

Editors and language-aware tooling

IDE plugins and internal tools use the engine for auto-completion, syntax highlighting and real-time error detection. The plsql_snippets entry point matters here: an editor can parse the block a developer is typing without wrapping it in a package first, and the positioned issues list is exactly what an editor needs to underline.

Refactoring and change-risk analysis

Rename a variable or a procedure across a package, extract a subroutine, split a monolithic procedure, retire deprecated syntax. And the case a text search cannot do at all: because %ROWTYPE and %TYPE anchor to the DDL, a column type change silently re-types PL/SQL variables in files that never mention the type. Static analysis over the AST finds them.

Migration off Oracle

The move to PostgreSQL and PL/pgSQL is the largest PL/SQL modernization market there is. Parse, map the AST, print the target — partial or substantially automated depending on how uniform the estate is. This is the work the code generator was written for, and it is why an assessment-only parser will not get you there.

Documentation and reverse engineering

In banking and insurance the rate tables, eligibility rules and settlement logic are implemented as Oracle packages, and the specification is the code. Extract the IF and CASE structure and the exception paths to generate procedural flow diagrams, dependency graphs or the input to a rules engine — regenerated from the source on every release rather than written once and left to rot.

Why license this instead of starting from the public grammar

A technical buyer already knows there is a free PL/SQL grammar, and we are not going to pretend otherwise: ours started as that grammar. The file header still credits Alexandre Porcelli, Ivan Kochurkin and Mark Adams, and the upstream project stays Apache 2.0. What a license buys is everything downstream of the parse tree — which is where all the work is.

A grammar file is a starting point, not a product. The difference is the six years of work between the two columns.
What you need antlr/grammars-v4 PL/SQL grammar This engine
A model to program against Raw ANTLR parse-tree contexts Designed AST, re-arranged and simplified
Proof that an upgrade did not change the tree Example inputs, no expected trees 508 fixtures with committed expected ASTs
A metamodel to generate code against Not included EMF/Ecore, shipped with the product
A way back to PL/SQL text Not included Code generator, AST to source
Positions you can trust Whatever you build yourself Asserted by the suite; the build fails otherwise
Behavior on input it dislikes Default ANTLR errors on the console Partial tree plus a positioned issue list
Someone to call The issue tracker, and good luck Support included in the license

The same reasoning applies to every engine in the family, and the method behind it has a name: read the Chisel Method for how we make parser work estimable, or Parsers and transpilers for the three ways into this work.

What to discuss before you license

Five things we would rather tell you now than have you find in week three.

  • Some syntax from the specification is not supported. Around twenty constructs sit in the test suite as ignored cases, chiefly exotic DBA-shaped DDL — things like ALTER CLUSTER foo MODIFY PARTITION bar ALLOCATE EXTENT. They are in the suite precisely so that we, and you, know which ones they are.
  • Dynamic SQL is opaque, by nature. EXECUTE IMMEDIATE l_stmt USING l_id, DBMS_SQL.PARSE and OPEN c FOR l_stmt take a string assembled at run time. The engine recovers the call, the bind lists and the position; it cannot recover a statement that does not exist until execution. Impact analysis has to treat those sites as opaque and report them, and any honest answer about your estate depends on how many there are.
  • Anchored types need the DDL. %ROWTYPE and %TYPE point at the catalog, and unqualified column names resolve there too. Without the schema definitions, that half of every embedded statement stays unresolved. Cross-file symbol resolution across specification, body and DDL is a scoped piece of work on top of the engine, not something the engine does on its own — bring the DDL and we will tell you what it takes.
  • This engine is Oracle PL/SQL only. If you also have Transact-SQL, Teradata or Snowflake in the estate, those are different products and a different conversation. Do not buy this one expecting it to cover them.
  • It runs on a JVM. Java 11 or later, in your environment. If your toolchain is Python, TypeScript or C#, you use the bindings or the serialized tree rather than the jar in-process — which works, and is worth designing for deliberately rather than discovering late.

What we will ask you

  1. Packages, or standalone procedures and scripts — and do you have the .pks and .pkb pairs, or only ALL_SOURCE dumps?
  2. Which Oracle version, and do you use MODEL, flashback, object types or blockchain tables?
  3. Do you need code generation back to PL/SQL, or read-only analysis?
  4. Do you have the DDL?
  5. How much dynamic SQL, and can we see a sample?
  6. Are your sources SQL*Plus installation scripts, or clean unit files?
  7. What is the target — an assessment report, a PL/pgSQL conversion, or a tool you are building?

Straight answers

Which Oracle versions does it cover?

The grammar descends from the public Oracle PL/SQL 11g grammar and has been extended well past it — flashback queries, the MODEL clause, PIVOT, materialized views and blockchain tables, a 21c feature, are all in the grammar and in the shipped examples. We deliberately do not advertise a certified version range, because the useful answer comes from running the engine over your files. Send a representative set and we will report what parses.

Can it read our installation scripts, or only clean unit sources?

Installation scripts are the normal case. SQL*Plus commands are grammar rules, not something stripped in a pre-processing step: PROMPT, the bare /, SHOW ERRORS, START, WHENEVER, EXIT and SET all parse, and the shipped example corpus is exactly these files — install.sql, uninstall.sql, create_tables.sql, create_types.sql.

Does it write PL/SQL back out, or only read it?

Both. A code generator walks the AST and emits PL/SQL text, which is what makes parse, transform and regenerate possible rather than assessment only. It is the only engine among the five SQL-family engines we license that has one. Its coverage tracks the parser’s: where the parser has a gap, so does the printer.

Do we have to send you our source code?

No. The library and the command-line tool run wherever a JVM does, inside your own network, with no connection back to us and none to the database. If you want us to run an evaluation over a sample, we can — under whatever agreement you need — but nothing about the product requires it.

Can we use it from Python, TypeScript or C#?

Yes. The engine is built on Starlasu and is available through bindings from Java, Kotlin, Python, TypeScript and C#, and it supports LionWeb, so the model interchanges with LionWeb-compliant tooling. If you would rather not embed anything, the command-line tool serializes the tree to JSON or XML and any language can read that.

Send us a package body the ugliest one you have

Bring us a script, and we will tell you what parses.

A representative sample — a package specification and body, an installation script, whatever your worst file is — and we will come back with what the engine reads, what it does not, and whether the code generator changes the shape of your project. If PL/SQL is not the hard part of your estate, we will say that too.

Scroll to Top