stmo-cli 0.11.0

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

use super::OutputFormat;
use crate::api::RedashClient;
use crate::models::DataSource;
use anyhow::Result;

fn build_status_string(ds: &DataSource) -> String {
    let mut status_parts = Vec::new();
    if ds.paused != 0 {
        status_parts.push("paused");
    }
    if ds.view_only {
        status_parts.push("view-only");
    }
    if let Some(desc) = &ds.description
        && desc.to_lowercase().contains("deprecated")
    {
        status_parts.push("deprecated");
    }

    if status_parts.is_empty() {
        String::new()
    } else {
        format!("[{}]", status_parts.join(", "))
    }
}

pub async fn list_data_sources(client: &RedashClient, format: OutputFormat) -> Result<()> {
    let data_sources = client.list_data_sources().await?;

    match format {
        OutputFormat::Json => {
            let json = serde_json::to_string_pretty(&data_sources)?;
            println!("{json}");
        }
        OutputFormat::Table => {
            println!("Fetching data sources from Redash...\n");
            println!("=== DATA SOURCES ({}) ===\n", data_sources.len());
            println!("{:<6} {:<40} {:<15} Status", "ID", "Name", "Type");
            println!("{}", "-".repeat(80));

            for ds in &data_sources {
                let status = build_status_string(ds);
                println!("{:<6} {:<40} {:<15} {}", ds.id, ds.name, ds.ds_type, status);
            }

            println!("\nUse 'stmo-cli data-sources <id>' to view details.");
            println!("Use 'stmo-cli data-sources <id> --schema' to view table schema.");
        }
    }

    Ok(())
}

pub async fn show_data_source(
    client: &RedashClient,
    data_source_id: u64,
    show_schema: bool,
    refresh_schema: bool,
    format: OutputFormat,
) -> Result<()> {
    let ds = client.get_data_source(data_source_id).await?;

    let schema = if show_schema {
        match client
            .get_data_source_schema(data_source_id, refresh_schema)
            .await
        {
            Ok(s) => Some(s),
            Err(e) => {
                eprintln!("Error fetching schema: {e:#}");
                None
            }
        }
    } else {
        None
    };

    match format {
        OutputFormat::Json => {
            let output = serde_json::json!({
                "data_source": ds,
                "schema": schema,
            });
            let json = serde_json::to_string_pretty(&output)?;
            println!("{json}");
        }
        OutputFormat::Table => {
            println!("=== DATA SOURCE: {} ===\n", ds.name);
            println!("ID:          {}", ds.id);
            println!("Type:        {}", ds.ds_type);

            if let Some(syntax) = &ds.syntax {
                println!("Syntax:      {syntax}");
            }

            if let Some(description) = &ds.description {
                println!("Description: {description}");
            }

            if ds.paused != 0 {
                println!("Status:      PAUSED");
                if let Some(reason) = &ds.pause_reason {
                    println!("Reason:      {reason}");
                }
            } else {
                println!("Status:      Active");
            }

            if ds.view_only {
                println!("Access:      View-only");
            }

            if let Some(queue) = &ds.queue_name {
                println!("Queue:       {queue}");
            }

            if show_schema {
                if let Some(schema) = schema {
                    println!("\n=== SCHEMA ({} tables) ===\n", schema.schema.len());

                    for table in &schema.schema {
                        println!("\nTable: {} ({} columns)", table.name, table.columns.len());
                        println!("  {:<40} Type", "Column");
                        println!("  {}", "-".repeat(60));

                        for column in &table.columns {
                            println!("  {:<40} {}", column.name, column.column_type);
                        }
                    }
                } else {
                    eprintln!("\nNote: Schema could not be fetched. See error above for details.");
                }
            } else {
                println!("\nUse --schema flag to view table schema.");
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_test_datasource(
        id: u64,
        name: &str,
        ds_type: &str,
        paused: u8,
        view_only: bool,
        description: Option<String>,
    ) -> DataSource {
        DataSource {
            id,
            name: name.to_string(),
            ds_type: ds_type.to_string(),
            syntax: Some("sql".to_string()),
            description,
            paused,
            pause_reason: None,
            view_only,
            queue_name: None,
            scheduled_queue_name: None,
            groups: None,
            options: None,
        }
    }

    #[test]
    fn test_build_status_string_no_status() {
        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, false, None);
        assert_eq!(build_status_string(&ds), "");
    }

    #[test]
    fn test_build_status_string_paused() {
        let ds = create_test_datasource(1, "Test DB", "bigquery", 1, false, None);
        assert_eq!(build_status_string(&ds), "[paused]");
    }

    #[test]
    fn test_build_status_string_view_only() {
        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
        assert_eq!(build_status_string(&ds), "[view-only]");
    }

    #[test]
    fn test_build_status_string_deprecated() {
        let ds = create_test_datasource(
            1,
            "Test DB",
            "bigquery",
            0,
            false,
            Some("This is deprecated".to_string()),
        );
        assert_eq!(build_status_string(&ds), "[deprecated]");
    }

    #[test]
    fn test_build_status_string_multiple() {
        let ds = create_test_datasource(
            1,
            "Test DB",
            "bigquery",
            1,
            true,
            Some("This is deprecated".to_string()),
        );
        assert_eq!(build_status_string(&ds), "[paused, view-only, deprecated]");
    }

    #[test]
    fn test_build_status_string_deprecated_case_insensitive() {
        let ds1 = create_test_datasource(
            1,
            "Test DB",
            "bigquery",
            0,
            false,
            Some("DEPRECATED: do not use".to_string()),
        );
        assert_eq!(build_status_string(&ds1), "[deprecated]");

        let ds2 = create_test_datasource(
            2,
            "Test DB",
            "bigquery",
            0,
            false,
            Some("Deprecated API".to_string()),
        );
        assert_eq!(build_status_string(&ds2), "[deprecated]");
    }

    #[test]
    fn test_build_status_string_no_description() {
        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
        assert_eq!(build_status_string(&ds), "[view-only]");
    }

    #[test]
    fn test_build_status_string_description_no_deprecated() {
        let ds = create_test_datasource(
            1,
            "Test DB",
            "bigquery",
            0,
            false,
            Some("Some other description".to_string()),
        );
        assert_eq!(build_status_string(&ds), "");
    }

    #[test]
    fn test_output_format_from_str() {
        assert!(matches!(
            "json".parse::<OutputFormat>().unwrap(),
            OutputFormat::Json
        ));
        assert!(matches!(
            "JSON".parse::<OutputFormat>().unwrap(),
            OutputFormat::Json
        ));
        assert!(matches!(
            "table".parse::<OutputFormat>().unwrap(),
            OutputFormat::Table
        ));
        assert!(matches!(
            "TABLE".parse::<OutputFormat>().unwrap(),
            OutputFormat::Table
        ));
        assert!("invalid".parse::<OutputFormat>().is_err());
        assert!("csv".parse::<OutputFormat>().is_err());
    }
}