Monorepo for Tangled tangled.org

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 <git@boltless.me>

boltless.me 56bf1c76 10b0fbe9

verified
+140 -30
+110
appview/db/db.go
··· 3 import ( 4 "context" 5 "database/sql" 6 "log/slog" 7 "strings" 8 ··· 1263 alter table profile_pinned_repositories_new rename to profile_pinned_repositories; 1264 `) 1265 return err 1266 }) 1267 1268 return &DB{
··· 3 import ( 4 "context" 5 "database/sql" 6 + "fmt" 7 "log/slog" 8 "strings" 9 ··· 1264 alter table profile_pinned_repositories_new rename to profile_pinned_repositories; 1265 `) 1266 return err 1267 + }) 1268 + 1269 + // several changes here 1270 + // 1. remove autoincrement id for these tables 1271 + // 2. remove unique constraints other than (did, rkey) to handle non-unique atproto records 1272 + // 3. add generated at_uri field 1273 + // 1274 + // see comments below and commit message for details 1275 + orm.RunMigration(conn, logger, "flexible-stars-reactions-follows-public_keys", func(tx *sql.Tx) error { 1276 + // - add at_uri 1277 + // - remove unique constraint (did, subject_at) 1278 + if _, err := tx.Exec(` 1279 + create table stars_new ( 1280 + did text not null, 1281 + rkey text not null, 1282 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.star' || '/' || rkey) stored, 1283 + 1284 + subject_at text not null, 1285 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1286 + 1287 + unique(did, rkey) 1288 + ); 1289 + 1290 + insert into stars_new (did, rkey, subject_at, created) 1291 + select did, rkey, subject_at, created from stars; 1292 + 1293 + drop table stars; 1294 + alter table stars_new rename to stars; 1295 + `); err != nil { 1296 + return fmt.Errorf("migrating stars: %w", err) 1297 + } 1298 + 1299 + // - add at_uri 1300 + // - reacted_by_did -> did 1301 + // - thread_at -> subject_at 1302 + // - remove unique constraint 1303 + if _, err := tx.Exec(` 1304 + create table reactions_new ( 1305 + did text not null, 1306 + rkey text not null, 1307 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.reaction' || '/' || rkey) stored, 1308 + 1309 + subject_at text not null, 1310 + kind text not null, 1311 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1312 + 1313 + unique(did, rkey) 1314 + ); 1315 + 1316 + insert into reactions_new (did, rkey, subject_at, kind, created) 1317 + select reacted_by_did, rkey, thread_at, kind, created from reactions; 1318 + 1319 + drop table reactions; 1320 + alter table reactions_new rename to reactions; 1321 + `); err != nil { 1322 + return fmt.Errorf("migrating reactions: %w", err) 1323 + } 1324 + 1325 + // - add at_uri column 1326 + // - user_did -> did 1327 + // - followed_at -> created 1328 + // - remove unique constraint 1329 + // - remove check constraint 1330 + if _, err := tx.Exec(` 1331 + create table follows_new ( 1332 + did text not null, 1333 + rkey text not null, 1334 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.graph.follow' || '/' || rkey) stored, 1335 + 1336 + subject_did text not null, 1337 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1338 + 1339 + unique(did, rkey) 1340 + ); 1341 + 1342 + insert into follows_new (did, rkey, subject_did, created) 1343 + select user_did, rkey, subject_did, followed_at from follows; 1344 + 1345 + drop table follows; 1346 + alter table follows_new rename to follows; 1347 + `); err != nil { 1348 + return fmt.Errorf("migrating follows: %w", err) 1349 + } 1350 + 1351 + // - add at_uri column 1352 + // - remove foreign key relationship from repos 1353 + if _, err := tx.Exec(` 1354 + create table public_keys_new ( 1355 + did text not null, 1356 + rkey text not null, 1357 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.publicKey' || '/' || rkey) stored, 1358 + 1359 + name text not null, 1360 + key text not null, 1361 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1362 + 1363 + unique(did, rkey) 1364 + ); 1365 + 1366 + insert into public_keys_new (did, rkey, name, key, created) 1367 + select did, rkey, name, key, created from public_keys; 1368 + 1369 + drop table public_keys; 1370 + alter table public_keys_new rename to public_keys; 1371 + `); err != nil { 1372 + return fmt.Errorf("migrating public_keys: %w", err) 1373 + } 1374 + 1375 + return nil 1376 }) 1377 1378 return &DB{
+12 -12
appview/db/follow.go
··· 11 ) 12 13 func AddFollow(e Execer, follow *models.Follow) error { 14 - query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)` 15 _, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey) 16 return err 17 } 18 19 // Get a follow record 20 func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) { 21 - query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?` 22 row := e.QueryRow(query, userDid, subjectDid) 23 24 var follow models.Follow ··· 41 42 // Remove a follow 43 func DeleteFollow(e Execer, userDid, subjectDid string) error { 44 - _, err := e.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid) 45 return err 46 } 47 48 // Remove a follow 49 func DeleteFollowByRkey(e Execer, userDid, rkey string) error { 50 - _, err := e.Exec(`delete from follows where user_did = ? and rkey = ?`, userDid, rkey) 51 return err 52 } 53 ··· 56 err := e.QueryRow( 57 `SELECT 58 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, 59 - COUNT(CASE WHEN user_did = ? THEN 1 END) AS following 60 FROM follows;`, did, did).Scan(&followers, &following) 61 if err != nil { 62 return models.FollowStats{}, err ··· 96 group by subject_did 97 ) f 98 full outer join ( 99 - select user_did as did, count(*) as following 100 from follows 101 - where user_did in (%s) 102 - group by user_did 103 ) g on f.did = g.did`, 104 placeholderStr, placeholderStr) 105 ··· 156 } 157 158 query := fmt.Sprintf( 159 - `select user_did, subject_did, followed_at, rkey 160 from follows 161 %s 162 - order by followed_at desc 163 %s 164 `, whereClause, limitClause) 165 ··· 198 } 199 200 func GetFollowing(e Execer, did string) ([]models.Follow, error) { 201 - return GetFollows(e, 0, orm.FilterEq("user_did", did)) 202 } 203 204 func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) { ··· 239 query := fmt.Sprintf(` 240 SELECT subject_did 241 FROM follows 242 - WHERE user_did = ? AND subject_did IN (%s) 243 `, strings.Join(placeholders, ",")) 244 245 rows, err := e.Query(query, args...)
··· 11 ) 12 13 func AddFollow(e Execer, follow *models.Follow) error { 14 + query := `insert or ignore into follows (did, subject_did, rkey) values (?, ?, ?)` 15 _, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey) 16 return err 17 } 18 19 // Get a follow record 20 func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) { 21 + query := `select did, subject_did, created, rkey from follows where did = ? and subject_did = ?` 22 row := e.QueryRow(query, userDid, subjectDid) 23 24 var follow models.Follow ··· 41 42 // Remove a follow 43 func DeleteFollow(e Execer, userDid, subjectDid string) error { 44 + _, err := e.Exec(`delete from follows where did = ? and subject_did = ?`, userDid, subjectDid) 45 return err 46 } 47 48 // Remove a follow 49 func DeleteFollowByRkey(e Execer, userDid, rkey string) error { 50 + _, err := e.Exec(`delete from follows where did = ? and rkey = ?`, userDid, rkey) 51 return err 52 } 53 ··· 56 err := e.QueryRow( 57 `SELECT 58 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, 59 + COUNT(CASE WHEN did = ? THEN 1 END) AS following 60 FROM follows;`, did, did).Scan(&followers, &following) 61 if err != nil { 62 return models.FollowStats{}, err ··· 96 group by subject_did 97 ) f 98 full outer join ( 99 + select did as did, count(*) as following 100 from follows 101 + where did in (%s) 102 + group by did 103 ) g on f.did = g.did`, 104 placeholderStr, placeholderStr) 105 ··· 156 } 157 158 query := fmt.Sprintf( 159 + `select did, subject_did, created, rkey 160 from follows 161 %s 162 + order by created desc 163 %s 164 `, whereClause, limitClause) 165 ··· 198 } 199 200 func GetFollowing(e Execer, did string) ([]models.Follow, error) { 201 + return GetFollows(e, 0, orm.FilterEq("did", did)) 202 } 203 204 func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) { ··· 239 query := fmt.Sprintf(` 240 SELECT subject_did 241 FROM follows 242 + WHERE did = ? AND subject_did IN (%s) 243 `, strings.Join(placeholders, ",")) 244 245 rows, err := e.Query(query, args...)
+17 -17
appview/db/reaction.go
··· 8 "tangled.org/core/appview/models" 9 ) 10 11 - func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind, rkey string) error { 12 - query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey) values (?, ?, ?, ?)` 13 - _, err := e.Exec(query, reactedByDid, threadAt, kind, rkey) 14 return err 15 } 16 17 // Get a reaction record 18 - func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind) (*models.Reaction, error) { 19 query := ` 20 - select reacted_by_did, thread_at, created, rkey 21 from reactions 22 - where reacted_by_did = ? and thread_at = ? and kind = ?` 23 - row := e.QueryRow(query, reactedByDid, threadAt, kind) 24 25 var reaction models.Reaction 26 var created string ··· 41 } 42 43 // Remove a reaction 44 - func DeleteReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind) error { 45 - _, err := e.Exec(`delete from reactions where reacted_by_did = ? and thread_at = ? and kind = ?`, reactedByDid, threadAt, kind) 46 return err 47 } 48 49 // Remove a reaction 50 - func DeleteReactionByRkey(e Execer, reactedByDid string, rkey string) error { 51 - _, err := e.Exec(`delete from reactions where reacted_by_did = ? and rkey = ?`, reactedByDid, rkey) 52 return err 53 } 54 55 - func GetReactionCount(e Execer, threadAt syntax.ATURI, kind models.ReactionKind) (int, error) { 56 count := 0 57 err := e.QueryRow( 58 - `select count(reacted_by_did) from reactions where thread_at = ? and kind = ?`, threadAt, kind).Scan(&count) 59 if err != nil { 60 return 0, err 61 } 62 return count, nil 63 } 64 65 - func GetReactionMap(e Execer, userLimit int, threadAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { 66 query := ` 67 - select kind, reacted_by_did, 68 row_number() over (partition by kind order by created asc) as rn, 69 count(*) over (partition by kind) as total 70 from reactions 71 - where thread_at = ? 72 order by kind, created asc` 73 74 - rows, err := e.Query(query, threadAt) 75 if err != nil { 76 return nil, err 77 }
··· 8 "tangled.org/core/appview/models" 9 ) 10 11 + func AddReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind, rkey string) error { 12 + query := `insert or ignore into reactions (did, subject_at, kind, rkey) values (?, ?, ?, ?)` 13 + _, err := e.Exec(query, did, subjectAt, kind, rkey) 14 return err 15 } 16 17 // Get a reaction record 18 + func GetReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind) (*models.Reaction, error) { 19 query := ` 20 + select did, subject_at, created, rkey 21 from reactions 22 + where did = ? and subject_at = ? and kind = ?` 23 + row := e.QueryRow(query, did, subjectAt, kind) 24 25 var reaction models.Reaction 26 var created string ··· 41 } 42 43 // Remove a reaction 44 + func DeleteReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind) error { 45 + _, err := e.Exec(`delete from reactions where did = ? and subject_at = ? and kind = ?`, did, subjectAt, kind) 46 return err 47 } 48 49 // Remove a reaction 50 + func DeleteReactionByRkey(e Execer, did string, rkey string) error { 51 + _, err := e.Exec(`delete from reactions where did = ? and rkey = ?`, did, rkey) 52 return err 53 } 54 55 + func GetReactionCount(e Execer, subjectAt syntax.ATURI, kind models.ReactionKind) (int, error) { 56 count := 0 57 err := e.QueryRow( 58 + `select count(did) from reactions where subject_at = ? and kind = ?`, subjectAt, kind).Scan(&count) 59 if err != nil { 60 return 0, err 61 } 62 return count, nil 63 } 64 65 + func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { 66 query := ` 67 + select kind, did, 68 row_number() over (partition by kind order by created asc) as rn, 69 count(*) over (partition by kind) as total 70 from reactions 71 + where subject_at = ? 72 order by kind, created asc` 73 74 + rows, err := e.Query(query, subjectAt) 75 if err != nil { 76 return nil, err 77 }
+1 -1
appview/db/timeline.go
··· 183 func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 184 filters := make([]orm.Filter, 0) 185 if userIsFollowing != nil { 186 - filters = append(filters, orm.FilterIn("user_did", userIsFollowing)) 187 } 188 189 follows, err := GetFollows(e, limit, filters...)
··· 183 func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 184 filters := make([]orm.Filter, 0) 185 if userIsFollowing != nil { 186 + filters = append(filters, orm.FilterIn("did", userIsFollowing)) 187 } 188 189 follows, err := GetFollows(e, limit, filters...)