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}