DB Olympics

Queries · q121 · Data Protection Officer · week 1, night 4

Linking rejected erasure requests to active legal holds

“Do the 3 long-open 'rejected' data subject requests correspond to the same shopper and tenant as the 3 active legal holds for payment disputes?”

Manager · asked the questionInes LaurentData Protection Officer. Lawyer by training, previously at a European marketplace. Also reports to the board's audit committee.
Analyst · planned the reportPriyanka DesaiAuditor's mindset: traces individual records end to end to check that the totals are real.
Engineer · wrote the SQLInesThinks in window functions: ranks, running totals, LAG/LEAD for comparisons, percent-of-total.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. Ines Laurent writes to Priyanka Desai

    The manager, at the start of week 1

    Subject: A few starting questions on privacy & data handling

    Hi Priyanka,

    Welcome aboard, and thank you for taking these on this week. As I get oriented, here's what I'd like to understand first. Given the slot constraints, please treat these as priorities in order — if only three fit this week, start from the top and we'll pick up the rest next week.

    1. How are we doing on privacy requests from shoppers? I want to know how many access/erasure/portability requests we're getting, how long they typically take to close, and whether any are overdue. This is the thing the board audit committee will ask about first, and it's also a legal deadline risk, so I need a realistic picture, not just a snapshot.
    2. What does our consent picture look like? I'd like a sense of how many of our users have actually opted in versus withdrawn or never given consent, and whether that's changed meaningfully over time. This tells me how exposed we are if we rely on consent for marketing or data use.
    3. Are there open compliance issues sitting unresolved, and how serious are they? I want to understand the volume and severity of flagged incidents (things like suspicious data access or retention problems) and how quickly they tend to get closed out. I need to know if we have a backlog building up anywhere.
    4. How long are we actually holding onto shopper data, and does it match what our retention rules say it should be? I'm less interested in the rulebook and more in whether reality matches it — are there data types we're sitting on well past when we said we would delete them?
    5. Are there any legal holds in place right now, and what do they cover? I want to know how many are active, what they're blocking, and whether any look stale (i.e., should have been released a while ago). This matters for both litigation readiness and for explaining why certain deletions haven't happened.

    For now, could you scope the first request for me? I'd like to see it broken down in a way that shows me trend over the last several months, not just a single total — I want to catch whether we're improving or falling behind.

    Let me know if any of these need to be split further once you see what the data looks like.

    Thanks, Ines

  2. Priyanka Desai plans the night

    The analyst · week 1, night 4, request 1 of 3

    The pieces from the last three nights line up almost too neatly to leave unverified: the 3 'rejected' DSARs (night 3, report 1) never get completed_at stamped — which inflated my earlier backlog numbers — and their received_at dates (2023-05-26, 2024-07-09, 2025-03-21) sit exactly one day before the 3 active legal holds' placed_at dates (2023-05-27, 2024-07-10, 2025-03-22) for the same three tenants (chargeback disputes). This looks like 'erasure correctly rejected pending an active legal hold,' which would completely reframe item 1 from 'overdue backlog' to 'holds that are now 1-2.5 years stale' — feeding directly into item 5. I want a direct join to confirm the same subject_user_id is involved before I write that into the memo, rather than relying on date coincidence. Separately, night 3's retention landscape report (report 2) surfaced the clearest answer yet to item 4: email_log has a 730-day/delete policy configured for all 120 tenants, but it is active=0 and never_run=120 for every single one — meaning it's pure paperwork, never enforced. I want to size that gap in actual months of retained email data. As a contrast, audit_log looks 'healthy' on paper (active, last_run_at=2025-12-31, action=archive) — I want to check whether old rows are actually leaving the live table, since 'archive' could just as easily mean nothing is really being purged in practice. This is my last night, so I'm spending all 3 slots on the highest-value remaining checks rather than broad new territory.

    Linking rejected erasure requests to active legal holds
    Question
    Do the 3 long-open 'rejected' data subject requests correspond to the same shopper and tenant as the 3 active legal holds for payment disputes?
    One row per
    data_subject_requests record with status = 'rejected', left-joined to legal_holds on matching tenant_id and subject_user_id = legal_holds.subject_user_id
    Columns
    • dsar public_id
    • tenant name
    • dsar subject_user_id
    • dsar request type
    • dsar received_at (date)
    • matched legal hold name (null if no match)
    • legal hold subject_user_id (null if no match)
    • legal hold reason (truncated, null if no match)
    • legal hold is_active
    • legal hold placed_at (date)
    • days between dsar received_at and hold placed_at
  3. Ines writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    WITH joined AS (
      SELECT
        dsr.id AS dsr_id,
        dsr.public_id AS dsar_public_id,
        dsr.tenant_id AS dsr_tenant_id,
        dsr.subject_user_id AS dsar_subject_user_id,
        dsr.request_type AS dsar_request_type,
        dsr.received_at AS dsar_received_at,
        lh.id AS lh_id,
        lh.name AS legal_hold_name,
        lh.subject_user_id AS legal_hold_subject_user_id,
        lh.reason AS legal_hold_reason,
        lh.is_active AS legal_hold_is_active,
        lh.placed_at AS legal_hold_placed_at,
        ROW_NUMBER() OVER (
          PARTITION BY dsr.id
          ORDER BY ABS(COALESCE(DATE(lh.placed_at) - DATE(dsr.received_at), 999999))
        ) AS rn
      FROM data_subject_requests dsr
      LEFT JOIN legal_holds lh
        ON lh.tenant_id = dsr.tenant_id
       AND (
            (dsr.subject_user_id IS NOT NULL AND lh.subject_user_id = dsr.subject_user_id)
            OR
            (dsr.subject_user_id IS NULL AND ABS(DATE(lh.placed_at) - DATE(dsr.received_at)) <= 3)
       )
      WHERE dsr.status = 'rejected'
    )
    SELECT
      j.dsar_public_id,
      t.name AS tenant_name,
      j.dsar_subject_user_id,
      j.dsar_request_type,
      DATE(j.dsar_received_at) AS dsar_received_at,
      j.legal_hold_name,
      j.legal_hold_subject_user_id,
      LEFT(j.legal_hold_reason, 200) AS legal_hold_reason,
      j.legal_hold_is_active,
      DATE(j.legal_hold_placed_at) AS legal_hold_placed_at,
      (DATE(j.legal_hold_placed_at) - DATE(j.dsar_received_at)) AS days_between,
      CASE
        WHEN j.lh_id IS NULL THEN 'no match'
        WHEN j.dsar_subject_user_id IS NOT NULL THEN 'subject_user_id match'
        ELSE 'tenant + date window fallback'
      END AS join_strategy
    FROM joined j
    JOIN tenants t ON t.id = j.dsr_tenant_id
    WHERE j.rn = 1
    ORDER BY j.dsar_received_at ASC, j.dsar_public_id ASC

    “Query compiles correctly with 12 columns as expected. Submitting.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. The query: - Filters data_subject_requests to status = 'rejected' (expected 3 rows). - Attempts the exact match on tenant_id + subject_user_id first; automatically falls back to a tenant_id-only match against legal holds placed within 3 days of received_at whenever subject_user_id is null on the DSAR row, picking the closest-dated candidate if several qualify. - Exposes a join_strategy column so the DPO's analyst can see at a glance which matching path produced each row (or that no match was found). - Keeps the grain at one row per rejected DSAR, truncates the hold reason to 200 chars, and orders by received_at ascending as requested.”

  4. The report comes back overnight

    As Priyanka Desai saw it, from the small dataset the agents worked on

    dsar_public_id                       | tenant_name          | dsar_subject_user_id | dsar_request_type | dsar_received_at | legal_hold_name           | legal_hold_subject_user_id | legal_hold_reason                        | legal_hold_is_active | legal_hold_placed_at | days_between | join_strategy        
    -------------------------------------+----------------------+----------------------+-------------------+------------------+---------------------------+----------------------------+------------------------------------------+----------------------+----------------------+--------------+----------------------
    0bd4ff3e-19c5-44d7-9288-04b73f44dccb | Marrowby & Vane      |                 1933 | erasure           | 2023-05-26       | Chargeback dispute 841484 |                       1933 | Payment dispute pending with the card ne | true                 | 2023-05-27           |            1 | subject_user_id match
    e5fcf055-b01f-479e-b995-34d0c5e3eb8c | Rue Lafayette Studio |                 5911 | erasure           | 2024-07-09       | Chargeback dispute 214867 |                       5911 | Payment dispute pending with the card ne | true                 | 2024-07-10           |            1 | subject_user_id match
    89c20516-5f50-4f0e-99c9-616fb24a9eaf | Cragsley Outfitters  |                 6446 | erasure           | 2025-03-21       | Chargeback dispute 206882 |                       6446 | Payment dispute pending with the card ne | true                 | 2025-03-22           |            1 | subject_user_id match
    (3 rows)
  5. What came of it

    The memo and the queries that followed; links found by title, so inferred

    Later queries that build on this report:

    It builds on:

  6. It becomes benchmark query q121

    Changed only where the benchmark needs the same result on every system

    • editAn output alias reused the name of an input column the same block still uses; DataFusion rejects the query, which the standard resolves (docs/RUNNER.md, Dialects: a rewrite that costs nothing on any system goes into the canonical query).
    • spellingdate cast (8×)
    • determinismtie-breaker: legal hold id
    • made explicita selected t.col gets AS col, its standard name made explicit (6×)
    • placeholderdate diff days (3×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (4×)
    The benchmark text, against the submitted SQL
    WITH joined AS (  SELECT    dsr.id AS dsr_id,    dsr.public_id AS dsar_public_id,    dsr.tenant_id AS dsr_tenant_id,    dsr.subject_user_id AS dsar_subject_user_id,    dsr.request_type AS dsar_request_type,    dsr.received_at AS dsar_received_at,    lh.id AS lh_id,    lh.name AS legal_hold_name,    lh.subject_user_id AS legal_hold_subject_user_id,    lh.reason AS legal_hold_reason,    lh.is_active AS legal_hold_is_active,    lh.placed_at AS legal_hold_placed_at,    ROW_NUMBER() OVER (      PARTITION BY dsr.id      ORDER BY ABS(COALESCE(DATE(lh.placed_at) - DATE(dsr.received_at), 999999))      ORDER BY ABS(COALESCE({{date_diff_days(dsr.received_at, lh.placed_at)}}, 999999)) NULLS LAST, lh.id NULLS LAST    ) AS rn  FROM data_subject_requests dsr  LEFT JOIN legal_holds lh    ON lh.tenant_id = dsr.tenant_id   AND (        (dsr.subject_user_id IS NOT NULL AND lh.subject_user_id = dsr.subject_user_id)        OR        (dsr.subject_user_id IS NULL AND ABS(DATE(lh.placed_at) - DATE(dsr.received_at)) <= 3)        (dsr.subject_user_id IS NULL AND ABS({{date_diff_days(dsr.received_at, lh.placed_at)}}) <= 3)   )  WHERE dsr.status = 'rejected')SELECT  j.dsar_public_id,  j.dsar_public_id AS dsar_public_id,  t.name AS tenant_name,  j.dsar_subject_user_id,  j.dsar_request_type,  DATE(j.dsar_received_at) AS dsar_received_at,  j.legal_hold_name,  j.legal_hold_subject_user_id,  j.dsar_subject_user_id AS dsar_subject_user_id,  j.dsar_request_type AS dsar_request_type,  CAST(j.dsar_received_at AS DATE) AS dsar_received_on,  j.legal_hold_name AS legal_hold_name,  j.legal_hold_subject_user_id AS legal_hold_subject_user_id,  LEFT(j.legal_hold_reason, 200) AS legal_hold_reason,  j.legal_hold_is_active,  DATE(j.legal_hold_placed_at) AS legal_hold_placed_at,  (DATE(j.legal_hold_placed_at) - DATE(j.dsar_received_at)) AS days_between,  j.legal_hold_is_active AS legal_hold_is_active,  CAST(j.legal_hold_placed_at AS DATE) AS legal_hold_placed_at,  {{date_diff_days(j.dsar_received_at, j.legal_hold_placed_at)}} AS days_between,  CASE    WHEN j.lh_id IS NULL THEN 'no match'    WHEN j.dsar_subject_user_id IS NOT NULL THEN 'subject_user_id match'    ELSE 'tenant + date window fallback'  END AS join_strategyFROM joined jJOIN tenants t ON t.id = j.dsr_tenant_idWHERE j.rn = 1ORDER BY j.dsar_received_at ASC, j.dsar_public_id ASCORDER BY j.dsar_received_at ASC NULLS LAST, j.dsar_public_id ASC NULLS LAST
    WITH joined AS (
      SELECT
        dsr.id AS dsr_id,
        dsr.public_id AS dsar_public_id,
        dsr.tenant_id AS dsr_tenant_id,
        dsr.subject_user_id AS dsar_subject_user_id,
        dsr.request_type AS dsar_request_type,
        dsr.received_at AS dsar_received_at,
        lh.id AS lh_id,
        lh.name AS legal_hold_name,
        lh.subject_user_id AS legal_hold_subject_user_id,
        lh.reason AS legal_hold_reason,
        lh.is_active AS legal_hold_is_active,
        lh.placed_at AS legal_hold_placed_at,
        ROW_NUMBER() OVER (
          PARTITION BY dsr.id
          ORDER BY ABS(COALESCE(DATE_DIFF('day', dsr.received_at, lh.placed_at), 999999)) NULLS LAST, lh.id NULLS LAST
        ) AS rn
      FROM data_subject_requests dsr
      LEFT JOIN legal_holds lh
        ON lh.tenant_id = dsr.tenant_id
       AND (
            (dsr.subject_user_id IS NOT NULL AND lh.subject_user_id = dsr.subject_user_id)
            OR
            (dsr.subject_user_id IS NULL AND ABS(DATE_DIFF('day', dsr.received_at, lh.placed_at)) <= 3)
       )
      WHERE dsr.status = 'rejected'
    )
    SELECT
      j.dsar_public_id AS dsar_public_id,
      t.name AS tenant_name,
      j.dsar_subject_user_id AS dsar_subject_user_id,
      j.dsar_request_type AS dsar_request_type,
      CAST(j.dsar_received_at AS DATE) AS dsar_received_on,
      j.legal_hold_name AS legal_hold_name,
      j.legal_hold_subject_user_id AS legal_hold_subject_user_id,
      LEFT(j.legal_hold_reason, 200) AS legal_hold_reason,
      j.legal_hold_is_active AS legal_hold_is_active,
      CAST(j.legal_hold_placed_at AS DATE) AS legal_hold_placed_at,
      DATE_DIFF('day', j.dsar_received_at, j.legal_hold_placed_at) AS days_between,
      CASE
        WHEN j.lh_id IS NULL THEN 'no match'
        WHEN j.dsar_subject_user_id IS NOT NULL THEN 'subject_user_id match'
        ELSE 'tenant + date window fallback'
      END AS join_strategy
    FROM joined j
    JOIN tenants t ON t.id = j.dsr_tenant_id
    WHERE j.rn = 1
    ORDER BY j.dsar_received_at ASC NULLS LAST, j.dsar_public_id ASC NULLS LAST
    1 ms10 ms100 ms1 sHyper6.7 msDuckDB22.6 msGizmoSQL26.4 msclickhouse_brew121 msFirebolt35.5 msDataFusionerrorClickHouse18.3 ms
    • DataFusionError during planning: Plan: ["BoundedWindowAggExec: wdw=[row_number() PARTITION BY [dsr.id] ORDER BY [abs(coalesce(CAST(lh.placed_at AS Date32) - CAST(dsr.received_at AS Date32), Int64(999999))) ASC NULLS LAST, lh.id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: Field { \"row_n
  7. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Ines's whole session, 7 turns
    • The request, as Ines received it
      Report request from Priyanka Desai (analyst for the Data Protection Officer):
      
      Title: Linking rejected erasure requests to active legal holds
      Question: Do the 3 long-open 'rejected' data subject requests correspond to the same shopper and tenant as the 3 active legal holds for payment disputes?
      Grain: one row per data_subject_requests record with status = 'rejected', left-joined to legal_holds on matching tenant_id and subject_user_id = legal_holds.subject_user_id
      Columns:
      - dsar public_id
      - tenant name
      - dsar subject_user_id
      - dsar request type
      - dsar received_at (date)
      - matched legal hold name (null if no match)
      - legal hold subject_user_id (null if no match)
      - legal hold reason (truncated, null if no match)
      - legal hold is_active
      - legal hold placed_at (date)
      - days between dsar received_at and hold placed_at
      Details: Source: data_subject_requests filtered to status = 'rejected' (expect exactly 3 rows, from Marrowby & Vane, Rue Lafayette Studio, Cragsley Outfitters per night 3). LEFT JOIN legal_holds ON legal_holds.tenant_id = data_subject_requests.tenant_id AND legal_holds.subject_user_id = data_subject_requests.subject_user_id. If subject_user_id isn't populated on data_subject_requests for these rows, fall back to matching on tenant_id only and show legal_holds rows placed within 3 days of received_at as candidate matches, noting which join strategy was used. Order by dsar received_at ascending.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Ines calls dry_run
      WITH joined AS (
        SELECT
          dsr.id AS dsr_id,
          dsr.public_id AS dsar_public_id,
          dsr.tenant_id AS dsr_tenant_id,
          dsr.subject_user_id AS dsar_subject_user_id,
          dsr.request_type AS dsar_request_type,
          dsr.received_at AS dsar_received_at,
          lh.id AS lh_id,
          lh.name AS legal_hold_name,
          lh.subject_user_id AS legal_hold_subject_user_id,
          lh.reason AS legal_hold_reason,
          lh.is_active AS legal_hold_is_active,
          lh.placed_at AS legal_hold_placed_at,
          ROW_NUMBER() OVER (
            PARTITION BY dsr.id
            ORDER BY ABS(COALESCE(DATE(lh.placed_at) - DATE(dsr.received_at), 999999))
          ) AS rn
        FROM data_subject_requests dsr
        LEFT JOIN legal_holds lh
          ON lh.tenant_id = dsr.tenant_id
         AND (
              (dsr.subject_user_id IS NOT NULL AND lh.subject_user_id = dsr.subject_user_id)
              OR
              (dsr.subject_user_id IS NULL AND ABS(DATE(lh.placed_at) - DATE(dsr.received_at)) <= 3)
         )
        WHERE dsr.status = 'rejected'
      )
      SELECT
        j.dsar_public_id,
        t.name AS tenant_name,
        j.dsar_subject_user_id,
        j.dsar_request_type,
        DATE(j.dsar_received_at) AS dsar_received_at,
        j.legal_hold_name,
        j.legal_hold_subject_user_id,
        LEFT(j.legal_hold_reason, 200) AS legal_hold_reason,
        j.legal_hold_is_active,
        DATE(j.legal_hold_placed_at) AS legal_hold_placed_at,
        (DATE(j.legal_hold_placed_at) - DATE(j.dsar_received_at)) AS days_between,
        CASE
          WHEN j.lh_id IS NULL THEN 'no match'
          WHEN j.dsar_subject_user_id IS NOT NULL THEN 'subject_user_id match'
          ELSE 'tenant + date window fallback'
        END AS join_strategy
      FROM joined j
      JOIN tenants t ON t.id = j.dsr_tenant_id
      WHERE j.rn = 1
      ORDER BY j.dsar_received_at ASC, j.dsar_public_id ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: dsar_public_id, tenant_name, dsar_subject_user_id, dsar_request_type, dsar_received_at, legal_hold_name, legal_hold_subject_user_id, legal_hold_reason, legal_hold_is_active, legal_hold_placed_at, days_between, join_strategy
    • InesQuery compiles correctly with 12 columns as expected. Submitting.
    • Ines calls submit
      WITH joined AS (
        SELECT
          dsr.id AS dsr_id,
          dsr.public_id AS dsar_public_id,
          dsr.tenant_id AS dsr_tenant_id,
          dsr.subject_user_id AS dsar_subject_user_id,
          dsr.request_type AS dsar_request_type,
          dsr.received_at AS dsar_received_at,
          lh.id AS lh_id,
          lh.name AS legal_hold_name,
          lh.subject_user_id AS legal_hold_subject_user_id,
          lh.reason AS legal_hold_reason,
          lh.is_active AS legal_hold_is_active,
          lh.placed_at AS legal_hold_placed_at,
          ROW_NUMBER() OVER (
            PARTITION BY dsr.id
            ORDER BY ABS(COALESCE(DATE(lh.placed_at) - DATE(dsr.received_at), 999999))
          ) AS rn
        FROM data_subject_requests dsr
        LEFT JOIN legal_holds lh
          ON lh.tenant_id = dsr.tenant_id
         AND (
              (dsr.subject_user_id IS NOT NULL AND lh.subject_user_id = dsr.subject_user_id)
              OR
              (dsr.subject_user_id IS NULL AND ABS(DATE(lh.placed_at) - DATE(dsr.received_at)) <= 3)
         )
        WHERE dsr.status = 'rejected'
      )
      SELECT
        j.dsar_public_id,
        t.name AS tenant_name,
        j.dsar_subject_user_id,
        j.dsar_request_type,
        DATE(j.dsar_received_at) AS dsar_received_at,
        j.legal_hold_name,
        j.legal_hold_subject_user_id,
        LEFT(j.legal_hold_reason, 200) AS legal_hold_reason,
        j.legal_hold_is_active,
        DATE(j.legal_hold_placed_at) AS legal_hold_placed_at,
        (DATE(j.legal_hold_placed_at) - DATE(j.dsar_received_at)) AS days_between,
        CASE
          WHEN j.lh_id IS NULL THEN 'no match'
          WHEN j.dsar_subject_user_id IS NOT NULL THEN 'subject_user_id match'
          ELSE 'tenant + date window fallback'
        END AS join_strategy
      FROM joined j
      JOIN tenants t ON t.id = j.dsr_tenant_id
      WHERE j.rn = 1
      ORDER BY j.dsar_received_at ASC, j.dsar_public_id ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • InesSubmitted. The query: - Filters `data_subject_requests` to `status = 'rejected'` (expected 3 rows). - Attempts the exact match on `tenant_id` + `subject_user_id` first; automatically falls back to a `tenant_id`-only match against legal holds placed within 3 days of `received_at` whenever `subject_user_id` is null on the DSAR row, picking the closest-dated candidate if several qualify. - Exposes a `join_strategy` column so the DPO's analyst can see at a glance which matching path produced each row (or that no match was found). - Keeps the grain at one row per rejected DSAR, truncates the hold reason to 200 chars, and orders by `received_at` ascending as requested.