Add support for MySQL's INSERT INTO ... SET syntax (#1641)

This commit is contained in:
Yoav Cohen 2025-01-06 20:13:38 +01:00 committed by GitHub
parent 4c6af0ae4f
commit 8cfc46277f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 43 additions and 20 deletions

View file

@ -32,8 +32,8 @@ use sqlparser_derive::{Visit, VisitMut};
pub use super::ddl::{ColumnDef, TableConstraint}; pub use super::ddl::{ColumnDef, TableConstraint};
use super::{ use super::{
display_comma_separated, display_separated, ClusteredBy, CommentDef, Expr, FileFormat, display_comma_separated, display_separated, Assignment, ClusteredBy, CommentDef, Expr,
FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident,
InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens,
OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine,
TableWithJoins, Tag, WrappedCollection, TableWithJoins, Tag, WrappedCollection,
@ -480,6 +480,9 @@ pub struct Insert {
pub overwrite: bool, pub overwrite: bool,
/// A SQL query that specifies what to insert /// A SQL query that specifies what to insert
pub source: Option<Box<Query>>, pub source: Option<Box<Query>>,
/// MySQL `INSERT INTO ... SET`
/// See: <https://dev.mysql.com/doc/refman/8.4/en/insert.html>
pub assignments: Vec<Assignment>,
/// partitioned insert (Hive) /// partitioned insert (Hive)
pub partitioned: Option<Vec<Expr>>, pub partitioned: Option<Vec<Expr>>,
/// Columns defined after PARTITION /// Columns defined after PARTITION
@ -545,9 +548,10 @@ impl Display for Insert {
if let Some(source) = &self.source { if let Some(source) = &self.source {
write!(f, "{source}")?; write!(f, "{source}")?;
} } else if !self.assignments.is_empty() {
write!(f, "SET ")?;
if self.source.is_none() && self.columns.is_empty() { write!(f, "{}", display_comma_separated(&self.assignments))?;
} else if self.source.is_none() && self.columns.is_empty() {
write!(f, "DEFAULT VALUES")?; write!(f, "DEFAULT VALUES")?;
} }

View file

@ -1153,6 +1153,7 @@ impl Spanned for Insert {
replace_into: _, // bool replace_into: _, // bool
priority: _, // todo, mysql specific priority: _, // todo, mysql specific
insert_alias: _, // todo, mysql specific insert_alias: _, // todo, mysql specific
assignments,
} = self; } = self;
union_spans( union_spans(
@ -1160,6 +1161,7 @@ impl Spanned for Insert {
.chain(table_alias.as_ref().map(|i| i.span)) .chain(table_alias.as_ref().map(|i| i.span))
.chain(columns.iter().map(|i| i.span)) .chain(columns.iter().map(|i| i.span))
.chain(source.as_ref().map(|q| q.span())) .chain(source.as_ref().map(|q| q.span()))
.chain(assignments.iter().map(|i| i.span()))
.chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span()))) .chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span())))
.chain(after_columns.iter().map(|i| i.span)) .chain(after_columns.iter().map(|i| i.span))
.chain(on.as_ref().map(|i| i.span())) .chain(on.as_ref().map(|i| i.span()))

View file

@ -775,6 +775,13 @@ pub trait Dialect: Debug + Any {
fn supports_table_sample_before_alias(&self) -> bool { fn supports_table_sample_before_alias(&self) -> bool {
false false
} }
/// Returns true if this dialect supports the `INSERT INTO ... SET col1 = 1, ...` syntax.
///
/// MySQL: <https://dev.mysql.com/doc/refman/8.4/en/insert.html>
fn supports_insert_set(&self) -> bool {
false
}
} }
/// This represents the operators for which precedence must be defined /// This represents the operators for which precedence must be defined

View file

@ -98,10 +98,15 @@ impl Dialect for MySqlDialect {
true true
} }
/// see <https://dev.mysql.com/doc/refman/8.4/en/create-table-select.html> /// See: <https://dev.mysql.com/doc/refman/8.4/en/create-table-select.html>
fn supports_create_table_select(&self) -> bool { fn supports_create_table_select(&self) -> bool {
true true
} }
/// See: <https://dev.mysql.com/doc/refman/8.4/en/insert.html>
fn supports_insert_set(&self) -> bool {
true
}
} }
/// `LOCK TABLES` /// `LOCK TABLES`

View file

@ -11899,9 +11899,9 @@ impl<'a> Parser<'a> {
let is_mysql = dialect_of!(self is MySqlDialect); let is_mysql = dialect_of!(self is MySqlDialect);
let (columns, partitioned, after_columns, source) = let (columns, partitioned, after_columns, source, assignments) =
if self.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) { if self.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) {
(vec![], None, vec![], None) (vec![], None, vec![], None, vec![])
} else { } else {
let (columns, partitioned, after_columns) = if !self.peek_subquery_start() { let (columns, partitioned, after_columns) = if !self.peek_subquery_start() {
let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?;
@ -11918,9 +11918,14 @@ impl<'a> Parser<'a> {
Default::default() Default::default()
}; };
let source = Some(self.parse_query()?); let (source, assignments) =
if self.dialect.supports_insert_set() && self.parse_keyword(Keyword::SET) {
(None, self.parse_comma_separated(Parser::parse_assignment)?)
} else {
(Some(self.parse_query()?), vec![])
};
(columns, partitioned, after_columns, source) (columns, partitioned, after_columns, source, assignments)
}; };
let insert_alias = if dialect_of!(self is MySqlDialect | GenericDialect) let insert_alias = if dialect_of!(self is MySqlDialect | GenericDialect)
@ -12000,6 +12005,7 @@ impl<'a> Parser<'a> {
columns, columns,
after_columns, after_columns,
source, source,
assignments,
table, table,
on, on,
returning, returning,
@ -14228,16 +14234,6 @@ mod tests {
assert!(Parser::parse_sql(&GenericDialect {}, sql).is_err()); assert!(Parser::parse_sql(&GenericDialect {}, sql).is_err());
} }
#[test]
fn test_replace_into_set() {
// NOTE: This is actually valid MySQL syntax, REPLACE and INSERT,
// but the parser does not yet support it.
// https://dev.mysql.com/doc/refman/8.3/en/insert.html
let sql = "REPLACE INTO t SET a='1'";
assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err());
}
#[test] #[test]
fn test_replace_into_set_placeholder() { fn test_replace_into_set_placeholder() {
let sql = "REPLACE INTO t SET ?"; let sql = "REPLACE INTO t SET ?";

View file

@ -119,6 +119,12 @@ fn parse_insert_values() {
verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)"); verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)");
} }
#[test]
fn parse_insert_set() {
let dialects = all_dialects_where(|d| d.supports_insert_set());
dialects.verified_stmt("INSERT INTO tbl1 SET col1 = 1, col2 = 'abc', col3 = current_date()");
}
#[test] #[test]
fn parse_replace_into() { fn parse_replace_into() {
let dialect = PostgreSqlDialect {}; let dialect = PostgreSqlDialect {};

View file

@ -4423,6 +4423,7 @@ fn test_simple_postgres_insert_with_alias() {
settings: None, settings: None,
format_clause: None, format_clause: None,
})), })),
assignments: vec![],
partitioned: None, partitioned: None,
after_columns: vec![], after_columns: vec![],
table: false, table: false,
@ -4493,6 +4494,7 @@ fn test_simple_postgres_insert_with_alias() {
settings: None, settings: None,
format_clause: None, format_clause: None,
})), })),
assignments: vec![],
partitioned: None, partitioned: None,
after_columns: vec![], after_columns: vec![],
table: false, table: false,
@ -4559,6 +4561,7 @@ fn test_simple_insert_with_quoted_alias() {
settings: None, settings: None,
format_clause: None, format_clause: None,
})), })),
assignments: vec![],
partitioned: None, partitioned: None,
after_columns: vec![], after_columns: vec![],
table: false, table: false,