From 2162d5a95943d9abaa52c520409bcf46313f36e2 Mon Sep 17 00:00:00 2001 From: brookjeynes Date: Mon, 3 Nov 2025 07:56:46 +1000 Subject: [PATCH] feat(db): add migration logic Change-Id: prvpxykwynyllnwpxusunzmmkopuuwqk Signed-off-by: brookjeynes --- internal/db/db.go | 48 +++++++++++++++++++++++++++++++++++++++++- internal/server/app.go | 2 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/internal/db/db.go b/internal/db/db.go index 90d2512..49840fd 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -8,6 +8,7 @@ import ( "strings" _ "github.com/mattn/go-sqlite3" + "yoten.app/internal/server/log" ) type DB struct { @@ -26,7 +27,7 @@ type Execer interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } -func Make(ctx context.Context, dbPath string, logger *slog.Logger) (*DB, error) { +func Make(ctx context.Context, dbPath string) (*DB, error) { opts := []string{ "_foreign_keys=1", "_journal_mode=WAL", @@ -34,6 +35,9 @@ func Make(ctx context.Context, dbPath string, logger *slog.Logger) (*DB, error) "_auto_vacuum=incremental", } + logger := log.FromContext(ctx) + logger = log.SubLogger(logger, "db") + db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) if err != nil { return nil, fmt.Errorf("failed to open db: %w", err) @@ -236,3 +240,45 @@ func Make(ctx context.Context, dbPath string, logger *slog.Logger) (*DB, error) logger, }, nil } + +type migrationFn = func(*sql.Tx) error + +func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error { + logger = logger.With("migration", name) + + tx, err := c.BeginTx(context.Background(), nil) + if err != nil { + return err + } + defer tx.Rollback() + + var exists bool + err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists) + if err != nil { + return err + } + + if !exists { + err = migrationFn(tx) + if err != nil { + logger.Error("failed to run migration", "err", err) + return err + } + + _, err = tx.Exec("insert into migrations (name) values (?)", name) + if err != nil { + logger.Error("failed to mark migration as complete", "err", err) + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + logger.Info("migration applied successfully") + } else { + logger.Warn("skipped migration, already applied") + } + + return nil +} diff --git a/internal/server/app.go b/internal/server/app.go index 96af718..b51755f 100644 --- a/internal/server/app.go +++ b/internal/server/app.go @@ -51,7 +51,7 @@ func (s *Server) Close() error { func Make(ctx context.Context, config *config.Config) (*Server, error) { logger := log.FromContext(ctx) - d, err := db.Make(ctx, config.Core.DbPath, log.SubLogger(logger, "db")) + d, err := db.Make(ctx, config.Core.DbPath) if err != nil { return nil, err } -- 2.43.0 From 24cc28d8f0ef9729a065edf6c551b4aab4bfd7b2 Mon Sep 17 00:00:00 2001 From: brookjeynes Date: Mon, 3 Nov 2025 07:56:46 +1000 Subject: [PATCH] feat(db/notifications): simplify notifications scheme Change-Id: zqozurktxrpxzylrsovpsqkokmzzkkus Signed-off-by: brookjeynes --- internal/db/db.go | 64 +++++++++++++++++++++++-- internal/db/notification.go | 12 ++--- migrations/update_notification_type.sql | 26 ---------- 3 files changed, 67 insertions(+), 35 deletions(-) delete mode 100644 migrations/update_notification_type.sql diff --git a/internal/db/db.go b/internal/db/db.go index 49840fd..9ae4122 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -178,8 +178,8 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { actor_did text not null, subject_uri text not null, - state text not null default 'unread' check(state in ('unread', 'read')), - type text not null check(type in ('follow', 'reaction', 'comment')), + state integer not null default 0, + type text not null, created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), @@ -217,7 +217,7 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { is_deleted boolean not null default false, created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - foreign key (did) references profiles(did) on delete cascade + foreign key (did) references profiles(did) on delete cascade, unique (did, rkey) ); @@ -235,6 +235,64 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { return nil, fmt.Errorf("failed to execute db create statement: %w", err) } + // This migration removes the type constraint on the notification type as + // it was painful to add new types. It also changes state to an integer + // check instead of text. + runMigration(conn, logger, "simplify-notification-constraints", func(tx *sql.Tx) error { + // Create new table with state as integer and no type constraint + _, err := tx.Exec(` + create table if not exists notifications_new ( + id integer primary key autoincrement, + + recipient_did text not null, + actor_did text not null, + subject_uri text not null, + + state integer not null default 0, + type text not null, + + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + foreign key (recipient_did) references profiles(did) on delete cascade, + foreign key (actor_did) references profiles(did) on delete cascade + ); + `) + if err != nil { + return err + } + + // Copy data, converting state from text to integer + _, err = tx.Exec(` + insert into notifications_new (id, recipient_did, actor_did, subject_uri, state, type, created_at) + select + id, + recipient_did, + actor_did, + subject_uri, + case state + when 'unread' then 0 + when 'read' then 1 + else 0 + end, + type, + created_at + from notifications; + `) + if err != nil { + return err + } + + // Drop old table + _, err = tx.Exec(`drop table notifications`) + if err != nil { + return err + } + + // Rename new table + _, err = tx.Exec(`alter table notifications_new rename to notifications`) + return err + }) + return &DB{ db, logger, diff --git a/internal/db/notification.go b/internal/db/notification.go index 4f2031f..f095f95 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -16,11 +16,11 @@ const ( NotificationTypeReply NotificationType = "reply" ) -type NotificationState string +type NotificationState int const ( - NotificationStateUnread NotificationState = "unread" - NotificationStateRead NotificationState = "read" + NotificationStateUnread NotificationState = 0 + NotificationStateRead NotificationState = 1 ) type NotificationWithBskyHandle struct { @@ -124,7 +124,7 @@ func GetNotificationsByDid(e Execer, did string, limit, offset int) ([]Notificat } func GetUnreadNotificationCount(e Execer, recipientDid string) (int, error) { - query := `select count(*) from notifications where recipient_did = ? and state = 'unread';` + query := `select count(*) from notifications where recipient_did = ? and state = 0;` var count int row := e.QueryRow(query, recipientDid) @@ -138,8 +138,8 @@ func GetUnreadNotificationCount(e Execer, recipientDid string) (int, error) { func MarkAllNotificationsAsRead(e Execer, did string) error { query := ` update notifications - set state = 'read' - where recipient_did = ? and state = 'unread'; + set state = 1 + where recipient_did = ? and state = 0; ` _, err := e.Exec(query, did) diff --git a/migrations/update_notification_type.sql b/migrations/update_notification_type.sql deleted file mode 100644 index 01f1cc2..0000000 --- a/migrations/update_notification_type.sql +++ /dev/null @@ -1,26 +0,0 @@ --- This script should be used and updated whenever a new notification type --- constraint needs to be added. - -BEGIN TRANSACTION; - -ALTER TABLE notifications RENAME TO notifications_old; - -CREATE TABLE notifications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - recipient_did TEXT NOT NULL, - actor_did TEXT NOT NULL, - subject_uri TEXT NOT NULL, - state TEXT NOT NULL DEFAULT 'unread' CHECK(state IN ('unread', 'read')), - type TEXT NOT NULL CHECK(type IN ('follow', 'reaction', 'comment', 'reply')), - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - FOREIGN KEY (recipient_did) REFERENCES profiles(did) ON DELETE CASCADE, - FOREIGN KEY (actor_did) REFERENCES profiles(did) ON DELETE CASCADE -); - -INSERT INTO notifications (id, recipient_did, actor_did, subject_uri, state, type, created_at) -SELECT id, recipient_did, actor_did, subject_uri, state, type, created_at -FROM notifications_old; - -DROP TABLE notifications_old; - -COMMIT; -- 2.43.0