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
37impl ToSqlValue for InList {
38 fn to_sql_value(&self) -> DbValue {
39 DbValue::array(self.0.clone())
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 #[test]
48 fn in_list_i64_collects_integer_values() {
49 // Different list lengths produce the same SQL template (`rarray(?1)`); only the bound
50 // parameter contents differ.
51 assert_eq!(InList::from_i64s([1]).0, vec![Value::Integer(1)]);
52 assert_eq!(
53 InList::from_i64s([1, 2, 3]).0,
54 vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)]
55 );
56 assert_eq!(InList::from_i64s(std::iter::empty()).0, Vec::<Value>::new());
57 }
58
59 #[test]
60 fn in_list_blob_collects_blob_values() {
61 assert_eq!(
62 InList::from_blobs([[0x0a, 0xff].as_slice()]).0,
63 vec![Value::Blob(vec![0x0a, 0xff])]
64 );
65 assert_eq!(
66 InList::from_blobs([[0x01].as_slice(), [0x02].as_slice()]).0,
67 vec![Value::Blob(vec![0x01]), Value::Blob(vec![0x02])]
68 );
69 }
70}