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 :