From c6a3910a7f5c6fdd6a2b51c826ef9bc7216d3025 Mon Sep 17 00:00:00 2001 From: MohamedAbdeen21 Date: Mon, 15 Sep 2025 22:09:22 +0100 Subject: [PATCH 1/4] MySQL: allow USING clause before ON in CREATE INDEX MySQL allows specifying the index type `USING index_type` before the `ON` clause in `CREATE INDEX` statements. This PR allows the `CREATE INDEX` parser to accept both positions of the `USING` clause, regardless of the dialect. docs: https://dev.mysql.com/doc/refman/8.4/en/create-index.html --- src/parser/mod.rs | 15 ++++++++----- tests/sqlparser_common.rs | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f47d4b9b5..cab8a60ea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7061,19 +7061,24 @@ impl<'a> Parser<'a> { pub fn parse_create_index(&mut self, unique: bool) -> Result { let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + + let mut using = None; + let index_name = if if_not_exists || !self.parse_keyword(Keyword::ON) { let index_name = self.parse_object_name(false)?; + // MySQL allows `USING index_type` either before or after `ON table_name` + using = self.parse_optional_using_then_index_type()?; self.expect_keyword_is(Keyword::ON)?; Some(index_name) } else { None }; + let table_name = self.parse_object_name(false)?; - let using = if self.parse_keyword(Keyword::USING) { - Some(self.parse_index_type()?) - } else { - None - }; + + // MySQL allows having two `USING` clauses. + // In that case, the second clause overwrites the first. + using = self.parse_optional_using_then_index_type()?.or(using); let columns = self.parse_parenthesized_index_column_list()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f46abc7d9..df3f2bd8d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17246,3 +17246,47 @@ fn parse_invisible_column() { _ => panic!("Unexpected statement {stmt}"), } } + +#[test] +fn parse_create_index_using_before_on() { + let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1)"; + // Can't use `verified_stmt` here as the USING will be placed after the `ON` clause + match all_dialects().parse_sql_statements(sql).unwrap()[0].clone() { + Statement::CreateIndex(CreateIndex { + name, + table_name, + using, + columns, + unique, + .. + }) => { + assert_eq!(name.unwrap().to_string(), "idx_name"); + assert_eq!(table_name.to_string(), "table_name"); + assert_eq!(using, Some(IndexType::BTree)); + assert_eq!(columns.len(), 1); + assert!(!unique); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_index_using_multiple_clauses() { + let sql = "CREATE INDEX idx_name USING BTREE ON table_name USING HASH (col1)"; + // Can't use `verified_stmt` here as the first USING will be ignored + match all_dialects().parse_sql_statements(sql).unwrap()[0].clone() { + Statement::CreateIndex(CreateIndex { + name, + table_name, + using, + columns, + .. + }) => { + assert_eq!(name.unwrap().to_string(), "idx_name"); + assert_eq!(table_name.to_string(), "table_name"); + assert_eq!(using, Some(IndexType::Hash)); + assert_eq!(columns.len(), 1); + } + _ => unreachable!(), + } +} From 43a114c76dbbb49b30c0d4bf745dfb49951de38b Mon Sep 17 00:00:00 2001 From: MohamedAbdeen21 Date: Wed, 24 Sep 2025 21:42:41 +0100 Subject: [PATCH 2/4] merge test cases for USING in CREATE INDEX --- src/ast/ddl.rs | 2 ++ src/parser/mod.rs | 7 +++++++ tests/sqlparser_common.rs | 21 ++++++++++----------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4c145f91e..c4f769675 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2361,6 +2361,8 @@ pub struct CreateIndex { pub name: Option, #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub table_name: ObjectName, + /// Index type used in the statement. Can also be found inside [`CreateIndex::index_options`] + /// depending on the position of the option within the statement. pub using: Option, pub columns: Vec, pub unique: bool, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cab8a60ea..b1f75067e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7123,6 +7123,13 @@ impl<'a> Parser<'a> { // parse it anyway (as we do inside `ALTER TABLE` and `CREATE TABLE` parsing). let index_options = self.parse_index_options()?; + if index_options + .iter() + .any(|opt| matches!(opt, IndexOption::Using(_))) + { + using = None; + }; + // MySQL allows `ALGORITHM` and `LOCK` options. Unlike in `ALTER TABLE`, they need not be comma separated. let mut alter_options = Vec::new(); while self diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index df3f2bd8d..1e0007b16 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17248,10 +17248,10 @@ fn parse_invisible_column() { } #[test] -fn parse_create_index_using_before_on() { +fn parse_create_index_different_using_positions() { let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1)"; - // Can't use `verified_stmt` here as the USING will be placed after the `ON` clause - match all_dialects().parse_sql_statements(sql).unwrap()[0].clone() { + let expected = "CREATE INDEX idx_name ON table_name USING BTREE (col1)"; + match all_dialects().one_statement_parses_to(sql, expected) { Statement::CreateIndex(CreateIndex { name, table_name, @@ -17268,24 +17268,23 @@ fn parse_create_index_using_before_on() { } _ => unreachable!(), } -} -#[test] -fn parse_create_index_using_multiple_clauses() { - let sql = "CREATE INDEX idx_name USING BTREE ON table_name USING HASH (col1)"; - // Can't use `verified_stmt` here as the first USING will be ignored - match all_dialects().parse_sql_statements(sql).unwrap()[0].clone() { + let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1) USING HASH"; + let expected = "CREATE INDEX idx_name ON table_name(col1) USING HASH"; + match all_dialects().one_statement_parses_to(sql, expected) { Statement::CreateIndex(CreateIndex { name, table_name, - using, columns, + index_options, .. }) => { assert_eq!(name.unwrap().to_string(), "idx_name"); assert_eq!(table_name.to_string(), "table_name"); - assert_eq!(using, Some(IndexType::Hash)); assert_eq!(columns.len(), 1); + assert!(index_options + .iter() + .any(|o| o == &IndexOption::Using(IndexType::Hash))); } _ => unreachable!(), } From bdbd173bd9a516ac827c39f68c6fcd471f15c12e Mon Sep 17 00:00:00 2001 From: MohamedAbdeen21 Date: Wed, 24 Sep 2025 21:54:33 +0100 Subject: [PATCH 3/4] add comment explaining USING behaviour --- src/parser/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b1f75067e..8d6c8b918 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7123,6 +7123,8 @@ impl<'a> Parser<'a> { // parse it anyway (as we do inside `ALTER TABLE` and `CREATE TABLE` parsing). let index_options = self.parse_index_options()?; + // Only keep the latest USING, we don't take the IndexOption out of the vec and place the + // value in the `using` var because we try to keep the order of the options as they appear. if index_options .iter() .any(|opt| matches!(opt, IndexOption::Using(_))) From 2713ea4f331e1cb73576e5e18831c3cb8a7b94bb Mon Sep 17 00:00:00 2001 From: MohamedAbdeen21 Date: Thu, 25 Sep 2025 21:19:12 +0100 Subject: [PATCH 4/4] allow multiple index types in parsed result --- src/parser/mod.rs | 9 --------- tests/sqlparser_common.rs | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d6c8b918..cab8a60ea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7123,15 +7123,6 @@ impl<'a> Parser<'a> { // parse it anyway (as we do inside `ALTER TABLE` and `CREATE TABLE` parsing). let index_options = self.parse_index_options()?; - // Only keep the latest USING, we don't take the IndexOption out of the vec and place the - // value in the `using` var because we try to keep the order of the options as they appear. - if index_options - .iter() - .any(|opt| matches!(opt, IndexOption::Using(_))) - { - using = None; - }; - // MySQL allows `ALGORITHM` and `LOCK` options. Unlike in `ALTER TABLE`, they need not be comma separated. let mut alter_options = Vec::new(); while self diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1e0007b16..56e265b80 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17270,7 +17270,7 @@ fn parse_create_index_different_using_positions() { } let sql = "CREATE INDEX idx_name USING BTREE ON table_name (col1) USING HASH"; - let expected = "CREATE INDEX idx_name ON table_name(col1) USING HASH"; + let expected = "CREATE INDEX idx_name ON table_name USING BTREE (col1) USING HASH"; match all_dialects().one_statement_parses_to(sql, expected) { Statement::CreateIndex(CreateIndex { name,