Skip to content

Commit e47cbc9

Browse files
committed
better error messages
1 parent 82d7186 commit e47cbc9

File tree

4 files changed

+71
-6
lines changed

4 files changed

+71
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This is a bugfix release.
1212
- adds support for MSSQL's `JSON_ARRAY` and `JSON_OBJECT` functions
1313
- adds support for PostgreSQL's `JSON_OBJECT(key : value)` and `JSON_OBJECT(key VALUE value)` syntax
1414
- fixes the parsing of `true` and `false` in Microsoft SQL Server (mssql): they are now correctly parsed as column names, not as boolean values, since mssql does not support boolean literals. This means you may have to replace `TRUE as some_property` with `1 as some_property` in your SQL code when working with mssql.
15+
- When your SQL contains errors, the error message now displays the precise line(s) number(s) of your file that contain the error.
1516

1617
## 0.32.0 (2024-12-29)
1718

src/webserver/database/error_highlighting.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6+
use super::sql::{SourceSpan, StmtWithParams};
7+
68
#[derive(Debug)]
79
struct NiceDatabaseError {
10+
/// The source file that contains the query.
811
source_file: PathBuf,
12+
/// The error that occurred.
913
db_err: sqlx::error::Error,
14+
/// The query that was executed.
1015
query: String,
16+
/// The start location of the query in the source file, if the query was extracted from a larger file.
17+
query_position: Option<SourceSpan>,
1118
}
1219

1320
impl std::fmt::Display for NiceDatabaseError {
@@ -22,12 +29,20 @@ impl std::fmt::Display for NiceDatabaseError {
2229
let Some(mut offset) = db_err.offset() else {
2330
return write!(f, "{}", self.query);
2431
};
25-
for (line_no, line) in self.query.lines().enumerate() {
32+
for line in self.query.lines() {
2633
if offset > line.len() {
2734
offset -= line.len() + 1;
2835
} else {
2936
highlight_line_offset(f, line, offset);
30-
write!(f, "line {}, character {offset}", line_no + 1)?;
37+
if let Some(query_position) = self.query_position {
38+
let start_line = query_position.start.line;
39+
let end_line = query_position.end.line;
40+
if start_line == end_line {
41+
write!(f, "{}: line {}", self.source_file.display(), start_line)?;
42+
} else {
43+
write!(f, "{}: lines {} to {}", self.source_file.display(), start_line, end_line)?;
44+
}
45+
}
3146
break;
3247
}
3348
}
@@ -51,6 +66,22 @@ pub fn display_db_error(
5166
source_file: source_file.to_path_buf(),
5267
db_err,
5368
query: query.to_string(),
69+
query_position: None,
70+
})
71+
}
72+
73+
/// Display a database error with a highlighted line and character offset.
74+
#[must_use]
75+
pub fn display_stmt_db_error(
76+
source_file: &Path,
77+
stmt: &StmtWithParams,
78+
db_err: sqlx::error::Error,
79+
) -> anyhow::Error {
80+
anyhow::Error::new(NiceDatabaseError {
81+
source_file: source_file.to_path_buf(),
82+
db_err,
83+
query: stmt.query.to_string(),
84+
query_position: Some(stmt.query_position),
5485
})
5586
}
5687

src/webserver/database/execute_queries.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::path::Path;
88
use std::pin::Pin;
99

1010
use super::csv_import::run_csv_import;
11+
use super::error_highlighting::display_stmt_db_error;
1112
use super::sql::{
1213
DelayedFunctionCall, ParsedSqlFile, ParsedStatement, SimpleSelectValue, StmtWithParams,
1314
};
@@ -62,7 +63,7 @@ pub fn stream_query_results_with_conn<'a>(
6263
let mut stream = connection.fetch_many(query);
6364
let mut error = None;
6465
while let Some(elem) = stream.next().await {
65-
let mut query_result = parse_single_sql_result(source_file, &stmt.query, elem);
66+
let mut query_result = parse_single_sql_result(source_file, stmt, elem);
6667
if let DbItem::Error(e) = query_result {
6768
error = Some(e);
6869
break;
@@ -196,7 +197,7 @@ async fn execute_set_variable_query<'a>(
196197
Ok(None) => None,
197198
Err(e) => {
198199
try_rollback_transaction(connection).await;
199-
let err = display_db_error(source_file, &statement.query, e);
200+
let err = display_stmt_db_error(source_file, statement, e);
200201
return Err(err);
201202
}
202203
};
@@ -257,7 +258,7 @@ async fn take_connection<'a, 'b>(
257258
#[inline]
258259
fn parse_single_sql_result(
259260
source_file: &Path,
260-
sql: &str,
261+
stmt: &StmtWithParams,
261262
res: sqlx::Result<Either<AnyQueryResult, AnyRow>>,
262263
) -> DbItem {
263264
match res {
@@ -272,7 +273,7 @@ fn parse_single_sql_result(
272273
DbItem::FinishedQuery
273274
}
274275
Err(err) => {
275-
let nice_err = display_db_error(source_file, sql, err);
276+
let nice_err = display_stmt_db_error(source_file, stmt, err);
276277
DbItem::Error(nice_err)
277278
}
278279
}

src/webserver/database/sql.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ impl AsyncFromStrWithState for ParsedSqlFile {
7070
pub(super) struct StmtWithParams {
7171
/// The SQL query with placeholders for parameters.
7272
pub query: String,
73+
/// The line and column of the first token in the query.
74+
pub query_position: SourceSpan,
7375
/// Parameters that should be bound to the query.
7476
/// They can contain functions that will be called before the query is executed,
7577
/// the result of which will be bound to the query.
@@ -82,6 +84,20 @@ pub(super) struct StmtWithParams {
8284
pub json_columns: Vec<String>,
8385
}
8486

87+
/// A location in the source code.
88+
#[derive(Debug, PartialEq, Clone, Copy)]
89+
pub(super) struct SourceSpan {
90+
pub start: SourceLocation,
91+
pub end: SourceLocation,
92+
}
93+
94+
/// A location in the source code.
95+
#[derive(Debug, PartialEq, Clone, Copy)]
96+
pub(super) struct SourceLocation {
97+
pub line: usize,
98+
pub column: usize,
99+
}
100+
85101
#[derive(Debug)]
86102
pub(super) enum ParsedStatement {
87103
StmtWithParams(StmtWithParams),
@@ -165,12 +181,27 @@ fn parse_single_statement(
165181
log::debug!("Final transformed statement: {stmt}");
166182
Some(ParsedStatement::StmtWithParams(StmtWithParams {
167183
query,
184+
query_position: extract_query_start(&stmt),
168185
params,
169186
delayed_functions,
170187
json_columns,
171188
}))
172189
}
173190

191+
fn extract_query_start(stmt: &impl Spanned) -> SourceSpan {
192+
let location = stmt.span();
193+
SourceSpan {
194+
start: SourceLocation {
195+
line: usize::try_from(location.start.line).unwrap_or(0),
196+
column: usize::try_from(location.start.column).unwrap_or(0),
197+
},
198+
end: SourceLocation {
199+
line: usize::try_from(location.end.line).unwrap_or(0),
200+
column: usize::try_from(location.end.column).unwrap_or(0),
201+
},
202+
}
203+
}
204+
174205
fn syntax_error(err: ParserError, parser: &Parser, sql: &str) -> ParsedStatement {
175206
let location = parser.peek_token_no_skip().span;
176207
ParsedStatement::Error(anyhow::Error::from(err).context(format!(
@@ -426,6 +457,7 @@ fn extract_set_variable(
426457
variable,
427458
StmtWithParams {
428459
query: select_stmt.to_string(),
460+
query_position: extract_query_start(&select_stmt),
429461
params: std::mem::take(params),
430462
delayed_functions,
431463
json_columns,

0 commit comments

Comments
 (0)