feat: setup goose and database module
This commit is contained in:
55
internal/database/database.go
Normal file
55
internal/database/database.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"mal/internal/db"
|
||||
"os"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(
|
||||
ProvideSQLDB,
|
||||
ProvideQueries,
|
||||
),
|
||||
fx.Invoke(RunMigrations),
|
||||
)
|
||||
|
||||
func ProvideSQLDB() (*sql.DB, error) {
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "mal.db"
|
||||
}
|
||||
dbConn, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
func ProvideQueries(sqlDB *sql.DB) *db.Queries {
|
||||
return db.New(sqlDB)
|
||||
}
|
||||
|
||||
func RunMigrations(sqlDB *sql.DB) error {
|
||||
goose.SetBaseFS(migrationsFS)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return fmt.Errorf("failed to set goose dialect: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Running database migrations...")
|
||||
if err := goose.Up(sqlDB, "migrations"); err != nil {
|
||||
return fmt.Errorf("failed to run migrations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
internal/database/migrations/001_init.sql
Normal file
42
internal/database/migrations/001_init.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
provider_account_id TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(provider, provider_account_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS anime (
|
||||
id INTEGER PRIMARY KEY, -- Jikan ID
|
||||
title TEXT NOT NULL,
|
||||
image_url TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS watch_list_entry (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
anime_id INTEGER NOT NULL REFERENCES anime(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL CHECK(status IN ('completed', 'dropped', 'plan_to_watch')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
current_episode INTEGER DEFAULT 0,
|
||||
last_episode_at DATETIME,
|
||||
current_time_seconds REAL NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, anime_id)
|
||||
);
|
||||
6
internal/database/migrations/002_add_anime_titles.sql
Normal file
6
internal/database/migrations/002_add_anime_titles.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Add English and Japanese title columns to anime table
|
||||
ALTER TABLE anime ADD COLUMN title_english TEXT;
|
||||
ALTER TABLE anime ADD COLUMN title_japanese TEXT;
|
||||
|
||||
-- Rename existing title to title_original for clarity
|
||||
ALTER TABLE anime RENAME COLUMN title TO title_original;
|
||||
2
internal/database/migrations/003_add_anime_airing.sql
Normal file
2
internal/database/migrations/003_add_anime_airing.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add airing status column to anime table
|
||||
ALTER TABLE anime ADD COLUMN airing BOOLEAN DEFAULT 0;
|
||||
10
internal/database/migrations/004_add_notifications.sql
Normal file
10
internal/database/migrations/004_add_notifications.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Note: watch_list_entry columns now in 001_init.sql
|
||||
|
||||
-- Add notification preferences
|
||||
CREATE TABLE IF NOT EXISTS notification_preference (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
notify_new_episodes BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
9
internal/database/migrations/005_add_anime_relations.sql
Normal file
9
internal/database/migrations/005_add_anime_relations.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE anime ADD COLUMN status TEXT DEFAULT '';
|
||||
ALTER TABLE anime ADD COLUMN relations_synced_at DATETIME;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS anime_relation (
|
||||
anime_id INTEGER NOT NULL REFERENCES anime(id) ON DELETE CASCADE,
|
||||
related_anime_id INTEGER NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
PRIMARY KEY (anime_id, related_anime_id)
|
||||
);
|
||||
6
internal/database/migrations/006_add_jikan_cache.sql
Normal file
6
internal/database/migrations/006_add_jikan_cache.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS jikan_cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
11
internal/database/migrations/007_add_query_indexes.sql
Normal file
11
internal/database/migrations/007_add_query_indexes.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_watch_list_entry_user_status_updated_at
|
||||
ON watch_list_entry(user_id, status, updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_anime_relation_anime_id_relation_type
|
||||
ON anime_relation(anime_id, relation_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_anime_relations_synced_at_status
|
||||
ON anime(relations_synced_at, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_jikan_cache_expires_at
|
||||
ON jikan_cache(expires_at);
|
||||
11
internal/database/migrations/009_add_anime_fetch_retry.sql
Normal file
11
internal/database/migrations/009_add_anime_fetch_retry.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS anime_fetch_retry (
|
||||
anime_id INTEGER PRIMARY KEY,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
next_retry_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_anime_fetch_retry_next_retry_at
|
||||
ON anime_fetch_retry(next_retry_at);
|
||||
@@ -0,0 +1 @@
|
||||
-- Note: watch_list_entry columns now in 001_init.sql
|
||||
13
internal/database/migrations/011_add_continue_watching.sql
Normal file
13
internal/database/migrations/011_add_continue_watching.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS continue_watching_entry (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
anime_id INTEGER NOT NULL REFERENCES anime(id) ON DELETE CASCADE,
|
||||
current_episode INTEGER,
|
||||
current_time_seconds REAL NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, anime_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_continue_watching_user_updated
|
||||
ON continue_watching_entry(user_id, updated_at DESC);
|
||||
22
internal/database/migrations/012_remove_recovery_key.sql
Normal file
22
internal/database/migrations/012_remove_recovery_key.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE user_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO user_new (id, username, password_hash, created_at)
|
||||
SELECT id, username, password_hash, created_at
|
||||
FROM user;
|
||||
|
||||
DROP TABLE user;
|
||||
|
||||
ALTER TABLE user_new RENAME TO user;
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
2
internal/database/migrations/013_drop_account.sql
Normal file
2
internal/database/migrations/013_drop_account.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS account;
|
||||
DROP TABLE IF EXISTS notification_preference;
|
||||
26
internal/database/migrations/014_add_watchlist_statuses.sql
Normal file
26
internal/database/migrations/014_add_watchlist_statuses.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Add "watching" and "on_hold" to the valid statuses for watch_list_entry
|
||||
|
||||
PRAGMA foreign_keys=OFF;
|
||||
|
||||
ALTER TABLE watch_list_entry RENAME TO watch_list_entry_old;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS watch_list_entry (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
||||
anime_id INTEGER NOT NULL REFERENCES anime(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL CHECK(status IN ('watching', 'completed', 'dropped', 'plan_to_watch', 'on_hold')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
current_episode INTEGER DEFAULT 0,
|
||||
last_episode_at DATETIME,
|
||||
current_time_seconds REAL NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, anime_id)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO watch_list_entry (id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at, current_time_seconds)
|
||||
SELECT id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at, current_time_seconds
|
||||
FROM watch_list_entry_old;
|
||||
|
||||
DROP TABLE watch_list_entry_old;
|
||||
|
||||
PRAGMA foreign_keys=ON;
|
||||
5
internal/database/migrations/015_add_duration.sql
Normal file
5
internal/database/migrations/015_add_duration.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add duration column to anime table to store episode duration in seconds
|
||||
ALTER TABLE anime ADD COLUMN duration_seconds REAL;
|
||||
|
||||
-- Add duration_seconds column to continue_watching_entry to track episode duration
|
||||
ALTER TABLE continue_watching_entry ADD COLUMN duration_seconds REAL;
|
||||
3
internal/database/migrations/016_add_avatar_url.sql
Normal file
3
internal/database/migrations/016_add_avatar_url.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE user ADD COLUMN avatar_url TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE user SET avatar_url = 'https://api.dicebear.com/9.x/dylan/svg?seed=' || username WHERE avatar_url = '';
|
||||
Reference in New Issue
Block a user