6.1 KiB
SQLite Database Persistence
This document describes the SQLite database persistence implementation for llamactl.
Overview
Llamactl uses SQLite3 for persisting instance configurations and state. The database provides:
- Reliable instance persistence across restarts
- Automatic migration from legacy JSON files
- Prepared for future multi-user features
Database Schema
instances Table
Stores all instance configurations and state.
CREATE TABLE IF NOT EXISTS instances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
backend_type TEXT NOT NULL CHECK(backend_type IN ('llama_cpp', 'mlx_lm', 'vllm')),
backend_config_json TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('stopped', 'running', 'failed', 'restarting', 'shutting_down')) DEFAULT 'stopped',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
auto_restart INTEGER NOT NULL DEFAULT 0,
max_restarts INTEGER NOT NULL DEFAULT -1,
restart_delay INTEGER NOT NULL DEFAULT 0,
on_demand_start INTEGER NOT NULL DEFAULT 0,
idle_timeout INTEGER NOT NULL DEFAULT 0,
docker_enabled INTEGER NOT NULL DEFAULT 0,
command_override TEXT,
nodes TEXT,
environment TEXT,
owner_user_id TEXT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
CREATE INDEX IF NOT EXISTS idx_instances_status ON instances(status);
CREATE INDEX IF NOT EXISTS idx_instances_backend_type ON instances(backend_type);
Architecture
Database Layer (pkg/database)
The database.DB type implements the Database interface:
// Database interface defines persistence operations
type Database interface {
Save(inst *instance.Instance) error
Delete(name string) error
LoadAll() ([]*instance.Instance, error)
Close() error
}
type DB struct {
*sql.DB
config *Config
}
// Database interface methods
func (db *DB) Save(inst *instance.Instance) error
func (db *DB) Delete(name string) error
func (db *DB) LoadAll() ([]*instance.Instance, error)
func (db *DB) Close() error
// Internal CRUD methods
func (db *DB) Create(ctx context.Context, inst *instance.Instance) error
func (db *DB) Update(ctx context.Context, inst *instance.Instance) error
func (db *DB) GetByName(ctx context.Context, name string) (*instance.Instance, error)
func (db *DB) GetAll(ctx context.Context) ([]*instance.Instance, error)
func (db *DB) DeleteInstance(ctx context.Context, name string) error
Key points:
- No repository pattern - DB directly implements persistence
- Simple, direct architecture with minimal layers
- Helper methods for row conversion are private to database package
Manager Integration
Manager accepts a Database via dependency injection:
func New(globalConfig *config.AppConfig, db database.Database) InstanceManager
Main creates the database, runs migrations, and injects it:
// Initialize database
db, err := database.Open(&database.Config{
Path: cfg.Database.Path,
MaxOpenConnections: cfg.Database.MaxOpenConnections,
MaxIdleConnections: cfg.Database.MaxIdleConnections,
ConnMaxLifetime: cfg.Database.ConnMaxLifetime,
})
// Run database migrations
if err := database.RunMigrations(db); err != nil {
log.Fatalf("Failed to run database migrations: %v", err)
}
// Migrate from JSON files if needed (one-time migration)
if err := migrateFromJSON(&cfg, db); err != nil {
log.Printf("Warning: Failed to migrate from JSON: %v", err)
}
instanceManager := manager.New(&cfg, db)
JSON Migration (cmd/server/migrate_json.go)
One-time migration utility that runs in main:
func migrateFromJSON(cfg *config.AppConfig, db database.Database) error
Note: This migration code is temporary and can be removed in a future version (post-1.0) once most users have migrated from JSON to SQLite.
Handles:
- Automatic one-time JSON→SQLite migration
- Archiving old JSON files after migration
Configuration
database:
path: "llamactl.db" # Relative to data_dir or absolute
max_open_connections: 25
max_idle_connections: 5
connection_max_lifetime: "5m"
Environment variables:
LLAMACTL_DATABASE_PATHLLAMACTL_DATABASE_MAX_OPEN_CONNECTIONSLLAMACTL_DATABASE_MAX_IDLE_CONNECTIONSLLAMACTL_DATABASE_CONN_MAX_LIFETIME
JSON Migration
On first startup with existing JSON files:
- Database is created with schema
- All JSON files are loaded and migrated to database
- Original JSON files are moved to
{instances_dir}/json_archive/ - Subsequent startups use only the database
Error Handling:
- Failed migrations block application startup
- Original JSON files are preserved for rollback
- No fallback to JSON after migration
Data Mapping
Direct mappings:
Instance.Name→instances.nameInstance.Created→instances.created_at(Unix timestamp)Instance.Status→instances.status
Backend configuration:
BackendOptions.BackendType→instances.backend_type- Typed backend options (LlamaServerOptions, etc.) →
instances.backend_config_json(marshaled via MarshalJSON)
Common options:
- Boolean pointers (
*bool) → INTEGER (0/1) - Integer pointers (
*int) → INTEGER nilvalues use column DEFAULT valuesNodesmap →instances.nodes(JSON array)Environmentmap →instances.environment(JSON object)
Migrations
Uses golang-migrate/migrate/v4 with embedded SQL files:
pkg/database/
├── database.go # Database interface and DB type
├── migrations.go # Migration runner
├── instances.go # Instance CRUD operations
└── migrations/
├── 001_initial_schema.up.sql
└── 001_initial_schema.down.sql
cmd/server/
└── migrate_json.go # Temporary JSON→SQLite migration (can be removed post-1.0)
Migration files are embedded at compile time using go:embed.
Testing
Tests use in-memory SQLite databases (:memory:) for speed, except when testing persistence across connections.
appConfig.Database.Path = ":memory:" // Fast in-memory database
For cross-connection persistence tests:
appConfig.Database.Path = tempDir + "/test.db" // File-based