Query icon
OutputValues icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Fail icon
Schedule icon

DB2 Check-Pending Table Recovery Guard

Detect DB2 tables stuck in check-pending state and rejecting reads, then clear them with SET INTEGRITY and prove they answer queries again.

Categories
DataInfrastructure

A DB2 table in check-pending state is an outage, not a slow query. After a LOAD or after a constraint is added with SET INTEGRITY ... OFF, SYSCAT.TABLES.STATUS goes to 'C' and from that moment the table rejects every SELECT and every INSERT, UPDATE or DELETE with SQL0668N reason code 1. The application reading it is down. Nothing times out, nothing degrades, the table simply refuses. The fix is one statement, SET INTEGRITY FOR <table> IMMEDIATE CHECKED, and the table stays broken until somebody runs it.

This blueprint reads SYSCAT.TABLES.STATUS for a schema, orders the pending tables parents first using SYSCAT.REFERENCES, then behind a dry run and a Pause runs SET INTEGRITY one table at a time. It reads STATUS again and fails the run if any target is still not 'N', then counts rows through each cleared table so the proof is a working query and not only a catalog flag.

This blueprint was created by zkasuran.

Findings

Verdict Rule Fix
ACTION At least one table in the schema has STATUS = 'C' SET INTEGRITY ... IMMEDIATE CHECKED, parents before children
BLOCKED Divert mode is on but a target has no exception table. Alternatively the only tables found are inoperative (STATUS = 'X') Create the exception tables. An inoperative table has to be dropped then recreated. The guard reports and stops
OK Every table in the schema reads STATUS = 'N' None

Safety

  • Read-only by default. SET INTEGRITY runs only when auto_remediate is true, dry_run is false, the verdict is ACTION and a human approves the Pause. An unanswered resume leaves approved null, which the gate reads as a refusal.
  • Order is enforced, not hoped for. A child table cannot be checked while its parent is still pending, so the dependency depth comes from SYSCAT.REFERENCES and the loop runs lowest depth first with concurrencyLimit: 1.
  • max_tables caps one run. SET INTEGRITY reads every row of the table to validate it, so a wide run is a long scan. Truncation takes the lowest depth first, which means parents, so the children are still checkable on the next run.
  • Rows that break the constraint are never discarded. In fail mode the statement fails and the table stays pending, which the re-verify step turns into a failed run. In divert_to_exception_tables mode each bad row is moved to that table's exception table and the final step counts those rows so they are reported.
  • A table with STATUS = 'X' (inoperative) is reported and never touched. SET INTEGRITY cannot fix it.

How it works

  1. detect (io.kestra.plugin.jdbc.db2.Query, read-only) selects every table in the schema whose STATUS is not 'N'. One recursive query over SYSCAT.REFERENCES gives each pending table its dependency depth and the pending parents that must be cleared first. It also reports whether an exception table exists for each target.
  2. assess (io.kestra.plugin.core.output.OutputValues) splits check-pending from inoperative, sorts parents first, applies max_tables, builds one SET INTEGRITY statement per target and the row count query used later as proof.
  3. summary logs the report. route (io.kestra.plugin.core.flow.If) stops with a log when every table reads 'N'.
  4. On a finding, alert posts to Slack and planned prints the exact statements a remediation run would execute, so a dry run is a readable plan.
    • remediate_gate opens only on ACTION with auto_remediate true and dry_run false. hold (io.kestra.plugin.core.flow.Pause) waits for approval.
    • remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs the statements in order, one table per iteration.
    • reverify reads SYSCAT.TABLES.STATUS again. verify_result fails the run when a target is still not 'N', naming it and why it can stay stuck.
    • prove_reads counts rows through every cleared table, which only works if the table really accepts reads again, then remediated logs the counts. In divert mode the exception tables are counted in the same query.
  5. errors posts a Slack alert when the flow fails, for example when the database is unreachable.

What you get

  • The list of tables that are refusing reads right now, with the dependency order they have to be cleared in.
  • A dry run that prints the exact statements, so the plan can be read before anything runs.
  • A one step recovery behind an approval gate, with two kinds of proof after it: STATUS back to 'N' and a real row count through the table.
  • A clear report of diverted rows when violations are moved to exception tables, so nothing is quietly dropped.

Who it's for

  • Teams whose nightly LOAD can leave a table unreadable, where the next working day starts with an application that cannot query a table nobody changed.
  • DBAs adding constraints to large tables, which is the other way a table lands in check-pending.
  • Platform teams who want the recovery statement run under an approval gate with evidence, rather than typed into a shell at 03:00.

Why orchestrate this with Kestra

The detection needs the catalog. The fix needs a dependency order. The order needs a loop that cannot run in parallel. All of it needs an approval, because the fix is a full table scan. Kestra holds all four in one flow: the If gates on auto_remediate plus dry_run, the Pause holds for a human, the Loop with concurrencyLimit: 1 keeps parents before children. A re-verify that fails the run means a partial recovery can never be reported as a success.

Tested end to end

On Kestra 2.0.5 OSS against DB2 Community Edition 12.1.5 (icr.io/db2_community/db2), database testdb, with a local HTTP sink standing in for Slack. Check-pending was created for real with SET INTEGRITY FOR <table> OFF and confirmed with the DB2 CLI: SYSCAT.TABLES.STATUS read 'C' while SELECT COUNT(*) on the same table returned SQL0668N Operation not allowed for reason code "1", SQLSTATE 57007. After the fix the same SELECT returned 5 rows. A second table in the schema stayed at STATUS = 'N' and readable the whole time.

Run Inputs Result
1 one table pending, defaults Verdict ACTION. DB2INST1.ORDERS reported at depth 0, alert sent, no change
2 auto_remediate: true, dry_run: false, pause declined No change. Schema still reporting 1 check-pending table
3 same, pause approved SET INTEGRITY ran. STATUS 'C' to 'N', 5 rows counted through the table. CLI agreed
4 healthy schema, defaults Verdict OK. 0 tables reported
5 parent and child both pending, approved Ordered PARENT_T depth 0 then CHILD_T depth 1, both cleared, 2 rows each
6 max_tables: 1 with 2 pending Only PARENT_T in scope, child left for the next run
7 a row breaking a CHECK constraint, fail mode, approved Statement failed, table stayed 'C', the run FAILED at the re-verify
8 divert_to_exception_tables with no exception table, armed Verdict BLOCKED. Nothing ran
9 divert_to_exception_tables with the exception table in place, approved 2 violating rows moved to BADROWS_EXCEPTIONS, base table back to 'N' with 2 readable rows, diverted count reported
10 unreachable port detect failed, the errors block posted to Slack, the execution is FAILED

Ordering is not a theory. Checking the child first by hand returned SQL3608N Cannot check a dependent table ... while the parent table ... is in the Set Integrity Pending state, SQLSTATE 428A8, which is what run 5 avoids by sorting on dependency depth.

Prerequisites

  • A DB2 user that can read SYSCAT.TABLES and SYSCAT.REFERENCES, plus CONTROL or the LOAD authority needed to run SET INTEGRITY on the target tables for remediation. The instance owner has all of these.
  • For divert_to_exception_tables, an exception table per target. Build it with CREATE TABLE <base>_EXCEPTIONS LIKE <base> then ALTER TABLE <base>_EXCEPTIONS ADD COLUMN TS TIMESTAMP ADD COLUMN MSG CLOB(32K). A hand written table with the same column types is not enough: DB2 rejected one with SQL3604N ... Reason code: "2", while the LIKE form was accepted and returned SQL3602W with the bad rows moved.
  • Local testing:
    • docker run -d --name db2 --privileged -p 50000:50000 -e LICENSE=accept -e DB2INST1_PASSWORD=... -e DBNAME=testdb icr.io/db2_community/db2:latest;
    • create a table, then run SET INTEGRITY FOR <table> OFF to put it into check-pending state.

Secrets

  • DB2_PASSWORD: the database password, used as the default for the jdbc_password input.
  • SLACK_WEBHOOK_URL: used by alert and the errors block.
  • In Kestra OSS, provide each one as a base64-encoded environment variable prefixed SECRET_.

Inputs

Input Default Purpose
jdbc_url jdbc:db2://localhost:50000/testdb Database to read
jdbc_username db2inst1 Database user
jdbc_password {{ secret('DB2_PASSWORD') }} Database password
table_schema DB2INST1 Schema whose tables are scanned
violation_handling fail fail stops on a bad row. divert_to_exception_tables moves bad rows to exception tables
exception_table_suffix _EXCEPTIONS Names the exception table in divert mode
max_tables 10 Tables checked per run, lowest dependency depth first
auto_remediate false Allow SET INTEGRITY to run
dry_run true Must be false for anything to change

Quick start

  1. Add the DB2_PASSWORD and SLACK_WEBHOOK_URL secrets.
  2. Import the flow, set jdbc_url, jdbc_username and table_schema, then run it with the defaults. The log names any table that is refusing reads.
  3. Read the planned log to see the exact statements and the order they would run in.
  4. When you are ready, run with auto_remediate: true and dry_run: false, then approve the pause. The last log carries the row count through each recovered table.
  5. Enable the every_15_minutes trigger, which always runs in safe mode.

How to extend

  • Point it at several schemas by running it once per schema from a parent flow.
  • Swap IMMEDIATE CHECKED for IMMEDIATE UNCHECKED in a controlled window when the data is known good and the full scan is not affordable. That skips validation, so it belongs behind its own gate.
  • Create the exception tables automatically from the base table layout before a divert run.
  • Add FULL ACCESS handling for tables that come out of check-pending into no-access state after a partial check.
  • Send the diverted row counts to a data quality dashboard instead of only to the log.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.