relay-knowledge 1.1.17

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

use rusqlite::{Connection, params};

use crate::{
    domain::{GraphVersion, IndexKind, IndexModality, IndexState},
    storage::{IndexCursor, IndexRefreshQueueRequest, StorageError},
};

pub(super) struct PlannedTask {
    pub(super) kind: IndexKind,
    pub(super) source_scope: String,
    pub(super) modality: IndexModality,
    pub(super) target_graph_version: GraphVersion,
    pub(super) cursor_before: GraphVersion,
}

pub(super) fn planned_tasks(
    connection: &Connection,
    request: &IndexRefreshQueueRequest,
) -> Result<Vec<PlannedTask>, StorageError> {
    let mut planned = Vec::new();
    for kind in &request.kinds {
        let missing_scopes = super::super::missing_cursor_scopes(connection, *kind)?;
        let missing_scope_set = missing_scopes.iter().cloned().collect::<BTreeSet<_>>();
        for scope in missing_scopes {
            super::super::ensure_cursor(
                connection,
                *kind,
                &scope,
                super::super::TEXT_MODALITY,
                IndexState::Stale,
            )?;
            planned.push(PlannedTask {
                kind: *kind,
                source_scope: scope,
                modality: super::super::TEXT_MODALITY,
                target_graph_version: request.target_graph_version,
                cursor_before: GraphVersion::ZERO,
            });
        }
        let cursors = stale_cursors_for_kind(connection, *kind)?;
        let stale_cursors = cursors
            .into_iter()
            .filter(|cursor| !missing_scope_set.contains(&cursor.source_scope))
            .collect::<Vec<_>>();
        if stale_cursors.is_empty() && missing_scope_set.is_empty() {
            if let Some(cursor_before) =
                fallback_cursor_before(connection, *kind, request.target_graph_version)?
            {
                super::super::ensure_cursor(
                    connection,
                    *kind,
                    super::super::DEFAULT_SCOPE,
                    super::super::TEXT_MODALITY,
                    IndexState::Stale,
                )?;
                planned.push(PlannedTask {
                    kind: *kind,
                    source_scope: super::super::DEFAULT_SCOPE.to_owned(),
                    modality: super::super::TEXT_MODALITY,
                    target_graph_version: request.target_graph_version,
                    cursor_before,
                });
            }
        } else {
            planned.extend(stale_cursors.into_iter().map(|cursor| PlannedTask {
                kind: cursor.kind,
                source_scope: cursor.source_scope,
                modality: cursor.modality,
                target_graph_version: request.target_graph_version,
                cursor_before: cursor.indexed_graph_version,
            }));
        }
    }

    Ok(planned)
}

fn stale_cursors_for_kind(
    connection: &Connection,
    kind: IndexKind,
) -> Result<Vec<IndexCursor>, StorageError> {
    let mut statement = connection.prepare(
        "
        SELECT kind, source_scope, modality, index_version,
               indexed_graph_version, state, last_error,
               source_hash, backend_cursor, model_name, model_dimension
        FROM index_cursors
        WHERE kind = ?1
          AND state != 'fresh'
        ORDER BY source_scope ASC, modality ASC
        ",
    )?;
    let rows = statement.query_map(params![kind.as_str()], |row| {
        Ok((
            row.get::<_, String>(0)?,
            row.get::<_, String>(1)?,
            row.get::<_, String>(2)?,
            row.get::<_, u64>(3)?,
            row.get::<_, u64>(4)?,
            row.get::<_, String>(5)?,
            row.get::<_, Option<String>>(6)?,
            row.get::<_, Option<String>>(7)?,
            row.get::<_, Option<String>>(8)?,
            row.get::<_, Option<String>>(9)?,
            row.get::<_, Option<u64>>(10)?,
        ))
    })?;

    rows.collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .map(
            |(
                kind,
                source_scope,
                modality,
                index_version,
                indexed_graph_version,
                state,
                last_error,
                source_hash,
                backend_cursor,
                model_name,
                model_dimension,
            )| {
                Ok(IndexCursor {
                    kind: super::super::parse_index_kind(&kind)?,
                    source_scope,
                    modality: super::super::parse_index_modality(&modality)?,
                    index_version,
                    indexed_graph_version: GraphVersion::new(indexed_graph_version),
                    state: super::super::parse_index_state(&state)?,
                    last_error,
                    source_hash,
                    backend_cursor,
                    model_name,
                    model_dimension: model_dimension
                        .map(super::super::cursor_metadata::checked_model_dimension)
                        .transpose()?,
                })
            },
        )
        .collect()
}

fn fallback_cursor_before(
    connection: &Connection,
    kind: IndexKind,
    graph_version: GraphVersion,
) -> Result<Option<GraphVersion>, StorageError> {
    let Some(status) = super::super::read_index_status(connection, kind)? else {
        return Ok(Some(GraphVersion::ZERO));
    };
    if status.is_stale_for(graph_version) {
        Ok(Some(status.indexed_graph_version))
    } else {
        Ok(None)
    }
}

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