From 6abc1661dd4edcee127a3f369e72dde56ec80029 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 21:59:12 +0200 Subject: [PATCH 1/3] Accept `VALUE` as a synonym of `VALUES` only in MySQL --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 7 +++++++ src/dialect/mysql.rs | 4 ++++ src/parser/mod.rs | 4 +++- tests/sqlparser_common.rs | 16 +++++++++++++++- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 9c1dc08b3a..9ca861f997 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -177,6 +177,10 @@ impl Dialect for GenericDialect { true } + fn supports_singular_value_keyword(&self) -> bool { + true + } + fn supports_comment_on(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 324c52d1d5..d99b04c049 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1515,6 +1515,13 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect accepts `VALUE` as a synonym of `VALUES`. + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/insert.html) + fn supports_singular_value_keyword(&self) -> bool { + false + } + /// Returns true if this dialect supports `SET` statements without an explicit /// assignment operator such as `=`. For example: `SET SHOWPLAN_XML ON`. fn supports_set_stmt_without_operator(&self) -> bool { diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index c5330d8eb5..9bde3f740e 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -100,6 +100,10 @@ impl Dialect for MySqlDialect { true } + fn supports_singular_value_keyword(&self) -> bool { + true + } + /// see fn supports_multiline_comment_hints(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4dfd3d51bc..7fd648c4a1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15464,7 +15464,9 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::VALUES) { let is_mysql = dialect_of!(self is MySqlDialect); SetExpr::Values(self.parse_values(is_mysql, false)?) - } else if self.parse_keyword(Keyword::VALUE) { + } else if self.dialect.supports_singular_value_keyword() + && self.parse_keyword(Keyword::VALUE) + { let is_mysql = dialect_of!(self is MySqlDialect); SetExpr::Values(self.parse_values(is_mysql, true)?) } else if self.parse_keyword(Keyword::TABLE) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6167ee4c87..a8f0aa1323 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -160,7 +160,9 @@ fn parse_insert_values() { expected_rows: &[Parens>], expected_value_keyword: bool, ) { - match verified_stmt(sql) { + let dialects = + all_dialects_where(|d| !expected_value_keyword || d.supports_singular_value_keyword()); + match dialects.verified_stmt(sql) { Statement::Insert(Insert { table: table_name, columns, @@ -21098,3 +21100,15 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } + +#[test] +fn parse_singular_value_keyword() { + let sql = "INSERT INTO t (a) VALUE (1)"; + all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); + assert_eq!( + all_dialects_where(|d| !d.supports_singular_value_keyword()).parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() + )) + ); +} From 13531f5fedee747ee43ca90288fb096d43b020a9 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:56:53 +0200 Subject: [PATCH 2/3] Cover singular `VALUE` query bodies and dialect errors --- tests/sqlparser_common.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a8f0aa1323..be8044e8a3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21111,4 +21111,30 @@ fn parse_singular_value_keyword() { "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() )) ); + + // Dialects with `INSERT` table aliases consume `VALUE` as an alias without a column list. + let sql = "INSERT INTO t VALUE (1)"; + all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); + let without_table_alias = all_dialects_where(|d| { + !d.supports_singular_value_keyword() && !d.supports_insert_table_alias() + }); + assert_eq!( + without_table_alias.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() + )) + ); + let with_table_alias = all_dialects_where(|d| { + !d.supports_singular_value_keyword() && d.supports_insert_table_alias() + }); + assert_eq!( + with_table_alias.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: identifier, found: 1".to_string() + )) + ); + + let dialects = all_dialects_where(|d| d.supports_singular_value_keyword()); + dialects.verified_stmt("SELECT 1 UNION VALUE (2)"); + dialects.verified_stmt("WITH c AS (VALUE (1)) SELECT * FROM c"); } From 541d83f820a8ae5b37bb6a3f2afad6e4a6649076 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:13:48 +0200 Subject: [PATCH 3/3] Place regression tests beside related syntax cases --- tests/sqlparser_common.rs | 76 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index be8044e8a3..e6299f60f1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -196,6 +196,44 @@ fn parse_insert_values() { verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)"); } +#[test] +fn parse_singular_value_keyword() { + let sql = "INSERT INTO t (a) VALUE (1)"; + all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); + assert_eq!( + all_dialects_where(|d| !d.supports_singular_value_keyword()).parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() + )) + ); + + // Dialects with `INSERT` table aliases consume `VALUE` as an alias without a column list. + let sql = "INSERT INTO t VALUE (1)"; + all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); + let without_table_alias = all_dialects_where(|d| { + !d.supports_singular_value_keyword() && !d.supports_insert_table_alias() + }); + assert_eq!( + without_table_alias.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() + )) + ); + let with_table_alias = all_dialects_where(|d| { + !d.supports_singular_value_keyword() && d.supports_insert_table_alias() + }); + assert_eq!( + with_table_alias.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: identifier, found: 1".to_string() + )) + ); + + let dialects = all_dialects_where(|d| d.supports_singular_value_keyword()); + dialects.verified_stmt("SELECT 1 UNION VALUE (2)"); + dialects.verified_stmt("WITH c AS (VALUE (1)) SELECT * FROM c"); +} + #[test] fn parse_insert_set() { let dialects = all_dialects_where(|d| d.supports_insert_set()); @@ -21100,41 +21138,3 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } - -#[test] -fn parse_singular_value_keyword() { - let sql = "INSERT INTO t (a) VALUE (1)"; - all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); - assert_eq!( - all_dialects_where(|d| !d.supports_singular_value_keyword()).parse_sql_statements(sql), - Err(ParserError::ParserError( - "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() - )) - ); - - // Dialects with `INSERT` table aliases consume `VALUE` as an alias without a column list. - let sql = "INSERT INTO t VALUE (1)"; - all_dialects_where(|d| d.supports_singular_value_keyword()).verified_stmt(sql); - let without_table_alias = all_dialects_where(|d| { - !d.supports_singular_value_keyword() && !d.supports_insert_table_alias() - }); - assert_eq!( - without_table_alias.parse_sql_statements(sql), - Err(ParserError::ParserError( - "Expected: SELECT, VALUES, or a subquery in the query body, found: VALUE".to_string() - )) - ); - let with_table_alias = all_dialects_where(|d| { - !d.supports_singular_value_keyword() && d.supports_insert_table_alias() - }); - assert_eq!( - with_table_alias.parse_sql_statements(sql), - Err(ParserError::ParserError( - "Expected: identifier, found: 1".to_string() - )) - ); - - let dialects = all_dialects_where(|d| d.supports_singular_value_keyword()); - dialects.verified_stmt("SELECT 1 UNION VALUE (2)"); - dialects.verified_stmt("WITH c AS (VALUE (1)) SELECT * FROM c"); -}