Monorepo for Tangled tangled.org
10

Configure Feed

Select the types of activity you want to include in your feed.

appview: ingest profile deletion

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (May 18, 2026, 11:13 AM +0300) commit f99fe266 parent f9c85351 change-id muyouotw
+183 -2
+10
appview/db/profile.go
··· 234 234 return tx.Commit() 235 235 } 236 236 237 + func DeleteProfile(tx *sql.Tx, did string) error { 238 + defer tx.Rollback() 239 + 240 + if _, err := tx.Exec(`delete from profile where did = ?`, did); err != nil { 241 + return err 242 + } 243 + 244 + return tx.Commit() 245 + } 246 + 237 247 func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) { 238 248 var conditions []string 239 249 var args []any
+151
appview/db/profile_test.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "testing" 7 + ) 8 + 9 + func seedProfile(t *testing.T, d *DB, did string) { 10 + t.Helper() 11 + if _, err := d.Exec( 12 + `insert into profile (did, description, include_bluesky, location, preferred_handle) 13 + values (?, ?, ?, ?, ?)`, 14 + did, "hi", 0, "", "boltless.bsky.social", 15 + ); err != nil { 16 + t.Fatalf("seed profile: %v", err) 17 + } 18 + if _, err := d.Exec( 19 + `insert into profile_links (did, link) values (?, ?)`, 20 + did, "https://boltless.example/blog", 21 + ); err != nil { 22 + t.Fatalf("seed profile_links: %v", err) 23 + } 24 + if _, err := d.Exec( 25 + `insert into profile_stats (did, kind) values (?, ?)`, 26 + did, "open-pull-request-count", 27 + ); err != nil { 28 + t.Fatalf("seed profile_stats: %v", err) 29 + } 30 + if _, err := d.Exec( 31 + `insert into profile_pinned_repositories (did, pin) values (?, ?)`, 32 + did, "did:plc:limpet", 33 + ); err != nil { 34 + t.Fatalf("seed profile_pinned_repositories: %v", err) 35 + } 36 + } 37 + 38 + func countRows(t *testing.T, d *DB, query string, args ...any) int { 39 + t.Helper() 40 + var n int 41 + if err := d.QueryRow(query, args...).Scan(&n); err != nil { 42 + t.Fatalf("count: %v", err) 43 + } 44 + return n 45 + } 46 + 47 + func TestDeleteProfile_CascadesAllChildTables(t *testing.T) { 48 + d := newTestDB(t) 49 + const did = "did:plc:boltless" 50 + seedProfile(t, d, did) 51 + 52 + if got := countRows(t, d, `select count(*) from profile where did = ?`, did); got != 1 { 53 + t.Fatalf("pre: profile rows = %d, want 1", got) 54 + } 55 + if got := countRows(t, d, `select count(*) from profile_links where did = ?`, did); got != 1 { 56 + t.Fatalf("pre: profile_links rows = %d, want 1", got) 57 + } 58 + if got := countRows(t, d, `select count(*) from profile_stats where did = ?`, did); got != 1 { 59 + t.Fatalf("pre: profile_stats rows = %d, want 1", got) 60 + } 61 + if got := countRows(t, d, `select count(*) from profile_pinned_repositories where did = ?`, did); got != 1 { 62 + t.Fatalf("pre: profile_pinned_repositories rows = %d, want 1", got) 63 + } 64 + 65 + tx, err := d.Begin() 66 + if err != nil { 67 + t.Fatalf("Begin: %v", err) 68 + } 69 + if err := DeleteProfile(tx, did); err != nil { 70 + t.Fatalf("DeleteProfile: %v", err) 71 + } 72 + 73 + if got := countRows(t, d, `select count(*) from profile where did = ?`, did); got != 0 { 74 + t.Errorf("post: profile rows = %d, want 0", got) 75 + } 76 + if got := countRows(t, d, `select count(*) from profile_links where did = ?`, did); got != 0 { 77 + t.Errorf("post: profile_links rows = %d, want 0 (cascade)", got) 78 + } 79 + if got := countRows(t, d, `select count(*) from profile_stats where did = ?`, did); got != 0 { 80 + t.Errorf("post: profile_stats rows = %d, want 0 (cascade)", got) 81 + } 82 + if got := countRows(t, d, `select count(*) from profile_pinned_repositories where did = ?`, did); got != 0 { 83 + t.Errorf("post: profile_pinned_repositories rows = %d, want 0 (cascade)", got) 84 + } 85 + } 86 + 87 + func TestDeleteProfile_NoRowsIsNoop(t *testing.T) { 88 + d := newTestDB(t) 89 + 90 + tx, err := d.Begin() 91 + if err != nil { 92 + t.Fatalf("Begin: %v", err) 93 + } 94 + if err := DeleteProfile(tx, "did:plc:akshay"); err != nil { 95 + t.Errorf("DeleteProfile on missing did: %v, want nil", err) 96 + } 97 + } 98 + 99 + func TestDeleteProfile_LeavesOtherDidsAlone(t *testing.T) { 100 + d := newTestDB(t) 101 + seedProfile(t, d, "did:plc:boltless") 102 + seedProfile(t, d, "did:plc:akshay") 103 + 104 + tx, err := d.Begin() 105 + if err != nil { 106 + t.Fatalf("Begin: %v", err) 107 + } 108 + if err := DeleteProfile(tx, "did:plc:boltless"); err != nil { 109 + t.Fatalf("DeleteProfile: %v", err) 110 + } 111 + 112 + if got := countRows(t, d, `select count(*) from profile where did = ?`, "did:plc:akshay"); got != 1 { 113 + t.Errorf("other profile should survive: rows = %d, want 1", got) 114 + } 115 + if got := countRows(t, d, `select count(*) from profile_links where did = ?`, "did:plc:akshay"); got != 1 { 116 + t.Errorf("other profile_links should survive: rows = %d, want 1", got) 117 + } 118 + if got := countRows(t, d, `select count(*) from profile_stats where did = ?`, "did:plc:akshay"); got != 1 { 119 + t.Errorf("other profile_stats should survive: rows = %d, want 1", got) 120 + } 121 + if got := countRows(t, d, `select count(*) from profile_pinned_repositories where did = ?`, "did:plc:akshay"); got != 1 { 122 + t.Errorf("other profile_pinned_repositories should survive: rows = %d, want 1", got) 123 + } 124 + } 125 + 126 + func TestGetPreferredHandle_AfterDeleteReturnsNoRows(t *testing.T) { 127 + d := newTestDB(t) 128 + const did = "did:plc:boltless" 129 + seedProfile(t, d, did) 130 + 131 + h, err := GetPreferredHandle(d, did) 132 + if err != nil { 133 + t.Fatalf("GetPreferredHandle pre-delete: %v", err) 134 + } 135 + if string(h) != "boltless.bsky.social" { 136 + t.Fatalf("handle = %q, want %q", h, "boltless.bsky.social") 137 + } 138 + 139 + tx, err := d.Begin() 140 + if err != nil { 141 + t.Fatalf("Begin: %v", err) 142 + } 143 + if err := DeleteProfile(tx, did); err != nil { 144 + t.Fatalf("DeleteProfile: %v", err) 145 + } 146 + 147 + _, err = GetPreferredHandle(d, did) 148 + if !errors.Is(err, sql.ErrNoRows) { 149 + t.Errorf("GetPreferredHandle post-delete: err = %v, want sql.ErrNoRows", err) 150 + } 151 + }
+22 -2
appview/ingester.go
··· 579 579 580 580 tx, err := i.Db.Begin() 581 581 if err != nil { 582 - return fmt.Errorf("failed to start transaction") 582 + return fmt.Errorf("failed to start transaction: %w", err) 583 583 } 584 584 585 585 err = db.ValidateProfile(tx, &profile) ··· 602 602 } 603 603 } 604 604 case jmodels.CommitOperationDelete: 605 - err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 605 + tx, beginErr := i.Db.Begin() 606 + if beginErr != nil { 607 + return fmt.Errorf("failed to start transaction: %w", beginErr) 608 + } 609 + 610 + priorHandle, phErr := db.GetPreferredHandle(tx, did) 611 + if phErr != nil && !errors.Is(phErr, sql.ErrNoRows) { 612 + l.Warn("failed to read prior preferred handle", "err", phErr) 613 + } 614 + 615 + err = db.DeleteProfile(tx, did) 616 + if err == nil && i.Cache != nil { 617 + pipe := i.Cache.Pipeline() 618 + pipe.Del(ctx, fmt.Sprintf(cache.PreferredHandleByDid, did)) 619 + if priorHandle != "" { 620 + pipe.Del(ctx, fmt.Sprintf(cache.PreferredHandleByHandle, string(priorHandle))) 621 + } 622 + if _, execErr := pipe.Exec(ctx); execErr != nil { 623 + l.Warn("failed to evict preferred handle cache", "err", execErr) 624 + } 625 + } 606 626 } 607 627 608 628 if err != nil {