1use 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}