Skip to main content

miden_node_db/sqlite/
codec.rs

1//! Column codec for the rusqlite-based SQLite framework.
2//!
3//! [`ToSqlValue`] and [`FromSqlValue`] are the per-column write/read codec for our domain types.
4//! They operate on [`DbValue`]/[`DbValueRef`], thin wrappers over rusqlite's value types, so that
5//! crates implementing a codec for their own types never have to name `rusqlite` directly.
6//!
7//! Most node types are stored as a BLOB via their `Serializable`/`Deserializable` impls; the
8//! [`impl_blob_codec!`](crate::impl_blob_codec) macro generates both traits for such a type. Scalar
9//! types map onto an SQLite `INTEGER`/`TEXT` and implement the traits directly (see the impls ported
10//! from the legacy `SqlTypeConvert` below).
11//!
12//! Integer primitives read back range-checked rather than cast, so a column holding a value outside
13//! the target type's range errors instead of silently truncating. The one place a lossy conversion
14//! is deliberate is `Felt`.
15
16use std::rc::Rc;
17
18use miden_protocol::Felt;
19use miden_protocol::account::StorageSlotName;
20use miden_protocol::block::BlockNumber;
21use miden_protocol::note::NoteTag;
22use rusqlite::ToSql;
23use rusqlite::types::{ToSqlOutput, Value, ValueRef};
24
25use crate::DatabaseError;
26
27// DB VALUE WRAPPERS
28// =================================================================================================
29
30/// An owned SQL value produced when binding a Rust value as a query parameter.
31///
32/// Wraps `rusqlite`'s value types so codec implementors never name `rusqlite`. A value is either a
33/// single column value or a list bound for a `rarray(?)` table-valued parameter (used by the
34/// cacheable IN-list helpers in [`in_list`](crate::sqlite::in_list)).
35#[derive(Debug, Clone)]
36pub enum DbValue {
37    /// A single SQL column value.
38    Single(Value),
39    /// A list of values bound via rusqlite's `array` extension for use with `rarray(?)`.
40    Array(Rc<Vec<Value>>),
41}
42
43impl DbValue {
44    /// Builds an `INTEGER` value.
45    pub fn integer(value: i64) -> Self {
46        Self::Single(Value::Integer(value))
47    }
48
49    /// Builds a `REAL` value.
50    pub fn real(value: f64) -> Self {
51        Self::Single(Value::Real(value))
52    }
53
54    /// Builds a `TEXT` value.
55    pub fn text(value: String) -> Self {
56        Self::Single(Value::Text(value))
57    }
58
59    /// Builds a `BLOB` value.
60    pub fn blob(value: Vec<u8>) -> Self {
61        Self::Single(Value::Blob(value))
62    }
63
64    /// Builds a `NULL` value.
65    pub fn null() -> Self {
66        Self::Single(Value::Null)
67    }
68
69    /// Builds a list value bound for a `rarray(?)` table-valued parameter.
70    pub(crate) fn array(values: Vec<Value>) -> Self {
71        Self::Array(Rc::new(values))
72    }
73}
74
75impl ToSql for DbValue {
76    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
77        match self {
78            Self::Single(value) => value.to_sql(),
79            Self::Array(values) => values.to_sql(),
80        }
81    }
82}
83
84/// A borrowed SQL value handed to [`FromSqlValue`] when reading a column.
85///
86/// Wraps `rusqlite::types::ValueRef` so codec implementors never name `rusqlite`.
87#[derive(Debug, Clone, Copy)]
88pub struct DbValueRef<'a>(ValueRef<'a>);
89
90impl<'a> DbValueRef<'a> {
91    pub(crate) fn new(value: ValueRef<'a>) -> Self {
92        Self(value)
93    }
94
95    /// Reads the value as an `i64`.
96    pub fn as_i64(self) -> Result<i64, DatabaseError> {
97        self.0.as_i64().map_err(|err| DatabaseError::deserialization("i64", err))
98    }
99
100    /// Reads the value as a borrowed BLOB.
101    pub fn as_blob(self) -> Result<&'a [u8], DatabaseError> {
102        self.0.as_blob().map_err(|err| DatabaseError::deserialization("blob", err))
103    }
104
105    /// Reads the value as a borrowed string.
106    pub fn as_str(self) -> Result<&'a str, DatabaseError> {
107        self.0.as_str().map_err(|err| DatabaseError::deserialization("str", err))
108    }
109
110    /// Returns `true` if the value is SQL `NULL`.
111    pub fn is_null(self) -> bool {
112        matches!(self.0, ValueRef::Null)
113    }
114}
115
116// CODEC TRAITS
117// =================================================================================================
118
119/// Converts a Rust value into its SQL parameter representation (the write side of the codec).
120pub trait ToSqlValue {
121    /// Returns the SQL value bound for this Rust value.
122    fn to_sql_value(&self) -> DbValue;
123}
124
125/// Builds a Rust value from a SQL column value (the read side of the codec).
126pub trait FromSqlValue: Sized {
127    /// Reads `Self` from a SQL column value.
128    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError>;
129}
130
131// Forward `ToSqlValue` through references so callers can pass `&value` in a parameter slice.
132impl<T: ToSqlValue + ?Sized> ToSqlValue for &T {
133    fn to_sql_value(&self) -> DbValue {
134        (**self).to_sql_value()
135    }
136}
137
138// PRIMITIVE IMPLS
139// =================================================================================================
140
141impl ToSqlValue for i64 {
142    fn to_sql_value(&self) -> DbValue {
143        DbValue::integer(*self)
144    }
145}
146
147impl FromSqlValue for i64 {
148    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
149        value.as_i64()
150    }
151}
152
153// The unsigned integers widen losslessly on the write side and are range-checked on the read side,
154// so a column holding a value outside the type's range errors instead of silently truncating.
155
156impl ToSqlValue for u8 {
157    fn to_sql_value(&self) -> DbValue {
158        DbValue::integer(i64::from(*self))
159    }
160}
161
162impl FromSqlValue for u8 {
163    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
164        Self::try_from(value.as_i64()?).map_err(|err| DatabaseError::deserialization("u8", err))
165    }
166}
167
168impl ToSqlValue for u16 {
169    fn to_sql_value(&self) -> DbValue {
170        DbValue::integer(i64::from(*self))
171    }
172}
173
174impl FromSqlValue for u16 {
175    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
176        Self::try_from(value.as_i64()?).map_err(|err| DatabaseError::deserialization("u16", err))
177    }
178}
179
180impl ToSqlValue for u32 {
181    fn to_sql_value(&self) -> DbValue {
182        DbValue::integer(i64::from(*self))
183    }
184}
185
186impl FromSqlValue for u32 {
187    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
188        Self::try_from(value.as_i64()?).map_err(|err| DatabaseError::deserialization("u32", err))
189    }
190}
191
192impl ToSqlValue for bool {
193    fn to_sql_value(&self) -> DbValue {
194        DbValue::integer(i64::from(*self))
195    }
196}
197
198impl FromSqlValue for bool {
199    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
200        Ok(value.as_i64()? != 0)
201    }
202}
203
204impl ToSqlValue for Vec<u8> {
205    fn to_sql_value(&self) -> DbValue {
206        DbValue::blob(self.clone())
207    }
208}
209
210impl FromSqlValue for Vec<u8> {
211    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
212        Ok(value.as_blob()?.to_vec())
213    }
214}
215
216impl ToSqlValue for str {
217    fn to_sql_value(&self) -> DbValue {
218        DbValue::text(self.to_owned())
219    }
220}
221
222impl ToSqlValue for String {
223    fn to_sql_value(&self) -> DbValue {
224        DbValue::text(self.clone())
225    }
226}
227
228impl FromSqlValue for String {
229    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
230        Ok(value.as_str()?.to_owned())
231    }
232}
233
234impl<T: ToSqlValue> ToSqlValue for Option<T> {
235    fn to_sql_value(&self) -> DbValue {
236        match self {
237            Some(value) => value.to_sql_value(),
238            None => DbValue::null(),
239        }
240    }
241}
242
243impl<T: FromSqlValue> FromSqlValue for Option<T> {
244    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
245        if value.is_null() {
246            Ok(None)
247        } else {
248            Ok(Some(T::from_sql_value(value)?))
249        }
250    }
251}
252
253// DOMAIN SCALAR IMPLS
254// =================================================================================================
255//
256// Domain types stored in an `INTEGER`/`TEXT` column rather than as a BLOB.
257
258impl ToSqlValue for BlockNumber {
259    fn to_sql_value(&self) -> DbValue {
260        DbValue::integer(i64::from(self.as_u32()))
261    }
262}
263
264impl FromSqlValue for BlockNumber {
265    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
266        u32::from_sql_value(value).map(BlockNumber::from)
267    }
268}
269
270impl ToSqlValue for NoteTag {
271    fn to_sql_value(&self) -> DbValue {
272        DbValue::integer(i64::from(self.as_u32()))
273    }
274}
275
276impl FromSqlValue for NoteTag {
277    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
278        u32::from_sql_value(value).map(NoteTag::new)
279    }
280}
281
282impl ToSqlValue for StorageSlotName {
283    fn to_sql_value(&self) -> DbValue {
284        DbValue::text(self.as_str().to_owned())
285    }
286}
287
288impl FromSqlValue for StorageSlotName {
289    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
290        StorageSlotName::new(value.as_str()?)
291            .map_err(|err| DatabaseError::deserialization("StorageSlotName", err))
292    }
293}
294
295/// A field element is stored as the bit reinterpretation of its canonical `u64`.
296impl ToSqlValue for Felt {
297    #[expect(
298        clippy::cast_possible_wrap,
299        reason = "canonical field elements are stored as the wrapped i64 bit pattern"
300    )]
301    fn to_sql_value(&self) -> DbValue {
302        DbValue::integer(self.as_canonical_u64() as i64)
303    }
304}
305
306impl FromSqlValue for Felt {
307    #[expect(clippy::cast_sign_loss, reason = "reverses the u64 -> i64 wrap applied on write")]
308    fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError> {
309        Felt::new(value.as_i64()? as u64).map_err(|err| DatabaseError::deserialization("Felt", err))
310    }
311}
312
313// BLOB CODEC MACRO
314// =================================================================================================
315
316/// Generates [`ToSqlValue`](crate::sqlite::ToSqlValue) and
317/// [`FromSqlValue`](crate::sqlite::FromSqlValue) for types stored as a BLOB via their
318/// `Serializable`/`Deserializable` impls.
319///
320/// The generated impls call the exact same `to_bytes()`/`read_from_bytes()` used elsewhere, so the
321/// on-disk byte layout is unchanged.
322#[macro_export]
323macro_rules! impl_blob_codec {
324    ($($t:ty),+ $(,)?) => {
325        $(
326            impl $crate::sqlite::ToSqlValue for $t {
327                fn to_sql_value(&self) -> $crate::sqlite::DbValue {
328                    $crate::sqlite::DbValue::blob(
329                        ::miden_protocol::utils::serde::Serializable::to_bytes(self),
330                    )
331                }
332            }
333
334            impl $crate::sqlite::FromSqlValue for $t {
335                fn from_sql_value(
336                    value: $crate::sqlite::DbValueRef<'_>,
337                ) -> ::core::result::Result<Self, $crate::DatabaseError> {
338                    let bytes = value.as_blob()?;
339                    <$t as ::miden_protocol::utils::serde::Deserializable>::read_from_bytes(bytes)
340                        .map_err(|err| {
341                            $crate::DatabaseError::deserialization(::core::stringify!($t), err)
342                        })
343                }
344            }
345        )+
346    };
347}
348
349// Codec for the common protocol types stored as BLOBs. Shared by all node crates so that the orphan
350// rule does not force each consumer to redeclare them.
351impl_blob_codec!(
352    miden_protocol::block::BlockHeader,
353    miden_protocol::block::BlockSignatures,
354    miden_protocol::block::ValidatorKeys,
355    miden_protocol::account::Account,
356    miden_protocol::account::AccountCode,
357    miden_protocol::account::AccountId,
358    miden_protocol::account::AccountStorageHeader,
359    miden_protocol::account::StorageMapKey,
360    miden_protocol::asset::Asset,
361    miden_protocol::transaction::TransactionId,
362    miden_protocol::note::Note,
363    miden_protocol::note::NoteAssets,
364    miden_protocol::note::NoteAttachments,
365    miden_protocol::note::NoteId,
366    miden_protocol::note::NoteScript,
367    miden_protocol::note::NoteStorage,
368    miden_protocol::note::Nullifier,
369    miden_protocol::crypto::merkle::SparseMerklePath,
370    miden_protocol::crypto::merkle::mmr::PartialMmr,
371    miden_protocol::Word,
372);
373
374// TESTS
375// =================================================================================================
376
377#[cfg(test)]
378mod tests {
379    use miden_protocol::Word;
380    use miden_protocol::block::BlockNumber;
381    use rusqlite::types::{Value, ValueRef};
382
383    use super::*;
384    use crate::SqlTypeConvert;
385
386    /// Returns the `i64` a value binds to, failing the test for non-integer values.
387    fn bound_integer(value: &impl ToSqlValue) -> i64 {
388        match value.to_sql_value() {
389            DbValue::Single(Value::Integer(raw)) => raw,
390            other => panic!("expected an INTEGER binding, got {other:?}"),
391        }
392    }
393
394    /// Reads a value back from the `i64` a column holds.
395    fn read_integer<T: FromSqlValue>(raw: i64) -> Result<T, DatabaseError> {
396        T::from_sql_value(DbValueRef::new(ValueRef::Integer(raw)))
397    }
398
399    // ENCODING PARITY WITH `SqlTypeConvert`
400    // ---------------------------------------------------------------------------------------------
401
402    #[test]
403    fn block_number_roundtrip() {
404        for block_num in [
405            BlockNumber::GENESIS,
406            BlockNumber::from(1),
407            BlockNumber::from(u32::MAX - 1),
408            BlockNumber::from(u32::MAX),
409        ] {
410            let raw = bound_integer(&block_num);
411            assert_eq!(raw, block_num.to_raw_sql(), "write side diverged for {block_num}");
412            assert_eq!(
413                read_integer::<BlockNumber>(raw).unwrap(),
414                BlockNumber::from_raw_sql(raw).unwrap(),
415                "read side diverged for {block_num}",
416            );
417        }
418    }
419
420    #[test]
421    fn note_tag_roundtrip() {
422        for tag in [
423            NoteTag::new(0),
424            NoteTag::new(1),
425            NoteTag::new((1 << 31) - 1),
426            NoteTag::new(1 << 31),
427            NoteTag::new(u32::MAX),
428        ] {
429            let raw = bound_integer(&tag);
430            assert_eq!(raw, i64::from(tag.as_u32()), "tags are stored unsigned: {tag:?}");
431            assert_eq!(
432                read_integer::<NoteTag>(raw).unwrap(),
433                tag,
434                "read side diverged for {tag:?}"
435            );
436        }
437    }
438
439    #[test]
440    fn felt_roundtrip() {
441        // `Felt::MAX` is the largest canonical element; it exceeds `i64::MAX` and is therefore
442        // stored as a negative integer.
443        for felt in [Felt::ZERO, Felt::ONE, Felt::from_u32(u32::MAX), Felt::MAX] {
444            let raw = bound_integer(&felt);
445            #[expect(clippy::cast_possible_wrap, reason = "mirrors the legacy nonce encoding")]
446            let legacy = felt.as_canonical_u64() as i64;
447            assert_eq!(raw, legacy, "write side diverged for {felt}");
448            assert_eq!(read_integer::<Felt>(raw).unwrap(), felt, "read side diverged for {felt}");
449        }
450    }
451
452    #[test]
453    fn storage_slot_name_roundtrip() {
454        let name = StorageSlotName::new("some_component::some_slot").unwrap();
455        let DbValue::Single(Value::Text(text)) = name.to_sql_value() else {
456            panic!("storage slot names are stored as TEXT");
457        };
458        assert_eq!(text, String::from(name.clone()));
459        assert_eq!(
460            StorageSlotName::from_sql_value(DbValueRef::new(ValueRef::Text(text.as_bytes())))
461                .unwrap(),
462            name,
463        );
464    }
465
466    // RANGE CHECKING
467    // ---------------------------------------------------------------------------------------------
468
469    #[test]
470    fn unsigned_int_roundtrip() {
471        assert_eq!(read_integer::<u8>(bound_integer(&u8::MAX)).unwrap(), u8::MAX);
472        assert_eq!(read_integer::<u16>(bound_integer(&u16::MAX)).unwrap(), u16::MAX);
473        assert_eq!(read_integer::<u32>(bound_integer(&u32::MAX)).unwrap(), u32::MAX);
474        assert_eq!(read_integer::<u8>(0).unwrap(), 0);
475    }
476
477    #[test]
478    fn out_of_range_ints_error_instead_of_truncating() {
479        // A cast would have yielded 0, 0, and `u32::MAX` respectively.
480        assert_matches::assert_matches!(
481            read_integer::<u8>(256),
482            Err(DatabaseError::ConversionSqlToRust { to: "u8", .. })
483        );
484        assert_matches::assert_matches!(
485            read_integer::<u16>(65_536),
486            Err(DatabaseError::ConversionSqlToRust { to: "u16", .. })
487        );
488        assert_matches::assert_matches!(
489            read_integer::<u32>(-1),
490            Err(DatabaseError::ConversionSqlToRust { to: "u32", .. })
491        );
492    }
493
494    #[test]
495    fn out_of_range_block_number_errors() {
496        // `BlockNumber` is a u32 on the wire; a wider column value is corruption, not a wrap.
497        assert_matches::assert_matches!(
498            read_integer::<BlockNumber>(i64::from(u32::MAX) + 1),
499            Err(DatabaseError::ConversionSqlToRust { to: "u32", .. })
500        );
501        assert_matches::assert_matches!(
502            read_integer::<BlockNumber>(-1),
503            Err(DatabaseError::ConversionSqlToRust { to: "u32", .. })
504        );
505    }
506
507    // BLOB CODEC
508    // ---------------------------------------------------------------------------------------------
509
510    #[test]
511    fn blob_roundtrip() {
512        let word = Word::from([1u32, 2, 3, 4]);
513        let DbValue::Single(Value::Blob(bytes)) = word.to_sql_value() else {
514            panic!("words are stored as BLOBs");
515        };
516        assert_eq!(Word::from_sql_value(DbValueRef::new(ValueRef::Blob(&bytes))).unwrap(), word);
517    }
518
519    #[test]
520    fn blob_deserialization_error_names_the_type() {
521        assert_matches::assert_matches!(
522            Word::from_sql_value(DbValueRef::new(ValueRef::Blob(&[0xff]))),
523            Err(DatabaseError::ConversionSqlToRust { to: "miden_protocol::Word", .. })
524        );
525    }
526}