PAYSQL/i is a payroll / HR gross-to-net application: it computes each employee's gross
pay, applies pretax and posttax deductions, withholds federal tax through a graduated annual
bracket table, arrives at net pay, posts the pay run to a general-ledger distribution, and accrues
year-to-date totals. Unlike a classic RPG application, PAYSQL/i is pure SQL PL — the
entire domain (schema, functions, procedures, triggers) lives in DB2 for i and is exercised from
STRSQL, a batch CALL driver, or a thin SQLRPGLE caller that issues
EXEC SQL CALL. There is no 5250 subfile screen; the “online” surface is the
callable procedure interface itself. This manual is the reference for the operator who runs the
payroll cycle and for the developer who maintains the routines. It is grounded entirely in the
committed source (sqlpl-app-pay/src/schema.sql, routines.sql, and the
test/pay_battle.mjs, pay_rpg.mjs and pay_volume_sim.mjs
drivers).
PAYSQL/i runs a biweekly payroll from employee master through to a posted general-ledger distribution:
ANNSAL/26 per biweekly
period (prorated for a mid-period termination); hourly employees earn
HRATE×80 standard biweekly hours.tax = base + rate × excess), then de-annualized (÷26) back to a
per-period withholding.gross − pretax − fedtax − posttax; a
would-be-negative net is vetoed, never posted.PAYSQL/i has a deliberately different shape from the RPG-orchestrated applications in this estate.
There is no RPG business logic and no DDS screen. Every rule — gross, proration, the
bracket walk, the deduction cursor, net, posting, GL distribution, YTD accrual — is implemented
as DB2 for i SQL PL: compound scalar functions, stored procedures (some nesting
CALLs into others), and table triggers. The schema lives in library PAYSQL.
schema.sql creates the tables, the seeded bracket
and deduction-code catalogs, and the payslip view; routines.sql creates 3 functions,
5 procedures and 3 triggers that carry all the logic. Integrity and accrual are enforced by
triggers, not by the caller.STRSQL (CALL PAYSQL.PY_RUN_POST(...)), a batch/JOBQ
CALL, and a compiled SQLRPGLE program (CALLPAY) that issues
EXEC SQL CALL. The RPG program is a proof of reach — it exists to show
the compound procedures are callable from compiled RPG and that OUT parameters and a
trigger-raised SIGNAL surface correctly as SQLCODE; it holds no payroll
math of its own.The benefit for operations: the payroll rules are one auditable place (the SQL-PL routines), and the same procedures answer identically from an operator's STRSQL session, a scheduled batch job, and any embedded-SQL caller.
CALLERS SQL-PL LOGIC (library PAYSQL) DATA + TRIGGERS
------- ---------------------------- ---------------
STRSQL ---CALL---> PY_RUN_POST(runid) PYEMP (master)
batch ---CALL---> | loops ACTIVE PYEMP PYBRK (brackets)
SQLRPGLE ---EXEC SQL CALL-> +--> PY_RUN_EMPLOYEE(run,emp) PYDEDC (ded codes)
(CALLPAY) +--> PY_COMPUTE_GROSS PYEMPDED (elections)
| FN_PRORATE/FN_ROUND2 |
| +--> PY_WITHHOLD v
| FN_TAXBRK/FN_ROUND2 PYRUNDT (payslip)
| INSERT PYRUNDT ------------> TR_PYRUNDT_VETO (net>=0)
| TR_PYRUNDT_ACCR --> PYACCUM (YTD)
+--> (GL pass) INSERT PYGL (both write PYAUD)
+--> UPDATE PYRUN RSTAT='P'
ADJUSTMENTS UPDATE PYSLIPV.NET --> TR_PYSLIPV_UPD (INSTEAD OF) --> PY_ADJUST_NET (net>=0) --> PYRUNDT + PYAUD
A single pay run flows: caller issues CALL PY_RUN_POST('RUN001', ...) → the
procedure loops every ACTIVE employee, nesting PY_COMPUTE_GROSS then
PY_WITHHOLD and inserting one PYRUNDT payslip row → that INSERT fires the
BEFORE veto (net≥0) and the AFTER accrual (YTD into PYACCUM) → a second pass writes
the balanced PYGL distribution and flips the run to POSTED.
| Object | Type | Role |
|---|---|---|
| PYEMP | PF | Employee master (pay type, salary/rate, filing status, exemptions). |
| PYBRK | PF | Graduated annual tax brackets per filing status. |
| PYDEDC | PF | Deduction-code catalog (pretax/posttax, flat/percent). |
| PYEMPDED | PF | Employee deduction elections. |
| PYRUN | PF | Pay-run header (period, pay date, status). |
| PYRUNDT | PF | Pay-run detail — one payslip per employee per run. |
| PYACCUM | PF | YTD accumulator (trigger-maintained only). |
| PYGL | PF | GL distribution (one row per run/employee/account). |
| PYAUD | PF | Generic trigger audit log. |
| PYSLIPV | View | Payslip view over PYRUNDT (INSTEAD OF UPDATE demo). |
| FN_* (3) | SQL functions | Cent rounding, graduated tax, proration factor. |
| PY_* (5) | SQL procedures | Gross / withhold / per-employee / run-post / net-adjust. |
| TR_* (3) | Triggers | Net veto, YTD accrual, view-update reroute. |
| CALLPAY | SQLRPGLE | Reach-proof caller (EXEC SQL CALL); not part of the cycle. |
The full catalogue is 3 functions + 5 procedures + 3 triggers, over 9 PFs and 1 view, reachable from
STRSQL, batch, or the CALLPAY SQLRPGLE program. Sections D and F expand each.
Honest statement: PAYSQL/i has no interactive 5250 subfile screen, no DDS display file, and
no menu program. It is a pure data-layer application. The operator surface is the callable
procedure interface, reached identically three ways — interactive SQL, a batch
CALL, and an SQLRPGLE caller. Before invoking anything, the job's library list must include
PAYSQL; the tested jobs run with LIBL = QSYS QGPL PAYSQL QTEMP and
CURLIB = PAYSQL.
| To do this | Type (STRSQL) / submit (batch) |
|---|---|
| Compute + post a whole pay run | CALL PAYSQL.PY_RUN_POST('RUN001', ?, ?, ?, ?) |
| Compute one employee only (no post) | CALL PAYSQL.PY_RUN_EMPLOYEE('RUN001','E00001', ?) |
| Inspect a computed gross / withholding | CALL PAYSQL.PY_COMPUTE_GROSS(...) / PY_WITHHOLD(...) |
| Adjust a posted payslip's net (sanctioned path) | UPDATE PAYSQL.PYSLIPV SET NET=<n> WHERE ... (reroutes to PY_ADJUST_NET) |
| Look at results / reconcile | SELECT ... FROM PAYSQL.PYRUNDT / PYGL / PYACCUM |
| Same, from compiled RPG | CALL PAYSQL/CALLPAY (issues the EXEC SQL CALLs) |
The batch idiom. PY_RUN_POST is the one-call payroll cycle: it takes the run id
IN and four counters OUT (COMPUTED, SKIPPED, VETOED,
POSTED). A scheduled submission is therefore a bare procedure call against a run id whose
header row already exists in PYRUN:
-- interactive / STRSQL CALL PAYSQL.PY_RUN_POST('RUN001', ?, ?, ?, ?); -- batch: wrap the CALL in a program/driver and SBMJOB it SBMJOB CMD(CALL PGM(PAYSQL/CALLPAY)) JOB(PAYRUN)
CALLPAY is the reach-proof driver (test/pay_rpg.mjs). It declares packed
host variables, then in sequence: (1) EXEC SQL CALL PY_COMPUTE_GROSS and
PY_WITHHOLD to round-trip IN/OUT values through the nested compound procedures; (2)
EXEC SQL CALL PY_RUN_POST and reads back the four OUT counters; (3)
EXEC SQL CALL PY_ADJUST_NET with a negative amount to confirm the trigger/procedure
SIGNAL reaches RPG as a negative SQLCODE (−438, the unhandled-SIGNAL
convention), then a valid adjustment to confirm normal success. Each step DSPLYs
value/%char(sqlcode).
SQLCODE / the OUT counters after each call.PAYSQL/i does not model a four-eyes maker–checker workflow: a pay run posts in one call and a sanctioned net adjustment applies immediately. The control posture is data-layer guards + automatic audit + idempotency:
TR_PYRUNDT_VETO (BEFORE INSERT on PYRUNDT)
SIGNALs SQLSTATE 75014 on any negative net — even an ad-hoc INSERT
that tries to bypass PY_WITHHOLD is rejected. A vetoed employee is caught by
PY_RUN_EMPLOYEE's UNDO handler (counted VETOED), and the run continues.TR_PYSLIPV_UPD (INSTEAD OF UPDATE on PYSLIPV)
reroutes a net change to PY_ADJUST_NET, which re-validates net≥0 itself
(75013) before touching the base table. A direct UPDATE of a payslip net cannot bypass
the guard.PYACCUM) is written only by
TR_PYRUNDT_ACCR (AFTER INSERT), never by the posting procedure — the books cannot
drift from the payslips because the accrual is a side effect of the payslip landing.OP='ACCRUE', one per posted employee) and every
net adjustment (OP='ADJUST') is written automatically by the triggers/procedure, giving
an after-the-fact trail.FN_TAXBRK, 75010), a zero-length period (FN_PRORATE, 75011) and an
unknown pay type (PY_COMPUTE_GROSS, 75012) all SIGNAL rather than tax at
zero or divide by zero.In sum, the control model is guarded data + trigger accrual + audit, all enforced in the SQL-PL layer, rather than a segregation-of-duties approval flow.
PAYSQL/i's “cycle” is a single per-period event: for each pay period, seed a
PYRUN header, then call PY_RUN_POST once. That one call computes every active
employee's payslip, posts the GL, accrues YTD, and closes the run. There is no separate daily / weekly /
monthly split — the natural cadence is one run per pay period (the tests use consecutive
biweekly periods). The section below documents the full routine set the cycle drives.
| Routine | Kind | Purpose | Calls / fires | In / Out |
|---|---|---|---|---|
| FN_ROUND2 | function | Round a DECIMAL to the cent, half-up. | — | (V) → DECIMAL(11,2) |
| FN_TAXBRK | function | Graduated federal tax on annual taxable income. | cursor over PYBRK; FN_ROUND2 | (FSTAT, TAXABLE) → DECIMAL(11,2) |
| FN_PRORATE | function | Partial-period proration factor, clamped [0,1]. | — | (WORKED, PERIOD) → DECIMAL(9,6) |
| PY_COMPUTE_GROSS | procedure | Biweekly gross (salaried ÷26, prorated on term; hourly ×80). | FN_PRORATE, FN_ROUND2 | IN run,emp; OUT gross |
| PY_WITHHOLD | procedure | Deductions → taxable → annualize → bracket → net. | cursor PYEMPDED⨯PYDEDC; FN_TAXBRK, FN_ROUND2 | IN emp,gross; OUT preded,fedtax,postded,net |
| PY_RUN_EMPLOYEE | procedure | One employee end-to-end; idempotent; UNDO on veto. | PY_COMPUTE_GROSS, PY_WITHHOLD; INSERT PYRUNDT | IN run,emp; OUT rc |
| PY_RUN_POST | procedure | Loop ACTIVE employees, post GL, flip run to POSTED. | PY_RUN_EMPLOYEE; INSERT PYGL; UPDATE PYRUN | IN run; OUT computed,skipped,vetoed,posted |
| PY_ADJUST_NET | procedure | Sanctioned net adjustment (net≥0), audited. | UPDATE PYRUNDT; INSERT PYAUD | IN run,emp,newnet |
| TR_PYRUNDT_VETO | trigger | BEFORE INSERT: veto negative net (75014). | on PYRUNDT | — |
| TR_PYRUNDT_ACCR | trigger | AFTER INSERT: UPSERT YTD, write PYAUD. | on PYRUNDT → PYACCUM, PYAUD | — |
| TR_PYSLIPV_UPD | trigger | INSTEAD OF UPDATE: reroute net change to PY_ADJUST_NET. | on PYSLIPV → PY_ADJUST_NET | — |
Reads the employee and the run's period. Salaried (PTYPE='S'):
gross = ROUND(ANNSAL/26.0); if the employee terminated inside the period
(TERMDT within [PSTART,PEND]) it prorates by a simple 14-day model
— WORKEDDAYS = day-of-month(TERMDT) − day-of-month(PSTART) + 1 over 14,
through FN_PRORATE. Hourly (PTYPE='H'):
gross = ROUND(HRATE×80), no proration. An unknown pay type SIGNALs 75012.
Cursor-walks the employee's elected deductions (PYEMPDED joined to PYDEDC),
summing pretax (DKIND='B') and posttax ('A') totals, each a flat amount
(DBASIS='F') or a percent of gross ('P'). Then:
taxable = gross − pretax (floored at 0); annualize
annual = taxable×26 − EXEMPT×2000 (floored at 0); look up
annualtax = FN_TAXBRK(FSTAT, annual); de-annualize
fedtax = ROUND(annualtax/26.0); finally
net = ROUND(gross − pretax − fedtax − posttax).
Worked example — E00001 (ann 78000, single, 1 exemption; 401K 5% + MED 75 pretax + UNIO 20 posttax):
gross = 78000.00 / 26 = 3000.00
preded = round(3000*0.05) + 75.00 = 150.00 + 75.00 = 225.00
taxable = 3000.00 - 225.00 = 2775.00
annual = 2775*26 - 1*2000 = 72150.00 - 2000 = 70150.00
bracket S: floor 44725 rate .22 base 5147.00
annualtax = 5147.00 + .22*(70150-44725) = 10740.50
fedtax = round(10740.50 / 26) = 413.10
net = 3000 - 225 - 413.10 - 20 = 2341.90
PY_RUN_EMPLOYEE is BEGIN ATOMIC: if a payslip already exists for
(RUNID,EMPID) it returns SKIPPED; otherwise it nests the two computes and
INSERTs the PYRUNDT row (returning COMPUTED). An UNDO handler on
SQLEXCEPTION catches a trigger veto, unwinds that one INSERT, and returns
VETOED — the run is not aborted. PY_RUN_POST loops every
ESTAT='A' employee through it, tallying the four counters; then, in a second pass over the
just-computed (DSTAT='C') rows, writes the four-line GL distribution per employee, flips
each row to DSTAT='P', and finally sets PYRUN.RSTAT='P'. If the run is already
posted, it returns immediately with all counters 0.
Expected OUT (battle RUN001: 5 salaried + 20 hourly, 1 net-negative vetoed):
COMPUTED = 24 SKIPPED = 0 VETOED = 1 POSTED = 24
+GROSS debit to wage-expense 700000,
−FEDTAX credit to tax-liability 210000, −(POSTDED+PREDED) credit to
benefits-payable 220000, −NET credit to cash 100000 — four lines that net to
0.00 per employee (the balance invariant the tests assert).PYRUN row (status 'O') must exist for the run
id before PY_RUN_POST is called — the procedure reads the period from it and, on
completion, flips it to 'P'.PY_RUN_POST on a posted run is a pure
no-op (early return); within a run, an already-present payslip is SKIPPED and GL rows
are written only for DSTAT='C' rows, which are flipped to 'P' in the same
pass. A re-run posts nothing new and does not double the YTD accumulator.PYRUNDT(RUNID,EMPID) rows; the YTD accumulator sums across runs in the same year via
the AFTER-insert trigger's UPSERT.All files are in library PAYSQL, grounded in schema.sql. Dates are stored as
INT in YYYYMMDD form (or YYYY for the accumulator year); money is
DECIMAL; bracket/deduction rates are DECIMAL proportions or percents.
| Field | Type | Meaning |
|---|---|---|
| EMPID | CHAR(6) | Employee number (PK), e.g. E00001. |
| ENAME | VARCHAR(30) | Employee name. |
| PTYPE | CHAR(1) | Pay type: S salary, H hourly. |
| ANNSAL | DECIMAL(11,2) | Annual salary (used when PTYPE=S). |
| HRATE | DECIMAL(9,4) | Hourly rate (used when PTYPE=H). |
| FSTAT | CHAR(1) | Filing status: S single, M married (bracket-table key). |
| EXEMPT | INT | Exemption count; each reduces annual taxable by $2000. |
| HIREDT | INT | Hire date (YYYYMMDD). |
| TERMDT | INT | Termination date (YYYYMMDD); 0 = not terminated. Drives proration. |
| ESTAT | CHAR(1) | Employee status: A active, T terminated. Only A employees are run. |
| Field | Type | Meaning |
|---|---|---|
| FSTAT | CHAR(1) | Filing status this bracket applies to (part of PK). |
| BFLOOR | DECIMAL(11,2) | Lowest annual income (inclusive) this bracket covers (part of PK). |
| BRATE | DECIMAL(6,4) | Marginal rate as a proportion (0.2200 = 22%). |
| BBASE | DECIMAL(11,2) | Cumulative tax already owed at the floor. |
Tax on income I in [BFLOOR, next BFLOOR) is
BBASE + BRATE×(I−BFLOOR). Seeded (illustrative, not a real-year IRS table):
S floors 0/11000/44725/95375/182100 at 10/12/22/24/32%; M floors
0/22000/89450/190750/364200 at the same rates.
| Field | Type | Meaning |
|---|---|---|
| DEDCODE | CHAR(4) | Deduction code (PK), e.g. 401K, MED . |
| DDESC | VARCHAR(20) | Description. |
| DKIND | CHAR(1) | B pretax (reduces taxable), A posttax. |
| DBASIS | CHAR(1) | F flat amount, P percent of gross. |
| DVAL | DECIMAL(9,4) | Amount (F) or percent 0–100 (P). |
Seeded: 401K (B/P 5%), MED (B/F $75), UNIO (A/F $20), GARN (A/F $500).
| Field | Type | Meaning |
|---|---|---|
| EMPID | CHAR(6) | Employee (part of PK). |
| DEDCODE | CHAR(4) | Elected deduction code (part of PK). |
The withhold cursor JOINs this to PYDEDC; an election whose code no longer exists in PYDEDC simply drops out of the join (no run abort).
| Field | Type | Meaning |
|---|---|---|
| RUNID | CHAR(6) | Run identifier (PK), e.g. RUN001. |
| PSTART / PEND | INT | Period start / end (YYYYMMDD). PEND's year drives the YTD year. |
| PAYDT | INT | Pay date (YYYYMMDD). |
| RSTAT | CHAR(1) | O open, P posted (set by PY_RUN_POST). |
| Field | Type | Meaning |
|---|---|---|
| RUNID / EMPID | CHAR(6)/CHAR(6) | Run + employee (PK) — one payslip per employee per run. |
| GROSS | DECIMAL(11,2) | Computed gross pay. |
| PREDED | DECIMAL(11,2) | Pretax deductions total. |
| TAXWAGE | DECIMAL(11,2) | Taxable wages (gross − pretax) this period. |
| FEDTAX | DECIMAL(11,2) | Federal withholding. |
| POSTDED | DECIMAL(11,2) | Posttax deductions total. |
| NET | DECIMAL(11,2) | Net pay. Vetoed if negative. |
| DSTAT | CHAR(1) | C computed, P posted (flipped during the GL pass). |
| Field | Type | Meaning |
|---|---|---|
| EMPID / PYEAR | CHAR(6)/INT | Employee + payroll year (PK). |
| YGROSS | DECIMAL(13,2) | Year-to-date gross. |
| YFEDTAX | DECIMAL(13,2) | Year-to-date federal tax. |
| YNET | DECIMAL(13,2) | Year-to-date net. |
Maintained only by TR_PYRUNDT_ACCR (AFTER INSERT on PYRUNDT), never
written directly by the posting procedure — the accrual is trigger-driven, not application
bookkeeping.
| Field | Type | Meaning |
|---|---|---|
| GSEQ | INT identity | GENERATED ALWAYS AS IDENTITY sequence. |
| RUNID / EMPID | CHAR(6)/CHAR(6) | Run + employee this GL line belongs to. |
| ACCTNO | CHAR(6) | GL account: 700000 wage expense, 210000 tax liability, 220000 benefits payable, 100000 cash. |
| GAMT | DECIMAL(11,2) | Signed amount: + debit, − credit. The four lines per employee net to 0.00. |
INTEGER PRIMARY KEY AUTOINCREMENT on its own, so the PK still exists; this is a deliberate
workaround for a rewrite regex bug, not a semantic change.| Field | Type | Meaning |
|---|---|---|
| ASEQ | INT identity | GENERATED ALWAYS AS IDENTITY sequence. |
| OP | CHAR(10) | Operation: ACCRUE (AFTER-insert accrual) or ADJUST (net adjustment). |
| RUNID / EMPID | CHAR(6)/CHAR(6) | Run + employee affected. |
| DETAIL | VARCHAR(60) | Human-readable detail written by the trigger/procedure. |
Exposes RUNID, EMPID, GROSS, FEDTAX, NET, DSTAT. Updatable only through
TR_PYSLIPV_UPD (INSTEAD OF UPDATE): a net change reroutes to PY_ADJUST_NET
(which re-validates net≥0); other columns are inert (the “view is otherwise inert”
contract).
PAYSQL
(LIBL = QSYS QGPL PAYSQL QTEMP, CURLIB = PAYSQL).PYEMP row with ESTAT='A' before it is picked up).INSERT INTO PAYSQL/PYRUN VALUES ('RUN001',<PSTART>,<PEND>,<PAYDT>,'O').-- STRSQL (or wrap in a batch driver / CALLPAY and SBMJOB it) CALL PAYSQL.PY_RUN_POST('RUN001', ?, ?, ?, ?); -- OUT: COMPUTED SKIPPED VETOED POSTED
Post-checks after the run:
COMPUTED = the number of active employees whose payslip was created this run;
SKIPPED = already-present payslips (re-run); VETOED = employees whose net
would have been negative (not posted); POSTED = GL-distributed rows (= COMPUTED on a
first run).SELECT RSTAT FROM PAYSQL/PYRUN WHERE RUNID='RUN001' = P.PYAUD ACCRUE row per posted employee.Adjusting a posted payslip. Change net only through the view, which reroutes to the validated procedure:
-- sanctioned: reroutes to PY_ADJUST_NET, re-validates net >= 0, audits UPDATE PAYSQL/PYSLIPV SET NET = 700.00 WHERE RUNID='RUN001' AND EMPID='E00003'; -- a negative net here is REJECTED (SQLSTATE 75013); the row is left unchanged
These are the same invariants the battle and volume simulations check against an independent hand-derived (Python) oracle — nothing is read back and asserted against itself.
NET = GROSS − PREDED − FEDTAX − POSTDED exactly, for every posted
row — no penny drift:SELECT EMPID FROM PAYSQL/PYRUNDT
WHERE RUNID='RUN001' AND ROUND(GROSS-PREDED-FEDTAX-POSTDED,2) <> NET;
-- expected: zero rows
SELECT EMPID FROM PAYSQL/PYGL WHERE RUNID='RUN001'
GROUP BY EMPID HAVING COUNT(*) <> 4 OR ROUND(SUM(GAMT),2) <> 0;
-- expected: zero rows
SUM(GAMT) on account 700000 = total gross posted.PYACCUM(EMPID,PYEAR) equals the sum of that employee's posted
PYRUNDT gross/fedtax/net for the year (e.g. after two runs E00001 shows
6000 / 826.20 / 4683.80).FN_TAXBRK at a floor returns the base exactly
(e.g. FN_TAXBRK('S',11000.00)=1100.00); an unknown filing status SIGNALs rather than
returning 0.FN_ROUND2 is HALF-UP at the cent, and the
engine's DECIMAL handling was hardened (SQLPL-PLAT-PAY-02, fixed at engine commit 407bee974) so the four
former float-boundary employees (E00011/E00013/E00021/E00023) now tie to the oracle with zero tolerance.
A re-appearing one-cent mismatch on those is a regression, not a tolerated gap.| Situation | Behaviour | Action |
|---|---|---|
| Re-run a posted run | PY_RUN_POST early-returns; all counters 0. | Safe no-op. YTD is not doubled, GL is not duplicated. Idempotent. |
| Re-run with new employees added | Existing payslips are SKIPPED; only the header being still 'O' lets new ones compute. | Existing rows are never recomputed; a fully posted run won't pick up late additions — use a new run id. |
| An employee's net would be negative | TR_PYRUNDT_VETO SIGNALs 75014; PY_RUN_EMPLOYEE's UNDO handler unwinds that INSERT, returns VETOED. | Run continues; the employee gets no payslip. Correct the deduction elections (e.g. an over-large garnishment) and re-run into a fresh run id. |
| Unknown filing status / pay type | FN_TAXBRK (75010) / PY_COMPUTE_GROSS (75012) SIGNAL. | Fix the PYEMP row; the guard prevents a silent zero-tax or bad gross. |
| Zero-length pay period | FN_PRORATE SIGNALs 75011. | Correct PSTART/PEND on the PYRUN header. |
| Ad-hoc UPDATE of a payslip net | Direct base-table balance edits are not sanctioned; the view reroutes and re-validates. | Adjust only via UPDATE PYSLIPV; a negative value is rejected (75013). |
| Adjustment via CALLPAY / RPG | An unhandled SIGNAL surfaces to RPG as SQLCODE -438. | Non-zero SQLCODE means the adjustment did not land; correct the amount and retry. |
PYRUNDT/PYGL/PYACCUM/PYAUD for reconciliation.The complete SQL-PL surface, from schema.sql and routines.sql. All objects
are in library PAYSQL. Signatures are given as declared; IN parameters precede
OUT.
| Object | Key | Notes |
|---|---|---|
| PYEMP | EMPID | Master; PTYPE S/H, FSTAT S/M, ESTAT A/T, TERMDT 0=active. |
| PYBRK | FSTAT, BFLOOR | Seeded 5 rows per filing status; tax=BBASE+BRATE×(I−BFLOOR). |
| PYDEDC | DEDCODE | Seeded 401K/MED/UNIO/GARN; DKIND B/A, DBASIS F/P. |
| PYEMPDED | EMPID, DEDCODE | Elections; JOINed to PYDEDC in PY_WITHHOLD. |
| PYRUN | RUNID | Header; RSTAT O→P; PEND year is the YTD year. |
| PYRUNDT | RUNID, EMPID | Payslip; DSTAT C→P; carries all computed money columns. |
| PYACCUM | EMPID, PYEAR | YTD; trigger-maintained only. |
| PYGL | GSEQ (identity) | 4 lines/employee netting to 0; accounts 700000/210000/220000/100000. |
| PYAUD | ASEQ (identity) | OP ACCRUE/ADJUST audit trail. |
| PYSLIPV | view/PYRUNDT | SELECT RUNID,EMPID,GROSS,FEDTAX,NET,DSTAT; INSTEAD OF UPDATE on NET. |
FLOOR, adds 1 if the fraction ≥0.5,
divides by 100.0. The divisor literal carries an explicit decimal point deliberately
— an integer-looking operand truncates like C division (SQLPL-PLAT-01 workaround).PYBRK for the filing status ordered by floor, keeping the
highest floor ≤ income (income floored at 0), then returns
FN_ROUND2(BESTBS + BESTRT×(INCOME−BESTFLR)). If no bracket matches (unknown
filing status) it SIGNALs SQLSTATE 75010 rather than taxing at zero.DAYSWORKED/DAYSINPERIOD clamped to [0,1]. A
non-positive period SIGNALs 75011. Multiplies by 1.0 to force decimal
(not integer-truncated) division (SQLPL-PLAT-01 workaround).FN_ROUND2(ANNSAL/26.0), prorated by
FN_PRORATE(WORKEDDAYS,14) when TERMDT falls in [PSTART,PEND]
(WORKEDDAYS = MOD(TERMDT,100)−MOD(PSTART,100)+1). Hourly:
FN_ROUND2(HRATE×80). Unknown PTYPE → 75012. Divisor 26.0
(not 26) is a SQLPL-PLAT-01 workaround.PYEMPDED⨯PYDEDC accumulating pretax (DKIND B) and posttax (A)
totals, flat (DBASIS F) or percent-of-gross (P, via GROSSIN×DVAL/100.0). Then
TAXABLE=GROSS−PREDED (≥0);
ANNUAL=TAXABLE×26−EXEMPT×2000 (≥0);
ANNUALTAX=FN_TAXBRK(FSTAT,ANNUAL); FEDTAX=FN_ROUND2(ANNUALTAX/26.0);
NET=FN_ROUND2(GROSS−PREDED−FEDTAX−POSTDED). A NOT-FOUND CONTINUE
handler ends the cursor; an orphaned election (code absent from PYDEDC) simply drops from the join.BEGIN ATOMIC. If a PYRUNDT row exists for (run,emp) → RCOUT='SKIPPED';
else nests PY_COMPUTE_GROSS then PY_WITHHOLD, computes
TAXWAGE=GROSS−PREDED, INSERTs the payslip (DSTAT 'C') →
RCOUT='COMPUTED'. An UNDO HANDLER FOR SQLEXCEPTION sets
RCOUT='VETOED' and unwinds the INSERT (so a trigger veto doesn't abort the caller's loop).RSTAT='P'. Cursor C1 over
ESTAT='A' employees calls PY_RUN_EMPLOYEE and tallies by return code. Cursor
C2 over the just-computed (DSTAT='C') rows writes the 4-line GL distribution per employee
(+GROSS/700000, −FEDTAX/210000,
−(POSTDED+PREDED)/220000, −NET/100000), flips each row to 'P',
increments POSTED; finally UPDATE PYRUN SET RSTAT='P'.TR_PYSLIPV_UPD).
Re-validates NEWNET≥0 (else 75013 — defense in depth, never trusting the caller),
UPDATEs PYRUNDT.NET, and writes an ADJUST row to PYAUD.N.NET < 0 → SIGNAL SQLSTATE '75014'. Rejects any negative-net
payslip, including an ad-hoc INSERT that tries to bypass PY_WITHHOLD.YR = PEND/10000), then UPSERTs
PYACCUM(EMPID,YR) (SELECT COUNT INTO → INSERT or additive UPDATE, since MERGE is not
assumed available), and writes an ACCRUE row to PYAUD. This is the sole writer of PYACCUM.CALL PY_ADJUST_NET(O.RUNID, O.EMPID, N.NET), so no direct
UPDATE of a payslip net can bypass the net≥0 re-validation. Only NET is rerouted; other view columns
are inert.PAYSQL/i is a compact tour of the compound-SQL-PL idioms; a maintainer sees these repeatedly:
CREATE FUNCTION ... LANGUAGE SQL BEGIN ... RETURN ...
with local DECLAREs, IF/WHILE, and a cursor (FN_TAXBRK, FN_ROUND2,
FN_PRORATE).DECLARE CONTINUE HANDLER FOR NOT FOUND SET DONE=1; then
OPEN / FETCH / WHILE DONE=0 DO ... FETCH ... END WHILE / CLOSE (the bracket walk and
the deduction accumulation; PY_RUN_POST's two cursors, resetting DONE=0 between them).BEGIN ATOMIC ... DECLARE UNDO HANDLER FOR
SQLEXCEPTION so a trigger SIGNAL rolls back one employee's INSERT and the loop
continues (PY_RUN_EMPLOYEE).SIGNAL SQLSTATE '750xx' SET MESSAGE_TEXT=...
for conservative refusals (unknown status/type, zero period, negative net).SELECT COUNT(*) INTO then IF
INSERT-or-additive-UPDATE (TR_PYRUNDT_ACCR's YTD accrual).EXEC SQL CALL proc(:in, :out) with packed
host variables; check SQLCODE (0 success, -438 unhandled SIGNAL) after each
call (CALLPAY).26.0, 100.0, ×1.0) so integer-looking DECIMAL operands
don't truncate (SQLPL-PLAT-01 workaround, documented in source).| SQLSTATE | Raised by | Meaning |
|---|---|---|
| 75010 | FN_TAXBRK | No bracket for the filing status (unknown FSTAT) — never silently tax at 0. |
| 75011 | FN_PRORATE | Non-positive pay-period length. |
| 75012 | PY_COMPUTE_GROSS | Unknown pay type (PTYPE not S or H). |
| 75013 | PY_ADJUST_NET | Negative net rejected (re-validation). |
| 75014 | TR_PYRUNDT_VETO | Negative net pay rejected on INSERT. |
When one of these SIGNALs is unhandled by an SQLRPGLE caller, it surfaces as
SQLCODE -438 (the platform convention proven in CALLPAY's PY_ADJUST_NET(-5.00) step).
tax = base + rate×(income−floor).FN_PRORATE, clamped to [0,1].SQLCODE -438.EXEC SQL CALL only to prove the SQL-PL procedures are
reachable from compiled RPG; it holds no payroll math.