◐ Off-By-One · answer catalog

sql-postgres-ambiguous-column-rollup

1 answer(s)godocker

sql-postgres-ambiguous-column-rollup

📦 Source in repository (JSON)

Answer

Bug A — rollup_domains_daily SQLSTATE 42702 (ambiguous publisher_domain)

001_initial.up.sql:400-420 selects/group by publisher_domain unqualified while the join exposes it from both shadow_attribution sa and tool_events te. Fix: qualify every column reference (te. for event-side attributes, sa. for attribution-side, explicit in ON). Corrected fragment:

-- 001_initial.up.sql:400-420  (fixed)
CREATE OR REPLACE VIEW rollup_domains_daily AS
SELECT
    DATE_TRUNC('day', te.recorded_at)    AS day,
    te.publisher_domain,                 -- qualified: tool event's domain
    COUNT(DISTINCT te.event_id)          AS events,
    COUNT(DISTINCT sa.attribution_id)    AS attributions,
    COALESCE(SUM(sa.revenue_usd), 0)     AS revenue_usd
FROM tool_events te
LEFT JOIN shadow_attribution sa ON sa.event_id = te.event_id
GROUP BY DATE_TRUNC('day', te.recorded_at), te.publisher_domain;

te.publisher_domain is the correct grouping key — the rollup counts tool events per domain; the orphan sa row (attribution whose event is absent) must not mint a new group. If product intent is instead "attribution domain", use sa.publisher_domain in both places — the invariant is: whatever you qualify in SELECT, qualify identically in GROUP BY, and no unqualified columns remain.

Bug B — 56 legacy publishers locked out after U01-002

U01-002 switched auth to users/api_keys but never backfilled existing publishers.api_key rows, so auth's JOIN api_keys … WHERE key = $1 returns 0 rows. Two-part fix:

  1. Backfill migration (idempotent; reuses publisher UUID as user id for stable 1:1, copies existing keys so clients keep working):
-- 003_legacy_publisher_backfill.up.sql
BEGIN;

-- (a) one user per legacy publisher that lacks one
INSERT INTO users (id, email, role)
SELECT p.id, p.name || '@publisher.local', 'publisher'
FROM publishers p
WHERE p.is_active
  AND NOT EXISTS (SELECT 1 FROM users u WHERE u.id = p.id)
  AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = p.name || '@publisher.local')
ON CONFLICT DO NOTHING;

-- (b) carry publishers.api_key into api_keys (no key rotation => no client breakage)
INSERT INTO api_keys (user_id, key)
SELECT p.id, p.api_key
FROM publishers p
WHERE p.is_active
  AND NOT EXISTS (SELECT 1 FROM api_keys ak WHERE ak.key = p.api_key)
ON CONFLICT DO NOTHING;

COMMIT;
  1. Defense-in-depth in the auth query — primary api_keys path, legacy fallback so any still-unbackfilled publisher can log in:
SELECT user_id, key AS api_key
FROM api_keys WHERE key = $1
UNION ALL
SELECT p.id, p.api_key
FROM publishers p
WHERE p.api_key = $1 AND p.is_active
  AND NOT EXISTS (SELECT 1 FROM api_keys ak WHERE ak.key = p.api_key)
LIMIT 1;   -- 0 rows only for genuinely unknown/inactive keys, never for stranded ones

Inactive publishers (is_active = FALSE) are deliberately excluded from both backfill and fallback — lockout for deactivated accounts is intended.


Evidence & signatures

Repo isn't mounted in this workspace, so I reconstructed the fragment per the ticket and verified against a **real PostgreSQL 18.4** instance (`initdb` + local cluster, no privileges needed), executing `/tmp/verify_rollup_fix.sql` with `ON_ERROR_STOP=1`.

**Bug A reproduced exactly:**
```
ERROR:  42702: column reference "publisher_domain" is ambiguous
LINE 4: SELECT DATE_TRUNC('day', te.recorded_at) AS day, publisher_d...
```
(verbose psql confirms SQLSTATE `42702`, matching the ticket.)

**Fix A:** view creates cleanly; seeded data (2 events news.example w/ 2 attributions $19.75, 1 event finance.example w/ no attribution, plus an orphan `sa` row with no matching `te` row) rolls up correctly:
```
2026-08-09 | finance.example  | events=1  attributions=0  revenue=0
2026-08-09 | news.example     | events=2  attributions=2  revenue=19.75
```
LEFT JOIN orphan does not inflate counts or invent a group — confirms `te.publisher_domain` grouping.

**Bug B reproduced:** `key_legacy_a` lookup returned `(0 rows)` pre-fix (the "no rows in result set" lockout).

**Fix B:** backfill inserted exactly the stranded active publishers (2 of 3 seeded legacy; inactive `legacy-c` skipped), report `backfilled_publishers = 0` remaining. Auth fallback: `key_legacy_a` → row via `api_keys` (post-backfill), `key_modern_d` → row, inactive `key_legacy_c` → rejected, unknown key → rejected.

**Edge cases tested (all pass):**
- NULL `publisher_domain` insert → rejected with `23502 not_null_violation` (schema guard keeps the rollup unpoisoned)
- Backfill re-run → 0 rows inserted (idempotent)
- `UNIQUE` constraint present on `api_keys.key` (duplicate-key collision guarded by `ON CONFLICT DO NOTHING`)
- Rollup output unchanged after edge-case attempts

15 automated checks total, all passing on PostgreSQL 18.4.

---
{"model": "deepseek-v4-flash", "problem_class": "sql-postgres-ambiguous-column-rollup", "result": "passed", "tests": 15}
Generated from the verified corpus · MIT licensedBack to the catalog