relay-knowledge 1.1.14

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

use crate::{domain::IndexKind, storage::StorageError};

use super::super::schema::columns;

pub(crate) fn initialize_schema(connection: &Connection) -> Result<(), StorageError> {
    connection.execute_batch(
        "
        CREATE TABLE IF NOT EXISTS index_status (
            kind TEXT PRIMARY KEY,
            index_version INTEGER NOT NULL,
            indexed_graph_version INTEGER NOT NULL,
            state TEXT NOT NULL,
            last_error TEXT
        );

        CREATE TABLE IF NOT EXISTS index_cursors (
            kind TEXT NOT NULL,
            source_scope TEXT NOT NULL,
            modality TEXT NOT NULL,
            index_version INTEGER NOT NULL,
            indexed_graph_version INTEGER NOT NULL,
            state TEXT NOT NULL,
            last_error TEXT,
            source_hash TEXT,
            backend_cursor TEXT,
            model_name TEXT,
            model_dimension INTEGER,
            PRIMARY KEY (kind, source_scope, modality)
        );

        CREATE TABLE IF NOT EXISTS index_scope_manifest (
            source_scope TEXT PRIMARY KEY
        );

        CREATE TABLE IF NOT EXISTS index_refresh_tasks (
            task_id TEXT PRIMARY KEY,
            kind TEXT NOT NULL,
            source_scope TEXT NOT NULL,
            modality TEXT NOT NULL,
            target_graph_version INTEGER NOT NULL,
            state TEXT NOT NULL,
            lease_owner TEXT,
            lease_expires_at_ms INTEGER,
            attempt_count INTEGER NOT NULL,
            next_retry_at_ms INTEGER NOT NULL,
            input_fingerprint TEXT NOT NULL,
            cursor_before INTEGER NOT NULL,
            cursor_after INTEGER,
            last_error_kind TEXT,
            last_error_message TEXT,
            created_at_ms INTEGER NOT NULL,
            updated_at_ms INTEGER NOT NULL
        );
        ",
    )?;
    ensure_index_schema_columns(connection)?;

    for kind in IndexKind::ALL {
        connection.execute(
            "INSERT OR IGNORE INTO index_status
             (kind, index_version, indexed_graph_version, state, last_error)
             VALUES (?1, 0, 0, 'fresh', NULL)",
            params![kind.as_str()],
        )?;
    }
    connection.execute(
        "
        INSERT OR IGNORE INTO index_scope_manifest (source_scope)
        SELECT DISTINCT source_scope FROM evidence
        ",
        [],
    )?;
    connection.execute(
        "
        INSERT OR IGNORE INTO index_scope_manifest (source_scope)
        SELECT DISTINCT source_scope FROM index_cursors
        ",
        [],
    )?;

    Ok(())
}

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

fn ensure_index_schema_columns(connection: &Connection) -> Result<(), StorageError> {
    columns::ensure_column(connection, "index_cursors", "source_hash", "TEXT")?;
    columns::ensure_column(connection, "index_cursors", "backend_cursor", "TEXT")?;
    columns::ensure_column(connection, "index_cursors", "model_name", "TEXT")?;
    columns::ensure_column(connection, "index_cursors", "model_dimension", "INTEGER")?;
    columns::ensure_column(connection, "index_refresh_tasks", "lease_owner", "TEXT")?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "lease_expires_at_ms",
        "INTEGER",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "attempt_count",
        "INTEGER NOT NULL DEFAULT 0",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "next_retry_at_ms",
        "INTEGER NOT NULL DEFAULT 0",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "input_fingerprint",
        "TEXT NOT NULL DEFAULT ''",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "cursor_before",
        "INTEGER NOT NULL DEFAULT 0",
    )?;
    columns::ensure_column(connection, "index_refresh_tasks", "cursor_after", "INTEGER")?;
    columns::ensure_column(connection, "index_refresh_tasks", "last_error_kind", "TEXT")?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "last_error_message",
        "TEXT",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "created_at_ms",
        "INTEGER NOT NULL DEFAULT 0",
    )?;
    columns::ensure_column(
        connection,
        "index_refresh_tasks",
        "updated_at_ms",
        "INTEGER NOT NULL DEFAULT 0",
    )?;
    connection.execute(
        "
        UPDATE index_refresh_tasks
        SET created_at_ms = CAST(strftime('%s', 'now') AS INTEGER) * 1000
        WHERE created_at_ms IS NULL OR created_at_ms = 0
        ",
        [],
    )?;
    connection.execute(
        "
        UPDATE index_refresh_tasks
        SET updated_at_ms = created_at_ms
        WHERE updated_at_ms IS NULL OR updated_at_ms = 0
        ",
        [],
    )?;
    connection.execute(
        "
        UPDATE index_refresh_tasks
        SET input_fingerprint = kind || ':' || source_scope || ':' || modality || ':' || target_graph_version
        WHERE input_fingerprint IS NULL OR input_fingerprint = ''
        ",
        [],
    )?;

    Ok(())
}