DB Olympics

Queries · q120 · Data Protection Officer · week 1, night 3

Legal holds: active scope, coverage, and staleness

“How many legal holds are currently active, what do they cover, and are any stale (placed long ago with no recent review or release)?”

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 SQLFelixJunior engineer who nests subqueries in FROM and WHERE (IN, EXISTS, correlated subqueries) rather than CTEs.

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 3, request 3 of 3

    Report 1's big finding: all 3 of the long-overdue DSARs are status='rejected' yet have completed_at = NULL — my original monthly report (night 1) treated 'completed_at IS NULL' as the sole "still open" signal, which may be wrongly counting rejected (i.e., actually-decided) requests as backlog. Before I hand Ines a number, I need to know whether 'rejected' is a systemic gap (completed_at never set on rejections) or an isolated failure on these 3 — that reshapes the whole answer to her question 1, so it's the top priority tonight. Report 3 (retention policy inventory) was dominated by one audit_log policy repeated per tenant and got truncated at 50 of 720 rows before showing any other entity_table — I need a properly aggregated view of what policies actually exist before I can check reality against them (item 4). With only this night and one more left, I'm also opening item 5 (legal holds) tonight since it looks like a single, self-contained report I can likely close in one shot, leaving night 4 free for the retention actual-vs-policy comparison and any remaining loose ends.

    Legal holds: active scope, coverage, and staleness
    Question
    How many legal holds are currently active, what do they cover, and are any stale (placed long ago with no recent review or release)?
    One row per
    legal_holds record
    Columns
    • tenant name
    • hold name
    • reason (truncated)
    • entity table scope (if set)
    • subject user id (if set, scope is a specific data subject)
    • is active
    • placed at (date)
    • released at (date, blank if still active)
    • days since placed (2026-01-05 minus placed_at, for active holds only)
  3. Felix 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)
    SELECT t.name AS tenant_name,
           lh.name AS hold_name,
           LEFT(lh.reason, 200) AS reason_truncated,
           lh.entity_table AS entity_table_scope,
           lh.subject_user_id,
           lh.is_active,
           CAST(lh.placed_at AS date) AS placed_at_date,
           CAST(lh.released_at AS date) AS released_at_date,
           CASE WHEN lh.is_active THEN (DATE '2026-01-05' - CAST(lh.placed_at AS date)) END AS days_since_placed
    FROM legal_holds lh
    JOIN tenants t ON t.id = lh.tenant_id
    ORDER BY lh.is_active DESC, lh.placed_at ASC, lh.id ASC

    “Query compiles cleanly with the expected columns and grain. Submitting now.”

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

    “Submitted. Summary of what the report does: - Grain: one row per legal_holds record. - Columns: tenant name, hold name, truncated reason (200 chars), entity table scope, subject user id, is_active, placed_at date, released_at date (blank if still active), and days since placed (computed only for active holds, using the fixed date 2026-01-05). - Ordering: active holds first, then oldest placed_at first within each group — so any stale, long-open holds surface at the top. - Note to analyst: I didn't impose a manual row limit; the standard reporting cap (50 rows) and its automatic total-row count will tell us whether the active+released holds exceed that and warrant a narrower follow-up query (e.g., active-only).”

  4. The report comes back overnight

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

    tenant_name          | hold_name                 | reason_truncated                         | entity_table_scope | subject_user_id | is_active | placed_at_date | released_at_date | days_since_placed
    ---------------------+---------------------------+------------------------------------------+--------------------+-----------------+-----------+----------------+------------------+------------------
    Marrowby & Vane      | Chargeback dispute 841484 | Payment dispute pending with the card ne |               NULL |            1933 | true      | 2023-05-27     |             NULL |               954
    Rue Lafayette Studio | Chargeback dispute 214867 | Payment dispute pending with the card ne |               NULL |            5911 | true      | 2024-07-10     |             NULL |               544
    Cragsley Outfitters  | Chargeback dispute 206882 | Payment dispute pending with the card ne |               NULL |            6446 | true      | 2025-03-22     |             NULL |               289
    (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:

  6. It becomes benchmark query q120

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

    • made explicita selected t.col gets AS col, its standard name made explicit (2×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (3×)
    The benchmark text, against the submitted SQL
    SELECT t.name AS tenant_name,       lh.name AS hold_name,       LEFT(lh.reason, 200) AS reason_truncated,       lh.entity_table AS entity_table_scope,       lh.subject_user_id,       lh.is_active,       lh.subject_user_id AS subject_user_id,       lh.is_active AS is_active,       CAST(lh.placed_at AS date) AS placed_at_date,       CAST(lh.released_at AS date) AS released_at_date,       CASE WHEN lh.is_active THEN (DATE '2026-01-05' - CAST(lh.placed_at AS date)) END AS days_since_placedFROM legal_holds lhJOIN tenants t ON t.id = lh.tenant_idORDER BY lh.is_active DESC, lh.placed_at ASC, lh.id ASCORDER BY lh.is_active DESC NULLS FIRST, lh.placed_at ASC NULLS LAST, lh.id ASC NULLS LAST
    SELECT t.name AS tenant_name,
           lh.name AS hold_name,
           LEFT(lh.reason, 200) AS reason_truncated,
           lh.entity_table AS entity_table_scope,
           lh.subject_user_id AS subject_user_id,
           lh.is_active AS is_active,
           CAST(lh.placed_at AS date) AS placed_at_date,
           CAST(lh.released_at AS date) AS released_at_date,
           CASE WHEN lh.is_active THEN (DATE '2026-01-05' - CAST(lh.placed_at AS date)) END AS days_since_placed
    FROM legal_holds lh
    JOIN tenants t ON t.id = lh.tenant_id
    ORDER BY lh.is_active DESC NULLS FIRST, lh.placed_at ASC NULLS LAST, lh.id ASC NULLS LAST
    1 ms10 ms100 msHyper3.3 msDuckDB3.4 msGizmoSQL5.9 msclickhouse_brew31.9 msFirebolt18.1 msDataFusion10.7 msClickHouse7.6 ms
  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

    Felix's whole session, 7 turns
    • The request, as Felix received it
      Report request from Priyanka Desai (analyst for the Data Protection Officer):
      
      Title: Legal holds: active scope, coverage, and staleness
      Question: How many legal holds are currently active, what do they cover, and are any stale (placed long ago with no recent review or release)?
      Grain: one row per legal_holds record
      Columns:
      - tenant name
      - hold name
      - reason (truncated)
      - entity table scope (if set)
      - subject user id (if set, scope is a specific data subject)
      - is active
      - placed at (date)
      - released at (date, blank if still active)
      - days since placed (2026-01-05 minus placed_at, for active holds only)
      Details: Source: legal_holds joined to tenants for the name. Include all rows (active and released) so released ones give context on typical hold duration, but order so active holds print first, oldest placed_at first, to surface any that look stale (placed many months/years ago and never released). Use literal date 2026-01-05 for age math, not CURRENT_DATE. If the table has close to or more than 50 rows, note the total count so we know whether this needs a narrower follow-up.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Felix calls dry_run
      SELECT t.name AS tenant_name,
             lh.name AS hold_name,
             LEFT(lh.reason, 200) AS reason_truncated,
             lh.entity_table AS entity_table_scope,
             lh.subject_user_id,
             lh.is_active,
             CAST(lh.placed_at AS date) AS placed_at_date,
             CAST(lh.released_at AS date) AS released_at_date,
             CASE WHEN lh.is_active THEN (DATE '2026-01-05' - CAST(lh.placed_at AS date)) END AS days_since_placed
      FROM legal_holds lh
      JOIN tenants t ON t.id = lh.tenant_id
      ORDER BY lh.is_active DESC, lh.placed_at ASC, lh.id ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: tenant_name, hold_name, reason_truncated, entity_table_scope, subject_user_id, is_active, placed_at_date, released_at_date, days_since_placed
    • FelixQuery compiles cleanly with the expected columns and grain. Submitting now.
    • Felix calls submit
      SELECT t.name AS tenant_name,
             lh.name AS hold_name,
             LEFT(lh.reason, 200) AS reason_truncated,
             lh.entity_table AS entity_table_scope,
             lh.subject_user_id,
             lh.is_active,
             CAST(lh.placed_at AS date) AS placed_at_date,
             CAST(lh.released_at AS date) AS released_at_date,
             CASE WHEN lh.is_active THEN (DATE '2026-01-05' - CAST(lh.placed_at AS date)) END AS days_since_placed
      FROM legal_holds lh
      JOIN tenants t ON t.id = lh.tenant_id
      ORDER BY lh.is_active DESC, lh.placed_at ASC, lh.id ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • FelixSubmitted. Summary of what the report does: - **Grain:** one row per `legal_holds` record. - **Columns:** tenant name, hold name, truncated reason (200 chars), entity table scope, subject user id, is_active, placed_at date, released_at date (blank if still active), and days since placed (computed only for active holds, using the fixed date 2026-01-05). - **Ordering:** active holds first, then oldest `placed_at` first within each group — so any stale, long-open holds surface at the top. - **Note to analyst:** I didn't impose a manual row limit; the standard reporting cap (50 rows) and its automatic total-row count will tell us whether the active+released holds exceed that and warrant a narrower follow-up query (e.g., active-only).