◐ Off-By-One · answer catalog

go-postgres-migration-ambiguous-column

1 answer(s)godocker

go-postgres-migration-ambiguous-column

📦 Source in repository (JSON)

Answer

Root cause: rollup_domains_daily() is an INSERT INTO metrics_domains_daily ... SELECT ... FROM shadow_attribution sa JOIN tool_events te ... whose SELECT list and GROUP BY referenced publisher_domain and user_id unqualified. Both columns exist in both tables, so Postgres raises ERROR: column reference "publisher_domain" is ambiguous — SQLSTATE 42702 — and the nightly rollup fails, leaving metrics_domains_daily empty.

Fix (3 parts):

1. Edit the base migration 001_*.up.sql for fresh installs

Every column that exists in both tables must be qualified (sa. = shadow_attribution, te. = tool_events):

CREATE OR REPLACE FUNCTION rollup_domains_daily(rollup_date date DEFAULT CURRENT_DATE)
RETURNS void AS $$
BEGIN
    INSERT INTO metrics_domains_daily (metric_date, publisher_domain, user_id, event_count, attributed_count)
    SELECT
        rollup_date,
        sa.publisher_domain,      -- was: publisher_domain  (ambiguous)
        sa.user_id,               -- was: user_id           (ambiguous)
        count(te.id),
        count(sa.id)
    FROM shadow_attribution sa
    JOIN tool_events te ON te.user_id = sa.user_id
                        AND te.publisher_domain = sa.publisher_domain
                        AND te.occurred_at::date = sa.attributed_at::date
    WHERE te.occurred_at::date = rollup_date
    GROUP BY sa.publisher_domain, sa.user_id;   -- was: GROUP BY publisher_domain, user_id
END;
$$ LANGUAGE plpgsql;

2. Add migration 015_rollup_domains_daily_fix.up.sql for live DBs

001 is already recorded in schema_migrations, so the migrate runner never re-executes it on live databases. The corrected function must be (re)defined in a new migration. CREATE OR REPLACE is safe here — it replaces the body of the existing function in place, so no DROP/re-grant dance is required:

-- 015_rollup_domains_daily_fix.up.sql
CREATE OR REPLACE FUNCTION rollup_domains_daily(rollup_date date DEFAULT CURRENT_DATE)
RETURNS void AS $$
BEGIN
    INSERT INTO metrics_domains_daily (metric_date, publisher_domain, user_id, event_count, attributed_count)
    SELECT rollup_date,
           sa.publisher_domain, sa.user_id,
           count(te.id), count(sa.id)
    FROM shadow_attribution sa
    JOIN tool_events te ON te.user_id = sa.user_id
                        AND te.publisher_domain = sa.publisher_domain
                        AND te.occurred_at::date = sa.attributed_at::date
    WHERE te.occurred_at::date = rollup_date
    GROUP BY sa.publisher_domain, sa.user_id;
END;
$$ LANGUAGE plpgsql;

-- 015_rollup_domains_daily_fix.down.sql
-- Re-create the original (buggy) body, or no-op with a comment; typically:
-- SELECT 1;  -- non-destructive; the fixed body is the desired state

3. Remove the test SKIP branch so post-conditions run

The integration test contained a branch that skipped the known failure:

// remove this whole block:
if os.Getenv("ROLLUP_SKIP_KNOWN_BUG") != "" {
    t.Skip("rollup_domains_daily has ambiguous-column bug (SQLSTATE 42702)")
}

After removal, the post-conditions execute and assert the fix:

func TestRollupDomainsDaily(t *testing.T) {
    seedAttributionsAndEvents(t)            // u1/example.com (2 events), u2/example.org (1 event)
    _, err := db.Exec("SELECT rollup_domains_daily()")
    if err != nil {
        t.Fatalf("rollup_domains_daily failed: %v", err) // was 42702 before fix
    }
    var n int
    if err := db.QueryRow(
        "SELECT count(*) FROM metrics_domains_daily WHERE metric_date = CURRENT_DATE",
    ).Scan(&n); err != nil {
        t.Fatal(err)
    }
    if n < 2 {
        t.Fatalf("expected >=2 rolled-up rows, got %d", n)
    }
}

Live verification (docker-compose)

docker compose build api && docker compose up -d api      # rebuild & restart
docker compose logs api | grep -i "migrat"                # expect: applied 015_rollup_domains_daily_fix
docker compose exec db psql -U app -d app -c "SELECT rollup_domains_daily();"
docker compose exec db psql -U app -d app -c "SELECT count(*) FROM metrics_domains_daily WHERE metric_date = CURRENT_DATE;"
docker compose logs api | grep -i "daily rollup failed"   # expect: no matches after fix

Evidence & signatures

I verified against a **real PostgreSQL 18.4 server** (spun up locally with `initdb`/`pg_ctl`), replicating the exact schema: `shadow_attribution` and `tool_events` both containing `user_id` and `publisher_domain`, plus `metrics_domains_daily` and `schema_migrations`.

1. **Bug reproduced (pre-fix, live-DB state):** applying the buggy function and running `SELECT rollup_domains_daily();` produced exactly the reported failure:
   ```
   ERROR:  column reference "publisher_domain" is ambiguous
   LINE 4:         publisher_domain, ...
   CONTEXT:  PL/pgSQL function rollup_domains_daily(date) line 3 at SQL statement
   ```
2. **015 fix (live-DB path):** with `001` recorded in `schema_migrations` (version 1 present) and the buggy function already in place, applying `015` via plain `CREATE OR REPLACE` succeeded (`CREATE FUNCTION`), the function then ran cleanly, and the post-condition returned **2 rows** in `metrics_domains_daily` with correct aggregates:
   - `example.com / u1 → event_count=2, attributed_count=2`
   - `example.org / u2 → event_count=1, attributed_count=1`
3. **Fresh-install path:** rebuilding from scratch using the edited `001` (qualified columns) produced the identical correct result — fresh installs no longer need `015`'s correction.
4. **Edge cases:**
   - **Empty source:** deleting all `tool_events` and rolling up a day with no activity → function returns successfully, 0 rows inserted, no error.
   - **Idempotency:** re-applying `015` (simulating a migrate re-run) is harmless — `CREATE OR REPLACE` succeeds again and `schema_migrations` is untouched.
   - **Qualification consistency:** every column in the SELECT list *and* the GROUP BY is qualified, so no expression can fall back to an ambiguous name even if the two tables gain more overlapping columns later.
5. **Test gating:** the removed SKIP branch was the only thing preventing the post-conditions from running; with it gone, `SELECT rollup_domains_daily()` + `count(*)` assertions execute and pass.
{"model": "deepseek-v4-flash", "problem_class": "go-postgres-migration-ambiguous-column", "result": "passed", "tests": 4}
Generated from the verified corpus · MIT licensedBack to the catalog