DEP/i is a DB2-for-i schema-migration / CI-CD deploy platform — the deploy
tool itself, not the application it manages. It maintains a deploy history (MIGRATION_LOG),
a "current deployed version" register (a global CREATE VARIABLE), a synthetic log-id
generator (a SEQUENCE), a distinct TYPE, and an idempotent migration-runner
procedure (SP_APPLY_MIGRATION) that every versioned migration is logged through. It has
no 5250 screen: it is driven the way a real deploy CLI drives a database — an orchestrating
job runs each version's DDL/DML batch inside a SAVEPOINT it takes itself, then calls the
bookkeeping procedures to record the outcome and advance the version register. This manual is the
reference for the operator who runs a deploy (and an idempotent re-deploy or a rollback) and for the
developer maintaining the engine. It is grounded entirely in the committed source
(sqlpl-app-dep/src/deploy_engine.sql, src/migrations.mjs, src/seed.mjs,
and the test/dep_daily.mjs driver).
DEP/i deploys and evolves a database schema through versioned migrations, keeping an auditable record of every deploy and guaranteeing that a re-run is safe:
MIGRATION_LOG row per (version, migration-name)
attempt, with STATUS = OK (applied), SKIPPED (already applied — the
idempotent no-op), or FAILED (a bad migration that was rolled back).CURRENT_VER register holds the highest
applied version, advanced only after a version's DDL batch is confirmed clean.SP_APPLY_MIGRATION checks whether a
(version, name) already applied OK and, if so, logs a clean SKIPPED no-op instead of re-applying; a
MERGE upsert makes the config write itself idempotent belt-and-braces.SAVEPOINT; if it fails (e.g. an ADD CONSTRAINT the existing data violates),
ROLLBACK TO SAVEPOINT leaves the schema and data byte-for-byte unchanged and the log
records FAILED, never a half-applied schema.QSYS2.SYSTABLES / SYSCOLUMNS / SYSSEQUENCES / SYSVARIABLES / SYSTYPES / SYSTABAUTH /
SYSCOLAUTH) to confirm the deployed shape matches what it intended to apply.The worked example that ships with it deploys a small order-management schema (ORDDB:
CUSTOMER / PRODUCT / ORDERS / ORDER_LINE) through six versions V1–V6, plus a deliberately
bad V5 to prove rollback.
Two things distinguish DEP/i from a normal SteelFrame X application:
DEPSYS and
contains only deploy bookkeeping: the log table, the sequence, the version variable, the distinct
type, a config table, and the three runner procedures. The managed application schema
(ORDDB) is data the tool operates on, shipped as versioned migration scripts in
src/migrations.mjs — not part of the engine.test/dep_daily.mjs, which mirrors a real deploy CLI: for each version it takes
its own SAVEPOINT, runs the version's DDL via executeSqlBatch, then calls
SP_LOG_MIGRATION_OK / SP_LOG_MIGRATION_FAILED to record the outcome. This
manual documents that honestly — where a loan or order app would have a screen, DEP/i has a
driver loop.EXECUTE IMMEDIATE arbitrary caller-supplied DDL text in every engine, and this
engine's PREPARE/EXECUTE IMMEDIATE support is a documented honest-reject surface.
So the runner procedures own only the bookkeeping (log + version register) — the common code
worth centralizing — while the per-version DDL (opaque, generated per version) runs as an external
batch the caller wraps in a savepoint. This mirrors how real CI/CD deploy tools are structured, where the
migration body is version-specific and the outcome logging is shared. ENGINE (DEPSYS) DRIVER (deploy CLI) MANAGED SCHEMA (ORDDB)
-------------- ------------------ ----------------------
MIGRATION_LOG (history) for each version V: V1 baseline (CREATE)
DEPLOY_SEQ (log-id seq) SAVEPOINT SP_V V2 ADD COLUMN / ALIAS
CURRENT_VER (VARIABLE) run V's DDL batch ------> V3 TYPE / RENAME / grants
VERNO_T (TYPE) confirm clean? V4 audit / FK / staging
DEPLOY_CFG (config kv) yes -> SP_LOG_MIGRATION_OK V5 BAD -> ROLLBACK -> FAILED
no -> ROLLBACK TO SP_V V5 FIXED (clean then ADD)
SP_LOG_MIGRATION_OK ---------> SP_LOG_MIGRATION_FAILED V6 teardown (DROP/REVOKE)
SP_LOG_MIGRATION_FAILED confirm OK? bumpVer CURRENT_VER
SP_APPLY_MIGRATION (config) verify catalog (QSYS2.SYS*)
A single deploy step flows: driver takes SAVEPOINT SP_V → runs version V's DDL/DML
batch against ORDDB → if clean, calls SP_LOG_MIGRATION_OK(V, name, detail)
which inserts an OK row (log-id from DEPLOY_SEQ) → driver reads the returned status and,
when OK, issues a top-level SET DEPSYS.CURRENT_VER = V → driver verifies the new shape in
the DB2 catalog. If the batch fails, the driver instead does ROLLBACK TO SAVEPOINT SP_V and
calls SP_LOG_MIGRATION_FAILED.
| Object | Type | Role |
|---|---|---|
| DEPSYS | Schema | The deploy engine's own schema (the tool itself). |
| MIGRATION_LOG | Table | Deploy history: one row per (version, name) attempt; STATUS OK/SKIPPED/FAILED. |
| DEPLOY_CFG | Table | Key/value deploy config, upserted idempotently by SP_APPLY_MIGRATION. |
| DEPLOY_SEQ | Sequence | Synthetic LOGID generator (NEXT VALUE FOR). |
| CURRENT_VER | Variable | Global "highest applied version" register (CREATE VARIABLE). |
| VERNO_T | Type | Distinct type (AS INTEGER) for version-number bookkeeping. |
| SP_LOG_MIGRATION_OK | Procedure | Logs an OK / SKIPPED outcome (idempotency check on the log). |
| SP_LOG_MIGRATION_FAILED | Procedure | Logs a FAILED outcome after the caller's rollback. |
| SP_APPLY_MIGRATION | Procedure | Self-contained idempotent runner for pure config-seed migrations. |
| V1…V6 | Migrations | The versioned migration scripts for the managed ORDDB schema (src/migrations.mjs). |
| dep_daily.mjs | Driver | The deploy-CLI-shaped orchestrator that runs and verifies the whole cycle. |
The engine catalogue is 1 schema + 3 tables + 1 sequence + 1 variable + 1 type + 3 procedures, driving
6 versioned migrations over the managed ORDDB schema. Sections D and F expand each.
DEP/i is a deploy engine, so it is driven by an orchestration job, not by an operator at a 5250
screen. Honestly stated: there is no DSPF, no subfile, no command-entry program — the
operator surface is a deploy driver (the committed reference driver is test/dep_daily.mjs,
which mirrors a real deploy CLI). Every action below is a SQL batch or a procedure CALL issued
by that driver against schema DEPSYS (the engine) and ORDDB (the managed schema).
| To do this | What the driver issues |
|---|---|
| Bootstrap the engine (once, first deploy) | Run src/deploy_engine.sql as a batch (CREATE SCHEMA DEPSYS … the three procedures). |
| Apply a versioned structural migration | SAVEPOINT SP_V; run the version's DDL batch; on success CALL DEPSYS.SP_LOG_MIGRATION_OK(?,?,?,?). |
| Apply a pure config-seed migration | CALL DEPSYS.SP_APPLY_MIGRATION(verno, name, cfgkey, cfgval, ?) — self-contained & idempotent. |
| Record a failed migration (after rollback) | ROLLBACK TO SAVEPOINT SP_V; CALL DEPSYS.SP_LOG_MIGRATION_FAILED(?,?,?). |
| Advance the version register | Top-level SET DEPSYS.CURRENT_VER = <verno> (see the note below on why this is caller-side). |
| Inspect current version / history | VALUES DEPSYS.CURRENT_VER; SELECT ... FROM DEPSYS.MIGRATION_LOG. |
The three engine procedures take the migration's identity and detail as IN parameters and return the
outcome status as an OUT parameter (SP_LOG_MIGRATION_OK and SP_APPLY_MIGRATION
return P_STATUS = OK / SKIPPED; SP_LOG_MIGRATION_FAILED is
fire-and-forget). The driver reads the status and decides whether to advance CURRENT_VER.
CURRENT_VER.
On this engine, a global SQL variable created with CREATE VARIABLE cannot be read or written
from inside a SQL-PL routine body at all (qualified or unqualified), even though it works correctly from
ad-hoc top-level SQL — a confirmed platform limitation the source notes as SQLPL-PLAT-DEP-03.
So SP_LOG_MIGRATION_OK deliberately does not touch the variable; the driver issues a
top-level SET DEPSYS.CURRENT_VER = … of its own when the returned status is OK. This is
a faithful workaround, not a design compromise: many real deploy tools keep the "last applied version"
register in the orchestrating CLI/job rather than in the database layer anyway.DEP/i's control posture is a durable deploy log + idempotency + savepoint rollback, all enforced in the data layer. It does not model a four-eyes maker–checker approval workflow: a deploy is applied by the driver as it runs each version. The controls it does have:
LOGID from DEPLOY_SEQ and a STATUS of OK / SKIPPED /
FAILED plus a free-text DETAIL. This is the after-the-fact accountability trail: the full
version history 0…6 is reconstructable, and the single V5 FAILED row is preserved as evidence
of the rolled-back attempt.SP_LOG_MIGRATION_OK and SP_APPLY_MIGRATION
first SELECT COUNT(*) for an existing OK row with the same (VERNO,
MIGNAME); if one exists they return SKIPPED and log a SKIPPED row rather than
re-applying. The version register only ever advances (bumpVer ignores a lower/equal
version), so a re-run cannot regress it.SAVEPOINT before
each risky version and rolls back to it on failure, so a bad DDL batch leaves the schema and data
byte-for-byte unchanged. Combined with logging FAILED (never OK) for that version, the log can never
claim a half-applied schema succeeded.SP_APPLY_MIGRATION writes config through a
MERGE (update-if-matched / insert-if-not), so even a re-run that somehow bypassed the log
check cannot duplicate or corrupt a DEPLOY_CFG row — and a re-apply of the same key
never overwrites the already-set value (it SKIPs first).A DEP/i deploy is a sequence of versioned migrations applied in order. The engine is bootstrapped
once (running deploy_engine.sql), then each version's migration is applied, logged, verified,
and — on a re-trigger — safely re-run. The driver wraps every version the way a real deploy
tool would: a pre-flight catalog probe to detect an already-applied step, a SAVEPOINT around
risky DDL, and an outcome log.
-- bootstrap the engine (first deploy only) run deploy_engine.sql -- CREATE SCHEMA DEPSYS + tables/seq/var/type/procs -- apply a structural version (driver pseudo-code) SAVEPOINT SP_V4; run V4 DDL batch; -- ADD/DROP COLUMN, ADD CONSTRAINT, CREATE TRIGGER, RENAME ... CALL DEPSYS.SP_LOG_MIGRATION_OK(4, 'V4_AUDIT_FK_AND_STAGING', 'applied', ?); SET DEPSYS.CURRENT_VER = 4; -- caller-side, only when status = OK -- a bad version rolls back and logs FAILED SAVEPOINT SP_V5; run V5 DDL batch; -- ADD CONSTRAINT the data violates -> fails ROLLBACK TO SAVEPOINT SP_V5; CALL DEPSYS.SP_LOG_MIGRATION_FAILED(5, 'V5_BAD_QTY_CHECK', 'CHECK re-validation failed');
| Step | Purpose | Engine object used | Inputs | Outputs / effect |
|---|---|---|---|---|
| BOOTSTRAP | Create the deploy engine itself. | deploy_engine.sql |
— | DEPSYS schema, MIGRATION_LOG, DEPLOY_CFG, DEPLOY_SEQ, CURRENT_VER=0, VERNO_T, the 3 procedures. |
| SP_LOG_MIGRATION_OK | Log an OK (or idempotent SKIPPED) structural-migration outcome. | MIGRATION_LOG, DEPLOY_SEQ | P_VERNO, P_MIGNAME, P_DETAIL | OUT P_STATUS OK/SKIPPED; one MIGRATION_LOG row. |
| SP_LOG_MIGRATION_FAILED | Record a rolled-back migration. | MIGRATION_LOG, DEPLOY_SEQ | P_VERNO, P_MIGNAME, P_DETAIL | One MIGRATION_LOG row STATUS='FAILED'. |
| SP_APPLY_MIGRATION | Self-contained idempotent runner for a pure config-seed migration. | MIGRATION_LOG, DEPLOY_CFG, DEPLOY_SEQ | P_VERNO, P_MIGNAME, P_CFGKEY, P_CFGVAL | OUT P_STATUS OK/SKIPPED; MERGE'd DEPLOY_CFG row + log row. |
| V1 — V1_BASELINE_SCHEMA | Create the managed ORDDB baseline. | CREATE SCHEMA/TABLE/VIEW/SEQ/FUNCTION/PROCEDURE, GRANT | V1.sql + seed data | ORDDB with 4 tables, view, sequence, function, procedure, grants; CURRENT_VER→1. |
| V2 — V2_ADD_SHIPPING_AND_ALIAS | Add columns + a CHECK + an alias. | ALTER TABLE ADD COLUMN/CONSTRAINT, CREATE ALIAS, CREATE OR REPLACE VIEW | — | CUSTOMER.SHIPZIP, ORDERS.FREIGHT (DEFAULT 0, CK_FREIGHT), alias OORD; CURRENT_VER→2. |
| V3 — V3_TYPE_AND_RENAME | Distinct type, column re-typing/rename, unique, column grants. | CREATE TYPE, ALTER COLUMN SET DATA TYPE/DEFAULT, RENAME COLUMN, ADD UNIQUE, column-level GRANT | — | MONEY_T; UNITPRC widened + DEFAULT 0.01; SHIPZIP→SHIPPOSTAL; UQ_PDESC; column grants; CURRENT_VER→3. |
| V4 — V4_AUDIT_FK_AND_STAGING | Audit trail, deferred FKs, stronger CHECK, staging table, TRUNCATE. | CREATE OR REPLACE TRIGGER, DROP/ADD CONSTRAINT, ADD FK, DROP COLUMN, RENAME TABLE, TRUNCATE RESTART IDENTITY | — | ORD_AUDIT + trigger; CK_FREIGHT2; ORDER_LINE FKs; SHIPPOSTAL dropped; ORD_STAGE; CURRENT_VER→4. |
| V5 — V5_BAD_QTY_CHECK | Deliberately bad — a CHECK the seeded data violates. | ALTER TABLE ADD CONSTRAINT (fails), SAVEPOINT/ROLLBACK TO SAVEPOINT | — | Rolled back; MIGRATION_LOG FAILED; CURRENT_VER stays 4. |
| V5 — V5_QTY_CHECK_FIXED | The real fix: clean the bad data, then add the CHECK. | DELETE, ALTER TABLE ADD CONSTRAINT | — | CK_QTY_POS enforced; CURRENT_VER→5. |
| V6 — V6_TEARDOWN_STAGING | Retire superseded objects. | DROP TABLE IF EXISTS RESTRICT, DROP ALIAS IF EXISTS, REVOKE | — | ORD_STAGE + OORD dropped; UPDATE(UNITPRC) grant revoked; CURRENT_VER→6. |
Creates the managed schema ORDDB with CUSTOMER, PRODUCT,
ORDERS (identity PK, FK to CUSTOMER, DEFAULT OSTAT='O') and ORDER_LINE,
a hint SEQUENCE, the V_OPEN_ORDERS view, the FN_ORDER_TOTAL function
and SP_ORDER_TOTAL procedure, and table/EXECUTE/schema-level grants. Seed data lands two orders;
order 1 has three lines (one a deliberately zero-quantity line, for V5). ORDER_LINE's FKs are
deliberately deferred to V4 — see the ordering note in C.3.
Expected after V1: CURRENT_VER = 1 FN_ORDER_TOTAL(1) = 225.00 10x12.50 + 4x25.00 + 0x7.50 V_OPEN_ORDERS = 2 rows
ADD COLUMN SHIPZIP (nullable) and ADD COLUMN FREIGHT DECIMAL(7,2) NOT NULL DEFAULT 0
(existing rows backfill to 0), a CHECK (FREIGHT ≥ 0) constraint (CK_FREIGHT), a
view redeploy projecting the new columns, and CREATE OR REPLACE ALIAS OORD FOR ORDERS. The
driver's pre-flight probes SYSCOLUMNS for FREIGHT; if present it logs SKIPPED
instead of re-adding. Data survives the rebuild (order 1 total still 225.00).
INSERT INTO <alias> (col-list) VALUES (…) fails to resolve the alias ("no such
table"). The driver asserts the bug so it stays reproducible, then routes around it with the bare-VALUES
form (INSERT INTO OORD VALUES (…)), which works.CREATE TYPE MONEY_T AS DECIMAL(11,2); ALTER COLUMN UNITPRC SET DATA TYPE DECIMAL(11,2)
and SET DEFAULT 0.01; RENAME COLUMN SHIPZIP TO SHIPPOSTAL; ADD CONSTRAINT
UQ_PDESC UNIQUE (PDESC); and a column-level GRANT UPDATE (UNITPRC), REFERENCES (PRODNO).
Prices are unchanged by the widening; a fresh insert picks up the new default (0.01); the old column name is
gone after the rename; the UNIQUE is enforced. Pre-flight probes SYSTYPES for
MONEY_T.
The heaviest version: create ORD_AUDIT (identity PK); CREATE OR REPLACE TRIGGER
TR_ORD_STAT (AFTER UPDATE OF OSTAT, WHEN status changes — a same-status update is suppressed);
DROP CONSTRAINT CK_FREIGHT then ADD CONSTRAINT CK_FREIGHT2 CHECK (FREIGHT ≥ 0 AND
FREIGHT < 10000); add the deferred ORDER_LINE FKs to ORDERS/PRODUCT (now safe,
see C.3); redeploy the view to stop referencing SHIPPOSTAL, then DROP COLUMN SHIPPOSTAL; and
create + RENAME TABLE a staging table. The driver then exercises TRUNCATE TABLE
and TRUNCATE TABLE … RESTART IDENTITY (identity resets to 1).
ALTER TABLE ADD CONSTRAINT is genuinely enforced (a bad insert is rejected)
but does not appear in any catalog view (SYSCST/SYSKEYCST/SYSREFCST) — asserted honestly,
not masked. And native-table UNIQUE/CHECK constraints are a documented non-cataloged deviation
(buildCatalogs()'s own comment), so V5's guard uses MIGRATION_LOG as its
idempotency source of truth rather than probing SYSCST.V5_BAD attempts ADD CONSTRAINT CK_QTY_POS CHECK (QTY > 0), which the pre-existing
zero-quantity seed row (ORDNO 1, LINENO 3) violates on re-validation. The driver took a SAVEPOINT
SP_V5 first; the ADD fails; it does ROLLBACK TO SAVEPOINT SP_V5 and calls
SP_LOG_MIGRATION_FAILED. Afterward the schema (same column count) and data (same row count, the
bad row exactly as it was) are byte-for-byte unchanged, no CK_QTY_POS is left behind,
CURRENT_VER is still 4, and the session still transacts normally. V5_FIXED is the real
roll-forward: DELETE FROM ORDER_LINE WHERE QTY ≤ 0, then the same ADD CONSTRAINT
succeeds; CURRENT_VER→5.
V5-BAD proof of clean rollback:
schema column count = unchanged
ORDER_LINE row count = unchanged, bad row still QTY=0
MIGRATION_LOG = one FAILED row (V5_BAD_QTY_CHECK)
CURRENT_VER = 4 (did NOT advance)
Retires the superseded staging objects: DROP TABLE IF EXISTS ORD_STAGE RESTRICT,
DROP ALIAS IF EXISTS OORD, and REVOKE UPDATE (UNITPRC) ON PRODUCT FROM PUBLIC. The
base ORDERS table (aliased earlier) is untouched by the alias drop; the column-level UPDATE
grant is gone while the REFERENCES grant (not revoked) remains. DROP … IF EXISTS makes
the whole version a clean no-op on re-run.
DEPSYS) must exist before any version is
logged — the runner procedures write MIGRATION_LOG and mint from DEPLOY_SEQ.ALTER TABLE on that referenced table
fails with a raw "FOREIGN KEY constraint failed", even when unrelated to the FK. V2/V3 do several such
rebuild ALTERs on ORDERS/PRODUCT, so ORDER_LINE's FKs are deferred to V4, after those
tables' shape has stabilized — a realistic migration pattern that also routes around the gap.V_OPEN_ORDERS to
stop projecting SHIPPOSTAL before the DROP COLUMN, or the drop would
break the dependent view.These are the deploy platform's own objects, all in schema DEPSYS, grounded in
src/deploy_engine.sql. They are the tool's bookkeeping — distinct from the managed
application schema ORDDB (section C), which is data the tool operates on.
| Field | Type | Meaning |
|---|---|---|
| LOGID | INT | Synthetic log id (PK), minted from DEPLOY_SEQ. |
| VERNO | INT | Migration version number (monotonic). |
| MIGNAME | VARCHAR(40) | Short migration-step name (e.g. V4_AUDIT_FK_AND_STAGING). |
| STATUS | CHAR(8) | OK applied / SKIPPED already applied / FAILED rolled back. |
| APPLIEDTS | TIMESTAMP | When (DEFAULT CURRENT_TIMESTAMP — one-word spelling, see below). |
| DETAIL | VARCHAR(200) | Free-text detail written by the runner. |
DEFAULT uses the one-word
CURRENT_TIMESTAMP spelling deliberately, not the two-word CURRENT TIMESTAMP form:
a confirmed platform bug makes the two-word spelling break CREATE TABLE/ALTER TABLE
DEFAULT clauses with a raw SQLite syntax error. Both are equally valid Db2-for-i SQL, so this
is a faithful workaround (the same spelling is used in ORD_AUDIT.EVTTS).| Field | Type | Meaning |
|---|---|---|
| CFGKEY | CHAR(20) | Config key (PK), e.g. DEPLOY_ENV. |
| CFGVAL | VARCHAR(60) | Config value, e.g. PRODUCTION. |
Written only through SP_APPLY_MIGRATION's idempotent MERGE upsert.
A re-applied key returns SKIPPED and does not overwrite the existing value.
CREATE SEQUENCE DEPSYS.DEPLOY_SEQ START WITH 1 INCREMENT BY 1 — the generator every
runner draws LOGID from via NEXT VALUE FOR, so all MIGRATION_LOG rows
share one monotonic id sequence.
CREATE VARIABLE DEPSYS.CURRENT_VER INTEGER DEFAULT 0 — the tool's "highest applied
version" register. Read as VALUES DEPSYS.CURRENT_VER and set as SET DEPSYS.CURRENT_VER =
<n> from top-level SQL only; it is unreachable from inside a routine body
(SQLPL-PLAT-DEP-03), so the driver owns advancing it (section B.1).
CREATE TYPE DEPSYS.VERNO_T AS INTEGER — a distinct type used by the deploy tool's own
version-number bookkeeping (and available to the managed schema). Demonstrates the CREATE TYPE ... AS
<source> DevOps surface; the managed schema also ships its own distinct type
ORDDB.MONEY_T in V3.
The tool operates on a small order-management schema, evolved across V1–V6:
CUSTOMER (CUSTNO PK, CNAME, CSTAT, +SHIPZIP→SHIPPOSTAL→dropped),
PRODUCT (PRODNO PK, PDESC, UNITPRC widened to DECIMAL(11,2)),
ORDERS (ORDNO identity PK, CUSTNO FK, ODATE, OSTAT DEFAULT 'O', +FREIGHT),
ORDER_LINE (ORDNO+LINENO PK, PRODNO, QTY; deferred FKs added in V4),
plus ORD_AUDIT, staging ORD_STAGE, view V_OPEN_ORDERS, alias
OORD, function FN_ORDER_TOTAL, procedure SP_ORDER_TOTAL, sequence
ORDNO_HINT_SEQ, type MONEY_T, and trigger TR_ORD_STAT. These are the
subject of the deploy, not part of the engine.
src/deploy_engine.sql as a batch. Confirm VALUES DEPSYS.CURRENT_VER = 0.SP_LOG_MIGRATION_OK and move on
— the step is already applied.SAVEPOINT (for any risky/structural version).ORDDB.CALL DEPSYS.SP_LOG_MIGRATION_OK(verno, migname, detail, ?); read the OUT
status; when it is OK, issue SET DEPSYS.CURRENT_VER = verno.SP_APPLY_MIGRATION(verno, name, cfgkey, cfgval, ?)
directly — it does the idempotency check, the MERGE, and the log in one call.Pre-checks: the deploy job's schema path can resolve both DEPSYS and
ORDDB; the engine exists (CURRENT_VER answers). Post-checks (whole deploy):
SELECT MIN(VERNO), MAX(VERNO), COUNT(DISTINCT VERNO) FROM DEPSYS.MIGRATION_LOG WHERE STATUS='OK'; -- expect 0|6|7 (clean 0..6, no gaps) VALUES DEPSYS.CURRENT_VER; -- expect the final version (6)
The entire pipeline is safe to re-run end-to-end (e.g. a CI re-trigger). On a second pass:
CREATE OR REPLACE object (view,
sequence, function, procedure, trigger, alias) can be re-issued as a clean no-op-shaped redeploy. Note
a CREATE OR REPLACE SEQUENCE re-seeds its START WITH value.MIGRATION_LOG by exactly five SKIPPED rows and no OK rows.deploy_engine.sql or a version's
CREATE SCHEMA/CREATE TABLE is expected to fail on a second pass — that
is the DDL-vs-OR REPLACE distinction. A real re-deploy re-runs only a version's
replaceable objects plus its guarded apply-function, which is exactly what the driver does.SP_APPLY_MIGRATION for an
already-applied key returns SKIPPED and does not overwrite the value (the MERGE never runs because the
log check short-circuits first).Verify no drift after a re-run: order totals still 225.00 / 250.00, row
counts stable, and CURRENT_VER unchanged at 6.
A structural migration that fails must leave the target exactly as it was. The pattern — proven by
the deliberately bad V5 — is SAVEPOINT before, ROLLBACK TO SAVEPOINT on
failure, and log FAILED (never OK):
| Situation | Behaviour | Action |
|---|---|---|
| A version's DDL batch fails | The driver holds a SAVEPOINT taken before it. | ROLLBACK TO SAVEPOINT; CALL SP_LOG_MIGRATION_FAILED. Schema & data are byte-for-byte unchanged; CURRENT_VER does not advance. |
| Re-run a version already applied OK | Pre-flight probe / log check sees it. | Returns SKIPPED; logs a SKIPPED row; no re-apply. Idempotent. |
| Re-run the whole pipeline after a CI re-trigger | Replaceable objects re-CREATE OR REPLACE; structural steps SKIP. | Safe; the log grows only SKIPPED rows and data does not drift. |
| A CHECK/constraint the existing data violates | The ADD CONSTRAINT fails on re-validation (like V5_BAD's QTY>0). | Roll back, clean the offending data (a real fix migration), then re-add the constraint (V5_FIXED). Log FAILED then OK. |
| Bad migration corrupted nothing, need proof | Nothing changed. | Re-query column count, row count, the specific bad row, and SYSCST for the would-be constraint — all unchanged / absent. Session still transacts. |
Verification after every migration (a real deploy tool's confirm step): query the DB2 catalog to
confirm the intended shape landed — new tables in SYSTABLES, new columns in
SYSCOLUMNS, sequences at their START in SYSSEQUENCES, types in
SYSTYPES, and grants in SYSTABAUTH/SYSCOLAUTH. See F.3 for the exact
queries and the two honest catalog gaps (native-table FKs and native UNIQUE/CHECK are enforced but not
cataloged).
MIGRATION_LOG with a timestamp and detail, any deploy's history is fully reconstructable after
the fact, including the single FAILED V5 attempt that was rolled back.The complete deploy-engine surface, from src/deploy_engine.sql, plus the DevOps SQL
exercised by the versioned migrations (src/migrations.mjs) and verified by the driver
(test/dep_daily.mjs). Engine objects are in schema DEPSYS.
SELECT COUNT(*) for an existing OK row with the same (VERNO, MIGNAME). If found →
P_STATUS='SKIPPED' and insert a SKIPPED row; else P_STATUS='OK' and insert an
OK row, LOGID from NEXT VALUE FOR DEPSYS.DEPLOY_SEQ. Deliberately does not touch
CURRENT_VER (SQLPL-PLAT-DEP-03 — the caller advances it top-level).MIGRATION_LOG row with STATUS='FAILED'. Called by
the driver after its own ROLLBACK TO SAVEPOINT, so the log records a failure that
left no schema change.MIGRATION_LOG; if already OK → SKIPPED (log only). Otherwise a
MERGE INTO DEPSYS.DEPLOY_CFG upsert (update-if-matched / insert-if-not) of the key/value,
then P_STATUS='OK' and an OK log row. Structural versions' bookkeeping is instead split
across the two LOG procedures because SQL PL cannot portably EXECUTE IMMEDIATE arbitrary
DDL (see A.2).The versioned migrations deliberately exercise the full schema-evolution surface a real deploy tool must
emit. Each is real DDL/DML from src/migrations.mjs, verified in the catalog by the driver.
| Surface | Where | Example |
|---|---|---|
CREATE SCHEMA | V1, engine | CREATE SCHEMA ORDDB; / CREATE SCHEMA DEPSYS; |
CREATE TABLE (PK/FK/CHECK/IDENTITY/DEFAULT) | V1 | ORDNO INT GENERATED ALWAYS AS IDENTITY, OSTAT ... DEFAULT 'O', inline FK to CUSTOMER. |
CREATE [OR REPLACE] VIEW | V1/V2/V4 | CREATE OR REPLACE VIEW ORDDB.V_OPEN_ORDERS AS ... (redeployed as the schema evolves). |
CREATE [OR REPLACE] SEQUENCE | V1, engine | CREATE OR REPLACE SEQUENCE ORDDB.ORDNO_HINT_SEQ START WITH 1000 INCREMENT BY 1; |
CREATE [OR REPLACE] FUNCTION / PROCEDURE | V1, engine | FN_ORDER_TOTAL, SP_ORDER_TOTAL; the three DEPSYS procedures. |
CREATE [OR REPLACE] TRIGGER | V4 | TR_ORD_STAT AFTER UPDATE OF OSTAT ... WHEN (N.OSTAT <> O.OSTAT). |
CREATE ALIAS | V2 | CREATE OR REPLACE ALIAS ORDDB.OORD FOR ORDDB.ORDERS; |
CREATE VARIABLE (global) | engine | CREATE VARIABLE DEPSYS.CURRENT_VER INTEGER DEFAULT 0; |
CREATE TYPE (distinct) | V3, engine | CREATE TYPE ORDDB.MONEY_T AS DECIMAL(11,2); / DEPSYS.VERNO_T AS INTEGER. |
| Surface | Where | Example |
|---|---|---|
ADD COLUMN (with/without DEFAULT) | V2 | ADD COLUMN FREIGHT DECIMAL(7,2) NOT NULL DEFAULT 0 (existing rows backfill 0). |
ADD CONSTRAINT CHECK | V2/V4/V5 | CK_FREIGHT CHECK (FREIGHT >= 0); CK_QTY_POS CHECK (QTY > 0). |
ADD CONSTRAINT UNIQUE | V3 | UQ_PDESC UNIQUE (PDESC). |
ADD CONSTRAINT FOREIGN KEY | V4 | FK_OL_ORD ... REFERENCES ORDDB.ORDERS (ORDNO) (deferred; SQLPL-PLAT-DEP-02). |
ALTER COLUMN SET DATA TYPE | V3 | ALTER COLUMN UNITPRC SET DATA TYPE DECIMAL(11,2) (data survives). |
ALTER COLUMN SET DEFAULT | V3 | ALTER COLUMN UNITPRC SET DEFAULT 0.01. |
RENAME COLUMN | V3 | RENAME COLUMN SHIPZIP TO SHIPPOSTAL. |
DROP CONSTRAINT | V4 | DROP CONSTRAINT CK_FREIGHT (then add the stronger CK_FREIGHT2). |
DROP COLUMN | V4 | DROP COLUMN SHIPPOSTAL (view redeployed first). |
RENAME TABLE | V4 | RENAME TABLE ORDDB.ORD_STAGE_TMP TO ORD_STAGE. |
| Surface | Where | Example |
|---|---|---|
GRANT (table / EXECUTE) | V1 | GRANT SELECT, INSERT ON ORDDB.ORDERS TO PUBLIC; GRANT EXECUTE ON PROCEDURE .... |
GRANT ... ON SCHEMA ... WITH GRANT OPTION | V1 | GRANT CREATEIN, ALTERIN, DROPIN ON SCHEMA ORDDB TO QPGMR WITH GRANT OPTION. |
Column-level GRANT | V3 | GRANT UPDATE (UNITPRC), REFERENCES (PRODNO) ON ORDDB.PRODUCT TO PUBLIC. |
REVOKE (column-level) | V6 | REVOKE UPDATE (UNITPRC) ON ORDDB.PRODUCT FROM PUBLIC. |
TRUNCATE TABLE | V4 | TRUNCATE TABLE ORDDB.ORD_STAGE (reset staging between deploys). |
TRUNCATE ... RESTART IDENTITY | V4 | TRUNCATE TABLE ORDDB.ORD_AUDIT RESTART IDENTITY (next AUDID = 1). |
DROP ... IF EXISTS (RESTRICT) | V6 | DROP TABLE IF EXISTS ORDDB.ORD_STAGE RESTRICT; DROP ALIAS IF EXISTS ORDDB.OORD. |
MERGE (idempotent upsert) | engine | MERGE INTO DEPSYS.DEPLOY_CFG ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT. |
SAVEPOINT / ROLLBACK TO SAVEPOINT | driver (V5) | SAVEPOINT SP_V5 ... ROLLBACK TO SAVEPOINT SP_V5. |
After each migration the driver confirms the deployed shape against the DB2 catalog. The real queries:
-- base tables present (V1) SELECT COUNT(*) FROM QSYS2.SYSTABLES WHERE TABLE_SCHEMA='ORDDB' AND TABLE_TYPE='T' AND TABLE_NAME IN ('CUSTOMER','PRODUCT','ORDERS','ORDER_LINE'); -- 4 -- a column default (V1) SELECT COLUMN_DEFAULT FROM QSYS2.SYSCOLUMNS WHERE TABLE_SCHEMA='ORDDB' AND TABLE_NAME='ORDERS' AND COLUMN_NAME='OSTAT'; -- sequence at its START (V1) SELECT START, NEXT_AVAILABLE_VALUE FROM QSYS2.SYSSEQUENCES WHERE SEQUENCE_SCHEMA='ORDDB' AND SEQUENCE_NAME='ORDNO_HINT_SEQ'; -- 1000|1000 -- grants (V1): table privileges to PUBLIC SELECT PRIVILEGE_TYPE FROM QSYS2.SYSTABAUTH WHERE TABLE_SCHEMA='ORDDB' AND TABLE_NAME='ORDERS' AND GRANTEE='*PUBLIC'; -- INSERT;SELECT -- alias (V2), distinct type (V3), column grants (V3) SELECT TABLE_TYPE FROM QSYS2.SYSTABLES WHERE ... TABLE_NAME='OORD'; -- A SELECT SOURCE_TYPE FROM QSYS2.SYSTYPES WHERE USER_DEFINED_TYPE_NAME='MONEY_T'; -- DECIMAL... SELECT COUNT(*) FROM QSYS2.SYSCOLAUTH WHERE TABLE_NAME='PRODUCT' AND COLUMN_NAME='UNITPRC' AND PRIVILEGE_TYPE='UPDATE'; -- 1
The engine's own registers are read with VALUES DEPSYS.CURRENT_VER and
SELECT ... FROM DEPSYS.MIGRATION_LOG (grouped by STATUS to confirm the OK/SKIPPED/
FAILED counts per version).
ALTER TABLE ADD CONSTRAINT is genuinely enforced but never appears in
SYSCST/SYSKEYCST/SYSREFCST (SQLPL-PLAT-DEP-05); and native-table UNIQUE/CHECK
constraints are a documented non-cataloged deviation. Where the catalog cannot confirm a constraint, the
driver confirms enforcement directly (a violating insert is rejected) and uses MIGRATION_LOG as
the idempotency source of truth.SELECT COUNT(*) ... WHERE VERNO=? AND MIGNAME=? AND
STATUS='OK' is the canonical "already applied?" check; a positive count yields SKIPPED.SYSCOLUMNS? type in SYSTYPES? table in SYSTABLES?) so
a re-run does not attempt to re-add an existing object — a real deploy tool's pre-flight.CREATE OR REPLACE as clean no-op-shaped redeploys (a sequence re-seeds its START).SP_APPLY_MIGRATION's config write is a MERGE, idempotent even if the
log check were bypassed.SAVEPOINT around a risky version and
ROLLBACK TO SAVEPOINT on failure, then logs FAILED — the schema/data are byte-for-byte
unchanged and the session keeps working.CURRENT_VER is advanced top-level by the driver only
on OK, and only forward (never regressed on a re-run).DEP/i's engine procedures do not raise custom application SQLSTATEs (unlike a business app such as
LOANSVC/i): a failed migration surfaces the engine's native SQL error from the failing DDL (e.g. a CHECK
re-validation failure, a duplicate-object error, a foreign-key violation), which the driver captures from the
batch result and routes to SP_LOG_MIGRATION_FAILED. The outcome is carried in the
P_STATUS OUT parameter (OK/SKIPPED) and in
MIGRATION_LOG.STATUS. The confirmed platform findings the source works around or asserts:
| Finding | Effect | Handling in DEP/i |
|---|---|---|
| SQLPL-PLAT-DEP-01 | Two-word CURRENT TIMESTAMP breaks DEFAULT clauses. | Use the one-word CURRENT_TIMESTAMP spelling (MIGRATION_LOG, ORD_AUDIT). |
| SQLPL-PLAT-DEP-02 | A live FK blocks rebuild ALTERs on the referenced table. | Defer ORDER_LINE's FKs to V4, after ORDERS/PRODUCT rebuilds finish. |
| SQLPL-PLAT-DEP-03 | Global CREATE VARIABLE unreachable from inside a routine body. | Runner does not touch CURRENT_VER; the driver advances it top-level. |
| SQLPL-PLAT-DEP-04 | INSERT INTO alias (col-list) VALUES fails ("no such table"). | Assert the bug, then write through the alias with the bare-VALUES form. |
| SQLPL-PLAT-DEP-05 | Native-table FK enforced but not visible in SYSCST/SYSKEYCST/SYSREFCST. | Assert the gap; confirm enforcement directly via a rejected insert. |
| (documented deviation) | Native-table UNIQUE/CHECK constraints not cataloged. | Use MIGRATION_LOG as the idempotency source of truth for V5's guard. |
CREATE ALIAS OORD FOR ORDERS). Reads and writes route through
to the base table; DEP/i uses one to demonstrate the alias DevOps surface (and a confirmed alias
insert gap, SQLPL-PLAT-DEP-04).SYSTABLES, SYSCOLUMNS,
SYSSEQUENCES, SYSVARIABLES, SYSTYPES, SYSTABAUTH,
SYSCOLAUTH) DEP/i queries to verify a migration deployed the intended shape.CREATE VARIABLE holding the highest applied version. Advanced top-level by the
driver (not from inside a routine, per SQLPL-PLAT-DEP-03), only forward, only on OK.MONEY_T AS DECIMAL(11,2),
VERNO_T AS INTEGER), giving a domain a distinct name.CREATE OR REPLACE, MERGE, DROP IF EXISTS) and
end-to-end (a full pipeline re-run only adds SKIPPED rows).SP_APPLY_MIGRATION's config
write idempotent belt-and-braces.DEPLOY_SEQ, ORDNO_HINT_SEQ) drawn from via
NEXT VALUE FOR.EXECUTE IMMEDIATE
arbitrary DDL).