relay-knowledge 1.1.9

Graph-database-based knowledge graph project.
Documentation
use std::collections::BTreeSet;

use rusqlite::{OptionalExtension, params};

use crate::storage::StorageError;

use super::{CodeScopeTable, IMPORT_SCHEMA};

const OPTIONAL_LEGACY_IMPORT_TABLES: &[&str] = &["code_repository_routes"];
const LEGACY_IMPORT_COLUMN_DEFAULTS: &[(&str, &str, &str)] = &[
    ("code_repository_files", "is_generated", "0"),
    ("code_repository_symbols", "symbol_role_json", "NULL"),
];

pub(super) fn copy_attached_code_table(
    transaction: &rusqlite::Transaction<'_>,
    table: &CodeScopeTable,
    source_scope: &str,
) -> Result<(), StorageError> {
    if !attached_code_table_exists(transaction, table.table)? {
        if OPTIONAL_LEGACY_IMPORT_TABLES.contains(&table.table) {
            tracing::debug!(
                table = table.table,
                "skipping code import table that is absent from legacy database"
            );
            return Ok(());
        }
        return Err(StorageError::InvalidInput(format!(
            "import database is missing required code table '{}'",
            table.table
        )));
    }
    let source_columns = attached_code_table_columns(transaction, table.table)?;
    let selected_columns = selected_attached_code_columns(table, &source_columns)?;
    transaction.execute(
        &format!(
            "INSERT INTO {table} ({columns}) SELECT {selected_columns} FROM {schema}.{table} WHERE source_scope = ?1",
            table = table.table,
            columns = table.columns,
            schema = IMPORT_SCHEMA,
        ),
        params![source_scope],
    )?;

    Ok(())
}

fn attached_code_table_exists(
    transaction: &rusqlite::Transaction<'_>,
    table: &str,
) -> Result<bool, StorageError> {
    transaction
        .query_row(
            &format!(
                "SELECT 1 FROM {IMPORT_SCHEMA}.sqlite_master WHERE type = 'table' AND name = ?1"
            ),
            params![table],
            |_| Ok(()),
        )
        .optional()
        .map(|row| row.is_some())
        .map_err(StorageError::from)
}

fn attached_code_table_columns(
    transaction: &rusqlite::Transaction<'_>,
    table: &str,
) -> Result<BTreeSet<String>, StorageError> {
    let mut statement =
        transaction.prepare(&format!("PRAGMA {IMPORT_SCHEMA}.table_info({table})"))?;
    let rows = statement.query_map([], |row| row.get::<_, String>(1))?;
    rows.collect::<Result<BTreeSet<_>, _>>()
        .map_err(StorageError::from)
}

fn selected_attached_code_columns(
    table: &CodeScopeTable,
    source_columns: &BTreeSet<String>,
) -> Result<String, StorageError> {
    let mut selected_columns = Vec::new();
    for column in table.columns.split(',') {
        let column = column.trim();
        if source_columns.contains(column) {
            selected_columns.push(column.to_owned());
        } else if let Some(default) = legacy_import_column_default(table.table, column) {
            selected_columns.push(format!("{default} AS {column}"));
        } else {
            return Err(StorageError::InvalidInput(format!(
                "import database table '{}' is missing required column '{}'",
                table.table, column
            )));
        }
    }
    Ok(selected_columns.join(", "))
}

fn legacy_import_column_default(table: &str, column: &str) -> Option<&'static str> {
    LEGACY_IMPORT_COLUMN_DEFAULTS
        .iter()
        .find(|(legacy_table, legacy_column, _)| *legacy_table == table && *legacy_column == column)
        .map(|(_, _, default)| *default)
}