This chapter zooms out to show how the pieces fit together: from
.pell source through to the multi-file PL/SQL deployment, with notes
on packages, runtime, schema layout, and dependency tracking.
What pell build produces
For a single module file:
hello.pell (your input)
↓ pell build
hello.sql (one CREATE PACKAGE + CREATE PACKAGE BODY)
For a directory of modules:
my_app/
├── employees.pell → employees.sql
├── billing.pell → billing.sql
└── reports.pell → reports.sql
Each module produces one PL/SQL file with its own package spec + body. No bundling, no cross-file optimization — each module is compiled independently.
Schema-level items (OBJECT TYPE for pub type, pub sealed type,
pub aggregate, and the OBJECT TABLE TYPES backing @pipelined) are
emitted before the package spec, as separate CREATE TYPE
statements. The output file orders them correctly:
CREATE OR REPLACE TYPE money AS OBJECT (...);
/
CREATE OR REPLACE TYPE money_list AS TABLE OF money;
/
CREATE OR REPLACE PACKAGE money_pkg AS ...; END money_pkg;
/
CREATE OR REPLACE PACKAGE BODY money_pkg AS ...; END money_pkg;
/
The runtime package
If your module declares pell errors, those compile to PRAGMA
EXCEPTION_INIT entries in a shared pell_runtime package. Generate it
with:
pell runtime > pell_runtime.sql
The runtime subcommand walks your project directory and aggregates
every error declaration into a single pell_runtime package
containing all the EXCEPTIONS plus a set_err / get_err /
clear_err triple for stashing error payloads in SYS_CONTEXT:
CREATE OR REPLACE CONTEXT pell_err USING pell_runtime;
CREATE OR REPLACE PACKAGE pell_runtime AS
PROCEDURE set_err(p_key IN VARCHAR2, p_payload IN VARCHAR2);
PROCEDURE clear_err(p_key IN VARCHAR2);
FUNCTION get_err(p_key IN VARCHAR2) RETURN VARCHAR2;
hr_employees_notfound EXCEPTION;
PRAGMA EXCEPTION_INIT(hr_employees_notfound, -20100);
...
END pell_runtime;
/
Install order: pell_runtime.sql first, then individual module
files. Pell module files can RAISE pell_runtime.<exception> and that
EXCEPTION_INIT mapping makes SQLCODE = -20100 (etc.) trigger the
right WHEN.
You also need to install pell_re.sql (for any module using regex):
# in this repo
sqlplus user/pass @runtime/pell_re.sql
pell_re has no dependencies on pell_runtime; install order between
them doesn’t matter.
Deployment order
A clean install:
pell_runtime.sql— once per schema.pell_re.sql— once per schema (if any module uses regex).- Each module’s
.sqlfile — order matters only if modules reference each other; pell doesn’t infer the topology, you do.
Pell does not currently emit a deploy.sql master script that lists
everything in the right order. The convention is to maintain that
script by hand, or use your build system’s dependency declarations.
Anonymous-block mode (pell exec / pell repl)
When you run pell exec, the emit mode flips:
- No CREATE PACKAGE — everything goes into a single
DECLARE/BEGIN/END;block. recorddecls becomeTYPE ... IS RECORDinsideDECLARE.fnitems become nested local subprograms.- Top-level statements become the body of a synthetic
pell_anon_mainprocedure thatBEGINcalls.
The block executes once per pell exec invocation, leaving no trace
on the schema. This is great for quick experiments but can’t host
schema-level objects (OBJECT TYPE, TABLE TYPE, aggregates) — those
need CREATE TYPE and pell exec errors out with a clear message
pointing you at pell build.
Dependency tracking
Static SQL inside sql!{} blocks is fully visible to Oracle. After
installing your package, ALL_DEPENDENCIES will show every
table/view/sequence/package the module references:
SELECT name, referenced_name, referenced_type
FROM all_dependencies
WHERE owner = 'HR' AND name = 'EMPLOYEES_PKG';
This is what makes “drop a column → packages depending on it become INVALID” work.
Dynamic SQL inside exec_dyn(...) is opaque to Oracle by default —
the SQL is a string. To make ALL_DEPENDENCIES see them, declare
@touches(...) on the unsafe fn:
unsafe fn count_for(tname: text) -> number
@touches(employees, departments)
{
...
}
Pell emits “pinning cursors” — never-called CURSOR declarations with
WHERE 1=0 — so Oracle’s parser sees the table reference and
records the dependency. See Dynamic SQL.
Compiler internals at a glance
.pell sources
↓ lexer (pell/lexer.py)
token stream
↓ parser (pell/parser.py)
AST (pell/ast.py)
↓ emitter (pell/emitter.py)
PL/SQL text (CREATE PACKAGE / BODY / TYPE / etc.)
There’s no separate intermediate representation — the AST goes straight to PL/SQL text via the emitter. The emitter is the largest file (~4200 lines as of this writing) and most of the language’s behavior lives there.
The pell_re regex engine is a different animal: it’s pure PL/SQL
runtime code (no Python), installed once and queried at execution
time. Its bytecode-compile-and-cache scheme is described in
Reference: pell_re.
Reproducible builds
Add --reproducible to omit volatile metadata (build timestamp,
uncommitted-tree hash) from the file header. The output then depends
only on the source content + compiler git SHA. Useful for:
- CI byte-comparing emitted SQL against committed snapshots.
- Diffing PRs cleanly — no “build timestamp” noise.
The test suite uses this for the expected/ snapshots.
The shape of a real deployment
A medium-size pell project might lay out like:
my_app/
├── README.md
├── compile.sh
├── deploy.sh
├── pell/ (the compiler — vendored or via PYTHONPATH)
├── src/
│ ├── auth.pell
│ ├── billing.pell
│ ├── reports.pell
│ └── inventory.pell
├── plsql/ (committed — `pell deploy . --build-only`)
│ ├── auth.sql the lowered SQL is the deployment
│ ├── billing.sql artifact: SHA-anchored, human-readable,
│ ├── reports.sql git-blameable. Commit it, audit it.
│ ├── inventory.sql
│ ├── pell_runtime.sql
│ └── pell_re.sql
└── tests/
├── test_auth.pell
└── test_billing.pell
compile.sh runs pell build + pell runtime. deploy.sh connects
to Oracle and applies the files in order. The pell source is the
source of truth; the .sql is the deployable artifact (gitignored
or stored separately depending on your team’s convention).
You’ve finished the tutorial
If you’ve read every chapter, you now know:
- How to declare modules, functions, records, and types.
- How to talk to the database with
sql!{}and consume results. - How to handle errors with typed
Result<T, E>and@retry. - How to use the advanced features: aggregates, pipelined functions, dynamic SQL, JSON, regex, jq.
- How to iterate interactively with
pell execandpell repl.
Reach for the Reference pages when you need exact behavior on a specific feature, and the Cookbook for worked solutions to common problems.