From 1abf777e3bbfedbf846b3ac7e873c54b8b4df365 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sat, 8 Nov 2025 12:23:14 +0100 Subject: [PATCH 1/7] Include UPDATE keyword in statement span --- src/ast/dml.rs | 9 ++++++--- src/ast/spans.rs | 20 ++++++++++++++++++++ src/parser/mod.rs | 14 +++++++++----- tests/sqlparser_common.rs | 16 ++++++++++++++-- tests/sqlparser_mysql.rs | 10 +++++++++- tests/sqlparser_sqlite.rs | 9 +++++++-- 6 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index c0bfcb19f..966399342 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -27,9 +27,10 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ - display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, - InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, - Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, + display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, + Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, + OrderByExpr, Query, SelectItem, Setting, SqliteOnConflict, TableObject, TableWithJoins, + UpdateTableFromKind, }; /// INSERT statement. @@ -246,6 +247,8 @@ impl Display for Delete { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Update { + /// Token for the `UPDATE` keyword + pub update_token: AttachedToken, /// TABLE pub table: TableWithJoins, /// Column assignments diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 719e261cb..487245bb3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -869,6 +869,7 @@ impl Spanned for Delete { impl Spanned for Update { fn span(&self) -> Span { let Update { + update_token, table, assignments, from, @@ -880,6 +881,7 @@ impl Spanned for Update { union_spans( core::iter::once(table.span()) + .chain(core::iter::once(update_token.0.span)) .chain(assignments.iter().map(|i| i.span())) .chain(from.iter().map(|i| i.span())) .chain(selection.iter().map(|i| i.span())) @@ -2540,4 +2542,22 @@ ALTER TABLE users assert_eq!(stmt_span.start, (2, 13).into()); assert_eq!(stmt_span.end, (4, 11).into()); } + + #[test] + fn test_update_statement_span() { + let sql = r#"-- foo + UPDATE foo + /* bar */ + SET bar = 3 + WHERE quux > 42 ; +"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 7).into()); + assert_eq!(stmt_span.end, (5, 17).into()); + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9615343c2..dc306a06b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -590,7 +590,7 @@ impl<'a> Parser<'a> { Keyword::INSERT => self.parse_insert(), Keyword::REPLACE => self.parse_replace(), Keyword::UNCACHE => self.parse_uncache_table(), - Keyword::UPDATE => self.parse_update(), + Keyword::UPDATE => self.parse_update(next_token), Keyword::ALTER => self.parse_alter(), Keyword::CALL => self.parse_call(), Keyword::COPY => self.parse_copy(), @@ -12014,7 +12014,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { with, - body: self.parse_update_setexpr_boxed()?, + body: self.parse_update_setexpr_boxed(self.get_current_token().clone())?, order_by: None, limit_clause: None, fetch: None, @@ -15742,11 +15742,14 @@ impl<'a> Parser<'a> { /// Parse an UPDATE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_update_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Update(self.parse_update()?))) + fn parse_update_setexpr_boxed( + &mut self, + update_token: TokenWithSpan, + ) -> Result, ParserError> { + Ok(Box::new(SetExpr::Update(self.parse_update(update_token)?))) } - pub fn parse_update(&mut self) -> Result { + pub fn parse_update(&mut self, update_token: TokenWithSpan) -> Result { let or = self.parse_conflict_clause(); let table = self.parse_table_and_joins()?; let from_before_set = if self.parse_keyword(Keyword::FROM) { @@ -15781,6 +15784,7 @@ impl<'a> Parser<'a> { None }; Ok(Update { + update_token: update_token.into(), table, assignments, from, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a235c392c..5d42bfda1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -39,8 +39,8 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; -use sqlparser::tokenizer::Tokenizer; -use sqlparser::tokenizer::{Location, Span}; +use sqlparser::tokenizer::{Location, Span, TokenWithSpan}; +use sqlparser::tokenizer::{Token, Tokenizer}; 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, @@ -456,6 +456,10 @@ fn parse_update_set_from() { assert_eq!( stmt, Statement::Update(Update { + update_token: AttachedToken(TokenWithSpan { + token: Token::make_keyword("UPDATE"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -551,6 +555,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, + update_token, }) => { assert_eq!( TableWithJoins { @@ -599,6 +604,13 @@ fn parse_update_with_table_alias() { selection ); assert_eq!(None, returning); + assert_eq!( + AttachedToken(TokenWithSpan { + token: Token::make_keyword("UPDATE"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), + update_token + ); } _ => unreachable!(), } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b31a5b7c0..bda12dc06 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -26,8 +26,8 @@ use sqlparser::ast::MysqlInsertPriority::{Delayed, HighPriority, LowPriority}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::Span; use sqlparser::tokenizer::Token; +use sqlparser::tokenizer::{Span, TokenWithSpan}; use test_utils::*; #[macro_use] @@ -2632,6 +2632,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, + update_token, }) => { assert_eq!( TableWithJoins { @@ -2706,6 +2707,13 @@ fn parse_update_with_joins() { selection ); assert_eq!(None, returning); + assert_eq!( + AttachedToken(TokenWithSpan { + token: Token::make_keyword("UPDATE"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), + update_token + ); } _ => unreachable!(), } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index f1f6cf49b..14daad2d9 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -22,6 +22,7 @@ #[macro_use] mod test_utils; +use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::keywords::Keyword; use test_utils::*; @@ -30,7 +31,7 @@ use sqlparser::ast::Value::Placeholder; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::Token; +use sqlparser::tokenizer::{Span, Token, TokenWithSpan}; #[test] fn pragma_no_value() { @@ -494,7 +495,11 @@ fn parse_update_tuple_row_values() { }, from: None, returning: None, - limit: None + limit: None, + update_token: AttachedToken(TokenWithSpan { + token: Token::make_keyword("UPDATE"), + span: Span::new((1, 1).into(), (1, 7).into()) + }) }) ); } From 78754548bf46e1fa1bd6819b8588fd64f87cc560 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sat, 8 Nov 2025 12:54:56 +0100 Subject: [PATCH 2/7] Include DELETE keyword in statement span --- src/ast/dml.rs | 2 ++ src/ast/spans.rs | 4 +++- src/parser/mod.rs | 11 ++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 966399342..821a83245 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -180,6 +180,8 @@ impl Display for Insert { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Delete { + /// Token for the `DELETE` keyword + pub delete_token: AttachedToken, /// Multi tables delete are supported in mysql pub tables: Vec, /// FROM diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 487245bb3..71f8925c3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -839,6 +839,7 @@ impl Spanned for CopySource { impl Spanned for Delete { fn span(&self) -> Span { let Delete { + delete_token, tables, from, using, @@ -849,6 +850,7 @@ impl Spanned for Delete { } = self; union_spans( + core::iter::once(delete_token.0.span).chain( tables .iter() .map(|i| i.span()) @@ -862,7 +864,7 @@ impl Spanned for Delete { .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) .chain(order_by.iter().map(|i| i.span())) .chain(limit.iter().map(|i| i.span())), - ) + )) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dc306a06b..ddc46eb57 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -586,7 +586,7 @@ impl<'a> Parser<'a> { Keyword::DISCARD => self.parse_discard(), Keyword::DECLARE => self.parse_declare(), Keyword::FETCH => self.parse_fetch_statement(), - Keyword::DELETE => self.parse_delete(), + Keyword::DELETE => self.parse_delete(next_token), Keyword::INSERT => self.parse_insert(), Keyword::REPLACE => self.parse_replace(), Keyword::UNCACHE => self.parse_uncache_table(), @@ -11817,8 +11817,8 @@ impl<'a> Parser<'a> { /// Parse a DELETE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_delete_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Delete(self.parse_delete()?))) + fn parse_delete_setexpr_boxed(&mut self, delete_token: TokenWithSpan) -> Result, ParserError> { + Ok(Box::new(SetExpr::Delete(self.parse_delete(delete_token)?))) } /// Parse a MERGE statement, returning a `Box`ed SetExpr @@ -11828,7 +11828,7 @@ impl<'a> Parser<'a> { Ok(Box::new(SetExpr::Merge(self.parse_merge()?))) } - pub fn parse_delete(&mut self) -> Result { + pub fn parse_delete(&mut self, delete_token: TokenWithSpan) -> Result { let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. // https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement @@ -11871,6 +11871,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::Delete(Delete { + delete_token: delete_token.into(), tables, from: if with_from_keyword { FromTable::WithFromKeyword(from) @@ -12028,7 +12029,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DELETE) { Ok(Query { with, - body: self.parse_delete_setexpr_boxed()?, + body: self.parse_delete_setexpr_boxed(self.get_current_token().clone())?, limit_clause: None, order_by: None, fetch: None, From 40649e78e6572b3e6e0a744e21f88f16112a6f08 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sat, 8 Nov 2025 13:13:43 +0100 Subject: [PATCH 3/7] Include INSERT keyword in statement span --- src/ast/dml.rs | 2 ++ src/ast/spans.rs | 2 ++ src/dialect/sqlite.rs | 2 +- src/parser/mod.rs | 17 +++++++++-------- tests/sqlparser_postgres.rs | 14 +++++++++++++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 821a83245..d6009ce8a 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -38,6 +38,8 @@ use super::{ #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Insert { + /// Token for the `INSERT` keyword (or its substitutes) + pub insert_token: AttachedToken, /// Only for Sqlite pub or: Option, /// Only for mysql diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 71f8925c3..b95f30d73 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1221,6 +1221,7 @@ impl Spanned for AlterIndexOperation { impl Spanned for Insert { fn span(&self) -> Span { let Insert { + insert_token, or: _, // enum, sqlite specific ignore: _, // bool into: _, // bool @@ -1244,6 +1245,7 @@ impl Spanned for Insert { union_spans( core::iter::once(table.span()) + .chain(core::iter::once(insert_token.0.span)) .chain(table_alias.as_ref().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 64a8d532f..ba4cb6173 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -68,7 +68,7 @@ impl Dialect for SQLiteDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::REPLACE) { parser.prev_token(); - Some(parser.parse_insert()) + Some(parser.parse_insert(parser.get_current_token().clone())) } else { None } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ddc46eb57..0a6151fa7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -587,8 +587,8 @@ impl<'a> Parser<'a> { Keyword::DECLARE => self.parse_declare(), Keyword::FETCH => self.parse_fetch_statement(), Keyword::DELETE => self.parse_delete(next_token), - Keyword::INSERT => self.parse_insert(), - Keyword::REPLACE => self.parse_replace(), + Keyword::INSERT => self.parse_insert(next_token), + Keyword::REPLACE => self.parse_replace(next_token), Keyword::UNCACHE => self.parse_uncache_table(), Keyword::UPDATE => self.parse_update(next_token), Keyword::ALTER => self.parse_alter(), @@ -12001,7 +12001,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::INSERT) { Ok(Query { with, - body: self.parse_insert_setexpr_boxed()?, + body: self.parse_insert_setexpr_boxed(self.get_current_token().clone())?, order_by: None, limit_clause: None, fetch: None, @@ -15459,7 +15459,7 @@ impl<'a> Parser<'a> { } /// Parse an REPLACE statement - pub fn parse_replace(&mut self) -> Result { + pub fn parse_replace(&mut self, replace_token: TokenWithSpan) -> Result { if !dialect_of!(self is MySqlDialect | GenericDialect) { return parser_err!( "Unsupported statement REPLACE", @@ -15467,7 +15467,7 @@ impl<'a> Parser<'a> { ); } - let mut insert = self.parse_insert()?; + let mut insert = self.parse_insert(replace_token)?; if let Statement::Insert(Insert { replace_into, .. }) = &mut insert { *replace_into = true; } @@ -15478,12 +15478,12 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_insert_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Insert(self.parse_insert()?))) + fn parse_insert_setexpr_boxed(&mut self, insert_token: TokenWithSpan) -> Result, ParserError> { + Ok(Box::new(SetExpr::Insert(self.parse_insert(insert_token)?))) } /// Parse an INSERT statement - pub fn parse_insert(&mut self) -> Result { + pub fn parse_insert(&mut self, insert_token: TokenWithSpan) -> Result { let or = self.parse_conflict_clause(); let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { None @@ -15652,6 +15652,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::Insert(Insert { + insert_token: insert_token.into(), or, table: table_object, table_alias, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 87cb43edd..da31af58f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -23,7 +23,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::Span; +use sqlparser::tokenizer::{Span, Token, TokenWithSpan}; use test_utils::*; use sqlparser::ast::*; @@ -5140,6 +5140,10 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken(TokenWithSpan { + token: Token::make_keyword("INSERT"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), or: None, ignore: false, into: true, @@ -5210,6 +5214,10 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken(TokenWithSpan { + token: Token::make_keyword("INSERT"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), or: None, ignore: false, into: true, @@ -5282,6 +5290,10 @@ fn test_simple_insert_with_quoted_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken(TokenWithSpan { + token: Token::make_keyword("INSERT"), + span: Span::new((1, 1).into(), (1, 7).into()), + }), or: None, ignore: false, into: true, From b6f1fb208cf616943842778cffe6f5eda80873f2 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sat, 8 Nov 2025 13:23:23 +0100 Subject: [PATCH 4/7] Cargo format --- src/ast/spans.rs | 29 +++++++++++++++-------------- src/parser/mod.rs | 15 ++++++++++++--- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b95f30d73..988b875a9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -851,20 +851,21 @@ impl Spanned for Delete { union_spans( core::iter::once(delete_token.0.span).chain( - tables - .iter() - .map(|i| i.span()) - .chain(core::iter::once(from.span())) - .chain( - using - .iter() - .map(|u| union_spans(u.iter().map(|i| i.span()))), - ) - .chain(selection.iter().map(|i| i.span())) - .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) - .chain(order_by.iter().map(|i| i.span())) - .chain(limit.iter().map(|i| i.span())), - )) + tables + .iter() + .map(|i| i.span()) + .chain(core::iter::once(from.span())) + .chain( + using + .iter() + .map(|u| union_spans(u.iter().map(|i| i.span()))), + ) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(order_by.iter().map(|i| i.span())) + .chain(limit.iter().map(|i| i.span())), + ), + ) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0a6151fa7..733368575 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11817,7 +11817,10 @@ impl<'a> Parser<'a> { /// Parse a DELETE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_delete_setexpr_boxed(&mut self, delete_token: TokenWithSpan) -> Result, ParserError> { + fn parse_delete_setexpr_boxed( + &mut self, + delete_token: TokenWithSpan, + ) -> Result, ParserError> { Ok(Box::new(SetExpr::Delete(self.parse_delete(delete_token)?))) } @@ -15459,7 +15462,10 @@ impl<'a> Parser<'a> { } /// Parse an REPLACE statement - pub fn parse_replace(&mut self, replace_token: TokenWithSpan) -> Result { + pub fn parse_replace( + &mut self, + replace_token: TokenWithSpan, + ) -> Result { if !dialect_of!(self is MySqlDialect | GenericDialect) { return parser_err!( "Unsupported statement REPLACE", @@ -15478,7 +15484,10 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_insert_setexpr_boxed(&mut self, insert_token: TokenWithSpan) -> Result, ParserError> { + fn parse_insert_setexpr_boxed( + &mut self, + insert_token: TokenWithSpan, + ) -> Result, ParserError> { Ok(Box::new(SetExpr::Insert(self.parse_insert(insert_token)?))) } From 80f1f952b1b12bbda5326f5f06b697c07a150747 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Tue, 11 Nov 2025 12:02:44 +0100 Subject: [PATCH 5/7] Drop redundant/unused values (in tests) --- tests/sqlparser_common.rs | 18 ++++-------------- tests/sqlparser_mysql.rs | 11 ++--------- tests/sqlparser_postgres.rs | 17 ++++------------- tests/sqlparser_sqlite.rs | 7 ++----- 4 files changed, 12 insertions(+), 41 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5d42bfda1..003d29b7e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -39,8 +39,8 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; -use sqlparser::tokenizer::{Location, Span, TokenWithSpan}; -use sqlparser::tokenizer::{Token, Tokenizer}; +use sqlparser::tokenizer::{Location, Span}; +use sqlparser::tokenizer::Tokenizer; 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, @@ -456,10 +456,7 @@ fn parse_update_set_from() { assert_eq!( stmt, Statement::Update(Update { - update_token: AttachedToken(TokenWithSpan { - token: Token::make_keyword("UPDATE"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), + update_token: AttachedToken::empty(), table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -555,7 +552,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, - update_token, + update_token: _, }) => { assert_eq!( TableWithJoins { @@ -604,13 +601,6 @@ fn parse_update_with_table_alias() { selection ); assert_eq!(None, returning); - assert_eq!( - AttachedToken(TokenWithSpan { - token: Token::make_keyword("UPDATE"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), - update_token - ); } _ => unreachable!(), } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index bda12dc06..09c99a1f1 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -27,7 +27,7 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::Token; -use sqlparser::tokenizer::{Span, TokenWithSpan}; +use sqlparser::tokenizer::Span; use test_utils::*; #[macro_use] @@ -2632,7 +2632,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, - update_token, + update_token: _, }) => { assert_eq!( TableWithJoins { @@ -2707,13 +2707,6 @@ fn parse_update_with_joins() { selection ); assert_eq!(None, returning); - assert_eq!( - AttachedToken(TokenWithSpan { - token: Token::make_keyword("UPDATE"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), - update_token - ); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index da31af58f..15c26a96a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -23,7 +23,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::{Span, Token, TokenWithSpan}; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::*; @@ -5140,10 +5140,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { - insert_token: AttachedToken(TokenWithSpan { - token: Token::make_keyword("INSERT"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, @@ -5214,10 +5211,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { - insert_token: AttachedToken(TokenWithSpan { - token: Token::make_keyword("INSERT"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, @@ -5290,10 +5284,7 @@ fn test_simple_insert_with_quoted_alias() { assert_eq!( statement, Statement::Insert(Insert { - insert_token: AttachedToken(TokenWithSpan { - token: Token::make_keyword("INSERT"), - span: Span::new((1, 1).into(), (1, 7).into()), - }), + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 14daad2d9..321cfef07 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -31,7 +31,7 @@ use sqlparser::ast::Value::Placeholder; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::{Span, Token, TokenWithSpan}; +use sqlparser::tokenizer::Token; #[test] fn pragma_no_value() { @@ -496,10 +496,7 @@ fn parse_update_tuple_row_values() { from: None, returning: None, limit: None, - update_token: AttachedToken(TokenWithSpan { - token: Token::make_keyword("UPDATE"), - span: Span::new((1, 1).into(), (1, 7).into()) - }) + update_token: AttachedToken::empty() }) ); } From 9e79bcb1e8831a0bf098537428c2773979066f10 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Tue, 11 Nov 2025 13:55:27 +0100 Subject: [PATCH 6/7] Add test coverage for INSERT/DELETE statement spans --- src/ast/spans.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 988b875a9..674030060 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1245,8 +1245,8 @@ impl Spanned for Insert { } = self; union_spans( - core::iter::once(table.span()) - .chain(core::iter::once(insert_token.0.span)) + core::iter::once(insert_token.0.span) + .chain(core::iter::once(table.span())) .chain(table_alias.as_ref().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) @@ -2565,4 +2565,55 @@ ALTER TABLE users assert_eq!(stmt_span.start, (2, 7).into()); assert_eq!(stmt_span.end, (5, 17).into()); } + + #[test] + fn test_insert_statement_span() { + let sql = r#" +/* foo */ INSERT INTO FOO (X, Y, Z) VALUES (1, 2, 3 ) +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 11).into()); + // XXX to be enabled once #2050 is fixed + // assert_eq!(stmt_span.end, (2, 60).into()); + } + + #[test] + fn test_replace_statement_span() { + let sql = r#" +/* foo */ REPLACE INTO public.customer (id, name, active) VALUES (1, 2, 3) +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + dbg!(&r[0]); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 11).into()); + // XXX to be enabled once #2050 is fixed + // assert_eq!(stmt_span.end, (2, 75).into()); + } + + #[test] + fn test_delete_statement_span() { + let sql = r#"-- foo + DELETE /* quux */ + FROM foo + WHERE foo.x = 42 +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 7).into()); + assert_eq!(stmt_span.end, (4, 24).into()); + } } From e447bd3c8ebbe9ff7a417feebaea7969ad3d43d4 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Tue, 11 Nov 2025 14:05:01 +0100 Subject: [PATCH 7/7] Cargo fmt --- tests/sqlparser_common.rs | 2 +- tests/sqlparser_mysql.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 003d29b7e..3a4bf90e6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -39,8 +39,8 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; -use sqlparser::tokenizer::{Location, Span}; use sqlparser::tokenizer::Tokenizer; +use sqlparser::tokenizer::{Location, Span}; 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, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 09c99a1f1..bc5d48baa 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -26,8 +26,8 @@ use sqlparser::ast::MysqlInsertPriority::{Delayed, HighPriority, LowPriority}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::Token; use sqlparser::tokenizer::Span; +use sqlparser::tokenizer::Token; use test_utils::*; #[macro_use]