tuible 0.0.2-alpha.1

A keyboard-driven database client for your terminal, built for both humans and AI agents.
pub mod dynamodb;
pub mod error;
pub mod model;
pub mod sqlite;

use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use dynamodb::DynamoSource;
use error::DbError;
use model::{QueryOutcome, SchemaColumn, TablePage, Value};
use sqlite::SqliteSource;

pub enum DataSource {
    Sqlite(SqliteSource),
    Dynamo(DynamoSource),
}

impl DataSource {
    pub async fn prepare_operation(&self, interrupted: Arc<AtomicBool>) -> Result<(), DbError> {
        if let DataSource::Sqlite(source) = self {
            source.prepare_operation(interrupted).await?;
        }
        Ok(())
    }

    pub async fn list_tables(&self) -> Result<Vec<String>, DbError> {
        match self {
            DataSource::Sqlite(source) => source.list_tables().await,
            DataSource::Dynamo(source) => source.list_tables().await,
        }
    }

    pub async fn fetch_rows(
        &self,
        table: &str,
        limit: i64,
        offset: i64,
    ) -> Result<TablePage, DbError> {
        match self {
            DataSource::Sqlite(source) => source.fetch_rows(table, limit, offset).await,
            DataSource::Dynamo(source) => source.fetch_rows(table, limit, offset).await,
        }
    }

    pub async fn table_schema(&self, table: &str) -> Result<Vec<SchemaColumn>, DbError> {
        match self {
            DataSource::Sqlite(source) => source.table_schema(table).await,
            DataSource::Dynamo(source) => source.table_schema(table).await,
        }
    }

    pub async fn schema_catalog(
        &self,
    ) -> Result<Option<BTreeMap<String, Vec<SchemaColumn>>>, DbError> {
        match self {
            DataSource::Sqlite(source) => source.schema_catalog().await.map(Some),
            DataSource::Dynamo(_) => Ok(None),
        }
    }

    pub async fn execute_sql(&self, sql: &str, max_rows: usize) -> Result<QueryOutcome, DbError> {
        match self {
            DataSource::Sqlite(source) => source.execute_sql(sql, max_rows).await,
            DataSource::Dynamo(source) => source.execute_sql(sql, max_rows).await,
        }
    }

    pub async fn execute_table_filter(
        &self,
        table: &str,
        sql: &str,
        max_rows: usize,
    ) -> Result<Option<(TablePage, bool)>, DbError> {
        match self {
            DataSource::Sqlite(source) => source
                .execute_table_filter(table, sql, max_rows)
                .await
                .map(Some),
            DataSource::Dynamo(_) => Ok(None),
        }
    }

    pub fn is_read_only(&self) -> bool {
        match self {
            DataSource::Sqlite(source) => source.is_read_only(),
            DataSource::Dynamo(source) => source.is_read_only(),
        }
    }

    pub async fn update_cell(
        &self,
        table: &str,
        rowid: i64,
        column: &str,
        value: &Value,
    ) -> Result<(), DbError> {
        match self {
            DataSource::Sqlite(source) => source.update_cell(table, rowid, column, value).await,
            DataSource::Dynamo(_) => Err(DbError::Unsupported(
                "inline DynamoDB item editing is not available; use `tuible dynamodb put`"
                    .to_string(),
            )),
        }
    }

    pub fn backend_name(&self) -> &'static str {
        match self {
            DataSource::Sqlite(_) => "SQLite",
            DataSource::Dynamo(_) => "DynamoDB",
        }
    }

    pub fn supports_cell_edit(&self) -> bool {
        matches!(self, DataSource::Sqlite(_))
    }

    pub fn auto_preview(&self) -> bool {
        matches!(self, DataSource::Sqlite(_))
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used, clippy::unwrap_used)]

    use super::*;
    use tempfile::tempdir;

    #[tokio::test]
    async fn delegates_list_tables_to_sqlite_variant() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("demo.sqlite");
        sqlite::ensure_demo_db(&path).await.unwrap();

        let source = DataSource::Sqlite(SqliteSource::connect(&path).await.unwrap());
        let mut tables = source.list_tables().await.unwrap();
        tables.sort();

        assert_eq!(
            tables,
            vec![
                "authors".to_string(),
                "books".to_string(),
                "events".to_string()
            ]
        );
    }
}