From 10e3b844e5c94b9d63c09c5b31c29c17d03a0393 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Tue, 18 Nov 2025 00:17:22 +0900 Subject: [PATCH] appview/db: split star subjects Change-Id: rxozvmonryklxymyxrtqkmomnoryosxw - rename `starred_by_did` column to `did` - rename `repo_at` column to `subject_at` and removed foreign key constraints. - rename `RepoAt` field to `SubjectAt` - remove `Repo` field and split `models.Star` to `RepoStar` and `StringStar` as now there can be two kinds of reverse mappings. Signed-off-by: Seongmin Lee --- appview/db/db.go | 39 +++++ appview/db/repos.go | 6 +- appview/db/star.go | 138 +++++------------- appview/db/timeline.go | 16 +- appview/ingester.go | 6 +- appview/models/star.go | 19 ++- appview/models/timeline.go | 2 +- appview/notify/db/db.go | 7 +- appview/notify/posthog/notifier.go | 4 +- .../timeline/fragments/timeline.html | 2 +- appview/state/profile.go | 8 +- appview/state/star.go | 6 +- 12 files changed, 117 insertions(+), 136 deletions(-) diff --git a/appview/db/db.go b/appview/db/db.go index 9dba07c7..7a645684 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -1128,6 +1128,45 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { return err }) + // remove the foreign key constraints from stars. + runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { + _, err := tx.Exec(` + create table stars_new ( + id integer primary key autoincrement, + did text not null, + rkey text not null, + + subject_at text not null, + + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + unique(did, rkey), + unique(did, subject_at) + ); + + insert into stars_new ( + id, + did, + rkey, + subject_at, + created + ) + select + id, + starred_by_did, + rkey, + repo_at, + created + from stars; + + drop table stars; + alter table stars_new rename to stars; + + create index if not exists idx_stars_created on stars(created); + create index if not exists idx_stars_subject_at_created on stars(subject_at, created); + `) + return err + }) + return &DB{ db, logger, diff --git a/appview/db/repos.go b/appview/db/repos.go index 095e6ded..7fdc87c7 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -208,10 +208,10 @@ func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { starCountQuery := fmt.Sprintf( `select - repo_at, count(1) + subject_at, count(1) from stars - where repo_at in (%s) - group by repo_at`, + where subject_at in (%s) + group by subject_at`, inClause, ) rows, err = e.Query(starCountQuery, args...) diff --git a/appview/db/star.go b/appview/db/star.go index f7f7c59b..b95fde65 100644 --- a/appview/db/star.go +++ b/appview/db/star.go @@ -14,10 +14,10 @@ import ( ) func AddStar(e Execer, star *models.Star) error { - query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)` + query := `insert or ignore into stars (did, subject_at, rkey) values (?, ?, ?)` _, err := e.Exec( query, - star.StarredByDid, + star.Did, star.RepoAt.String(), star.Rkey, ) @@ -25,16 +25,16 @@ func AddStar(e Execer, star *models.Star) error { } // Get a star record -func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*models.Star, error) { +func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) { query := ` - select starred_by_did, repo_at, created, rkey + select did, subject_at, created, rkey from stars - where starred_by_did = ? and repo_at = ?` - row := e.QueryRow(query, starredByDid, repoAt) + where did = ? and subject_at = ?` + row := e.QueryRow(query, did, subjectAt) var star models.Star var created string - err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey) + err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) if err != nil { return nil, err } @@ -51,21 +51,21 @@ func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*models.Star, } // Remove a star -func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error { - _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt) +func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error { + _, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt) return err } // Remove a star -func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error { - _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey) +func DeleteStarByRkey(e Execer, did string, rkey string) error { + _, err := e.Exec(`delete from stars where did = ? and rkey = ?`, did, rkey) return err } -func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) { +func GetStarCount(e Execer, subjectAt syntax.ATURI) (int, error) { stars := 0 err := e.QueryRow( - `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars) + `select count(did) from stars where subject_at = ?`, subjectAt).Scan(&stars) if err != nil { return 0, err } @@ -89,9 +89,9 @@ func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[stri } query := fmt.Sprintf(` - SELECT repo_at + SELECT subject_at FROM stars - WHERE starred_by_did = ? AND repo_at IN (%s) + WHERE did = ? AND subject_at IN (%s) `, strings.Join(placeholders, ",")) rows, err := e.Query(query, args...) @@ -118,19 +118,22 @@ func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[stri return result, nil } -func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool { - statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt}) +func GetStarStatus(e Execer, userDid string, subjectAt syntax.ATURI) bool { + statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{subjectAt}) if err != nil { return false } - return statuses[repoAt.String()] + return statuses[subjectAt.String()] } // GetStarStatuses returns a map of repo URIs to star status for a given user -func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { - return getStarStatuses(e, userDid, repoAts) +func GetStarStatuses(e Execer, userDid string, subjectAts []syntax.ATURI) (map[string]bool, error) { + return getStarStatuses(e, userDid, subjectAts) } -func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { + +// GetRepoStars return a list of stars each holding target repository. +// If there isn't known repo with starred at-uri, those stars will be ignored. +func GetRepoStars(e Execer, limit int, filters ...filter) ([]models.RepoStar, error) { var conditions []string var args []any for _, filter := range filters { @@ -149,7 +152,7 @@ func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { } repoQuery := fmt.Sprintf( - `select starred_by_did, repo_at, created, rkey + `select did, subject_at, created, rkey from stars %s order by created desc @@ -166,7 +169,7 @@ func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { for rows.Next() { var star models.Star var created string - err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey) + err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) if err != nil { return nil, err } @@ -197,20 +200,19 @@ func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { return nil, err } + var repoStars []models.RepoStar for _, r := range repos { if stars, ok := starMap[string(r.RepoAt())]; ok { - for i := range stars { - stars[i].Repo = &r + for _, star := range stars { + repoStars = append(repoStars, models.RepoStar{ + Star: star, + Repo: &r, + }) } } } - var stars []models.Star - for _, s := range starMap { - stars = append(stars, s...) - } - - slices.SortFunc(stars, func(a, b models.Star) int { + slices.SortFunc(repoStars, func(a, b models.RepoStar) int { if a.Created.After(b.Created) { return -1 } @@ -220,7 +222,7 @@ func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) { return 0 }) - return stars, nil + return repoStars, nil } func CountStars(e Execer, filters ...filter) (int64, error) { @@ -247,87 +249,25 @@ func CountStars(e Execer, filters ...filter) (int64, error) { return count, nil } -func GetAllStars(e Execer, limit int) ([]models.Star, error) { - var stars []models.Star - - rows, err := e.Query(` - select - s.starred_by_did, - s.repo_at, - s.rkey, - s.created, - r.did, - r.name, - r.knot, - r.rkey, - r.created - from stars s - join repos r on s.repo_at = r.at_uri - `) - - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var star models.Star - var repo models.Repo - var starCreatedAt, repoCreatedAt string - - if err := rows.Scan( - &star.StarredByDid, - &star.RepoAt, - &star.Rkey, - &starCreatedAt, - &repo.Did, - &repo.Name, - &repo.Knot, - &repo.Rkey, - &repoCreatedAt, - ); err != nil { - return nil, err - } - - star.Created, err = time.Parse(time.RFC3339, starCreatedAt) - if err != nil { - star.Created = time.Now() - } - repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt) - if err != nil { - repo.Created = time.Now() - } - star.Repo = &repo - - stars = append(stars, star) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return stars, nil -} - // GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) { // first, get the top repo URIs by star count from the last week query := ` with recent_starred_repos as ( - select distinct repo_at + select distinct subject_at from stars where created >= datetime('now', '-7 days') ), repo_star_counts as ( select - s.repo_at, + s.subject_at, count(*) as stars_gained_last_week from stars s - join recent_starred_repos rsr on s.repo_at = rsr.repo_at + join recent_starred_repos rsr on s.subject_at = rsr.subject_at where s.created >= datetime('now', '-7 days') - group by s.repo_at + group by s.subject_at ) - select rsc.repo_at + select rsc.subject_at from repo_star_counts rsc order by rsc.stars_gained_last_week desc limit 8 diff --git a/appview/db/timeline.go b/appview/db/timeline.go index 46439c77..4b90f1bb 100644 --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -146,24 +146,14 @@ func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowi func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { filters := make([]filter, 0) if userIsFollowing != nil { - filters = append(filters, FilterIn("starred_by_did", userIsFollowing)) + filters = append(filters, FilterIn("did", userIsFollowing)) } - stars, err := GetStars(e, limit, filters...) + stars, err := GetRepoStars(e, limit, filters...) if err != nil { return nil, err } - // filter star records without a repo - n := 0 - for _, s := range stars { - if s.Repo != nil { - stars[n] = s - n++ - } - } - stars = stars[:n] - var repos []models.Repo for _, s := range stars { repos = append(repos, *s.Repo) @@ -179,7 +169,7 @@ func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowi isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses) events = append(events, models.TimelineEvent{ - Star: &s, + RepoStar: &s, EventAt: s.Created, IsStarred: isStarred, StarCount: starCount, diff --git a/appview/ingester.go b/appview/ingester.go index 1c5d6d0a..40c9e572 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -121,9 +121,9 @@ func (i *Ingester) ingestStar(e *jmodels.Event) error { return err } err = db.AddStar(i.Db, &models.Star{ - StarredByDid: did, - RepoAt: subjectUri, - Rkey: e.Commit.RKey, + Did: did, + RepoAt: subjectUri, + Rkey: e.Commit.RKey, }) case jmodels.CommitOperationDelete: err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) diff --git a/appview/models/star.go b/appview/models/star.go index 8ba5806f..99d450a0 100644 --- a/appview/models/star.go +++ b/appview/models/star.go @@ -7,11 +7,20 @@ import ( ) type Star struct { - StarredByDid string - RepoAt syntax.ATURI - Created time.Time - Rkey string + Did string + RepoAt syntax.ATURI + Created time.Time + Rkey string +} - // optionally, populate this when querying for reverse mappings +// RepoStar is used for reverse mapping to repos +type RepoStar struct { + Star Repo *Repo } + +// StringStar is used for reverse mapping to strings +type StringStar struct { + Star + String *String +} diff --git a/appview/models/timeline.go b/appview/models/timeline.go index 10068570..5f303a0c 100644 --- a/appview/models/timeline.go +++ b/appview/models/timeline.go @@ -5,7 +5,7 @@ import "time" type TimelineEvent struct { *Repo *Follow - *Star + *RepoStar EventAt time.Time diff --git a/appview/notify/db/db.go b/appview/notify/db/db.go index 7ef9321a..0cd55892 100644 --- a/appview/notify/db/db.go +++ b/appview/notify/db/db.go @@ -7,6 +7,7 @@ import ( "slices" "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/models" "tangled.org/core/appview/notify" @@ -36,6 +37,10 @@ func (n *databaseNotifier) NewRepo(ctx context.Context, repo *models.Repo) { } func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) { + if star.RepoAt.Collection().String() != tangled.RepoNSID { + // skip string stars for now + return + } var err error repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt))) if err != nil { @@ -43,7 +48,7 @@ func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) { return } - actorDid := syntax.DID(star.StarredByDid) + actorDid := syntax.DID(star.Did) recipients := []syntax.DID{syntax.DID(repo.Did)} eventType := models.NotificationTypeRepoStarred entityType := "repo" diff --git a/appview/notify/posthog/notifier.go b/appview/notify/posthog/notifier.go index 4ca57511..2679d946 100644 --- a/appview/notify/posthog/notifier.go +++ b/appview/notify/posthog/notifier.go @@ -37,7 +37,7 @@ func (n *posthogNotifier) NewRepo(ctx context.Context, repo *models.Repo) { func (n *posthogNotifier) NewStar(ctx context.Context, star *models.Star) { err := n.client.Enqueue(posthog.Capture{ - DistinctId: star.StarredByDid, + DistinctId: star.Did, Event: "star", Properties: posthog.Properties{"repo_at": star.RepoAt.String()}, }) @@ -48,7 +48,7 @@ func (n *posthogNotifier) NewStar(ctx context.Context, star *models.Star) { func (n *posthogNotifier) DeleteStar(ctx context.Context, star *models.Star) { err := n.client.Enqueue(posthog.Capture{ - DistinctId: star.StarredByDid, + DistinctId: star.Did, Event: "unstar", Properties: posthog.Properties{"repo_at": star.RepoAt.String()}, }) diff --git a/appview/pages/templates/timeline/fragments/timeline.html b/appview/pages/templates/timeline/fragments/timeline.html index 081e112a..2f262c5a 100644 --- a/appview/pages/templates/timeline/fragments/timeline.html +++ b/appview/pages/templates/timeline/fragments/timeline.html @@ -61,7 +61,7 @@ {{ $event := index . 1 }} {{ $star := $event.Star }} {{ with $star }} - {{ $starrerHandle := resolve .StarredByDid }} + {{ $starrerHandle := resolve .Did }} {{ $repoOwnerHandle := resolve .Repo.Did }}
{{ template "user/fragments/picHandleLink" $starrerHandle }} diff --git a/appview/state/profile.go b/appview/state/profile.go index 2c47add2..efc91e1c 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -66,7 +66,7 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { return nil, fmt.Errorf("failed to get string count: %w", err) } - starredCount, err := db.CountStars(s.db, db.FilterEq("starred_by_did", did)) + starredCount, err := db.CountStars(s.db, db.FilterEq("did", did)) if err != nil { return nil, fmt.Errorf("failed to get starred repo count: %w", err) } @@ -211,7 +211,7 @@ func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { } l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) - stars, err := db.GetStars(s.db, 0, db.FilterEq("starred_by_did", profile.UserDid)) + stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid)) if err != nil { l.Error("failed to get stars", "err", err) s.pages.Error500(w) @@ -219,9 +219,7 @@ func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { } var repos []models.Repo for _, s := range stars { - if s.Repo != nil { - repos = append(repos, *s.Repo) - } + repos = append(repos, *s.Repo) } err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ diff --git a/appview/state/star.go b/appview/state/star.go index 3473f65d..49c4e96d 100644 --- a/appview/state/star.go +++ b/appview/state/star.go @@ -57,9 +57,9 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { log.Println("created atproto record: ", resp.Uri) star := &models.Star{ - StarredByDid: currentUser.Did, - RepoAt: subjectUri, - Rkey: rkey, + Did: currentUser.Did, + RepoAt: subjectUri, + Rkey: rkey, } err = db.AddStar(s.db, star) -- 2.43.0