Skip to content

Commit 589f2b1

Browse files
committed
improve error messages
1 parent 3551cfd commit 589f2b1

File tree

10 files changed

+68
-42
lines changed

10 files changed

+68
-42
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
- Add support for `change_percent` without `description` in the big_number component to display the percentage change of a value.
3333
- Add support for `freeze_columns` and `freeze_headers` in the table component to freeze columns and headers.
3434
- Fix an error that occured when the site_prefix configuration option was set to a value containing special characters like `-` and a request was made directly to the server without url-encoding the site prefix.
35+
- Improve error messages:
36+
- The error message now always includes the name of the file where the error occurred, which is useful when embedding SQLPage pages using `sqlpage.run_sql`, or the card component.
37+
- When an error occurs while executing a SQL statement, the error message now always includes the (potentially transformed) SQL statement that was sent to the database.
38+
- Fixed a problem where database errors would be displayed twice in the error message.
3539

3640
## 0.30.1 (2024-10-31)
3741
- fix a bug where table sorting would break if table search was not also enabled.

src/app_config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ mod test {
561561
assert_eq!(normalize_site_prefix("*-+/:;,?%\"'{"), "/*-+/:;,%3F%%22'{/");
562562
assert_eq!(
563563
normalize_site_prefix(
564-
&(0..=0x7F).map(|b| char::from(b)).collect::<String>()
564+
&(0..=0x7F).map(char::from).collect::<String>()
565565
),
566566
"/%00%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%&'()*+,-./0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~%7F/"
567567
);

src/file_cache.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
139139

140140
let parsed = match file_contents {
141141
Ok(contents) => {
142-
let value = T::from_str_with_state(app_state, &contents).await?;
142+
let value = T::from_str_with_state(app_state, &contents, path).await?;
143143
Ok(Cached::new(value))
144144
}
145145
// If a file is not found, we try to load it from the static files
@@ -184,5 +184,6 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
184184

185185
#[async_trait(? Send)]
186186
pub trait AsyncFromStrWithState: Sized {
187-
async fn from_str_with_state(app_state: &AppState, source: &str) -> anyhow::Result<Self>;
188-
}
187+
/// Parses the string into an object.
188+
async fn from_str_with_state(app_state: &AppState, source: &str, source_path: &Path) -> anyhow::Result<Self>;
189+
}

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::app_config::AppConfig;
1717
use crate::filesystem::FileSystem;
1818
use crate::webserver::database::ParsedSqlFile;
1919
use file_cache::FileCache;
20-
use std::path::PathBuf;
20+
use std::path::{Path, PathBuf};
2121
use templates::AllTemplates;
2222
use webserver::Database;
2323

@@ -47,7 +47,7 @@ impl AppState {
4747
let file_system = FileSystem::init(&config.web_root, &db).await;
4848
sql_file_cache.add_static(
4949
PathBuf::from("index.sql"),
50-
ParsedSqlFile::new(&db, include_str!("../index.sql")),
50+
ParsedSqlFile::new(&db, include_str!("../index.sql"), Path::new("index.sql")),
5151
);
5252
Ok(AppState {
5353
db,

src/templates.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{AppState, FileCache, TEMPLATES_DIR};
55
use async_trait::async_trait;
66
use handlebars::{template::TemplateElement, Handlebars, Template};
77
use include_dir::{include_dir, Dir};
8-
use std::path::PathBuf;
8+
use std::path::{Path, PathBuf};
99
use std::sync::Arc;
1010

1111
pub struct SplitTemplate {
@@ -52,7 +52,8 @@ pub fn split_template(mut original: Template) -> SplitTemplate {
5252

5353
#[async_trait(? Send)]
5454
impl AsyncFromStrWithState for SplitTemplate {
55-
async fn from_str_with_state(_app_state: &AppState, source: &str) -> anyhow::Result<Self> {
55+
async fn from_str_with_state(_app_state: &AppState, source: &str, source_path: &Path) -> anyhow::Result<Self> {
56+
log::debug!("Compiling template {:?}", source_path);
5657
let tpl = Template::compile_with_name(source, "SQLPage component".to_string())?;
5758
Ok(split_template(tpl))
5859
}

src/webserver/database/error_highlighting.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
use std::fmt::Write;
1+
use std::{
2+
fmt::Write,
3+
path::{Path, PathBuf},
4+
};
25

36
#[derive(Debug)]
47
struct NiceDatabaseError {
8+
source_file: PathBuf,
59
db_err: sqlx::error::Error,
610
query: String,
711
}
812

913
impl std::fmt::Display for NiceDatabaseError {
1014
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11-
self.db_err.fmt(f)?;
15+
write!(
16+
f,
17+
"In \"{}\": The following error occurred while executing an SQL statement:\n{}\n\nThe SQL statement sent by SQLPage was:\n",
18+
self.source_file.display(),
19+
self.db_err
20+
)?;
1221
if let sqlx::error::Error::Database(db_err) = &self.db_err {
1322
let Some(mut offset) = db_err.offset() else {
14-
return Ok(());
23+
return write!(f, "{}", self.query);
1524
};
16-
write!(f, "\n\nError in query:\n")?;
1725
for (line_no, line) in self.query.lines().enumerate() {
1826
if offset > line.len() {
1927
offset -= line.len() + 1;
@@ -23,23 +31,24 @@ impl std::fmt::Display for NiceDatabaseError {
2331
break;
2432
}
2533
}
34+
Ok(())
2635
} else {
27-
write!(f, "{}", self.query.lines().next().unwrap_or_default())?;
36+
write!(f, "{}", self.query)
2837
}
29-
Ok(())
3038
}
3139
}
3240

33-
impl std::error::Error for NiceDatabaseError {
34-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35-
self.db_err.source()
36-
}
37-
}
41+
impl std::error::Error for NiceDatabaseError {}
3842

3943
/// Display a database error with a highlighted line and character offset.
4044
#[must_use]
41-
pub fn display_db_error(query: &str, db_err: sqlx::error::Error) -> anyhow::Error {
45+
pub fn display_db_error(
46+
source_file: &Path,
47+
query: &str,
48+
db_err: sqlx::error::Error,
49+
) -> anyhow::Error {
4250
anyhow::Error::new(NiceDatabaseError {
51+
source_file: source_file.to_path_buf(),
4352
db_err,
4453
query: query.to_string(),
4554
})

src/webserver/database/execute_queries.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use futures_util::StreamExt;
44
use serde_json::Value;
55
use std::borrow::Cow;
66
use std::collections::HashMap;
7+
use std::path::Path;
78
use std::pin::Pin;
89

910
use super::csv_import::run_csv_import;
@@ -34,7 +35,7 @@ impl Database {
3435
.prepare_with(query, param_types)
3536
.await
3637
.map(|s| s.to_owned())
37-
.map_err(|e| display_db_error(query, e))
38+
.map_err(|e| display_db_error(Path::new("autogenerated sqlpage query"), query, e))
3839
}
3940
}
4041

@@ -43,6 +44,7 @@ pub fn stream_query_results_with_conn<'a>(
4344
request: &'a mut RequestInfo,
4445
db_connection: &'a mut DbConn,
4546
) -> impl Stream<Item = DbItem> + 'a {
47+
let source_file = &sql_file.source_path;
4648
async_stream::try_stream! {
4749
for res in &sql_file.statements {
4850
match res {
@@ -58,7 +60,7 @@ pub fn stream_query_results_with_conn<'a>(
5860
let mut stream = connection.fetch_many(query);
5961
while let Some(elem) = stream.next().await {
6062
let is_err = elem.is_err();
61-
let mut query_result = parse_single_sql_result(&stmt.query, elem);
63+
let mut query_result = parse_single_sql_result(source_file, &stmt.query, elem);
6264
apply_json_columns(&mut query_result, &stmt.json_columns);
6365
apply_delayed_functions(request, &stmt.delayed_functions, &mut query_result).await?;
6466
for i in parse_dynamic_rows(query_result) {
@@ -80,7 +82,7 @@ pub fn stream_query_results_with_conn<'a>(
8082
yield i;
8183
}
8284
}
83-
ParsedStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(e)),
85+
ParsedStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(source_file, e)),
8486
}
8587
}
8688
}
@@ -216,7 +218,11 @@ async fn take_connection<'a, 'b>(
216218
}
217219

218220
#[inline]
219-
fn parse_single_sql_result(sql: &str, res: sqlx::Result<Either<AnyQueryResult, AnyRow>>) -> DbItem {
221+
fn parse_single_sql_result(
222+
source_file: &Path,
223+
sql: &str,
224+
res: sqlx::Result<Either<AnyQueryResult, AnyRow>>,
225+
) -> DbItem {
220226
match res {
221227
Ok(Either::Right(r)) => {
222228
if log::log_enabled!(log::Level::Trace) {
@@ -228,7 +234,7 @@ fn parse_single_sql_result(sql: &str, res: sqlx::Result<Either<AnyQueryResult, A
228234
log::debug!("Finished query with result: {:?}", res);
229235
DbItem::FinishedQuery
230236
}
231-
Err(err) => DbItem::Error(display_db_error(sql, err)),
237+
Err(err) => DbItem::Error(display_db_error(source_file, sql, err)),
232238
}
233239
}
234240

@@ -252,8 +258,8 @@ fn debug_row(r: &AnyRow) {
252258
log::trace!("Received db row: {}", row_str);
253259
}
254260

255-
fn clone_anyhow_err(err: &anyhow::Error) -> anyhow::Error {
256-
let mut e = anyhow!("SQLPage could not parse and prepare this SQL statement");
261+
fn clone_anyhow_err(source_file: &Path, err: &anyhow::Error) -> anyhow::Error {
262+
let mut e = anyhow!("{source_file:?} contains a syntax error preventing SQLPage from parsing and preparing its SQL statements.");
257263
for c in err.chain().rev() {
258264
e = e.context(c.to_string());
259265
}

src/webserver/database/migrations.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyh
3535
match err {
3636
MigrateError::Execute(n, source) => {
3737
let migration = migrator.iter().find(|&m| m.version == n).unwrap();
38-
display_db_error(&migration.sql, source).context(format!(
38+
let source_file =
39+
migrations_dir.join(format!("{:04}_{}.sql", n, migration.description));
40+
display_db_error(&source_file, &migration.sql, source).context(format!(
3941
"Failed to apply migration {}",
4042
DisplayMigration(migration)
4143
))

src/webserver/database/sql.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,43 @@ use sqlparser::tokenizer::Token::{SemiColon, EOF};
1717
use sqlparser::tokenizer::Tokenizer;
1818
use sqlx::any::AnyKind;
1919
use std::ops::ControlFlow;
20+
use std::path::{Path, PathBuf};
2021
use std::str::FromStr;
2122

2223
#[derive(Default)]
2324
pub struct ParsedSqlFile {
2425
pub(super) statements: Vec<ParsedStatement>,
26+
pub(super) source_path: PathBuf,
2527
}
2628

2729
impl ParsedSqlFile {
2830
#[must_use]
29-
pub fn new(db: &Database, sql: &str) -> ParsedSqlFile {
31+
pub fn new(db: &Database, sql: &str, source_path: &Path) -> ParsedSqlFile {
3032
let dialect = dialect_for_db(db.connection.any_kind());
33+
log::debug!("Parsing SQL file {:?}", source_path);
3134
let parsed_statements = match parse_sql(dialect.as_ref(), sql) {
3235
Ok(parsed) => parsed,
33-
Err(err) => return Self::from_err(err),
36+
Err(err) => return Self::from_err(err, source_path),
3437
};
3538
let statements = parsed_statements.collect();
36-
ParsedSqlFile { statements }
39+
ParsedSqlFile {
40+
statements,
41+
source_path: source_path.to_path_buf(),
42+
}
3743
}
3844

39-
fn from_err(e: impl Into<anyhow::Error>) -> Self {
45+
fn from_err(e: impl Into<anyhow::Error>, source_path: &Path) -> Self {
4046
Self {
41-
statements: vec![ParsedStatement::Error(
42-
e.into().context("SQLPage could not parse the SQL file"),
43-
)],
47+
statements: vec![ParsedStatement::Error(e.into().context(format!("While parsing file {source_path:?}")))],
48+
source_path: source_path.to_path_buf(),
4449
}
4550
}
4651
}
4752

4853
#[async_trait(? Send)]
4954
impl AsyncFromStrWithState for ParsedSqlFile {
50-
async fn from_str_with_state(app_state: &AppState, source: &str) -> anyhow::Result<Self> {
51-
Ok(ParsedSqlFile::new(&app_state.db, source))
55+
async fn from_str_with_state(app_state: &AppState, source: &str, source_path: &Path) -> anyhow::Result<Self> {
56+
Ok(ParsedSqlFile::new(&app_state.db, source, source_path))
5257
}
5358
}
5459

@@ -96,7 +101,7 @@ fn parse_sql<'a>(
96101
.tokenize_with_location()
97102
.map_err(|err| {
98103
let location = err.location;
99-
anyhow::Error::new(err).context(format!("The SQLPage parser couldn't understand the SQL file. Tokenization failed. Please check for syntax errors:\n{}", quote_source_with_highlight(sql, location.line, location.column)))
104+
anyhow::Error::new(err).context(format!("The SQLPage parser could not understand the SQL file. Tokenization failed. Please check for syntax errors:\n{}", quote_source_with_highlight(sql, location.line, location.column)))
100105
})?;
101106
let mut parser = Parser::new(dialect).with_tokens_with_locations(tokens);
102107
let db_kind = kind_of_dialect(dialect);

src/webserver/database/sql_to_json.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,15 +408,13 @@ mod tests {
408408
}
409409
writeln!(
410410
&mut comparison_string,
411-
"{key:<width$} actual : {actual_value:?}\n{key:width$} expected: {expected_value:?}\n",
412-
width = max_key_len
411+
"{key:<max_key_len$} actual : {actual_value:?}\n{key:max_key_len$} expected: {expected_value:?}\n"
413412
)
414413
.unwrap();
415414
}
416415
assert_eq!(
417416
actual, expected,
418-
"JSON objects are not equal:\n\n{}",
419-
comparison_string
417+
"JSON objects are not equal:\n\n{comparison_string}"
420418
);
421419
}
422420
}

0 commit comments

Comments
 (0)