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 :