From 84be602454c5e8e2d72015c6ed5fb718192643b4 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 16 Jan 2026 10:41:29 +0000 Subject: [PATCH] appview/db: more flexible tables migrate tables: `stars`, `reactions`, `follows`, `public_keys` Two major changes: 1. Remove autoincrement id for these tables. AUTOINCREMENT primary key does not help much for these tables and only introduces slice performance overhead. Use default `rowid` with non-autoincrement integer instead. 2. Remove unique constraints other than `(did, rkey)` We cannot block users creating non-unique atproto records. Appview needs to handle those properly. For example, if user unstar a repo, appview should delete all existing star records pointing to that repo. To allow this, remove all constraints other than `(did, rkey)`. Minor changes done while migrating tables: - rename `thread_at` in `reactions` to `subject_at` to match with other tables - follow common column names like `did` and `created` - allow self-follow (similar reason to 2nd major change. we should block it from service layer instead) Signed-off-by: Seongmin Lee --- appview/db/db.go | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ appview/db/follow.go | 24 ++++++++++++------------ appview/db/reaction.go | 34 +++++++++++++++++----------------- appview/db/timeline.go | 2 +- 4 file(s) changed, 140 insertion(s)(+), 30 deletion(s)(-) diff --git a/appview/db/db.go b/appview/db/db.go --- a/appview/db/db.go +++ b/appview/db/db.go @@ -3,6 +3,7 @@ import ( "context" "database/sql" + "fmt" "log/slog" "strings" @@ -1202,6 +1203,115 @@ drop table profile_pinned_repositories; alter table profile_pinned_repositories_new rename to profile_pinned_repositories; `) return err + }) + + // several changes here + // 1. remove autoincrement id for these tables + // 2. remove unique constraints other than (did, rkey) to handle non-unique atproto records + // 3. add generated at_uri field + // + // see comments below and commit message for details + orm.RunMigration(conn, logger, "flexible-stars-reactions-follows-public_keys", func(tx *sql.Tx) error { + // - add at_uri + // - remove unique constraint (did, subject_at) + if _, err := tx.Exec(` + create table stars_new ( + did text not null, + rkey text not null, + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.star' || '/' || rkey) stored, + + subject_at text not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + unique(did, rkey) + ); + + insert into stars_new (did, rkey, subject_at, created) + select did, rkey, subject_at, created from stars; + + drop table stars; + alter table stars_new rename to stars; + `); err != nil { + return fmt.Errorf("migrating stars: %w", err) + } + + // - add at_uri + // - reacted_by_did -> did + // - thread_at -> subject_at + // - remove unique constraint + if _, err := tx.Exec(` + create table reactions_new ( + did text not null, + rkey text not null, + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.reaction' || '/' || rkey) stored, + + subject_at text not null, + kind text not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + unique(did, rkey) + ); + + insert into reactions_new (did, rkey, subject_at, kind, created) + select reacted_by_did, rkey, thread_at, kind, created from reactions; + + drop table reactions; + alter table reactions_new rename to reactions; + `); err != nil { + return fmt.Errorf("migrating reactions: %w", err) + } + + // - add at_uri column + // - user_did -> did + // - followed_at -> created + // - remove unique constraint + // - remove check constraint + if _, err := tx.Exec(` + create table follows_new ( + did text not null, + rkey text not null, + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.graph.follow' || '/' || rkey) stored, + + subject_did text not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + unique(did, rkey) + ); + + insert into follows_new (did, rkey, subject_did, created) + select user_did, rkey, subject_did, followed_at from follows; + + drop table follows; + alter table follows_new rename to follows; + `); err != nil { + return fmt.Errorf("migrating follows: %w", err) + } + + // - add at_uri column + // - remove foreign key relationship from repos + if _, err := tx.Exec(` + create table public_keys_new ( + did text not null, + rkey text not null, + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.publicKey' || '/' || rkey) stored, + + name text not null, + key text not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + unique(did, rkey) + ); + + insert into public_keys_new (did, rkey, name, key, created) + select did, rkey, name, key, created from public_keys; + + drop table public_keys; + alter table public_keys_new rename to public_keys; + `); err != nil { + return fmt.Errorf("migrating public_keys: %w", err) + } + + return nil }) return &DB{ diff --git a/appview/db/follow.go b/appview/db/follow.go --- a/appview/db/follow.go +++ b/appview/db/follow.go @@ -11,14 +11,14 @@ "tangled.org/core/orm" ) func AddFollow(e Execer, follow *models.Follow) error { - query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)` + query := `insert or ignore into follows (did, subject_did, rkey) values (?, ?, ?)` _, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey) return err } // Get a follow record func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) { - query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?` + query := `select did, subject_did, created, rkey from follows where did = ? and subject_did = ?` row := e.QueryRow(query, userDid, subjectDid) var follow models.Follow @@ -41,13 +41,13 @@ } // Remove a follow func DeleteFollow(e Execer, userDid, subjectDid string) error { - _, err := e.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid) + _, err := e.Exec(`delete from follows where did = ? and subject_did = ?`, userDid, subjectDid) return err } // Remove a follow func DeleteFollowByRkey(e Execer, userDid, rkey string) error { - _, err := e.Exec(`delete from follows where user_did = ? and rkey = ?`, userDid, rkey) + _, err := e.Exec(`delete from follows where did = ? and rkey = ?`, userDid, rkey) return err } @@ -56,7 +56,7 @@ var followers, following int64 err := e.QueryRow( `SELECT COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, - COUNT(CASE WHEN user_did = ? THEN 1 END) AS following + COUNT(CASE WHEN did = ? THEN 1 END) AS following FROM follows;`, did, did).Scan(&followers, &following) if err != nil { return models.FollowStats{}, err @@ -96,10 +96,10 @@ where subject_did in (%s) group by subject_did ) f full outer join ( - select user_did as did, count(*) as following + select did as did, count(*) as following from follows - where user_did in (%s) - group by user_did + where did in (%s) + group by did ) g on f.did = g.did`, placeholderStr, placeholderStr) @@ -156,10 +156,10 @@ args = append(args, limit) } query := fmt.Sprintf( - `select user_did, subject_did, followed_at, rkey + `select did, subject_did, created, rkey from follows %s - order by followed_at desc + order by created desc %s `, whereClause, limitClause) @@ -198,7 +198,7 @@ return GetFollows(e, 0, orm.FilterEq("subject_did", did)) } func GetFollowing(e Execer, did string) ([]models.Follow, error) { - return GetFollows(e, 0, orm.FilterEq("user_did", did)) + return GetFollows(e, 0, orm.FilterEq("did", did)) } func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) { @@ -239,7 +239,7 @@ query := fmt.Sprintf(` SELECT subject_did FROM follows - WHERE user_did = ? AND subject_did IN (%s) + WHERE did = ? AND subject_did IN (%s) `, strings.Join(placeholders, ",")) rows, err := e.Query(query, args...) diff --git a/appview/db/reaction.go b/appview/db/reaction.go --- a/appview/db/reaction.go +++ b/appview/db/reaction.go @@ -8,19 +8,19 @@ "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/models" ) -func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind, rkey string) error { - query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey) values (?, ?, ?, ?)` - _, err := e.Exec(query, reactedByDid, threadAt, kind, rkey) +func AddReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind, rkey string) error { + query := `insert or ignore into reactions (did, subject_at, kind, rkey) values (?, ?, ?, ?)` + _, err := e.Exec(query, did, subjectAt, kind, rkey) return err } // Get a reaction record -func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind) (*models.Reaction, error) { +func GetReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind) (*models.Reaction, error) { query := ` - select reacted_by_did, thread_at, created, rkey + select did, subject_at, created, rkey from reactions - where reacted_by_did = ? and thread_at = ? and kind = ?` - row := e.QueryRow(query, reactedByDid, threadAt, kind) + where did = ? and subject_at = ? and kind = ?` + row := e.QueryRow(query, did, subjectAt, kind) var reaction models.Reaction var created string @@ -41,37 +41,37 @@ return &reaction, nil } // Remove a reaction -func DeleteReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind) error { - _, err := e.Exec(`delete from reactions where reacted_by_did = ? and thread_at = ? and kind = ?`, reactedByDid, threadAt, kind) +func DeleteReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind) error { + _, err := e.Exec(`delete from reactions where did = ? and subject_at = ? and kind = ?`, did, subjectAt, kind) return err } // Remove a reaction -func DeleteReactionByRkey(e Execer, reactedByDid string, rkey string) error { - _, err := e.Exec(`delete from reactions where reacted_by_did = ? and rkey = ?`, reactedByDid, rkey) +func DeleteReactionByRkey(e Execer, did string, rkey string) error { + _, err := e.Exec(`delete from reactions where did = ? and rkey = ?`, did, rkey) return err } -func GetReactionCount(e Execer, threadAt syntax.ATURI, kind models.ReactionKind) (int, error) { +func GetReactionCount(e Execer, subjectAt syntax.ATURI, kind models.ReactionKind) (int, error) { count := 0 err := e.QueryRow( - `select count(reacted_by_did) from reactions where thread_at = ? and kind = ?`, threadAt, kind).Scan(&count) + `select count(did) from reactions where subject_at = ? and kind = ?`, subjectAt, kind).Scan(&count) if err != nil { return 0, err } return count, nil } -func GetReactionMap(e Execer, userLimit int, threadAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { +func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { query := ` - select kind, reacted_by_did, + select kind, did, row_number() over (partition by kind order by created asc) as rn, count(*) over (partition by kind) as total from reactions - where thread_at = ? + where subject_at = ? order by kind, created asc` - rows, err := e.Query(query, threadAt) + rows, err := e.Query(query, subjectAt) if err != nil { return nil, err } diff --git a/appview/db/timeline.go b/appview/db/timeline.go --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -183,7 +183,7 @@ func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { filters := make([]orm.Filter, 0) if userIsFollowing != nil { - filters = append(filters, orm.FilterIn("user_did", userIsFollowing)) + filters = append(filters, orm.FilterIn("did", userIsFollowing)) } follows, err := GetFollows(e, limit, filters...) -- tangled.sh