stmo-cli 0.11.0

Turn Claude Code into a data analyst on sql.telemetry.mozilla.org
Documentation
#![allow(clippy::missing_errors_doc)]

use anyhow::{Context, Result};
use std::fs;
use std::path::Path;

use crate::api::RedashClient;

fn find_query_files(query_id: u64) -> Result<Option<(String, String)>> {
    let queries_dir = Path::new("queries");

    if !queries_dir.exists() {
        return Ok(None);
    }

    let mut sql_path = None;
    let mut yaml_path = None;

    for entry in fs::read_dir(queries_dir).context("Failed to read queries directory")? {
        let entry = entry.context("Failed to read directory entry")?;
        let path = entry.path();

        if let Some(filename) = path.file_name().and_then(|f| f.to_str())
            && let Some(id_str) = filename.split('-').next()
            && let Ok(id) = id_str.parse::<u64>()
            && id == query_id
        {
            if path.extension().is_some_and(|ext| ext == "sql") {
                sql_path = Some(path.to_string_lossy().to_string());
            } else if path.extension().is_some_and(|ext| ext == "yaml") {
                yaml_path = Some(path.to_string_lossy().to_string());
            }
        }
    }

    match (sql_path, yaml_path) {
        (Some(sql), Some(yaml)) => Ok(Some((sql, yaml))),
        _ => Ok(None),
    }
}

fn delete_query_files(sql_path: &str, yaml_path: &str) -> Result<()> {
    fs::remove_file(sql_path).context(format!("Failed to delete {sql_path}"))?;
    fs::remove_file(yaml_path).context(format!("Failed to delete {yaml_path}"))?;
    Ok(())
}

pub async fn archive(client: &RedashClient, query_ids: Vec<u64>) -> Result<()> {
    let mut errors = Vec::new();
    let mut archived_count = 0;

    println!("Archiving {} queries...\n", query_ids.len());

    for query_id in &query_ids {
        match client.archive_query(*query_id).await {
            Ok(query) => {
                println!("  ✓ Archived query {query_id} - {}", query.name);

                if let Ok(Some((sql_path, yaml_path))) = find_query_files(*query_id) {
                    if let Err(e) = delete_query_files(&sql_path, &yaml_path) {
                        eprintln!("  ⚠ Failed to delete local files for query {query_id}: {e}");
                    } else {
                        println!("    Deleted local files");
                    }
                } else {
                    println!("    No local files found");
                }

                archived_count += 1;
            }
            Err(e) => {
                eprintln!("  ✗ Failed to archive query {query_id}: {e}");
                errors.push((*query_id, e));
            }
        }
    }

    println!("\n✓ Archived {archived_count}/{} queries", query_ids.len());

    if !errors.is_empty() {
        anyhow::bail!("Failed to archive {} queries", errors.len());
    }

    Ok(())
}

pub async fn cleanup(client: &RedashClient) -> Result<()> {
    let queries_dir = Path::new("queries");

    if !queries_dir.exists() {
        println!("No queries directory found");
        return Ok(());
    }

    let mut query_ids = Vec::new();

    for entry in fs::read_dir(queries_dir).context("Failed to read queries directory")? {
        let entry = entry.context("Failed to read directory entry")?;
        let path = entry.path();

        if path.extension().is_some_and(|ext| ext == "yaml")
            && let Some(filename) = path.file_name().and_then(|f| f.to_str())
            && let Some(id_str) = filename.split('-').next()
            && let Ok(id) = id_str.parse::<u64>()
        {
            query_ids.push(id);
        }
    }

    query_ids.sort_unstable();
    query_ids.dedup();

    if query_ids.is_empty() {
        println!("No queries found in queries/ directory");
        return Ok(());
    }

    println!(
        "Checking {} queries for archive status...\n",
        query_ids.len()
    );

    let mut cleaned_count = 0;
    let mut errors = Vec::new();

    for query_id in &query_ids {
        match client.get_query(*query_id).await {
            Ok(query) => {
                if query.is_archived {
                    println!("  Found archived query {query_id} - {}", query.name);

                    if let Ok(Some((sql_path, yaml_path))) = find_query_files(*query_id) {
                        if let Err(e) = delete_query_files(&sql_path, &yaml_path) {
                            eprintln!("    ✗ Failed to delete files: {e}");
                            errors.push((*query_id, e));
                        } else {
                            println!("    ✓ Deleted local files");
                            cleaned_count += 1;
                        }
                    }
                }
            }
            Err(e) => {
                eprintln!("  ⚠ Failed to check query {query_id}: {e}");
            }
        }
    }

    if cleaned_count > 0 {
        println!("\n✓ Cleaned up {cleaned_count} archived queries");
    } else {
        println!("\n✓ No archived queries with local files found");
    }

    if !errors.is_empty() {
        anyhow::bail!("Failed to clean up {} queries", errors.len());
    }

    Ok(())
}

pub async fn unarchive(client: &RedashClient, query_ids: Vec<u64>) -> Result<()> {
    let mut errors = Vec::new();
    let mut unarchived_count = 0;

    println!("Unarchiving {} queries...\n", query_ids.len());

    for query_id in &query_ids {
        match client.unarchive_query(*query_id).await {
            Ok(query) => {
                println!("  ✓ Unarchived query {query_id} - {}", query.name);
                unarchived_count += 1;
            }
            Err(e) => {
                let error_msg = e.to_string();
                if error_msg.contains("403") || error_msg.contains("Permission") {
                    eprintln!("  ✗ Permission denied to unarchive query {query_id}");
                } else {
                    eprintln!("  ✗ Failed to unarchive query {query_id}: {e}");
                }
                errors.push((*query_id, e));
            }
        }
    }

    println!(
        "\n✓ Unarchived {unarchived_count}/{} queries",
        query_ids.len()
    );

    if !errors.is_empty() {
        anyhow::bail!("Failed to unarchive {} queries", errors.len());
    }

    Ok(())
}