Report source location on every parser error
diff --git a/Cargo.lock b/Cargo.lock
index ce338b5..42cbb4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -647,6 +647,7 @@
  "serde_json",
  "simple_logger",
  "sqlparser_derive",
+ "thiserror",
 ]
 
 [[package]]
@@ -702,6 +703,26 @@
 ]
 
 [[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
 name = "time"
 version = "0.3.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 8b1ab55..a32ec2b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,6 +65,7 @@
 bigdecimal = { version = "0.4.1", features = ["serde"], optional = true }
 log = "0.4"
 recursive = { version = "0.1.1", optional = true}
+thiserror = { version = "2.0", default-features = false }
 
 serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true }
 # serde_json is only used in examples/cli, but we have to put it outside
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
index b9427b6..d84e91c 100644
--- a/fuzz/Cargo.lock
+++ b/fuzz/Cargo.lock
@@ -512,7 +512,7 @@
  "prost-build",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -814,6 +814,7 @@
 dependencies = [
  "log",
  "recursive",
+ "thiserror 2.0.21",
 ]
 
 [[package]]
@@ -887,7 +888,16 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
 ]
 
 [[package]]
@@ -902,6 +912,17 @@
 ]
 
 [[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
 name = "typed-path"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/fuzz/fuzz_targets/fuzz_duckdb_accepts.rs b/fuzz/fuzz_targets/fuzz_duckdb_accepts.rs
index 0a7e647..9a734ce 100644
--- a/fuzz/fuzz_targets/fuzz_duckdb_accepts.rs
+++ b/fuzz/fuzz_targets/fuzz_duckdb_accepts.rs
@@ -197,7 +197,7 @@
             return;
         }
         match Parser::parse_sql(&DuckDbDialect {}, sql) {
-            Ok(_) | Err(ParserError::RecursionLimitExceeded) => {}
+            Ok(_) | Err(ParserError::RecursionLimitExceeded { .. }) => {}
             Err(err) => {
                 // SAFETY: duckdb_library_version returns a static NUL-terminated string.
                 let version = unsafe { CStr::from_ptr(ffi::duckdb_library_version()) };
diff --git a/fuzz/fuzz_targets/fuzz_postgres_accepts.rs b/fuzz/fuzz_targets/fuzz_postgres_accepts.rs
index e387d8a..84fa1bd 100644
--- a/fuzz/fuzz_targets/fuzz_postgres_accepts.rs
+++ b/fuzz/fuzz_targets/fuzz_postgres_accepts.rs
@@ -29,7 +29,7 @@
         return;
     };
     match Parser::parse_sql(&PostgreSqlDialect {}, sql) {
-        Ok(_) | Err(ParserError::RecursionLimitExceeded) => {}
+        Ok(_) | Err(ParserError::RecursionLimitExceeded { .. }) => {}
         Err(err) => panic!(
             "PostgreSQL {} accepts SQL the PostgreSQL dialect rejects\n  sql: {sql:?}\n  error: {err}",
             parsed.protobuf.version
diff --git a/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs b/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs
index 7d45b68..59cde46 100644
--- a/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs
+++ b/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs
@@ -137,7 +137,7 @@
         return;
     }
     match Parser::parse_sql(&SQLiteDialect {}, sql) {
-        Ok(_) | Err(ParserError::RecursionLimitExceeded) => {}
+        Ok(_) | Err(ParserError::RecursionLimitExceeded { .. }) => {}
         Err(err) => {
             // SAFETY: sqlite3_libversion returns a static NUL terminated string.
             let version = unsafe { CStr::from_ptr(ffi::sqlite3_libversion()) };
diff --git a/src/ast/helpers/stmt_create_database.rs b/src/ast/helpers/stmt_create_database.rs
index e524228..c791402 100644
--- a/src/ast/helpers/stmt_create_database.rs
+++ b/src/ast/helpers/stmt_create_database.rs
@@ -28,6 +28,7 @@
     CatalogSyncNamespaceMode, ContactEntry, ObjectName, Statement, StorageSerializationPolicy, Tag,
 };
 use crate::parser::ParserError;
+use crate::tokenizer::Span;
 
 /// Builder for create database statement variant ([1]).
 ///
@@ -357,9 +358,10 @@
                 with_tags,
                 with_contacts,
             }),
-            _ => Err(ParserError::ParserError(format!(
-                "Expected create database statement, but received: {stmt}"
-            ))),
+            _ => Err(ParserError::ParserError {
+                message: format!("Expected create database statement, but received: {stmt}"),
+                span: Span::empty(),
+            }),
         }
     }
 }
@@ -369,6 +371,7 @@
     use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder;
     use crate::ast::{Ident, ObjectName, Statement};
     use crate::parser::ParserError;
+    use crate::tokenizer::Span;
 
     #[test]
     pub fn test_from_valid_statement() {
@@ -389,9 +392,10 @@
 
         assert_eq!(
             CreateDatabaseBuilder::try_from(stmt).unwrap_err(),
-            ParserError::ParserError(
-                "Expected create database statement, but received: COMMIT".to_owned()
-            )
+            ParserError::ParserError {
+                message: "Expected create database statement, but received: COMMIT".to_owned(),
+                span: Span::empty(),
+            }
         );
     }
 }
diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs
index 4ab9ac2..1d5fb6a 100644
--- a/src/ast/helpers/stmt_create_table.rs
+++ b/src/ast/helpers/stmt_create_table.rs
@@ -33,6 +33,7 @@
 };
 
 use crate::parser::ParserError;
+use crate::tokenizer::Span;
 
 /// Builder for create table statement variant ([1]).
 ///
@@ -667,9 +668,10 @@
     fn try_from(stmt: Statement) -> Result<Self, Self::Error> {
         match stmt {
             Statement::CreateTable(create_table) => Ok(create_table.into()),
-            _ => Err(ParserError::ParserError(format!(
-                "Expected create table statement, but received: {stmt}"
-            ))),
+            _ => Err(ParserError::ParserError {
+                message: format!("Expected create table statement, but received: {stmt}"),
+                span: Span::empty(),
+            }),
         }
     }
 }
@@ -758,6 +760,7 @@
     use crate::ast::helpers::stmt_create_table::CreateTableBuilder;
     use crate::ast::{Ident, ObjectName, Statement};
     use crate::parser::ParserError;
+    use crate::tokenizer::Span;
 
     #[test]
     pub fn test_from_valid_statement() {
@@ -779,9 +782,10 @@
 
         assert_eq!(
             CreateTableBuilder::try_from(stmt).unwrap_err(),
-            ParserError::ParserError(
-                "Expected create table statement, but received: COMMIT".to_owned()
-            )
+            ParserError::ParserError {
+                message: "Expected create table statement, but received: COMMIT".to_owned(),
+                span: Span::empty(),
+            }
         );
     }
 }
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index ce4cde4..2a7cfc1 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -38,7 +38,7 @@
 };
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
-use crate::parser::{IsOptional, Parser, ParserError};
+use crate::parser::{parser_err, IsOptional, Parser, ParserError};
 use crate::tokenizer::TokenWithSpan;
 use crate::tokenizer::{Span, Token};
 #[cfg(not(feature = "std"))]
@@ -461,7 +461,7 @@
             } else if parser.parse_keywords(&[Keyword::TAG]) {
                 Ok(parse_column_tags(parser, with).map(|p| Some(ColumnOption::Tags(p))))
             } else {
-                Err(ParserError::ParserError("not found match".to_string()))
+                parser_err!("not found match", parser.peek_token_ref().span)
             }
         })
     }
@@ -768,9 +768,10 @@
         Keyword::REMOVE | Keyword::RM => {
             Ok(Statement::Remove(FileStagingCommand { stage, pattern }))
         }
-        _ => Err(ParserError::ParserError(
-            "unexpected stage command, expecting LIST, LS, REMOVE or RM".to_string(),
-        )),
+        _ => parser_err!(
+            "unexpected stage command, expecting LIST, LS, REMOVE or RM",
+            parser.get_current_token().span
+        ),
     }
 }
 
@@ -1121,9 +1122,10 @@
     builder = builder.table_options(table_options);
 
     if iceberg && builder.base_location.is_none() {
-        return Err(ParserError::ParserError(
-            "BASE_LOCATION is required for ICEBERG tables".to_string(),
-        ));
+        return parser_err!(
+            "BASE_LOCATION is required for ICEBERG tables",
+            parser.peek_token_ref().span
+        );
     }
 
     Ok(builder.build())
@@ -1592,9 +1594,15 @@
     let next_token = parser.next_token();
     match next_token.token {
         Token::Placeholder(w) => {
-            file_col_num = w.to_string().split_off(1).parse::<i32>().map_err(|e| {
-                ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}"))
-            })?;
+            file_col_num = match w.to_string().split_off(1).parse::<i32>() {
+                Ok(n) => n,
+                Err(e) => {
+                    return parser_err!(
+                        format!("Could not parse '{w}' as i32: {e}"),
+                        next_token.span
+                    )
+                }
+            };
             Ok(())
         }
         Token::Word(w) => {
@@ -1610,9 +1618,15 @@
         let col_num_token = parser.next_token();
         match col_num_token.token {
             Token::Placeholder(w) => {
-                file_col_num = w.to_string().split_off(1).parse::<i32>().map_err(|e| {
-                    ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}"))
-                })?;
+                file_col_num = match w.to_string().split_off(1).parse::<i32>() {
+                    Ok(n) => n,
+                    Err(e) => {
+                        return parser_err!(
+                            format!("Could not parse '{w}' as i32: {e}"),
+                            col_num_token.span
+                        )
+                    }
+                };
                 Ok(())
             }
             _ => parser.expected("file_col_num", col_num_token),
@@ -1763,9 +1777,7 @@
         }
     }
     if options.is_empty() {
-        Err(ParserError::ParserError(
-            "expected at least one option".to_string(),
-        ))
+        parser_err!("expected at least one option", parser.peek_token_ref().span)
     } else {
         Ok(options)
     }
diff --git a/src/parser/alter.rs b/src/parser/alter.rs
index 4000eb2..d0e25e9 100644
--- a/src/parser/alter.rs
+++ b/src/parser/alter.rs
@@ -38,9 +38,10 @@
             return self.parse_mssql_alter_role();
         }
 
-        Err(ParserError::ParserError(
-            "ALTER ROLE is only support for PostgreSqlDialect, MsSqlDialect".into(),
-        ))
+        parser_err!(
+            "ALTER ROLE is only support for PostgreSqlDialect, MsSqlDialect",
+            self.get_current_token().span
+        )
     }
 
     /// Parse ALTER POLICY statement
diff --git a/src/parser/merge.rs b/src/parser/merge.rs
index 79a77df..53ddd87 100644
--- a/src/parser/merge.rs
+++ b/src/parser/merge.rs
@@ -123,8 +123,8 @@
                         MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget
                     ) {
                         return parser_err!(
-                            format_args!("UPDATE is not allowed in a {clause_kind} merge clause"),
-                            self.get_current_token().span.start
+                            format!("UPDATE is not allowed in a {clause_kind} merge clause"),
+                            self.get_current_token().span
                         );
                     }
 
@@ -159,8 +159,8 @@
                         MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget
                     ) {
                         return parser_err!(
-                            format_args!("DELETE is not allowed in a {clause_kind} merge clause"),
-                            self.get_current_token().span.start
+                            format!("DELETE is not allowed in a {clause_kind} merge clause"),
+                            self.get_current_token().span
                         );
                     };
 
@@ -175,8 +175,8 @@
                         MergeClauseKind::NotMatched | MergeClauseKind::NotMatchedByTarget
                     ) {
                         return parser_err!(
-                            format_args!("INSERT is not allowed in a {clause_kind} merge clause"),
-                            self.get_current_token().span.start
+                            format!("INSERT is not allowed in a {clause_kind} merge clause"),
+                            self.get_current_token().span
                         );
                     };
 
@@ -222,7 +222,7 @@
                 _ => {
                     return parser_err!(
                         "expected UPDATE, DELETE, INSERT or DO NOTHING in merge clause",
-                        self.peek_token_ref().span.start
+                        self.peek_token_ref().span
                     );
                 }
             };
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index db54296..3085a83 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -21,10 +21,7 @@
     vec,
     vec::Vec,
 };
-use core::{
-    fmt::{self, Display},
-    str::FromStr,
-};
+use core::{fmt::Display, str::FromStr};
 #[cfg(feature = "std")]
 use std::collections::{BTreeMap, BTreeSet};
 
@@ -32,7 +29,7 @@
 
 use log::debug;
 
-use recursion::RecursionCounter;
+use recursion::{DepthGuard, RecursionCounter};
 use IsLateral::*;
 use IsOptional::*;
 
@@ -52,22 +49,53 @@
 use sqlparser::parser::ParserState::ColumnDefinition;
 
 /// Errors produced by the SQL parser.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
 pub enum ParserError {
-    /// Error originating from the tokenizer with a message.
-    TokenizerError(String),
-    /// Generic parser error with a message.
-    ParserError(String),
+    /// Error originating from the tokenizer.
+    #[error("sql parser error: {message}{}", .span.start)]
+    TokenizerError {
+        /// Description of the error.
+        message: String,
+        /// Where the error was detected, empty when unknown.
+        span: Span,
+    },
+    /// Generic parser error.
+    #[error("sql parser error: {message}{}", .span.start)]
+    ParserError {
+        /// Description of the error.
+        message: String,
+        /// The offending source, empty when unknown.
+        span: Span,
+    },
     /// Raised when a recursion depth limit is exceeded.
-    RecursionLimitExceeded,
+    #[error("sql parser error: recursion limit exceeded{}", .span.start)]
+    RecursionLimitExceeded {
+        /// The token at which the limit was reached, empty when unknown.
+        span: Span,
+    },
+}
+
+impl ParserError {
+    /// The source span the error refers to, empty when unknown.
+    pub fn span(&self) -> Span {
+        match self {
+            ParserError::TokenizerError { span, .. }
+            | ParserError::ParserError { span, .. }
+            | ParserError::RecursionLimitExceeded { span } => *span,
+        }
+    }
 }
 
 // Use `Parser::expected` instead, if possible
 macro_rules! parser_err {
-    ($MSG:expr, $loc:expr) => {
-        Err(ParserError::ParserError(format!("{}{}", $MSG, $loc)))
+    ($MSG:expr, $span:expr) => {
+        Err(ParserError::ParserError {
+            message: ::alloc::string::String::from($MSG),
+            span: $span,
+        })
     };
 }
+pub(crate) use parser_err;
 
 mod alter;
 mod merge;
@@ -80,8 +108,6 @@
     use alloc::rc::Rc;
     use core::cell::Cell;
 
-    use super::ParserError;
-
     /// Tracks remaining recursion depth. This value is decremented on
     /// each call to [`RecursionCounter::try_decrease()`], when it reaches 0 an error will
     /// be returned.
@@ -107,18 +133,18 @@
 
         /// Decreases the remaining depth by 1.
         ///
-        /// Returns [`Err`] if the remaining depth falls to 0.
+        /// Returns [`None`] if the remaining depth is already 0.
         ///
         /// Returns a [`DepthGuard`] which will adds 1 to the
         /// remaining depth upon drop;
-        pub fn try_decrease(&self) -> Result<DepthGuard, ParserError> {
+        pub fn try_decrease(&self) -> Option<DepthGuard> {
             let old_value = self.remaining_depth.get();
             // ran out of space
             if old_value == 0 {
-                Err(ParserError::RecursionLimitExceeded)
+                None
             } else {
                 self.remaining_depth.set(old_value - 1);
-                Ok(DepthGuard::new(Rc::clone(&self.remaining_depth)))
+                Some(DepthGuard::new(Rc::clone(&self.remaining_depth)))
             }
         }
     }
@@ -171,38 +197,16 @@
 
 impl From<TokenizerError> for ParserError {
     fn from(e: TokenizerError) -> Self {
-        ParserError::TokenizerError(e.to_string())
+        ParserError::TokenizerError {
+            message: e.message,
+            span: Span::new(e.location, e.location),
+        }
     }
 }
 
-impl fmt::Display for ParserError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(
-            f,
-            "sql parser error: {}",
-            match self {
-                ParserError::TokenizerError(s) => s,
-                ParserError::ParserError(s) => s,
-                ParserError::RecursionLimitExceeded => "recursion limit exceeded",
-            }
-        )
-    }
-}
-
-impl core::error::Error for ParserError {}
-
 // By default, allow expressions up to this deep before erroring
 const DEFAULT_REMAINING_DEPTH: usize = 50;
 
-// A constant EOF token that can be referenced.
-const EOF_TOKEN: TokenWithSpan = TokenWithSpan {
-    token: Token::EOF,
-    span: Span {
-        start: Location { line: 0, column: 0 },
-        end: Location { line: 0, column: 0 },
-    },
-};
-
 /// Composite types declarations using angle brackets syntax can be arbitrary
 /// nested such that the following declaration is possible:
 ///      `ARRAY<ARRAY<INT>>`
@@ -313,7 +317,7 @@
 /// * The "previous" token is the token at `index - 2`
 ///
 /// If `index` is equal to the length of the token stream, the 'next' token is
-/// [`Token::EOF`].
+/// [`Token::EOF`], located right after the last non-whitespace token.
 ///
 /// For example, the SQL string "SELECT * FROM foo" will be tokenized into
 /// following tokens:
@@ -333,6 +337,8 @@
 pub struct Parser<'a> {
     /// The tokens
     tokens: Vec<TokenWithSpan>,
+    /// Returned when reading past the end of [`Parser::tokens`].
+    eof_token: TokenWithSpan,
     /// The index of the first unprocessed token in [`Parser::tokens`].
     index: usize,
     /// The current state of the parser.
@@ -370,14 +376,14 @@
 /// memoization, so the caches hold no strings.
 #[derive(Debug, Clone, Copy)]
 enum ExprPrefixError {
-    RecursionLimitExceeded,
+    RecursionLimitExceeded(Span),
     Err,
 }
 
 impl From<&ParserError> for ExprPrefixError {
     fn from(e: &ParserError) -> Self {
         match e {
-            ParserError::RecursionLimitExceeded => Self::RecursionLimitExceeded,
+            ParserError::RecursionLimitExceeded { span } => Self::RecursionLimitExceeded(*span),
             _ => Self::Err,
         }
     }
@@ -402,6 +408,7 @@
     pub fn new(dialect: &'a dyn Dialect) -> Self {
         Self {
             tokens: vec![],
+            eof_token: TokenWithSpan::new_eof(),
             index: 0,
             state: ParserState::Normal,
             dialect,
@@ -430,7 +437,7 @@
     ///   .with_recursion_limit(1)
     ///   .try_with_sql("SELECT * FROM foo WHERE (a OR (b OR (c OR d)))")?
     ///   .parse_statements();
-    ///   assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    ///   assert!(matches!(result, Err(ParserError::RecursionLimitExceeded { .. })));
     /// # Ok(())
     /// # }
     /// ```
@@ -446,6 +453,16 @@
         self
     }
 
+    /// Enters one level of recursion, failing at the next token once the
+    /// limit set by [`Parser::with_recursion_limit`] is reached.
+    fn recursion_guard(&self) -> Result<DepthGuard, ParserError> {
+        self.recursion_counter
+            .try_decrease()
+            .ok_or_else(|| ParserError::RecursionLimitExceeded {
+                span: self.peek_token_ref().span,
+            })
+    }
+
     /// Specify additional parser options
     ///
     /// [`Parser`] supports additional options ([`ParserOptions`])
@@ -475,6 +492,13 @@
 
     /// Reset this parser to parse the specified token stream
     pub fn with_tokens_with_locations(mut self, tokens: Vec<TokenWithSpan>) -> Self {
+        let end = tokens
+            .iter()
+            .rfind(|t| !matches!(t.token, Token::Whitespace(_)))
+            .map(|t| t.span.end)
+            .or_else(|| tokens.first().map(|t| t.span.start))
+            .unwrap_or_else(Location::empty);
+        self.eof_token = TokenWithSpan::new(Token::EOF, Span::new(end, end));
         self.tokens = tokens;
         self.index = 0;
         self.failed_prefix_positions.clear();
@@ -509,7 +533,12 @@
         let tokens = Tokenizer::new(self.dialect, sql)
             .with_unescape(self.options.unescape)
             .tokenize_with_location()?;
-        Ok(self.with_tokens_with_locations(tokens))
+        let mut parser = self.with_tokens_with_locations(tokens);
+        if parser.tokens.is_empty() {
+            let start = Location::new(1, 1);
+            parser.eof_token.span = Span::new(start, start);
+        }
+        Ok(parser)
     }
 
     /// Parse potentially multiple statements
@@ -622,7 +651,7 @@
     /// Parse a single top-level statement (such as SELECT, INSERT, CREATE, etc.),
     /// stopping before the statement separator, if any.
     pub fn parse_statement(&mut self) -> Result<Statement, ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
 
         // allow the dialect to override statement parsing
         if let Some(statement) = self.dialect.parse_statement(self) {
@@ -1015,10 +1044,7 @@
         let mut export = false;
 
         if !dialect_of!(self is MySqlDialect | GenericDialect) {
-            return parser_err!(
-                "Unsupported statement FLUSH",
-                self.peek_token_ref().span.start
-            );
+            return parser_err!("Unsupported statement FLUSH", self.peek_token_ref().span);
         }
 
         let location = if self.parse_keyword(Keyword::NO_WRITE_TO_BINLOG) {
@@ -1348,9 +1374,10 @@
                     Token::Word(w) => w.into_ident(next_token.span),
                     Token::SingleQuotedString(s) => Ident::with_quote('\'', s),
                     _ => {
-                        return Err(ParserError::ParserError(
-                            "Internal parser error: unexpected token type".to_string(),
-                        ))
+                        return parser_err!(
+                            "Internal parser error: unexpected token type",
+                            next_token.span
+                        )
                     }
                 }];
 
@@ -1428,7 +1455,7 @@
     /// Parse tokens until the precedence changes.
     #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
     pub fn parse_subexpr(&mut self, precedence: u8) -> Result<Expr, ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
         debug!("parsing expr");
         let mut expr = self.parse_prefix()?;
 
@@ -1789,7 +1816,9 @@
         found: &TokenWithSpan,
     ) -> Result<T, ParserError> {
         match cached {
-            ExprPrefixError::RecursionLimitExceeded => Err(ParserError::RecursionLimitExceeded),
+            ExprPrefixError::RecursionLimitExceeded(span) => {
+                Err(ParserError::RecursionLimitExceeded { span })
+            }
             ExprPrefixError::Err => self.expected_ref("an expression", found),
         }
     }
@@ -1811,7 +1840,7 @@
         // Note also that naively `SELECT date` looks like a syntax error because the `date` type
         // name is not followed by a string literal, but in fact in PostgreSQL it is a valid
         // expression that should parse as the column name "date".
-        let loc = self.peek_token_ref().span.start;
+        let loc = self.peek_token_ref().span;
         let opt_expr = self.maybe_parse(|parser| {
             match parser.parse_data_type()? {
                 DataType::Interval { .. } => parser.parse_interval(),
@@ -1950,9 +1979,10 @@
                     Token::PGCubeRoot => UnaryOperator::PGCubeRoot,
                     Token::AtSign => UnaryOperator::PGAbs,
                     _ => {
-                        return Err(ParserError::ParserError(
-                            "Internal parser error: unexpected unary operator token".to_string(),
-                        ))
+                        return parser_err!(
+                            "Internal parser error: unexpected unary operator token",
+                            self.get_current_token().span
+                        )
                     }
                 };
                 Ok(Expr::UnaryOp {
@@ -1980,9 +2010,10 @@
                     Token::QuestionMarkDash => UnaryOperator::QuestionDash,
                     Token::QuestionPipe => UnaryOperator::QuestionPipe,
                     _ => {
-                        return Err(ParserError::ParserError(format!(
-                            "Unexpected token in unary operator parsing: {tok:?}"
-                        )))
+                        return parser_err!(
+                            format!("Unexpected token in unary operator parsing: {tok:?}"),
+                            self.get_current_token().span
+                        )
                     }
                 };
                 Ok(Expr::UnaryOp {
@@ -2023,34 +2054,35 @@
                 Ok(Expr::Value(self.parse_value()?))
             }
             Token::LParen => {
-                let expr =
-                    if let Some(expr) = self.try_parse_expr_sub_query()? {
-                        expr
-                    } else if let Some(lambda) = self.try_parse_lambda()? {
-                        return Ok(lambda);
-                    } else {
-                        // Parentheses in expressions switch to "normal" parsing state.
-                        // This matters for dialects (SQLite, DuckDB) where `NOT NULL` can
-                        // be an alias for `IS NOT NULL`. In column definitions like:
-                        //
-                        //   CREATE TABLE t (c INT DEFAULT (42 NOT NULL) NOT NULL)
-                        //
-                        // The `(42 NOT NULL)` is an expression with parens, so it parses
-                        // as `IsNotNull(42)`. The trailing `NOT NULL` is outside those
-                        // expression parens (the outer parens are CREATE TABLE syntax),
-                        // so it remains a column constraint.
-                        let exprs = self.with_state(ParserState::Normal, |p| {
-                            p.parse_comma_separated(Parser::parse_expr)
-                        })?;
-                        match exprs.len() {
-                            0 => return Err(ParserError::ParserError(
-                                "Internal parser error: parse_comma_separated returned empty list"
-                                    .to_string(),
-                            )),
-                            1 => Expr::Nested(Box::new(exprs.into_iter().next().unwrap())),
-                            _ => Expr::Tuple(exprs),
+                let expr = if let Some(expr) = self.try_parse_expr_sub_query()? {
+                    expr
+                } else if let Some(lambda) = self.try_parse_lambda()? {
+                    return Ok(lambda);
+                } else {
+                    // Parentheses in expressions switch to "normal" parsing state.
+                    // This matters for dialects (SQLite, DuckDB) where `NOT NULL` can
+                    // be an alias for `IS NOT NULL`. In column definitions like:
+                    //
+                    //   CREATE TABLE t (c INT DEFAULT (42 NOT NULL) NOT NULL)
+                    //
+                    // The `(42 NOT NULL)` is an expression with parens, so it parses
+                    // as `IsNotNull(42)`. The trailing `NOT NULL` is outside those
+                    // expression parens (the outer parens are CREATE TABLE syntax),
+                    // so it remains a column constraint.
+                    let exprs = self.with_state(ParserState::Normal, |p| {
+                        p.parse_comma_separated(Parser::parse_expr)
+                    })?;
+                    match exprs.len() {
+                        0 => {
+                            return parser_err!(
+                                "Internal parser error: parse_comma_separated returned empty list",
+                                self.peek_token_ref().span
+                            )
                         }
-                    };
+                        1 => Expr::Nested(Box::new(exprs.into_iter().next().unwrap())),
+                        _ => Expr::Tuple(exprs),
+                    }
+                };
                 self.expect_token(&Token::RParen)?;
                 Ok(expr)
             }
@@ -2258,7 +2290,7 @@
                 .all(|access| matches!(access, AccessExpr::Dot(Expr::Identifier(_))))
         {
             let Some(AccessExpr::Dot(Expr::Function(mut func))) = access_chain.pop() else {
-                return parser_err!("expected function expression", root.span().start);
+                return parser_err!("expected function expression", root.span());
             };
 
             let compound_func_name = [root]
@@ -2290,14 +2322,14 @@
             )
         {
             let Some(AccessExpr::Dot(Expr::OuterJoin(inner_expr))) = access_chain.pop() else {
-                return parser_err!("expected (+) expression", root.span().start);
+                return parser_err!("expected (+) expression", root.span());
             };
 
             if !Self::is_all_ident(&root, &[]) {
-                return parser_err!("column identifier before (+)", root.span().start);
+                return parser_err!("column identifier before (+)", root.span());
             };
 
-            let token_start = root.span().start;
+            let token_start = root.span();
             let mut idents = Self::exprs_to_idents(root, vec![])?;
             match *inner_expr {
                 Expr::CompoundIdentifier(suffix) => idents.extend(suffix),
@@ -2344,18 +2376,12 @@
                 if let AccessExpr::Dot(Expr::Identifier(ident)) = x {
                     idents.push(ident);
                 } else {
-                    return parser_err!(
-                        format!("Expected identifier, found: {}", x),
-                        x.span().start
-                    );
+                    return parser_err!(format!("Expected identifier, found: {}", x), x.span());
                 }
             }
             Ok(idents)
         } else {
-            parser_err!(
-                format!("Expected identifier, found: {}", root),
-                root.span().start
-            )
+            parser_err!(format!("Expected identifier, found: {}", root), root.span())
         }
     }
 
@@ -2977,9 +3003,7 @@
         {
             ExtractSyntax::Comma
         } else {
-            return Err(ParserError::ParserError(
-                "Expected 'FROM' or ','".to_string(),
-            ));
+            return self.expected_ref("'FROM' or ','", self.peek_token_ref());
         };
 
         let expr = self.parse_expr()?;
@@ -3005,9 +3029,7 @@
             if matches!(v.value, Value::Number(_, _)) {
                 CeilFloorKind::Scale(v)
             } else {
-                return Err(ParserError::ParserError(
-                    "Scale field can only be of number type".to_string(),
-                ));
+                return parser_err!("Scale field can only be of number type", v.span);
             }
         } else {
             CeilFloorKind::DateTimeField(DateTimeField::NoDateTime)
@@ -3436,7 +3458,7 @@
     /// Note that we do not currently attempt to parse the quoted value.
     #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
     pub fn parse_interval(&mut self) -> Result<Expr, ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
 
         // The SQL standard allows an optional sign before the value string, but
         // it is not clear if any implementations support that syntax, so we
@@ -3465,7 +3487,7 @@
         } else if self.dialect.require_interval_qualifier() {
             return parser_err!(
                 "INTERVAL requires a unit after the literal value",
-                self.peek_token_ref().span.start
+                self.peek_token_ref().span
             );
         } else {
             None
@@ -3564,10 +3586,7 @@
         let (fields, trailing_bracket) =
             self.parse_struct_type_def(Self::parse_struct_field_def)?;
         if trailing_bracket.0 {
-            return parser_err!(
-                "unmatched > in STRUCT literal",
-                self.peek_token_ref().span.start
-            );
+            return parser_err!("unmatched > in STRUCT literal", self.peek_token_ref().span);
         }
 
         // Parse the struct values `(expr1 [, ... ])`
@@ -3598,7 +3617,7 @@
             if typed_syntax {
                 return parser_err!("Typed syntax does not allow AS", {
                     self.prev_token();
-                    self.peek_token_ref().span.start
+                    self.peek_token_ref().span
                 });
             }
             let field_name = self.parse_identifier()?;
@@ -4094,7 +4113,7 @@
                         format!(
                         "Expected one of [=, >, <, =>, =<, !=, ~, ~*, !~, !~*, ~~, ~~*, !~~, !~~*] as comparison operator, found: {op}"
                     ),
-                        span.start
+                        span
                     );
                 };
 
@@ -4110,9 +4129,7 @@
                         right: Box::new(right),
                         is_some: keyword == Keyword::SOME,
                     },
-                    unexpected_keyword => return Err(ParserError::ParserError(
-                        format!("Internal parser error: expected any of {{ALL, ANY, SOME}}, got {unexpected_keyword:?}"),
-                    )),
+                    unexpected_keyword => return parser_err!(format!("Internal parser error: expected any of {{ALL, ANY, SOME}}, got {unexpected_keyword:?}"), self.get_current_token().span),
                 })
             } else {
                 Ok(Expr::BinaryOp {
@@ -4266,7 +4283,7 @@
                 // Can only happen if `get_next_precedence` got out of sync with this function
                 _ => parser_err!(
                     format!("No infix parser for token {:?}", tok.token),
-                    tok.span.start
+                    tok.span
                 ),
             }
         } else if Token::DoubleColon == *tok {
@@ -4290,7 +4307,7 @@
             // Can only happen if `get_next_precedence` got out of sync with this function
             parser_err!(
                 format!("No infix parser for token {:?}", tok.token),
-                tok.span.start
+                tok.span
             )
         }
     }
@@ -4535,7 +4552,7 @@
                 negated,
                 global,
             }),
-            Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded),
+            Err(e @ ParserError::RecursionLimitExceeded { .. }) => Err(e),
             Err(e) => match missing_rparen {
                 Some(index) if self.index <= index => {
                     self.index = index;
@@ -4579,7 +4596,7 @@
     /// Return the token at the given location, or EOF if the index is beyond
     /// the length of the current set of tokens.
     pub fn token_at(&self, index: usize) -> &TokenWithSpan {
-        self.tokens.get(index).unwrap_or(&EOF_TOKEN)
+        self.tokens.get(index).unwrap_or(&self.eof_token)
     }
 
     /// Return the first non-whitespace token that has not yet been processed
@@ -4639,10 +4656,7 @@
             {
                 continue;
             }
-            break token.cloned().unwrap_or(TokenWithSpan {
-                token: Token::EOF,
-                span: Span::empty(),
-            });
+            break token.cloned().unwrap_or_else(|| self.eof_token.clone());
         })
     }
 
@@ -4662,7 +4676,7 @@
             {
                 continue;
             }
-            break token.unwrap_or(&EOF_TOKEN);
+            break token.unwrap_or(&self.eof_token);
         })
     }
 
@@ -4683,7 +4697,7 @@
                 }) => continue,
                 non_whitespace => {
                     if n == 0 {
-                        return non_whitespace.unwrap_or(&EOF_TOKEN);
+                        return non_whitespace.unwrap_or(&self.eof_token);
                     }
                     n -= 1;
                 }
@@ -4710,7 +4724,7 @@
 
     /// Return nth token, possibly whitespace, that has not yet been processed.
     fn peek_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan {
-        self.tokens.get(self.index + n).unwrap_or(&EOF_TOKEN)
+        self.tokens.get(self.index + n).unwrap_or(&self.eof_token)
     }
 
     /// Return true if the next tokens exactly `expected`
@@ -4806,27 +4820,18 @@
 
     /// Report `found` was encountered instead of `expected`
     pub fn expected<T>(&self, expected: &str, found: TokenWithSpan) -> Result<T, ParserError> {
-        parser_err!(
-            format!("Expected: {expected}, found: {found}"),
-            found.span.start
-        )
+        parser_err!(format!("Expected: {expected}, found: {found}"), found.span)
     }
 
     /// report `found` was encountered instead of `expected`
     pub fn expected_ref<T>(&self, expected: &str, found: &TokenWithSpan) -> Result<T, ParserError> {
-        parser_err!(
-            format!("Expected: {expected}, found: {found}"),
-            found.span.start
-        )
+        parser_err!(format!("Expected: {expected}, found: {found}"), found.span)
     }
 
     /// Report that the token at `index` was found instead of `expected`.
     pub fn expected_at<T>(&self, expected: &str, index: usize) -> Result<T, ParserError> {
-        let found = self.tokens.get(index).unwrap_or(&EOF_TOKEN);
-        parser_err!(
-            format!("Expected: {expected}, found: {found}"),
-            found.span.start
-        )
+        let found = self.tokens.get(index).unwrap_or(&self.eof_token);
+        parser_err!(format!("Expected: {expected}, found: {found}"), found.span)
     }
 
     /// If the current token is the `expected` keyword, consume it and returns
@@ -5031,15 +5036,16 @@
         }
     }
 
-    fn parse<T: FromStr>(s: String, loc: Location) -> Result<T, ParserError>
+    fn parse<T: FromStr>(s: String, span: Span) -> Result<T, ParserError>
     where
         <T as FromStr>::Err: Display,
     {
-        s.parse::<T>().map_err(|e| {
-            ParserError::ParserError(format!(
-                "Could not parse '{s}' as {}: {e}{loc}",
+        s.parse::<T>().map_err(|e| ParserError::ParserError {
+            message: format!(
+                "Could not parse '{s}' as {}: {e}",
                 core::any::type_name::<T>()
-            ))
+            ),
+            span,
         })
     }
 
@@ -5284,7 +5290,7 @@
     {
         match self.try_parse(f) {
             Ok(t) => Ok(Some(t)),
-            Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded),
+            Err(e @ ParserError::RecursionLimitExceeded { .. }) => Err(e),
             _ => Ok(None),
         }
     }
@@ -5308,7 +5314,7 @@
     /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed
     /// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found.
     pub fn parse_all_or_distinct(&mut self) -> Result<Option<Distinct>, ParserError> {
-        let loc = self.peek_token_ref().span.start;
+        let loc = self.peek_token_ref().span;
         let distinct = match self.parse_one_of_keywords(&[Keyword::ALL, Keyword::DISTINCT]) {
             Some(Keyword::ALL) => {
                 if self.peek_keyword(Keyword::DISTINCT) {
@@ -5346,7 +5352,7 @@
 
     /// Parse a SQL CREATE statement
     pub fn parse_create(&mut self) -> Result<Statement, ParserError> {
-        let modifier_loc = self.peek_token_ref().span.start;
+        let modifier_span = self.peek_token_ref().span;
         let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
         let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]);
         let multiset = self.maybe_parse_multiset();
@@ -5466,7 +5472,7 @@
             {
                 return parser_err!(
                     "CREATE FOREIGN TABLE does not accept this modifier",
-                    modifier_loc
+                    modifier_span
                 );
             }
             self.parse_create_foreign_table().map(Into::into)
@@ -5486,9 +5492,9 @@
             Keyword::CONFIGURATION => Ok(TextSearchObjectType::Configuration),
             Keyword::TEMPLATE => Ok(TextSearchObjectType::Template),
             Keyword::PARSER => Ok(TextSearchObjectType::Parser),
-            unexpected_keyword => Err(ParserError::ParserError(format!(
+            unexpected_keyword => parser_err!(format!(
                 "Internal parser error: expected any of {{DICTIONARY, CONFIGURATION, TEMPLATE, PARSER}}, got {unexpected_keyword:?}"
-            ))),
+            ), self.get_current_token().span),
         }
     }
 
@@ -5992,28 +5998,31 @@
         let mut body = Body::default();
         let mut set_params: Vec<FunctionDefinitionSetParam> = Vec::new();
         loop {
-            fn ensure_not_set<T>(field: &Option<T>, name: &str) -> Result<(), ParserError> {
+            fn ensure_not_set<T>(
+                field: &Option<T>,
+                name: &str,
+                span: Span,
+            ) -> Result<(), ParserError> {
                 if field.is_some() {
-                    return Err(ParserError::ParserError(format!(
-                        "{name} specified more than once",
-                    )));
+                    return parser_err!(format!("{name} specified more than once"), span);
                 }
                 Ok(())
             }
+            let clause = self.peek_token_ref().span;
             if self.parse_keyword(Keyword::AS) {
-                ensure_not_set(&body.function_body, "AS")?;
+                ensure_not_set(&body.function_body, "AS", clause)?;
                 body.function_body = Some(self.parse_create_function_body_string()?);
             } else if self.parse_keyword(Keyword::LANGUAGE) {
-                ensure_not_set(&body.language, "LANGUAGE")?;
+                ensure_not_set(&body.language, "LANGUAGE", clause)?;
                 body.language = Some(self.parse_identifier()?);
             } else if self.parse_keyword(Keyword::IMMUTABLE) {
-                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?;
+                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE", clause)?;
                 body.behavior = Some(FunctionBehavior::Immutable);
             } else if self.parse_keyword(Keyword::STABLE) {
-                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?;
+                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE", clause)?;
                 body.behavior = Some(FunctionBehavior::Stable);
             } else if self.parse_keyword(Keyword::VOLATILE) {
-                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?;
+                ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE", clause)?;
                 body.behavior = Some(FunctionBehavior::Volatile);
             } else if self.parse_keywords(&[
                 Keyword::CALLED,
@@ -6024,6 +6033,7 @@
                 ensure_not_set(
                     &body.called_on_null,
                     "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT",
+                    clause,
                 )?;
                 body.called_on_null = Some(FunctionCalledOnNull::CalledOnNullInput);
             } else if self.parse_keywords(&[
@@ -6036,16 +6046,22 @@
                 ensure_not_set(
                     &body.called_on_null,
                     "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT",
+                    clause,
                 )?;
                 body.called_on_null = Some(FunctionCalledOnNull::ReturnsNullOnNullInput);
             } else if self.parse_keyword(Keyword::STRICT) {
                 ensure_not_set(
                     &body.called_on_null,
                     "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT",
+                    clause,
                 )?;
                 body.called_on_null = Some(FunctionCalledOnNull::Strict);
             } else if self.parse_keyword(Keyword::PARALLEL) {
-                ensure_not_set(&body.parallel, "PARALLEL { UNSAFE | RESTRICTED | SAFE }")?;
+                ensure_not_set(
+                    &body.parallel,
+                    "PARALLEL { UNSAFE | RESTRICTED | SAFE }",
+                    clause,
+                )?;
                 if self.parse_keyword(Keyword::UNSAFE) {
                     body.parallel = Some(FunctionParallel::Unsafe);
                 } else if self.parse_keyword(Keyword::RESTRICTED) {
@@ -6057,7 +6073,7 @@
                         .expected_ref("one of UNSAFE | RESTRICTED | SAFE", self.peek_token_ref());
                 }
             } else if self.parse_keyword(Keyword::SECURITY) {
-                ensure_not_set(&body.security, "SECURITY { DEFINER | INVOKER }")?;
+                ensure_not_set(&body.security, "SECURITY { DEFINER | INVOKER }", clause)?;
                 if self.parse_keyword(Keyword::DEFINER) {
                     body.security = Some(FunctionSecurity::Definer);
                 } else if self.parse_keyword(Keyword::INVOKER) {
@@ -6082,7 +6098,7 @@
                 };
                 set_params.push(FunctionDefinitionSetParam { name, value });
             } else if self.parse_keyword(Keyword::RETURN) {
-                ensure_not_set(&body.function_body, "RETURN")?;
+                ensure_not_set(&body.function_body, "RETURN", clause)?;
                 body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?));
             } else {
                 break;
@@ -6252,7 +6268,7 @@
                 }
                 _ => parser_err!(
                     "Expected table column definitions after TABLE keyword",
-                    p.peek_token_ref().span.start
+                    p.peek_token_ref().span
                 )?,
             };
 
@@ -6289,11 +6305,11 @@
             } else {
                 parser_err!(
                     "Expected a subquery (or bare SELECT statement) after RETURN",
-                    self.peek_token_ref().span.start
+                    self.peek_token_ref().span
                 )?
             }
         } else {
-            parser_err!("Unparsable function body", self.peek_token_ref().span.start)?
+            parser_err!("Unparsable function body", self.peek_token_ref().span)?
         };
 
         Ok(CreateFunction {
@@ -6381,7 +6397,7 @@
                 // This dummy error is ignored in `maybe_parse`
                 parser_err!(
                     "The DEFAULT keyword is not a type",
-                    parser.peek_token_ref().span.start
+                    parser.peek_token_ref().span
                 )
             } else {
                 parser.parse_data_type()
@@ -6445,7 +6461,7 @@
                 // Dummy error ignored by maybe_parse
                 parser_err!(
                     "The current token cannot start an aggregate argument type",
-                    parser.peek_token_ref().span.start
+                    parser.peek_token_ref().span
                 )
             } else {
                 parser.parse_data_type()
@@ -6498,9 +6514,7 @@
         let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) {
             Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade),
             Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict),
-            Some(unexpected_keyword) => return Err(ParserError::ParserError(
-                format!("Internal parser error: expected any of {{CASCADE, RESTRICT}}, got {unexpected_keyword:?}"),
-            )),
+            Some(unexpected_keyword) => return parser_err!(format!("Internal parser error: expected any of {{CASCADE, RESTRICT}}, got {unexpected_keyword:?}"), self.get_current_token().span),
             None => None,
         };
         Ok(DropTrigger {
@@ -6553,9 +6567,7 @@
                 match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? {
                     Keyword::ROW => TriggerObject::Row,
                     Keyword::STATEMENT => TriggerObject::Statement,
-                    unexpected_keyword => return Err(ParserError::ParserError(
-                        format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in ROW/STATEMENT"),
-                    )),
+                    unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in ROW/STATEMENT"), self.get_current_token().span),
                 };
 
             Some(if include_each {
@@ -6618,9 +6630,7 @@
                 Keyword::INSTEAD => self
                     .expect_keyword_is(Keyword::OF)
                     .map(|_| TriggerPeriod::InsteadOf)?,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger period"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger period"), self.get_current_token().span),
             },
         )
     }
@@ -6645,9 +6655,7 @@
                 }
                 Keyword::DELETE => TriggerEvent::Delete,
                 Keyword::TRUNCATE => TriggerEvent::Truncate,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger event"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger event"), self.get_current_token().span),
             },
         )
     }
@@ -6683,9 +6691,7 @@
             {
                 Keyword::FUNCTION => TriggerExecBodyType::Function,
                 Keyword::PROCEDURE => TriggerExecBodyType::Procedure,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger exec body"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger exec body"), self.get_current_token().span),
             },
             func_desc: self.parse_function_desc()?,
         })
@@ -7085,10 +7091,7 @@
         let mut admin = vec![];
 
         while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) {
-            let loc = self
-                .tokens
-                .get(self.index - 1)
-                .map_or(Location { line: 0, column: 0 }, |t| t.span.start);
+            let loc = self.get_current_token().span;
             match keyword {
                 Keyword::AUTHORIZATION => {
                     if authorization_owner.is_some() {
@@ -7255,21 +7258,31 @@
 
     /// Parse an `OWNER` clause.
     pub fn parse_owner(&mut self) -> Result<Owner, ParserError> {
-        let owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) {
+        let owner = match self.parse_one_of_keywords(&[
+            Keyword::CURRENT_USER,
+            Keyword::CURRENT_ROLE,
+            Keyword::SESSION_USER,
+        ]) {
             Some(Keyword::CURRENT_USER) => Owner::CurrentUser,
             Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole,
             Some(Keyword::SESSION_USER) => Owner::SessionUser,
-            Some(unexpected_keyword) => return Err(ParserError::ParserError(
-                format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in owner"),
-            )),
-            None => {
-                match self.parse_identifier() {
-                    Ok(ident) => Owner::Ident(ident),
-                    Err(e) => {
-                        return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}")))
-                    }
-                }
+            Some(unexpected_keyword) => {
+                return parser_err!(
+                    format!(
+                        "Internal parser error: unexpected keyword `{unexpected_keyword}` in owner"
+                    ),
+                    self.get_current_token().span
+                )
             }
+            None => match self.maybe_parse(|parser| parser.parse_identifier())? {
+                Some(ident) => Owner::Ident(ident),
+                None => {
+                    return self.expected_ref(
+                        "CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO",
+                        self.peek_token_ref(),
+                    )
+                }
+            },
         };
         Ok(owner)
     }
@@ -7323,9 +7336,7 @@
             Some(match keyword {
                 Keyword::PERMISSIVE => CreatePolicyType::Permissive,
                 Keyword::RESTRICTIVE => CreatePolicyType::Restrictive,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in policy type"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in policy type"), self.get_current_token().span),
             })
         } else {
             None
@@ -7345,9 +7356,7 @@
                 Keyword::INSERT => CreatePolicyCommand::Insert,
                 Keyword::UPDATE => CreatePolicyCommand::Update,
                 Keyword::DELETE => CreatePolicyCommand::Delete,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in policy command"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in policy command"), self.get_current_token().span),
             })
         } else {
             None
@@ -7541,10 +7550,10 @@
                     options.push(OperatorOption::Join(Some(self.parse_object_name(false)?)));
                 }
                 _ => {
-                    return Err(ParserError::ParserError(format!(
-                        "Duplicate or unexpected keyword {:?} in CREATE OPERATOR",
-                        keyword
-                    )))
+                    return parser_err!(
+                        format!("Duplicate or unexpected keyword {keyword:?} in CREATE OPERATOR"),
+                        self.get_current_token().span
+                    )
                 }
             }
 
@@ -7557,9 +7566,12 @@
         self.expect_token(&Token::RParen)?;
 
         // FUNCTION is required
-        let function = function.ok_or_else(|| {
-            ParserError::ParserError("CREATE OPERATOR requires FUNCTION parameter".to_string())
-        })?;
+        let Some(function) = function else {
+            return parser_err!(
+                "CREATE OPERATOR requires FUNCTION parameter",
+                self.get_current_token().span
+            );
+        };
 
         Ok(CreateOperator {
             name,
@@ -7780,7 +7792,7 @@
         let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
         let names = self.parse_comma_separated(|p| p.parse_object_name(false))?;
 
-        let loc = self.peek_token_ref().span.start;
+        let loc = self.peek_token_ref().span;
         let cascade = self.parse_keyword(Keyword::CASCADE);
         let restrict = self.parse_keyword(Keyword::RESTRICT);
         let purge = self.parse_keyword(Keyword::PURGE);
@@ -7985,9 +7997,7 @@
                 match keyword {
                     Keyword::WITH => Some(true),
                     Keyword::WITHOUT => Some(false),
-                    unexpected_keyword => return Err(ParserError::ParserError(
-                        format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in cursor hold"),
-                    )),
+                    unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in cursor hold"), self.get_current_token().span),
                 }
             }
             None => None,
@@ -8207,9 +8217,7 @@
                     Token::Word(w) if w.keyword == Keyword::CURSOR
                 )
             {
-                Err(ParserError::TokenizerError(
-                    "Invalid MsSql variable declaration.".to_string(),
-                ))
+                parser_err!("Invalid MsSql variable declaration.", ident.span)
             } else {
                 Ok(ident)
             }
@@ -8348,7 +8356,7 @@
             self.expect_keyword(Keyword::IN)?;
             FetchPosition::In
         } else {
-            return parser_err!("Expected FROM or IN", self.peek_token_ref().span.start);
+            return parser_err!("Expected FROM or IN", self.peek_token_ref().span);
         };
 
         let name = self.parse_identifier()?;
@@ -9169,7 +9177,7 @@
         } else {
             parser_err!(
                 "Expecting DELETE ROWS, PRESERVE ROWS or DROP",
-                self.peek_token_ref()
+                self.peek_token_ref().span
             )
         }
     }
@@ -10723,9 +10731,7 @@
 
             Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns)))
         } else {
-            Err(ParserError::ParserError(
-                "invalid CLUSTERED sequence".to_string(),
-            ))
+            parser_err!("invalid CLUSTERED sequence", self.peek_token_ref().span)
         }
     }
 
@@ -11329,9 +11335,7 @@
             Keyword::PART => Ok(Partition::Part(self.parse_expr()?)),
             Keyword::PARTITION => Ok(Partition::Expr(self.parse_expr()?)),
             // unreachable because expect_one_of_keywords used above
-            unexpected_keyword => Err(ParserError::ParserError(
-                format!("Internal parser error: expected any of {{PART, PARTITION}}, got {unexpected_keyword:?}"),
-            )),
+            unexpected_keyword => parser_err!(format!("Internal parser error: expected any of {{PART, PARTITION}}, got {unexpected_keyword:?}"), self.get_current_token().span),
         }
     }
 
@@ -11408,9 +11412,7 @@
             }
             Keyword::USER => self.parse_alter_user().map(Into::into),
             // unreachable because expect_one_of_keywords used above
-            unexpected_keyword => Err(ParserError::ParserError(
-                format!("Internal parser error: expected any of {{TEXT SEARCH, VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"),
-            )),
+            unexpected_keyword => parser_err!(format!("Internal parser error: expected any of {{TEXT SEARCH, VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), self.get_current_token().span),
         }
     }
 
@@ -11856,9 +11858,7 @@
                     Keyword::MERGES => {
                         options.push(OperatorOption::Merges);
                     }
-                    unexpected_keyword => return Err(ParserError::ParserError(
-                        format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in operator option"),
-                    )),
+                    unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in operator option"), self.get_current_token().span),
                 }
 
                 if !self.consume_token(&Token::Comma) {
@@ -12126,7 +12126,7 @@
                 Expr::Function(f) => Ok(Statement::Call(f)),
                 other => parser_err!(
                     format!("Expected a simple procedure call but found: {other}"),
-                    self.peek_token_ref().span.start
+                    self.peek_token_ref().span
                 ),
             }
         } else {
@@ -12166,9 +12166,10 @@
             // Use a separate if statement to prevent Rust compiler from complaining about
             // "if statement in this position is unstable: https://github.com/rust-lang/rust/issues/53667"
             if let CopySource::Query(_) = source {
-                return Err(ParserError::ParserError(
-                    "COPY ... FROM does not support query as a source".to_string(),
-                ));
+                return parser_err!(
+                    "COPY ... FROM does not support query as a source",
+                    self.get_current_token().span
+                );
             }
         }
         let target = if self.parse_keyword(Keyword::STDIN) {
@@ -12539,10 +12540,7 @@
     fn parse_literal_char(&mut self) -> Result<char, ParserError> {
         let s = self.parse_literal_string()?;
         if s.len() != 1 {
-            let loc = self
-                .tokens
-                .get(self.index - 1)
-                .map_or(Location { line: 0, column: 0 }, |t| t.span.start);
+            let loc = self.get_current_token().span;
             return parser_err!(format!("Expect a char, found {s:?}"), loc);
         }
         Ok(s.chars().next().unwrap())
@@ -12620,7 +12618,7 @@
             // The call to n.parse() returns a bigdecimal when the
             // bigdecimal feature is enabled, and is otherwise a no-op
             // (i.e., it returns the input string).
-            Token::Number(n, l) => ok_value(Value::Number(Self::parse(n, span.start)?, l)),
+            Token::Number(n, l) => ok_value(Value::Number(Self::parse(n, span)?, l)),
             Token::SingleQuotedString(ref s) => ok_value(Value::SingleQuotedString(
                 self.maybe_concat_string_literal(s.to_string()),
             )),
@@ -12683,7 +12681,10 @@
                 // 2. Not calling self.next_token() to enforce `tok`
                 //    be followed immediately by a word/number, ie.
                 //    without any whitespace in between
-                let next_token = self.next_token_no_skip().unwrap_or(&EOF_TOKEN).clone();
+                let next_token = match self.next_token_no_skip() {
+                    Some(token) => token.clone(),
+                    None => self.eof_token.clone(),
+                };
                 let ident = match next_token.token {
                     Token::Word(w) if w.quote_style.is_none() => Ok(w.into_ident(next_token.span)),
                     Token::Number(w, false) => Ok(Ident::with_span(next_token.span, w)),
@@ -12801,7 +12802,7 @@
     pub fn parse_literal_uint(&mut self) -> Result<u64, ParserError> {
         let next_token = self.next_token();
         match next_token.token {
-            Token::Number(s, _) => Self::parse::<u64>(s, next_token.span.start),
+            Token::Number(s, _) => Self::parse::<u64>(s, next_token.span),
             _ => self.expected("literal int", next_token),
         }
     }
@@ -12950,7 +12951,7 @@
         if trailing_bracket.0 {
             return parser_err!(
                 format!("unmatched > after parsing data type {ty}"),
-                self.peek_token_ref()
+                self.peek_token_ref().span
             );
         }
 
@@ -12961,7 +12962,7 @@
     fn parse_data_type_helper(
         &mut self,
     ) -> Result<(DataType, MatchedTrailingBracket), ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
 
         let dialect = self.dialect;
         self.advance_token();
@@ -13356,7 +13357,7 @@
                     if key_trailing_bracket.0 {
                         return parser_err!(
                             format!("unmatched > after parsing data type {key_data_type}"),
-                            self.peek_token_ref()
+                            self.peek_token_ref().span
                         );
                     }
                     self.expect_token(&Token::Comma)?;
@@ -13581,12 +13582,14 @@
         &mut self,
         operator_name: &str,
     ) -> Result<SetQuantifier, ParserError> {
+        let span = self.peek_token_ref().span;
         let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect));
         match quantifier {
             SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier),
-            _ => Err(ParserError::ParserError(format!(
-                "{operator_name} pipe operator requires DISTINCT modifier",
-            ))),
+            _ => parser_err!(
+                format!("{operator_name} pipe operator requires DISTINCT modifier"),
+                span
+            ),
         }
     }
 
@@ -13780,7 +13783,7 @@
                         _ => {
                             return parser_err!(
                                 "BUG: expected to match GroupBy modifier keyword",
-                                self.peek_token_ref().span.start
+                                self.peek_token_ref().span
                             )
                         }
                     });
@@ -13853,11 +13856,12 @@
                 && expr.is_some() // ALL not supported with comma
                 && self.consume_token(&Token::Comma)
             {
-                let offset = expr.ok_or_else(|| {
-                    ParserError::ParserError(
-                        "Missing offset for LIMIT <offset>, <limit>".to_string(),
-                    )
-                })?;
+                let Some(offset) = expr else {
+                    return parser_err!(
+                        "Missing offset for LIMIT <offset>, <limit>",
+                        self.get_current_token().span
+                    );
+                };
                 return Ok(Some(LimitClause::OffsetCommaLimit {
                     offset,
                     limit: self.parse_expr()?,
@@ -14065,7 +14069,7 @@
     /// let actual = parser.parse_multipart_identifier().unwrap_err();
     /// assert_eq!(
     ///     actual.to_string(),
-    ///     "sql parser error: Unexpected token in identifier: +"
+    ///     "sql parser error: Unexpected token in identifier: + at Line: 1, Column: 5"
     /// );
     /// ```
     ///
@@ -14078,14 +14082,13 @@
         match next_token.token {
             Token::Word(w) => idents.push(w.into_ident(next_token.span)),
             Token::EOF => {
-                return Err(ParserError::ParserError(
-                    "Empty input when parsing identifier".to_string(),
-                ))?
+                return parser_err!("Empty input when parsing identifier", next_token.span)
             }
             token => {
-                return Err(ParserError::ParserError(format!(
-                    "Unexpected token in identifier: {token}"
-                )))?
+                return parser_err!(
+                    format!("Unexpected token in identifier: {token}"),
+                    next_token.span
+                )
             }
         };
 
@@ -14098,22 +14101,22 @@
                     match next_token.token {
                         Token::Word(w) => idents.push(w.into_ident(next_token.span)),
                         Token::EOF => {
-                            return Err(ParserError::ParserError(
-                                "Trailing period in identifier".to_string(),
-                            ))?
+                            return parser_err!("Trailing period in identifier", next_token.span)
                         }
                         token => {
-                            return Err(ParserError::ParserError(format!(
-                                "Unexpected token following period in identifier: {token}"
-                            )))?
+                            return parser_err!(
+                                format!("Unexpected token following period in identifier: {token}"),
+                                next_token.span
+                            )
                         }
                     }
                 }
                 Token::EOF => break,
                 token => {
-                    Err(ParserError::ParserError(format!(
-                        "Unexpected token in identifier: {token}"
-                    )))?;
+                    return parser_err!(
+                        format!("Unexpected token in identifier: {token}"),
+                        self.get_current_token().span
+                    );
                 }
             }
         }
@@ -14574,9 +14577,9 @@
         match &current_token.token {
             Token::Number(s, _) => {
                 let s = s.clone();
-                let span_start = current_token.span.start;
+                let span = current_token.span;
                 self.advance_token();
-                let value = Self::parse::<i64>(s, span_start)?;
+                let value = Self::parse::<i64>(s, span)?;
                 Ok(if is_negative { -value } else { value })
             }
             _ => self.expected_ref("number", current_token),
@@ -14787,10 +14790,11 @@
             }
         }
 
+        let statement_span = self.peek_token_ref().span;
         match self.maybe_parse(|parser| parser.parse_statement())? {
-            Some(Statement::Explain { .. }) | Some(Statement::ExplainTable { .. }) => Err(
-                ParserError::ParserError("Explain must be root of the plan".to_string()),
-            ),
+            Some(Statement::Explain { .. }) | Some(Statement::ExplainTable { .. }) => {
+                parser_err!("Explain must be root of the plan", statement_span)
+            }
             Some(statement) => Ok(Statement::Explain {
                 describe_alias,
                 analyze,
@@ -14833,7 +14837,7 @@
     /// expect the initial keyword to be already consumed
     #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
     pub fn parse_query(&mut self) -> Result<Box<Query>, ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
         let with = if self.parse_keyword(Keyword::WITH) {
             let with_token = self.get_current_token();
             Some(With {
@@ -15100,9 +15104,10 @@
                         let alias = self.parse_identifier_optional_alias()?;
                         pipe_operators.push(PipeOperator::Call { function, alias });
                     } else {
-                        return Err(ParserError::ParserError(
-                            "Expected function call after CALL".to_string(),
-                        ));
+                        return parser_err!(
+                            "Expected function call after CALL",
+                            function_expr.span()
+                        );
                     }
                 }
                 Keyword::PIVOT => {
@@ -15169,19 +15174,19 @@
                 | Keyword::FULL
                 | Keyword::CROSS => {
                     self.prev_token();
+                    let join_span = self.peek_token_ref().span;
                     let mut joins = self.parse_joins()?;
                     if joins.len() != 1 {
-                        return Err(ParserError::ParserError(
-                            "Join pipe operator must have a single join".to_string(),
-                        ));
+                        return parser_err!("Join pipe operator must have a single join", join_span);
                     }
                     let join = joins.swap_remove(0);
                     pipe_operators.push(PipeOperator::Join(join))
                 }
                 unhandled => {
-                    return Err(ParserError::ParserError(format!(
-                    "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"
-                )))
+                    return parser_err!(
+                        format!("`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"),
+                        self.get_current_token().span
+                    )
                 }
             }
         }
@@ -15240,9 +15245,10 @@
             }
             ForXml::Path(element_name)
         } else {
-            return Err(ParserError::ParserError(
-                "Expected FOR XML [RAW | AUTO | EXPLICIT | PATH ]".to_string(),
-            ));
+            return self.expected_ref(
+                "RAW, AUTO, EXPLICIT or PATH after FOR XML",
+                self.peek_token_ref(),
+            );
         };
         let mut elements = false;
         let mut binary_base64 = false;
@@ -15279,9 +15285,7 @@
         } else if self.parse_keyword(Keyword::PATH) {
             ForJson::Path
         } else {
-            return Err(ParserError::ParserError(
-                "Expected FOR JSON [AUTO | PATH ]".to_string(),
-            ));
+            return self.expected_ref("AUTO or PATH after FOR JSON", self.peek_token_ref());
         };
         let mut root = None;
         let mut include_null_values = false;
@@ -16125,10 +16129,13 @@
                 return if assignments.len() > 1 {
                     Ok(Set::MultipleAssignments { assignments }.into())
                 } else {
-                    let SetAssignment { scope, name, value } =
-                        assignments.into_iter().next().ok_or_else(|| {
-                            ParserError::ParserError("Expected at least one assignment".to_string())
-                        })?;
+                    let Some(SetAssignment { scope, name, value }) = assignments.into_iter().next()
+                    else {
+                        return parser_err!(
+                            "Expected at least one assignment",
+                            self.get_current_token().span
+                        );
+                    };
 
                     Ok(Set::SingleAssignment {
                         scope,
@@ -16280,9 +16287,10 @@
         } else if self.parse_keyword(Keyword::PROCESSLIST) {
             Ok(Statement::ShowProcessList { full })
         } else if extended || full {
-            Err(ParserError::ParserError(
-                "EXTENDED/FULL are not supported with this type of SHOW query".to_string(),
-            ))
+            parser_err!(
+                "EXTENDED/FULL are not supported with this type of SHOW query",
+                self.peek_token_ref().span
+            )
         } else if self.parse_one_of_keywords(&[Keyword::CREATE]).is_some() {
             Ok(self.parse_show_create()?)
         } else if self.parse_keyword(Keyword::COLLATION) {
@@ -16374,9 +16382,10 @@
             Keyword::PROCEDURE => Ok(ShowCreateObject::Procedure),
             Keyword::EVENT => Ok(ShowCreateObject::Event),
             Keyword::VIEW => Ok(ShowCreateObject::View),
-            keyword => Err(ParserError::ParserError(format!(
-                "Unable to map keyword to ShowCreateObject: {keyword:?}"
-            ))),
+            keyword => parser_err!(
+                format!("Unable to map keyword to ShowCreateObject: {keyword:?}"),
+                self.get_current_token().span
+            ),
         }?;
 
         let obj_name = self.parse_object_name(false)?;
@@ -16663,9 +16672,10 @@
                                 }
                             }
                             _ => {
-                                return Err(ParserError::ParserError(format!(
-                                    "expected OUTER, SEMI, ANTI or JOIN after {kw:?}"
-                                )))
+                                return self.expected_ref(
+                                    &format!("OUTER, SEMI, ANTI or JOIN after {kw:?}"),
+                                    self.peek_token_ref(),
+                                )
                             }
                         }
                     }
@@ -16742,7 +16752,7 @@
     /// A table name or a parenthesized subquery, followed by optional `[AS] alias`
     #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
     pub fn parse_table_factor(&mut self) -> Result<TableFactor, ParserError> {
-        let _guard = self.recursion_counter.try_decrease()?;
+        let _guard = self.recursion_guard()?;
         if self.parse_keyword(Keyword::LATERAL) {
             // LATERAL must always be followed by a subquery or table function.
             if self.consume_token(&Token::LParen) {
@@ -16816,9 +16826,7 @@
                     table = match kw {
                         Keyword::PIVOT => self.parse_pivot_table_factor(table)?,
                         Keyword::UNPIVOT => self.parse_unpivot_table_factor(table)?,
-                        unexpected_keyword => return Err(ParserError::ParserError(
-                            format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in pivot/unpivot"),
-                        )),
+                        unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in pivot/unpivot"), self.get_current_token().span),
                     }
                 }
                 return Ok(table);
@@ -16880,9 +16888,10 @@
                         | TableFactor::NestedJoin { alias, .. } => {
                             // but not `FROM (mytable AS alias1) AS alias2`.
                             if let Some(inner_alias) = alias {
-                                return Err(ParserError::ParserError(format!(
-                                    "duplicate alias {inner_alias}"
-                                )));
+                                return parser_err!(
+                                    format!("duplicate alias {inner_alias}"),
+                                    outer_alias.name.span
+                                );
                             }
                             // Act as if the alias was specified normally next
                             // to the table name: `(mytable) AS alias` ->
@@ -16890,10 +16899,10 @@
                             alias.replace(outer_alias);
                         }
                         TableFactor::UnpivotExpr { .. } => {
-                            return Err(ParserError::ParserError(
-                                "alias after parenthesized UNPIVOT expression is not supported"
-                                    .to_string(),
-                            ))
+                            return parser_err!(
+                                "alias after parenthesized UNPIVOT expression is not supported",
+                                outer_alias.name.span
+                            )
                         }
                     };
                 }
@@ -17087,9 +17096,7 @@
                 table = match kw {
                     Keyword::PIVOT => self.parse_pivot_table_factor(table)?,
                     Keyword::UNPIVOT => self.parse_unpivot_table_factor(table)?,
-                    unexpected_keyword => return Err(ParserError::ParserError(
-                        format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in pivot/unpivot"),
-                    )),
+                    unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in pivot/unpivot"), self.get_current_token().span),
                 }
             }
 
@@ -17191,7 +17198,7 @@
                     } else {
                         return parser_err!(
                             "Expecting number or byte length e.g. 100M",
-                            self.peek_token_ref().span.start
+                            self.peek_token_ref().span
                         );
                     }
                 }
@@ -17387,30 +17394,34 @@
         while self.peek_token_ref().token != Token::RParen {
             if self.parse_keyword(Keyword::DIMENSIONS) {
                 if !dimensions.is_empty() {
-                    return Err(ParserError::ParserError(
-                        "DIMENSIONS clause can only be specified once".to_string(),
-                    ));
+                    return parser_err!(
+                        "DIMENSIONS clause can only be specified once",
+                        self.get_current_token().span
+                    );
                 }
                 dimensions = self.parse_comma_separated(Parser::parse_wildcard_expr)?;
             } else if self.parse_keyword(Keyword::METRICS) {
                 if !metrics.is_empty() {
-                    return Err(ParserError::ParserError(
-                        "METRICS clause can only be specified once".to_string(),
-                    ));
+                    return parser_err!(
+                        "METRICS clause can only be specified once",
+                        self.get_current_token().span
+                    );
                 }
                 metrics = self.parse_comma_separated(Parser::parse_wildcard_expr)?;
             } else if self.parse_keyword(Keyword::FACTS) {
                 if !facts.is_empty() {
-                    return Err(ParserError::ParserError(
-                        "FACTS clause can only be specified once".to_string(),
-                    ));
+                    return parser_err!(
+                        "FACTS clause can only be specified once",
+                        self.get_current_token().span
+                    );
                 }
                 facts = self.parse_comma_separated(Parser::parse_wildcard_expr)?;
             } else if self.parse_keyword(Keyword::WHERE) {
                 if where_clause.is_some() {
-                    return Err(ParserError::ParserError(
-                        "WHERE clause can only be specified once".to_string(),
-                    ));
+                    return parser_err!(
+                        "WHERE clause can only be specified once",
+                        self.get_current_token().span
+                    );
                 }
                 where_clause = Some(self.parse_expr()?);
             } else {
@@ -17420,7 +17431,7 @@
                         "Expected one of DIMENSIONS, METRICS, FACTS or WHERE, got {}",
                         tok.token
                     ),
-                    tok.span.start
+                    tok.span
                 )?;
             }
         }
@@ -17600,7 +17611,7 @@
                             return self.expected("literal number", next_token);
                         };
                         self.expect_token(&Token::RBrace)?;
-                        RepetitionQuantifier::AtMost(Self::parse(n, token.span.start)?)
+                        RepetitionQuantifier::AtMost(Self::parse(n, token.span)?)
                     }
                     Token::Number(n, _) if self.consume_token(&Token::Comma) => {
                         let next_token = self.next_token();
@@ -17608,19 +17619,19 @@
                             Token::Number(m, _) => {
                                 self.expect_token(&Token::RBrace)?;
                                 RepetitionQuantifier::Range(
-                                    Self::parse(n, token.span.start)?,
-                                    Self::parse(m, token.span.start)?,
+                                    Self::parse(n, token.span)?,
+                                    Self::parse(m, token.span)?,
                                 )
                             }
                             Token::RBrace => {
-                                RepetitionQuantifier::AtLeast(Self::parse(n, token.span.start)?)
+                                RepetitionQuantifier::AtLeast(Self::parse(n, token.span)?)
                             }
                             _ => return self.expected("} or upper bound", next_token),
                         }
                     }
                     Token::Number(n, _) => {
                         self.expect_token(&Token::RBrace)?;
-                        RepetitionQuantifier::Exactly(Self::parse(n, token.span.start)?)
+                        RepetitionQuantifier::Exactly(Self::parse(n, token.span)?)
                     }
                     _ => return self.expected("quantifier range", token),
                 }
@@ -18311,9 +18322,7 @@
                         }
                     }
                     Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)),
-                    Some(unexpected_keyword) => return Err(ParserError::ParserError(
-                        format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in grant objects"),
-                    )),
+                    Some(unexpected_keyword) => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in grant objects"), self.get_current_token().span),
                 }
             }
         } else {
@@ -18626,7 +18635,7 @@
             None => {
                 return parser_err!(
                     "DENY statements must specify an object",
-                    self.peek_token_ref().span.start
+                    self.peek_token_ref().span
                 )
             }
         };
@@ -18683,10 +18692,7 @@
         replace_token: TokenWithSpan,
     ) -> Result<Statement, ParserError> {
         if !dialect_of!(self is MySqlDialect | GenericDialect) {
-            return parser_err!(
-                "Unsupported statement REPLACE",
-                self.peek_token_ref().span.start
-            );
+            return parser_err!("Unsupported statement REPLACE", self.peek_token_ref().span);
         }
 
         let mut insert = self.parse_insert(replace_token)?;
@@ -19373,9 +19379,7 @@
             let kind = match self.expect_one_of_keywords(&[Keyword::MIN, Keyword::MAX])? {
                 Keyword::MIN => HavingBoundKind::Min,
                 Keyword::MAX => HavingBoundKind::Max,
-                unexpected_keyword => return Err(ParserError::ParserError(
-                    format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in having bound"),
-                )),
+                unexpected_keyword => return parser_err!(format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in having bound"), self.get_current_token().span),
             };
             clauses.push(FunctionArgumentClause::Having(HavingBound(
                 kind,
@@ -19433,7 +19437,7 @@
     }
 
     fn parse_duplicate_treatment(&mut self) -> Result<Option<DuplicateTreatment>, ParserError> {
-        let loc = self.peek_token_ref().span.start;
+        let loc = self.peek_token_ref().span;
         match (
             self.parse_keyword(Keyword::ALL),
             self.parse_keyword(Keyword::DISTINCT),
@@ -19465,7 +19469,7 @@
             Expr::Identifier(v) if v.value.to_lowercase() == "from" && v.quote_style.is_none() => {
                 parser_err!(
                     format!("Expected an expression, found: {}", v),
-                    self.peek_token_ref().span.start
+                    self.peek_token_ref().span
                 )
             }
             Expr::BinaryOp {
@@ -19479,13 +19483,13 @@
                 {
                     return parser_err!(
                         format!("Expected an expression, found: {right}"),
-                        self.peek_token_ref().span.start
+                        self.peek_token_ref().span
                     );
                 }
                 let Expr::Identifier(alias) = *left else {
                     return parser_err!(
                         "BUG: expected identifier expression as alias",
-                        self.peek_token_ref().span.start
+                        self.peek_token_ref().span
                     );
                 };
                 Ok(SelectItem::ExprWithAlias {
@@ -19894,7 +19898,7 @@
         } else {
             let next_token = self.next_token();
             let quantity = match next_token.token {
-                Token::Number(s, _) => Self::parse::<u64>(s, next_token.span.start)?,
+                Token::Number(s, _) => Self::parse::<u64>(s, next_token.span)?,
                 _ => self.expected("literal int", next_token)?,
             };
             Some(TopQuantity::Constant(quantity))
@@ -19967,9 +19971,7 @@
         let lock_type = match self.expect_one_of_keywords(&[Keyword::UPDATE, Keyword::SHARE])? {
             Keyword::UPDATE => LockType::Update,
             Keyword::SHARE => LockType::Share,
-            unexpected_keyword => return Err(ParserError::ParserError(
-                format!("Internal parser error: expected any of {{UPDATE, SHARE}}, got {unexpected_keyword:?}"),
-            )),
+            unexpected_keyword => return parser_err!(format!("Internal parser error: expected any of {{UPDATE, SHARE}}, got {unexpected_keyword:?}"), self.get_current_token().span),
         };
         let of = if self.parse_keyword(Keyword::OF) {
             Some(self.parse_object_name(false)?)
@@ -21186,11 +21188,12 @@
             Some(Keyword::CATEGORY) => {
                 self.expect_token(&Token::Eq)?;
                 let category_str = self.parse_literal_string()?;
-                let category_char = category_str.chars().next().ok_or_else(|| {
-                    ParserError::ParserError(
-                        "CATEGORY value must be a single character".to_string(),
-                    )
-                })?;
+                let Some(category_char) = category_str.chars().next() else {
+                    return parser_err!(
+                        "CATEGORY value must be a single character",
+                        self.get_current_token().span
+                    );
+                };
                 Ok(UserDefinedTypeSqlDefinitionOption::Category(category_char))
             }
             Some(Keyword::PREFERRED) => {
@@ -22245,22 +22248,21 @@
         let ast = Parser::parse_sql(&GenericDialect, sql);
         assert_eq!(
             ast,
-            Err(ParserError::TokenizerError(
-                "Unterminated string literal at Line: 1, Column: 5".to_string()
-            ))
+            Err(ParserError::TokenizerError {
+                message: "Unterminated string literal".to_string(),
+                span: Span::new(Location::new(1, 5), Location::new(1, 5)),
+            })
         );
     }
 
     #[test]
     fn test_parser_error_loc() {
         let sql = "SELECT this is a syntax error";
-        let ParserError::ParserError(msg) = Parser::parse_sql(&GenericDialect, sql).unwrap_err()
-        else {
-            panic!("expected ParserError::ParserError");
-        };
+        let err = Parser::parse_sql(&GenericDialect, sql).unwrap_err();
+        assert!(matches!(err, ParserError::ParserError { .. }));
         assert!(
-            msg.ends_with("found: a at Line: 1, Column: 16"),
-            "unexpected error message: {msg}"
+            err.to_string().ends_with("found: a at Line: 1, Column: 16"),
+            "unexpected error message: {err}"
         );
     }
 
@@ -22270,9 +22272,10 @@
         let ast = Parser::parse_sql(&GenericDialect, sql);
         assert_eq!(
             ast,
-            Err(ParserError::ParserError(
-                "Explain must be root of the plan".to_string()
-            ))
+            Err(ParserError::ParserError {
+                message: "Explain must be root of the plan".to_string(),
+                span: Span::new(Location::new(1, 9), Location::new(1, 16)),
+            })
         );
     }
 
@@ -22335,27 +22338,27 @@
 
         test_parse_multipart_identifier_error!(
             "",
-            "sql parser error: Empty input when parsing identifier",
+            "sql parser error: Empty input when parsing identifier at Line: 1, Column: 1",
         );
 
         test_parse_multipart_identifier_error!(
             "*schema.table",
-            "sql parser error: Unexpected token in identifier: *",
+            "sql parser error: Unexpected token in identifier: * at Line: 1, Column: 1",
         );
 
         test_parse_multipart_identifier_error!(
             "schema.table*",
-            "sql parser error: Unexpected token in identifier: *",
+            "sql parser error: Unexpected token in identifier: * at Line: 1, Column: 13",
         );
 
         test_parse_multipart_identifier_error!(
             "schema.table.",
-            "sql parser error: Trailing period in identifier",
+            "sql parser error: Trailing period in identifier at Line: 1, Column: 14",
         );
 
         test_parse_multipart_identifier_error!(
             "schema.*",
-            "sql parser error: Unexpected token following period in identifier: *",
+            "sql parser error: Unexpected token following period in identifier: * at Line: 1, Column: 8",
         );
     }
 
@@ -22389,9 +22392,10 @@
         let sql = "REPLACE INTO t (a) VALUES (&a)";
 
         assert_eq!(
-            ParserError::ParserError(
-                "Expected: an expression, found: & at Line: 1, Column: 28".to_string()
-            ),
+            ParserError::ParserError {
+                message: "Expected: an expression, found: &".to_string(),
+                span: Span::new(Location::new(1, 28), Location::new(1, 29)),
+            },
             Parser::parse_sql(&GenericDialect {}, sql).unwrap_err()
         );
     }
@@ -22401,10 +22405,11 @@
         let sql = "REPLACE INTO t SET ?";
 
         assert_eq!(
-            ParserError::ParserError(
-                "Expected: SELECT, VALUES, or a subquery in the query body, found: SET at Line: 1, Column: 16"
-                    .to_string()
-            ),
+            ParserError::ParserError {
+                message: "Expected: SELECT, VALUES, or a subquery in the query body, found: SET"
+                    .to_string(),
+                span: Span::new(Location::new(1, 16), Location::new(1, 19)),
+            },
             Parser::parse_sql(&GenericDialect {}, sql).unwrap_err()
         );
     }
@@ -22414,26 +22419,30 @@
         let sql = r#"REPLACE"#;
 
         assert_eq!(
-            ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+            ParserError::ParserError {
+                message: "Expected: identifier, found: EOF".to_string(),
+                span: Span::new(Location::new(1, 8), Location::new(1, 8)),
+            },
             Parser::parse_sql(&MySqlDialect {}, sql).unwrap_err()
         );
     }
 
     #[test]
     fn test_placeholder_invalid_whitespace() {
-        for (w, expected) in [
-            (
-                "  ",
-                "Expected: placeholder, found:   at Line: 3, Column: 4",
-            ),
+        for (w, expected, end_column) in [
+            ("  ", "Expected: placeholder, found:  ", 5),
             (
                 "/*invalid*/",
-                "Expected: placeholder, found: /*invalid*/ at Line: 3, Column: 4",
+                "Expected: placeholder, found: /*invalid*/",
+                15,
             ),
         ] {
             let sql = format!("\nSELECT\n  :{w}fooBar");
             assert_eq!(
-                ParserError::ParserError(expected.to_string()),
+                ParserError::ParserError {
+                    message: expected.to_string(),
+                    span: Span::new(Location::new(3, 4), Location::new(3, end_column)),
+                },
                 Parser::parse_sql(&GenericDialect, &sql).unwrap_err()
             );
         }
diff --git a/src/test_utils.rs b/src/test_utils.rs
index c4d1d0d..14c35e8 100644
--- a/src/test_utils.rs
+++ b/src/test_utils.rs
@@ -33,7 +33,7 @@
 
 use crate::dialect::*;
 use crate::parser::{Parser, ParserError};
-use crate::tokenizer::{Token, Tokenizer};
+use crate::tokenizer::{Span, Token, Tokenizer};
 use crate::{ast::*, parser::ParserOptions};
 
 #[cfg(test)]
@@ -135,6 +135,18 @@
         // Parser::parse_sql(&**self.dialects.first().unwrap(), sql)
     }
 
+    /// Parses statements while preserving source locations.
+    pub fn parse_sql_statements_with_locations(
+        &self,
+        sql: &str,
+    ) -> Result<Vec<Statement>, ParserError> {
+        self.one_of_identical_results(|dialect| {
+            self.new_parser(dialect)
+                .try_with_sql(sql)?
+                .parse_statements()
+        })
+    }
+
     /// Ensures that `sql` parses as a single [Statement] for all tested
     /// dialects.
     ///
@@ -370,6 +382,14 @@
     Value::SingleQuotedString(s.into())
 }
 
+/// Creates a parser error without a source location.
+pub fn parser_error(message: impl Into<String>) -> ParserError {
+    ParserError::ParserError {
+        message: message.into(),
+        span: Span::empty(),
+    }
+}
+
 pub fn table_alias(explicit: bool, name: impl Into<String>) -> Option<TableAlias> {
     Some(TableAlias {
         explicit,
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
index 4e76be8..3906a28 100644
--- a/src/tokenizer.rs
+++ b/src/tokenizer.rs
@@ -777,7 +777,8 @@
 }
 
 /// An error reported by the tokenizer, with a human-readable `message` and a `location`.
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, thiserror::Error)]
+#[error("{message}{location}")]
 pub struct TokenizerError {
     /// A descriptive error message.
     pub message: String,
@@ -785,14 +786,6 @@
     pub location: Location,
 }
 
-impl fmt::Display for TokenizerError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}{}", self.message, self.location,)
-    }
-}
-
-impl core::error::Error for TokenizerError {}
-
 struct State<'a> {
     peekable: Peekable<Chars<'a>>,
     line: u64,
diff --git a/tests/no_std_recursion.rs b/tests/no_std_recursion.rs
index e69e767..13dd9d3 100644
--- a/tests/no_std_recursion.rs
+++ b/tests/no_std_recursion.rs
@@ -28,7 +28,10 @@
         .unwrap()
         .parse_statements();
 
-    assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        result,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -42,7 +45,10 @@
 
     let result = Parser::parse_sql(&dialect, &sql);
 
-    assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        result,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -55,10 +61,8 @@
     // Deep nesting errors with a generic "expected end of statement" error
     // rather than `RecursionLimitExceeded`, instead of overflowing the stack.
     assert_eq!(
-        result,
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: NOT at Line: 1, Column: 211".to_string()
-        ))
+        result.unwrap_err().to_string(),
+        "sql parser error: Expected: end of statement, found: NOT at Line: 1, Column: 211"
     );
 }
 
@@ -91,7 +95,10 @@
 
     let result = Parser::parse_sql(&dialect, &sql);
 
-    assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        result,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -105,7 +112,10 @@
 
     let result = Parser::parse_sql(&dialect, &sql);
 
-    assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        result,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -115,5 +125,8 @@
 
     let result = Parser::parse_sql(&dialect, &sql);
 
-    assert_eq!(result, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        result,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index daebe63..38207a3 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -24,7 +24,7 @@
 use sqlparser::ast::*;
 use sqlparser::dialect::{BigQueryDialect, GenericDialect};
 use sqlparser::keywords::Keyword;
-use sqlparser::parser::{ParserError, ParserOptions};
+use sqlparser::parser::ParserOptions;
 use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word};
 use test_utils::*;
 
@@ -295,13 +295,13 @@
         bigquery()
             .parse_sql_statements("BEGIN SELECT 1; SELECT 2 END")
             .unwrap_err(),
-        ParserError::ParserError("Expected: ;, found: END".to_string())
+        parser_error("Expected: ;, found: END".to_string())
     );
     assert_eq!(
         bigquery()
             .parse_sql_statements("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2 END")
             .unwrap_err(),
-        ParserError::ParserError("Expected: ;, found: END".to_string())
+        parser_error("Expected: ;, found: END".to_string())
     );
 }
 
@@ -1579,7 +1579,7 @@
         let sql = format!("SELECT 1 FROM {ident}");
         assert_eq!(
             bigquery().parse_sql_statements(&sql),
-            Err(ParserError::ParserError(expected.to_string()))
+            Err(parser_error(expected.to_string()))
         );
     }
 
@@ -1776,8 +1776,8 @@
     let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string();
     assert_eq!(
         bigquery().parse_sql_statements(&sql),
-        Err(ParserError::ParserError(
-            "Expected: one of UPDATE or SHARE, found: SYSTEM".to_string()
+        Err(parser_error(
+            "Expected: one of UPDATE or SHARE, found: SYSTEM"
         ))
     );
 }
@@ -2084,10 +2084,7 @@
         ),
     ] {
         let res = dialects.parse_sql_statements(sql);
-        assert_eq!(
-            ParserError::ParserError(err_msg.to_string()),
-            res.unwrap_err()
-        );
+        assert_eq!(parser_error(err_msg.to_string()), res.unwrap_err());
     }
 }
 
@@ -2209,13 +2206,13 @@
 
     let error_sql = "DECLARE x";
     assert_eq!(
-        ParserError::ParserError("Expected: a data type name, found: EOF".to_owned()),
+        parser_error("Expected: a data type name, found: EOF".to_owned()),
         bigquery().parse_sql_statements(error_sql).unwrap_err()
     );
 
     let error_sql = "DECLARE x 42";
     assert_eq!(
-        ParserError::ParserError("Expected: a data type name, found: 42".to_owned()),
+        parser_error("Expected: a data type name, found: 42".to_owned()),
         bigquery().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -2446,7 +2443,7 @@
     ];
     for (sql, error) in error_sqls {
         assert_eq!(
-            ParserError::ParserError(error.to_owned()),
+            parser_error(error.to_owned()),
             bigquery().parse_sql_statements(sql).unwrap_err()
         );
     }
@@ -2476,7 +2473,7 @@
     // missing comma separation
     let error_sql = "SELECT TRIM('xyz' 'a')";
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 'a'".to_owned()),
+        parser_error("Expected: ), found: 'a'".to_owned()),
         bigquery().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -2639,7 +2636,7 @@
 
     // Bad case with missing closing bracket
     assert_eq!(
-        ParserError::ParserError("Expected: >, found: )".to_owned()),
+        parser_error("Expected: >, found: )".to_owned()),
         bigquery()
             .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRING, b INT64)")
             .unwrap_err()
@@ -2647,9 +2644,7 @@
 
     // Bad case with redundant closing bracket
     assert_eq!(
-        ParserError::ParserError(
-            "unmatched > after parsing data type STRUCT<a STRING, b INT64>)".to_owned()
-        ),
+        parser_error("unmatched > after parsing data type STRUCT<a STRING, b INT64>".to_owned()),
         bigquery()
             .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRING, b INT64>>)")
             .unwrap_err()
@@ -2657,9 +2652,7 @@
 
     // Base case with redundant closing bracket in nested struct
     assert_eq!(
-        ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: >".to_owned()
-        ),
+        parser_error("Expected: ',' or ')' after column definition, found: >".to_owned()),
         bigquery()
             .parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRUCT<b INT>>>, c INT64)")
             .unwrap_err()
@@ -2670,7 +2663,7 @@
         bigquery_and_generic()
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError("unmatched > in STRUCT literal".to_string())
+        parser_error("unmatched > in STRUCT literal".to_string())
     );
 
     let sql = "SELECT STRUCT<STRUCT<INT64>>>(NULL)";
@@ -2678,7 +2671,7 @@
         bigquery_and_generic()
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError("Expected: (, found: >".to_string())
+        parser_error("Expected: (, found: >".to_string())
     );
 
     let sql = "CREATE TABLE table (x STRUCT<STRUCT<INT64>>>)";
@@ -2686,9 +2679,7 @@
         bigquery_and_generic()
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: >".to_string()
-        )
+        parser_error("Expected: ',' or ')' after column definition, found: >".to_string())
     );
 }
 
@@ -2995,9 +2986,7 @@
         .parse_sql_statements("SELECT B'unterminated")
         .unwrap_err();
     assert_eq!(
-        ParserError::TokenizerError(
-            "Unterminated string literal at Line: 1, Column: 9".to_string(),
-        ),
-        err
+        err.to_string(),
+        "sql parser error: Unterminated string literal at Line: 1, Column: 9"
     );
 }
diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs
index 189300d..a47aab5 100644
--- a/tests/sqlparser_clickhouse.rs
+++ b/tests/sqlparser_clickhouse.rs
@@ -32,7 +32,6 @@
 use sqlparser::ast::*;
 use sqlparser::dialect::ClickHouseDialect;
 use sqlparser::dialect::GenericDialect;
-use sqlparser::parser::ParserError::ParserError;
 
 #[test]
 fn parse_map_access_expr() {
@@ -350,13 +349,13 @@
             clickhouse_and_generic()
                 .parse_sql_statements(format!("ALTER TABLE t0 {operation} PARTITION").as_str())
                 .unwrap_err(),
-            ParserError("Expected: an expression, found: EOF".to_string())
+            parser_error("Expected: an expression, found: EOF".to_string())
         );
         assert_eq!(
             clickhouse_and_generic()
                 .parse_sql_statements(format!("ALTER TABLE t0 {operation} PART").as_str())
                 .unwrap_err(),
-            ParserError("Expected: an expression, found: EOF".to_string())
+            parser_error("Expected: an expression, found: EOF".to_string())
         );
     }
 }
@@ -419,19 +418,19 @@
         clickhouse_and_generic()
             .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name")
             .unwrap_err(),
-        ParserError("Expected: (, found: EOF".to_string())
+        parser_error("Expected: (, found: EOF".to_string())
     );
     assert_eq!(
         clickhouse_and_generic()
             .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name ()")
             .unwrap_err(),
-        ParserError("Expected: SELECT, found: )".to_string())
+        parser_error("Expected: SELECT, found: )".to_string())
     );
     assert_eq!(
         clickhouse_and_generic()
             .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name (SELECT)")
             .unwrap_err(),
-        ParserError("Expected: an expression, found: )".to_string())
+        parser_error("Expected: an expression, found: )".to_string())
     );
 }
 
@@ -461,7 +460,7 @@
         clickhouse_and_generic()
             .parse_sql_statements("ALTER TABLE t0 DROP PROJECTION")
             .unwrap_err(),
-        ParserError("Expected: identifier, found: EOF".to_string())
+        parser_error("Expected: identifier, found: EOF".to_string())
     );
 }
 
@@ -508,7 +507,7 @@
             clickhouse_and_generic()
                 .parse_sql_statements(format!("ALTER TABLE t0 {keyword} PROJECTION",).as_str())
                 .unwrap_err(),
-            ParserError("Expected: identifier, found: EOF".to_string())
+            parser_error("Expected: identifier, found: EOF".to_string())
         );
 
         assert_eq!(
@@ -517,7 +516,7 @@
                     format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN PARTITION",).as_str()
                 )
                 .unwrap_err(),
-            ParserError("Expected: identifier, found: EOF".to_string())
+            parser_error("Expected: identifier, found: EOF".to_string())
         );
 
         assert_eq!(
@@ -526,7 +525,7 @@
                     format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN",).as_str()
                 )
                 .unwrap_err(),
-            ParserError("Expected: end of statement, found: IN".to_string())
+            parser_error("Expected: end of statement, found: IN".to_string())
         );
     }
 }
@@ -574,19 +573,19 @@
         clickhouse_and_generic()
             .parse_sql_statements("OPTIMIZE TABLE t0 DEDUPLICATE BY")
             .unwrap_err(),
-        ParserError("Expected: an expression, found: EOF".to_string())
+        parser_error("Expected: an expression, found: EOF".to_string())
     );
     assert_eq!(
         clickhouse_and_generic()
             .parse_sql_statements("OPTIMIZE TABLE t0 PARTITION")
             .unwrap_err(),
-        ParserError("Expected: an expression, found: EOF".to_string())
+        parser_error("Expected: an expression, found: EOF".to_string())
     );
     assert_eq!(
         clickhouse_and_generic()
             .parse_sql_statements("OPTIMIZE TABLE t0 PARTITION ID")
             .unwrap_err(),
-        ParserError("Expected: identifier, found: EOF".to_string())
+        parser_error("Expected: identifier, found: EOF".to_string())
     );
 }
 
@@ -785,7 +784,7 @@
         clickhouse()
             .parse_sql_statements("CREATE TABLE t (a ARRAY<INT>)")
             .unwrap_err(),
-        ParserError("Expected: (, found: <".to_string())
+        parser_error("Expected: (, found: <")
     );
 }
 
@@ -1135,7 +1134,7 @@
             clickhouse_and_generic()
                 .parse_sql_statements(sql)
                 .unwrap_err(),
-            ParserError(error_msg.to_string())
+            parser_error(error_msg.to_string())
         );
     }
 }
@@ -1640,7 +1639,7 @@
             clickhouse_and_generic()
                 .parse_sql_statements(format!("ALTER TABLE t0 {operation_name} PARTITION").as_str())
                 .unwrap_err(),
-            ParserError("Expected: an expression, found: EOF".to_string())
+            parser_error("Expected: an expression, found: EOF".to_string())
         );
         assert_eq!(
             clickhouse_and_generic()
@@ -1648,7 +1647,7 @@
                     format!("ALTER TABLE t0 {operation_name} PARTITION p0 WITH").as_str()
                 )
                 .unwrap_err(),
-            ParserError("Expected: NAME, found: EOF".to_string())
+            parser_error("Expected: NAME, found: EOF".to_string())
         );
         assert_eq!(
             clickhouse_and_generic()
@@ -1656,7 +1655,7 @@
                     format!("ALTER TABLE t0 {operation_name} PARTITION p0 WITH NAME").as_str()
                 )
                 .unwrap_err(),
-            ParserError("Expected: identifier, found: EOF".to_string())
+            parser_error("Expected: identifier, found: EOF".to_string())
         );
     }
 }
@@ -1914,7 +1913,7 @@
 
     assert_eq!(
         clickhouse().parse_sql_statements("ALTER TABLE t ADD COLUMN c UInt8 AFTER"),
-        Err(ParserError("Expected: identifier, found: EOF".to_string()))
+        Err(parser_error("Expected: identifier, found: EOF".to_string()))
     );
 }
 
@@ -1967,7 +1966,7 @@
     ] {
         assert_eq!(
             clickhouse_and_generic().parse_sql_statements(sql),
-            Err(ParserError(err.to_string())),
+            Err(parser_error(err.to_string())),
             "{sql}"
         );
     }
@@ -2091,13 +2090,13 @@
 
     assert_eq!(
         clickhouse().parse_sql_statements("SELECT t."),
-        Err(ParserError(
+        Err(parser_error(
             "Expected: an identifier or a '*' after '.', found: EOF".to_string()
         ))
     );
     assert_eq!(
         clickhouse().parse_sql_statements("SELECT t.1."),
-        Err(ParserError("Expected: identifier, found: EOF".to_string()))
+        Err(parser_error("Expected: identifier, found: EOF".to_string()))
     );
 
     all_dialects_where(|d| !d.supports_tuple_element_access()).verified_stmt("SELECT (1, 'a') . 1");
@@ -2110,7 +2109,7 @@
                 !d.supports_tuple_element_access() && d.supports_numeric_prefix() == numeric_prefix
             })
             .parse_sql_statements("SELECT t.1 FROM t"),
-            Err(ParserError(err.to_string()))
+            Err(parser_error(err.to_string()))
         );
     }
 }
@@ -2160,7 +2159,7 @@
 
     assert_eq!(
         clickhouse().parse_sql_statements("SELECT * FROM t WHERE x GLOBAL y"),
-        Err(ParserError(
+        Err(parser_error(
             "Expected: end of statement, found: GLOBAL".to_string()
         ))
     );
@@ -2168,7 +2167,7 @@
     assert_eq!(
         all_dialects_where(|d| !d.supports_global_in())
             .parse_sql_statements("SELECT * FROM t WHERE x GLOBAL IN (1)"),
-        Err(ParserError(
+        Err(parser_error(
             "Expected: end of statement, found: GLOBAL".to_string()
         ))
     );
@@ -2294,14 +2293,14 @@
             clickhouse_and_generic()
                 .parse_sql_statements(sql)
                 .unwrap_err(),
-            ParserError(err.to_string())
+            parser_error(err.to_string())
         );
     }
     assert_eq!(
         all_dialects_where(|d| !d.supports_settings())
             .parse_sql_statements("DELETE FROM hits WHERE x = 1 SETTINGS a = 1")
             .unwrap_err(),
-        ParserError("Expected: end of statement, found: SETTINGS".to_string())
+        parser_error("Expected: end of statement, found: SETTINGS".to_string())
     );
 }
 
@@ -2355,14 +2354,14 @@
             clickhouse_and_generic()
                 .parse_sql_statements(sql)
                 .unwrap_err(),
-            ParserError(err.to_string())
+            parser_error(err.to_string())
         );
     }
     assert_eq!(
         all_dialects_where(|d| !d.supports_settings())
             .parse_sql_statements("UPDATE hits SET x = 1 WHERE y = 2 SETTINGS a = 1")
             .unwrap_err(),
-        ParserError("Expected: end of statement, found: SETTINGS".to_string())
+        parser_error("Expected: end of statement, found: SETTINGS".to_string())
     );
 }
 
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 4d51989..21f1a99 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -42,8 +42,8 @@
 use sqlparser::tokenizer::{Location, Span, Token, Tokenizer, Whitespace};
 use test_utils::{
     all_dialects, all_dialects_where, all_dialects_with_options, alter_table_op, assert_eq_vec,
-    call, expr_from_projection, join, number, only, table, table_alias, table_from_name,
-    TestedDialects,
+    call, expr_from_projection, join, number, only, parser_error, table, table_alias,
+    table_from_name, TestedDialects,
 };
 
 #[macro_use]
@@ -206,8 +206,8 @@
     let sql = "REPLACE INTO public.customer (id, name, active) VALUES (1, 2, 3)";
 
     assert_eq!(
-        ParserError::ParserError("Unsupported statement REPLACE at Line: 1, Column: 9".to_string()),
-        Parser::parse_sql(&dialect, sql,).unwrap_err(),
+        Parser::parse_sql(&dialect, sql).unwrap_err().to_string(),
+        "sql parser error: Unsupported statement REPLACE at Line: 1, Column: 9",
     )
 }
 
@@ -298,7 +298,7 @@
 
     let insert_with_columns_and_default_values = "INSERT INTO test_table (test_col) DEFAULT VALUES";
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: DEFAULT".to_string()
         ),
         parse_sql_statements(insert_with_columns_and_default_values).unwrap_err()
@@ -307,20 +307,20 @@
     let insert_with_default_values_and_hive_after_columns =
         "INSERT INTO test_table DEFAULT VALUES (some_column)";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: (".to_string()),
+        parser_error("Expected: end of statement, found: (".to_string()),
         parse_sql_statements(insert_with_default_values_and_hive_after_columns).unwrap_err()
     );
 
     let insert_with_default_values_and_hive_partition =
         "INSERT INTO test_table DEFAULT VALUES PARTITION (some_column)";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: PARTITION".to_string()),
+        parser_error("Expected: end of statement, found: PARTITION".to_string()),
         parse_sql_statements(insert_with_default_values_and_hive_partition).unwrap_err()
     );
 
     let insert_with_default_values_and_values_list = "INSERT INTO test_table DEFAULT VALUES (1)";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: (".to_string()),
+        parser_error("Expected: end of statement, found: (".to_string()),
         parse_sql_statements(insert_with_default_values_and_values_list).unwrap_err()
     );
 }
@@ -453,14 +453,14 @@
     let sql = "UPDATE t WHERE 1";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: SET, found: WHERE".to_string()),
+        parser_error("Expected: SET, found: WHERE".to_string()),
         res.unwrap_err()
     );
 
     let sql = "UPDATE t SET a = 1 extrabadstuff";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: extrabadstuff".to_string()),
+        parser_error("Expected: end of statement, found: extrabadstuff".to_string()),
         res.unwrap_err()
     );
 }
@@ -723,9 +723,10 @@
         parser.parse_object_name(false)
     });
     assert_eq!(
-        ParserError::ParserError(
-            "Expected: identifier, found: . at Line: 1, Column: 11".to_string()
-        ),
+        ParserError::ParserError {
+            message: "Expected: identifier, found: .".to_string(),
+            span: Span::new(Location::new(1, 11), Location::new(1, 12)),
+        },
         ast.unwrap_err()
     );
 }
@@ -734,7 +735,10 @@
 fn parse_no_table_name() {
     let ast = all_dialects().run_parser_method("", |parser| parser.parse_object_name(false));
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+        ParserError::ParserError {
+            message: "Expected: identifier, found: EOF".to_string(),
+            span: Span::new(Location::new(1, 1), Location::new(1, 1)),
+        },
         ast.unwrap_err()
     );
 }
@@ -765,7 +769,7 @@
     });
     let res = dialects.parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: FROM, found: WHERE".to_string()),
+        parser_error("Expected: FROM, found: WHERE".to_string()),
         res.unwrap_err()
     );
 }
@@ -1080,7 +1084,7 @@
 
     let res = dialects.parse_sql_statements("SELECT 1 FROM T WHERE 1 = 2 (+)");
     assert_eq!(
-        ParserError::ParserError("Expected: column identifier before (+), found: 2".to_string()),
+        parser_error("Expected: column identifier before (+), found: 2".to_string()),
         res.unwrap_err()
     );
 }
@@ -1113,7 +1117,7 @@
 fn parse_select_distinct_missing_paren() {
     let result = parse_sql_statements("SELECT DISTINCT (name, id FROM customer");
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: FROM".to_string()),
+        parser_error("Expected: ), found: FROM".to_string()),
         result.unwrap_err(),
     );
 }
@@ -1127,17 +1131,17 @@
 fn parse_select_all_distinct() {
     let result = parse_sql_statements("SELECT ALL DISTINCT name FROM customer");
     assert_eq!(
-        ParserError::ParserError("Cannot specify ALL then DISTINCT".to_string()),
+        parser_error("Cannot specify ALL then DISTINCT".to_string()),
         result.unwrap_err(),
     );
     let result = parse_sql_statements("SELECT DISTINCT ALL name FROM customer");
     assert_eq!(
-        ParserError::ParserError("Cannot specify DISTINCT then ALL".to_string()),
+        parser_error("Cannot specify DISTINCT then ALL".to_string()),
         result.unwrap_err(),
     );
     let result = parse_sql_statements("SELECT ALL DISTINCT ON(name) name FROM customer");
     assert_eq!(
-        ParserError::ParserError("Cannot specify ALL then DISTINCT".to_string()),
+        parser_error("Cannot specify ALL then DISTINCT".to_string()),
         result.unwrap_err(),
     );
 }
@@ -1171,7 +1175,7 @@
     let sql = "SELECT * INTO table0 asdf FROM table1";
     let result = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: asdf".to_string()),
+        parser_error("Expected: end of statement, found: asdf".to_string()),
         result.unwrap_err()
     )
 }
@@ -1211,7 +1215,7 @@
     let sql = "SELECT * + * FROM foo;";
     let result = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: +".to_string()),
+        parser_error("Expected: end of statement, found: +".to_string()),
         result.unwrap_err(),
     );
 }
@@ -1319,7 +1323,7 @@
     // Invalid
     let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: .".to_string()),
+        parser_error("Expected: end of statement, found: .".to_string()),
         res.unwrap_err()
     );
 
@@ -1366,13 +1370,13 @@
 fn test_eof_after_as() {
     let res = parse_sql_statements("SELECT foo AS");
     assert_eq!(
-        ParserError::ParserError("Expected: an identifier after AS, found: EOF".to_string()),
+        parser_error("Expected: an identifier after AS, found: EOF".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("SELECT 1 FROM foo AS");
     assert_eq!(
-        ParserError::ParserError("Expected: an identifier after AS, found: EOF".to_string()),
+        parser_error("Expected: an identifier after AS, found: EOF".to_string()),
         res.unwrap_err()
     );
 }
@@ -1383,7 +1387,7 @@
 
     let res = dialects.parse_sql_statements("ASSERT-URA<<");
     assert_eq!(
-        ParserError::ParserError("No infix parser for token ShiftLeft".to_string()),
+        parser_error("No infix parser for token ShiftLeft".to_string()),
         res.unwrap_err()
     );
 }
@@ -1442,7 +1446,7 @@
     let sql = "SELECT COUNT(ALL DISTINCT + x) FROM customer";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Cannot specify both ALL and DISTINCT".to_string()),
+        parser_error("Cannot specify both ALL and DISTINCT".to_string()),
         res.unwrap_err()
     );
 }
@@ -1458,7 +1462,7 @@
 fn parse_invalid_infix_not() {
     let res = parse_sql_statements("SELECT c FROM t WHERE c NOT (");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: NOT".to_string()),
+        parser_error("Expected: end of statement, found: NOT".to_string()),
         res.unwrap_err(),
     );
 }
@@ -2623,7 +2627,7 @@
     let res =
         all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: (, found: segment".to_string()),
+        parser_error("Expected: (, found: segment".to_string()),
         res.unwrap_err()
     );
 }
@@ -2835,14 +2839,14 @@
     let sql = "select (1";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: EOF".to_string()),
+        parser_error("Expected: ), found: EOF".to_string()),
         res.unwrap_err()
     );
 
     let sql = "select (), 2";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: )".to_string()),
+        parser_error("Expected: an expression, found: )".to_string()),
         res.unwrap_err()
     );
 }
@@ -3145,7 +3149,7 @@
     let err = dialects
         .parse_sql_statements("SELECT a FROM t ORDER BY a USING OPERATOR();")
         .unwrap_err();
-    assert!(matches!(err, ParserError::ParserError(_)));
+    assert!(matches!(err, ParserError::ParserError { .. }));
 }
 
 #[test]
@@ -3615,7 +3619,7 @@
     let dialects = all_dialects_except(|d| d.allow_extract_custom());
     let res = dialects.parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)");
     assert_eq!(
-        ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()),
+        parser_error("Expected: date/time field, found: JIFFY".to_string()),
         res.unwrap_err()
     );
 }
@@ -3718,7 +3722,7 @@
     let dialects = all_dialects_except(|d| d.allow_extract_custom());
     let res = dialects.parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df");
     assert_eq!(
-        ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()),
+        parser_error("Expected: date/time field, found: JIFFY".to_string()),
         res.unwrap_err()
     );
 }
@@ -3745,7 +3749,7 @@
     let dialects = all_dialects_except(|d| d.allow_extract_custom());
     let res = dialects.parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df");
     assert_eq!(
-        ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()),
+        parser_error("Expected: date/time field, found: JIFFY".to_string()),
         res.unwrap_err()
     );
 }
@@ -3951,7 +3955,7 @@
     let sql = "SELECT LAG(1 IGNORE NULLS) IGNORE NULLS OVER () FROM t1";
     assert_eq!(
         dialects.parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: NULLS".to_string())
+        parser_error("Expected: end of statement, found: NULLS".to_string())
     );
 
     let sql = "SELECT LAG(1 IGNORE NULLS) IGNORE NULLS OVER () FROM t1";
@@ -3959,7 +3963,7 @@
         all_dialects_where(|d| !d.supports_window_function_null_treatment_arg())
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError("Expected: ), found: IGNORE".to_string())
+        parser_error("Expected: ), found: IGNORE".to_string())
     );
 }
 
@@ -4684,7 +4688,7 @@
 
     assert_eq!(
         dialects.parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: >, found: )".to_string())
+        parser_error("Expected: >, found: )".to_string())
     );
 }
 
@@ -5317,14 +5321,12 @@
 
     assert_eq!(
         parse_sql_statements("RENAME TABLE old_table TO new_table a").unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: a".to_string())
+        parser_error("Expected: end of statement, found: a".to_string())
     );
 
     assert_eq!(
         parse_sql_statements("RENAME TABLE1 old_table TO new_table a").unwrap_err(),
-        ParserError::ParserError(
-            "Expected: KEYWORD `TABLE` after RENAME, found: TABLE1".to_string()
-        )
+        parser_error("Expected: KEYWORD `TABLE` after RENAME, found: TABLE1".to_string())
     );
 }
 
@@ -5358,7 +5360,7 @@
         .parse_sql_statements("ALTER TABLE t ON CLUSTER 123 ADD CONSTRAINT bar PRIMARY KEY (baz)");
     assert_eq!(
         res.unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: 123".to_string())
+        parser_error("Expected: identifier, found: 123".to_string())
     )
 }
 
@@ -5630,7 +5632,7 @@
         "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'"
     ));
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: USING".to_string()),
+        parser_error("Expected: end of statement, found: USING".to_string()),
         res.unwrap_err()
     );
 }
@@ -5661,7 +5663,7 @@
 
     let res = parse_sql_statements("ALTER TABLE tab DROP CONSTRAINT is_active TEXT");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: TEXT".to_string()),
+        parser_error("Expected: end of statement, found: TEXT".to_string()),
         res.unwrap_err()
     );
 }
@@ -5670,15 +5672,13 @@
 fn parse_bad_constraint() {
     let res = parse_sql_statements("ALTER TABLE tab ADD");
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+        parser_error("Expected: identifier, found: EOF".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("CREATE TABLE tab (foo int,");
     assert_eq!(
-        ParserError::ParserError(
-            "Expected: column name or constraint definition, found: EOF".to_string()
-        ),
+        parser_error("Expected: column name or constraint definition, found: EOF".to_string()),
         res.unwrap_err()
     );
 }
@@ -5905,7 +5905,7 @@
 
     // missing PLAN keyword should return error
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: SELECT".to_string()),
+        parser_error("Expected: end of statement, found: SELECT".to_string()),
         all_dialects()
             .parse_sql_statements("EXPLAIN QUERY SELECT sqrt(id) FROM foo")
             .unwrap_err()
@@ -6178,7 +6178,7 @@
     });
     let res = dialects.parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: (, found: window2".to_string()),
+        parser_error("Expected: (, found: window2".to_string()),
         res.unwrap_err()
     );
 }
@@ -6687,13 +6687,13 @@
 
     let result = parse_sql_statements("SELECT INTERVAL '1' SECOND TO SECOND");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: SECOND".to_string()),
+        parser_error("Expected: end of statement, found: SECOND".to_string()),
         result.unwrap_err(),
     );
 
     let result = parse_sql_statements("SELECT INTERVAL '10' HOUR (1) TO HOUR (2)");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: (".to_string()),
+        parser_error("Expected: end of statement, found: (".to_string()),
         result.unwrap_err(),
     );
 
@@ -7349,13 +7349,13 @@
 
     let res = parse_sql_statements("SELECT * FROM TABLE '1' AS a");
     assert_eq!(
-        ParserError::ParserError("Expected: (, found: \'1\'".to_string()),
+        parser_error("Expected: (, found: \'1\'".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("SELECT * FROM TABLE (FUN(a) AS a");
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: AS".to_string()),
+        parser_error("Expected: ), found: AS".to_string()),
         res.unwrap_err()
     );
 }
@@ -8062,7 +8062,7 @@
 
     let sql = "SELECT * FROM t1 natural";
     assert_eq!(
-        ParserError::ParserError("Expected: a join type after NATURAL, found: EOF".to_string()),
+        parser_error("Expected: a join type after NATURAL, found: EOF".to_string()),
         parse_sql_statements(sql).unwrap_err(),
     );
 }
@@ -8138,7 +8138,7 @@
     let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::OUTER, &mut Parser::new(d)));
     let res = dialects.parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1");
     assert_eq!(
-        ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()),
+        parser_error("Expected: APPLY, found: JOIN".to_string()),
         res.unwrap_err()
     );
 }
@@ -8383,7 +8383,7 @@
         // Check that forgetting the semicolon results in an error:
         let res = parse_sql_statements(&(sql1.to_owned() + " " + sql2_kw + sql2_rest));
         assert_eq!(
-            ParserError::ParserError("Expected: end of statement, found: ".to_string() + sql2_kw),
+            parser_error("Expected: end of statement, found: ".to_string() + sql2_kw),
             res.unwrap_err()
         );
     }
@@ -8451,7 +8451,7 @@
         "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3 FOR 12)",
     );
     assert_eq!(
-        ParserError::ParserError("Expected: PLACING, found: FROM".to_owned()),
+        parser_error("Expected: PLACING, found: FROM".to_owned()),
         parse_sql_statements("SELECT OVERLAY('abccccde' FROM 3)").unwrap_err(),
     );
 
@@ -8502,7 +8502,7 @@
     );
 
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 'xyz'".to_owned()),
+        parser_error("Expected: ), found: 'xyz'".to_owned()),
         parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err()
     );
 
@@ -8549,13 +8549,13 @@
     // dialects without comma-style TRIM syntax should fail
     let unsupported_dialects = all_dialects_where(|d| !d.supports_comma_separated_trim());
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: ,".to_owned()),
+        parser_error("Expected: ), found: ,".to_owned()),
         unsupported_dialects
             .parse_sql_statements("SELECT TRIM('xyz', 'a')")
             .unwrap_err()
     );
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 'xyz'".to_owned()),
+        parser_error("Expected: ), found: 'xyz'"),
         unsupported_dialects
             .parse_sql_statements("SELECT TRIM(FROM 'xyz')")
             .unwrap_err()
@@ -8591,7 +8591,7 @@
     let res = all_dialects_except(|d| d.is::<DatabricksDialect>())
         .parse_sql_statements("SELECT EXISTS (");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: EOF".to_string()
         ),
         res.unwrap_err(),
@@ -8600,7 +8600,7 @@
     let res = all_dialects_except(|d| d.is::<DatabricksDialect>())
         .parse_sql_statements("SELECT EXISTS (NULL)");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: NULL".to_string()
         ),
         res.unwrap_err(),
@@ -9092,13 +9092,13 @@
 
     let sql = "DROP TABLE";
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+        parser_error("Expected: identifier, found: EOF".to_string()),
         parse_sql_statements(sql).unwrap_err(),
     );
 
     let sql = "DROP TABLE IF EXISTS foo, bar CASCADE RESTRICT";
     assert_eq!(
-        ParserError::ParserError("Cannot specify both CASCADE and RESTRICT in DROP".to_string()),
+        parser_error("Cannot specify both CASCADE and RESTRICT in DROP".to_string()),
         parse_sql_statements(sql).unwrap_err(),
     );
 }
@@ -9158,7 +9158,7 @@
 fn parse_invalid_subquery_without_parens() {
     let res = parse_sql_statements("SELECT SELECT 1 FROM bar WHERE 1=1 FROM baz");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: 1".to_string()),
+        parser_error("Expected: end of statement, found: 1".to_string()),
         res.unwrap_err()
     );
 }
@@ -9389,14 +9389,14 @@
     let sql = "SELECT * FROM LATERAL UNNEST ([10,20,30]) as numbers WITH OFFSET;";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: WITH".to_string()),
+        parser_error("Expected: end of statement, found: WITH".to_string()),
         res.unwrap_err()
     );
 
     let sql = "SELECT * FROM a LEFT JOIN LATERAL (b CROSS JOIN c)";
     let res = parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: b".to_string()
         ),
         res.unwrap_err()
@@ -9522,19 +9522,19 @@
 
     let res = dialects.parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD");
     assert_eq!(
-        ParserError::ParserError("Expected: isolation level, found: BAD".to_string()),
+        parser_error("Expected: isolation level, found: BAD".to_string()),
         res.unwrap_err()
     );
 
     let res = dialects.parse_sql_statements("START TRANSACTION BAD");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: BAD".to_string()),
+        parser_error("Expected: end of statement, found: BAD".to_string()),
         res.unwrap_err()
     );
 
     let res = dialects.parse_sql_statements("START TRANSACTION READ ONLY,");
     assert_eq!(
-        ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()),
+        parser_error("Expected: transaction mode, found: EOF".to_string()),
         res.unwrap_err()
     );
 
@@ -9688,7 +9688,7 @@
     ];
     for (sql, error) in error_sqls {
         assert_eq!(
-            ParserError::ParserError(error.to_string()),
+            parser_error(error.to_string()),
             multi_variable_dialects
                 .parse_sql_statements(sql)
                 .unwrap_err()
@@ -9870,7 +9870,7 @@
 
     assert_eq!(
         parse_sql_statements("ABORT TO test1").unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: TO".to_string()),
+        parser_error("Expected: end of statement, found: TO".to_string()),
     );
 }
 
@@ -10807,10 +10807,7 @@
         ),
     ] {
         let res = dialects.parse_sql_statements(sql);
-        assert_eq!(
-            ParserError::ParserError(err_msg.to_string()),
-            res.unwrap_err()
-        );
+        assert_eq!(parser_error(err_msg.to_string()), res.unwrap_err());
     }
 }
 
@@ -11091,19 +11088,19 @@
     // Can't repeat OFFSET / LIMIT
     let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 OFFSET 2");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: OFFSET".to_string()),
+        parser_error("Expected: end of statement, found: OFFSET".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("SELECT foo FROM bar LIMIT 2 LIMIT 2");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: LIMIT".to_string()),
+        parser_error("Expected: end of statement, found: LIMIT".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 LIMIT 2 OFFSET 2");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: OFFSET".to_string()),
+        parser_error("Expected: end of statement, found: OFFSET".to_string()),
         res.unwrap_err()
     );
 }
@@ -11183,7 +11180,7 @@
     let res =
         all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: (, found: )".to_string()),
+        parser_error("Expected: (, found: )".to_string()),
         res.unwrap_err()
     );
 
@@ -11191,7 +11188,7 @@
         all_dialects_where(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
 
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: )".to_string()),
+        parser_error("Expected: an expression, found: )".to_string()),
         result_unparenthesized.unwrap_err()
     );
 }
@@ -11299,7 +11296,7 @@
     ] {
         assert_eq!(
             parse_sql_statements(sql),
-            Err(ParserError::ParserError(format!(
+            Err(parser_error(format!(
                 "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: {found}"
             ))),
             "{sql}"
@@ -11405,7 +11402,7 @@
     ] {
         assert_eq!(
             dialects.parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string())),
+            Err(parser_error(err.to_string())),
             "{sql}"
         );
     }
@@ -11596,7 +11593,7 @@
 
     let res = parse_sql_statements("CACHE TABLE 'table_name' foo");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string()
         ),
         res.unwrap_err()
@@ -11604,7 +11601,7 @@
 
     let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') foo");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string()
         ),
         res.unwrap_err()
@@ -11612,7 +11609,7 @@
 
     let res = parse_sql_statements("CACHE TABLE 'table_name' AS foo");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string()
         ),
         res.unwrap_err()
@@ -11620,7 +11617,7 @@
 
     let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') AS foo");
     assert_eq!(
-        ParserError::ParserError(
+        parser_error(
             "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string()
         ),
         res.unwrap_err()
@@ -11628,19 +11625,19 @@
 
     let res = parse_sql_statements("CACHE 'table_name'");
     assert_eq!(
-        ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()),
+        parser_error("Expected: a `TABLE` keyword, found: 'table_name'".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("CACHE 'table_name' OPTIONS('K1'='V1')");
     assert_eq!(
-        ParserError::ParserError("Expected: a `TABLE` keyword, found: OPTIONS".to_string()),
+        parser_error("Expected: a `TABLE` keyword, found: OPTIONS".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("CACHE flag 'table_name' OPTIONS('K1'='V1')");
     assert_eq!(
-        ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()),
+        parser_error("Expected: a `TABLE` keyword, found: 'table_name'".to_string()),
         res.unwrap_err()
     );
 }
@@ -11665,19 +11662,19 @@
 
     let res = parse_sql_statements("UNCACHE TABLE 'table_name' foo");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: foo".to_string()),
+        parser_error("Expected: end of statement, found: foo".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("UNCACHE 'table_name' foo");
     assert_eq!(
-        ParserError::ParserError("Expected: TABLE, found: 'table_name'".to_string()),
+        parser_error("Expected: TABLE, found: 'table_name'".to_string()),
         res.unwrap_err()
     );
 
     let res = parse_sql_statements("UNCACHE IF EXISTS 'table_name' foo");
     assert_eq!(
-        ParserError::ParserError("Expected: TABLE, found: IF".to_string()),
+        parser_error("Expected: TABLE, found: IF".to_string()),
         res.unwrap_err()
     );
 }
@@ -11686,21 +11683,30 @@
 fn parse_deeply_nested_parens_hits_recursion_limits() {
     let sql = "(".repeat(1000);
     let res = parse_sql_statements(&sql);
-    assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err());
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
 fn parse_update_deeply_nested_parens_hits_recursion_limits() {
     let sql = format!("\nUPDATE\n\n\n\n\n\n\n\n\n\n{}", "(".repeat(1000));
     let res = parse_sql_statements(&sql);
-    assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err());
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
 fn parse_deeply_nested_unary_op_hits_recursion_limits() {
     let sql = format!("SELECT {}", "+".repeat(1000));
     let res = parse_sql_statements(&sql);
-    assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err());
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -11715,7 +11721,10 @@
         .expect("tokenize to work")
         .parse_statements();
 
-    assert_eq!(res, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -11730,7 +11739,10 @@
         .expect("tokenize to work")
         .parse_statements();
 
-    assert_eq!(res, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -11744,7 +11756,10 @@
         .expect("tokenize to work")
         .parse_statements();
 
-    assert_eq!(res, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -11762,7 +11777,10 @@
         .expect("tokenize to work")
         .parse_statements();
 
-    assert_eq!(res, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -11787,7 +11805,10 @@
         .with_recursion_limit(20)
         .parse_statements();
 
-    assert_eq!(res, Err(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        res,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 
     // limit recursion to 50, expect it to succeed
     let res = Parser::new(&dialect)
@@ -12454,7 +12475,7 @@
         Box::new(MsSqlDialect {}),
     ]);
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: 💝".to_string()),
+        parser_error("Expected: an expression, found: 💝".to_string()),
         supported_dialects
             .parse_sql_statements("SELECT 💝 FROM table1")
             .unwrap_err()
@@ -12510,23 +12531,21 @@
         trailing_commas
             .parse_sql_statements("SELECT name, age, from employees;")
             .unwrap_err(),
-        ParserError::ParserError("Expected an expression, found: from".to_string())
+        parser_error("Expected an expression, found: from".to_string())
     );
 
     assert_eq!(
         trailing_commas
             .parse_sql_statements("REVOKE USAGE, SELECT, ON p TO u")
             .unwrap_err(),
-        ParserError::ParserError("Expected: a privilege keyword, found: ON".to_string())
+        parser_error("Expected: a privilege keyword, found: ON".to_string())
     );
 
     assert_eq!(
         trailing_commas
             .parse_sql_statements("CREATE TABLE employees (name text, age int,)")
             .unwrap_err(),
-        ParserError::ParserError(
-            "Expected: column name or constraint definition, found: )".to_string()
-        )
+        parser_error("Expected: column name or constraint definition, found: )".to_string())
     );
 
     let unsupported_dialects = all_dialects_where(|d| !d.supports_trailing_commas());
@@ -12534,7 +12553,7 @@
         unsupported_dialects
             .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,")
             .unwrap_err(),
-        ParserError::ParserError("Expected: an expression, found: EOF".to_string())
+        parser_error("Expected: an expression, found: EOF".to_string())
     );
 }
 
@@ -12560,7 +12579,7 @@
         unsupported_dialects
             .parse_sql_statements("SELECT album_id, name, FROM track")
             .unwrap_err(),
-        ParserError::ParserError("Expected an expression, found: FROM".to_string())
+        parser_error("Expected an expression, found: FROM".to_string())
     );
 }
 
@@ -12975,7 +12994,7 @@
     dialects.verified_stmt("EXECUTE 'SELECT 1'");
 
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: ,".to_string()),
+        parser_error("Expected: identifier, found: ,".to_string()),
         dialects
             .parse_sql_statements("EXECUTE IMMEDIATE 'SELECT 1' USING 1 AS, y")
             .unwrap_err()
@@ -13269,9 +13288,10 @@
         all_dialects_where(|d| !d.supports_hash_comment())
             .parse_sql_statements("SELECT 1 # it's a comment")
             .unwrap_err(),
-        ParserError::TokenizerError(
-            "Unterminated string literal at Line: 1, Column: 14".to_string()
-        )
+        ParserError::TokenizerError {
+            message: "Unterminated string literal".to_string(),
+            span: Span::new(Location::new(1, 14), Location::new(1, 14)),
+        }
     );
 }
 
@@ -13298,7 +13318,7 @@
     ] {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("No infix parser for token Sharp".to_string()),
+            parser_error("No infix parser for token Sharp".to_string()),
             "{sql}"
         );
     }
@@ -14103,7 +14123,7 @@
         );
         assert_eq!(
             all_dialects_where(|d| d.supports_match_recognize()).parse_sql_statements(&sql),
-            Err(ParserError::ParserError(format!(
+            Err(parser_error(format!(
                 "Expected: identifier, found: {found}"
             ))),
             "{pattern}"
@@ -14796,7 +14816,7 @@
     all_dialects().verified_stmt("TRUNCATE TABLE t");
 
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+        parser_error("Expected: identifier, found: EOF".to_string()),
         all_dialects()
             .parse_sql_statements("TRUNCATE TABLE t ON CLUSTER")
             .unwrap_err()
@@ -15033,7 +15053,7 @@
             .parse_sql_statements("CREATE POLICY my_policy ON my_table TO")
             .unwrap_err()
             .to_string(),
-        "sql parser error: Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF"
+        "sql parser error: Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO, found: EOF"
     );
     // missing USING expression
     assert_eq!(
@@ -15387,14 +15407,14 @@
         dialects
             .parse_sql_statements("SELECT a = FROM")
             .unwrap_err(),
-        ParserError::ParserError("Expected an expression, found: FROM".to_string())
+        parser_error("Expected an expression, found: FROM")
     );
 
     assert_eq!(
         dialects
             .parse_sql_statements("SELECT a = FROM some_table")
             .unwrap_err(),
-        ParserError::ParserError("Expected an expression, found: FROM".to_string())
+        parser_error("Expected an expression, found: FROM")
     );
 
     let dialects = all_dialects_where(|d| !d.supports_eq_alias_assignment());
@@ -15551,14 +15571,14 @@
 
     assert_eq!(
         dialects.parse_sql_statements("LISTEN *").unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: *".to_string())
+        parser_error("Expected: identifier, found: *".to_string())
     );
 
     let dialects = all_dialects_where(|d| !d.supports_listen_notify());
 
     assert_eq!(
         dialects.parse_sql_statements("LISTEN test1").unwrap_err(),
-        ParserError::ParserError("Expected: an SQL statement, found: LISTEN".to_string())
+        parser_error("Expected: an SQL statement, found: LISTEN".to_string())
     );
 }
 
@@ -15582,14 +15602,14 @@
 
     assert_eq!(
         dialects.parse_sql_statements("UNLISTEN +").unwrap_err(),
-        ParserError::ParserError("Expected: wildcard or identifier, found: +".to_string())
+        parser_error("Expected: wildcard or identifier, found: +".to_string())
     );
 
     let dialects = all_dialects_where(|d| !d.supports_listen_notify());
 
     assert_eq!(
         dialects.parse_sql_statements("UNLISTEN test1").unwrap_err(),
-        ParserError::ParserError("Expected: an SQL statement, found: UNLISTEN".to_string())
+        parser_error("Expected: an SQL statement, found: UNLISTEN".to_string())
     );
 }
 
@@ -15618,13 +15638,13 @@
 
     assert_eq!(
         dialects.parse_sql_statements("NOTIFY *").unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: *".to_string())
+        parser_error("Expected: identifier, found: *".to_string())
     );
     assert_eq!(
         dialects
             .parse_sql_statements("NOTIFY test1, *")
             .unwrap_err(),
-        ParserError::ParserError("Expected: literal string, found: *".to_string())
+        parser_error("Expected: literal string, found: *".to_string())
     );
 
     let sql_statements = [
@@ -15636,7 +15656,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: an SQL statement, found: NOTIFY".to_string())
+            parser_error("Expected: an SQL statement, found: NOTIFY".to_string())
         );
     }
 }
@@ -15697,13 +15717,13 @@
         only_supports_load_extension_dialects
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: INPATH".to_string())
+        parser_error("Expected: end of statement, found: INPATH".to_string())
     );
     assert_eq!(
         not_supports_load_dialects
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError(
+        parser_error(
             "Expected: `DATA` or an extension name after `LOAD`, found: INPATH".to_string()
         )
     );
@@ -15736,13 +15756,13 @@
         only_supports_load_extension_dialects
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: LOCAL".to_string())
+        parser_error("Expected: end of statement, found: LOCAL".to_string())
     );
     assert_eq!(
         not_supports_load_dialects
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError(
+        parser_error(
             "Expected: `DATA` or an extension name after `LOAD`, found: LOCAL".to_string()
         )
     );
@@ -15848,7 +15868,7 @@
     let sql = "LOAD DATA2 LOCAL INPATH '/local/path/to/data.txt' INTO TABLE test.my_table";
     assert_eq!(
         dialects.parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError(
+        parser_error(
             "Expected: `DATA` or an extension name after `LOAD`, found: DATA2".to_string()
         )
     );
@@ -15871,7 +15891,7 @@
         not_supports_load_extension_dialects
             .parse_sql_statements(sql)
             .unwrap_err(),
-        ParserError::ParserError(
+        parser_error(
             "Expected: `DATA` or an extension name after `LOAD`, found: my_extension".to_string()
         )
     );
@@ -15936,7 +15956,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("No infix parser for token ExclamationMark".to_string())
+            parser_error("No infix parser for token ExclamationMark".to_string())
         );
     }
 
@@ -15946,7 +15966,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: an expression, found: !".to_string())
+            parser_error("Expected: an expression, found: !".to_string())
         );
     }
 }
@@ -15982,7 +16002,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: an expression, found: !".to_string())
+            parser_error("Expected: an expression, found: !".to_string())
         );
     }
 
@@ -15997,7 +16017,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("No infix parser for token ExclamationMark".to_string())
+            parser_error("No infix parser for token ExclamationMark".to_string())
         );
     }
 
@@ -16010,7 +16030,7 @@
     for &sql in &sql_statements {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("No infix parser for token ExclamationMark".to_string())
+            parser_error("No infix parser for token ExclamationMark".to_string())
         );
     }
 }
@@ -16093,7 +16113,7 @@
         all_dialects_where(|d| d.supports_comment_on())
             .parse_sql_statements("COMMENT ON TABLE t0")
             .unwrap_err(),
-        ParserError::ParserError("Expected: IS, found: EOF".to_string())
+        parser_error("Expected: IS, found: EOF".to_string())
     );
 
     // missing comment literal
@@ -16101,7 +16121,7 @@
         all_dialects_where(|d| d.supports_comment_on())
             .parse_sql_statements("COMMENT ON TABLE t0 IS")
             .unwrap_err(),
-        ParserError::ParserError("Expected: literal string, found: EOF".to_string())
+        parser_error("Expected: literal string, found: EOF".to_string())
     );
 
     // unknown object type
@@ -16109,7 +16129,7 @@
         all_dialects_where(|d| d.supports_comment_on())
             .parse_sql_statements("COMMENT ON UNKNOWN t0 IS 'comment'")
             .unwrap_err(),
-        ParserError::ParserError("Expected: comment object_type, found: UNKNOWN".to_string())
+        parser_error("Expected: comment object_type, found: UNKNOWN".to_string())
     );
 }
 
@@ -16129,7 +16149,7 @@
     for sql in [sql_1, sql_2] {
         assert_eq!(
             dialects.parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: end of statement, found: SELECT".to_string())
+            parser_error("Expected: end of statement, found: SELECT".to_string())
         );
     }
 }
@@ -16143,7 +16163,7 @@
     let sql = "SELECT MAX(interval) FROM tbl";
     assert_eq!(
         dialects.parse_sql_statements(sql),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an expression, found: )".to_string()
         ))
     );
@@ -16415,7 +16435,7 @@
         all_dialects()
             .parse_sql_statements("CREATE TABLE t0 (foo ENUM8('a' = 1, 'b' = ))")
             .unwrap_err(),
-        ParserError::ParserError("Expected: a value, found: )".to_string())
+        parser_error("Expected: a value, found: )".to_string())
     );
 
     // invalid case that name is not a string
@@ -16423,7 +16443,7 @@
         all_dialects()
             .parse_sql_statements("CREATE TABLE t0 (foo ENUM8('a' = 1, 2))")
             .unwrap_err(),
-        ParserError::ParserError("Expected: literal string, found: 2".to_string())
+        parser_error("Expected: literal string, found: 2".to_string())
     );
 }
 
@@ -16530,7 +16550,7 @@
     let query =
     "UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name FROM (SELECT name from t2) AS t2";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: FROM".to_string()),
+        parser_error("Expected: end of statement, found: FROM".to_string()),
         parse_sql_statements(query).unwrap_err()
     );
 }
@@ -16560,9 +16580,7 @@
         unsupported_dialects
             .parse_sql_statements("CREATE TABLE employees (name text, age int,)")
             .unwrap_err(),
-        ParserError::ParserError(
-            "Expected: column name or constraint definition, found: )".to_string()
-        ),
+        parser_error("Expected: column name or constraint definition, found: )".to_string()),
     );
 }
 
@@ -16669,11 +16687,11 @@
     ));
 
     assert_eq!(
-        ParserError::ParserError("Expected: THEN, found: END".to_string()),
+        parser_error("Expected: THEN, found: END".to_string()),
         parse_sql_statements("CASE 1 WHEN a END").unwrap_err()
     );
     assert_eq!(
-        ParserError::ParserError("Expected: WHEN, found: ELSE".to_string()),
+        parser_error("Expected: WHEN, found: ELSE".to_string()),
         parse_sql_statements("CASE 1 ELSE SELECT 1; END").unwrap_err()
     );
 }
@@ -16752,7 +16770,7 @@
     dialects.verified_stmt("IF 1 THEN SELECT 1; ELSEIF 1 THEN END IF");
 
     assert_eq!(
-        ParserError::ParserError("Expected: IF, found: EOF".to_string()),
+        parser_error("Expected: IF, found: EOF".to_string()),
         dialects
             .parse_sql_statements("IF 1 THEN SELECT 1; ELSEIF 1 THEN SELECT 2; END")
             .unwrap_err()
@@ -16832,7 +16850,7 @@
     verified_stmt("RAISE");
 
     assert_eq!(
-        ParserError::ParserError("Expected: =, found: error".to_string()),
+        parser_error("Expected: =, found: error".to_string()),
         parse_sql_statements("RAISE USING MESSAGE error").unwrap_err()
     );
 }
@@ -17709,7 +17727,7 @@
 
     // Test that plain EXCEPT without DISTINCT fails
     assert_eq!(
-        ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)")
             .unwrap_err()
@@ -17717,7 +17735,7 @@
 
     // Test that EXCEPT ALL fails
     assert_eq!(
-        ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)")
             .unwrap_err()
@@ -17725,7 +17743,7 @@
 
     // Test that EXCEPT BY NAME without DISTINCT fails
     assert_eq!(
-        ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)")
             .unwrap_err()
@@ -17733,7 +17751,7 @@
 
     // Test that EXCEPT ALL BY NAME fails
     assert_eq!(
-        ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements(
                 "SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)"
@@ -17743,7 +17761,7 @@
 
     // Test that plain INTERSECT without DISTINCT fails
     assert_eq!(
-        ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)")
             .unwrap_err()
@@ -17751,7 +17769,7 @@
 
     // Test that INTERSECT ALL fails
     assert_eq!(
-        ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)")
             .unwrap_err()
@@ -17759,7 +17777,7 @@
 
     // Test that INTERSECT BY NAME without DISTINCT fails
     assert_eq!(
-        ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)")
             .unwrap_err()
@@ -17767,7 +17785,7 @@
 
     // Test that INTERSECT ALL BY NAME fails
     assert_eq!(
-        ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
+        parser_error("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
         dialects
             .parse_sql_statements(
                 "SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)"
@@ -17899,10 +17917,7 @@
         ),
     ] {
         let res = dialects.parse_sql_statements(sql);
-        assert_eq!(
-            ParserError::ParserError(err_msg.to_string()),
-            res.unwrap_err()
-        );
+        assert_eq!(parser_error(err_msg.to_string()), res.unwrap_err());
     }
 }
 
@@ -18677,7 +18692,7 @@
             .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test")
             .err()
             .unwrap(),
-        ParserError::ParserError("Expected: end of statement, found: c2".to_string())
+        parser_error("Expected: end of statement, found: c2".to_string())
     );
 
     // Dialects that only support the wildcard form and accept EXCLUDE as an implicity alias
@@ -18692,7 +18707,7 @@
             .parse_sql_statements("SELECT *, c1 EXCLUDE c2 FROM test")
             .err()
             .unwrap(),
-        ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string())
+        parser_error("Expected: end of statement, found: EXCLUDE".to_string())
     );
 }
 
@@ -18803,11 +18818,11 @@
     let res = supports_dictionary.parse_sql_statements(sql);
     let res_dict = dictionary_unsupported.parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: :, found: '14:12:01'".to_string()),
+        parser_error("Expected: :, found: '14:12:01'".to_string()),
         res.unwrap_err()
     );
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: {".to_string()),
+        parser_error("Expected: an expression, found: {".to_string()),
         res_dict.unwrap_err()
     );
 }
@@ -18944,7 +18959,7 @@
     let sql = "CREATE VIEW IF NOT EXISTS AS SELECT 1";
     let res = all_dialects().parse_sql_statements(sql);
     assert_eq!(
-        ParserError::ParserError("Expected: AS, found: SELECT".to_string()),
+        parser_error("Expected: AS, found: SELECT".to_string()),
         res.unwrap_err()
     );
 }
@@ -19271,7 +19286,7 @@
     ] {
         assert_eq!(
             dialects.parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string())),
+            Err(parser_error(err.to_string())),
             "{sql}"
         );
     }
@@ -19766,7 +19781,7 @@
     // This should return a parser error, not panic.
     assert_eq!(
         parse_sql_statements("SET AUTHORIZATION TIME TIME"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: SESSION, LOCAL, or other scope modifier before AUTHORIZATION, found: AUTHORIZATION"
                 .to_string()
         ))
@@ -19984,7 +19999,7 @@
         .verified_stmt("SELECT stack(2, 'a', 'b', 'c', 'd') AS (col1, col2) FROM t");
 
     assert_eq!(
-        ParserError::ParserError("Expected: an identifier after AS, found: (".to_string()),
+        parser_error("Expected: an identifier after AS, found: (".to_string()),
         all_dialects_where(|d| !d.supports_select_item_multi_column_alias())
             .parse_sql_statements("SELECT stack(2, 'a', 'b') AS (col1, col2)")
             .unwrap_err()
@@ -20009,7 +20024,7 @@
     dialects.verified_only_select(r#"SELECT foo(a AS "x y")"#);
     dialects.verified_only_select("SELECT foo(bar(a AS x) AS y)");
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: AS".to_string()),
+        parser_error("Expected: ), found: AS".to_string()),
         all_dialects_except(|d| d.supports_aliased_function_args())
             .parse_sql_statements("SELECT foo(a AS x)")
             .unwrap_err()
@@ -20049,7 +20064,7 @@
 
     // XMLPARSE needs both a mode word and a value.
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: )".to_string()),
+        parser_error("Expected: an expression, found: )".to_string()),
         dialects
             .parse_sql_statements("SELECT xmlparse('<a/>')")
             .unwrap_err()
@@ -20065,7 +20080,7 @@
     let others = all_dialects_except(|d| d.supports_xml_expressions());
     others.verified_only_select("SELECT xmlparse(1)");
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: '<a/>'".to_string()),
+        parser_error("Expected: ), found: '<a/>'".to_string()),
         others
             .parse_sql_statements("SELECT xmlparse(content '<a/>')")
             .unwrap_err()
@@ -20338,7 +20353,7 @@
         .parse_sql_statements("CREATE TABLE new_table AS TABLE %mpty")
         .unwrap_err();
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: %".to_string()),
+        parser_error("Expected: identifier, found: %".to_string()),
         err
     );
 
@@ -20346,7 +20361,7 @@
         .parse_sql_statements("CREATE TABLE new_table AS TABLE `x` ORE")
         .unwrap_err();
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: ORE".to_string()),
+        parser_error("Expected: end of statement, found: ORE".to_string()),
         err
     );
 }
@@ -20369,7 +20384,7 @@
         unsupported
             .parse_sql_statements("SELECT * FROM @stage")
             .unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: @".to_string()),
+        parser_error("Expected: identifier, found: @".to_string()),
     );
 }
 
@@ -20401,7 +20416,7 @@
         _ => unreachable!(),
     }
     assert_eq!(
-        ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+        parser_error("Expected: identifier, found: EOF".to_string()),
         dialects
             .parse_sql_statements("ALTER TABLE tab ADD COLUMN c INT AFTER")
             .unwrap_err()
@@ -20414,7 +20429,7 @@
     ] {
         assert_eq!(
             dialects.parse_sql_statements(sql),
-            Err(ParserError::ParserError(format!(
+            Err(parser_error(format!(
                 "Expected: end of statement, found: {found}"
             ))),
             "{sql}"
@@ -20435,10 +20450,7 @@
     let err = all_dialects_where(|d| !d.supports_numeric_prefix())
         .parse_sql_statements("SELECT (1.i)")
         .unwrap_err();
-    assert_eq!(
-        err,
-        ParserError::ParserError("Expected: ), found: i".to_string())
-    );
+    assert_eq!(err, parser_error("Expected: ), found: i"));
 }
 
 #[test]
@@ -20454,33 +20466,18 @@
 
     // Quoted identifiers are not valid placeholders
     let err = dialects.parse_sql_statements("SELECT :`a`").unwrap_err();
-    assert_eq!(
-        ParserError::ParserError("Expected: placeholder, found: `a`".to_string()),
-        err
-    );
+    assert_eq!(parser_error("Expected: placeholder, found: `a`"), err);
     let err = dialects.parse_sql_statements("SELECT :\"a\"").unwrap_err();
-    assert_eq!(
-        ParserError::ParserError("Expected: placeholder, found: \"a\"".to_string()),
-        err
-    );
+    assert_eq!(parser_error("Expected: placeholder, found: \"a\""), err);
     let err = dialects.parse_sql_statements("SELECT:` a` a").unwrap_err();
-    assert_eq!(
-        ParserError::ParserError("Expected: placeholder, found: ` a`".to_string()),
-        err
-    );
+    assert_eq!(parser_error("Expected: placeholder, found: ` a`"), err);
 
     let ansi = TestedDialects::new(vec![Box::new(AnsiDialect {})]);
     ansi.verified_stmt("SELECT @x");
     let err = ansi.parse_sql_statements("SELECT @`a`").unwrap_err();
-    assert_eq!(
-        ParserError::ParserError("Expected: placeholder, found: `a`".to_string()),
-        err
-    );
+    assert_eq!(parser_error("Expected: placeholder, found: `a`"), err);
     let err = ansi.parse_sql_statements("SELECT @\"a\"").unwrap_err();
-    assert_eq!(
-        ParserError::ParserError("Expected: placeholder, found: \"a\"".to_string()),
-        err
-    );
+    assert_eq!(parser_error("Expected: placeholder, found: \"a\""), err);
 }
 
 #[test]
@@ -20548,21 +20545,21 @@
     );
 
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 1".to_string()),
+        parser_error("Expected: ), found: 1"),
         parse_sql_statements("SELECT * FROM t WHERE x IN ((SELECT a FROM u) 1)").unwrap_err()
     );
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: EOF".to_string()),
+        parser_error("Expected: ), found: EOF"),
         parse_sql_statements("SELECT * FROM t WHERE x IN (SELECT a FROM u").unwrap_err()
     );
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: EOF".to_string()),
+        parser_error("Expected: ), found: EOF"),
         parse_sql_statements("SELECT * FROM t WHERE x IN ((SELECT a FROM u), 1").unwrap_err()
     );
     // The inner `IN` is read again for the outer list after its subquery
     // arm was cached as failed.
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: EOF".to_string()),
+        parser_error("Expected: ), found: EOF"),
         parse_sql_statements("SELECT * FROM t WHERE x IN ((SELECT 1 FROM u WHERE y IN (SELECT 2")
             .unwrap_err()
     );
@@ -20704,15 +20701,11 @@
     dialects.verified_stmt("SELECT CAST(NULL AS ARRAY<STRUCT<a INT>>[])");
     assert_eq!(
         dialects.parse_sql_statements("SELECT CAST(NULL AS STRUCT<a STRUCT<b INT>>[)"),
-        Err(ParserError::ParserError(
-            "Expected: ], found: )".to_string()
-        ))
+        Err(parser_error("Expected: ], found: )".to_string()))
     );
     assert_eq!(
         dialects.parse_sql_statements("SELECT CAST(NULL AS STRUCT<a STRUCT<b INT>>[x])"),
-        Err(ParserError::ParserError(
-            "Expected: ], found: x".to_string()
-        ))
+        Err(parser_error("Expected: ], found: x".to_string()))
     );
 
     let dialects = all_dialects_where(|d| d.supports_struct_literal());
@@ -20755,3 +20748,112 @@
     dialects.verified_stmt("SELECT 1 AS 'x'");
     dialects.verified_stmt("SELECT a FROM t AS 'y'");
 }
+
+#[test]
+fn parse_error_at_end_of_input_reports_location() {
+    let dialects = all_dialects();
+    assert_eq!(
+        dialects
+            .parse_sql_statements_with_locations("SELECT 1 FROM t WHERE")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Expected: an expression, found: EOF at Line: 1, Column: 22"
+    );
+    assert_eq!(
+        dialects
+            .parse_sql_statements_with_locations("SELECT 1\nFROM t\nWHERE\n-- trailing\n")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Expected: an expression, found: EOF at Line: 3, Column: 6"
+    );
+    for sql in ["", " \n "] {
+        let err = dialects.one_of_identical_results(|dialect| {
+            Parser::new(dialect)
+                .try_with_sql(sql)
+                .unwrap()
+                .parse_expr()
+                .unwrap_err()
+        });
+        assert_eq!(
+            err.to_string(),
+            "sql parser error: Expected: an expression, found: EOF at Line: 1, Column: 1"
+        );
+    }
+}
+
+#[test]
+fn parse_error_span() {
+    let dialect = GenericDialect {};
+    let err = Parser::parse_sql(&dialect, "SELECT 1 FROM t x y").unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "sql parser error: Expected: end of statement, found: y at Line: 1, Column: 19"
+    );
+    assert_eq!(
+        err.span(),
+        Span::new(Location::new(1, 19), Location::new(1, 20))
+    );
+
+    let err = Parser::parse_sql(&dialect, "SELECT 1 FROM t WHERE").unwrap_err();
+    assert_eq!(
+        err.span(),
+        Span::new(Location::new(1, 22), Location::new(1, 22))
+    );
+
+    let err = Parser::parse_sql(&dialect, "SELECT 'abc").unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "sql parser error: Unterminated string literal at Line: 1, Column: 8"
+    );
+    assert_eq!(
+        err.span(),
+        Span::new(Location::new(1, 8), Location::new(1, 8))
+    );
+}
+
+#[test]
+fn parse_recursion_limit_error_reports_location() {
+    let err = Parser::new(&GenericDialect {})
+        .with_recursion_limit(4)
+        .try_with_sql("SELECT (((1)))")
+        .unwrap()
+        .parse_statements()
+        .unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "sql parser error: recursion limit exceeded at Line: 1, Column: 9"
+    );
+    assert_eq!(
+        err,
+        ParserError::RecursionLimitExceeded {
+            span: Span::new(Location::new(1, 9), Location::new(1, 10))
+        }
+    );
+}
+
+#[test]
+fn parse_extract_missing_from_reports_location() {
+    assert_eq!(
+        all_dialects()
+            .parse_sql_statements_with_locations("SELECT EXTRACT(YEAR x)")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Expected: 'FROM' or ',', found: x at Line: 1, Column: 21"
+    );
+}
+
+#[test]
+fn peek_tokens_at_end_of_input_reports_location() {
+    let [eof] = all_dialects().one_of_identical_results(|dialect| {
+        let mut parser = Parser::new(dialect)
+            .try_with_sql("SELECT\n-- trailing\n")
+            .unwrap();
+        parser.next_token();
+        parser.peek_tokens_with_location()
+    });
+    assert_eq!(eof.token, Token::EOF);
+    assert_eq!(
+        eof.span,
+        Span::new(Location::new(1, 7), Location::new(1, 7))
+    );
+}
diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs
index c9fd75b..fe7421f 100644
--- a/tests/sqlparser_custom_dialect.rs
+++ b/tests/sqlparser_custom_dialect.rs
@@ -152,11 +152,10 @@
 
     let dialect = MyDialect {};
     let sql = "SELECT MAP {1: 2}";
+    let err = Parser::parse_sql(&dialect, sql).unwrap_err();
     assert_eq!(
-        Parser::parse_sql(&dialect, sql),
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: { at Line: 1, Column: 12".to_string()
-        ))
+        err.to_string(),
+        "sql parser error: Expected: end of statement, found: { at Line: 1, Column: 12"
     );
     Ok(())
 }
@@ -198,11 +197,10 @@
         }
     }
 
+    let err = Parser::parse_sql(&WithoutPartitions {}, sql).unwrap_err();
     assert_eq!(
-        Parser::parse_sql(&WithoutPartitions {}, sql),
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: PARTITION at Line: 1, Column: 25".to_string()
-        ))
+        err.to_string(),
+        "sql parser error: Expected: end of statement, found: PARTITION at Line: 1, Column: 25"
     );
     Ok(())
 }
@@ -332,10 +330,9 @@
         sql,
         &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
     );
+    let err = Parser::parse_sql(&dialect, "SELECT transform(xs, lambda x : x + 1)").unwrap_err();
     assert_eq!(
-        Parser::parse_sql(&dialect, "SELECT transform(xs, lambda x : x + 1)"),
-        Err(ParserError::ParserError(
-            "Expected: ), found: x at Line: 1, Column: 29".to_string()
-        ))
+        err.to_string(),
+        "sql parser error: Expected: ), found: x at Line: 1, Column: 29"
     );
 }
diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs
index b9eb028..11822c7 100644
--- a/tests/sqlparser_databricks.rs
+++ b/tests/sqlparser_databricks.rs
@@ -18,7 +18,6 @@
 use sqlparser::ast::helpers::attached_token::AttachedToken;
 use sqlparser::ast::*;
 use sqlparser::dialect::{DatabricksDialect, GenericDialect};
-use sqlparser::parser::ParserError;
 use sqlparser::tokenizer::Span;
 use test_utils::*;
 
@@ -86,7 +85,7 @@
     let res = databricks().parse_sql_statements("SELECT EXISTS (");
     assert_eq!(
         // TODO: improve this error message...
-        ParserError::ParserError("Expected: an expression, found: EOF".to_string()),
+        parser_error("Expected: an expression, found: EOF".to_string()),
         res.unwrap_err(),
     );
 }
@@ -294,7 +293,7 @@
     for sql in &invalid_cases {
         assert_eq!(
             databricks().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+            parser_error("Expected: identifier, found: EOF".to_string()),
         );
     }
 }
@@ -455,17 +454,15 @@
 
     assert_eq!(
         databricks().parse_sql_statements("SELECT 1 FROM t1 FOR TIMESTAMP AS OF 'some_timestamp'"),
-        Err(ParserError::ParserError(
-            "Expected: one of UPDATE or SHARE, found: TIMESTAMP".to_string()
+        Err(parser_error(
+            "Expected: one of UPDATE or SHARE, found: TIMESTAMP"
         ))
     );
 
     assert_eq!(
         all_dialects_where(|d| d.supports_table_versioning())
             .parse_sql_statements("SELECT 1 FROM t1 VERSION AS OF 1 - 2"),
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: -".to_string()
-        ))
+        Err(parser_error("Expected: end of statement, found: -"))
     );
 }
 
@@ -528,13 +525,13 @@
         databricks()
             .parse_sql_statements("OPTIMIZE my_table ZORDER BY")
             .unwrap_err(),
-        ParserError::ParserError("Expected: (, found: EOF".to_string())
+        parser_error("Expected: (, found: EOF".to_string())
     );
     assert_eq!(
         databricks()
             .parse_sql_statements("OPTIMIZE my_table ZORDER BY ()")
             .unwrap_err(),
-        ParserError::ParserError("Expected: an expression, found: )".to_string())
+        parser_error("Expected: an expression, found: )".to_string())
     );
 }
 
@@ -743,9 +740,7 @@
     assert_eq!(
         all_dialects_where(|d| !d.supports_cte_without_as())
             .parse_sql_statements("WITH cte (SELECT 1) SELECT * FROM cte"),
-        Err(ParserError::ParserError(
-            "Expected: ), found: 1".to_string()
-        ))
+        Err(parser_error("Expected: ), found: 1"))
     );
 }
 
@@ -779,8 +774,6 @@
     assert_eq!(
         TestedDialects::new(vec![Box::new(GenericDialect {})])
             .parse_sql_statements("CREATE TABLE t (c ARRAY<STRING COLLATE UTF8_LCASE>)"),
-        Err(ParserError::ParserError(
-            "Expected: >, found: COLLATE".to_string()
-        ))
+        Err(parser_error("Expected: >, found: COLLATE"))
     );
 }
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index 216d514..5ed8d2b 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -19,12 +19,12 @@
 mod test_utils;
 
 use helpers::attached_token::AttachedToken;
-use sqlparser::tokenizer::Span;
+use sqlparser::parser::ParserError;
+use sqlparser::tokenizer::{Location, Span};
 use test_utils::*;
 
 use sqlparser::ast::*;
 use sqlparser::dialect::{DuckDbDialect, GenericDialect};
-use sqlparser::parser::ParserError;
 
 fn duckdb() -> TestedDialects {
     TestedDialects::new(vec![Box::new(DuckDbDialect {})])
@@ -880,7 +880,7 @@
     // missing comma separation
     let error_sql = "SELECT TRIM('xyz' 'a')";
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 'a'".to_owned()),
+        parser_error("Expected: ), found: 'a'".to_owned()),
         duckdb().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -934,9 +934,10 @@
 
     assert_eq!(
         duckdb().parse_sql_statements("SELECT a /* outer /* inner */ FROM t"),
-        Err(ParserError::TokenizerError(
-            "Unexpected EOF while in a multi-line comment at Line: 1, Column: 37".to_string()
-        ))
+        Err(ParserError::TokenizerError {
+            message: "Unexpected EOF while in a multi-line comment".to_string(),
+            span: Span::new(Location::new(1, 37), Location::new(1, 37)),
+        })
     );
 }
 
diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs
index 21e76e2..61e3e1b 100644
--- a/tests/sqlparser_hive.rs
+++ b/tests/sqlparser_hive.rs
@@ -27,7 +27,6 @@
     Value,
 };
 use sqlparser::dialect::{AnsiDialect, GenericDialect, HiveDialect};
-use sqlparser::parser::ParserError;
 use sqlparser::test_utils::*;
 
 #[test]
@@ -150,7 +149,7 @@
     );
     assert_eq!(
         hive().parse_sql_statements(invalid_sql).unwrap_err(),
-        ParserError::ParserError("Expected: end of statement, found: COMMENT".to_string())
+        parser_error("Expected: end of statement, found: COMMENT".to_string())
     );
 }
 
@@ -201,28 +200,28 @@
     hive_and_generic().parse_sql_statements(
         "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b)"
     ).unwrap_err(),
-        ParserError::ParserError("Expected: INTO, found: EOF".to_string())
+        parser_error("Expected: INTO, found: EOF".to_string())
    );
     // missing CLUSTER BY columns
     assert_eq!(
      hive_and_generic().parse_sql_statements(
           "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY () INTO 4 BUCKETS"
      ).unwrap_err(),
-          ParserError::ParserError("Expected: identifier, found: )".to_string())
+          parser_error("Expected: identifier, found: )".to_string())
     );
     // missing SORT BY columns
     assert_eq!(
      hive_and_generic().parse_sql_statements(
           "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b) SORTED BY INTO 4 BUCKETS"
      ).unwrap_err(),
-          ParserError::ParserError("Expected: (, found: INTO".to_string())
+          parser_error("Expected: (, found: INTO".to_string())
     );
     // missing number BUCKETS
     assert_eq!(
      hive_and_generic().parse_sql_statements(
           "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b) SORTED BY (a ASC, b DESC) INTO"
      ).unwrap_err(),
-          ParserError::ParserError("Expected: a value, found: EOF".to_string())
+          parser_error("Expected: a value, found: EOF".to_string())
     );
 }
 
@@ -390,7 +389,7 @@
 
     assert_eq!(
         hive().parse_sql_statements("SET hive.tez.java.opts = -"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: variable value, found: EOF".to_string()
         ))
     )
@@ -434,15 +433,13 @@
 
     assert_eq!(
         unsupported_dialects.parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError(
-            "Expected: an object type after CREATE, found: FUNCTION".to_string()
-        )
+        parser_error("Expected: an object type after CREATE, found: FUNCTION".to_string())
     );
 
     let sql = "CREATE TEMPORARY FUNCTION mydb.myfunc AS 'org.random.class.Name' USING JAR";
     assert_eq!(
         hive().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: literal string, found: EOF".to_string()),
+        parser_error("Expected: literal string, found: EOF".to_string()),
     );
 }
 
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index e07d870..20c4a27 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -84,8 +84,8 @@
     let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string();
     assert_eq!(
         ms().parse_sql_statements(&sql),
-        Err(ParserError::ParserError(
-            "Expected: one of UPDATE or SHARE, found: SYSTEM".to_owned()
+        Err(parser_error(
+            "Expected: one of UPDATE or SHARE, found: SYSTEM"
         ))
     );
 }
@@ -398,7 +398,7 @@
         END\
     ";
     assert_eq!(
-        ParserError::ParserError("Unparsable function body".to_owned()),
+        parser_error("Unparsable function body".to_owned()),
         ms().parse_sql_statements(create_multi_statement_tvf_without_table_definition)
             .unwrap_err()
     );
@@ -410,9 +410,7 @@
         RETURN 'hi'\
     ";
     assert_eq!(
-        ParserError::ParserError(
-            "Expected a subquery (or bare SELECT statement) after RETURN".to_owned()
-        ),
+        parser_error("Expected a subquery (or bare SELECT statement) after RETURN".to_owned()),
         ms().parse_sql_statements(create_inline_tvf_without_subquery_or_bare_select)
             .unwrap_err()
     );
@@ -962,9 +960,10 @@
 fn dont_parse_trailing_for() {
     assert_eq!(
         ms().run_parser_method("SELECT * FROM foo FOR", |p| p.parse_query()),
-        Err(ParserError::ParserError(
-            "Expected: one of UPDATE or SHARE, found: EOF".to_owned()
-        ))
+        Err(ParserError::ParserError {
+            message: "Expected: one of UPDATE or SHARE, found: EOF".to_string(),
+            span: Span::new(Location::new(1, 22), Location::new(1, 22)),
+        })
     );
 }
 
@@ -1340,7 +1339,7 @@
 
     let error_sql = "SELECT CONVERT(INT, 'foo',) FROM T";
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: )".to_owned()),
+        parser_error("Expected: an expression, found: )".to_owned()),
         ms().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -1747,9 +1746,7 @@
     let res = ms_and_generic().parse_sql_statements("WAITFOR '00:00:05'");
     assert_eq!(
         res,
-        Err(ParserError::ParserError(
-            "Expected: DELAY or TIME, found: '00:00:05'".to_owned()
-        ))
+        Err(parser_error("Expected: DELAY or TIME, found: '00:00:05'"))
     );
 }
 
@@ -2663,7 +2660,7 @@
     ] {
         assert_eq!(
             tsql().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_owned()))
+            Err(parser_error(err.to_owned()))
         );
     }
 }
@@ -2691,7 +2688,7 @@
     ] {
         assert_eq!(
             tsql().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_owned()))
+            Err(parser_error(err.to_owned()))
         );
     }
 }
@@ -3004,3 +3001,15 @@
         .expect_err("PROC should remain MSSQL-specific");
     ms_and_generic().verified_stmt("SELECT proc FROM jobs");
 }
+
+#[test]
+fn parse_declare_without_at_sign_reports_location() {
+    let err = ms()
+        .parse_sql_statements_with_locations("DECLARE foo INT")
+        .unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "sql parser error: Invalid MsSql variable declaration. at Line: 1, Column: 9"
+    );
+    assert!(matches!(err, ParserError::ParserError { .. }));
+}
diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs
index 8baa83b..520f29a 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -26,8 +26,8 @@
 use sqlparser::ast::*;
 use sqlparser::dialect::{GenericDialect, MySqlDialect};
 use sqlparser::parser::{ParserError, ParserOptions};
-use sqlparser::tokenizer::Span;
 use sqlparser::tokenizer::Token;
+use sqlparser::tokenizer::{Location, Span};
 use test_utils::*;
 
 #[macro_use]
@@ -539,8 +539,8 @@
     ] {
         assert_eq!(
             mysql_and_generic().parse_sql_statements(sql),
-            Err(ParserError::ParserError(
-                "EXTENDED/FULL are not supported with this type of SHOW query".to_string()
+            Err(parser_error(
+                "EXTENDED/FULL are not supported with this type of SHOW query"
             ))
         );
     }
@@ -1365,7 +1365,7 @@
         r"CREATE TABLE foo (id INT(11)) ENGINE = InnoDB AS SELECT 1 DEFAULT CHARSET = utf8mb3";
     assert!(matches!(
         mysql_and_generic().parse_sql_statements(sql),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 }
 
@@ -1665,17 +1665,19 @@
     let sql = r"SELECT 'I\'m not fine\'";
     assert_eq!(
         mysql().parse_sql_statements(sql),
-        Err(ParserError::TokenizerError(
-            "Unterminated string literal at Line: 1, Column: 8".to_string()
-        ))
+        Err(ParserError::TokenizerError {
+            message: "Unterminated string literal".to_string(),
+            span: Span::new(Location::new(1, 8), Location::new(1, 8)),
+        })
     );
 
     let sql = r"SELECT 'I\\'m not fine'";
     assert_eq!(
         mysql().parse_sql_statements(sql),
-        Err(ParserError::TokenizerError(
-            "Unterminated string literal at Line: 1, Column: 23".to_string()
-        ))
+        Err(ParserError::TokenizerError {
+            message: "Unterminated string literal".to_string(),
+            span: Span::new(Location::new(1, 23), Location::new(1, 23)),
+        })
     );
 }
 
@@ -1948,15 +1950,15 @@
     mysql_and_generic().verified_stmt("CREATE TABLE t (a INT(10) UNSIGNED ZEROFILL)");
     assert_eq!(
         mysql_and_generic().parse_sql_statements("CREATE TABLE t (a VARCHAR(10) ZEROFILL)"),
-        Err(ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: ZEROFILL".to_string()
+        Err(parser_error(
+            "Expected: ',' or ')' after column definition, found: ZEROFILL"
         ))
     );
     assert_eq!(
         all_dialects_except(|d| d.supports_data_type_zerofill_suffix())
             .parse_sql_statements("CREATE TABLE t (a INT ZEROFILL)"),
-        Err(ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: ZEROFILL".to_string()
+        Err(parser_error(
+            "Expected: ',' or ')' after column definition, found: ZEROFILL"
         ))
     );
 }
@@ -2250,7 +2252,7 @@
     let sql = r"INSERT INTO `table` (`date`) VALUES ('2024-01-01') AS `alias` ()";
     assert!(matches!(
         mysql_and_generic().parse_sql_statements(sql),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 
     let sql = r"INSERT INTO `table` (`id`, `date`) VALUES (1, '2024-01-01') AS `alias` (`mek_id`, `mek_date`)";
@@ -3637,8 +3639,8 @@
     let sql = "CREATE TABLE foo (bar INT, CONSTRAINT constr INDEX index (bar))";
     assert_eq!(
         mysql_and_generic().parse_sql_statements(sql),
-        Err(ParserError::ParserError(
-            "Expected: PRIMARY, UNIQUE, FOREIGN, CHECK, or EXCLUDE, found: INDEX".to_string()
+        Err(parser_error(
+            "Expected: PRIMARY, UNIQUE, FOREIGN, CHECK, or EXCLUDE, found: INDEX"
         ))
     );
 
@@ -3808,7 +3810,7 @@
     let err = mysql()
         .parse_sql_statements("SELECT 5 DIV")
         .expect_err("expected an error");
-    assert_matches!(err, ParserError::ParserError(_));
+    assert_matches!(err, ParserError::ParserError { .. });
 }
 
 #[test]
@@ -4764,7 +4766,7 @@
     ] {
         assert_eq!(
             mysql().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string()))
+            Err(parser_error(err))
         );
     }
 }
@@ -5029,7 +5031,7 @@
     ] {
         assert_eq!(
             mysql_and_generic().parse_sql_statements(sql),
-            Err(ParserError::ParserError(missing.to_string()))
+            Err(parser_error(missing))
         );
     }
 }
@@ -5074,7 +5076,7 @@
     let err = mysql_and_generic()
         .parse_sql_statements("SELECT * FROM employees PARTITION")
         .expect_err("expected an error");
-    assert_matches!(err, ParserError::ParserError(_));
+    assert_matches!(err, ParserError::ParserError { .. });
 }
 
 #[test]
@@ -5191,7 +5193,7 @@
     ] {
         assert_eq!(
             mysql_and_generic().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string())),
+            Err(parser_error(err)),
             "{sql}"
         );
     }
@@ -5202,3 +5204,14 @@
     mysql_and_generic().verified_stmt("CREATE TABLE t (a INT) ENGINE = `InnoDB`");
     mysql_and_generic().verified_stmt("CREATE TABLE t (a INT) ENGINE = InnoDB");
 }
+
+#[test]
+fn parse_alter_role_unsupported_reports_location() {
+    assert_eq!(
+        mysql()
+            .parse_sql_statements_with_locations("ALTER ROLE r")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: ALTER ROLE is only support for PostgreSqlDialect, MsSqlDialect at Line: 1, Column: 7"
+    );
+}
diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs
index 888778e..b0dc982 100644
--- a/tests/sqlparser_oracle.rs
+++ b/tests/sqlparser_oracle.rs
@@ -26,10 +26,9 @@
         Statement, TableAliasWithoutColumns, TableObject, Value, ValueWithSpan,
     },
     dialect::OracleDialect,
-    parser::ParserError,
     tokenizer::Span,
 };
-use test_utils::{all_dialects_where, expr_from_projection, number, TestedDialects};
+use test_utils::{all_dialects_where, expr_from_projection, number, parser_error, TestedDialects};
 
 mod test_utils;
 
@@ -215,27 +214,30 @@
     // invalid quote delimiter
     for q in [' ', '\t', '\r', '\n'] {
         assert_eq!(
-            dialect.parse_sql_statements(&format!("SELECT Q'{q}abc{q}' FROM dual")),
-            Err(ParserError::TokenizerError(
-                "Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10".into()
-            )),
+            dialect
+                .parse_sql_statements(&format!("SELECT Q'{q}abc{q}' FROM dual"))
+                .unwrap_err()
+                .to_string(),
+            "sql parser error: Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10",
             "with quote char {q:?}"
         );
     }
     // invalid eof after quote
     assert_eq!(
-        dialect.parse_sql_statements("SELECT Q'"),
-        Err(ParserError::TokenizerError(
-            "Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10".into()
-        )),
+        dialect
+            .parse_sql_statements("SELECT Q'")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10",
         "with EOF quote char"
     );
     // unterminated string
     assert_eq!(
-        dialect.parse_sql_statements("SELECT Q'|asdfa...."),
-        Err(ParserError::TokenizerError(
-            "Unterminated string literal at Line: 1, Column: 9".into()
-        )),
+        dialect
+            .parse_sql_statements("SELECT Q'|asdfa....")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Unterminated string literal at Line: 1, Column: 9",
         "with EOF quote char"
     );
 }
@@ -497,8 +499,8 @@
     let sql = "INSERT INTO t default SELECT 'a' FROM dual";
     assert_eq!(
         oracle_dialect.parse_sql_statements(sql),
-        Err(ParserError::ParserError(
-            "Expected: SELECT, VALUES, or a subquery in the query body, found: default".into()
+        Err(parser_error(
+            "Expected: SELECT, VALUES, or a subquery in the query body, found: default"
         ))
     );
 
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 1e00288..0385e63 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -306,7 +306,7 @@
 
     assert!(matches!(
         pg().parse_sql_statements("CREATE SEQUENCE foo INCREMENT 1 NO MINVALUE NO"),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 }
 
@@ -662,7 +662,7 @@
     ] {
         let result = dialects.parse_sql_statements(sql);
         assert_eq!(
-            ParserError::ParserError(expected_message.to_string()),
+            parser_error(expected_message.to_string()),
             result.unwrap_err()
         );
     }
@@ -672,8 +672,8 @@
     assert_eq!(
         all_dialects_where(|d| !d.supports_exclude_constraint())
             .parse_sql_statements("CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =))"),
-        Err(ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: gist".to_string()
+        Err(parser_error(
+            "Expected: ',' or ')' after column definition, found: gist"
         ))
     );
 }
@@ -1186,7 +1186,7 @@
         "ALTER TABLE t ALTER COLUMN id ADD GENERATED ( INCREMENT 1 MINVALUE 1 )",
     );
     assert_eq!(
-        ParserError::ParserError("Expected: AS, found: (".to_string()),
+        parser_error("Expected: AS, found: (".to_string()),
         res.unwrap_err()
     );
 
@@ -1194,14 +1194,14 @@
         "ALTER TABLE t ALTER COLUMN id ADD GENERATED AS IDENTITY ( INCREMENT )",
     );
     assert_eq!(
-        ParserError::ParserError("Expected: a value, found: )".to_string()),
+        parser_error("Expected: a value, found: )".to_string()),
         res.unwrap_err()
     );
 
     let res =
         pg().parse_sql_statements("ALTER TABLE t ALTER COLUMN id ADD GENERATED AS IDENTITY (");
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: EOF".to_string()),
+        parser_error("Expected: ), found: EOF".to_string()),
         res.unwrap_err()
     );
 }
@@ -1310,13 +1310,15 @@
 
     let res = pg().parse_sql_statements("ALTER TABLE tab OWNER TO CREATE FOO");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: FOO".to_string()),
+        parser_error("Expected: end of statement, found: FOO".to_string()),
         res.unwrap_err()
     );
 
     let res = pg().parse_sql_statements("ALTER TABLE tab OWNER TO 4");
     assert_eq!(
-        ParserError::ParserError("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: 4".to_string()),
+        parser_error(
+            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO, found: 4"
+        ),
         res.unwrap_err()
     );
 }
@@ -1368,25 +1370,25 @@
 fn parse_bad_if_not_exists() {
     let res = pg().parse_sql_statements("CREATE TABLE NOT EXISTS uk_cities ()");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()),
+        parser_error("Expected: end of statement, found: EXISTS".to_string()),
         res.unwrap_err()
     );
 
     let res = pg().parse_sql_statements("CREATE TABLE IF EXISTS uk_cities ()");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()),
+        parser_error("Expected: end of statement, found: EXISTS".to_string()),
         res.unwrap_err()
     );
 
     let res = pg().parse_sql_statements("CREATE TABLE IF uk_cities ()");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: uk_cities".to_string()),
+        parser_error("Expected: end of statement, found: uk_cities".to_string()),
         res.unwrap_err()
     );
 
     let res = pg().parse_sql_statements("CREATE TABLE IF NOT uk_cities ()");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: NOT".to_string()),
+        parser_error("Expected: end of statement, found: NOT".to_string()),
         res.unwrap_err()
     );
 }
@@ -1678,7 +1680,7 @@
 fn parse_copy_from_error() {
     let res = pg().parse_sql_statements("COPY (SELECT 42 AS a, 'hello' AS b) FROM 'query.csv'");
     assert_eq!(
-        ParserError::ParserError("COPY ... FROM does not support query as a source".to_string()),
+        parser_error("COPY ... FROM does not support query as a source".to_string()),
         res.unwrap_err()
     );
 }
@@ -1999,21 +2001,19 @@
 
     assert_eq!(
         pg_and_generic().parse_sql_statements("SET"),
-        Err(ParserError::ParserError(
-            "Expected: identifier, found: EOF".to_string()
-        )),
+        Err(parser_error("Expected: identifier, found: EOF".to_string())),
     );
 
     assert_eq!(
         pg_and_generic().parse_sql_statements("SET a b"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: equals sign or TO, found: b".to_string()
         )),
     );
 
     assert_eq!(
         pg_and_generic().parse_sql_statements("SET a ="),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: variable value, found: EOF".to_string()
         )),
     );
@@ -3377,7 +3377,7 @@
 fn parse_create_table_with_empty_inherits_fails() {
     assert!(matches!(
         pg().parse_sql_statements("CREATE TABLE child_table (child_column INT) INHERITS ()"),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 }
 
@@ -4444,7 +4444,7 @@
 fn parse_operator_empty_parens_rejected() {
     let result = pg_and_generic().parse_sql_statements("SELECT a OPERATOR() b");
     assert_eq!(
-        ParserError::ParserError("Expected: operator name, found: )".to_string()),
+        parser_error("Expected: operator name, found: )".to_string()),
         result.unwrap_err()
     );
 }
@@ -4539,7 +4539,7 @@
         let sql = format!("CREATE ROLE abc {negatable_kw} NO{negatable_kw}");
         assert_eq!(
             pg().parse_sql_statements(&sql),
-            Err(ParserError::ParserError(format!(
+            Err(parser_error(format!(
                 "Found multiple {negatable_kw} or NO{negatable_kw}"
             )))
         );
@@ -5134,7 +5134,7 @@
     let incorrect_sql = "CREATE FUNCTION add(function(struct<a,b> int64), b INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'";
     assert_eq!(
         pg().parse_sql_statements(incorrect_sql).unwrap_err(),
-        ParserError::ParserError("Expected: type modifiers, found: <".to_string())
+        parser_error("Expected: type modifiers, found: <")
     );
 }
 
@@ -5273,9 +5273,7 @@
     let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL PARALLEL BLAH AS 'select $1 + $2;'";
     assert_eq!(
         pg().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError(
-            "Expected: one of UNSAFE | RESTRICTED | SAFE, found: BLAH".to_string()
-        )
+        parser_error("Expected: one of UNSAFE | RESTRICTED | SAFE, found: BLAH")
     );
 }
 
@@ -5576,13 +5574,13 @@
 
     let res = pg().parse_sql_statements("DROP PROCEDURE testproc DROP");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: DROP".to_string()),
+        parser_error("Expected: end of statement, found: DROP".to_string()),
         res.unwrap_err()
     );
 
     let res = pg().parse_sql_statements("DROP PROCEDURE testproc SET NULL");
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: SET".to_string()),
+        parser_error("Expected: end of statement, found: SET".to_string()),
         res.unwrap_err()
     );
 }
@@ -5677,23 +5675,28 @@
 
 #[test]
 fn parse_incorrect_dollar_quoted_string() {
-    for (sql, expected) in [
+    for (sql, expected, location) in [
         (
             "SELECT $x$hello$$",
-            "Unterminated dollar-quoted, expected $ at Line: 1, Column: 18",
+            "Unterminated dollar-quoted, expected $",
+            (1, 18),
         ),
         (
             "SELECT $hello$$",
-            "Unterminated dollar-quoted, expected $ at Line: 1, Column: 16",
+            "Unterminated dollar-quoted, expected $",
+            (1, 16),
         ),
-        (
-            "SELECT $$$",
-            "Unterminated dollar-quoted string at Line: 1, Column: 11",
-        ),
+        ("SELECT $$$", "Unterminated dollar-quoted string", (1, 11)),
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::TokenizerError(expected.to_string())
+            ParserError::TokenizerError {
+                message: expected.to_string(),
+                span: Span::new(
+                    Location::new(location.0, location.1),
+                    Location::new(location.0, location.1),
+                ),
+            }
         );
     }
 }
@@ -6061,7 +6064,7 @@
 
     let res = pg().parse_sql_statements("CREATE UNLOGGED VIEW v AS SELECT 1");
     assert_eq!(
-        ParserError::ParserError("Expected: an object type after CREATE, found: UNLOGGED".into()),
+        parser_error("Expected: an object type after CREATE, found: UNLOGGED"),
         res.unwrap_err()
     );
 }
@@ -7814,7 +7817,7 @@
         assert_eq!(
             pg().parse_sql_statements(&format!("CREATE OPERATOR + (FUNCTION = f, {field})"))
                 .unwrap_err(),
-            ParserError::ParserError(format!(
+            parser_error(format!(
                 "Duplicate or unexpected keyword {keyword} in CREATE OPERATOR"
             ))
         );
@@ -7824,29 +7827,23 @@
     assert_eq!(
         pg().parse_sql_statements("CREATE OPERATOR + (LEFTARG = INT4)")
             .unwrap_err(),
-        ParserError::ParserError("CREATE OPERATOR requires FUNCTION parameter".to_string())
+        parser_error("CREATE OPERATOR requires FUNCTION parameter")
     );
 
     // Test empty parameter list error
     assert_eq!(
         pg().parse_sql_statements("CREATE OPERATOR + ()").unwrap_err(),
-        ParserError::ParserError(
-            "Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: )".to_string()
-        )
+        parser_error("Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: )")
     );
 
     // Test nested empty parentheses error
     assert_eq!(
         pg().parse_sql_statements("CREATE OPERATOR > (()").unwrap_err(),
-        ParserError::ParserError(
-            "Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: (".to_string()
-        )
+        parser_error("Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: (")
     );
     assert_eq!(
         pg().parse_sql_statements("CREATE OPERATOR > ())").unwrap_err(),
-        ParserError::ParserError(
-            "Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: )".to_string()
-        )
+        parser_error("Expected: one of FUNCTION or PROCEDURE or LEFTARG or RIGHTARG or COMMUTATOR or NEGATOR or RESTRICT or JOIN or HASHES or MERGES, found: )")
     );
 }
 
@@ -7939,14 +7936,14 @@
     let sql = "DROP OPERATOR (INTEGER, INTEGER)";
     assert_eq!(
         pg().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: (, found: INTEGER".to_string())
+        parser_error("Expected: (, found: INTEGER")
     );
 
     // Test error: DROP OPERATOR IF EXISTS with no operators
     let sql = "DROP OPERATOR IF EXISTS (INTEGER, INTEGER)";
     assert_eq!(
         pg().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: (, found: INTEGER".to_string())
+        parser_error("Expected: (, found: INTEGER")
     );
 }
 
@@ -8357,7 +8354,7 @@
         // OWNER TO with invalid owner
         (
             "ALTER OPERATOR FAMILY my_ops USING btree OWNER TO 123invalid",
-            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: 123"
+            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO, found: 123"
         ),
         // SET SCHEMA with invalid schema name
         ("ALTER OPERATOR FAMILY my_ops USING btree SET SCHEMA 123invalid", "Expected: identifier, found: 123"),
@@ -8398,7 +8395,7 @@
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError(expected.to_string())
+            parser_error(expected)
         );
     }
 
@@ -8408,7 +8405,7 @@
             "ALTER OPERATOR FAMILY my_ops USING btree ADD FUNCTION 1 () func"
         )
         .unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: )".to_string())
+        parser_error("Expected: identifier, found: )")
     );
 
     for (sql, expected) in [
@@ -8430,7 +8427,7 @@
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError(expected.to_string())
+            parser_error(expected)
         );
     }
 }
@@ -8521,7 +8518,7 @@
         // Missing owner for OWNER TO
         (
             "ALTER OPERATOR CLASS int_ops USING btree OWNER TO",
-            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF"
+            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO, found: EOF"
         ),
         // Missing schema for SET SCHEMA
         ("ALTER OPERATOR CLASS int_ops USING btree SET SCHEMA", "Expected: identifier, found: EOF"),
@@ -8530,7 +8527,7 @@
         // Invalid owner
         (
             "ALTER OPERATOR CLASS int_ops USING btree OWNER TO 123invalid",
-            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: 123"
+            "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO, found: 123"
         ),
         // Invalid schema name
         ("ALTER OPERATOR CLASS int_ops USING btree SET SCHEMA 123invalid", "Expected: identifier, found: 123"),
@@ -8561,7 +8558,7 @@
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError(expected.to_string())
+            parser_error(expected)
         );
     }
 }
@@ -8792,7 +8789,7 @@
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError(expected.to_string())
+            parser_error(expected)
         );
     }
 }
@@ -8817,16 +8814,14 @@
         pg()
             .parse_sql_statements(r#"CREATE TEXT SEARCH "DICTIONARY" d (template = simple)"#)
             .unwrap_err(),
-        ParserError::ParserError(
-            "Expected: one of DICTIONARY or CONFIGURATION or TEMPLATE or PARSER, found: \"DICTIONARY\"".to_string()
-        )
+        parser_error("Expected: one of DICTIONARY or CONFIGURATION or TEMPLATE or PARSER, found: \"DICTIONARY\"")
     );
 
     // CREATE options are key-value pairs in PostgreSQL syntax.
     assert_eq!(
         pg().parse_sql_statements("CREATE TEXT SEARCH DICTIONARY d (template)")
             .unwrap_err(),
-        ParserError::ParserError("Expected: =, found: )".to_string())
+        parser_error("Expected: =, found: )")
     );
 }
 
@@ -8915,14 +8910,14 @@
     let sql = "DROP OPERATOR FAMILY USING btree";
     assert_eq!(
         pg_and_generic().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: USING, found: btree".to_string())
+        parser_error("Expected: USING, found: btree")
     );
 
     // Test error: DROP OPERATOR FAMILY IF EXISTS with no names
     let sql = "DROP OPERATOR FAMILY IF EXISTS USING btree";
     assert_eq!(
         pg_and_generic().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: USING, found: btree".to_string())
+        parser_error("Expected: USING, found: btree")
     );
 }
 
@@ -8984,14 +8979,14 @@
     let sql = "DROP OPERATOR CLASS USING btree";
     assert_eq!(
         pg_and_generic().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: USING, found: btree".to_string())
+        parser_error("Expected: USING, found: btree")
     );
 
     // Test error: DROP OPERATOR CLASS IF EXISTS with no names
     let sql = "DROP OPERATOR CLASS IF EXISTS USING btree";
     assert_eq!(
         pg_and_generic().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: USING, found: btree".to_string())
+        parser_error("Expected: USING, found: btree")
     );
 }
 
@@ -9251,7 +9246,7 @@
             "CREATE OPERATOR CLASS test_ops FOR TYPE INT4 USING btree AS FUNCTION 1 cas_cmp(()"
         )
         .unwrap_err(),
-        ParserError::ParserError("Expected: a data type name, found: (".to_string())
+        parser_error("Expected: a data type name, found: (")
     );
 }
 
@@ -9485,7 +9480,7 @@
     ] {
         assert_eq!(
             pg_and_generic().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string())),
+            Err(parser_error(err)),
             "{sql}"
         );
     }
@@ -9614,7 +9609,7 @@
     // Without the parens Display would invent a `()` the input never had.
     assert!(matches!(
         pg_and_generic().parse_sql_statements("CREATE FOREIGN TABLE ft SERVER s"),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 
     // An empty list is still legal PostgreSQL.
@@ -9648,7 +9643,7 @@
     assert!(matches!(
         pg_and_generic()
             .parse_sql_statements("CREATE OR REPLACE FOREIGN TABLE ft (a INT) SERVER s"),
-        Err(ParserError::ParserError(_))
+        Err(ParserError::ParserError { .. })
     ));
 }
 
@@ -9928,9 +9923,7 @@
         pg_and_generic().parse_sql_statements(
             "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN DO UPDATE"
         ),
-        Err(ParserError::ParserError(
-            "Expected: NOTHING, found: UPDATE".into()
-        ))
+        Err(parser_error("Expected: NOTHING, found: UPDATE"))
     );
 }
 
@@ -10046,7 +10039,7 @@
     ] {
         assert_eq!(
             pg_and_generic().parse_sql_statements(sql),
-            Err(ParserError::ParserError(err.to_string())),
+            Err(parser_error(err)),
             "{sql}"
         );
     }
@@ -10079,10 +10072,8 @@
     pg().one_statement_parses_to("SELECT @a", "SELECT @ a");
     let err = pg().parse_sql_statements("SELECT @-2").unwrap_err();
     assert_eq!(
-        ParserError::TokenizerError(
-            "Expected a valid binary operator after '@-' at Line: 1, Column: 10".to_string(),
-        ),
-        err
+        err.to_string(),
+        "sql parser error: Expected a valid binary operator after '@-' at Line: 1, Column: 10"
     );
 }
 
@@ -10115,7 +10106,7 @@
 
     let err = pg().parse_sql_statements("SELECT a!!").unwrap_err();
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: !!".to_string()),
+        parser_error("Expected: end of statement, found: !!".to_string()),
         err
     );
 }
@@ -10132,7 +10123,7 @@
     let sql = "SELECT * FROM @stage";
     assert_eq!(
         pg().parse_sql_statements(sql).unwrap_err(),
-        ParserError::ParserError("Expected: identifier, found: @".to_string()),
+        parser_error("Expected: identifier, found: @".to_string()),
     );
 }
 
@@ -10196,9 +10187,22 @@
     ] {
         assert_eq!(
             pg().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError(expected.to_string()),
+            parser_error(expected.to_string()),
             "{sql}"
         );
     }
     pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#);
 }
+
+#[test]
+fn parse_create_function_repeated_clause_reports_location() {
+    assert_eq!(
+        pg_and_generic()
+            .parse_sql_statements_with_locations(
+                "CREATE FUNCTION f() RETURNS INT LANGUAGE sql LANGUAGE sql AS 'select 1'"
+            )
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: LANGUAGE specified more than once at Line: 1, Column: 46"
+    );
+}
diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs
index 5bf1a89..667d213 100644
--- a/tests/sqlparser_redshift.rs
+++ b/tests/sqlparser_redshift.rs
@@ -18,7 +18,7 @@
 #[macro_use]
 mod test_utils;
 
-use sqlparser::tokenizer::Span;
+use sqlparser::tokenizer::{Location, Span};
 use test_utils::*;
 
 use sqlparser::ast::*;
@@ -390,9 +390,10 @@
     // invalid query
     assert_eq!(
         redshift().parse_sql_statements(r#"SELECT 1 AS ["1]"#),
-        Err(ParserError::TokenizerError(
-            "Expected close delimiter '\"' before EOF. at Line: 1, Column: 14".to_string()
-        ))
+        Err(ParserError::TokenizerError {
+            message: "Expected close delimiter '\"' before EOF.".to_string(),
+            span: Span::new(Location::new(1, 14), Location::new(1, 14)),
+        })
     );
 }
 
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 6c8c73c..4c4ae69 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -449,14 +449,14 @@
 fn test_snowflake_create_invalid_local_global_table() {
     assert_eq!(
         snowflake().parse_sql_statements("CREATE LOCAL GLOBAL TABLE my_table (a INT)"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an SQL statement, found: LOCAL".to_string()
         ))
     );
 
     assert_eq!(
         snowflake().parse_sql_statements("CREATE GLOBAL LOCAL TABLE my_table (a INT)"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an SQL statement, found: GLOBAL".to_string()
         ))
     );
@@ -466,14 +466,14 @@
 fn test_snowflake_create_invalid_temporal_table() {
     assert_eq!(
         snowflake().parse_sql_statements("CREATE TEMP TEMPORARY TABLE my_table (a INT)"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an object type after CREATE, found: TEMPORARY".to_string()
         ))
     );
 
     assert_eq!(
         snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an object type after CREATE, found: TRANSIENT".to_string()
         ))
     );
@@ -483,7 +483,7 @@
 fn test_snowflake_create_invalid_temporal_file_format() {
     assert_eq!(
         snowflake().parse_sql_statements("CREATE TEMPORARY VOLATILE FILE FORMAT my_fmt"),
-        Err(ParserError::ParserError(
+        Err(parser_error(
             "Expected: an object type after CREATE, found: FILE".to_string()
         ))
     );
@@ -1031,7 +1031,7 @@
 fn test_snowflake_create_iceberg_table_without_location() {
     let res = snowflake().parse_sql_statements("CREATE ICEBERG TABLE my_table (a INT)");
     assert_eq!(
-        ParserError::ParserError("BASE_LOCATION is required for ICEBERG tables".to_string()),
+        parser_error("BASE_LOCATION is required for ICEBERG tables".to_string()),
         res.unwrap_err()
     );
 }
@@ -1068,9 +1068,7 @@
     assert_eq!(
         snowflake_and_generic()
             .parse_sql_statements("CREATE OR REPLACE VIEW v COMMENT 'hello, world' AS SELECT 1"),
-        Err(ParserError::ParserError(
-            "Expected: =, found: 'hello, world'".to_string()
-        ))
+        Err(parser_error("Expected: =, found: 'hello, world'"))
     );
 }
 
@@ -1270,7 +1268,7 @@
 
     let res = snowflake().parse_sql_statements("SELECT * FROM (a b) c");
     assert_eq!(
-        ParserError::ParserError("duplicate alias b".to_string()),
+        parser_error("duplicate alias b".to_string()),
         res.unwrap_err()
     );
 }
@@ -1778,13 +1776,13 @@
 
     let error_sql = "DECLARE c1 CURSOR SELECT id FROM invoices";
     assert_eq!(
-        ParserError::ParserError("Expected: FOR, found: SELECT".to_owned()),
+        parser_error("Expected: FOR, found: SELECT".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 
     let error_sql = "DECLARE c1 CURSOR res";
     assert_eq!(
-        ParserError::ParserError("Expected: FOR, found: res".to_owned()),
+        parser_error("Expected: FOR, found: res".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -1832,13 +1830,13 @@
 
     let error_sql = "DECLARE res RESULTSET DEFAULT";
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: EOF".to_owned()),
+        parser_error("Expected: an expression, found: EOF".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 
     let error_sql = "DECLARE res RESULTSET :=";
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: EOF".to_owned()),
+        parser_error("Expected: an expression, found: EOF".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -1924,19 +1922,19 @@
 
     let error_sql = "DECLARE profit INT 2";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: 2".to_owned()),
+        parser_error("Expected: end of statement, found: 2".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 
     let error_sql = "DECLARE profit INT DEFAULT";
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: EOF".to_owned()),
+        parser_error("Expected: an expression, found: EOF".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 
     let error_sql = "DECLARE profit DEFAULT";
     assert_eq!(
-        ParserError::ParserError("Expected: an expression, found: EOF".to_owned()),
+        parser_error("Expected: an expression, found: EOF".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -1971,7 +1969,7 @@
 
     let error_sql = "DECLARE profit DEFAULT 42 c1 CURSOR FOR res;";
     assert_eq!(
-        ParserError::ParserError("Expected: end of statement, found: c1".to_owned()),
+        parser_error("Expected: end of statement, found: c1".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -2892,7 +2890,7 @@
     // missing comma separation
     let error_sql = "SELECT TRIM('xyz' 'a')";
     assert_eq!(
-        ParserError::ParserError("Expected: ), found: 'a'".to_owned()),
+        parser_error("Expected: ), found: 'a'".to_owned()),
         snowflake().parse_sql_statements(error_sql).unwrap_err()
     );
 }
@@ -3376,7 +3374,7 @@
     for sql in &invalid_cases {
         assert_eq!(
             snowflake().parse_sql_statements(sql).unwrap_err(),
-            ParserError::ParserError("Expected: identifier, found: EOF".to_string()),
+            parser_error("Expected: identifier, found: EOF".to_string()),
         );
     }
 
@@ -3457,7 +3455,10 @@
     let sql = format!("SELECT * FROM {l_parens}a.b.c{r_parens}");
     let parsed =
         snowflake_with_recursion_limit(max_nesting_level).parse_sql_statements(sql.as_str());
-    assert_eq!(parsed.err(), Some(ParserError::RecursionLimitExceeded));
+    assert!(matches!(
+        parsed,
+        Err(ParserError::RecursionLimitExceeded { .. })
+    ));
 }
 
 #[test]
@@ -4108,7 +4109,7 @@
     ] {
         assert_eq!(
             snowflake().parse_sql_statements(&format!("SELECT 1, {kw}")),
-            Err(ParserError::ParserError(err.to_string()))
+            Err(parser_error(err.to_string()))
         );
     }
 }
@@ -4143,7 +4144,7 @@
     ] {
         assert_eq!(
             snowflake().parse_sql_statements(&format!("SELECT 1 {kw}")),
-            Err(ParserError::ParserError(err.to_string()))
+            Err(parser_error(err.to_string()))
         );
     }
 
@@ -4240,7 +4241,7 @@
     ] {
         assert_eq!(
             snowflake().parse_sql_statements(&format!("SELECT * FROM tbl {kw}")),
-            Err(ParserError::ParserError(err.to_string()))
+            Err(parser_error(err.to_string()))
         );
     }
 
@@ -4271,9 +4272,7 @@
     // ORDER is reserved
     assert_eq!(
         snowflake().parse_sql_statements("SELECT * FROM tbl, order"),
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: order".to_string()
-        ))
+        Err(parser_error("Expected: end of statement, found: order"))
     );
 }
 
@@ -4834,15 +4833,15 @@
         snowflake().parse_sql_statements(
             "CREATE TABLE IDENTIFIER('db1').IDENTIFIER('sc1').IDENTIFIER('tbl') (id INT)"
         ),
-        Err(ParserError::ParserError(
-            "Expected: column name or constraint definition, found: 'sc1'".to_string()
+        Err(parser_error(
+            "Expected: column name or constraint definition, found: 'sc1'"
         ))
     );
     assert_eq!(
         snowflake()
             .parse_sql_statements("CREATE TABLE IDENTIFIER('db1')..IDENTIFIER('tbl') (id INT)"),
-        Err(ParserError::ParserError(
-            "Expected: column name or constraint definition, found: 'tbl'".to_string()
+        Err(parser_error(
+            "Expected: column name or constraint definition, found: 'tbl'"
         ))
     );
 }
@@ -4888,9 +4887,7 @@
     for sql in ["CREATE DATABASE", "CREATE DATABASE my_db CLONE"] {
         assert_eq!(
             snowflake().parse_sql_statements(sql),
-            Err(ParserError::ParserError(
-                "Expected: identifier, found: EOF".to_string()
-            )),
+            Err(parser_error("Expected: identifier, found: EOF")),
             "{sql}"
         );
     }
@@ -5038,8 +5035,8 @@
 
     assert_eq!(
         snowflake().parse_sql_statements("CREATE TABLE t (o OBJECT(city VARCHAR)"),
-        Err(ParserError::ParserError(
-            "Expected: ',' or ')' after column definition, found: EOF".to_string()
+        Err(parser_error(
+            "Expected: ',' or ')' after column definition, found: EOF"
         ))
     );
 
@@ -5080,24 +5077,35 @@
         snowflake()
             .parse_sql_statements("SELECT * FROM @")
             .unwrap_err(),
-        ParserError::ParserError("Expected: stage name identifier, found: EOF".to_string()),
+        parser_error("Expected: stage name identifier, found: EOF".to_string()),
     );
     assert_eq!(
         snowflake()
             .parse_sql_statements("SELECT * FROM @;")
             .unwrap_err(),
-        ParserError::ParserError("Expected: stage name identifier, found: ;".to_string()),
+        parser_error("Expected: stage name identifier, found: ;".to_string()),
     );
     assert_eq!(
         snowflake()
             .parse_sql_statements("SELECT * FROM @, item")
             .unwrap_err(),
-        ParserError::ParserError("Expected: stage name identifier, found: ,".to_string()),
+        parser_error("Expected: stage name identifier, found: ,".to_string()),
     );
     assert_eq!(
         snowflake()
             .parse_sql_statements("SELECT * FROM @.stage")
             .unwrap_err(),
-        ParserError::ParserError("Expected: stage name identifier, found: .".to_string()),
+        parser_error("Expected: stage name identifier, found: .".to_string()),
+    );
+}
+
+#[test]
+fn parse_iceberg_table_without_base_location_reports_location() {
+    assert_eq!(
+        snowflake()
+            .parse_sql_statements_with_locations("CREATE ICEBERG TABLE t (a INT)")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: BASE_LOCATION is required for ICEBERG tables at Line: 1, Column: 31"
     );
 }
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index 2e60c75..627792c 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -30,7 +30,7 @@
 use sqlparser::ast::Value::Placeholder;
 use sqlparser::ast::*;
 use sqlparser::dialect::{AnsiDialect, GenericDialect, SQLiteDialect};
-use sqlparser::parser::{ParserError, ParserOptions};
+use sqlparser::parser::ParserOptions;
 use sqlparser::tokenizer::Token;
 
 #[test]
@@ -363,7 +363,7 @@
         .unwrap_err();
     assert_eq!(
         err,
-        ParserError::ParserError(
+        parser_error(
             "Expected: one of ROLLBACK or ABORT or FAIL or IGNORE or REPLACE, found: BOH"
                 .to_string()
         )
@@ -625,9 +625,7 @@
     for sql in ["SELECT 1 REGEXP", "SELECT 1 MATCH"] {
         assert_eq!(
             sqlite().parse_sql_statements(sql),
-            Err(ParserError::ParserError(
-                "Expected: an expression, found: EOF".to_string()
-            ))
+            Err(parser_error("Expected: an expression, found: EOF"))
         );
     }
 }
@@ -649,9 +647,7 @@
     // Should return an error, not panic
     assert_eq!(
         sqlite().parse_sql_statements("SELECT 1 GLOB"),
-        Err(ParserError::ParserError(
-            "Expected: an expression, found: EOF".to_string()
-        ))
+        Err(parser_error("Expected: an expression, found: EOF"))
     );
 }
 
@@ -990,7 +986,7 @@
     // `=` still works
     sqlite().verified_stmt("UPDATE t SET a = 1");
     // Other dialects reject `==` in SET
-    let res = ParserError::ParserError("Expected: =, found: ==".to_string());
+    let res = parser_error("Expected: =, found: ==");
     assert_eq!(
         all_dialects_except(|d| d.supports_double_eq_assignment())
             .parse_sql_statements("UPDATE t SET a == 1")
@@ -1007,18 +1003,16 @@
     sqlite().verified_stmt("SELECT 𒀀");
     sqlite().verified_stmt("SELECT 𒀀𒀁");
     // U+DFFFE is not alphabetic, so GenericDialect rejects it as an identifier.
+    let err =
+        sqlparser::parser::Parser::parse_sql(&GenericDialect {}, "SELECT \u{DFFFE}").unwrap_err();
     assert_eq!(
-        sqlparser::parser::Parser::parse_sql(&GenericDialect {}, "SELECT \u{DFFFE}"),
-        Err(ParserError::ParserError(
-            "Expected: an expression, found: \u{DFFFE} at Line: 1, Column: 8".to_string()
-        ))
+        err.to_string(),
+        "sql parser error: Expected: an expression, found: \u{DFFFE} at Line: 1, Column: 8"
     );
     // SQLite rejects U+007F as an unrecognized token.
     assert_eq!(
         sqlite().parse_sql_statements("SELECT \u{007f}"),
-        Err(ParserError::ParserError(
-            "Expected: an expression, found: \u{7f}".to_string()
-        ))
+        Err(parser_error("Expected: an expression, found: \u{7f}"))
     );
 }
 
@@ -1030,12 +1024,11 @@
         r#"CREATE TABLE '""' ('id' INT UNSIGNED NOT NULL, 'name' TEXT NOT NULL, 'zip' INT UNSIGNED NULL)"#,
     );
     // Generic dialect does not support this
+    let err = sqlparser::parser::Parser::parse_sql(&GenericDialect {}, "CREATE TABLE t ('a')")
+        .unwrap_err();
     assert_eq!(
-        sqlparser::parser::Parser::parse_sql(&GenericDialect {}, "CREATE TABLE t ('a')"),
-        Err(ParserError::ParserError(
-            "Expected: column name or constraint definition, found: 'a' at Line: 1, Column: 17"
-                .to_string()
-        ))
+        err.to_string(),
+        "sql parser error: Expected: column name or constraint definition, found: 'a' at Line: 1, Column: 17"
     );
 }
 
@@ -1050,7 +1043,7 @@
         TestedDialects::new(vec![Box::new(GenericDialect {})])
             .parse_sql_statements("SELECT CAST(a AS)")
             .unwrap_err(),
-        ParserError::ParserError("Expected: a data type name, found: )".to_string())
+        parser_error("Expected: a data type name, found: )")
     );
 }
 
@@ -1136,9 +1129,7 @@
     assert_eq!(
         all_dialects_where(|d| !d.supports_parens_around_table_factor())
             .parse_sql_statements("SELECT * FROM (t)"),
-        Err(ParserError::ParserError(
-            "Expected: joined table, found: )".to_string()
-        ))
+        Err(parser_error("Expected: joined table, found: )"))
     );
 }
 
diff --git a/tests/sqlparser_teradata.rs b/tests/sqlparser_teradata.rs
index 86e7f06..838f938 100644
--- a/tests/sqlparser_teradata.rs
+++ b/tests/sqlparser_teradata.rs
@@ -18,9 +18,8 @@
 //! Test SQL syntax, specific to [sqlparser::dialect::TeradataDialect].
 
 use sqlparser::dialect::{Dialect, GenericDialect, TeradataDialect};
-use sqlparser::parser::ParserError;
 use sqlparser::test_utils::all_dialects_where;
-use test_utils::TestedDialects;
+use test_utils::{parser_error, TestedDialects};
 
 mod test_utils;
 
@@ -120,8 +119,6 @@
         all_dialects_where(|d| !d.supports_leading_comma_before_table_options());
     assert_eq!(
         unsupported_dialects.parse_sql_statements("CREATE TABLE foo, FALLBACK (id INT)"),
-        Err(ParserError::ParserError(
-            "Expected: end of statement, found: ,".to_string()
-        ))
+        Err(parser_error("Expected: end of statement, found: ,"))
     );
 }