diff --git a/migrations/2025-11-01-114838_actors/up.sql b/migrations/2025-11-01-114838_actors/up.sql index d08e17ad..2061e3e4 100644 --- a/migrations/2025-11-01-114838_actors/up.sql +++ b/migrations/2025-11-01-114838_actors/up.sql @@ -13,7 +13,6 @@ CREATE TYPE actor_status AS ENUM ('active', 'takendown', 'suspended', 'deleted', 'deactivated'); CREATE TYPE actor_sync_state AS ENUM ('synced', 'dirty', 'partial', 'processing'); -CREATE TYPE unresolved_reason AS ENUM ('pending', 'deleted', 'forbidden', 'not_found'); -- ============================================================================= -- PREFERENCE TYPES @@ -40,41 +39,16 @@ CREATE TABLE actors ( sync_state actor_sync_state NOT NULL DEFAULT 'dirty', repo_rev TEXT, repo_cid BYTEA, - last_indexed TIMESTAMPTZ + last_indexed TIMESTAMPTZ, + account_created_at TIMESTAMPTZ ); CREATE INDEX idx_actors_handle ON actors(handle) WHERE handle IS NOT NULL; CREATE INDEX idx_actors_sync_state ON actors(sync_state); CREATE INDEX idx_actors_status ON actors(status); CREATE INDEX idx_actors_last_indexed ON actors(last_indexed) WHERE last_indexed IS NOT NULL; +CREATE INDEX idx_actors_account_created_at ON actors(account_created_at) WHERE account_created_at IS NOT NULL; --- ============================================================================= --- UNRESOLVED REFERENCES --- ============================================================================= --- Tracks posts that haven't been received yet or have been deleted. --- Enables 100 replies to reference a single unresolved CID via FK reuse. --- Stores AT URI components (actor_did, rkey) for reconstruction. - -CREATE TABLE unresolved_refs ( - id SERIAL PRIMARY KEY, - cid BYTEA NOT NULL UNIQUE, - reason unresolved_reason NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 0, - actor_did TEXT NOT NULL, - rkey INT8 NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - last_checked TIMESTAMPTZ -); - -CREATE INDEX idx_unresolved_refs_reason ON unresolved_refs(reason); -CREATE INDEX idx_unresolved_refs_created_at ON unresolved_refs(created_at); - --- Allowlist for managing which actors to index -CREATE TABLE allowlist ( - actor_id INTEGER PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - description TEXT -); -- ============================================================================= -- PROFILES diff --git a/migrations/2025-11-01-114839_posts/down.sql b/migrations/2025-11-01-114839_posts/down.sql index 01c7ed69..bcf568b5 100644 --- a/migrations/2025-11-01-114839_posts/down.sql +++ b/migrations/2025-11-01-114839_posts/down.sql @@ -13,9 +13,15 @@ DROP TABLE IF EXISTS post_embed_images; DROP TABLE IF EXISTS posts; DROP TABLE IF EXISTS uris; +-- Drop functions +DROP FUNCTION IF EXISTS i64_to_tid(BIGINT); +DROP FUNCTION IF EXISTS tid_to_i64(TEXT); +DROP FUNCTION IF EXISTS tid_timestamp(BIGINT); + -- Drop types DROP TYPE IF EXISTS facet_type; DROP TYPE IF EXISTS language_code; DROP TYPE IF EXISTS caption_mime_type; DROP TYPE IF EXISTS video_mime_type; DROP TYPE IF EXISTS embed_type; +DROP TYPE IF EXISTS post_status; diff --git a/migrations/2025-11-01-114839_posts/up.sql b/migrations/2025-11-01-114839_posts/up.sql index b29ccb87..4a0a73b4 100644 --- a/migrations/2025-11-01-114839_posts/up.sql +++ b/migrations/2025-11-01-114839_posts/up.sql @@ -11,6 +11,7 @@ -- POST & EMBED TYPES -- ============================================================================= +CREATE TYPE post_status AS ENUM ('complete', 'stub', 'deleted', 'forbidden'); CREATE TYPE embed_type AS ENUM ('images', 'video', 'external', 'record', 'record_with_media'); CREATE TYPE video_mime_type AS ENUM ('video/mp4', 'video/webm', 'video/quicktime', 'video/mpeg'); CREATE TYPE caption_mime_type AS ENUM ('text/vtt'); @@ -47,46 +48,102 @@ CREATE TYPE facet_type AS ENUM ('mention', 'link', 'tag'); -- POSTS -- ============================================================================= +-- ============================================================================= +-- TID HELPER FUNCTIONS +-- ============================================================================= + +-- Extract timestamp from TID (INT8) +CREATE OR REPLACE FUNCTION tid_timestamp(tid_int8 BIGINT) +RETURNS TIMESTAMPTZ AS $$ +BEGIN + -- Extract timestamp (53 bits) by right shifting 10 bits (clock ID) + -- Convert microseconds to PostgreSQL timestamp + RETURN to_timestamp((tid_int8 >> 10) / 1000000.0); +END; +$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; + +-- Decode base32-sortable TID string to INT8 +CREATE OR REPLACE FUNCTION tid_to_i64(tid TEXT) RETURNS BIGINT AS $$ +DECLARE + alphabet TEXT := '234567abcdefghijklmnopqrstuvwxyz'; + result BIGINT := 0; + char_pos INTEGER; + value INTEGER; + i INTEGER; +BEGIN + IF LENGTH(tid) != 13 THEN + RAISE EXCEPTION 'TID must be 13 characters, got %', LENGTH(tid); + END IF; + IF POSITION(SUBSTRING(tid FROM 1 FOR 1) IN '234567abcdefghij') = 0 THEN + RAISE EXCEPTION 'Invalid first character in TID: %', SUBSTRING(tid FROM 1 FOR 1); + END IF; + FOR i IN 1..13 LOOP + char_pos := POSITION(SUBSTRING(tid FROM i FOR 1) IN alphabet); + IF char_pos = 0 THEN + RAISE EXCEPTION 'Invalid character in TID at position %: %', i, SUBSTRING(tid FROM i FOR 1); + END IF; + value := char_pos - 1; + result := (result << 5) | value; + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql IMMUTABLE STRICT; + +COMMENT ON FUNCTION tid_to_i64(TEXT) IS 'Decodes a base32-sortable TID string (13 characters) to a BIGINT. Uses AT Protocol TID encoding: https://atproto.com/specs/tid'; + +-- Encode INT8 to base32-sortable TID string +CREATE OR REPLACE FUNCTION i64_to_tid(value BIGINT) RETURNS TEXT AS $$ +DECLARE + alphabet TEXT := '234567abcdefghijklmnopqrstuvwxyz'; + result TEXT := ''; + char_index INTEGER; + i INTEGER; +BEGIN + FOR i IN 1..13 LOOP + char_index := (value & 31) + 1; + result := SUBSTRING(alphabet FROM char_index FOR 1) || result; + value := value >> 5; + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql IMMUTABLE STRICT; + +COMMENT ON FUNCTION i64_to_tid(BIGINT) IS 'Encodes a BIGINT to a base32-sortable TID string (13 characters). Uses AT Protocol TID encoding: https://atproto.com/specs/tid'; + CREATE TABLE posts ( id BIGSERIAL PRIMARY KEY, actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - content TEXT NOT NULL, + content BYTEA, + content_version SMALLINT, language_code language_code, tags TEXT[] NOT NULL DEFAULT '{}', - -- Resolved references (post exists in our database) parent_post_id BIGINT REFERENCES posts(id) ON DELETE SET NULL, root_post_id BIGINT REFERENCES posts(id) ON DELETE SET NULL, - -- Unresolved references (post pending/deleted/forbidden/not_found) - unresolved_parent_id INTEGER REFERENCES unresolved_refs(id) ON DELETE SET NULL, - unresolved_root_id INTEGER REFERENCES unresolved_refs(id) ON DELETE SET NULL, embed_type embed_type, embed_subtype embed_type, violates_threadgate BOOLEAN NOT NULL DEFAULT FALSE, + status post_status NOT NULL DEFAULT 'complete', + search_vector tsvector, UNIQUE (actor_id, rkey), - -- Ensure exactly one parent reference type (resolved XOR unresolved, or neither for root posts) - CHECK ( - (parent_post_id IS NULL AND unresolved_parent_id IS NULL) OR -- Root post - (parent_post_id IS NOT NULL AND unresolved_parent_id IS NULL) OR -- Resolved - (parent_post_id IS NULL AND unresolved_parent_id IS NOT NULL) -- Unresolved - ), - -- Ensure exactly one root reference type CHECK ( - (root_post_id IS NULL AND unresolved_root_id IS NULL) OR - (root_post_id IS NOT NULL AND unresolved_root_id IS NULL) OR - (root_post_id IS NULL AND unresolved_root_id IS NOT NULL) + (content IS NULL AND content_version IS NULL) OR + (content IS NOT NULL AND content_version IS NOT NULL) ) ); +COMMENT ON COLUMN posts.content IS 'Zstd-compressed post content (BYTEA). NULL for stub/deleted posts.'; +COMMENT ON COLUMN posts.content_version IS 'Dictionary version (NULL if no content)'; +COMMENT ON COLUMN posts.search_vector IS 'Full-text search vector (NULL except for allowlisted users)'; +COMMENT ON COLUMN posts.status IS 'Post lifecycle state: complete (normal), stub (placeholder), deleted (soft-deleted), forbidden (access denied)'; + CREATE INDEX idx_posts_actor ON posts(actor_id); -CREATE INDEX idx_posts_created_at ON posts(created_at DESC); CREATE INDEX idx_posts_parent ON posts(parent_post_id) WHERE parent_post_id IS NOT NULL; CREATE INDEX idx_posts_root ON posts(root_post_id) WHERE root_post_id IS NOT NULL; -CREATE INDEX idx_posts_unresolved_parent ON posts(unresolved_parent_id) WHERE unresolved_parent_id IS NOT NULL; -CREATE INDEX idx_posts_unresolved_root ON posts(unresolved_root_id) WHERE unresolved_root_id IS NOT NULL; CREATE INDEX idx_posts_tags ON posts USING GIN(tags); +CREATE INDEX idx_posts_status ON posts(status) WHERE status != 'complete'; +CREATE INDEX idx_posts_search_vector ON posts USING GIN (search_vector) WHERE search_vector IS NOT NULL; -- Add foreign key constraints to profiles and statuses ALTER TABLE profiles ADD CONSTRAINT fk_profiles_pinned_post diff --git a/migrations/2025-11-01-114840_social_graph/up.sql b/migrations/2025-11-01-114840_social_graph/up.sql index d2806591..0b8f7adc 100644 --- a/migrations/2025-11-01-114840_social_graph/up.sql +++ b/migrations/2025-11-01-114840_social_graph/up.sql @@ -12,7 +12,6 @@ CREATE TABLE follows ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), subject_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, UNIQUE (actor_id, rkey), UNIQUE (actor_id, subject_actor_id) @@ -27,7 +26,6 @@ CREATE TABLE blocks ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), subject_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, UNIQUE (actor_id, rkey), UNIQUE (actor_id, subject_actor_id) diff --git a/migrations/2025-11-01-114841_engagement/down.sql b/migrations/2025-11-01-114841_engagement/down.sql index be391803..284b2b8a 100644 --- a/migrations/2025-11-01-114841_engagement/down.sql +++ b/migrations/2025-11-01-114841_engagement/down.sql @@ -2,3 +2,4 @@ DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS reposts; DROP TABLE IF EXISTS likes; DROP TYPE IF EXISTS like_subject_type; +DROP TYPE IF EXISTS bookmark_subject_type; diff --git a/migrations/2025-11-01-114841_engagement/up.sql b/migrations/2025-11-01-114841_engagement/up.sql index b1d616bf..e7893518 100644 --- a/migrations/2025-11-01-114841_engagement/up.sql +++ b/migrations/2025-11-01-114841_engagement/up.sql @@ -11,6 +11,7 @@ -- ============================================================================= CREATE TYPE like_subject_type AS ENUM ('post', 'feedgen', 'labeler'); +CREATE TYPE bookmark_subject_type AS ENUM ('post', 'feedgen', 'list', 'labeler', 'starterpack'); -- ============================================================================= -- LIKES @@ -22,20 +23,14 @@ CREATE TABLE likes ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), subject_type like_subject_type NOT NULL DEFAULT 'post', - subject_id BIGINT, -- Nullable for pending linkage + subject_id BIGINT, via_post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, - -- Pending linkage for likes that arrive before their subjects - pending_subject_cid BYTEA, - pending_via_post_cid BYTEA, UNIQUE (actor_id, rkey) ); CREATE INDEX idx_likes_actor ON likes(actor_id); CREATE INDEX idx_likes_subject ON likes(subject_type, subject_id); -CREATE INDEX idx_likes_pending_subject_cid ON likes(pending_subject_cid) WHERE pending_subject_cid IS NOT NULL; -CREATE INDEX idx_likes_pending_via_post_cid ON likes(pending_via_post_cid) WHERE pending_via_post_cid IS NOT NULL; -- ============================================================================= -- REPOSTS @@ -46,31 +41,25 @@ CREATE TABLE reposts ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, -- Nullable for pending linkage + post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, via_post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, - -- Pending linkage for reposts that arrive before their targets - pending_post_cid BYTEA, - pending_via_post_cid BYTEA, - UNIQUE (actor_id, rkey), - UNIQUE (actor_id, post_id) + UNIQUE (actor_id, rkey) ); CREATE INDEX idx_reposts_actor ON reposts(actor_id); CREATE INDEX idx_reposts_post ON reposts(post_id); -CREATE INDEX idx_reposts_pending_post_cid ON reposts(pending_post_cid) WHERE pending_post_cid IS NOT NULL; -CREATE INDEX idx_reposts_pending_via_post_cid ON reposts(pending_via_post_cid) WHERE pending_via_post_cid IS NOT NULL; -- ============================================================================= -- BOOKMARKS -- ============================================================================= --- Bookmarks (off-protocol, almost always posts) +-- Bookmarks (off-protocol, supports multiple entity types) CREATE TABLE bookmarks ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - subject_post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (actor_id, subject_post_id) + rkey INT8 NOT NULL, + subject_type bookmark_subject_type NOT NULL, + subject_id BIGINT, + PRIMARY KEY (actor_id, rkey) ); -CREATE INDEX idx_bookmarks_actor_created ON bookmarks(actor_id, created_at DESC); +CREATE INDEX idx_bookmarks_subject ON bookmarks(subject_type, subject_id) WHERE subject_id IS NOT NULL; diff --git a/migrations/2025-11-01-114842_notifications/up.sql b/migrations/2025-11-01-114842_notifications/up.sql index 4a402ddd..488a173c 100644 --- a/migrations/2025-11-01-114842_notifications/up.sql +++ b/migrations/2025-11-01-114842_notifications/up.sql @@ -1,43 +1,13 @@ -- ============================================================================= --- NOTIFICATIONS +-- NOTIFICATION PREFERENCES -- ============================================================================= -- --- Notification system: notifications, seen tracking, preferences +-- Notification preferences only (notifications are stored in Redis) -- -- ============================================================================= --- ============================================================================= --- NOTIFICATION TYPES --- ============================================================================= - -CREATE TYPE notification_record_type AS ENUM ('post', 'like', 'repost', 'follow', 'block'); -CREATE TYPE notification_reason AS ENUM ('follow', 'like', 'repost', 'reply', 'mention', 'quote'); CREATE TYPE notif_allow_subscriptions AS ENUM ('none', 'mutuals', 'followers'); --- ============================================================================= --- NOTIFICATIONS --- ============================================================================= - -CREATE TABLE notifications ( - recipient_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - record_type notification_record_type NOT NULL, - record_id BIGINT NOT NULL, - reason notification_reason NOT NULL, - reason_subject_post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (recipient_actor_id, record_type, record_id) -); - -CREATE INDEX idx_notifications_recipient_created ON notifications(recipient_actor_id, created_at DESC); -CREATE INDEX idx_notifications_recipient_unread ON notifications(recipient_actor_id) WHERE is_read = FALSE; - --- Notification seen tracking -CREATE TABLE notification_seens ( - actor_id INTEGER PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE, - seen_at TIMESTAMPTZ NOT NULL -); - -- Notification preferences CREATE TABLE notif_decl ( actor_id INTEGER PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE, diff --git a/migrations/2025-11-01-114843_thread_moderation/down.sql b/migrations/2025-11-01-114843_thread_moderation/down.sql index 382216e9..2fe0e6f7 100644 --- a/migrations/2025-11-01-114843_thread_moderation/down.sql +++ b/migrations/2025-11-01-114843_thread_moderation/down.sql @@ -4,5 +4,6 @@ DROP TABLE IF EXISTS postgates; DROP TABLE IF EXISTS threadgate_allowed_lists; DROP TABLE IF EXISTS threadgate_hidden_replies; DROP TABLE IF EXISTS threadgates; +DROP FUNCTION IF EXISTS maintain_postgates(TEXT, TEXT[], TIMESTAMP); DROP TYPE IF EXISTS postgate_rule; DROP TYPE IF EXISTS threadgate_rule; diff --git a/migrations/2025-11-01-114843_thread_moderation/up.sql b/migrations/2025-11-01-114843_thread_moderation/up.sql index 4877d9e3..e038714d 100644 --- a/migrations/2025-11-01-114843_thread_moderation/up.sql +++ b/migrations/2025-11-01-114843_thread_moderation/up.sql @@ -32,16 +32,12 @@ CREATE TABLE threadgates ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, -- Nullable for pending linkage + post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, allow threadgate_rule[], - -- Pending linkage for threadgates that arrive before their posts - pending_post_cid BYTEA, UNIQUE (actor_id, rkey) ); CREATE INDEX idx_threadgates_post ON threadgates(post_id); -CREATE INDEX idx_threadgates_pending_post_cid ON threadgates(pending_post_cid) WHERE pending_post_cid IS NOT NULL; -- Threadgate Hidden Replies CREATE TABLE threadgate_hidden_replies ( @@ -66,16 +62,12 @@ CREATE TABLE postgates ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, -- Nullable for pending linkage + post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE, rules postgate_rule[] NOT NULL DEFAULT '{}', - -- Pending linkage for postgates that arrive before their posts - pending_post_cid BYTEA, UNIQUE (actor_id, rkey) ); CREATE INDEX idx_postgates_post ON postgates(post_id); -CREATE INDEX idx_postgates_pending_post_cid ON postgates(pending_post_cid) WHERE pending_post_cid IS NOT NULL; -- Postgate Detached Embeds CREATE TABLE postgate_detached ( @@ -84,6 +76,66 @@ CREATE TABLE postgate_detached ( PRIMARY KEY (postgate_id, detached_post_id) ); +-- ============================================================================= +-- POSTGATE MAINTENANCE FUNCTION +-- ============================================================================= + +-- Updates the 'detached' flag on existing post_embed_record entries when a postgate changes +CREATE OR REPLACE FUNCTION maintain_postgates( + post_uri TEXT, + detached_uris TEXT[], + disable_effective TIMESTAMP +) RETURNS BIGINT AS $$ +DECLARE + target_post_id BIGINT; + rows_updated BIGINT; +BEGIN + -- Look up the post_id from the post URI + SELECT p.id INTO target_post_id + FROM posts p + INNER JOIN actors a ON p.actor_id = a.id + WHERE 'at://' || a.did || '/app.bsky.feed.post/' || i64_to_tid(p.rkey) = post_uri; + + IF target_post_id IS NULL THEN + RETURN 0; + END IF; + + -- Update post_embed_record entries to mark quotes as detached + WITH detached_post_ids AS ( + SELECT p.id + FROM UNNEST(detached_uris) AS uri + CROSS JOIN LATERAL ( + SELECT + SPLIT_PART(SUBSTRING(uri FROM 6), '/', 1) as did, + SPLIT_PART(SUBSTRING(uri FROM 6), '/', 3) as rkey + ) parts + INNER JOIN actors a ON a.did = parts.did + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64(parts.rkey) + ), + posts_to_update AS ( + SELECT per.post_id + FROM post_embed_record per + WHERE per.embedded_post_id = target_post_id + ) + UPDATE post_embed_record per + SET detached = ( + per.post_id IN (SELECT id FROM detached_post_ids) + OR + (disable_effective IS NOT NULL AND ( + SELECT tid_timestamp(p.rkey) + FROM posts p + WHERE p.id = per.post_id + ) > disable_effective) + ) + WHERE per.embedded_post_id = target_post_id; + + GET DIAGNOSTICS rows_updated = ROW_COUNT; + RETURN rows_updated; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION maintain_postgates IS 'Updates detached flag on post_embed_record when postgates change'; + -- ============================================================================= -- THREAD MUTES -- ============================================================================= diff --git a/migrations/2025-11-01-114844_lists/down.sql b/migrations/2025-11-01-114844_lists/down.sql index 05c47737..b43cf8d6 100644 --- a/migrations/2025-11-01-114844_lists/down.sql +++ b/migrations/2025-11-01-114844_lists/down.sql @@ -4,3 +4,4 @@ DROP TABLE IF EXISTS list_blocks; DROP TABLE IF EXISTS list_items; DROP TABLE IF EXISTS lists; DROP TYPE IF EXISTS list_type; +DROP TYPE IF EXISTS record_status; diff --git a/migrations/2025-11-01-114844_lists/up.sql b/migrations/2025-11-01-114844_lists/up.sql index 794a725c..24f5acea 100644 --- a/migrations/2025-11-01-114844_lists/up.sql +++ b/migrations/2025-11-01-114844_lists/up.sql @@ -16,6 +16,8 @@ CREATE TYPE list_type AS ENUM ( 'app.bsky.graph.defs#referencelist' ); +CREATE TYPE record_status AS ENUM ('complete', 'stub', 'deleted', 'forbidden'); + -- ============================================================================= -- LISTS -- ============================================================================= @@ -25,18 +27,22 @@ CREATE TABLE lists ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), owner_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - list_type list_type NOT NULL, - name TEXT NOT NULL, + list_type list_type, + name TEXT, description TEXT, description_facets JSONB, avatar_cid BYTEA, + status record_status NOT NULL DEFAULT 'complete', UNIQUE (actor_id, rkey) ); +COMMENT ON COLUMN lists.status IS 'Status of the list record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = user deleted, ''forbidden'' = removed by moderators.'; +COMMENT ON COLUMN lists.list_type IS 'Type of list (e.g., modlist, curatelist). NULL for stub lists (not yet fetched).'; +COMMENT ON COLUMN lists.name IS 'List display name. NULL for stub lists (not yet fetched).'; + CREATE INDEX idx_lists_owner ON lists(owner_actor_id); -CREATE INDEX idx_lists_type ON lists(list_type); +CREATE INDEX idx_lists_type ON lists(list_type) WHERE list_type IS NOT NULL; -- ============================================================================= -- LIST ITEMS @@ -47,17 +53,13 @@ CREATE TABLE list_items ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - list_id BIGINT REFERENCES lists(id) ON DELETE CASCADE, -- Nullable for pending linkage + list_id BIGINT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, subject_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - -- Pending linkage for list items that arrive before their lists - pending_list_cid BYTEA, UNIQUE (actor_id, rkey) ); CREATE INDEX idx_list_items_list ON list_items(list_id); CREATE INDEX idx_list_items_subject ON list_items(subject_actor_id); -CREATE INDEX idx_list_items_pending_list_cid ON list_items(pending_list_cid) WHERE pending_list_cid IS NOT NULL; -- ============================================================================= -- LIST BLOCKS & MUTES @@ -69,7 +71,6 @@ CREATE TABLE list_blocks ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), list_id BIGINT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, UNIQUE (actor_id, rkey) ); diff --git a/migrations/2025-11-01-114845_feeds/down.sql b/migrations/2025-11-01-114845_feeds/down.sql index 44ebe6ea..21b8d15d 100644 --- a/migrations/2025-11-01-114845_feeds/down.sql +++ b/migrations/2025-11-01-114845_feeds/down.sql @@ -3,3 +3,5 @@ DROP TABLE IF EXISTS starterpack_feeds; DROP TABLE IF EXISTS starterpacks; DROP TABLE IF EXISTS feedgens; DROP TYPE IF EXISTS content_mode; +DROP TYPE IF EXISTS feedgen_status; +DROP TYPE IF EXISTS labeler_status; diff --git a/migrations/2025-11-01-114845_feeds/up.sql b/migrations/2025-11-01-114845_feeds/up.sql index 71479d1f..f3140aac 100644 --- a/migrations/2025-11-01-114845_feeds/up.sql +++ b/migrations/2025-11-01-114845_feeds/up.sql @@ -11,6 +11,8 @@ -- ============================================================================= CREATE TYPE content_mode AS ENUM ('contentModeUnspecified', 'contentModeVideo'); +CREATE TYPE feedgen_status AS ENUM ('complete', 'stub', 'deleted'); +CREATE TYPE labeler_status AS ENUM ('complete', 'stub', 'deleted'); -- ============================================================================= -- FEED GENERATORS @@ -19,20 +21,25 @@ CREATE TYPE content_mode AS ENUM ('contentModeUnspecified', 'contentModeVideo'); CREATE TABLE feedgens ( id BIGSERIAL PRIMARY KEY, actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - rkey INT8 NOT NULL, + rkey TEXT NOT NULL, cid BYTEA NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), owner_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, service_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, content_mode content_mode, - name TEXT NOT NULL, + name TEXT, description TEXT, description_facets JSONB, avatar_cid BYTEA, accepts_interactions BOOLEAN NOT NULL DEFAULT FALSE, + status feedgen_status NOT NULL DEFAULT 'complete', UNIQUE (actor_id, rkey) ); +COMMENT ON COLUMN feedgens.rkey IS 'Feedgen record key. Can be arbitrary string (e.g., "music", "discover") or TID format.'; +COMMENT ON COLUMN feedgens.status IS 'Status of the feedgen record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = soft-deleted.'; +COMMENT ON COLUMN feedgens.name IS 'Feedgen display name. NULL for stub feedgens (not yet fetched) or deleted feedgens (soft-deleted).'; + CREATE INDEX idx_feedgens_owner ON feedgens(owner_actor_id); CREATE INDEX idx_feedgens_service ON feedgens(service_actor_id); @@ -45,26 +52,25 @@ CREATE TABLE starterpacks ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), owner_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - name TEXT NOT NULL, + name TEXT, description TEXT, description_facets JSONB, list_id BIGINT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + search_vector tsvector, + status record_status NOT NULL DEFAULT 'complete', UNIQUE (actor_id, rkey) ); +COMMENT ON COLUMN starterpacks.status IS 'Status of the starter pack record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = user deleted, ''forbidden'' = removed by moderators.'; +COMMENT ON COLUMN starterpacks.name IS 'Starter pack name. NULL for stub starter packs (not yet fetched).'; + CREATE INDEX idx_starterpacks_owner ON starterpacks(owner_actor_id); CREATE INDEX idx_starterpacks_list ON starterpacks(list_id); -- Full-text search on starter packs -ALTER TABLE starterpacks ADD COLUMN search_vector tsvector - GENERATED ALWAYS AS ( - setweight(to_tsvector('english', coalesce(name, '')), 'A') || - setweight(to_tsvector('english', coalesce(description, '')), 'B') - ) STORED; - -CREATE INDEX idx_starterpacks_search ON starterpacks USING GIN(search_vector); +-- Using a manual update approach instead of GENERATED ALWAYS AS since name can be NULL +CREATE INDEX idx_starterpacks_search ON starterpacks USING GIN(search_vector) WHERE search_vector IS NOT NULL; -- Starter Pack Feeds CREATE TABLE starterpack_feeds ( diff --git a/migrations/2025-11-01-114846_moderation/up.sql b/migrations/2025-11-01-114846_moderation/up.sql index a35180a0..800f3037 100644 --- a/migrations/2025-11-01-114846_moderation/up.sql +++ b/migrations/2025-11-01-114846_moderation/up.sql @@ -26,9 +26,12 @@ CREATE TABLE labelers ( created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reasons reason_type[], subject_types subject_type[], - subject_collections TEXT[] + subject_collections TEXT[], + status labeler_status NOT NULL DEFAULT 'complete' ); +COMMENT ON COLUMN labelers.status IS 'Status of the labeler record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = soft-deleted.'; + -- Labeler Definitions CREATE TABLE labeler_defs ( labeler_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, @@ -72,7 +75,6 @@ CREATE TABLE verification ( actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, rkey INT8 NOT NULL, cid BYTEA NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), verifier_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, subject_actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, handle TEXT NOT NULL, diff --git a/migrations/2025-11-01-192943_bookmarks_discriminated_union/down.sql b/migrations/2025-11-01-192943_bookmarks_discriminated_union/down.sql deleted file mode 100644 index abfd36a7..00000000 --- a/migrations/2025-11-01-192943_bookmarks_discriminated_union/down.sql +++ /dev/null @@ -1,48 +0,0 @@ --- ============================================================================= --- BOOKMARKS: Revert Discriminated Union Pattern --- ============================================================================= --- --- Rollback migration: revert bookmarks table to post-only schema --- --- WARNING: This migration will DROP all bookmarks for non-post subjects --- (feedgens, lists, labelers, starter packs) --- ============================================================================= - --- Add back the old column -ALTER TABLE bookmarks ADD COLUMN subject_post_id BIGINT; - --- Copy post bookmarks back -UPDATE bookmarks -SET subject_post_id = subject_id -WHERE subject_type = 'post'; - --- Drop new indexes -DROP INDEX idx_bookmarks_subject; -DROP INDEX idx_bookmarks_actor_created; - --- Drop new primary key -ALTER TABLE bookmarks DROP CONSTRAINT bookmarks_pkey; - --- Delete non-post bookmarks (can't be preserved in old schema) -DELETE FROM bookmarks WHERE subject_type != 'post'; - --- Make subject_post_id NOT NULL -ALTER TABLE bookmarks ALTER COLUMN subject_post_id SET NOT NULL; - --- Drop new columns -ALTER TABLE bookmarks DROP COLUMN rkey; -ALTER TABLE bookmarks DROP COLUMN subject_type; -ALTER TABLE bookmarks DROP COLUMN subject_id; - --- Add back old primary key -ALTER TABLE bookmarks ADD PRIMARY KEY (actor_id, subject_post_id); - --- Add back old foreign key -ALTER TABLE bookmarks ADD CONSTRAINT bookmarks_subject_post_id_fkey - FOREIGN KEY (subject_post_id) REFERENCES posts(id) ON DELETE CASCADE; - --- Recreate old index -CREATE INDEX idx_bookmarks_actor_created ON bookmarks(actor_id, created_at DESC); - --- Drop enum type -DROP TYPE bookmark_subject_type; diff --git a/migrations/2025-11-01-192943_bookmarks_discriminated_union/up.sql b/migrations/2025-11-01-192943_bookmarks_discriminated_union/up.sql deleted file mode 100644 index da3aa4f5..00000000 --- a/migrations/2025-11-01-192943_bookmarks_discriminated_union/up.sql +++ /dev/null @@ -1,33 +0,0 @@ --- ============================================================================= --- BOOKMARKS: Upgrade to Discriminated Union Pattern --- ============================================================================= --- --- Upgrade bookmarks table from post-only to support multiple entity types: --- posts, feed generators, lists, labelers, and starter packs --- --- Migration strategy: --- 1. Create new enum type for subject types --- 2. Add new columns (rkey, subject_type, subject_id) --- 3. Drop old table and create new one (fresh database approach) --- 4. Create new primary key and indexes --- ============================================================================= - --- Drop old bookmarks table completely -DROP TABLE IF EXISTS bookmarks CASCADE; - --- Create enum for bookmark subject types -CREATE TYPE bookmark_subject_type AS ENUM ('post', 'feedgen', 'list', 'labeler', 'starterpack'); - --- Create new bookmarks table with INT8 rkeys -CREATE TABLE bookmarks ( - actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE, - rkey INT8 NOT NULL, - subject_type bookmark_subject_type NOT NULL, - subject_id BIGINT, -- Nullable for pending linkage - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (actor_id, rkey) -); - --- Create indexes -CREATE INDEX idx_bookmarks_actor_created ON bookmarks(actor_id, created_at DESC); -CREATE INDEX idx_bookmarks_subject ON bookmarks(subject_type, subject_id) WHERE subject_id IS NOT NULL; diff --git a/migrations/2025-11-01-234535_drop_allowlist_table/down.sql b/migrations/2025-11-01-234535_drop_allowlist_table/down.sql deleted file mode 100644 index 6c356529..00000000 --- a/migrations/2025-11-01-234535_drop_allowlist_table/down.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Recreate allowlist table from actors.sync_state --- --- This rollback migration recreates the allowlist table and populates it --- from actors with sync_state IN ('synced', 'dirty', 'processing') - --- Step 1: Recreate allowlist table -CREATE TABLE allowlist ( - actor_id INTEGER PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - description TEXT -); - --- Step 2: Repopulate from actors table --- Actors with sync_state IN ('synced', 'dirty', 'processing') are allowlisted -INSERT INTO allowlist (actor_id, created_at, description) -SELECT id, COALESCE(last_indexed, NOW()), NULL -FROM actors -WHERE sync_state IN ('synced'::actor_sync_state, 'dirty'::actor_sync_state, 'processing'::actor_sync_state); diff --git a/migrations/2025-11-01-234535_drop_allowlist_table/up.sql b/migrations/2025-11-01-234535_drop_allowlist_table/up.sql deleted file mode 100644 index dabe2b18..00000000 --- a/migrations/2025-11-01-234535_drop_allowlist_table/up.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Drop allowlist table (redundant with actors.sync_state column) --- --- Allowlist membership is now determined by sync_state: --- - sync_state IN ('synced', 'dirty', 'processing') = fully allowed --- - sync_state = 'partial' = not allowed (interacts with allowlisted users) --- --- This migration: --- 1. Ensures all currently allowlisted DIDs have correct sync_state --- 2. Drops the allowlist table --- 3. Removes any triggers/functions (if they existed) - --- Step 1: Ensure all allowlisted actors have correct sync_state --- (This is idempotent and safe to run multiple times) -UPDATE actors -SET sync_state = 'dirty'::actor_sync_state -WHERE id IN (SELECT actor_id FROM allowlist) - AND sync_state = 'partial'::actor_sync_state; - --- Step 2: Drop allowlist table -DROP TABLE IF EXISTS allowlist; - --- Step 3: Drop NOTIFY function/trigger (if it existed) --- Note: These were never created in migrations, but drop them just in case -DROP TRIGGER IF EXISTS allowlist_changes_trigger ON actors; -DROP FUNCTION IF EXISTS notify_allowlist_changes() CASCADE; diff --git a/migrations/2025-11-02-193056_add_tid_to_i64_function/down.sql b/migrations/2025-11-02-193056_add_tid_to_i64_function/down.sql deleted file mode 100644 index 41cb2189..00000000 --- a/migrations/2025-11-02-193056_add_tid_to_i64_function/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Drop the tid_to_i64 function -DROP FUNCTION IF EXISTS tid_to_i64(TEXT); diff --git a/migrations/2025-11-02-193056_add_tid_to_i64_function/up.sql b/migrations/2025-11-02-193056_add_tid_to_i64_function/up.sql deleted file mode 100644 index f0cf626e..00000000 --- a/migrations/2025-11-02-193056_add_tid_to_i64_function/up.sql +++ /dev/null @@ -1,49 +0,0 @@ --- Create PostgreSQL function to decode base32-sortable TID strings to INT8 --- This function implements the same logic as parakeet-db/src/tid_util.rs::decode_tid --- TIDs are 13-character base32-sortable encoded 64-bit integers --- Alphabet: 234567abcdefghijklmnopqrstuvwxyz (32 chars = 5 bits each) --- Valid first characters: 234567abcdefghij (timestamp constraint) - -CREATE OR REPLACE FUNCTION tid_to_i64(tid TEXT) RETURNS BIGINT AS $$ -DECLARE - alphabet TEXT := '234567abcdefghijklmnopqrstuvwxyz'; - result BIGINT := 0; - char_pos INTEGER; - value INTEGER; - i INTEGER; -BEGIN - -- Validate length - IF LENGTH(tid) != 13 THEN - RAISE EXCEPTION 'TID must be 13 characters, got %', LENGTH(tid); - END IF; - - -- Validate first character (must be in range for valid timestamps) - IF POSITION(SUBSTRING(tid FROM 1 FOR 1) IN '234567abcdefghij') = 0 THEN - RAISE EXCEPTION 'Invalid first character in TID: %', SUBSTRING(tid FROM 1 FOR 1); - END IF; - - -- Decode base32-sortable string - FOR i IN 1..13 LOOP - -- Find position of character in alphabet (1-indexed) - char_pos := POSITION(SUBSTRING(tid FROM i FOR 1) IN alphabet); - - IF char_pos = 0 THEN - RAISE EXCEPTION 'Invalid character in TID at position %: %', i, SUBSTRING(tid FROM i FOR 1); - END IF; - - -- Convert to 0-indexed value - value := char_pos - 1; - - -- Shift left by 5 bits and add new value - result := (result << 5) | value; - END LOOP; - - RETURN result; -END; -$$ LANGUAGE plpgsql IMMUTABLE STRICT; - --- Add comment for documentation -COMMENT ON FUNCTION tid_to_i64(TEXT) IS -'Decodes a base32-sortable TID string (13 characters) to a BIGINT. -Uses AT Protocol TID encoding: https://atproto.com/specs/tid -Alphabet: 234567abcdefghijklmnopqrstuvwxyz'; diff --git a/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/down.sql b/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/down.sql deleted file mode 100644 index aa544dd7..00000000 --- a/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/down.sql +++ /dev/null @@ -1,8 +0,0 @@ --- This file should undo anything in `up.sql` --- --- NOTE: This down migration is NOT safe to run if you have feedgens with --- arbitrary string rkeys (non-TID format). It will fail with a type conversion error. --- --- Only run this if you're certain all feedgen rkeys are valid TIDs. --- -ALTER TABLE feedgens ALTER COLUMN rkey TYPE INT8 USING tid_to_i64(rkey); diff --git a/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/up.sql b/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/up.sql deleted file mode 100644 index fc5caa82..00000000 --- a/migrations/2025-11-02-194138_revert_feedgens_rkey_to_text/up.sql +++ /dev/null @@ -1,17 +0,0 @@ --- ============================================================================= --- Revert feedgens.rkey from INT8 to TEXT --- ============================================================================= --- --- REASON: Feedgens allow arbitrary user-chosen string rkeys (not TIDs) --- Example feedgen rkeys: "music", "discover", "whats-hot", "for-you" --- --- Analysis of parakeet_historical shows: --- - 856 total feedgens --- - 553 (64.6%) use arbitrary string rkeys --- - Only 303 (35.4%) use TID format --- --- Other collections (posts, likes, reposts, etc.) correctly use INT8 for TIDs. --- Feedgens are the exception that MUST remain TEXT. --- ============================================================================= - -ALTER TABLE feedgens ALTER COLUMN rkey TYPE TEXT; diff --git a/migrations/2025-11-02-201750_add_i64_to_tid_function/down.sql b/migrations/2025-11-02-201750_add_i64_to_tid_function/down.sql deleted file mode 100644 index 1dbd90f7..00000000 --- a/migrations/2025-11-02-201750_add_i64_to_tid_function/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Drop i64_to_tid function -DROP FUNCTION IF EXISTS i64_to_tid(BIGINT); diff --git a/migrations/2025-11-02-201750_add_i64_to_tid_function/up.sql b/migrations/2025-11-02-201750_add_i64_to_tid_function/up.sql deleted file mode 100644 index f3693d8e..00000000 --- a/migrations/2025-11-02-201750_add_i64_to_tid_function/up.sql +++ /dev/null @@ -1,33 +0,0 @@ --- Create PostgreSQL function to encode INT8 to base32-sortable TID strings --- This function implements the reverse of tid_to_i64 --- TIDs are 13-character base32-sortable encoded 64-bit integers --- Alphabet: 234567abcdefghijklmnopqrstuvwxyz (32 chars = 5 bits each) - -CREATE OR REPLACE FUNCTION i64_to_tid(value BIGINT) RETURNS TEXT AS $$ -DECLARE - alphabet TEXT := '234567abcdefghijklmnopqrstuvwxyz'; - result TEXT := ''; - char_index INTEGER; - i INTEGER; -BEGIN - -- Encode to base32-sortable string (13 characters) - FOR i IN 1..13 LOOP - -- Extract rightmost 5 bits - char_index := (value & 31) + 1; -- 31 = 0b11111, +1 for 1-indexed substring - - -- Prepend character to result (we build from right to left) - result := SUBSTRING(alphabet FROM char_index FOR 1) || result; - - -- Shift right by 5 bits - value := value >> 5; - END LOOP; - - RETURN result; -END; -$$ LANGUAGE plpgsql IMMUTABLE STRICT; - --- Add comment for documentation -COMMENT ON FUNCTION i64_to_tid(BIGINT) IS -'Encodes a BIGINT to a base32-sortable TID string (13 characters). -Uses AT Protocol TID encoding: https://atproto.com/specs/tid -Alphabet: 234567abcdefghijklmnopqrstuvwxyz'; diff --git a/migrations/2025-11-03-223026_add_post_status_enum/down.sql b/migrations/2025-11-03-223026_add_post_status_enum/down.sql deleted file mode 100644 index 58012d60..00000000 --- a/migrations/2025-11-03-223026_add_post_status_enum/down.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Revert post_status enum addition - --- Drop the index -DROP INDEX IF EXISTS idx_posts_status; - --- Drop the status column -ALTER TABLE posts DROP COLUMN IF EXISTS status; - --- Drop the enum type -DROP TYPE IF EXISTS post_status; diff --git a/migrations/2025-11-03-223026_add_post_status_enum/up.sql b/migrations/2025-11-03-223026_add_post_status_enum/up.sql deleted file mode 100644 index 6ce0d751..00000000 --- a/migrations/2025-11-03-223026_add_post_status_enum/up.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Add post_status enum type --- This replaces the unresolved_refs table with a status column on posts - -CREATE TYPE post_status AS ENUM ( - 'complete', -- Normal post with full content - 'stub', -- Placeholder for referenced post that doesn't exist yet - 'deleted', -- Soft-deleted post - 'forbidden' -- Post exists but is forbidden to access -); - --- Add status column to posts table -ALTER TABLE posts -ADD COLUMN status post_status NOT NULL DEFAULT 'complete'; - --- Create index on status column (partial index for non-complete posts) -CREATE INDEX idx_posts_status ON posts(status) WHERE status != 'complete'; - --- Add comment for documentation -COMMENT ON COLUMN posts.status IS -'Post lifecycle state: complete (normal), stub (placeholder), deleted (soft-deleted), forbidden (access denied)'; diff --git a/migrations/2025-11-04-060024_add_maintain_postgates_function/down.sql b/migrations/2025-11-04-060024_add_maintain_postgates_function/down.sql deleted file mode 100644 index d9e94e24..00000000 --- a/migrations/2025-11-04-060024_add_maintain_postgates_function/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Remove maintain_postgates function -DROP FUNCTION IF EXISTS maintain_postgates(TEXT, TEXT[], TIMESTAMP); diff --git a/migrations/2025-11-04-060024_add_maintain_postgates_function/up.sql b/migrations/2025-11-04-060024_add_maintain_postgates_function/up.sql deleted file mode 100644 index 214821b4..00000000 --- a/migrations/2025-11-04-060024_add_maintain_postgates_function/up.sql +++ /dev/null @@ -1,70 +0,0 @@ --- Create maintain_postgates function --- Updates the 'detached' flag on existing post_embed_record entries when a postgate changes --- --- Parameters: --- $1: post_uri - AT URI of the post that has the postgate (e.g., "at://did:plc:abc/app.bsky.feed.post/xyz") --- $2: detached_uris - Array of post URIs that should be marked as detached --- $3: disable_effective - Timestamp when the global disable rule became effective (nullable) --- --- Returns: Number of rows updated (as BIGINT for compatibility with conn.execute) -CREATE OR REPLACE FUNCTION maintain_postgates( - post_uri TEXT, - detached_uris TEXT[], - disable_effective TIMESTAMP -) RETURNS BIGINT AS $$ -DECLARE - target_post_id BIGINT; - rows_updated BIGINT; -BEGIN - -- Look up the post_id from the post URI - SELECT p.id INTO target_post_id - FROM posts p - INNER JOIN actors a ON p.actor_id = a.id - WHERE 'at://' || a.did || '/app.bsky.feed.post/' || i64_to_tid(p.rkey) = post_uri; - - -- If post doesn't exist, nothing to do - IF target_post_id IS NULL THEN - RETURN 0; - END IF; - - -- Update post_embed_record entries to mark quotes as detached - -- A quote post is detached if: - -- 1. It's in the detached_uris list, OR - -- 2. The global disable rule exists (disable_effective is not null) AND the quoting post was created after disable_effective - WITH detached_post_ids AS ( - -- Resolve the detached URIs to post IDs - SELECT p.id - FROM UNNEST(detached_uris) AS uri - CROSS JOIN LATERAL ( - SELECT - SPLIT_PART(SUBSTRING(uri FROM 6), '/', 1) as did, - SPLIT_PART(SUBSTRING(uri FROM 6), '/', 3) as rkey - ) parts - INNER JOIN actors a ON a.did = parts.did - INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64(parts.rkey) - ), - posts_to_update AS ( - -- Find all posts that quote the target post - SELECT per.post_id - FROM post_embed_record per - WHERE per.embedded_post_id = target_post_id - ) - UPDATE post_embed_record per - SET detached = ( - -- Mark as detached if either: - -- 1. The quoting post is in the specific detached list - per.post_id IN (SELECT id FROM detached_post_ids) - OR - -- 2. Global disable rule applies (quote created after disable_effective) - (disable_effective IS NOT NULL AND ( - SELECT tid_timestamp(p.rkey) - FROM posts p - WHERE p.id = per.post_id - ) > disable_effective) - ) - WHERE per.embedded_post_id = target_post_id; - - GET DIAGNOSTICS rows_updated = ROW_COUNT; - RETURN rows_updated; -END; -$$ LANGUAGE plpgsql; diff --git a/migrations/2025-11-04-063554_make_post_content_nullable/down.sql b/migrations/2025-11-04-063554_make_post_content_nullable/down.sql deleted file mode 100644 index 1a45ee7b..00000000 --- a/migrations/2025-11-04-063554_make_post_content_nullable/down.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Revert content to NOT NULL --- Set empty string for any NULL values before adding constraint - -UPDATE posts -SET content = '' -WHERE content IS NULL; - -ALTER TABLE posts -ALTER COLUMN content SET NOT NULL; diff --git a/migrations/2025-11-04-063554_make_post_content_nullable/up.sql b/migrations/2025-11-04-063554_make_post_content_nullable/up.sql deleted file mode 100644 index e4b60ca6..00000000 --- a/migrations/2025-11-04-063554_make_post_content_nullable/up.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Make post content nullable --- This allows stubs (status='stub') and deleted posts (status='deleted') to have NULL content --- instead of empty strings, which is more semantically correct. - -ALTER TABLE posts -ALTER COLUMN content DROP NOT NULL; - -COMMENT ON COLUMN posts.content IS -'Post text content. NULL for stub posts (not yet fetched) or deleted posts (soft-deleted).'; diff --git a/migrations/2025-11-04-064359_add_feedgen_labeler_status/down.sql b/migrations/2025-11-04-064359_add_feedgen_labeler_status/down.sql deleted file mode 100644 index 1643b8ed..00000000 --- a/migrations/2025-11-04-064359_add_feedgen_labeler_status/down.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Revert feedgen.name to NOT NULL (set empty string for NULL values) -UPDATE feedgens -SET name = '' -WHERE name IS NULL; - -ALTER TABLE feedgens -ALTER COLUMN name SET NOT NULL; - --- Drop status columns -ALTER TABLE labelers -DROP COLUMN status; - -ALTER TABLE feedgens -DROP COLUMN status; - --- Drop status enums -DROP TYPE labeler_status; -DROP TYPE feedgen_status; diff --git a/migrations/2025-11-04-064359_add_feedgen_labeler_status/up.sql b/migrations/2025-11-04-064359_add_feedgen_labeler_status/up.sql deleted file mode 100644 index 3151f656..00000000 --- a/migrations/2025-11-04-064359_add_feedgen_labeler_status/up.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Create status enums for feedgens and labelers -CREATE TYPE feedgen_status AS ENUM ('complete', 'stub', 'deleted'); -CREATE TYPE labeler_status AS ENUM ('complete', 'stub', 'deleted'); - --- Add status columns with default 'complete' for existing records -ALTER TABLE feedgens -ADD COLUMN status feedgen_status NOT NULL DEFAULT 'complete'; - -ALTER TABLE labelers -ADD COLUMN status labeler_status NOT NULL DEFAULT 'complete'; - --- Make feedgen.name nullable (NULL for stubs) -ALTER TABLE feedgens -ALTER COLUMN name DROP NOT NULL; - --- Add comments explaining stub usage -COMMENT ON COLUMN feedgens.status IS -'Status of the feedgen record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = soft-deleted.'; - -COMMENT ON COLUMN labelers.status IS -'Status of the labeler record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = soft-deleted.'; - -COMMENT ON COLUMN feedgens.name IS -'Feedgen display name. NULL for stub feedgens (not yet fetched) or deleted feedgens (soft-deleted).'; diff --git a/migrations/2025-11-04-133658_remove_unresolved_refs/down.sql b/migrations/2025-11-04-133658_remove_unresolved_refs/down.sql deleted file mode 100644 index e34d4eae..00000000 --- a/migrations/2025-11-04-133658_remove_unresolved_refs/down.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Recreate unresolved_reason enum -CREATE TYPE unresolved_reason AS ENUM ('pending', 'deleted', 'forbidden', 'not_found'); - --- Recreate unresolved_refs table -CREATE TABLE unresolved_refs ( - id SERIAL PRIMARY KEY, - cid BYTEA NOT NULL UNIQUE, - reason unresolved_reason NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 1, - actor_did TEXT NOT NULL, - rkey BIGINT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - last_checked TIMESTAMPTZ -); - -CREATE INDEX idx_unresolved_refs_cid ON unresolved_refs(cid); -CREATE INDEX idx_unresolved_refs_reason ON unresolved_refs(reason); -CREATE INDEX idx_unresolved_refs_last_checked ON unresolved_refs(last_checked) WHERE last_checked IS NOT NULL; - --- Add back unresolved reference columns to posts table -ALTER TABLE posts -ADD COLUMN unresolved_parent_id INTEGER REFERENCES unresolved_refs(id), -ADD COLUMN unresolved_root_id INTEGER REFERENCES unresolved_refs(id); - -CREATE INDEX idx_posts_unresolved_parent ON posts(unresolved_parent_id) WHERE unresolved_parent_id IS NOT NULL; -CREATE INDEX idx_posts_unresolved_root ON posts(unresolved_root_id) WHERE unresolved_root_id IS NOT NULL; diff --git a/migrations/2025-11-04-133658_remove_unresolved_refs/up.sql b/migrations/2025-11-04-133658_remove_unresolved_refs/up.sql deleted file mode 100644 index 91bd1490..00000000 --- a/migrations/2025-11-04-133658_remove_unresolved_refs/up.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Remove unresolved reference columns from posts table --- These columns are replaced by the stub pattern -ALTER TABLE posts -DROP COLUMN unresolved_parent_id, -DROP COLUMN unresolved_root_id; - --- Drop the unresolved_refs table (unused - 0 rows) -DROP TABLE IF EXISTS unresolved_refs; - --- Drop the unresolved_reason enum type -DROP TYPE IF EXISTS unresolved_reason; diff --git a/migrations/2025-11-04-154903_remove_pending_cid_columns/down.sql b/migrations/2025-11-04-154903_remove_pending_cid_columns/down.sql deleted file mode 100644 index 963028e1..00000000 --- a/migrations/2025-11-04-154903_remove_pending_cid_columns/down.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Recreate pending CID columns (for rollback purposes) - --- Add columns to likes table -ALTER TABLE likes -ADD COLUMN pending_subject_cid BYTEA, -ADD COLUMN pending_via_post_cid BYTEA; - --- Add columns to reposts table -ALTER TABLE reposts -ADD COLUMN pending_post_cid BYTEA, -ADD COLUMN pending_via_post_cid BYTEA; - --- Add columns to threadgates table -ALTER TABLE threadgates -ADD COLUMN pending_post_cid BYTEA; - --- Add columns to postgates table -ALTER TABLE postgates -ADD COLUMN pending_post_cid BYTEA; - --- Recreate indexes -CREATE INDEX idx_likes_pending_subject_cid ON likes(pending_subject_cid) WHERE pending_subject_cid IS NOT NULL; -CREATE INDEX idx_likes_pending_via_post_cid ON likes(pending_via_post_cid) WHERE pending_via_post_cid IS NOT NULL; -CREATE INDEX idx_reposts_pending_post_cid ON reposts(pending_post_cid) WHERE pending_post_cid IS NOT NULL; -CREATE INDEX idx_reposts_pending_via_post_cid ON reposts(pending_via_post_cid) WHERE pending_via_post_cid IS NOT NULL; -CREATE INDEX idx_threadgates_pending_post_cid ON threadgates(pending_post_cid) WHERE pending_post_cid IS NOT NULL; -CREATE INDEX idx_postgates_pending_post_cid ON postgates(pending_post_cid) WHERE pending_post_cid IS NOT NULL; diff --git a/migrations/2025-11-04-154903_remove_pending_cid_columns/up.sql b/migrations/2025-11-04-154903_remove_pending_cid_columns/up.sql deleted file mode 100644 index 7ca5342f..00000000 --- a/migrations/2025-11-04-154903_remove_pending_cid_columns/up.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Remove pending CID columns from likes, reposts, threadgates, and postgates tables --- These columns are replaced by the stub pattern where we create stub records immediately - --- Drop indexes first -DROP INDEX IF EXISTS idx_likes_pending_subject_cid; -DROP INDEX IF EXISTS idx_likes_pending_via_post_cid; -DROP INDEX IF EXISTS idx_reposts_pending_post_cid; -DROP INDEX IF EXISTS idx_reposts_pending_via_post_cid; -DROP INDEX IF EXISTS idx_threadgates_pending_post_cid; -DROP INDEX IF EXISTS idx_postgates_pending_post_cid; - --- Drop columns from likes table -ALTER TABLE likes -DROP COLUMN IF EXISTS pending_subject_cid, -DROP COLUMN IF EXISTS pending_via_post_cid; - --- Drop columns from reposts table -ALTER TABLE reposts -DROP COLUMN IF EXISTS pending_post_cid, -DROP COLUMN IF EXISTS pending_via_post_cid; - --- Drop columns from threadgates table -ALTER TABLE threadgates -DROP COLUMN IF EXISTS pending_post_cid; - --- Drop columns from postgates table -ALTER TABLE postgates -DROP COLUMN IF EXISTS pending_post_cid; diff --git a/migrations/2025-11-04-190708_drop_notification_tables/down.sql b/migrations/2025-11-04-190708_drop_notification_tables/down.sql deleted file mode 100644 index b1dcb9cd..00000000 --- a/migrations/2025-11-04-190708_drop_notification_tables/down.sql +++ /dev/null @@ -1,22 +0,0 @@ --- Recreate notification tables and types (for rollback) - -CREATE TYPE notification_reason AS ENUM ('follow', 'like', 'repost', 'reply', 'mention', 'quote'); -CREATE TYPE notification_record_type AS ENUM ('post', 'like', 'repost', 'follow', 'block'); - -CREATE TABLE notification_seens ( - actor_id INT4 PRIMARY KEY REFERENCES actors(id), - seen_at TIMESTAMPTZ NOT NULL -); - -CREATE TABLE notifications ( - recipient_actor_id INT4 NOT NULL REFERENCES actors(id), - record_type notification_record_type NOT NULL, - record_id INT8 NOT NULL, - reason notification_reason NOT NULL, - reason_subject_post_id INT8 REFERENCES posts(id), - is_read BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (recipient_actor_id, record_type, record_id) -); - -CREATE INDEX notifications_recipient_indexed_at_idx ON notifications (recipient_actor_id, created_at DESC); diff --git a/migrations/2025-11-04-190708_drop_notification_tables/up.sql b/migrations/2025-11-04-190708_drop_notification_tables/up.sql deleted file mode 100644 index 091572d0..00000000 --- a/migrations/2025-11-04-190708_drop_notification_tables/up.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Drop notification tables and types --- --- NOTE: This migration removes notification tables from the database schema. --- Notifications are now stored in Redis for better performance and automatic TTL-based cleanup. --- --- Consumer writes to Redis via redis_notifications module. --- Parakeet AppView will read from Redis (to be implemented in separate PR). - -DROP TABLE IF EXISTS notifications; -DROP TABLE IF EXISTS notification_seens; -DROP TYPE IF EXISTS notification_record_type; -DROP TYPE IF EXISTS notification_reason; diff --git a/migrations/2025-11-05-021106_add_list_starterpack_status/down.sql b/migrations/2025-11-05-021106_add_list_starterpack_status/down.sql deleted file mode 100644 index 67de7c2e..00000000 --- a/migrations/2025-11-05-021106_add_list_starterpack_status/down.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Restore list_items.list_id to nullable -ALTER TABLE list_items -ALTER COLUMN list_id DROP NOT NULL; - --- Restore list_items.pending_list_cid column -ALTER TABLE list_items -ADD COLUMN pending_list_cid bytea; - --- Create index for pending list CID lookups -CREATE INDEX idx_list_items_pending_list_cid -ON list_items (pending_list_cid) -WHERE pending_list_cid IS NOT NULL; - --- Restore NOT NULL constraints -ALTER TABLE starterpacks -ALTER COLUMN name SET NOT NULL; - -ALTER TABLE lists -ALTER COLUMN list_type SET NOT NULL, -ALTER COLUMN name SET NOT NULL; - --- Remove status columns -ALTER TABLE starterpacks -DROP COLUMN status; - -ALTER TABLE lists -DROP COLUMN status; - --- Drop status enum types (from the original migration that was run) -DROP TYPE IF EXISTS record_status; -DROP TYPE IF EXISTS starterpack_status; -DROP TYPE IF EXISTS list_status; diff --git a/migrations/2025-11-05-021106_add_list_starterpack_status/up.sql b/migrations/2025-11-05-021106_add_list_starterpack_status/up.sql deleted file mode 100644 index 6f5a366a..00000000 --- a/migrations/2025-11-05-021106_add_list_starterpack_status/up.sql +++ /dev/null @@ -1,53 +0,0 @@ --- Create a shared record_status enum type for all record tables --- This replaces the pattern of creating separate enums (post_status, feedgen_status, etc.) --- All tables can share this single enum type -CREATE TYPE record_status AS ENUM ('complete', 'stub', 'deleted', 'forbidden'); - --- Add status columns with default 'complete' for existing records -ALTER TABLE lists -ADD COLUMN status record_status NOT NULL DEFAULT 'complete'; - -ALTER TABLE starterpacks -ADD COLUMN status record_status NOT NULL DEFAULT 'complete'; - --- Make list fields nullable for stub support --- Stub lists don't have list_type or name until fetched -ALTER TABLE lists -ALTER COLUMN list_type DROP NOT NULL, -ALTER COLUMN name DROP NOT NULL; - --- Make starterpack fields nullable for stub support --- Stub starterpacks don't have name until fetched -ALTER TABLE starterpacks -ALTER COLUMN name DROP NOT NULL; - --- Delete orphaned list_items with NULL list_id --- These are list items referencing lists that don't exist yet --- With the new stubbing system, these will be recreated with stub lists -DELETE FROM list_items WHERE list_id IS NULL; - --- Remove list_items.pending_list_cid column (replaced by list stubbing) --- With list stubbing, we can now create stub lists and reference them directly --- instead of storing a pending CID -ALTER TABLE list_items -DROP COLUMN IF EXISTS pending_list_cid; - --- Make list_items.list_id NOT NULL since we can now always reference a list (stub or complete) -ALTER TABLE list_items -ALTER COLUMN list_id SET NOT NULL; - --- Add comments explaining status usage -COMMENT ON COLUMN lists.status IS -'Status of the list record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = user deleted, ''forbidden'' = removed by moderators.'; - -COMMENT ON COLUMN lists.list_type IS -'Type of list (e.g., modlist, curatelist). NULL for stub lists (not yet fetched).'; - -COMMENT ON COLUMN lists.name IS -'List display name. NULL for stub lists (not yet fetched).'; - -COMMENT ON COLUMN starterpacks.status IS -'Status of the starter pack record. ''complete'' = full data, ''stub'' = placeholder (not yet fetched), ''deleted'' = user deleted, ''forbidden'' = removed by moderators.'; - -COMMENT ON COLUMN starterpacks.name IS -'Starter pack name. NULL for stub starter packs (not yet fetched).'; diff --git a/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/down.sql b/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/down.sql deleted file mode 100644 index 7498bb1c..00000000 --- a/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Restore the UNIQUE (actor_id, post_id) constraint. --- Note: This may fail if there are now multiple reposts of the same post by the same actor. - -ALTER TABLE reposts ADD CONSTRAINT reposts_actor_id_post_id_key UNIQUE (actor_id, post_id); diff --git a/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/up.sql b/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/up.sql deleted file mode 100644 index 474e6e12..00000000 --- a/migrations/2025-11-05-055339_remove_reposts_actor_post_unique_constraint/up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Remove the incorrect UNIQUE (actor_id, post_id) constraint from reposts table. --- --- In the AT Protocol, users CAN create multiple reposts of the same post with different rkeys. --- Each repost is identified by its AT-URI: at://did/app.bsky.feed.repost/{rkey} --- --- The only uniqueness constraint should be (actor_id, rkey), which identifies the repost record itself. - -ALTER TABLE reposts DROP CONSTRAINT reposts_actor_id_post_id_key; diff --git a/migrations/2025-11-05-120000_add_account_created_at_to_actors/down.sql b/migrations/2025-11-05-120000_add_account_created_at_to_actors/down.sql deleted file mode 100644 index 2b92ccf7..00000000 --- a/migrations/2025-11-05-120000_add_account_created_at_to_actors/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Remove account_created_at column from actors table -DROP INDEX IF EXISTS idx_actors_account_created_at; -ALTER TABLE actors DROP COLUMN account_created_at; diff --git a/migrations/2025-11-05-120000_add_account_created_at_to_actors/up.sql b/migrations/2025-11-05-120000_add_account_created_at_to_actors/up.sql deleted file mode 100644 index 924995e2..00000000 --- a/migrations/2025-11-05-120000_add_account_created_at_to_actors/up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Add account_created_at column to actors table --- This represents when the account was created (from PLC directory first operation) --- Nullable because we may not know it for all actors initially (especially stubs) --- Will be populated during handle resolution -ALTER TABLE actors ADD COLUMN account_created_at TIMESTAMP WITH TIME ZONE; - --- Create index for potential queries by account age -CREATE INDEX idx_actors_account_created_at ON actors(account_created_at) WHERE account_created_at IS NOT NULL; diff --git a/migrations/2025-11-07-192945_posts_content_compression/down.sql b/migrations/2025-11-07-192945_posts_content_compression/down.sql deleted file mode 100644 index 8063b4fc..00000000 --- a/migrations/2025-11-07-192945_posts_content_compression/down.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Revert post content compression --- Changes posts.content back from BYTEA to TEXT --- Removes content_version and search_vector columns - --- Drop index first -DROP INDEX IF EXISTS idx_posts_search_vector; - --- Drop CHECK constraint -ALTER TABLE posts - DROP CONSTRAINT IF EXISTS posts_content_version_check; - --- Drop columns if they exist (use IF EXISTS for idempotency) -ALTER TABLE posts - DROP COLUMN IF EXISTS content_version, - DROP COLUMN IF EXISTS search_vector; - --- Revert content column from BYTEA to TEXT -ALTER TABLE posts - ALTER COLUMN content TYPE TEXT; diff --git a/migrations/2025-11-07-192945_posts_content_compression/up.sql b/migrations/2025-11-07-192945_posts_content_compression/up.sql deleted file mode 100644 index 51a36ea3..00000000 --- a/migrations/2025-11-07-192945_posts_content_compression/up.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Add post content compression --- Changes posts.content from TEXT to BYTEA (zstd-compressed) --- Adds content_version to track dictionary version --- Adds search_vector for selective full-text search on allowlisted users - --- Alter posts table: TEXT -> BYTEA -ALTER TABLE posts - ALTER COLUMN content TYPE BYTEA USING NULL, - ADD COLUMN content_version SMALLINT, - ADD COLUMN search_vector tsvector; - -COMMENT ON COLUMN posts.content IS 'Zstd-compressed post content (BYTEA)'; -COMMENT ON COLUMN posts.content_version IS 'Dictionary version (NULL if no content)'; -COMMENT ON COLUMN posts.search_vector IS 'Full-text search vector (NULL except for allowlisted users)'; - --- Add CHECK constraint: both NULL or both set -ALTER TABLE posts - ADD CONSTRAINT posts_content_version_check - CHECK ( - (content IS NULL AND content_version IS NULL) OR - (content IS NOT NULL AND content_version IS NOT NULL) - ); - --- Create GIN index for search_vector (sparse index since most are NULL) -CREATE INDEX idx_posts_search_vector ON posts USING GIN (search_vector) - WHERE search_vector IS NOT NULL; diff --git a/migrations/2025-11-08-001834_drop_tid_created_at_columns/down.sql b/migrations/2025-11-08-001834_drop_tid_created_at_columns/down.sql deleted file mode 100644 index d49c2a8d..00000000 --- a/migrations/2025-11-08-001834_drop_tid_created_at_columns/down.sql +++ /dev/null @@ -1,49 +0,0 @@ --- Rollback: Re-add created_at columns and populate from TID rkeys - --- Add created_at columns back (nullable initially) -ALTER TABLE posts ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE postgates ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE likes ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE reposts ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE follows ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE blocks ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE bookmarks ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE lists ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE list_items ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE list_blocks ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE starterpacks ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE threadgates ADD COLUMN created_at TIMESTAMPTZ; -ALTER TABLE verification ADD COLUMN created_at TIMESTAMPTZ; - --- Populate from TID rkeys using helper function -UPDATE posts SET created_at = tid_timestamp(rkey); -UPDATE postgates SET created_at = tid_timestamp(rkey); -UPDATE likes SET created_at = tid_timestamp(rkey); -UPDATE reposts SET created_at = tid_timestamp(rkey); -UPDATE follows SET created_at = tid_timestamp(rkey); -UPDATE blocks SET created_at = tid_timestamp(rkey); -UPDATE bookmarks SET created_at = tid_timestamp(rkey); -UPDATE lists SET created_at = tid_timestamp(rkey); -UPDATE list_items SET created_at = tid_timestamp(rkey); -UPDATE list_blocks SET created_at = tid_timestamp(rkey); -UPDATE starterpacks SET created_at = tid_timestamp(rkey); -UPDATE threadgates SET created_at = tid_timestamp(rkey); -UPDATE verification SET created_at = tid_timestamp(rkey); - --- Set NOT NULL constraints -ALTER TABLE posts ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE postgates ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE likes ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE reposts ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE follows ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE blocks ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE bookmarks ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE lists ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE list_items ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE list_blocks ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE starterpacks ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE threadgates ALTER COLUMN created_at SET NOT NULL; -ALTER TABLE verification ALTER COLUMN created_at SET NOT NULL; - --- Drop the helper function -DROP FUNCTION IF EXISTS tid_timestamp(BIGINT); diff --git a/migrations/2025-11-08-001834_drop_tid_created_at_columns/up.sql b/migrations/2025-11-08-001834_drop_tid_created_at_columns/up.sql deleted file mode 100644 index 9aefc57e..00000000 --- a/migrations/2025-11-08-001834_drop_tid_created_at_columns/up.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Drop created_at column from TID-based tables --- TIDs encode microsecond timestamps in their upper 53 bits, making created_at redundant --- Storage savings: ~140 GB at network scale (2B posts, 11.5B likes, 3B follows, 600M reposts) - --- Add PostgreSQL helper function to extract timestamp from TID -CREATE OR REPLACE FUNCTION tid_timestamp(tid_int8 BIGINT) -RETURNS TIMESTAMPTZ AS $$ -BEGIN - -- Extract timestamp (53 bits) by right shifting 10 bits (clock ID) - -- Convert microseconds to PostgreSQL timestamp - RETURN to_timestamp((tid_int8 >> 10) / 1000000.0); -END; -$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; - --- Drop created_at columns from tables with TID rkeys --- These timestamps can be derived from the rkey using tid_timestamp(rkey) - -ALTER TABLE posts DROP COLUMN created_at; -ALTER TABLE postgates DROP COLUMN created_at; -ALTER TABLE likes DROP COLUMN created_at; -ALTER TABLE reposts DROP COLUMN created_at; -ALTER TABLE follows DROP COLUMN created_at; -ALTER TABLE blocks DROP COLUMN created_at; -ALTER TABLE bookmarks DROP COLUMN created_at; -ALTER TABLE lists DROP COLUMN created_at; -ALTER TABLE list_items DROP COLUMN created_at; -ALTER TABLE list_blocks DROP COLUMN created_at; -ALTER TABLE starterpacks DROP COLUMN created_at; -ALTER TABLE threadgates DROP COLUMN created_at; -ALTER TABLE verification DROP COLUMN created_at; diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs index ea30c65e..02ee6ec1 100644 --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -447,6 +447,7 @@ diesel::table! { rkey -> Int8, cid -> Bytea, content -> Nullable, + content_version -> Nullable, language_code -> Nullable, tags -> Array>, parent_post_id -> Nullable, @@ -455,7 +456,6 @@ diesel::table! { embed_subtype -> Nullable, violates_threadgate -> Bool, status -> PostStatus, - content_version -> Nullable, search_vector -> Nullable, } }