diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e9480d727..1112236a1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1443,6 +1443,10 @@ pub enum Statement { on: Option, /// RETURNING returning: Option>, + /// Only for mysql + replace_into: bool, + /// Only for mysql + priority: Option, }, // TODO: Support ROW FORMAT Directory { @@ -2404,18 +2408,29 @@ impl fmt::Display for Statement { table, on, returning, + replace_into, + priority, } => { if let Some(action) = or { write!(f, "INSERT OR {action} INTO {table_name} ")?; } else { write!( f, - "INSERT{ignore}{over}{int}{tbl} {table_name} ", + "{start}", + start = if *replace_into { "REPLACE" } else { "INSERT" }, + )?; + if let Some(priority) = priority { + write!(f, " {priority}",)?; + } + + write!( + f, + "{ignore}{over}{int}{tbl} {table_name} ", table_name = table_name, ignore = if *ignore { " IGNORE" } else { "" }, over = if *overwrite { " OVERWRITE" } else { "" }, int = if *into { " INTO" } else { "" }, - tbl = if *table { " TABLE" } else { "" } + tbl = if *table { " TABLE" } else { "" }, )?; } if !columns.is_empty() { @@ -4522,6 +4537,31 @@ impl fmt::Display for SqliteOnConflict { } } +/// Mysql specific syntax +/// +/// See [Mysql documentation](https://dev.mysql.com/doc/refman/8.0/en/replace.html) +/// See [Mysql documentation](https://dev.mysql.com/doc/refman/8.0/en/insert.html) +/// for more details. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MysqlInsertPriority { + LowPriority, + Delayed, + HighPriority, +} + +impl fmt::Display for crate::ast::MysqlInsertPriority { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use MysqlInsertPriority::*; + match self { + LowPriority => write!(f, "LOW_PRIORITY"), + Delayed => write!(f, "DELAYED"), + HighPriority => write!(f, "HIGH_PRIORITY"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/keywords.rs b/src/keywords.rs index 32721c038..a6a29159e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -210,6 +210,7 @@ define_keywords!( DECLARE, DEFAULT, DEFERRED, + DELAYED, DELETE, DELIMITED, DELIMITER, @@ -315,6 +316,7 @@ define_keywords!( HASH, HAVING, HEADER, + HIGH_PRIORITY, HISTORY, HIVEVAR, HOLD, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d6bc66e4b..853ab3d17 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -490,6 +490,7 @@ impl<'a> Parser<'a> { Keyword::FETCH => Ok(self.parse_fetch_statement()?), Keyword::DELETE => Ok(self.parse_delete()?), Keyword::INSERT => Ok(self.parse_insert()?), + Keyword::REPLACE => Ok(self.parse_replace()?), Keyword::UNCACHE => Ok(self.parse_uncache_table()?), Keyword::UPDATE => Ok(self.parse_update()?), Keyword::ALTER => Ok(self.parse_alter()?), @@ -7379,6 +7380,20 @@ impl<'a> Parser<'a> { }) } + /// Parse an REPLACE statement + pub fn parse_replace(&mut self) -> Result { + if !dialect_of!(self is MySqlDialect | GenericDialect) { + return parser_err!("Unsupported statement REPLACE", self.peek_token().location); + } + + let insert = &mut self.parse_insert().unwrap(); + if let Statement::Insert { replace_into, .. } = insert { + *replace_into = true; + } + + Ok(insert.clone()) + } + /// Parse an INSERT statement pub fn parse_insert(&mut self) -> Result { let or = if !dialect_of!(self is SQLiteDialect) { @@ -7399,9 +7414,23 @@ impl<'a> Parser<'a> { None }; + let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { + None + } else if self.parse_keyword(Keyword::LOW_PRIORITY) { + Some(MysqlInsertPriority::LowPriority) + } else if self.parse_keyword(Keyword::DELAYED) { + Some(MysqlInsertPriority::Delayed) + } else if self.parse_keyword(Keyword::HIGH_PRIORITY) { + Some(MysqlInsertPriority::HighPriority) + } else { + None + }; + let ignore = dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_keyword(Keyword::IGNORE); + let replace_into = false; + let action = self.parse_one_of_keywords(&[Keyword::INTO, Keyword::OVERWRITE]); let into = action == Some(Keyword::INTO); let overwrite = action == Some(Keyword::OVERWRITE); @@ -7511,6 +7540,8 @@ impl<'a> Parser<'a> { table, on, returning, + replace_into, + priority, }) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fea90c4c6..6d393d615 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -107,6 +107,17 @@ fn parse_insert_values() { verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)"); } +#[test] +fn parse_replace_into() { + let dialect = PostgreSqlDialect {}; + 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(), + ) +} + #[test] fn parse_insert_default_values() { let insert_with_default_values = verified_stmt("INSERT INTO test_table DEFAULT VALUES"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index cedac1b2c..bb53d232a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -16,6 +16,7 @@ use matches::assert_matches; use sqlparser::ast::Expr; +use sqlparser::ast::MysqlInsertPriority::{Delayed, HighPriority, LowPriority}; use sqlparser::ast::Value; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; @@ -1035,6 +1036,130 @@ fn parse_ignore_insert() { } } +#[test] +fn parse_priority_insert() { + let sql = r"INSERT HIGH_PRIORITY INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; + + match mysql_and_generic().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + source, + on, + priority, + .. + } => { + assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); + assert!(on.is_none()); + assert_eq!(priority, Some(HighPriority)); + assert_eq!( + Some(Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), + Expr::Value(number("1")) + ]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + })), + source + ); + } + _ => unreachable!(), + } + + let sql2 = r"INSERT LOW_PRIORITY INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; + + match mysql().verified_stmt(sql2) { + Statement::Insert { + table_name, + columns, + source, + on, + priority, + .. + } => { + assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); + assert!(on.is_none()); + assert_eq!(priority, Some(LowPriority)); + assert_eq!( + Some(Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), + Expr::Value(number("1")) + ]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + })), + source + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_replace_insert() { + let sql = r"REPLACE DELAYED INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; + match mysql().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + source, + on, + replace_into, + priority, + .. + } => { + assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); + assert!(on.is_none()); + assert!(replace_into); + assert_eq!(priority, Some(Delayed)); + assert_eq!( + Some(Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), + Expr::Value(number("1")) + ]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + })), + source + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_empty_row_insert() { let sql = "INSERT INTO tb () VALUES (), ()";