relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
use rusqlite::{Connection, params, params_from_iter, types::Value};

use crate::{
    domain::{
        GraphVersion, RepositoryCodeRange, SoftwareGlobalRequest, SoftwareTopic, SoftwareTopicInput,
    },
    project::{KNOWLEDGE_MAP_RELATIVE_PATH, LEGACY_KNOWLEDGE_MAP_RELATIVE_PATH},
    storage::StorageError,
};

const TOPIC_PROJECTION_PAGE_SIZE: usize = 512;

pub(in crate::storage::sqlite::software) fn materialize_topics(
    connection: &Connection,
    source_scope: &str,
    graph_version: GraphVersion,
) -> Result<usize, StorageError> {
    let mut offset = 0;
    loop {
        let topics = markdown_heading_topic_page(
            connection,
            source_scope,
            graph_version,
            TOPIC_PROJECTION_PAGE_SIZE,
            offset,
        )?;
        if topics.is_empty() {
            break;
        }
        for topic in &topics {
            insert_topic(connection, topic)?;
        }
        offset += topics.len();
    }

    let mut offset = 0;
    loop {
        let topics = knowledge_map_topic_page(
            connection,
            source_scope,
            graph_version,
            TOPIC_PROJECTION_PAGE_SIZE,
            offset,
        )?;
        if topics.is_empty() {
            break;
        }
        for topic in &topics {
            insert_topic(connection, topic)?;
        }
        offset += topics.len();
    }

    count_topics(connection, source_scope)
}

fn insert_topic(connection: &Connection, topic: &SoftwareTopic) -> Result<(), StorageError> {
    connection.execute(
        "
        INSERT OR REPLACE INTO software_topics (
            topic_id, repository_id, source_scope, name, topic_kind, source_path,
            line_start, line_end, created_graph_version
        )
        VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
        ",
        params![
            topic.topic_id,
            topic.repository_id,
            topic.source_scope,
            topic.name,
            topic.topic_kind,
            topic.source_path,
            topic.line_range.start,
            topic.line_range.end,
            topic.created_graph_version.get(),
        ],
    )?;

    Ok(())
}

pub(in crate::storage::sqlite::software) fn topics_for_scope(
    connection: &Connection,
    source_scope: &str,
    request: &SoftwareGlobalRequest,
    limit: usize,
) -> Result<Vec<SoftwareTopic>, StorageError> {
    let path_filter = super::super::path_filter_sql_for_column(
        "topics.source_path",
        &request.repository.path_filters,
    );
    let language_filter = super::super::language_filter_sql_for_column(
        "files.language_id",
        &request.repository.language_filters,
    );
    let query = format!(
        "
        SELECT topics.topic_id, topics.repository_id, topics.source_scope, topics.name,
               topics.topic_kind, topics.source_path, topics.line_start, topics.line_end,
               topics.created_graph_version
        FROM software_topics topics
        JOIN software_files files
          ON files.source_scope = topics.source_scope
         AND files.path = topics.source_path
        WHERE topics.source_scope = ?1
        {path_filter}
        {language_filter}
        ORDER BY
            CASE
                WHEN topics.topic_kind = 'document_heading'
                 AND instr(topics.source_path, '/') > 0 THEN 0
                WHEN topics.topic_kind = 'knowledge_map_topic' THEN 1
                WHEN topics.topic_kind = 'document_heading' THEN 2
                ELSE 3
            END ASC,
            topics.source_path ASC,
            topics.line_start ASC
        LIMIT ?
        ",
    );
    let mut values = vec![Value::Text(source_scope.to_owned())];
    super::super::push_path_filter_values(&mut values, &request.repository.path_filters);
    super::super::push_language_filter_values(&mut values, &request.repository.language_filters);
    values.push(Value::Integer(limit as i64));
    let mut statement = connection.prepare(&query)?;
    let rows = statement.query_map(params_from_iter(values), topic_from_row)?;

    rows.collect::<Result<Vec<_>, _>>()
        .map_err(StorageError::from)
}

fn count_topics(connection: &Connection, source_scope: &str) -> Result<usize, StorageError> {
    let count = connection.query_row(
        "
        SELECT COUNT(*)
        FROM software_topics
        WHERE source_scope = ?1
        ",
        params![source_scope],
        |row| row.get::<_, usize>(0),
    )?;

    Ok(count)
}

fn markdown_heading_topic_page(
    connection: &Connection,
    source_scope: &str,
    graph_version: GraphVersion,
    limit: usize,
    offset: usize,
) -> Result<Vec<SoftwareTopic>, StorageError> {
    let mut statement = connection.prepare(
        "
        SELECT repository_id, source_scope, name, path, line_start, line_end
        FROM code_repository_symbols
        WHERE source_scope = ?1
          AND language_id = 'markdown'
          AND kind = 'heading'
        ORDER BY path ASC, line_start ASC
        LIMIT ?2 OFFSET ?3
        ",
    )?;
    let rows = statement.query_map(params![source_scope, limit as i64, offset as i64], |row| {
        Ok(SoftwareTopicInput {
            repository_id: row.get(0)?,
            source_scope: row.get(1)?,
            name: row.get(2)?,
            topic_kind: "document_heading".to_owned(),
            source_path: row.get(3)?,
            line_range: RepositoryCodeRange {
                start: row.get(4)?,
                end: row.get(5)?,
            },
            created_graph_version: graph_version,
        })
    })?;

    rows.map(|row| {
        row.map_err(StorageError::from).and_then(|input| {
            SoftwareTopic::new(input).map_err(|error| StorageError::InvalidInput(error.to_string()))
        })
    })
    .collect()
}

fn knowledge_map_topic_page(
    connection: &Connection,
    source_scope: &str,
    graph_version: GraphVersion,
    limit: usize,
    offset: usize,
) -> Result<Vec<SoftwareTopic>, StorageError> {
    let mut statement = connection.prepare(
        "
        SELECT repository_id, source_scope, path, name, line_start, line_end
        FROM (
            SELECT legacy.repository_id, legacy.source_scope, legacy.path, legacy.name,
                   legacy.line_start, legacy.line_end
            FROM code_repository_symbols legacy
            WHERE legacy.source_scope = ?1
              AND legacy.path IN (?2, ?3)
              AND legacy.kind = 'knowledge_map_topic'
            UNION ALL
            SELECT shards.repository_id, shards.source_scope, shards.path, shards.name,
                   shards.line_start, shards.line_end
            FROM code_repository_symbols refs
            JOIN code_repository_symbols topics
              ON topics.repository_id = refs.repository_id
             AND topics.source_scope = refs.source_scope
             AND topics.path = refs.path
             AND topics.line_start = refs.line_start
             AND topics.kind = 'knowledge_map_topic_shard_topic'
            JOIN code_repository_symbols root_identity
              ON root_identity.repository_id = refs.repository_id
             AND root_identity.source_scope = refs.source_scope
             AND root_identity.path = refs.path
             AND root_identity.line_start = refs.line_start
             AND root_identity.kind = 'knowledge_map_topic_shard_identity'
            JOIN code_repository_symbols shards
              ON shards.repository_id = refs.repository_id
             AND shards.source_scope = refs.source_scope
             AND shards.path = refs.name
             AND shards.name = topics.name
             AND shards.kind = 'knowledge_map_topic_shard'
            JOIN code_repository_symbols shard_identity
              ON shard_identity.repository_id = shards.repository_id
             AND shard_identity.source_scope = shards.source_scope
             AND shard_identity.path = shards.path
             AND shard_identity.line_start = shards.line_start
             AND shard_identity.kind = 'knowledge_map_topic_shard_identity'
             AND shard_identity.name = root_identity.name
            WHERE refs.source_scope = ?1
              AND refs.path IN (?2, ?3)
              AND refs.kind = 'knowledge_map_topic_shard_ref'
        ) current_topics
        ORDER BY path ASC, line_start ASC
        LIMIT ?4 OFFSET ?5
        ",
    )?;
    let rows = statement.query_map(
        params![
            source_scope,
            KNOWLEDGE_MAP_RELATIVE_PATH,
            LEGACY_KNOWLEDGE_MAP_RELATIVE_PATH,
            limit as i64,
            offset as i64
        ],
        |row| {
            Ok(SoftwareTopicInput {
                repository_id: row.get(0)?,
                source_scope: row.get(1)?,
                source_path: row.get(2)?,
                name: row.get(3)?,
                topic_kind: "knowledge_map_topic".to_owned(),
                line_range: RepositoryCodeRange {
                    start: row.get(4)?,
                    end: row.get(5)?,
                },
                created_graph_version: graph_version,
            })
        },
    )?;
    let mut topics = Vec::new();
    for row in rows {
        let input = row?;
        topics.push(
            SoftwareTopic::new(input)
                .map_err(|error| StorageError::InvalidInput(error.to_string()))?,
        );
    }

    Ok(topics)
}

fn topic_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SoftwareTopic> {
    Ok(SoftwareTopic {
        topic_id: row.get(0)?,
        repository_id: row.get(1)?,
        source_scope: row.get(2)?,
        name: row.get(3)?,
        topic_kind: row.get(4)?,
        source_path: row.get(5)?,
        line_range: RepositoryCodeRange {
            start: row.get(6)?,
            end: row.get(7)?,
        },
        created_graph_version: GraphVersion::new(row.get::<_, u64>(8)?),
    })
}

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