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.
57 lines
2.3 KiB
SQL
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;
|