inno comply

Forensic deletion verification and data-residue scanning - the inverse of inno undelete. Where undelete recovers data that still lingers, comply checks whether a value has been purged from the InnoDB structures in a file - clustered-index records (live, delete-marked, and free-list) plus undo entries for primary-key columns - and reports every place it still appears. Add --thorough to also sweep raw page bytes (slack space and other page regions).

Scope and honesty

comply verifies residue within the file(s) you pass in only. It cannot see the OS page cache, replicas, other backups, or binary-log archives, and it does not certify legal compliance. It reports byte-level and record-level residue; interpreting that against a regulation is your call, not the tool's.

Modes

Exactly one mode per invocation:

ModeFlagAnswers
Deletion verification--verify-deleted --where <col>=<value>Has this value been purged from all record structures?
Residue scan--scan-residue --pattern <needle>Where do these raw bytes still appear on disk?
Encryption audit--encryption-auditWhich pages are encrypted, and is a key available?

Usage

# Verify a specific value has been deleted everywhere (logical structures)
inno comply -f users.ibd --verify-deleted --where email=alice@example.com

# Also sweep raw slack/unallocated space for the encoded value
inno comply -f users.ibd --verify-deleted --where email=alice@example.com --thorough

# Raw byte-pattern residue scan (UTF-8 text)
inno comply -f users.ibd --scan-residue --pattern "alice@example.com"

# Raw scan for a specific byte sequence
inno comply -f users.ibd --scan-residue --pattern hex:0a1b2c3d

# Encryption audit
inno comply -f users.ibd --encryption-audit

# JSON output for any mode
inno comply -f users.ibd --verify-deleted --where id=42 --json

Options

OptionDescription
-f, --filePath to the InnoDB tablespace file (.ibd)
--verify-deletedMode: verify a value has been purged from all InnoDB-retained locations
--scan-residueMode: raw byte-pattern residue scan across all pages
--encryption-auditMode: report encrypted vs plaintext pages and key availability
--wherecolumn=value to check (verify mode)
--patternLiteral needle to search for (scan mode): UTF-8 text, or hex:00ff.. for raw bytes
--thoroughVerify mode: also sweep raw slack space for the encoded value
--max-hitsScan mode: cap on matches reported (default 1000)
-t, --tableTable-name filter (verify mode); errors if it mismatches the SDI table
--jsonEmit JSON
-v, --verboseShow additional detail
--page-sizeOverride page size (default: auto-detect)
--keyringPath to MySQL keyring file for encrypted tablespaces
--mmapUse memory-mapped I/O

The global --format csv flag produces CSV output for any mode.

How verification works

--verify-deleted runs a logical pass: it decodes real record structures and compares the target column's value. It covers, in order:

  1. Live clustered records - the value exists in a current row (reported as live_record; from a deletion standpoint this is a failure - the value is still there).
  2. Delete-marked records - DELETEd but not yet purged (delete_marked), with the approximate transaction id.
  3. Free-list records - purged from the active chain but not overwritten (free_list).
  4. Undo DEL_MARK entries - only scanned when the target column is part of the primary key, because undo stores PK fields for deletes (undo_del_mark). Undo lives in ibdata1/undo tablespaces, so a bare .ibd finds nothing here.

The logical pass is type-aware and authoritative for "is this value still reachable as a record field." It is blind to torn or overwritten bytes sitting in page slack space.

--thorough adds a raw pass: it encodes the target value the way InnoDB stores it (UTF-8 for strings; big-endian with the sign bit flipped for signed integers) and searches every page's bytes across all page regions. Each hit is tagged raw_<region> (e.g. raw_free_space for slack, raw_record_heap when it overlaps a live record), so a match inside a live record is not mislabeled as slack. This is O(file) per value and noisier, so it is opt-in. Without --thorough, the summary states plainly that only logical structures were checked.

fully_purged is true only when no residue site was found in any scanned region.

How residue scanning works

--scan-residue --pattern searches every page for a literal byte needle and reports each hit as page / page_type / offset / region, with 32 bytes of surrounding context in hex. Regions are record_heap, free_space (INDEX pages, at or above heap_top), fil_header, fil_trailer, and body.

There is no regex - the needle is literal. Pass UTF-8 text directly, or raw bytes with a hex: prefix (hex: followed by an even number of hex digits).

Note: SDI metadata pages are zlib-compressed, so scanning for a schema string like a table or column name finds nothing - the bytes are not stored uncompressed. Residue scanning finds values as they appear in record data and slack space.

JSON output

{
  "table_name": "users",
  "column": "email",
  "target_value": "alice@example.com",
  "fully_purged": false,
  "residue_sites": [
    { "region": "delete_marked", "page_number": 4, "offset": 0, "delete_marked": true, "trx_id": 1875 }
  ],
  "regions_scanned": ["clustered_leaf_live", "clustered_leaf_delete_marked", "free_list"],
  "thorough": false,
  "records_examined": 3
}

Directory-wide scanning

To sweep an entire data directory for residue rather than a single file, use inno audit --compliance:

inno audit -d /var/lib/mysql --compliance --pattern "alice@example.com"

See the GDPR Verification guide for an end-to-end walkthrough.