From 3d2a225dcb341b135dad414b0cad4e021e83ac87 Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Tue, 14 Apr 2026 11:41:07 -0700 Subject: [PATCH 1/3] Add ASYNC keyword support for CREATE INDEX Amazon Aurora DSQL uses `CREATE INDEX ASYNC` to create indexes asynchronously. This adds parsing support for the ASYNC keyword in CREATE INDEX statements. Changes: - Add ASYNC keyword to the keyword list - Add `async` field to CreateIndex struct - Parse ASYNC keyword after CONCURRENTLY in parse_create_index - Round-trip support via Display impl - Tests for CREATE INDEX ASYNC and CREATE UNIQUE INDEX ASYNC --- src/ast/ddl.rs | 5 ++- src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 2 ++ tests/sqlparser_common.rs | 4 +++ tests/sqlparser_postgres.rs | 70 +++++++++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1f0432909..29f46ab37 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2818,6 +2818,8 @@ pub struct CreateIndex { pub unique: bool, /// whether the index is created concurrently pub concurrently: bool, + /// whether the index is created asynchronously + pub r#async: bool, /// IF NOT EXISTS clause pub if_not_exists: bool, /// INCLUDE clause: @@ -2843,13 +2845,14 @@ impl fmt::Display for CreateIndex { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "CREATE {unique}INDEX {concurrently}{if_not_exists}", + "CREATE {unique}INDEX {concurrently}{async_}{if_not_exists}", unique = if self.unique { "UNIQUE " } else { "" }, concurrently = if self.concurrently { "CONCURRENTLY " } else { "" }, + async_ = if self.r#async { "ASYNC " } else { "" }, if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 9a32dc5a0..bbbad5dba 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -682,6 +682,7 @@ impl Spanned for CreateIndex { columns, unique: _, // bool concurrently: _, // bool + r#async: _, // bool if_not_exists: _, // bool include, nulls_distinct: _, // bool diff --git a/src/keywords.rs b/src/keywords.rs index 4ed30d243..f8cc1ba0b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -131,6 +131,7 @@ define_keywords!( ASOF, ASSERT, ASYMMETRIC, + ASYNC, AT, ATOMIC, ATTACH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 69a3605fa..86821d46b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8029,6 +8029,7 @@ impl<'a> Parser<'a> { /// Parse a `CREATE INDEX` statement. pub fn parse_create_index(&mut self, unique: bool) -> Result { let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); + let r#async = self.parse_keyword(Keyword::ASYNC); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let mut using = None; @@ -8108,6 +8109,7 @@ impl<'a> Parser<'a> { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 80c37588a..1e0f28ff4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9541,6 +9541,7 @@ fn test_create_index_with_using_function() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -9555,6 +9556,7 @@ fn test_create_index_with_using_function() { assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert!(include.is_empty()); assert!(with.is_empty()); @@ -9596,6 +9598,7 @@ fn test_create_index_with_with_clause() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -9609,6 +9612,7 @@ fn test_create_index_with_with_clause() { pretty_assertions::assert_eq!(indexed_columns, columns); assert!(unique); assert!(!concurrently); + assert!(!r#async); assert!(!if_not_exists); assert!(include.is_empty()); pretty_assertions::assert_eq!(with_parameters, with); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 07b62dd93..0d32e45a4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2806,6 +2806,7 @@ fn parse_create_index() { columns, unique, concurrently, + r#async, if_not_exists, nulls_distinct: None, include, @@ -2819,6 +2820,7 @@ fn parse_create_index() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -2841,6 +2843,7 @@ fn parse_create_anonymous_index() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -2854,6 +2857,7 @@ fn parse_create_anonymous_index() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(!if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -2959,6 +2963,7 @@ fn parse_create_indices_with_operator_classes() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -2987,6 +2992,7 @@ fn parse_create_indices_with_operator_classes() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3070,6 +3076,7 @@ fn parse_create_bloom() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3126,6 +3133,7 @@ fn parse_create_brin() { columns, unique: false, concurrently: false, + r#async: false, if_not_exists: false, include, nulls_distinct: None, @@ -3193,6 +3201,7 @@ fn parse_create_index_concurrently() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3206,6 +3215,7 @@ fn parse_create_index_concurrently() { assert_eq!(None, using); assert!(!unique); assert!(concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3217,6 +3227,58 @@ fn parse_create_index_concurrently() { } } +#[test] +fn parse_create_index_async() { + let sql = "CREATE INDEX ASYNC my_index ON my_table(col1)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + r#async, + if_not_exists, + include, + nulls_distinct: None, + with, + predicate: None, + index_options, + alter_options, + }) => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(r#async); + assert!(!if_not_exists); + assert_eq_vec(&["col1"], &columns); + assert!(include.is_empty()); + assert!(with.is_empty()); + assert!(index_options.is_empty()); + assert!(alter_options.is_empty()); + } + _ => unreachable!(), + } + + let sql = "CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + unique, + concurrently, + r#async, + .. + }) => { + assert!(unique); + assert!(!concurrently); + assert!(r#async); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_index_with_predicate() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) WHERE col3 IS NULL"; @@ -3228,6 +3290,7 @@ fn parse_create_index_with_predicate() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3241,6 +3304,7 @@ fn parse_create_index_with_predicate() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3263,6 +3327,7 @@ fn parse_create_index_with_include() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: None, @@ -3276,6 +3341,7 @@ fn parse_create_index_with_include() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert_eq_vec(&["col3", "col4"], &include); @@ -3298,6 +3364,7 @@ fn parse_create_index_with_nulls_distinct() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: Some(nulls_distinct), @@ -3311,6 +3378,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); @@ -3331,6 +3399,7 @@ fn parse_create_index_with_nulls_distinct() { columns, unique, concurrently, + r#async, if_not_exists, include, nulls_distinct: Some(nulls_distinct), @@ -3344,6 +3413,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq!(None, using); assert!(!unique); assert!(!concurrently); + assert!(!r#async); assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); From 204f04701b6593e5593ac75629a4bbb3e102b65a Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Wed, 29 Apr 2026 10:51:25 -0700 Subject: [PATCH 2/3] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0d32e45a4..3598cefb0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3229,39 +3229,7 @@ fn parse_create_index_concurrently() { #[test] fn parse_create_index_async() { - let sql = "CREATE INDEX ASYNC my_index ON my_table(col1)"; - match pg().verified_stmt(sql) { - Statement::CreateIndex(CreateIndex { - name: Some(ObjectName(name)), - table_name: ObjectName(table_name), - using, - columns, - unique, - concurrently, - r#async, - if_not_exists, - include, - nulls_distinct: None, - with, - predicate: None, - index_options, - alter_options, - }) => { - assert_eq_vec(&["my_index"], &name); - assert_eq_vec(&["my_table"], &table_name); - assert_eq!(None, using); - assert!(!unique); - assert!(!concurrently); - assert!(r#async); - assert!(!if_not_exists); - assert_eq_vec(&["col1"], &columns); - assert!(include.is_empty()); - assert!(with.is_empty()); - assert!(index_options.is_empty()); - assert!(alter_options.is_empty()); - } - _ => unreachable!(), - } + pg().verified_stmt("CREATE INDEX ASYNC my_index ON my_table(col1)"); let sql = "CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)"; match pg().verified_stmt(sql) { From 91bb7445e2afa4e544e1a497cbc8f3e834aca4d8 Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Wed, 29 Apr 2026 11:01:13 -0700 Subject: [PATCH 3/3] Address review feedback for CREATE INDEX ASYNC - Add DSQL docs link to the async field - Simplify async index tests to round-trip verified_stmt calls - Move async index tests from sqlparser_postgres.rs to sqlparser_common.rs --- src/ast/ddl.rs | 1 + tests/sqlparser_common.rs | 6 ++++++ tests/sqlparser_postgres.rs | 20 -------------------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 29f46ab37..561836034 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2819,6 +2819,7 @@ pub struct CreateIndex { /// whether the index is created concurrently pub concurrently: bool, /// whether the index is created asynchronously + /// [DSQL]: pub r#async: bool, /// IF NOT EXISTS clause pub if_not_exists: bool, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1e0f28ff4..f48b70817 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9623,6 +9623,12 @@ fn test_create_index_with_with_clause() { } } +#[test] +fn parse_create_index_async() { + verified_stmt("CREATE INDEX ASYNC my_index ON my_table(col1)"); + verified_stmt("CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)"); +} + #[test] fn parse_drop_index() { let sql = "DROP INDEX idx_a"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3598cefb0..6ac8d49a5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3227,26 +3227,6 @@ fn parse_create_index_concurrently() { } } -#[test] -fn parse_create_index_async() { - pg().verified_stmt("CREATE INDEX ASYNC my_index ON my_table(col1)"); - - let sql = "CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)"; - match pg().verified_stmt(sql) { - Statement::CreateIndex(CreateIndex { - unique, - concurrently, - r#async, - .. - }) => { - assert!(unique); - assert!(!concurrently); - assert!(r#async); - } - _ => unreachable!(), - } -} - #[test] fn parse_create_index_with_predicate() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) WHERE col3 IS NULL";