A vibe coded tangled fork which supports pijul.

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>

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