miden_client_sqlite_store/db_management/
utils.rs1use 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#[macro_export]
19macro_rules! subst {
20 ($src:tt, $dst:expr_2021) => {
21 $dst
22 };
23}
24
25#[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
57type 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
72pub 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 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
181pub 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}