diff --git a/appview/db/db.go b/appview/db/db.go index ab4060b5..f98efb6f 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -2247,6 +2247,124 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { 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 autoincrement id and the (did, subject) unique constraint + 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_type text not null, + subject 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_type, subject, created) + select did, rkey, subject_type, subject, created from stars; + + drop table stars; + alter table stars_new rename to stars; + + create index if not exists idx_stars_subject on stars(subject); + create index if not exists idx_stars_subject_type on stars(subject_type); + create index if not exists idx_stars_created on stars(created); + create index if not exists idx_stars_did_type_created on stars(did, subject_type, created); + `); 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; + + create index if not exists idx_follows_subject_did on follows(subject_did); + create index if not exists idx_follows_created on follows(created); + `); 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 or ignore 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{ db, logger, diff --git a/appview/db/follow.go b/appview/db/follow.go index fb123b6f..d2527ce8 100644 --- a/appview/db/follow.go +++ b/appview/db/follow.go @@ -11,14 +11,14 @@ import ( ) 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 @@ func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) { // 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 @@ func GetFollowerFollowingCount(e Execer, did string) (models.FollowStats, error) 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 @@ func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]models.Foll 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 @@ func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, er } 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 @@ func GetFollowers(e Execer, did string) ([]models.Follow, error) { } 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 @@ func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[stri 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/profile.go b/appview/db/profile.go index f981fad8..736c24e8 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -496,7 +496,7 @@ func GetVanityStat(e Execer, did string, stat models.VanityStatKind) (uint64, er query = `select count(id) from repos where did = ?` args = append(args, did) case models.VanityStatStarCount: - query = `select count(s.id) from stars s join repos r on s.subject = r.repo_did where s.subject_type = 'repo' and r.did = ?` + query = `select count(s.at_uri) from stars s join repos r on s.subject = r.repo_did where s.subject_type = 'repo' and r.did = ?` args = append(args, did) case models.VanityStatNone: return 0, nil diff --git a/appview/db/reaction.go b/appview/db/reaction.go index c7f584c4..4a8ef0a8 100644 --- a/appview/db/reaction.go +++ b/appview/db/reaction.go @@ -10,19 +10,19 @@ import ( "tangled.org/core/orm" ) -func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind, rkey string, created time.Time) error { - query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey, created) values (?, ?, ?, ?, ?)` - _, err := e.Exec(query, reactedByDid, threadAt, kind, rkey, created.UTC().Format(time.RFC3339)) +func AddReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind, rkey string, created time.Time) error { + query := `insert or ignore into reactions (did, subject_at, kind, rkey, created) values (?, ?, ?, ?, ?)` + _, err := e.Exec(query, did, subjectAt, kind, rkey, created.UTC().Format(time.RFC3339)) 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 @@ -43,30 +43,30 @@ func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind mode } // 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) (int, error) { +func GetReactionCount(e Execer, subjectAt syntax.ATURI) (int, error) { count := 0 - err := e.QueryRow(`select count(reacted_by_did) from reactions where thread_at = ?`, threadAt).Scan(&count) + err := e.QueryRow(`select count(did) from reactions where subject_at = ?`, subjectAt).Scan(&count) if err != nil { return 0, err } return count, nil } -func GetReactionCountByKind(e Execer, threadAt syntax.ATURI, kind models.ReactionKind) (int, error) { +func GetReactionCountByKind(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 } @@ -74,9 +74,9 @@ func GetReactionCountByKind(e Execer, threadAt syntax.ATURI, kind models.Reactio } // GetReactionDisplayDataMap returns map of [models.ReactionKind]->[models.ReactionDisplayData] -func GetReactionMap(e Execer, userLimit int, threadAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { - reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{threadAt}, userLimit) - return reactionMaps[threadAt], err +func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { + reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{subjectAt}, userLimit) + return reactionMaps[subjectAt], err } // ListReactionDisplayDataMap returns map of [syntax.ATURI]->[models.ReactionKind]->[models.ReactionDisplayData] @@ -85,25 +85,25 @@ func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) return nil, nil } - filter := orm.FilterIn("thread_at", threads) + filter := orm.FilterIn("subject_at", threads) args := filter.Arg() args = append(args, userLimit) rows, err := e.Query( fmt.Sprintf( `with ranked_reactions as ( select - thread_at, + subject_at, kind, - reacted_by_did, - row_number() over (partition by thread_at, kind order by created asc) as rn, - count(*) over (partition by thread_at, kind) as total + did, + row_number() over (partition by subject_at, kind order by created asc) as rn, + count(*) over (partition by subject_at, kind) as total from reactions where %s ) - select thread_at, kind, reacted_by_did, total + select subject_at, kind, did, total from ranked_reactions where rn <= ? - order by thread_at, kind, rn asc`, + order by subject_at, kind, rn asc`, filter.Condition(), ), args..., @@ -143,9 +143,9 @@ func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) } // GetReactionStatusMap returns map of [models.ReactionKind]->[bool] -func GetReactionStatusMap(e Execer, userDid syntax.DID, threadAt syntax.ATURI) (map[models.ReactionKind]bool, error) { - reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{threadAt}, userDid) - return reactionMaps[threadAt], err +func GetReactionStatusMap(e Execer, userDid syntax.DID, subjectAt syntax.ATURI) (map[models.ReactionKind]bool, error) { + reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{subjectAt}, userDid) + return reactionMaps[subjectAt], err } // ListReactionStatusMap returns map of [syntax.ATURI]->[models.ReactionKind]->[bool] @@ -154,13 +154,13 @@ func ListReactionStatusMap(e Execer, threads []syntax.ATURI, userDid syntax.DID) return nil, nil } - filter := orm.FilterIn("thread_at", threads) + filter := orm.FilterIn("subject_at", threads) args := []any{userDid} args = append(args, filter.Arg()...) rows, err := e.Query( fmt.Sprintf( - `select thread_at, kind from reactions - where reacted_by_did = ? and %s`, + `select subject_at, kind from reactions + where did = ? and %s`, filter.Condition(), ), args..., diff --git a/appview/db/timeline.go b/appview/db/timeline.go index 38bc9ea9..161fefa7 100644 --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -12,7 +12,7 @@ import ( // keeping the following-set check inside sqlite rather than materializing the // followed dids into a huge placeholder list. func followingFilter(key, loggedInUserDid string) orm.Filter { - return orm.FilterInSubquery(key, "select subject_did from follows where user_did = ?", loggedInUserDid) + return orm.FilterInSubquery(key, "select subject_did from follows where did = ?", loggedInUserDid) } // TODO: this gathers heterogenous events from different sources and aggregates @@ -220,7 +220,7 @@ func getTimelineStars(e Execer, limit int, loggedInUserDid string, followingOnly func getTimelineFollows(e Execer, limit int, loggedInUserDid string, followingOnly string) ([]models.TimelineEvent, error) { filters := make([]orm.Filter, 0) if followingOnly != "" { - filters = append(filters, followingFilter("user_did", followingOnly)) + filters = append(filters, followingFilter("did", followingOnly)) } follows, err := GetFollows(e, limit, filters...) diff --git a/appview/db/timeline_test.go b/appview/db/timeline_test.go index 54d4dc96..0898e636 100644 --- a/appview/db/timeline_test.go +++ b/appview/db/timeline_test.go @@ -9,7 +9,7 @@ import ( func seedFollow(t *testing.T, d *DB, userDid, subjectDid, rkey, followedAt string) { t.Helper() if _, err := d.Exec( - `insert into follows (user_did, subject_did, rkey, followed_at) values (?, ?, ?, ?)`, + `insert into follows (did, subject_did, rkey, created) values (?, ?, ?, ?)`, userDid, subjectDid, rkey, followedAt, ); err != nil { t.Fatalf("seedFollow %s -> %s: %v", userDid, subjectDid, err) diff --git a/appview/db/vouch.go b/appview/db/vouch.go index ed0c18bf..3e10bf0f 100644 --- a/appview/db/vouch.go +++ b/appview/db/vouch.go @@ -408,10 +408,10 @@ func GetVouchSuggestions(e Execer, did string, limit int) ([]models.VouchSuggest union all - select f.subject_did as did, 7 as priority, f.followed_at as created, + select f.subject_did as did, 7 as priority, f.created as created, 'You recently followed this user' as reason from follows f - where f.user_did = ? + where f.did = ? and f.subject_did != ? union all diff --git a/cmd/dbtest/main.go b/cmd/dbtest/main.go new file mode 100644 index 00000000..12665be1 --- /dev/null +++ b/cmd/dbtest/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + "log" + + "tangled.org/core/appview/db" +) + +func main() { + _, err := db.Make(context.Background(), "./tmp/appview.db") + if err != nil { + log.Fatalln("failed to make db:", err) + } +}