A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

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

Add migrations

+162
+5
internal/db/db.go
··· 103 103 return nil, err 104 104 } 105 105 106 + if err := runMigrations(d); err != nil { 107 + d.Close() 108 + return nil, err 109 + } 110 + 106 111 if err := initArticlesSchema(d); err != nil { 107 112 d.Close() 108 113 return nil, err
+157
internal/db/migrations.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "strings" 7 + ) 8 + 9 + type migration struct { 10 + id int 11 + name string 12 + run func(db *DB) error 13 + } 14 + 15 + var migrations = []migration{ 16 + { 17 + id: 1, 18 + name: "add_person_target_type", 19 + run: migrateAddPersonTargetType, 20 + }, 21 + { 22 + id: 2, 23 + name: "feed_type_atproto", 24 + run: migrateFeedTypeATProto, 25 + }, 26 + } 27 + 28 + func runMigrations(db *DB) error { 29 + if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( 30 + id INTEGER PRIMARY KEY, 31 + name TEXT NOT NULL, 32 + run_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 33 + )`); err != nil { 34 + return fmt.Errorf("create schema_migrations table: %w", err) 35 + } 36 + 37 + for _, m := range migrations { 38 + var id int 39 + err := db.QueryRow("SELECT id FROM schema_migrations WHERE id = ?", m.id).Scan(&id) 40 + if err == nil { 41 + continue 42 + } 43 + if err != sql.ErrNoRows { 44 + return fmt.Errorf("check migration %d: %w", m.id, err) 45 + } 46 + 47 + if err := m.run(db); err != nil { 48 + return fmt.Errorf("migration %d (%s): %w", m.id, m.name, err) 49 + } 50 + 51 + if _, err := db.Exec("INSERT INTO schema_migrations (id, name) VALUES (?, ?)", m.id, m.name); err != nil { 52 + return fmt.Errorf("record migration %d: %w", m.id, err) 53 + } 54 + } 55 + 56 + return nil 57 + } 58 + 59 + func migrateFeedTypeATProto(db *DB) error { 60 + var checkClause string 61 + err := db.QueryRow("SELECT sql FROM articles.sqlite_master WHERE type='table' AND name='feeds'").Scan(&checkClause) 62 + if err != nil { 63 + if err == sql.ErrNoRows { 64 + return nil 65 + } 66 + return fmt.Errorf("read feeds schema: %w", err) 67 + } 68 + if strings.Contains(checkClause, "'atproto'") { 69 + return nil 70 + } 71 + 72 + for _, stmt := range []string{ 73 + `CREATE TABLE articles.feeds_new ( 74 + feed_url TEXT PRIMARY KEY, 75 + title TEXT, 76 + site_url TEXT, 77 + description TEXT, 78 + feed_type TEXT CHECK(feed_type IN ('rss', 'atom', 'json', 'atproto')), 79 + last_fetched_at DATETIME, 80 + last_error TEXT, 81 + subscriber_count INTEGER NOT NULL DEFAULT 0, 82 + consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, 83 + error_count INTEGER NOT NULL DEFAULT 0, 84 + favicon_url TEXT 85 + )`, 86 + `INSERT INTO articles.feeds_new SELECT * FROM articles.feeds`, 87 + `DROP TABLE articles.feeds`, 88 + `ALTER TABLE articles.feeds_new RENAME TO feeds`, 89 + } { 90 + if _, err := db.Exec(stmt); err != nil { 91 + return fmt.Errorf("migrate feed_type atproto: %w", err) 92 + } 93 + } 94 + 95 + return nil 96 + } 97 + 98 + func migrateAddPersonTargetType(db *DB) error { 99 + for _, m := range []struct { 100 + table string 101 + create string 102 + index string 103 + }{ 104 + { 105 + table: "dismissed_recommendations", 106 + create: `CREATE TABLE dismissed_recommendations_new ( 107 + user_did TEXT NOT NULL, 108 + target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), 109 + target_id TEXT NOT NULL, 110 + reason TEXT, 111 + dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 112 + PRIMARY KEY (user_did, target_type, target_id) 113 + )`, 114 + index: "idx_dismissed_user_type", 115 + }, 116 + { 117 + table: "recommendation_impressions", 118 + create: `CREATE TABLE recommendation_impressions_new ( 119 + user_did TEXT NOT NULL, 120 + target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')), 121 + target_id TEXT NOT NULL, 122 + first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 123 + last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 124 + shown_count INTEGER NOT NULL DEFAULT 1, 125 + acted BOOLEAN NOT NULL DEFAULT 0, 126 + PRIMARY KEY (user_did, target_type, target_id) 127 + )`, 128 + index: "idx_impressions_user_unacted", 129 + }, 130 + } { 131 + var schema string 132 + _ = db.QueryRow("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", m.table).Scan(&schema) 133 + if strings.Contains(schema, "'person'") { 134 + continue 135 + } 136 + 137 + db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s_new", m.table)) 138 + 139 + for _, stmt := range []string{ 140 + m.create, 141 + fmt.Sprintf("INSERT INTO %s_new SELECT * FROM %s", m.table, m.table), 142 + fmt.Sprintf("DROP TABLE %s", m.table), 143 + fmt.Sprintf("ALTER TABLE %s_new RENAME TO %s", m.table, m.table), 144 + fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s(user_did, target_type)", m.index, m.table), 145 + } { 146 + if _, err := db.Exec(stmt); err != nil { 147 + return fmt.Errorf("migrating %s: %w", m.table, err) 148 + } 149 + } 150 + } 151 + 152 + if _, err := db.Exec("CREATE INDEX IF NOT EXISTS idx_impressions_last_shown ON recommendation_impressions(last_shown_at)"); err != nil { 153 + return err 154 + } 155 + 156 + return nil 157 + }