Skip to main content

miden_client_sqlite_store/db_management/
utils.rs

1use std::string::String;
2use std::sync::LazyLock;
3use std::vec::Vec;
4
5use miden_client::crypto::{Blake3_160, Blake3Digest};
6use miden_client::store::StoreError;
7use rusqlite::types::FromSql;
8use rusqlite::{Connection, OptionalExtension, Result, ToSql, Transaction, params};
9use rusqlite_migration::{M, Migrations, SchemaVersion};
10
11use super::errors::SqliteStoreError;
12use crate::sql_error::SqlResultExt;
13
14// MACROS
15// ================================================================================================
16
17/// Auxiliary macro which substitutes `$src` token by `$dst` expression.
18#[macro_export]
19macro_rules! subst {
20    ($src:tt, $dst:expr_2021) => {
21        $dst
22    };
23}
24
25/// Generates a simple insert SQL statement with parameters for the provided table name and fields.
26/// Supports optional conflict resolution (adding "| REPLACE" or "| IGNORE" at the end will generate
27/// "OR REPLACE" and "OR IGNORE", correspondingly).
28///
29/// # Usage:
30///
31/// ```ignore
32/// insert_sql!(users { id, first_name, last_name, age } | REPLACE);
33/// ```
34///
35/// which generates:
36/// ```sql
37/// INSERT OR REPLACE INTO `users` (`id`, `first_name`, `last_name`, `age`) VALUES (?, ?, ?, ?)
38/// ```
39#[macro_export]
40macro_rules! insert_sql {
41    ($table:ident { $first_field:ident $(, $($field:ident),+)? $(,)? } $(| $on_conflict:expr)?) => {
42        concat!(
43            stringify!(INSERT $(OR $on_conflict)? INTO ),
44            "`",
45            stringify!($table),
46            "` (`",
47            stringify!($first_field),
48            $($(concat!("`, `", stringify!($field))),+ ,)?
49            "`) VALUES (",
50            subst!($first_field, "?"),
51            $($(subst!($field, ", ?")),+ ,)?
52            ")"
53        )
54    };
55}
56
57// MIGRATIONS
58// ================================================================================================
59
60type Hash = Blake3Digest<20>;
61
62const MIGRATION_SCRIPTS: [&str; 1] = [include_str!("../store.sql")];
63static MIGRATION_HASHES: LazyLock<Vec<Hash>> = LazyLock::new(compute_migration_hashes);
64static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(prepare_migrations);
65
66fn up(s: &'static str) -> M<'static> {
67    M::up(s).foreign_key_check()
68}
69
70const DB_MIGRATION_HASH_FIELD: &str = "db-migration-hash";
71
72/// Applies the migrations to the database.
73pub fn apply_migrations(conn: &mut Connection) -> Result<(), SqliteStoreError> {
74    let version_before = MIGRATIONS.current_version(conn)?;
75
76    if let SchemaVersion::Inside(ver) = version_before {
77        if !table_exists(&conn.transaction()?, "migrations")? {
78            return Err(SqliteStoreError::MissingMigrationsTable);
79        }
80
81        let expected_hash = &*MIGRATION_HASHES[ver.get() - 1];
82
83        let Ok(Some(actual_hash)) = get_migrations_value::<Vec<u8>>(conn, DB_MIGRATION_HASH_FIELD)
84        else {
85            return Err(SqliteStoreError::DatabaseError("Migration hash not found".to_owned()));
86        };
87
88        if &actual_hash[..] != expected_hash {
89            return Err(SqliteStoreError::MigrationHashMismatch);
90        }
91    }
92
93    MIGRATIONS.to_latest(conn)?;
94
95    let version_after = MIGRATIONS.current_version(conn)?;
96
97    if version_before != version_after {
98        let new_hash = &*MIGRATION_HASHES[MIGRATION_HASHES.len() - 1];
99        set_migrations_value(conn, DB_MIGRATION_HASH_FIELD, &new_hash)?;
100    }
101
102    Ok(())
103}
104
105fn prepare_migrations() -> Migrations<'static> {
106    Migrations::new(MIGRATION_SCRIPTS.map(up).to_vec())
107}
108
109fn compute_migration_hashes() -> Vec<Hash> {
110    let mut accumulator = Hash::default();
111    MIGRATION_SCRIPTS
112        .iter()
113        .map(|sql| {
114            let script_hash = Blake3_160::hash(preprocess_sql(sql).as_bytes());
115            accumulator = Blake3_160::merge(&[accumulator, script_hash]);
116            accumulator
117        })
118        .collect()
119}
120
121fn preprocess_sql(sql: &str) -> String {
122    // TODO: We can also remove all comments here (need to analyze the SQL script in order to remove
123    //       comments in string literals).
124    remove_spaces(sql)
125}
126
127fn remove_spaces(str: &str) -> String {
128    str.chars().filter(|chr| !chr.is_whitespace()).collect()
129}
130
131pub fn get_migrations_value<T: FromSql>(conn: &mut Connection, name: &str) -> Result<Option<T>> {
132    conn.transaction()?
133        .query_row("SELECT value FROM migrations WHERE name = $1", params![name], |row| row.get(0))
134        .optional()
135}
136
137pub fn set_migrations_value<T: ToSql>(conn: &Connection, name: &str, value: &T) -> Result<()> {
138    let count =
139        conn.execute(insert_sql!(migrations { name, value } | REPLACE), params![name, value])?;
140
141    debug_assert_eq!(count, 1);
142
143    Ok(())
144}
145
146pub fn get_setting<T: FromSql>(conn: &mut Connection, name: &str) -> Result<Option<T>, StoreError> {
147    conn.transaction()
148        .into_store_error()?
149        .query_row("SELECT value FROM settings WHERE name = $1", params![name], |row| row.get(0))
150        .optional()
151        .into_store_error()
152}
153
154pub fn set_setting<T: ToSql>(conn: &Connection, name: &str, value: &T) -> Result<()> {
155    let count =
156        conn.execute(insert_sql!(settings { name, value } | REPLACE), params![name, value])?;
157
158    debug_assert_eq!(count, 1);
159
160    Ok(())
161}
162
163pub fn remove_setting(conn: &Connection, name: &str) -> Result<(), StoreError> {
164    let count = conn
165        .execute("DELETE FROM settings WHERE name = $1", params![name])
166        .into_store_error()?;
167
168    debug_assert_eq!(count, 1);
169
170    Ok(())
171}
172
173pub fn list_setting_keys(conn: &Connection) -> Result<Vec<String>, StoreError> {
174    let mut stmt = conn.prepare("SELECT name FROM settings").into_store_error()?;
175    stmt.query_map([], |row| row.get::<_, String>(0))
176        .into_store_error()?
177        .collect::<Result<Vec<String>, _>>()
178        .into_store_error()
179}
180
181/// Checks if a table exists in the database.
182pub fn table_exists(transaction: &Transaction, table_name: &str) -> rusqlite::Result<bool> {
183    Ok(transaction
184        .query_row(
185            "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = $1",
186            params![table_name],
187            |_| Ok(()),
188        )
189        .optional()?
190        .is_some())
191}