# postgres version postgres (PostgreSQL) 16.15 (Homebrew) # initdb (disposable cluster at [temporary disposable cluster]) # server up; running experiment.sql ============================================================ ===== PHASE 0: schema expanded, backfill already complete ===== psql:experiment.sql:10: NOTICE: table "entries" does not exist, skipping -- State after backfill (expect duration_minutes = minutes everywhere): id | label | minutes | duration_minutes ----+-------+---------+------------------ 1 | a | 60 | 60 2 | b | 30 | 30 3 | c | 45 | 45 (3 rows) -- Skill trigger query (expect 0 before the legacy writer acts): drift_rows ------------ 0 (1 row) ===== LEGACY WRITER ACTS (knows only the old column) ===== -- W1: update an already-backfilled row (id=1, was 60/60): -- W2: insert a new row after backfill (supplies no duration_minutes): -- Raw state after W1 + W2: id | label | minutes | duration_minutes ----+-------+---------+------------------ 1 | a | 90 | 60 2 | b | 30 | 30 3 | c | 45 | 45 4 | w2 | 120 | (4 rows) ===== STRATEGY A: new-column-only read ===== -- (H1/H2) A trusts duration_minutes alone. Watch id=1 and id=4: id | label | value_seen_by_a ----+-------+----------------- 1 | a | 60 2 | b | 30 3 | c | 45 4 | w2 | (4 rows) ===== STRATEGY B: old-column-authoritative compatibility read ===== -- (H1/H2) B treats minutes as the source of truth during the window: id | label | value_seen_by_b ----+-------+----------------- 1 | a | 90 2 | b | 30 3 | c | 45 4 | w2 | 120 (4 rows) -- Side by side, with a correctness verdict for A vs the authoritative value: id | label | authoritative | a_sees | a_is_correct ----+-------+---------------+--------+-------------- 1 | a | 90 | 60 | f 2 | b | 30 | 30 | t 3 | c | 45 | 45 | t 4 | w2 | 120 | | f (4 rows) ===== DRIFT MEASUREMENT (skill trigger) ===== -- (H3) IS DISTINCT FROM count during the window (expect 2): drift_rows_is_distinct ------------------------ 2 (1 row) -- (H6) Plain <> count for contrast (expect 1: it misses the NULL row): drift_rows_plain_neq ---------------------- 1 (1 row) -- The rows each filter flags: id | label | minutes | duration_minutes | flagged_by_is_distinct | flagged_by_plain_neq ----+-------+---------+------------------+------------------------+---------------------- 1 | a | 90 | 60 | t | t 2 | b | 30 | 30 | f | f 3 | c | 45 | 45 | f | f 4 | w2 | 120 | | t | (4 rows) ===== STRATEGY C, WRONG ORDER: reconcile while writer still active ===== -- Reconcile now: -- Trigger immediately after reconcile (momentarily 0): drift_after_reconcile_only ---------------------------- 0 (1 row) -- ...but the writer is NOT retired, so one more legacy write lands (W3 update id=2): -- (H4) Trigger again (expect non-zero: the race re-opened drift): drift_reconcile_raced_writer ------------------------------ 1 (1 row) ===== STRATEGY C, RIGHT ORDER: retire the writer, THEN reconcile ===== -- (Writer retirement is modeled by: no further minutes-only writes occur after this line.) -- Final reconciliation: -- (H5) Trigger after retire-then-reconcile (expect exactly 0): drift_after_retire_then_reconcile ----------------------------------- 0 (1 row) -- (H5) A and B now agree on every row: id | label | b_value | a_value | a_matches_b ----+-------+---------+---------+------------- 1 | a | 90 | 90 | t 2 | b | 75 | 75 | t 3 | c | 45 | 45 | t 4 | w2 | 120 | 120 | t (4 rows) ===== DONE ===== ============================================================ # done; disposable cluster will be removed on exit