relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
//! SQLite statement preparation, retry classification, and read-model outage mapping.

use std::{thread, time::Duration};

use rusqlite::{Connection, Statement};

use crate::{
    domain::{CodeQueryKind, CodeRetrievalRequest},
    storage::StorageError,
};

const CODE_SEARCH_PREPARE_RETRY_DELAYS_MS: [u64; 3] = [4, 12, 36];
const CODE_SEARCH_OPERATION_RETRY_DELAYS_MS: [u64; 4] = [10, 30, 90, 270];

pub(in crate::storage::sqlite::code) fn retry_code_search_operation<T>(
    mut operation: impl FnMut() -> Result<T, StorageError>,
) -> Result<T, StorageError> {
    for delay_ms in CODE_SEARCH_OPERATION_RETRY_DELAYS_MS {
        match operation() {
            Ok(value) => return Ok(value),
            Err(error) if code_search_storage_error_is_retryable(&error) => {
                thread::sleep(Duration::from_millis(delay_ms));
            }
            Err(error) => return Err(error),
        }
    }

    operation()
}

pub(super) fn code_search_error_can_use_empty_results(
    request: &CodeRetrievalRequest,
    error: &StorageError,
) -> bool {
    code_search_plannable_outage_reason(request, error).is_some()
}

pub(super) fn code_search_plannable_outage_reason(
    request: &CodeRetrievalRequest,
    error: &StorageError,
) -> Option<String> {
    if !code_query_can_plan_source_fallback(request) {
        return None;
    }
    code_search_read_model_unavailable_reason(error)
}

pub(super) fn code_search_read_model_unavailable_reason(error: &StorageError) -> Option<String> {
    match error {
        StorageError::Sqlite(error) => {
            let message = error.to_string();
            if code_search_read_model_unavailable_message(&message) {
                Some(format!("code search read model unavailable: {error}"))
            } else if code_search_prepare_error_message_is_retryable(&message) {
                Some(format!(
                    "code search read model temporarily unavailable: {error}"
                ))
            } else {
                None
            }
        }
        _ => None,
    }
}

fn code_query_can_plan_source_fallback(request: &CodeRetrievalRequest) -> bool {
    match request.code_query_kind {
        CodeQueryKind::Definition => code_query_definition_identity(&request.query).is_some(),
        CodeQueryKind::References | CodeQueryKind::Hybrid => {
            code_query_source_identifier(&request.query).is_some()
        }
        CodeQueryKind::Symbol
        | CodeQueryKind::Imports
        | CodeQueryKind::Callers
        | CodeQueryKind::Callees
        | CodeQueryKind::Sbom
        | CodeQueryKind::Impact => false,
    }
}

fn code_query_definition_identity(query: &str) -> Option<&str> {
    let mut identity = None;
    for raw_token in query.split_whitespace().map(str::trim) {
        if raw_token.contains('/') || raw_token.contains('\\') {
            continue;
        }
        let terms = raw_token
            .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
            .filter(|term| !term.is_empty())
            .collect::<Vec<_>>();
        if let Some(term) = terms
            .last()
            .filter(|term| code_query_single_identifier(term))
        {
            identity = Some(*term);
        }
    }

    identity
}

fn code_query_source_identifier(query: &str) -> Option<&str> {
    let identity = code_query_definition_identity(query)?;
    (query.split_whitespace().count() == 1).then_some(identity)
}

fn code_query_single_identifier(value: &str) -> bool {
    let mut characters = value.chars();
    let Some(first) = characters.next() else {
        return false;
    };
    if !(first == '_' || first.is_ascii_alphabetic()) {
        return false;
    }

    characters.all(|character| character == '_' || character.is_ascii_alphanumeric())
}

pub(super) fn prepare_code_search_statement<'connection>(
    connection: &'connection Connection,
    sql: &str,
) -> Result<Statement<'connection>, StorageError> {
    for delay_ms in CODE_SEARCH_PREPARE_RETRY_DELAYS_MS {
        match connection.prepare(sql) {
            Ok(statement) => return Ok(statement),
            Err(error) if code_search_prepare_error_is_retryable(&error) => {
                thread::sleep(Duration::from_millis(delay_ms));
            }
            Err(error) => return Err(StorageError::from(error)),
        }
    }

    connection.prepare(sql).map_err(StorageError::from)
}

fn code_search_prepare_error_is_retryable(error: &rusqlite::Error) -> bool {
    code_search_prepare_error_message_is_retryable(&error.to_string())
}

fn code_search_storage_error_is_retryable(error: &StorageError) -> bool {
    match error {
        StorageError::Sqlite(error) => code_search_prepare_error_is_retryable(error),
        _ => false,
    }
}

fn code_search_prepare_error_message_is_retryable(message: &str) -> bool {
    message.contains("vtable constructor failed: code_repository_search")
        || message.contains("database schema is locked")
        || message.contains("database is locked")
}

fn code_search_read_model_unavailable_message(message: &str) -> bool {
    message.contains("vtable constructor failed: code_repository_search")
        || message.contains("no such table: code_repository_search")
        || message.contains("no such module: fts5")
}

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;