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_TRIGGER, 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 /// A foreign key constraint failed.
121 ForeignKey
122}
123
124
125/// Attempt to interpret a [`rusqlite::Error`], and turn it into an
126/// [`InterpretedError`].
127#[must_use]
128pub fn deconstruct_error(err: &Error) -> Option<InterpretedError<'_>> {
129 //println!("deconstruct: {err:?}");
130
131 match err {
132 Error::SqliteFailure(
133 sys::Error {
134 code,
135 extended_code
136 },
137 Some(msg)
138 ) if *code == sys::ErrorCode::ConstraintViolation => {
139 match *extended_code {
140 SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => {
141 //println!("uniqueness");
142
143 deconstruct_unique(msg)
144 }
145 SQLITE_CONSTRAINT_CHECK => {
146 //println!("check");
147
148 deconstruct_check(msg)
149 }
150 SQLITE_CONSTRAINT_TRIGGER => deconstruct_constraint(msg),
151 _ => {
152 //panic!("{err}");
153 None
154 }
155 }
156 }
157 _e => {
158 //panic!("Unexpected error: {e}");
159 None
160 }
161 }
162}
163
164fn deconstruct_unique(msg: &str) -> Option<InterpretedError<'_>> {
165 //println!("{msg}");
166
167 // Extract the table and column name from
168 // s: "UNIQUE constraint failed: table.col"
169 // s: "UNIQUE constraint failed: table.col1, table.col2"
170
171 // Extract the part after "UNIQUE constraint failed:", which is assumed
172 // to be the list of columns where the violation occurred.
173 let Some((_, ns)) = msg.rsplit_once(':') else {
174 //panic!("No ':' in constraint violation");
175
176 // Bail
177 return None;
178 };
179
180 // Generate a list of <table>.<column>
181 let mut lst: Vec<(&str, &str)> = ns
182 .trim()
183 .split(',')
184 .map(str::trim)
185 .filter_map(|e| e.split_once('.'))
186 .collect();
187
188 // Sort table/column entries
189 lst.sort_unstable();
190
191 //println!("{lst:?}");
192
193 Some(InterpretedError::NotUnique(lst))
194}
195
196fn deconstruct_check(msg: &str) -> Option<InterpretedError<'_>> {
197 //println!("{msg}");
198
199 // Extract the table and column name from
200 // CHECK constraint failed: length(name)<4"
201
202 // Extract the part after "UNIQUE constraint failed:", which is assumed
203 // to be the list of columns where the violation occurred.
204 let Some((_, rule)) = msg.rsplit_once(':') else {
205 //panic!("No ':' in constraint violation");
206 return None;
207 };
208
209 Some(InterpretedError::Check(rule.trim()))
210}
211
212fn deconstruct_constraint(msg: &str) -> Option<InterpretedError<'_>> {
213 if msg == "FOREIGN KEY constraint failed" {
214 Some(InterpretedError::ForeignKey)
215 } else {
216 None
217 }
218}
219
220// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :