saya-cli 0.3.2

Database-aware AI agent for the terminal: full-screen TUI, schema discovery, and bounded read-only SQL over PostgreSQL, MySQL, SQLite, DuckDB, and Snowflake.
use super::{
    connection, connection_schema_cache,
    output::{emit, failure},
    state,
};
use crate::{
    config::runtime::RuntimeConfig,
    profile_identity::profile_identity,
    render::{RenderFormat, TerminalEvent},
};
use saya_connectors::DatabaseConnector;
use saya_store::{AuditOperation, AuditStatus, SchemaStore, SqliteStateStore};
use std::time::Instant;

pub(crate) async fn run(
    name: &str,
    refresh: bool,
    runtime: &RuntimeConfig,
    format: RenderFormat,
    can_prompt: bool,
    store: &SqliteStateStore,
) -> Result<i32, Box<dyn std::error::Error>> {
    let profile = match runtime.named_profile(name) {
        Ok(profile) => profile,
        Err(error) => {
            return failure(
                3,
                saya_types::ConnectionError::invalid_configuration(error.to_string()),
                format,
            );
        }
    };
    // The identity is derived once as a `ProfileIdentity` so the reconciliation
    // pass (which keys claims to schema by profile) gets the same value the
    // store keys schema by, without re-parsing the string form.
    let identity = profile_identity(name, profile, &runtime.cache_scope);
    let identity_str = identity.as_str();
    let mut persistence_failed = refresh && store.invalidate_schema(identity_str).await.is_err();
    let started = Instant::now();
    let connector = match connection::build(profile, runtime, can_prompt).await {
        Ok(connector) => connector,
        Err(error) if !refresh => {
            return connection_schema_cache::fallback(
                store,
                identity_str,
                started,
                error,
                format,
                &mut persistence_failed,
            )
            .await;
        }
        Err(error) => {
            persistence_failed |= state::audit_silent(
                store,
                identity_str,
                AuditOperation::SchemaRefresh,
                AuditStatus::Failure,
                started.elapsed(),
                None,
                None,
            )
            .await
            .is_err();
            warn(persistence_failed, format);
            return failure(3, error, format);
        }
    };
    match live_schema(&*connector).await {
        Ok(schema) => {
            persistence_failed |= store.upsert_schema(identity_str, &schema).await.is_err();
            persistence_failed |= state::audit_silent(
                store,
                identity_str,
                AuditOperation::SchemaRefresh,
                AuditStatus::Success,
                started.elapsed(),
                None,
                None,
            )
            .await
            .is_err();
            warn(persistence_failed, format);
            emit(
                TerminalEvent::Schema {
                    schema: schema.clone(),
                },
                format,
            );
            Ok(0)
        }
        Err(error) if !refresh => {
            connection_schema_cache::fallback(
                store,
                identity_str,
                started,
                error,
                format,
                &mut persistence_failed,
            )
            .await
        }
        Err(error) => {
            persistence_failed |= state::audit_silent(
                store,
                identity_str,
                AuditOperation::SchemaRefresh,
                AuditStatus::Failure,
                started.elapsed(),
                None,
                None,
            )
            .await
            .is_err();
            warn(persistence_failed, format);
            failure(3, error, format)
        }
    }
}

async fn live_schema(
    connector: &dyn DatabaseConnector,
) -> Result<saya_types::SchemaTree, saya_types::ConnectionError> {
    connector.connect().await?;
    connector.schema().await
}

fn warn(persistence_failed: bool, format: RenderFormat) {
    if persistence_failed {
        state::diagnostic(format);
    }
}