Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Mysql REPLACE statement and PRIORITY clause of INSERT #1072

Merged
merged 7 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,10 @@ pub enum Statement {
on: Option<OnInsert>,
/// RETURNING
returning: Option<Vec<SelectItem>>,
/// Only for mysql
replace_into: bool,
/// Only for mysql
priority: Option<MysqlInsertPriority>,
},
// TODO: Support ROW FORMAT
Directory {
Expand Down Expand Up @@ -2404,18 +2408,26 @@ 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}{prior}{ignore}{over}{int}{tbl} {table_name} ",
emin100 marked this conversation as resolved.
Show resolved Hide resolved
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 { "" },
start = if *replace_into { "REPLACE" } else { "INSERT" },
prior = if let Some(priority) = priority {
" ".to_string() + &priority.to_string()
} else {
"".to_string()
},
)?;
}
if !columns.is_empty() {
Expand Down Expand Up @@ -4522,6 +4534,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))]
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ define_keywords!(
DECLARE,
DEFAULT,
DEFERRED,
DELAYED,
DELETE,
DELIMITED,
DELIMITER,
Expand Down Expand Up @@ -315,6 +316,7 @@ define_keywords!(
HASH,
HAVING,
HEADER,
HIGH_PRIORITY,
HISTORY,
HIVEVAR,
HOLD,
Expand Down
42 changes: 42 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?),
Expand Down Expand Up @@ -7379,6 +7380,31 @@ impl<'a> Parser<'a> {
})
}

/// Parse an REPLACE statement
pub fn parse_replace(&mut self) -> Result<Statement, ParserError> {
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,
priority,
..
} = insert
{
if *priority == Some(MysqlInsertPriority::HighPriority) {
emin100 marked this conversation as resolved.
Show resolved Hide resolved
return parser_err!(
"Unmatched priority type for replace statement",
self.peek_token().location
);
}
*replace_into = true;
}

Ok(insert.clone())
}

/// Parse an INSERT statement
pub fn parse_insert(&mut self) -> Result<Statement, ParserError> {
let or = if !dialect_of!(self is SQLiteDialect) {
Expand All @@ -7399,9 +7425,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);
Expand Down Expand Up @@ -7511,6 +7551,8 @@ impl<'a> Parser<'a> {
table,
on,
returning,
replace_into,
priority,
})
}
}
Expand Down
11 changes: 11 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
125 changes: 125 additions & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1035,6 +1036,130 @@ fn parse_ignore_insert() {
}
}

#[test]
emin100 marked this conversation as resolved.
Show resolved Hide resolved
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 (), ()";
Expand Down
Loading