sqlite_err_parser/lib.rs
1#![allow(clippy::doc_markdown)]
2//! Turn certain sqlite errors (received through rusqlite) into structured
3//! data.
4//!
5//! # Caveat Emptor
6//! This crate will parse the raw error strings generated by the underlying
7//! SQLite library. There are several potential pitfalls with this:
8//!
9//! - SQLite does not appear to provide any stability guarantees with regards
10//! to its error messages.
11//! - Compliation options may affect what error messages SQLite will generate.
12//! - SQLite libraries from third parties may have modified error messages.
13//!
14//! There are more robust ways to solve the problem this crate attempts to
15//! solve -- for instance using triggers.
16
17use std::sync::OnceLock;
18
19use rusqlite::{
20 Error,
21 ffi::{
22 self as sys, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_NOTNULL,
23 SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_TRIGGER,
24 SQLITE_CONSTRAINT_UNIQUE
25 }
26};
27
28/// Translator function used to translate an [`InterpretedError`] to a
29/// `String`.
30#[allow(clippy::type_complexity)]
31static SQLERR_INTERPRETER: OnceLock<
32 Box<dyn Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync>
33> = OnceLock::new();
34
35
36/// Register a closure that translates an [`InterpretedError`] into a `String`.
37///
38/// The closure is called by [`interpret()`].
39pub fn register_interpreter(
40 f: impl Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync
41) {
42 let _ = SQLERR_INTERPRETER.set(Box::new(f));
43}
44
45
46/// Interpret an [`rusqlite::Error`] into a `String`.
47///
48/// ```
49/// use rusqlite::Connection;
50/// use sqlite_err_parser::{register_interpreter, InterpretedError, interpret};
51///
52/// fn stringify(ie: &InterpretedError) -> Option<String> {
53/// match ie {
54/// InterpretedError::NotUnique(ids) => match ids.as_slice() {
55/// [("testtbl1", "id")] => Some("id already exists".into()),
56/// [("testtbl1", "name")] => Some("name already exists".into()),
57/// [("testtbl2", "name1"), ("testtbl2", "name2")] => {
58/// Some("name pair already exists".into())
59/// }
60/// _ => None
61/// },
62/// InterpretedError::Check(rule) => None,
63/// _ => None
64/// }
65/// }
66///
67/// // Register translator function.
68/// register_interpreter(stringify);
69///
70/// // Open and initialize database
71/// let conn = Connection::open_in_memory().unwrap();
72/// conn
73/// .execute("CREATE TABLE IF NOT EXISTS testtbl1 (
74/// id INTEGER PRIMARY KEY,
75/// name TEXT UNIQUE NOT NULL
76/// );", []).unwrap();
77/// conn
78/// .execute("CREATE TABLE IF NOT EXISTS testtbl2 (
79/// id INTEGER PRIMARY KEY,
80/// name1 TEXT NOT NULL,
81/// name2 TEXT NOT NULL,
82/// UNIQUE(name1, name2)
83/// );", []).unwrap();
84///
85/// // Populate with some test data
86/// conn.execute(
87/// "INSERT INTO testtbl1 (id, name) VALUES (1, 'frank');", []
88/// ).unwrap();
89/// conn.execute(
90/// "INSERT INTO testtbl2 (id, name1, name2) VALUES (1, 'bill', 'frank');",
91/// []
92/// ).unwrap();
93///
94/// // testtbl1.id uniqueness violation
95/// let err = conn.execute(
96/// "INSERT INTO testtbl1 (id, name) VALUES (1, 'bill');", []
97/// ).unwrap_err();
98/// assert_eq!(interpret(&err), Some("id already exists".into()));
99///
100/// // testtbl1.name uniqueness violation
101/// let err = conn.execute(
102/// "INSERT INTO testtbl1 (name) VALUES ('frank');", []
103/// ).unwrap_err();
104/// assert_eq!(interpret(&err), Some("name already exists".into()));
105///
106/// // (testtbl2.name1, testtbl2.name2) uniqueness violation
107/// let err = conn.execute(
108/// "INSERT INTO testtbl2 (name1, name2) VALUES ('bill', 'frank');", []
109/// ).unwrap_err();
110/// assert_eq!(interpret(&err), Some("name pair already exists".into()));
111/// ```
112#[must_use]
113pub fn interpret(err: &rusqlite::Error) -> Option<String> {
114 deconstruct_error(err).as_ref().and_then(|ie| {
115 SQLERR_INTERPRETER.get().and_then(|stringify| stringify(ie))
116 })
117}
118
119
120/// A structured interpretation of certain rusqlite errors.
121#[derive(Debug)]
122#[non_exhaustive]
123pub enum InterpretedError<'e> {
124 /// A uniqueness violation.
125 ///
126 /// Sorted list of `(<table>, <column>)` pairs, specifying which columns
127 /// caused the uniqueness constraints violation.
128 NotUnique(Vec<(&'e str, &'e str)>),
129
130 /// A "not null" violation.
131 NotNull(Vec<(&'e str, &'e str)>),
132
133 /// A `CHECK` constraint violation.
134 ///
135 /// The field data contains the specific rule that caused the error.
136 Check(&'e str),
137
138 /// A foreign key constraint failed.
139 ForeignKey
140}
141
142
143/// Attempt to interpret a [`rusqlite::Error`], and turn it into an
144/// [`InterpretedError`].
145#[must_use]
146pub fn deconstruct_error(err: &Error) -> Option<InterpretedError<'_>> {
147 //println!("deconstruct: {err:?}");
148
149 match err {
150 Error::SqliteFailure(
151 sys::Error {
152 code,
153 extended_code
154 },
155 Some(msg)
156 ) if *code == sys::ErrorCode::ConstraintViolation => {
157 match *extended_code {
158 SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => {
159 //println!("uniqueness");
160 deconstruct_unique(msg)
161 }
162 SQLITE_CONSTRAINT_NOTNULL => {
163 //println!("notnull");
164 deconstruct_notnull(msg)
165 }
166 SQLITE_CONSTRAINT_CHECK => {
167 //println!("check");
168 deconstruct_check(msg)
169 }
170 SQLITE_CONSTRAINT_TRIGGER => deconstruct_constraint(msg),
171 _ => {
172 //panic!("{err}");
173 None
174 }
175 }
176 }
177 _e => {
178 //panic!("Unexpected error: {e}");
179 None
180 }
181 }
182}
183
184fn deconstruct_unique(msg: &str) -> Option<InterpretedError<'_>> {
185 //println!("{msg}");
186
187 // Extract the table and column name from
188 // s: "UNIQUE constraint failed: table.col"
189 // s: "UNIQUE constraint failed: table.col1, table.col2"
190
191 // Extract the part after "UNIQUE constraint failed:", which is assumed
192 // to be the list of columns where the violation occurred.
193 let Some((_, ns)) = msg.rsplit_once(':') else {
194 //panic!("No ':' in constraint violation");
195
196 // Bail
197 return None;
198 };
199
200 // Generate a list of <table>.<column>
201 let mut lst: Vec<(&str, &str)> = ns
202 .trim()
203 .split(',')
204 .map(str::trim)
205 .filter_map(|e| e.split_once('.'))
206 .collect();
207
208 // Sort table/column entries
209 lst.sort_unstable();
210
211 //println!("{lst:?}");
212
213 Some(InterpretedError::NotUnique(lst))
214}
215
216fn deconstruct_notnull(msg: &str) -> Option<InterpretedError<'_>> {
217 //println!("{msg}");
218
219 // Extract the table and column name from
220 // "NOT NULL constraint failed: agents.role_id"
221
222 let Some((_, ns)) = msg.rsplit_once(':') else {
223 //panic!("No ':' in constraint violation");
224 // Bail
225 return None;
226 };
227
228 // Generate a list of <table>.<column>
229 let mut lst: Vec<(&str, &str)> = ns
230 .trim()
231 .split(',')
232 .map(str::trim)
233 .filter_map(|e| e.split_once('.'))
234 .collect();
235
236 // Sort table/column entries
237 lst.sort_unstable();
238
239 //println!("{lst:?}");
240
241 Some(InterpretedError::NotNull(lst))
242}
243
244fn deconstruct_check(msg: &str) -> Option<InterpretedError<'_>> {
245 //println!("{msg}");
246
247 // Extract the table and column name from
248 // CHECK constraint failed: length(name)<4"
249
250 // Extract the part after "UNIQUE constraint failed:", which is assumed
251 // to be the list of columns where the violation occurred.
252 let Some((_, rule)) = msg.rsplit_once(':') else {
253 //panic!("No ':' in constraint violation");
254 return None;
255 };
256
257 Some(InterpretedError::Check(rule.trim()))
258}
259
260fn deconstruct_constraint(msg: &str) -> Option<InterpretedError<'_>> {
261 if msg == "FOREIGN KEY constraint failed" {
262 Some(InterpretedError::ForeignKey)
263 } else {
264 None
265 }
266}
267
268// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :