rototo 0.1.0-alpha.6

Control plane for runtime configuration of your application.
Documentation
use rusqlite::{Connection, Transaction, TransactionBehavior};

use crate::error::{Result, RototoError};

use super::util::db_err;

const CURRENT_SCHEMA_VERSION: i32 = 8;

pub(super) fn initialize_schema(conn: &Connection) -> Result<()> {
    configure_connection(conn)?;
    migrate_schema(conn)
}

fn configure_connection(conn: &Connection) -> Result<()> {
    conn.execute_batch(
        r#"
        PRAGMA journal_mode = WAL;
        PRAGMA foreign_keys = ON;
        "#,
    )
    .map_err(|err| RototoError::new(format!("failed to configure console database: {err}")))
}

fn migrate_schema(conn: &Connection) -> Result<()> {
    let version = schema_version(conn)?;
    if version > CURRENT_SCHEMA_VERSION {
        return Err(RototoError::new(format!(
            "console database schema version {version} is newer than this rototo binary supports ({CURRENT_SCHEMA_VERSION})"
        )));
    }
    if version == CURRENT_SCHEMA_VERSION {
        return Ok(());
    }
    if version != 0 {
        return Err(RototoError::new(format!(
            "console database schema version {version} is not supported; remove the console data directory and start from a fresh source tree"
        )));
    }

    let tx = Transaction::new_unchecked(conn, TransactionBehavior::Immediate).map_err(db_err)?;
    let version = schema_version(&tx)?;
    if version > CURRENT_SCHEMA_VERSION {
        return Err(RototoError::new(format!(
            "console database schema version {version} is newer than this rototo binary supports ({CURRENT_SCHEMA_VERSION})"
        )));
    }
    if version != 0 {
        return Err(RototoError::new(format!(
            "console database schema version {version} is not supported; remove the console data directory and start from a fresh source tree"
        )));
    }
    create_schema_v8(&tx)?;
    set_schema_version(&tx, CURRENT_SCHEMA_VERSION)?;

    tx.commit().map_err(db_err)?;
    Ok(())
}

fn create_schema_v8(conn: &Connection) -> Result<()> {
    create_auth_tables(conn)?;
    conn.execute_batch(
        r#"
        CREATE TABLE IF NOT EXISTS source_trees (
          id TEXT PRIMARY KEY,
          principal_id TEXT NOT NULL,
          kind TEXT NOT NULL,
          source TEXT NOT NULL,
          display_name TEXT NOT NULL,
          default_revision TEXT NOT NULL,
          created_at TEXT NOT NULL,
          updated_at TEXT NOT NULL,
          last_discovered_at TEXT,
          UNIQUE(principal_id, source)
        );

        CREATE TABLE IF NOT EXISTS source_tree_packages (
          id TEXT PRIMARY KEY,
          source_tree_id TEXT NOT NULL,
          path TEXT NOT NULL,
          revision TEXT NOT NULL,
          source_tree_label TEXT NOT NULL,
          source TEXT NOT NULL,
          discovered_at TEXT NOT NULL,
          active INTEGER NOT NULL DEFAULT 1,
          UNIQUE(source_tree_id, path, revision),
          FOREIGN KEY(source_tree_id) REFERENCES source_trees(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS active_branches (
          id TEXT PRIMARY KEY,
          source_tree_id TEXT NOT NULL,
          principal_id TEXT NOT NULL,
          branch TEXT NOT NULL,
          base_ref TEXT NOT NULL,
          base_commit TEXT,
          pr_url TEXT,
          pr_number INTEGER,
          pr_state TEXT,
          pr_merged_at TEXT,
          pr_synced_at TEXT,
          last_selected_package_path TEXT,
          last_seen_commit TEXT,
          status TEXT NOT NULL,
          created_at TEXT NOT NULL,
          last_opened_at TEXT NOT NULL,
          last_edited_at TEXT,
          archived_at TEXT,
          UNIQUE(source_tree_id, principal_id, branch),
          FOREIGN KEY(source_tree_id) REFERENCES source_trees(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS active_branch_packages (
          branch_id TEXT NOT NULL,
          package_path TEXT NOT NULL,
          added_at TEXT NOT NULL,
          PRIMARY KEY(branch_id, package_path),
          FOREIGN KEY(branch_id) REFERENCES active_branches(id) ON DELETE CASCADE
        );
        "#,
    )
    .map_err(|err| {
        RototoError::new(format!(
            "failed to initialize console database schema: {err}"
        ))
    })?;
    Ok(())
}

fn create_auth_tables(conn: &Connection) -> Result<()> {
    conn.execute_batch(
        r#"
        CREATE TABLE IF NOT EXISTS sessions (
          id TEXT PRIMARY KEY,
          principal_id TEXT NOT NULL,
          github_login TEXT NOT NULL,
          github_name TEXT,
          github_avatar_url TEXT,
          github_token_ciphertext TEXT NOT NULL,
          created_at TEXT NOT NULL,
          expires_at TEXT NOT NULL
        );

        CREATE TABLE IF NOT EXISTS oauth_states (
          state TEXT PRIMARY KEY,
          created_at TEXT NOT NULL
        );
        "#,
    )
    .map_err(|err| {
        RototoError::new(format!(
            "failed to initialize console database schema: {err}"
        ))
    })?;
    Ok(())
}

fn schema_version(conn: &Connection) -> Result<i32> {
    conn.query_row("PRAGMA user_version", [], |row| row.get(0))
        .map_err(db_err)
}

fn set_schema_version(conn: &Connection, version: i32) -> Result<()> {
    conn.execute_batch(&format!("PRAGMA user_version = {version}"))
        .map_err(db_err)
}