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

feat(db): add migration logic #34

merged opened by brookjeynes.dev targeting master from push-zqozurktxrpx
Labels

None yet.

Participants 1
AT URI
at://did:plc:4mj54vc4ha3lh32ksxwunnbh/sh.tangled.repo.pull/3m4u6d36dr222
+121 -43
Diff #0
+108 -4
internal/db/db.go
··· 8 8 "strings" 9 9 10 10 _ "github.com/mattn/go-sqlite3" 11 + "yoten.app/internal/server/log" 11 12 ) 12 13 13 14 type DB struct { ··· 26 27 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 27 28 } 28 29 29 - func Make(ctx context.Context, dbPath string, logger *slog.Logger) (*DB, error) { 30 + func Make(ctx context.Context, dbPath string) (*DB, error) { 30 31 opts := []string{ 31 32 "_foreign_keys=1", 32 33 "_journal_mode=WAL", 33 34 "_synchronous=NORMAL", 34 35 "_auto_vacuum=incremental", 35 36 } 37 + 38 + logger := log.FromContext(ctx) 39 + logger = log.SubLogger(logger, "db") 36 40 37 41 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 38 42 if err != nil { ··· 174 178 actor_did text not null, 175 179 subject_uri text not null, 176 180 177 - state text not null default 'unread' check(state in ('unread', 'read')), 178 - type text not null check(type in ('follow', 'reaction', 'comment')), 181 + state integer not null default 0, 182 + type text not null, 179 183 180 184 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 181 185 ··· 213 217 is_deleted boolean not null default false, 214 218 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 215 219 216 - foreign key (did) references profiles(did) on delete cascade 220 + foreign key (did) references profiles(did) on delete cascade, 217 221 unique (did, rkey) 218 222 ); 219 223 ··· 231 235 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 232 236 } 233 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 + 234 296 return &DB{ 235 297 db, 236 298 logger, 237 299 }, nil 238 300 } 301 + 302 + type migrationFn = func(*sql.Tx) error 303 + 304 + func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error { 305 + logger = logger.With("migration", name) 306 + 307 + tx, err := c.BeginTx(context.Background(), nil) 308 + if err != nil { 309 + return err 310 + } 311 + defer tx.Rollback() 312 + 313 + var exists bool 314 + err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists) 315 + if err != nil { 316 + return err 317 + } 318 + 319 + if !exists { 320 + err = migrationFn(tx) 321 + if err != nil { 322 + logger.Error("failed to run migration", "err", err) 323 + return err 324 + } 325 + 326 + _, err = tx.Exec("insert into migrations (name) values (?)", name) 327 + if err != nil { 328 + logger.Error("failed to mark migration as complete", "err", err) 329 + return err 330 + } 331 + 332 + if err := tx.Commit(); err != nil { 333 + return err 334 + } 335 + 336 + logger.Info("migration applied successfully") 337 + } else { 338 + logger.Warn("skipped migration, already applied") 339 + } 340 + 341 + return nil 342 + }
+6 -6
internal/db/notification.go
··· 16 16 NotificationTypeReply NotificationType = "reply" 17 17 ) 18 18 19 - type NotificationState string 19 + type NotificationState int 20 20 21 21 const ( 22 - NotificationStateUnread NotificationState = "unread" 23 - NotificationStateRead NotificationState = "read" 22 + NotificationStateUnread NotificationState = 0 23 + NotificationStateRead NotificationState = 1 24 24 ) 25 25 26 26 type NotificationWithBskyHandle struct { ··· 124 124 } 125 125 126 126 func GetUnreadNotificationCount(e Execer, recipientDid string) (int, error) { 127 - query := `select count(*) from notifications where recipient_did = ? and state = 'unread';` 127 + query := `select count(*) from notifications where recipient_did = ? and state = 0;` 128 128 129 129 var count int 130 130 row := e.QueryRow(query, recipientDid) ··· 138 138 func MarkAllNotificationsAsRead(e Execer, did string) error { 139 139 query := ` 140 140 update notifications 141 - set state = 'read' 142 - where recipient_did = ? and state = 'unread'; 141 + set state = 1 142 + where recipient_did = ? and state = 0; 143 143 ` 144 144 145 145 _, err := e.Exec(query, did)
+1 -1
internal/server/app.go
··· 51 51 func Make(ctx context.Context, config *config.Config) (*Server, error) { 52 52 logger := log.FromContext(ctx) 53 53 54 - d, err := db.Make(ctx, config.Core.DbPath, log.SubLogger(logger, "db")) 54 + d, err := db.Make(ctx, config.Core.DbPath) 55 55 if err != nil { 56 56 return nil, err 57 57 }
+1 -1
internal/server/oauth/consts.go
··· 2 2 3 3 const ( 4 4 ClientName = "Yoten" 5 - ClientURI = "https://yoten.app" 5 + ClientURI = "yoten.app" 6 6 SessionName = "yoten-oauth-session-v2" 7 7 SessionHandle = "handle" 8 8 SessionDid = "did"
+5 -5
internal/server/oauth/handler.go
··· 34 34 clientName := ClientName 35 35 clientUri := ClientURI 36 36 37 - meta := o.ClientApp.Config.ClientMetadata() 38 - meta.JWKSURI = &o.JwksUri 39 - meta.ClientName = &clientName 40 - meta.ClientURI = &clientUri 37 + doc := o.ClientApp.Config.ClientMetadata() 38 + doc.JWKSURI = &o.JwksUri 39 + doc.ClientName = &clientName 40 + doc.ClientURI = &clientUri 41 41 42 42 w.Header().Set("Content-Type", "application/json") 43 - if err := json.NewEncoder(w).Encode(meta); err != nil { 43 + if err := json.NewEncoder(w).Encode(doc); err != nil { 44 44 http.Error(w, err.Error(), http.StatusInternalServerError) 45 45 return 46 46 }
-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;

History

1 round 0 comments
sign up or login to add to the discussion
brookjeynes.dev submitted #0
2 commits
expand
feat(db): add migration logic
feat(db/notifications): simplify notifications scheme
expand 0 comments
pull request successfully merged