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 :