Files
llamactl/spec.md

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_PATH
  • LLAMACTL_DATABASE_MAX_OPEN_CONNECTIONS
  • LLAMACTL_DATABASE_MAX_IDLE_CONNECTIONS
  • LLAMACTL_DATABASE_CONN_MAX_LIFETIME

JSON Migration

On first startup with existing JSON files:

  1. Database is created with schema
  2. All JSON files are loaded and migrated to database
  3. Original JSON files are moved to {instances_dir}/json_archive/
  4. 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.Nameinstances.name
  • Instance.Createdinstances.created_at (Unix timestamp)
  • Instance.Statusinstances.status

Backend configuration:

  • BackendOptions.BackendTypeinstances.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
  • nil values use column DEFAULT values
  • Nodes map → instances.nodes (JSON array)
  • Environment map → 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