something
This commit is contained in:
@@ -4,18 +4,20 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
_ "github.com/marcboeker/go-duckdb/v2"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// DB wraps the database connection
|
||||
// DB wraps the database connection with mutex protection
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
db *sql.DB
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewDB creates a new database connection
|
||||
func NewDB(dbPath string) (*DB, error) {
|
||||
db, err := sql.Open("duckdb", dbPath)
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
@@ -24,7 +26,12 @@ func NewDB(dbPath string) (*DB, error) {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
database := &DB{DB: db}
|
||||
// Enable foreign keys for SQLite
|
||||
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
|
||||
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
database := &DB{db: db}
|
||||
if err := database.migrate(); err != nil {
|
||||
return nil, fmt.Errorf("failed to migrate database: %w", err)
|
||||
}
|
||||
@@ -32,58 +39,74 @@ func NewDB(dbPath string) (*DB, error) {
|
||||
return database, nil
|
||||
}
|
||||
|
||||
// With executes a function with mutex-protected access to the database
|
||||
// The function receives the underlying *sql.DB connection
|
||||
func (db *DB) With(fn func(*sql.DB) error) error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
return fn(db.db)
|
||||
}
|
||||
|
||||
// WithTx executes a function within a transaction with mutex protection
|
||||
// The function receives a *sql.Tx transaction
|
||||
// If the function returns an error, the transaction is rolled back
|
||||
// If the function returns nil, the transaction is committed
|
||||
func (db *DB) WithTx(fn func(*sql.Tx) error) error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
tx, err := db.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
return fmt.Errorf("transaction error: %w, rollback error: %v", err, rbErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrate runs database migrations
|
||||
func (db *DB) migrate() error {
|
||||
// Create sequences for auto-incrementing primary keys
|
||||
sequences := []string{
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_users_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_jobs_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_runners_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_tasks_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_job_files_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_manager_secrets_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_registration_tokens_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_runner_api_keys_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_task_logs_id START 1`,
|
||||
`CREATE SEQUENCE IF NOT EXISTS seq_task_steps_id START 1`,
|
||||
}
|
||||
|
||||
for _, seq := range sequences {
|
||||
if _, err := db.Exec(seq); err != nil {
|
||||
return fmt.Errorf("failed to create sequence: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite uses INTEGER PRIMARY KEY AUTOINCREMENT instead of sequences
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_users_id'),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
oauth_provider TEXT NOT NULL,
|
||||
oauth_id TEXT NOT NULL,
|
||||
password_hash TEXT,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT false,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(oauth_provider, oauth_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS runner_api_keys (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_runner_api_keys_id'),
|
||||
key_prefix TEXT NOT NULL, -- First part of API key (e.g., "jk_r1")
|
||||
key_hash TEXT NOT NULL, -- SHA256 hash of full API key
|
||||
name TEXT NOT NULL, -- Human-readable name
|
||||
description TEXT, -- Optional description
|
||||
scope TEXT NOT NULL DEFAULT 'user', -- 'manager' or 'user' - manager scope allows all jobs, user scope only allows jobs from key owner
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key_prefix TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
scope TEXT NOT NULL DEFAULT 'user',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT,
|
||||
created_by INTEGER,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
UNIQUE(key_prefix)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_jobs_id'),
|
||||
user_id BIGINT NOT NULL,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
job_type TEXT NOT NULL DEFAULT 'render',
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
@@ -91,9 +114,11 @@ func (db *DB) migrate() error {
|
||||
frame_start INTEGER,
|
||||
frame_end INTEGER,
|
||||
output_format TEXT,
|
||||
allow_parallel_runners BOOLEAN NOT NULL DEFAULT true,
|
||||
allow_parallel_runners INTEGER NOT NULL DEFAULT 1,
|
||||
timeout_seconds INTEGER DEFAULT 86400,
|
||||
blend_metadata TEXT,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
@@ -102,25 +127,25 @@ func (db *DB) migrate() error {
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS runners (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_runners_id'),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
ip_address TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'offline',
|
||||
last_heartbeat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
capabilities TEXT,
|
||||
api_key_id BIGINT, -- Reference to the API key used for this runner
|
||||
api_key_scope TEXT NOT NULL DEFAULT 'user', -- Scope of the API key ('manager' or 'user')
|
||||
api_key_id INTEGER,
|
||||
api_key_scope TEXT NOT NULL DEFAULT 'user',
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
fingerprint TEXT, -- Hardware fingerprint (NULL for fixed API keys)
|
||||
fingerprint TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (api_key_id) REFERENCES runner_api_keys(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_tasks_id'),
|
||||
job_id BIGINT NOT NULL,
|
||||
runner_id BIGINT,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
runner_id INTEGER,
|
||||
frame_start INTEGER NOT NULL,
|
||||
frame_end INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
@@ -133,44 +158,50 @@ func (db *DB) migrate() error {
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
error_message TEXT
|
||||
error_message TEXT,
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(id),
|
||||
FOREIGN KEY (runner_id) REFERENCES runners(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job_files (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_job_files_id'),
|
||||
job_id BIGINT NOT NULL,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
file_type TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
file_size INTEGER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manager_secrets (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_manager_secrets_id'),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
secret TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_logs (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_task_logs_id'),
|
||||
task_id BIGINT NOT NULL,
|
||||
runner_id BIGINT,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
runner_id INTEGER,
|
||||
log_level TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
step_name TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
||||
FOREIGN KEY (runner_id) REFERENCES runners(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_steps (
|
||||
id BIGINT PRIMARY KEY DEFAULT nextval('seq_task_steps_id'),
|
||||
task_id BIGINT NOT NULL,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
step_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT
|
||||
error_message TEXT,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_user_id ON jobs(user_id);
|
||||
@@ -184,6 +215,7 @@ func (db *DB) migrate() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_job_files_job_id ON job_files(job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_runner_api_keys_prefix ON runner_api_keys(key_prefix);
|
||||
CREATE INDEX IF NOT EXISTS idx_runner_api_keys_active ON runner_api_keys(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_runner_api_keys_created_by ON runner_api_keys(created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_runners_api_key_id ON runners(api_key_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_logs_task_id_created_at ON task_logs(task_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_logs_task_id_id ON task_logs(task_id, id DESC);
|
||||
@@ -196,9 +228,28 @@ func (db *DB) migrate() error {
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
||||
`
|
||||
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
if err := db.With(func(conn *sql.DB) error {
|
||||
_, err := conn.Exec(schema)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to create schema: %w", err)
|
||||
}
|
||||
|
||||
@@ -212,19 +263,26 @@ func (db *DB) migrate() error {
|
||||
}
|
||||
|
||||
for _, migration := range migrations {
|
||||
// DuckDB supports IF NOT EXISTS for ALTER TABLE, so we can safely execute
|
||||
if _, err := db.Exec(migration); err != nil {
|
||||
if err := db.With(func(conn *sql.DB) error {
|
||||
_, err := conn.Exec(migration)
|
||||
return err
|
||||
}); err != nil {
|
||||
// Log but don't fail - column might already exist or table might not exist yet
|
||||
// This is fine for migrations that run after schema creation
|
||||
// For the file_size migration, if it fails (e.g., already BIGINT), that's fine
|
||||
log.Printf("Migration warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize registration_enabled setting (default: true) if it doesn't exist
|
||||
var settingCount int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", "registration_enabled").Scan(&settingCount)
|
||||
err := db.With(func(conn *sql.DB) error {
|
||||
return conn.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", "registration_enabled").Scan(&settingCount)
|
||||
})
|
||||
if err == nil && settingCount == 0 {
|
||||
_, err = db.Exec("INSERT INTO settings (key, value) VALUES (?, ?)", "registration_enabled", "true")
|
||||
err = db.With(func(conn *sql.DB) error {
|
||||
_, err := conn.Exec("INSERT INTO settings (key, value) VALUES (?, ?)", "registration_enabled", "true")
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
// Log but don't fail - setting might have been created by another process
|
||||
log.Printf("Note: Could not initialize registration_enabled setting: %v", err)
|
||||
@@ -234,7 +292,16 @@ func (db *DB) migrate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping checks the database connection
|
||||
func (db *DB) Ping() error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
return db.db.Ping()
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *DB) Close() error {
|
||||
return db.DB.Close()
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
return db.db.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user