Skip to main content

cursor_helper/cursor/
sqlite_value.rs

1//! Shared SQLite value helpers for Cursor-managed databases.
2
3use rusqlite::types::ValueRef;
4use rusqlite::{Connection, OptionalExtension, Row, params};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum Utf8SqlValue {
8    Text(String),
9    Blob(String),
10}
11
12impl Utf8SqlValue {
13    pub fn as_str(&self) -> &str {
14        match self {
15            Self::Text(value) => value,
16            Self::Blob(value) => value,
17        }
18    }
19
20    pub fn from_row(row: &Row<'_>, idx: usize) -> rusqlite::Result<Option<Self>> {
21        match row.get_ref(idx)? {
22            ValueRef::Null => Ok(None),
23            ValueRef::Text(bytes) => Ok(Some(Self::Text(std::str::from_utf8(bytes)?.to_string()))),
24            ValueRef::Blob(bytes) => match std::str::from_utf8(bytes) {
25                Ok(value) => Ok(Some(Self::Blob(value.to_string()))),
26                Err(_) => Ok(None),
27            },
28            ValueRef::Integer(_) | ValueRef::Real(_) => Ok(None),
29        }
30    }
31
32    pub fn write_back(
33        &self,
34        conn: &Connection,
35        query: &str,
36        rowid: i64,
37    ) -> rusqlite::Result<usize> {
38        Ok(match self {
39            Self::Text(value) => conn.execute(query, params![value, rowid])?,
40            Self::Blob(value) => conn.execute(query, params![value.as_bytes(), rowid])?,
41        })
42    }
43}
44
45pub fn query_optional_utf8_value(
46    conn: &Connection,
47    query: &str,
48    key: &str,
49) -> rusqlite::Result<Option<String>> {
50    let text_result = conn
51        .query_row(query, rusqlite::params![key], |row| {
52            Utf8SqlValue::from_row(row, 0)
53        })
54        .optional()?;
55    if let Some(value) = text_result {
56        return Ok(value.map(|value| value.as_str().to_string()));
57    }
58
59    conn.query_row(query, rusqlite::params![key.as_bytes()], |row| {
60        Utf8SqlValue::from_row(row, 0)
61    })
62    .optional()
63    .map(|value| value.flatten().map(|value| value.as_str().to_string()))
64}
65
66pub fn query_optional_utf8_string_like_value(
67    conn: &Connection,
68    query: &str,
69    key: &str,
70    column_name: &str,
71) -> rusqlite::Result<Option<String>> {
72    let strict_reader = |row: &Row<'_>| {
73        let idx = 0;
74        let value = row.get_ref(idx)?;
75
76        match value {
77            ValueRef::Null => Ok(None),
78            ValueRef::Text(bytes) => Ok(Some(std::str::from_utf8(bytes)?.to_string())),
79            ValueRef::Blob(bytes) => Ok(Some(std::str::from_utf8(bytes)?.to_string())),
80            ValueRef::Integer(_) | ValueRef::Real(_) => Err(rusqlite::Error::InvalidColumnType(
81                idx,
82                column_name.to_string(),
83                value.data_type(),
84            )),
85        }
86    };
87
88    let text_result = conn
89        .query_row(query, rusqlite::params![key], strict_reader)
90        .optional()?;
91    if let Some(value) = text_result {
92        return Ok(value);
93    }
94
95    conn.query_row(query, rusqlite::params![key.as_bytes()], strict_reader)
96        .optional()
97        .map(|value| value.flatten())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use rusqlite::Connection;
104
105    #[test]
106    fn reads_utf8_blob_values() {
107        let conn = Connection::open_in_memory().unwrap();
108        conn.execute("CREATE TABLE data (value BLOB)", []).unwrap();
109        conn.execute(
110            "INSERT INTO data(value) VALUES (?1)",
111            [Vec::from("file:///workspace".as_bytes())],
112        )
113        .unwrap();
114
115        let value = conn
116            .query_row("SELECT value FROM data", [], |row| {
117                Utf8SqlValue::from_row(row, 0)
118            })
119            .unwrap()
120            .unwrap();
121
122        assert_eq!(value, Utf8SqlValue::Blob("file:///workspace".to_string()));
123        assert_eq!(value.as_str(), "file:///workspace");
124    }
125
126    #[test]
127    fn skips_invalid_utf8_blob_values() {
128        let conn = Connection::open_in_memory().unwrap();
129        conn.execute("CREATE TABLE data (value BLOB)", []).unwrap();
130        conn.execute("INSERT INTO data(value) VALUES (X'80')", [])
131            .unwrap();
132
133        let value = conn
134            .query_row("SELECT value FROM data", [], |row| {
135                Utf8SqlValue::from_row(row, 0)
136            })
137            .unwrap();
138
139        assert!(value.is_none());
140    }
141
142    #[test]
143    fn strict_reader_accepts_utf8_blob_values() {
144        let conn = Connection::open_in_memory().unwrap();
145        conn.execute("CREATE TABLE data (key TEXT PRIMARY KEY, value BLOB)", [])
146            .unwrap();
147        conn.execute(
148            "INSERT INTO data(key, value) VALUES (?1, ?2)",
149            ("composer", Vec::from("file:///workspace".as_bytes())),
150        )
151        .unwrap();
152
153        let value = query_optional_utf8_string_like_value(
154            &conn,
155            "SELECT value FROM data WHERE key = ?1",
156            "composer",
157            "value",
158        )
159        .unwrap();
160
161        assert_eq!(value.as_deref(), Some("file:///workspace"));
162    }
163
164    #[test]
165    fn strict_reader_rejects_integer_values() {
166        let conn = Connection::open_in_memory().unwrap();
167        conn.execute(
168            "CREATE TABLE data (key TEXT PRIMARY KEY, value INTEGER)",
169            [],
170        )
171        .unwrap();
172        conn.execute("INSERT INTO data(key, value) VALUES (?1, 42)", ["composer"])
173            .unwrap();
174
175        let err = query_optional_utf8_string_like_value(
176            &conn,
177            "SELECT value FROM data WHERE key = ?1",
178            "composer",
179            "value",
180        )
181        .unwrap_err();
182
183        assert!(matches!(err, rusqlite::Error::InvalidColumnType(..)));
184    }
185
186    #[test]
187    fn strict_reader_rejects_invalid_utf8_blob_values() {
188        let conn = Connection::open_in_memory().unwrap();
189        conn.execute("CREATE TABLE data (key TEXT PRIMARY KEY, value BLOB)", [])
190            .unwrap();
191        conn.execute(
192            "INSERT INTO data(key, value) VALUES (?1, X'80')",
193            ["composer"],
194        )
195        .unwrap();
196
197        let err = query_optional_utf8_string_like_value(
198            &conn,
199            "SELECT value FROM data WHERE key = ?1",
200            "composer",
201            "value",
202        )
203        .unwrap_err();
204
205        assert!(matches!(err, rusqlite::Error::Utf8Error(_)));
206    }
207
208    #[test]
209    fn permissive_reader_matches_blob_keys() {
210        let conn = Connection::open_in_memory().unwrap();
211        conn.execute("CREATE TABLE data (key BLOB PRIMARY KEY, value BLOB)", [])
212            .unwrap();
213        conn.execute(
214            "INSERT INTO data(key, value) VALUES (?1, ?2)",
215            (
216                Vec::from("composerData:workspace".as_bytes()),
217                Vec::from("file:///workspace".as_bytes()),
218            ),
219        )
220        .unwrap();
221
222        let value = query_optional_utf8_value(
223            &conn,
224            "SELECT value FROM data WHERE key = ?1",
225            "composerData:workspace",
226        )
227        .unwrap();
228
229        assert_eq!(value.as_deref(), Some("file:///workspace"));
230    }
231
232    #[test]
233    fn strict_reader_matches_blob_keys() {
234        let conn = Connection::open_in_memory().unwrap();
235        conn.execute("CREATE TABLE data (key BLOB PRIMARY KEY, value BLOB)", [])
236            .unwrap();
237        conn.execute(
238            "INSERT INTO data(key, value) VALUES (?1, ?2)",
239            (
240                Vec::from("composer".as_bytes()),
241                Vec::from("file:///workspace".as_bytes()),
242            ),
243        )
244        .unwrap();
245
246        let value = query_optional_utf8_string_like_value(
247            &conn,
248            "SELECT value FROM data WHERE key = ?1",
249            "composer",
250            "value",
251        )
252        .unwrap();
253
254        assert_eq!(value.as_deref(), Some("file:///workspace"));
255    }
256}