commonmeta 0.8.26

Library for conversions to/from the Commonmeta scholarly metadata format
Documentation
use clap::{Arg, ArgMatches, Command};
use std::path::Path;

use crate::cmd::resolve_db_path;

const STATS_TABLES: &[&str] = &[
    "works",
    "organizations",
    "people",
    "prefixes",
    "works_ror",
    "works_orcid",
    "works_references",
];

pub fn command() -> Command {
    Command::new("settings")
        .about("Show key/value settings stored in the local SQLite database")
        .long_about(
            "Read and display all rows from the `settings` table of the local \
             commonmeta SQLite database. Settings record installed vocabulary \
             versions and bulk-import dates.\n\n\
             Use --stats to show record counts for the main tables instead. \
             Counts are returned instantly even on 200M-row tables because \
             MAX(rowid) is used rather than COUNT(*).\n\n\
             Examples:\n\n\
             commonmeta settings\n\
             commonmeta settings --stats\n\
             commonmeta settings --file /data/commonmeta.sqlite3\n\
             commonmeta settings --people-db /data/people.sqlite3\n\
             commonmeta settings --stats --file /data/commonmeta.sqlite3",
        )
        .arg(
            Arg::new("file")
                .long("file")
                .help("Path to the works SQLite database (overrides COMMONMETA_DB and platform default)"),
        )
        .arg(
            Arg::new("people-db")
                .long("people-db")
                .help("Path to the people SQLite database (shows ORCID Public Data File version)"),
        )
        .arg(
            Arg::new("stats")
                .long("stats")
                .help("Show record counts for all main tables instead of settings values")
                .action(clap::ArgAction::SetTrue),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<(), String> {
    let db_path_str = resolve_db_path(matches.get_one::<String>("file"));
    let db_path = Path::new(&db_path_str);
    let stats = matches.get_flag("stats");

    if stats {
        print_stats(db_path, &db_path_str)?;
        if let Some(people_str) = matches.get_one::<String>("people-db") {
            let people_path = Path::new(people_str);
            if people_path != db_path {
                println!();
                print_stats(people_path, people_str)?;
            }
        }
        return Ok(());
    }

    print_settings_table(db_path, &db_path_str)?;

    if let Some(people_str) = matches.get_one::<String>("people-db") {
        let people_path = Path::new(people_str);
        if people_path != db_path {
            println!();
            print_settings_table(people_path, people_str)?;
        }
    }

    Ok(())
}

fn print_stats(path: &Path, label: &str) -> Result<(), String> {
    if !path.exists() {
        return Err(format!("database not found at '{}'", label));
    }

    let conn = rusqlite::Connection::open(path)
        .map_err(|e| format!("cannot open '{}': {}", label, e))?;

    println!("{}:", label);
    let name_width = STATS_TABLES.iter().map(|t| t.len()).max().unwrap_or(0);
    for &table in STATS_TABLES {
        let exists: bool = conn
            .query_row(
                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
                rusqlite::params![table],
                |row| row.get::<_, i64>(0),
            )
            .unwrap_or(0)
            > 0;

        if exists {
            // MAX(rowid) is O(log n) via the B-tree index; COUNT(*) scans all pages.
            let count: i64 = conn
                .query_row(
                    &format!("SELECT COALESCE(MAX(rowid), 0) FROM \"{}\"", table),
                    [],
                    |row| row.get(0),
                )
                .unwrap_or(0);
            println!("  {:<width$}  {}", table, count, width = name_width);
        } else {
            println!("  {:<width$}  (table not found)", table, width = name_width);
        }
    }
    Ok(())
}

fn print_settings_table(path: &Path, label: &str) -> Result<(), String> {
    if !path.exists() {
        return Err(format!("database not found at '{}'", label));
    }

    let rows = commonmeta::get_all_sqlite_settings(path).map_err(|e| e.to_string())?;

    println!("{}:", label);
    if rows.is_empty() {
        println!("  (no settings)");
        return Ok(());
    }

    let key_width = rows.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
    for (key, value) in &rows {
        println!("  {:<width$}  {}", key, value, width = key_width);
    }
    Ok(())
}