miden-node-store 0.17.1

Miden node's state store component
Documentation
//! A blocking handle over the framework's connection pools, for testing query functions.
//!
//! Query functions take a [`ReadTx`]/[`WriteTx`], which the pools only ever hand out inside a
//! `read`/`write` closure on an async call. That is the right shape for production code, but it
//! would force every query-level test to be `async`. [`TestDb`] owns a current-thread runtime and
//! blocks on those calls, so the tests stay plain `#[test]` functions while still exercising the
//! same pools, PRAGMAs, and transaction behaviour the node uses.

use std::path::Path;

use miden_node_db::sqlite::{DbReader, DbWriter, ReadTx, WriteTx};
use miden_node_db::{DatabaseError, default_connection_pool_size};
use tokio::runtime::Runtime;

use crate::db::migrations::bootstrap_database;

/// A database with framework handles over it, driven synchronously.
pub(crate) struct TestDb {
    // Held as `Option` so [`Drop`] can drop the pools inside the runtime's context: their pooled
    // connections are closed on a blocking task, which panics without a runtime to spawn it on.
    writer: Option<DbWriter>,
    reader: Option<DbReader>,
    runtime: Runtime,
}

impl TestDb {
    /// Bootstraps a throwaway database in the OS temp directory and opens handles over it.
    ///
    /// The temporary directory is intentionally leaked so the file outlives the handle; these are
    /// test databases in the OS temp directory.
    pub(crate) fn new() -> Self {
        let temp_dir = tempfile::tempdir().expect("failed to create temp directory");
        let path = temp_dir.path().join("test.sqlite3");
        bootstrap_database(&path).expect("database should bootstrap");
        let _kept_dir = temp_dir.keep();

        Self::open(&path)
    }

    /// Opens handles over an existing, already migrated database file.
    pub(crate) fn open(path: &Path) -> Self {
        let (writer, reader) =
            miden_node_db::sqlite::open_with_pool_size(path, default_connection_pool_size())
                .expect("temp file sqlite should always work");
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("test runtime should build");

        Self {
            writer: Some(writer),
            reader: Some(reader),
            runtime,
        }
    }

    /// Runs `query` inside a read-only transaction.
    pub(crate) fn read<R, E, F>(&self, query: F) -> Result<R, E>
    where
        F: FnOnce(&ReadTx<'_>) -> Result<R, E> + Send + 'static,
        R: Send + 'static,
        E: From<DatabaseError> + Send + 'static,
    {
        let reader = self.reader.as_ref().expect("handles live until drop");
        self.runtime.block_on(reader.read("test read", query))
    }

    /// Runs `query` inside a read-write transaction, committing it if `query` returns `Ok`.
    pub(crate) fn write<R, E, F>(&self, query: F) -> Result<R, E>
    where
        F: FnOnce(&WriteTx<'_>) -> Result<R, E> + Send + 'static,
        R: Send + 'static,
        E: From<DatabaseError> + Send + 'static,
    {
        let writer = self.writer.as_ref().expect("handles live until drop");
        self.runtime.block_on(writer.write("test write", query))
    }
}

impl Drop for TestDb {
    fn drop(&mut self) {
        let _guard = self.runtime.enter();
        self.writer.take();
        self.reader.take();
    }
}