package sqlite import ( "context" "database/sql" "fmt" "os" "path/filepath" "time" _ "github.com/mattn/go-sqlite3" "github.com/vincentc-afk/gitea-notification-hub/internal/storage" ) // Repository implements storage.Repository using SQLite type Repository struct { db *sql.DB } // New creates a new SQLite repository func New(dsn string) (*Repository, error) { // Ensure directory exists dir := filepath.Dir(dsn) if dir != "" && dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("creating data directory: %w", err) } } db, err := sql.Open("sqlite3", dsn+"?_foreign_keys=on&_journal_mode=WAL") if err != nil { return nil, fmt.Errorf("opening database: %w", err) } // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("pinging database: %w", err) } return &Repository{db: db}, nil } // Migrate runs database migrations func (r *Repository) Migrate(ctx context.Context) error { schema := ` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, gitea_username TEXT UNIQUE, gitea_id INTEGER, email TEXT UNIQUE, full_name TEXT, slack_id TEXT, slack_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_users_gitea_username ON users(gitea_username); CREATE INDEX IF NOT EXISTS idx_users_slack_id ON users(slack_id); ` _, err := r.db.ExecContext(ctx, schema) if err != nil { return fmt.Errorf("running migrations: %w", err) } return nil } // GetUserByEmail retrieves a user by email func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*storage.User, error) { query := ` SELECT id, gitea_username, gitea_id, email, full_name, slack_id, slack_name, created_at, updated_at FROM users WHERE email = ? ` var user storage.User err := r.db.QueryRowContext(ctx, query, email).Scan( &user.ID, &user.GiteaUsername, &user.GiteaID, &user.Email, &user.FullName, &user.SlackID, &user.SlackName, &user.CreatedAt, &user.UpdatedAt, ) if err == sql.ErrNoRows { return nil, &storage.ErrNotFound{Message: fmt.Sprintf("user with email %s not found", email)} } if err != nil { return nil, fmt.Errorf("querying user by email: %w", err) } return &user, nil } // GetUserByGiteaUsername retrieves a user by Gitea username func (r *Repository) GetUserByGiteaUsername(ctx context.Context, username string) (*storage.User, error) { query := ` SELECT id, gitea_username, gitea_id, email, full_name, slack_id, slack_name, created_at, updated_at FROM users WHERE gitea_username = ? ` var user storage.User err := r.db.QueryRowContext(ctx, query, username).Scan( &user.ID, &user.GiteaUsername, &user.GiteaID, &user.Email, &user.FullName, &user.SlackID, &user.SlackName, &user.CreatedAt, &user.UpdatedAt, ) if err == sql.ErrNoRows { return nil, &storage.ErrNotFound{Message: fmt.Sprintf("user with username %s not found", username)} } if err != nil { return nil, fmt.Errorf("querying user by username: %w", err) } return &user, nil } // GetUserBySlackID retrieves a user by Slack ID func (r *Repository) GetUserBySlackID(ctx context.Context, slackID string) (*storage.User, error) { query := ` SELECT id, gitea_username, gitea_id, email, full_name, slack_id, slack_name, created_at, updated_at FROM users WHERE slack_id = ? ` var user storage.User err := r.db.QueryRowContext(ctx, query, slackID).Scan( &user.ID, &user.GiteaUsername, &user.GiteaID, &user.Email, &user.FullName, &user.SlackID, &user.SlackName, &user.CreatedAt, &user.UpdatedAt, ) if err == sql.ErrNoRows { return nil, &storage.ErrNotFound{Message: fmt.Sprintf("user with slack_id %s not found", slackID)} } if err != nil { return nil, fmt.Errorf("querying user by slack_id: %w", err) } return &user, nil } // UpsertUser inserts or updates a user func (r *Repository) UpsertUser(ctx context.Context, user *storage.User) error { query := ` INSERT INTO users (gitea_username, gitea_id, email, full_name, slack_id, slack_name, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(email) DO UPDATE SET gitea_username = COALESCE(excluded.gitea_username, users.gitea_username), gitea_id = COALESCE(excluded.gitea_id, users.gitea_id), full_name = COALESCE(excluded.full_name, users.full_name), slack_id = COALESCE(excluded.slack_id, users.slack_id), slack_name = COALESCE(excluded.slack_name, users.slack_name), updated_at = excluded.updated_at ` _, err := r.db.ExecContext(ctx, query, user.GiteaUsername, user.GiteaID, user.Email, user.FullName, user.SlackID, user.SlackName, time.Now(), ) if err != nil { return fmt.Errorf("upserting user: %w", err) } return nil } // ListUsers returns all users func (r *Repository) ListUsers(ctx context.Context) ([]*storage.User, error) { query := ` SELECT id, gitea_username, gitea_id, email, full_name, slack_id, slack_name, created_at, updated_at FROM users ORDER BY created_at DESC ` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("querying users: %w", err) } defer rows.Close() var users []*storage.User for rows.Next() { var user storage.User if err := rows.Scan( &user.ID, &user.GiteaUsername, &user.GiteaID, &user.Email, &user.FullName, &user.SlackID, &user.SlackName, &user.CreatedAt, &user.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scanning user: %w", err) } users = append(users, &user) } return users, nil } // Close closes the database connection func (r *Repository) Close() error { return r.db.Close() }