Skip to main content

miden_node_db/sqlite/
in_list.rs

1//! Variable-length `IN (...)` lists that keep the SQL text constant.
2//!
3//! Binding a list as `IN (?, ?, ...)` produces a different SQL string per list length, so SQLite
4//! cannot cache the prepared statement. Instead, bind the list as a single array parameter via
5//! rusqlite's [`array`](https://docs.rs/rusqlite/latest/rusqlite/vtab/array/index.html) extension
6//! and expand it with `rarray`, keeping the SQL text constant and the comparison on the raw column
7//! (so an index on the column can be used):
8//!
9//! ```sql
10//! ... WHERE col IN (SELECT value FROM rarray(?1))
11//! ```
12//!
13//! The same idiom works for both integer and BLOB keys: the values are bound natively, so there is
14//! no per-row `hex()`/`unhex()` conversion and no JSON serialization.
15
16use rusqlite::types::Value;
17
18use crate::sqlite::codec::{DbValue, ToSqlValue};
19
20/// A list bound as an array parameter for use with `rarray`.
21#[derive(Debug, Clone, PartialEq)]
22pub struct InList(Vec<Value>);
23
24impl InList {
25    /// Builds an integer-keyed `IN` list. Pair with `... IN (SELECT value FROM rarray(?))`.
26    pub fn from_i64s(items: impl IntoIterator<Item = i64>) -> Self {
27        Self(items.into_iter().map(Value::Integer).collect())
28    }
29
30    /// Builds a BLOB-keyed `IN` list. Pair with `... IN (SELECT value FROM rarray(?))`; the column
31    /// is compared directly against the bound blobs, with no hex conversion.
32    pub fn from_blobs<'a>(items: impl IntoIterator<Item = &'a [u8]>) -> Self {
33        Self(items.into_iter().map(|bytes| Value::Blob(bytes.to_vec())).collect())
34    }
35
36    /// Builds an `IN` list from typed keys, binding each through its column codec. Pair with
37    /// `... IN (SELECT value FROM rarray(?))`.
38    ///
39    /// Prefer this over [`Self::from_i64s`] and [`Self::from_blobs`] whenever the keys are typed.
40    /// Going through [`ToSqlValue`] binds exactly what the column stores - a BLOB for the types
41    /// carrying a blob codec, an `INTEGER` for the scalar ones - so the list cannot disagree with
42    /// the column it is compared against. Serializing every key to bytes instead would bind blobs
43    /// against an `INTEGER` column and silently match nothing.
44    ///
45    /// The codec also produces the bound value directly, so the caller does not have to
46    /// materialize a `Vec<Vec<u8>>` to keep borrowed slices alive across the query. A blanket impl
47    /// covers references, so a `&[T]` slice of keys can be passed as-is.
48    pub fn from_values<T: ToSqlValue>(items: impl IntoIterator<Item = T>) -> Self {
49        let mut values = Vec::new();
50        for item in items {
51            match item.to_sql_value() {
52                DbValue::Single(value) => values.push(value),
53                // Only an `InList` binds an array value. SQLite has no nested arrays, so splicing
54                // is the only reading `rarray` can express.
55                DbValue::Array(nested) => values.extend(nested.iter().cloned()),
56            }
57        }
58        Self(values)
59    }
60}
61
62impl ToSqlValue for InList {
63    fn to_sql_value(&self) -> DbValue {
64        DbValue::array(self.0.clone())
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn in_list_i64_collects_integer_values() {
74        // Different list lengths produce the same SQL template (`rarray(?1)`); only the bound
75        // parameter contents differ.
76        assert_eq!(InList::from_i64s([1]).0, vec![Value::Integer(1)]);
77        assert_eq!(
78            InList::from_i64s([1, 2, 3]).0,
79            vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)]
80        );
81        assert_eq!(InList::from_i64s(std::iter::empty()).0, Vec::<Value>::new());
82    }
83
84    #[test]
85    fn in_list_values_bind_each_key_through_its_codec() {
86        use miden_protocol::Word;
87        use miden_protocol::block::BlockNumber;
88        use miden_protocol::utils::serde::Serializable;
89
90        // A blob-backed key binds a BLOB holding exactly what the column stores.
91        let word = Word::from([1_u32, 2, 3, 4]);
92        assert_eq!(InList::from_values([word]).0, vec![Value::Blob(word.to_bytes())]);
93
94        // A key whose codec maps onto an `INTEGER` column binds an integer, not its serialization.
95        assert_eq!(InList::from_values([BlockNumber::from(7_u32)]).0, vec![Value::Integer(7)]);
96
97        assert_eq!(InList::from_values(std::iter::empty::<u32>()).0, Vec::<Value>::new());
98    }
99
100    #[test]
101    fn in_list_blob_collects_blob_values() {
102        assert_eq!(
103            InList::from_blobs([[0x0a, 0xff].as_slice()]).0,
104            vec![Value::Blob(vec![0x0a, 0xff])]
105        );
106        assert_eq!(
107            InList::from_blobs([[0x01].as_slice(), [0x02].as_slice()]).0,
108            vec![Value::Blob(vec![0x01]), Value::Blob(vec![0x02])]
109        );
110    }
111}