go-postgres-migration-ambiguous-column
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):
001_*.up.sql for fresh installsEvery 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;
015_rollup_domains_daily_fix.up.sql for live DBs001 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
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)
}
}
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
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}