diff --git a/.gitignore b/.gitignore index b6bfef9..7227b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Backup files +*.bak + + # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/internal/storage/migrations/001_initial.sql b/internal/storage/migrations/001_initial.sql new file mode 100644 index 0000000..71b4bc9 --- /dev/null +++ b/internal/storage/migrations/001_initial.sql @@ -0,0 +1,36 @@ +-- sessions table stores high-level chat session metadata. +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + model_name TEXT NOT NULL, + summary TEXT +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name); + +-- messages table stores individual conversation messages linked to sessions. +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + token_count INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_messages_session_created_at ON messages(session_id, created_at); + +-- models table tracks available models and usage metadata. +CREATE TABLE IF NOT EXISTS models ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + display_name TEXT, + provider TEXT, + is_default BOOLEAN NOT NULL DEFAULT 0, + last_used_at DATETIME +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_models_name ON models(name); diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 77bea44..77d1d36 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -4,9 +4,11 @@ import ( "database/sql" "errors" "fmt" + "io/fs" "os" "path/filepath" "runtime" + "sort" "strings" "sync" @@ -57,6 +59,11 @@ func (m *Manager) Open() (*sql.DB, error) { return nil, fmt.Errorf("ping sqlite: %w", err) } + if err := applyMigrations(db); err != nil { + _ = db.Close() + return nil, err + } + m.db = db return db, nil } @@ -112,3 +119,49 @@ func ensureDir(dir string) error { return nil } + +func applyMigrations(db *sql.DB) error { + migrationsFS := os.DirFS("internal/storage/migrations") + + entries, err := fs.ReadDir(migrationsFS, ".") + if err != nil { + return fmt.Errorf("read migrations: %w", err) + } + + var files []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + if filepath.Ext(entry.Name()) != ".sql" { + continue + } + files = append(files, entry.Name()) + } + + sort.Strings(files) + + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("begin migration tx: %w", err) + } + + for _, name := range files { + data, readErr := fs.ReadFile(migrationsFS, name) + if readErr != nil { + _ = tx.Rollback() + return fmt.Errorf("read migration %s: %w", name, readErr) + } + + if _, execErr := tx.Exec(string(data)); execErr != nil { + _ = tx.Rollback() + return fmt.Errorf("apply migration %s: %w", name, execErr) + } + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit migrations: %w", err) + } + + return nil +}