Yōten: A social tracker for your language learning journey built on the atproto.

feat(db/notifications): simplify notifications scheme

Signed-off-by: brookjeynes <me@brookjeynes.dev>

authored by brookjeynes.dev and committed by Tangled 2e6ca257 056d2db7

Changed files
+67 -35
internal
migrations
+61 -3
internal/db/db.go
··· 178 actor_did text not null, 179 subject_uri text not null, 180 181 - state text not null default 'unread' check(state in ('unread', 'read')), 182 - type text not null check(type in ('follow', 'reaction', 'comment')), 183 184 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 185 ··· 217 is_deleted boolean not null default false, 218 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 219 220 - foreign key (did) references profiles(did) on delete cascade 221 unique (did, rkey) 222 ); 223 ··· 234 if err != nil { 235 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 236 } 237 238 return &DB{ 239 db,
··· 178 actor_did text not null, 179 subject_uri text not null, 180 181 + state integer not null default 0, 182 + type text not null, 183 184 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 185 ··· 217 is_deleted boolean not null default false, 218 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 219 220 + foreign key (did) references profiles(did) on delete cascade, 221 unique (did, rkey) 222 ); 223 ··· 234 if err != nil { 235 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 236 } 237 + 238 + // This migration removes the type constraint on the notification type as 239 + // it was painful to add new types. It also changes state to an integer 240 + // check instead of text. 241 + runMigration(conn, logger, "simplify-notification-constraints", func(tx *sql.Tx) error { 242 + // Create new table with state as integer and no type constraint 243 + _, err := tx.Exec(` 244 + create table if not exists notifications_new ( 245 + id integer primary key autoincrement, 246 + 247 + recipient_did text not null, 248 + actor_did text not null, 249 + subject_uri text not null, 250 + 251 + state integer not null default 0, 252 + type text not null, 253 + 254 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 255 + 256 + foreign key (recipient_did) references profiles(did) on delete cascade, 257 + foreign key (actor_did) references profiles(did) on delete cascade 258 + ); 259 + `) 260 + if err != nil { 261 + return err 262 + } 263 + 264 + // Copy data, converting state from text to integer 265 + _, err = tx.Exec(` 266 + insert into notifications_new (id, recipient_did, actor_did, subject_uri, state, type, created_at) 267 + select 268 + id, 269 + recipient_did, 270 + actor_did, 271 + subject_uri, 272 + case state 273 + when 'unread' then 0 274 + when 'read' then 1 275 + else 0 276 + end, 277 + type, 278 + created_at 279 + from notifications; 280 + `) 281 + if err != nil { 282 + return err 283 + } 284 + 285 + // Drop old table 286 + _, err = tx.Exec(`drop table notifications`) 287 + if err != nil { 288 + return err 289 + } 290 + 291 + // Rename new table 292 + _, err = tx.Exec(`alter table notifications_new rename to notifications`) 293 + return err 294 + }) 295 296 return &DB{ 297 db,
+6 -6
internal/db/notification.go
··· 16 NotificationTypeReply NotificationType = "reply" 17 ) 18 19 - type NotificationState string 20 21 const ( 22 - NotificationStateUnread NotificationState = "unread" 23 - NotificationStateRead NotificationState = "read" 24 ) 25 26 type NotificationWithBskyHandle struct { ··· 124 } 125 126 func GetUnreadNotificationCount(e Execer, recipientDid string) (int, error) { 127 - query := `select count(*) from notifications where recipient_did = ? and state = 'unread';` 128 129 var count int 130 row := e.QueryRow(query, recipientDid) ··· 138 func MarkAllNotificationsAsRead(e Execer, did string) error { 139 query := ` 140 update notifications 141 - set state = 'read' 142 - where recipient_did = ? and state = 'unread'; 143 ` 144 145 _, err := e.Exec(query, did)
··· 16 NotificationTypeReply NotificationType = "reply" 17 ) 18 19 + type NotificationState int 20 21 const ( 22 + NotificationStateUnread NotificationState = 0 23 + NotificationStateRead NotificationState = 1 24 ) 25 26 type NotificationWithBskyHandle struct { ··· 124 } 125 126 func GetUnreadNotificationCount(e Execer, recipientDid string) (int, error) { 127 + query := `select count(*) from notifications where recipient_did = ? and state = 0;` 128 129 var count int 130 row := e.QueryRow(query, recipientDid) ··· 138 func MarkAllNotificationsAsRead(e Execer, did string) error { 139 query := ` 140 update notifications 141 + set state = 1 142 + where recipient_did = ? and state = 0; 143 ` 144 145 _, err := e.Exec(query, did)
-26
migrations/update_notification_type.sql
··· 1 - -- This script should be used and updated whenever a new notification type 2 - -- constraint needs to be added. 3 - 4 - BEGIN TRANSACTION; 5 - 6 - ALTER TABLE notifications RENAME TO notifications_old; 7 - 8 - CREATE TABLE notifications ( 9 - id INTEGER PRIMARY KEY AUTOINCREMENT, 10 - recipient_did TEXT NOT NULL, 11 - actor_did TEXT NOT NULL, 12 - subject_uri TEXT NOT NULL, 13 - state TEXT NOT NULL DEFAULT 'unread' CHECK(state IN ('unread', 'read')), 14 - type TEXT NOT NULL CHECK(type IN ('follow', 'reaction', 'comment', 'reply')), 15 - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 16 - FOREIGN KEY (recipient_did) REFERENCES profiles(did) ON DELETE CASCADE, 17 - FOREIGN KEY (actor_did) REFERENCES profiles(did) ON DELETE CASCADE 18 - ); 19 - 20 - INSERT INTO notifications (id, recipient_did, actor_did, subject_uri, state, type, created_at) 21 - SELECT id, recipient_did, actor_did, subject_uri, state, type, created_at 22 - FROM notifications_old; 23 - 24 - DROP TABLE notifications_old; 25 - 26 - COMMIT;
···