New to Kestra?
Use blueprints to kickstart your first workflows.
Purge aged dropped tables from the Oracle recycle bin behind an age gate, a dry run plus an approval pause, then re-read dba_recyclebin to confirm.
Dropping an Oracle table does not free its segments. The table is renamed to a
BIN$... object in the recycle bin so FLASHBACK TABLE ... TO BEFORE DROP can bring
it back. Every block it owned stays allocated in its tablespace. Its indexes go
with it. Weeks of test tables, staging copies, failed loads then sit there holding
space nobody can account for.
The views disagree about that space, which is why it goes unnoticed. dba_tables
shows none of those tables. dba_segments shows every one of them as a BIN$...
segment. dba_free_space counts the same blocks as free, because Oracle takes them
back when it has to. A capacity report built on segments says the tablespace is
filling, one built on free space says it is empty. Neither names a table you can
go and look at. Oracle does reclaim the space on its own, but only at the moment an
allocation needs it, by purging recycle bin entries oldest first, with nothing
recorded and no regard for which table somebody still wanted back.
This blueprint reads the recycle bin itself. It lists the dropped tables old enough
to be given up on, says how much space each one is holding, purges them with
PURGE TABLE behind an age gate, a dry run plus an approval pause, then reads
dba_recyclebin again and fails the run if a target survived.
This blueprint was created by zkasuran.
A purge is not a cleanup. It is the end of recovery for that table. Once
PURGE TABLE "BIN$..." runs, FLASHBACK TABLE ... TO BEFORE DROP can never bring
the table back, which is the only reason the recycle bin exists. So the age gate is
the heart of this flow, not decoration.
min_age_hours defaults to 168, seven days. A table dropped minutes ago is
reported in the findings but is never a purge target.dba_recyclebin.droptime, so the same predicate
that selected a target selects it again in the re-read.auto_remediate is true, dry_run is false and a human
resumes the approval_gate pause with approved set to true. Three independent
conditions, all off by default.PURGE RECYCLEBIN or PURGE DBA_RECYCLEBIN. Those take
everything regardless of age. Each statement names one object.totals_before (io.kestra.plugin.jdbc.oracle.Query, read-only) reads how many
recycle bin entries the checked schemas hold and how many bytes they occupy, at any
age, joining dba_tablespaces for the block size.audit (read-only) lists the dropped tables whose droptime is at least
min_age_hours old and whose can_purge is YES. For each one it sums the space of
every recycle bin entry sharing the same base_object, so an index dropped with the
table is counted. It also builds the exact PURGE TABLE statement.targets (io.kestra.plugin.core.output.OutputValues) collects those statements
plus the total bytes they should release. summary logs the per-object detail.route (io.kestra.plugin.core.flow.If):alert posts the objects, their tablespaces plus their drop times to Slack.remediate_gate runs only when auto_remediate is true and dry_run is false.approval_gate (io.kestra.plugin.core.flow.Pause) holds the run. apply checks
the resume answer, so an unanswered pause never reads as approval.remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs one
PURGE TABLE per object, in turn.reaudit re-runs the same age-gated query. totals_after re-reads the totals.
verify_result fails the run through io.kestra.plugin.core.execution.Fail if any
target is still there, otherwise remediated logs the before and after space.errors posts to Slack if the flow fails for any reason.dba_segments by
object name will not tell you because the segment is named BIN$... now.PURGE RECYCLEBIN and hoping nobody needed those tables.The usual answer is PURGE RECYCLEBIN, which takes everything including the table
somebody dropped by mistake an hour ago. Kestra turns it into a policy: run on a
schedule, scope it to the schemas you own, keep anything inside the flashback window,
post the candidates to Slack, hold the destructive step behind a human approval, then
prove the space came back by reading dba_recyclebin again. Every run keeps its logs,
so you can show what was purged, when it had been dropped plus how many bytes it freed.
On Kestra 2.0.5 OSS against Oracle Database Free 23ai (gvenzl/oracle-free, service
FREEPDB1), with a local HTTP sink standing in for Slack. The test schema APP held
five tables, each 20000 rows plus a secondary index, all created before any drop.
T1 through T4 were dropped as the aged batch. T5 was dropped immediately before
the gated runs so it stayed inside every age window. Each dropped table leaves three
recycle bin entries (the table, its secondary index, its primary key index) totalling
1464 blocks or 11993088 bytes at an 8 KB block size.
| Run | Setup and inputs | Result |
|---|---|---|
| 1 | empty recycle bin, defaults | 0 entries, clean branch, SUCCESS |
| 2 | 4 dropped tables, min_age_hours: 0.05 |
4 detected with 47972352 bytes, posted to Slack, no change, SUCCESS |
| 3 | same, auto_remediate: true, dry_run: true |
Same report, Slack said no change applied, recycle bin unchanged. The dry run gate held |
| 4 | same, auto_remediate: true, dry_run: false, default min_age_hours: 168.0 |
0 targets, clean branch, nothing purged. Fully armed, held by the age gate alone |
| 5 | 4 aged plus the fresh T5, min_age_hours: 0.05, armed, pause resumed approved: true |
4 purged, reaudit found 0, SUCCESS. 15 entries and 59965440 bytes went to 3 entries and 11993088 bytes, which is T5 still there |
| 6 | T5 now aged, armed, pause resumed approved: false |
Purge declined, nothing purged, SUCCESS |
| 7 | same target, a user with SELECT ANY DICTIONARY but no DROP ANY TABLE, armed, approved |
PURGE refused with ORA-01031, reaudit still found the target, the run FAILED and the errors block alerted Slack |
Runs 1 and 2 are the proof the check can fail: the same query returned 0 against an
empty recycle bin and 4 against the loaded one. Runs 4 and 5 are the proof the age
gate holds: run 4 armed the purge and the shipped seven day default still returned 0
targets, then run 5 purged the four aged tables at a three minute threshold while the
table dropped a minute earlier stayed. Every count plus byte figure was read back from
dba_recyclebin with sqlplus, not only from the Kestra log.
jdbc:oracle:thin:@//host:1521/service).dba_recyclebin (SELECT_CATALOG_ROLE or SELECT ANY DICTIONARY) and read dba_tablespaces. Purging another schema's dropped table needs
DROP ANY TABLE. A user purging only its own dropped tables needs no extra privilege.recyclebin left at its default ON. With it OFF a drop frees space at once, there is
no recycle bin to read plus nothing to flashback.ORACLE_PASSWORD: the database password. It is the default for the password input.SLACK_WEBHOOK_URL: used by alert and the errors block.SECRET_.| Input | Default | Purpose |
|---|---|---|
jdbc_url |
jdbc:oracle:thin:@//localhost:1521/FREEPDB1 |
The Oracle instance to check |
username |
SYSTEM |
The database user |
password |
{{ secret('ORACLE_PASSWORD') }} |
The database password |
object_owners |
[APP] |
Schema owners whose dropped tables are checked |
min_age_hours |
168.0 |
A drop younger than this is reported but never purged |
auto_remediate |
false |
Allow PURGE TABLE |
dry_run |
true |
Must be false for a purge to run |
ORACLE_PASSWORD and SLACK_WEBHOOK_URL secrets.jdbc_url plus username, then list the schemas in object_owners.min_age_hours to it.auto_remediate: true plus dry_run: false, then
resume the pause with approved: true.daily trigger, which always runs in safe mode.PURGE TABLESPACE <name> USER <owner> when you only
care about a single tablespace and accept giving up flashback for everything in it.
That form ignores drop time, so it drops the age gate with it.