Files
maarcadetweet/migrations/appview/0005_profiles.sql
T
tomdebone 6ebf17b493 fix(appview): resolve_profile also looks up DID in profiles cache
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/<handle>` now returns the user's profile
metadata even when they have no posts indexed yet.
2026-07-18 18:12:00 +02:00

57 lines
2.3 KiB
SQL

-- AppView database schema 0005: profile metadata + per-post avatar refs.
--
-- Why
-- The Profile-View-Page and PostCard both render a user avatar.
-- Fetching the live `app.bsky.actor.profile/self` record from every
-- user's PDS on every render doesn't scale, and isn't always reachable
-- (e.g. a did:web: user whose PDS is offline). We cache the
-- denormalised profile fields the UI shows keyed by did, indexed by
-- handle so the `/api/profile/<handle>` lookup is index-driven.
--
-- Source of truth: the user's own PDS. The PDS pushes profile records
-- via the existing `/internal/ingest-commit` path; this migration
-- adds the `(collection, action, rkey) == ('app.bsky.actor.profile',
-- 'create', 'self')` arm to the AppView's indexer to populate this
-- table.
--
-- All fields nullable: a profile record can omit displayName,
-- description, avatar, banner independently.
--
-- post_count / follower_count / following_count are denormalised
-- counts populated only when the row is created/replaced; the
-- Profile-View-Page reads them here so it doesn't have to issue a
-- separate COUNT(*) over posts/follows.
--
-- The avatar_cid on posts is the resolved profile-avatar blob ref
-- (or NULL) for the post's author. The AppView fills it in at
-- upsert_post-time from the profiles table; the PostCard reads it
-- to inline an <Avatar cid={post.avatar_cid}/> without a per-row
-- PDS round-trip.
CREATE TABLE profiles (
did TEXT PRIMARY KEY,
handle TEXT NOT NULL,
display_name TEXT,
description TEXT,
avatar_cid TEXT,
banner_cid TEXT,
post_count BIGINT NOT NULL DEFAULT 0,
follower_count BIGINT NOT NULL DEFAULT 0,
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
-- resolved through the posts.did → posts.handle mapping. The display
-- fields stay NULL — they need the live PDS profile record.
INSERT INTO profiles (did, handle)
SELECT DISTINCT ON (did) did, handle
FROM posts
WHERE handle <> ''
ORDER BY did, indexed_at DESC
ON CONFLICT (did) DO NOTHING;
ALTER TABLE posts ADD COLUMN IF NOT EXISTS avatar_cid TEXT;