From 6ebf17b4939fac6ea21757c9a89b99aee18fb037 Mon Sep 17 00:00:00 2001 From: tomdebone Date: Sat, 18 Jul 2026 18:12:00 +0200 Subject: [PATCH] fix(appview): resolve_profile also looks up DID in profiles cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user who set their profile via the PDS push path before posting anything has a row in `profiles` but no rows in `posts` — the handle→DID lookup in `resolve_profile` only checked `posts`, so the route synthesised an empty profile (`did: ""`) for these users. The fix: query `profiles` first, fall back to `posts` only when there's no profile row. The new query uses `LOWER(handle) = LOWER($1)` against the `profiles_handle_idx` index (which is therefore no longer dead weight and stays in 0005; 0007 still drops it idempotently in case future edits reintroduce the original 'only-by-DID' pattern). Verified end-to-end against a running dev stack: `GET /api/profile/` now returns the user's profile metadata even when they have no posts indexed yet. --- crates/appview/src/routes.rs | 24 +++++++++++++++++++++--- migrations/appview/0005_profiles.sql | 1 + 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/appview/src/routes.rs b/crates/appview/src/routes.rs index 2c32b71..243d42f 100644 --- a/crates/appview/src/routes.rs +++ b/crates/appview/src/routes.rs @@ -343,13 +343,31 @@ async fn resolve_profile( // Order by `indexed_at DESC` so we get the most recent DID for // this handle (a single user can re-use a handle if account // history allows, but the latest is the active one). - sqlx::query_scalar::<_, String>( - "SELECT did FROM posts WHERE handle = $1 ORDER BY indexed_at DESC LIMIT 1", + // + // Prefer the `profiles` cache over `posts` — a user can have + // a profile row (set via PDS push before posting) but no posts + // yet, and we want the profile page to render with the right + // DID rather than synthesise an empty one. + let row = sqlx::query_scalar::<_, String>( + "SELECT did FROM profiles WHERE LOWER(handle) = LOWER($1) \ + ORDER BY indexed_at DESC LIMIT 1", ) .bind(h) .fetch_optional(&state.db) .await - .map_err(db_err)? + .map_err(db_err)?; + if row.is_some() { + row + } else { + sqlx::query_scalar::<_, String>( + "SELECT did FROM posts WHERE handle = $1 \ + ORDER BY indexed_at DESC LIMIT 1", + ) + .bind(h) + .fetch_optional(&state.db) + .await + .map_err(db_err)? + } } else { None }; diff --git a/migrations/appview/0005_profiles.sql b/migrations/appview/0005_profiles.sql index 3ec7fdc..8c9b9dc 100644 --- a/migrations/appview/0005_profiles.sql +++ b/migrations/appview/0005_profiles.sql @@ -40,6 +40,7 @@ CREATE TABLE profiles ( following_count BIGINT NOT NULL DEFAULT 0, indexed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE INDEX profiles_handle_idx ON profiles (LOWER(handle)); CREATE INDEX profiles_indexed_at_idx ON profiles (indexed_at DESC); -- Backfill: seed a profile row for every handle we've already