1use 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#[derive(Debug, Clone)]
36pub enum DbValue {
37 Single(Value),
39 Array(Rc<Vec<Value>>),
41}
42
43impl DbValue {
44 pub fn integer(value: i64) -> Self {
46 Self::Single(Value::Integer(value))
47 }
48
49 pub fn real(value: f64) -> Self {
51 Self::Single(Value::Real(value))
52 }
53
54 pub fn text(value: String) -> Self {
56 Self::Single(Value::Text(value))
57 }
58
59 pub fn blob(value: Vec<u8>) -> Self {
61 Self::Single(Value::Blob(value))
62 }
63
64 pub fn null() -> Self {
66 Self::Single(Value::Null)
67 }
68
69 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#[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 pub fn as_i64(self) -> Result<i64, DatabaseError> {
97 self.0.as_i64().map_err(|err| DatabaseError::deserialization("i64", err))
98 }
99
100 pub fn as_blob(self) -> Result<&'a [u8], DatabaseError> {
102 self.0.as_blob().map_err(|err| DatabaseError::deserialization("blob", err))
103 }
104
105 pub fn as_str(self) -> Result<&'a str, DatabaseError> {
107 self.0.as_str().map_err(|err| DatabaseError::deserialization("str", err))
108 }
109
110 pub fn is_null(self) -> bool {
112 matches!(self.0, ValueRef::Null)
113 }
114}
115
116pub trait ToSqlValue {
121 fn to_sql_value(&self) -> DbValue;
123}
124
125pub trait FromSqlValue: Sized {
127 fn from_sql_value(value: DbValueRef<'_>) -> Result<Self, DatabaseError>;
129}
130
131impl<T: ToSqlValue + ?Sized> ToSqlValue for &T {
133 fn to_sql_value(&self) -> DbValue {
134 (**self).to_sql_value()
135 }
136}
137
138impl 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
153impl 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
253impl 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
295impl 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#[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
349impl_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#[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 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 fn read_integer<T: FromSqlValue>(raw: i64) -> Result<T, DatabaseError> {
396 T::from_sql_value(DbValueRef::new(ValueRef::Integer(raw)))
397 }
398
399 #[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 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 #[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 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 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 #[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}