sqlite_err_parser/lib.rs
1//! Turn certain sqlite errors (received through rusqlite) into structured
2//! data.
3
4use std::sync::OnceLock;
5
6use rusqlite::{
7 Error,
8 ffi::{
9 self as sys, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_PRIMARYKEY,
10 SQLITE_CONSTRAINT_UNIQUE
11 }
12};
13
14/// Translator function used to translate an [`InterpretedError`] to a
15/// `String`.
16#[allow(clippy::type_complexity)]
17static SQLERR_INTERPRETER: OnceLock<
18 Box<dyn Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync>
19> = OnceLock::new();
20
21
22/// Register a closure that translates an [`InterpretedError`] into a `String`.
23///
24/// The closure is called by [`interpret()`].
25pub fn register_interpreter(
26 f: impl Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync
27) {
28 let _ = SQLERR_INTERPRETER.set(Box::new(f));
29}
30
31
32/// Interpret an [`rusqlite::Error`] into a `String`.
33///
34/// ```
35/// use rusqlite::Connection;
36/// use sqlite_err_parser::{register_interpreter, InterpretedError, interpret};
37///
38/// fn stringify(ie: &InterpretedError) -> Option<String> {
39/// match ie {
40/// InterpretedError::NotUnique(ids) => match ids.as_slice() {
41/// [("testtbl1", "id")] => Some("id already exists".into()),
42/// [("testtbl1", "name")] => Some("name already exists".into()),
43/// [("testtbl2", "name1"), ("testtbl2", "name2")] => {
44/// Some("name pair already exists".into())
45/// }
46/// _ => None
47/// },
48/// InterpretedError::Check(rule) => None,
49/// _ => None
50/// }
51/// }
52///
53/// // Register translator function.
54/// register_interpreter(stringify);
55///
56/// // Open and initialize database
57/// let conn = Connection::open_in_memory().unwrap();
58/// conn
59/// .execute("CREATE TABLE IF NOT EXISTS testtbl1 (
60/// id INTEGER PRIMARY KEY,
61/// name TEXT UNIQUE NOT NULL
62/// );", []).unwrap();
63/// conn
64/// .execute("CREATE TABLE IF NOT EXISTS testtbl2 (
65/// id INTEGER PRIMARY KEY,
66/// name1 TEXT NOT NULL,
67/// name2 TEXT NOT NULL,
68/// UNIQUE(name1, name2)
69/// );", []).unwrap();
70///
71/// // Populate with some test data
72/// conn.execute(
73/// "INSERT INTO testtbl1 (id, name) VALUES (1, 'frank');", []
74/// ).unwrap();
75/// conn.execute(
76/// "INSERT INTO testtbl2 (id, name1, name2) VALUES (1, 'bill', 'frank');",
77/// []
78/// ).unwrap();
79///
80/// // testtbl1.id uniqueness violation
81/// let err = conn.execute(
82/// "INSERT INTO testtbl1 (id, name) VALUES (1, 'bill');", []
83/// ).unwrap_err();
84/// assert_eq!(interpret(&err), Some("id already exists".into()));
85///
86/// // testtbl1.name uniqueness violation
87/// let err = conn.execute(
88/// "INSERT INTO testtbl1 (name) VALUES ('frank');", []
89/// ).unwrap_err();
90/// assert_eq!(interpret(&err), Some("name already exists".into()));
91///
92/// // (testtbl2.name1, testtbl2.name2) uniqueness violation
93/// let err = conn.execute(
94/// "INSERT INTO testtbl2 (name1, name2) VALUES ('bill', 'frank');", []
95/// ).unwrap_err();
96/// assert_eq!(interpret(&err), Some("name pair already exists".into()));
97/// ```
98#[must_use]
99pub fn interpret(err: &rusqlite::Error) -> Option<String> {
100 deconstruct_error(err).as_ref().and_then(|ie| {
101 SQLERR_INTERPRETER.get().and_then(|stringify| stringify(ie))
102 })
103}
104
105
106/// A structured interpretation of certain rusqlite errors.
107#[derive(Debug)]
108pub enum InterpretedError<'e> {
109 /// A uniqueness violation.
110 ///
111 /// Sorted list of `(<table>, <column>)` pairs, specifying which columns
112 /// caused the uniqueness constraints violation.
113 NotUnique(Vec<(&'e str, &'e str)>),
114
115 /// A `CHECK` constraint violation.
116 ///
117 /// The field data contains the specific rule that caused the error.
118 Check(&'e str)
119}
120
121
122/// Attempt to interpret a [`rusqlite::Error`], and turn it into an
123/// [`InterpretedError`].
124#[must_use]
125pub fn deconstruct_error(err: &Error) -> Option<InterpretedError<'_>> {
126 //println!("deconstruct: {err:?}");
127
128 match err {
129 Error::SqliteFailure(
130 sys::Error {
131 code,
132 extended_code
133 },
134 Some(msg)
135 ) if *code == sys::ErrorCode::ConstraintViolation => {
136 match *extended_code {
137 SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => {
138 //println!("uniqueness");
139
140 deconstruct_unique(msg)
141 }
142 SQLITE_CONSTRAINT_CHECK => {
143 //println!("check");
144
145 deconstruct_check(msg)
146 }
147 _ => {
148 //panic!("{err}");
149 None
150 }
151 }
152 }
153 _e => {
154 //panic!("Unexpected error: {e}");
155 None
156 }
157 }
158}
159
160fn deconstruct_unique(msg: &str) -> Option<InterpretedError<'_>> {
161 //println!("{msg}");
162
163 // Extract the table and column name from
164 // s: "UNIQUE constraint failed: table.col"
165 // s: "UNIQUE constraint failed: table.col1, table.col2"
166
167 // Extract the part after "UNIQUE constraint failed:", which is assumed
168 // to be the list of columns where the violation occurred.
169 let Some((_, ns)) = msg.rsplit_once(':') else {
170 //panic!("No ':' in constraint violation");
171
172 // Bail
173 return None;
174 };
175
176 // Generate a list of <table>.<column>
177 let mut lst: Vec<(&str, &str)> = ns
178 .trim()
179 .split(',')
180 .map(str::trim)
181 .filter_map(|e| e.split_once('.'))
182 .collect();
183
184 // Sort table/column entries
185 lst.sort_unstable();
186
187 //println!("{lst:?}");
188
189 Some(InterpretedError::NotUnique(lst))
190}
191
192fn deconstruct_check(msg: &str) -> Option<InterpretedError<'_>> {
193 //println!("{msg}");
194
195 // Extract the table and column name from
196 // CHECK constraint failed: length(name)<4"
197
198 // Extract the part after "UNIQUE constraint failed:", which is assumed
199 // to be the list of columns where the violation occurred.
200 let Some((_, rule)) = msg.rsplit_once(':') else {
201 //panic!("No ':' in constraint violation");
202 return None;
203 };
204
205 Some(InterpretedError::Check(rule.trim()))
206}
207
208// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :