Crosshire
Classification & PII
Govern17 Classification & PII · 3 of 3

Leaving, and being erased

Data leaves the platform, and data has to disappear from it. The first is a design choice made once; the second is a request that arrives without warning and finds out whether the first one was made well.

Stand 29.07.2026

#Share, do not export

What
When somebody outside the warehouse needs numbers there are two options, and one of them survives a governance review. A CSV export is a copy that leaves with no lineage, no revocation, no audit trail past the download, and no way to honour an erasure request against it. Delta Sharing — the docs now file it under OpenSharing, paths and all — keeps the data where it is and lends a view of it.
Why
Revocation is the whole argument. REVOKE SELECT ON SHARE takes effect on the next read; a spreadsheet in someone's Downloads folder is permanent. So is erasure: Art. 17 obliges you to remove the customer's data, and you cannot remove it from a file you no longer control — the export made a promise you can no longer keep.
The second argument is evidence, with one boundary on it. Recipient reads land in system.access.audit like any other access, so 'who saw this, and when' has an answer for external consumers and not only internal ones — as long as they are reading through the sharing server. A cloud-token recipient reading the storage root directly is outside that record, which is why the evidence argument and the credential choice are the same decision.
How
sql
Share the aggregate, not the dimension — and refuse history explicitly
CREATE SHARE revenue_reporting
  COMMENT 'Monthly revenue for group controlling. No personal data crosses.';

ALTER SHARE revenue_reporting
  ADD TABLE dwh_prod.gold.agg_revenue_month
  WITHOUT HISTORY;   -- NOT optional boilerplate. WITH HISTORY is the DEFAULT
                     -- for table sharing on DBR 16.2+, and for SCHEMA sharing
                     -- on any runtime. Omit this line and the recipient gets
                     -- every commit version, including the ones from before an
                     -- erasure redacted them.

-- Databricks-to-Databricks: the recipient is an identity, not a token. The
-- sharing identifier is <cloud>:<region>:<metastore-uuid>, and it is theirs to
-- hand you — they run SELECT CURRENT_METASTORE() and send you the result.
CREATE RECIPIENT group_controlling
  USING ID 'aws:eu-central-1:00000000-0000-0000-0000-000000000000'
  COMMENT 'Group controlling metastore.';

GRANT SELECT ON SHARE revenue_reporting TO RECIPIENT group_controlling;
The design decision is in the second statement. gold.agg_revenue_month is month × region × product group, so nothing personal crosses the boundary and the sharing conversation never becomes a privacy conversation. If the recipient truly needs customer grain, share a view that drops email, phone and street — not the dimension with a mask on it.
The WITHOUT HISTORY line is the one people leave out, and it is a default you are overriding rather than an option you are adding. In this warehouse the consequence is specific: the erasure topic below redacts in place, so shared history hands the recipient the versions from before the redaction. An Art. 17 request honoured perfectly inside your catalog is still readable across the sharing boundary, and nothing in the share DDL says so.
Doing it
  • Write WITHOUT HISTORY on every ADD TABLE, and re-check it after any share editSilence means history, not no-history.
  • Default to Databricks-to-Databricks when the recipient is on DatabricksOpen sharing hands out a credential file, and a bearer token is an identity anyone holding the file can assume.
  • Establish whether the recipient path uses cloud token accessDo it before you write 'no personal data leaves the platform' in a concept. With it, they hold the table's storage root and its Delta log — commit history, committer identities and un-vacuumed deleted files.
  • Share aggregates and purpose-built viewsNever share a dimension that carries contact columns.
  • Where open sharing is unavoidable, rotate the recipient token on a schedule and monitor its expiryA D2D recipient has no token, so this chore only exists in the case you were trying to avoid — and an unrotated token is either a silent outage on the expiry date or a permanent credential.
  • Check whether query-result download is even enabled in the workspaceIf exports are possible, they are happening.
Embedding it
  • Ask what the last CSV that left the building contained and who still has itThe silence is the lesson.
  • Have the team put the share and the recipient under version controlTerraform, or the DDL above in a reviewed migration job. Bundles had no share resource type at the 29.07.2026 pass, so 'just declare it in the bundle' is the answer people give and then cannot implement.
  • Put a revocation drill in the game day: revoke, confirm the recipient's read fails, restoreTen minutes, and the control stops being theoretical.
Defaults
  • Sharing over export without exceptionAn unavoidable export gets an owner, a purpose and an expiry date in writing.
  • D2D over open sharing whenever the recipient has a metastore
  • Share the narrowest object that answers the question — aggregate first, view second, base table never
  • WITHOUT HISTORY on every shared table unless a named requirement asks for history in writing
  • The share and its recipients are code, reviewed and versioned like the tables they exposeNot a Catalog Explorer click nobody can diff.
Gotchas
  • Assuming a column mask or row filter travels with a shared tableIt fails one of two ways and you cannot tell which in advance: ADD TABLE rejects the table outright, which costs an afternoon, or the recipient reads the column unmasked and you never find out, because their reads reach your audit log and their result sets do not. Share a view that physically has no contact columns and the question stops existing.
  • Open sharing to a recipient who is on DatabricksYou replaced an identity with a file, and files get forwarded — the access log then shows one recipient for an unknown number of readers.
  • Getting history because you never refused itWITH HISTORY is the default on DBR 16.2+ and for schema shares on any runtime, so the minimal-looking ADD TABLE is the one that shares the most — every version from before you removed the column, and every version from before an erasure redacted it.
  • Treating cloud token access as 'sharing, just with a token'It scopes credentials to the shared table's root directory, so the recipient holds the Delta log: commit history, committer information, and deleted data VACUUM has not yet cleared. Their table reads land in your audit log; their file reads are a different question.
  • The export that already happenedNobody volunteers it. system.access.audit is where the result downloads are, and the answer is usually 'yes, monthly, for two years'.

#Erasure against an SCD2 dimension

What
An Art. 17 request arrives for one customer. In this warehouse that customer exists in bronze.sap_kna1 once per daily extract for as long as bronze has been running, in silver.customer and gold.dim_customer once per validity interval, and as a customer_sk on every gold.fct_order_line row they ever generated. Deleting the dimension rows breaks the facts and destroys the revenue history.
Redact, do not delete. Keep the key, the intervals and the non-identifying attributes; overwrite the identifying ones, in place, across every interval.
Who the data subject is takes one sentence more than teams give it, and it is a rule with a carve-out rather than an absolute. The rule: a legal person — GmbH, AG, B.V. — is outside the GDPR, so the data subject is the named contact reachable through email, phone and street, and customer_name is a company name that stays; erasing it would destroy a business attribute the request never covered. The carve-out: a material slice of a German B2B customer base are natural persons — Einzelkaufleute (e.K.), Freiberufler, and the partners behind a GbR or OHG — whose firm name routinely contains the owner's surname. For those customers customer_name is personal data, and the Art. 17 right is the customer's own, not a contact's.
Which of the two a row is cannot be decided per request under a one-month deadline. It belongs in gold.dim_customer as a legal-form column carried from the SAP customer master — the canonical model does not carry one yet, and this is the route that adds it — so the erasure job branches on data rather than on a paragraph in the concept. State it as an absolute in the kick-off and the DPO corrects you in the first workshop, in front of the client.
Why
The step everyone forgets is the source. If SAP still holds the record, the next daily full extract lands in bronze.sap_kna1, the snapshot comparison sees a change, and the pipeline writes the email back overnight. The ticket is closed, the DPO has been answered, and the data is back inside twenty-four hours with nobody watching. Erase at source first; keep a suppression list the pipeline reads, so a re-delivery is re-redacted rather than re-applied. That list is itself a list of data subjects — same tags, same grants.
The second forgotten step is physical, and it applies to the UPDATE above as much as to a delete. The redacted row lands in a new file; the file holding the old email address is still there and still readable by time travel until VACUUM passes the retention threshold. Where rows are genuinely deleted rather than redacted, deletion vectors add one more layer: a DELETE marks rows without rewriting the Parquet file, so REORG TABLE … APPLY (PURGE) is what rewrites it and VACUUM is what removes the old one.
Either way the honest answer to 'when is it gone' is a number derived from that threshold, and it has to be shorter than the deadline you promised. Check who owns the schedule before you quote it: on a managed table under predictive optimization you are not the one running VACUUM, so the number is that feature's cadence rather than your job's — see the predictive-optimization register entry.
How
sql
Erasure is a correction, not a business change — it must not open a new version
UPDATE dwh_prod.gold.dim_customer
   SET email      = NULL,
       phone      = NULL,
       street     = NULL,
       _tech_hash = sha2(concat_ws('||', customer_segment), 256)
 WHERE customer_id = :customer_id;

-- Same in silver, where the SCD2 comparison actually runs. The hash is
-- recomputed over the rule-4 trigger set ONLY: customer_segment opens a new
-- version, an address change does not. Skip this line and the next run reads a
-- difference, closes the current interval and opens a fresh one holding the
-- redaction.
UPDATE dwh_prod.silver.customer
   SET email      = NULL,
       phone      = NULL,
       street     = NULL,
       _tech_hash = sha2(concat_ws('||', customer_segment), 256)
 WHERE customer_id = :customer_id;
customer_sk, region_sk, customer_segment, country and the intervals survive, so gold.fct_order_line still joins, gold.agg_revenue_month still reconciles, and the accounting record required by § 147 AO stays intact. customer_name survives for a legal person and is redacted alongside the contact columns for a natural-person customer — which is the branch the legal-form column exists to drive. Which columns survive is not an engineering decision — the DPO names them, and the list belongs in the anonymisation concept, versioned.
Doing it
  • Erase at source, then in the warehouse, then verify after the next scheduled load that it stayed erased
  • Carry the legal form into dim_customer before the first request arrives, and let the job branch on itDeciding natural-person-or-not by hand, per request, under a one-month deadline is how the wrong answer gets given twice.
  • Check every open share over a redacted tableA share carrying history exposes the pre-redaction versions, and a cloud-token recipient holds the Delta log — your erasure evidence otherwise covers only your own catalog.
  • Redact in place; never delete SCD2 rowsRecompute or exclude _tech_hash so the erasure does not open a new interval.
  • Set the bronze retention window in week onebronze.sap_kna1 keeps one row per customer per day forever by default, and 'forever' is what makes every future erasure a full-history rewrite.
  • Where physical removal is required, follow DELETE with REORG TABLE … APPLY (PURGE) and VACUUMState the resulting lag in the reply to the data subject.
  • Ship the erasure as a parameterised job with a fixture customer and an assertionNot a notebook someone opens under pressure.
Embedding it
  • Have the team list every place one customer physically exists before you show them yoursThe list is always shorter than reality, and the gap is the lesson.
  • Make the erasure job theirs, with its own test, and exercise it in the game day alongside the revocation drill
  • Send them to the DPO for the redact/retain column list before any SQL is writtenEngineering guesses this wrong in both directions.
  • Diary the follow-up check for the day after the next full loadWhoever finds a resurrected row once never skips that check again.
Defaults
  • Redaction over deletion for dimensions; deletion only where nothing downstream depends on the key
  • Source first, warehouse second, verification after the next load — as three steps in one runbook
  • A deliberate bronze retention window, so erasure is bounded work rather than a full-history rewrite
  • The stated erasure lag matches the VACUUM retention actually configuredThat includes the cadence predictive optimization actually runs at — not the one you would like.
Gotchas
  • The overnight resurrectionErasure applied only in the warehouse; the next full extract of bronze.sap_kna1 re-delivers the customer and the pipeline writes the email back. Nobody notices until the next access review, if there is one.
  • The erasure opening a new SCD2 version_tech_hash was computed over every attribute rather than over the rule-4 trigger set, so nulling the contact columns looks like a change: the next run closes the current interval and opens a new one, and the history now claims the customer changed address on the day they asked to be forgotten.
  • DELETE with deletion vectorsSELECT returns nothing and the bytes remain in the Parquet files until REORG TABLE … APPLY (PURGE) plus a VACUUM past the threshold. 'It is deleted' is true for readers and false for storage.
  • Time travel and clonesDELETE does not touch older table versions, and last quarter's DEEP CLONE is a separate table with its own copy — the erasure has to reach each one, which is one more bill for the dev copy nobody dropped.
  • Promising 'immediately'VACUUM cannot go below the retention threshold without disabling a safety check, and disabling it to keep a promise is how a concurrent reader gets a missing file instead of a row.
  • Redacting in place while a share is open over the tableHistory is shared by default, so the recipient reads the version before the UPDATE; with cloud token access they read the Delta log directly. The erasure is complete inside your catalog and untouched outside it, and the reply you already sent says otherwise.
  • Getting the legal form wrong in either directionErase an Einzelkaufmann's customer_name and you have honoured the request; refuse to and you have not. Erase a GmbH's and you have destroyed a business attribute no request covered. Same column, opposite answers, and a job that does not read the legal form is wrong for one of the two every single time.