Skip to content

Commit 8c5ccd7

Browse files
Support CREATE VECTOR INDEX
Several dialects create a vector index for approximate nearest-neighbor search over an embedding column with a `CREATE VECTOR INDEX` statement — BigQuery, Oracle, SQL Server, MariaDB and TiDB. `VECTOR` is not a keyword, so `parse_create` previously failed with `Expected: an object type after CREATE, found: VECTOR`. The common core is `CREATE [OR REPLACE] VECTOR INDEX [IF NOT EXISTS] <name> ON <table>(<column | expr>)`, after which dialects add different trailers (BigQuery `OPTIONS(...)`, SQL Server `WITH (...)`, Oracle `INCLUDE`/bare clauses, TiDB `USING`). It is parsed permissively for every dialect. ### Changes - Add `vector`, `or_replace` and `options` fields to `CreateIndex`. `VECTOR` is a modifier on `CREATE INDEX` (like `UNIQUE`), so it reuses the existing node rather than a new statement variant. `Display` renders `CREATE [OR REPLACE ]VECTOR INDEX ...` and the `OPTIONS(...)` trailer. - Route `CREATE VECTOR INDEX` through `parse_create_index`, so it inherits the existing index trailers — `USING`, `INCLUDE`, `WITH`, expression targets and index options — that cover the Oracle / SQL Server / TiDB variants. The BigQuery `OPTIONS(...)` clause reuses `parse_options` / `SqlOption`, parsing and rendering like `CREATE TABLE` / `CREATE VIEW` OPTIONS. - Plain `CREATE INDEX` is unchanged (the three fields default to false/empty). - Test `parse_create_vector_index` in `tests/sqlparser_common.rs` verifies the round-trip across all dialects and covers `OR REPLACE`, `IF NOT EXISTS`, schema-qualified names, `OPTIONS(...)` and the shared `INCLUDE` trailer. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 30d0836 commit 8c5ccd7

5 files changed

Lines changed: 180 additions & 1 deletion

File tree

src/ast/ddl.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,11 @@ pub struct CreateIndex {
28272827
pub using: Option<IndexType>,
28282828
/// columns included in the index
28292829
pub columns: Vec<IndexColumn>,
2830+
/// whether this is a `CREATE VECTOR INDEX` (BigQuery, Oracle, SQL Server,
2831+
/// MariaDB, TiDB)
2832+
pub vector: bool,
2833+
/// whether the statement is `CREATE OR REPLACE`
2834+
pub or_replace: bool,
28302835
/// whether the index is unique
28312836
pub unique: bool,
28322837
/// whether the index is created concurrently
@@ -2843,6 +2848,8 @@ pub struct CreateIndex {
28432848
pub nulls_distinct: Option<bool>,
28442849
/// WITH clause: <https://www.postgresql.org/docs/current/sql-createindex.html>
28452850
pub with: Vec<Expr>,
2851+
/// `OPTIONS(...)` clause (BigQuery, e.g. on `CREATE VECTOR INDEX`)
2852+
pub options: Vec<SqlOption>,
28462853
/// WHERE clause: <https://www.postgresql.org/docs/current/sql-createindex.html>
28472854
pub predicate: Option<Expr>,
28482855
/// Index options: <https://www.postgresql.org/docs/current/sql-createindex.html>
@@ -2860,8 +2867,10 @@ impl fmt::Display for CreateIndex {
28602867
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28612868
write!(
28622869
f,
2863-
"CREATE {unique}INDEX {concurrently}{async_}{if_not_exists}",
2870+
"CREATE {or_replace}{unique}{vector}INDEX {concurrently}{async_}{if_not_exists}",
2871+
or_replace = if self.or_replace { "OR REPLACE " } else { "" },
28642872
unique = if self.unique { "UNIQUE " } else { "" },
2873+
vector = if self.vector { "VECTOR " } else { "" },
28652874
concurrently = if self.concurrently {
28662875
"CONCURRENTLY "
28672876
} else {
@@ -2895,6 +2904,9 @@ impl fmt::Display for CreateIndex {
28952904
if !self.with.is_empty() {
28962905
write!(f, " WITH ({})", display_comma_separated(&self.with))?;
28972906
}
2907+
if !self.options.is_empty() {
2908+
write!(f, " OPTIONS({})", display_comma_separated(&self.options))?;
2909+
}
28982910
if let Some(predicate) = &self.predicate {
28992911
write!(f, " WHERE {predicate}")?;
29002912
}

src/ast/spans.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,13 +699,16 @@ impl Spanned for CreateIndex {
699699
table_name,
700700
using: _,
701701
columns,
702+
vector: _, // bool
703+
or_replace: _, // bool
702704
unique: _, // bool
703705
concurrently: _, // bool
704706
r#async: _, // bool
705707
if_not_exists: _, // bool
706708
include,
707709
nulls_distinct: _, // bool
708710
with,
711+
options,
709712
predicate,
710713
index_options: _,
711714
alter_options,
@@ -718,6 +721,7 @@ impl Spanned for CreateIndex {
718721
.chain(columns.iter().map(|i| i.column.span()))
719722
.chain(include.iter().map(|i| i.span))
720723
.chain(with.iter().map(|i| i.span()))
724+
.chain(options.iter().map(|i| i.span()))
721725
.chain(predicate.iter().map(|i| i.span()))
722726
.chain(alter_options.iter().map(|i| i.span())),
723727
)

src/parser/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5288,6 +5288,16 @@ impl<'a> Parser<'a> {
52885288
self.parse_create_schema(or_replace)
52895289
} else if self.parse_keyword(Keyword::WAREHOUSE) {
52905290
self.parse_create_warehouse(or_replace).map(Into::into)
5291+
} else if matches!(
5292+
&self.peek_token_ref().token,
5293+
Token::Word(w) if w.keyword == Keyword::NoKeyword && w.value.eq_ignore_ascii_case("VECTOR")
5294+
) {
5295+
// `CREATE [OR REPLACE] VECTOR INDEX ...` (BigQuery, Oracle, SQL Server,
5296+
// MariaDB, TiDB). VECTOR is not a reserved keyword.
5297+
self.next_token();
5298+
self.expect_keyword_is(Keyword::INDEX)?;
5299+
self.parse_create_index_inner(false, true, or_replace)
5300+
.map(Into::into)
52915301
} else if or_replace {
52925302
self.expected_ref(
52935303
"[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or SCHEMA or WAREHOUSE after CREATE OR REPLACE",
@@ -8247,6 +8257,24 @@ impl<'a> Parser<'a> {
82478257

82488258
/// Parse a `CREATE INDEX` statement.
82498259
pub fn parse_create_index(&mut self, unique: bool) -> Result<CreateIndex, ParserError> {
8260+
self.parse_create_index_inner(unique, false, false)
8261+
}
8262+
8263+
/// Parse the body of a `CREATE [UNIQUE | VECTOR] INDEX` statement, with the
8264+
/// leading `[UNIQUE | VECTOR] INDEX` keywords already consumed.
8265+
///
8266+
/// `CREATE VECTOR INDEX` is supported by several dialects (BigQuery, Oracle,
8267+
/// SQL Server, MariaDB, TiDB) and is parsed permissively for all of them.
8268+
/// Its common core is `... name ON tbl(column | expr)`; the dialect-specific
8269+
/// trailers reuse the standard index clauses parsed below — `USING <method>`
8270+
/// (TiDB), `INCLUDE (...)` (Oracle/SQL Server), `WITH (...)` (SQL Server),
8271+
/// and the BigQuery `OPTIONS(...)` clause.
8272+
fn parse_create_index_inner(
8273+
&mut self,
8274+
unique: bool,
8275+
vector: bool,
8276+
or_replace: bool,
8277+
) -> Result<CreateIndex, ParserError> {
82508278
let concurrently = self.parse_keyword(Keyword::CONCURRENTLY);
82518279
let r#async = self.parse_keyword(Keyword::ASYNC);
82528280
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
@@ -8299,6 +8327,10 @@ impl<'a> Parser<'a> {
82998327
Vec::new()
83008328
};
83018329

8330+
// BigQuery `OPTIONS(...)` clause (e.g. on `CREATE VECTOR INDEX`); no-op
8331+
// when absent, so it is harmless on a plain `CREATE INDEX`.
8332+
let options = self.parse_options(Keyword::OPTIONS)?;
8333+
83028334
let predicate = if self.parse_keyword(Keyword::WHERE) {
83038335
Some(self.parse_expr()?)
83048336
} else {
@@ -8326,13 +8358,16 @@ impl<'a> Parser<'a> {
83268358
table_name,
83278359
using,
83288360
columns,
8361+
vector,
8362+
or_replace,
83298363
unique,
83308364
concurrently,
83318365
r#async,
83328366
if_not_exists,
83338367
include,
83348368
nulls_distinct,
83358369
with,
8370+
options,
83368371
predicate,
83378372
index_options,
83388373
alter_options,

tests/sqlparser_common.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9728,6 +9728,9 @@ fn test_create_index_with_using_function() {
97289728
predicate: None,
97299729
index_options,
97309730
alter_options,
9731+
vector: _,
9732+
or_replace: _,
9733+
options: _,
97319734
}) => {
97329735
assert_eq!("idx_name", name.to_string());
97339736
assert_eq!("test", table_name.to_string());
@@ -9785,6 +9788,9 @@ fn test_create_index_with_with_clause() {
97859788
predicate: None,
97869789
index_options,
97879790
alter_options,
9791+
vector: _,
9792+
or_replace: _,
9793+
options: _,
97889794
}) => {
97899795
pretty_assertions::assert_eq!("title_idx", name.to_string());
97909796
pretty_assertions::assert_eq!("films", table_name.to_string());
@@ -9808,6 +9814,95 @@ fn parse_create_index_async() {
98089814
verified_stmt("CREATE UNIQUE INDEX ASYNC my_index ON my_table(col1)");
98099815
}
98109816

9817+
#[test]
9818+
fn parse_create_vector_index() {
9819+
// `CREATE VECTOR INDEX` is not dialect-specific — BigQuery, Oracle,
9820+
// SQL Server, MariaDB and TiDB all have it — so it is parsed for every
9821+
// dialect, as a `CreateIndex` flagged `vector`. Its common core is
9822+
// `... name ON tbl(column)`; the BigQuery `OPTIONS(...)` trailer lands in
9823+
// `options`, and the standard index trailers (`INCLUDE`, `USING`, `WITH`)
9824+
// are shared with `CREATE INDEX`.
9825+
let sql =
9826+
"CREATE VECTOR INDEX emb ON t(embedding) OPTIONS(distance_type = 'COSINE', dimension = 4)";
9827+
match verified_stmt(sql) {
9828+
Statement::CreateIndex(CreateIndex {
9829+
name,
9830+
table_name,
9831+
using,
9832+
columns,
9833+
vector,
9834+
or_replace,
9835+
unique,
9836+
if_not_exists,
9837+
with,
9838+
options,
9839+
..
9840+
}) => {
9841+
assert!(vector);
9842+
assert!(!or_replace);
9843+
assert!(!unique);
9844+
assert!(!if_not_exists);
9845+
assert_eq!(name.unwrap().to_string(), "emb");
9846+
assert_eq!(table_name.to_string(), "t");
9847+
assert_eq!(using, None);
9848+
assert!(with.is_empty());
9849+
assert_eq!(
9850+
columns.iter().map(|c| c.to_string()).collect::<Vec<_>>(),
9851+
vec!["embedding"]
9852+
);
9853+
assert_eq!(
9854+
options,
9855+
vec![
9856+
SqlOption::KeyValue {
9857+
key: Ident::new("distance_type"),
9858+
value: Expr::Value(
9859+
Value::SingleQuotedString("COSINE".to_string()).with_empty_span()
9860+
),
9861+
},
9862+
SqlOption::KeyValue {
9863+
key: Ident::new("dimension"),
9864+
value: Expr::value(number("4")),
9865+
},
9866+
]
9867+
);
9868+
}
9869+
other => panic!("expected CreateIndex, got {other:?}"),
9870+
}
9871+
9872+
// `OR REPLACE` (BigQuery, MariaDB).
9873+
match verified_stmt(
9874+
"CREATE OR REPLACE VECTOR INDEX emb ON t(embedding) OPTIONS(distance_type = 'COSINE')",
9875+
) {
9876+
Statement::CreateIndex(CreateIndex {
9877+
vector, or_replace, ..
9878+
}) => {
9879+
assert!(vector);
9880+
assert!(or_replace);
9881+
}
9882+
other => panic!("expected CreateIndex, got {other:?}"),
9883+
}
9884+
9885+
// `IF NOT EXISTS` and schema-qualified names.
9886+
match verified_stmt(
9887+
"CREATE VECTOR INDEX IF NOT EXISTS s.emb ON s.t(embedding) OPTIONS(distance_type = 'EUCLIDEAN')",
9888+
) {
9889+
Statement::CreateIndex(CreateIndex {
9890+
vector,
9891+
if_not_exists,
9892+
..
9893+
}) => {
9894+
assert!(vector);
9895+
assert!(if_not_exists);
9896+
}
9897+
other => panic!("expected CreateIndex, got {other:?}"),
9898+
}
9899+
9900+
// The bare common core, and the shared `INCLUDE` covering-column trailer
9901+
// (Oracle / SQL Server), both round-trip.
9902+
verified_stmt("CREATE VECTOR INDEX emb ON t(embedding)");
9903+
verified_stmt("CREATE VECTOR INDEX emb ON t(embedding) INCLUDE (a, b)");
9904+
}
9905+
98119906
#[test]
98129907
fn parse_drop_index() {
98139908
let sql = "DROP INDEX idx_a";

tests/sqlparser_postgres.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,6 +2993,9 @@ fn parse_create_index() {
29932993
predicate: None,
29942994
index_options,
29952995
alter_options,
2996+
vector: _,
2997+
or_replace: _,
2998+
options: _,
29962999
}) => {
29973000
assert_eq_vec(&["my_index"], &name);
29983001
assert_eq_vec(&["my_table"], &table_name);
@@ -3030,6 +3033,9 @@ fn parse_create_anonymous_index() {
30303033
predicate: None,
30313034
index_options,
30323035
alter_options,
3036+
vector: _,
3037+
or_replace: _,
3038+
options: _,
30333039
}) => {
30343040
assert_eq!(None, name);
30353041
assert_eq_vec(&["my_table"], &table_name);
@@ -3150,6 +3156,9 @@ fn parse_create_indices_with_operator_classes() {
31503156
predicate: None,
31513157
index_options,
31523158
alter_options,
3159+
vector: _,
3160+
or_replace: _,
3161+
options: _,
31533162
}) => {
31543163
assert_eq_vec(&["the_index_name"], &name);
31553164
assert_eq_vec(&["users"], &table_name);
@@ -3179,6 +3188,9 @@ fn parse_create_indices_with_operator_classes() {
31793188
predicate: None,
31803189
index_options,
31813190
alter_options,
3191+
vector: _,
3192+
or_replace: _,
3193+
options: _,
31823194
}) => {
31833195
assert_eq_vec(&["the_index_name"], &name);
31843196
assert_eq_vec(&["users"], &table_name);
@@ -3263,6 +3275,9 @@ fn parse_create_bloom() {
32633275
predicate: None,
32643276
index_options,
32653277
alter_options,
3278+
vector: _,
3279+
or_replace: _,
3280+
options: _,
32663281
}) => {
32673282
assert_eq_vec(&["bloomidx"], &name);
32683283
assert_eq_vec(&["tbloom"], &table_name);
@@ -3320,6 +3335,9 @@ fn parse_create_brin() {
33203335
predicate: None,
33213336
index_options,
33223337
alter_options,
3338+
vector: _,
3339+
or_replace: _,
3340+
options: _,
33233341
}) => {
33243342
assert_eq_vec(&["brin_sensor_data_recorded_at"], &name);
33253343
assert_eq_vec(&["sensor_data"], &table_name);
@@ -3388,6 +3406,9 @@ fn parse_create_index_concurrently() {
33883406
predicate: None,
33893407
index_options,
33903408
alter_options,
3409+
vector: _,
3410+
or_replace: _,
3411+
options: _,
33913412
}) => {
33923413
assert_eq_vec(&["my_index"], &name);
33933414
assert_eq_vec(&["my_table"], &table_name);
@@ -3425,6 +3446,9 @@ fn parse_create_index_with_predicate() {
34253446
predicate: Some(_),
34263447
index_options,
34273448
alter_options,
3449+
vector: _,
3450+
or_replace: _,
3451+
options: _,
34283452
}) => {
34293453
assert_eq_vec(&["my_index"], &name);
34303454
assert_eq_vec(&["my_table"], &table_name);
@@ -3462,6 +3486,9 @@ fn parse_create_index_with_include() {
34623486
predicate: None,
34633487
index_options,
34643488
alter_options,
3489+
vector: _,
3490+
or_replace: _,
3491+
options: _,
34653492
}) => {
34663493
assert_eq_vec(&["my_index"], &name);
34673494
assert_eq_vec(&["my_table"], &table_name);
@@ -3499,6 +3526,9 @@ fn parse_create_index_with_nulls_distinct() {
34993526
predicate: None,
35003527
index_options,
35013528
alter_options,
3529+
vector: _,
3530+
or_replace: _,
3531+
options: _,
35023532
}) => {
35033533
assert_eq_vec(&["my_index"], &name);
35043534
assert_eq_vec(&["my_table"], &table_name);
@@ -3534,6 +3564,9 @@ fn parse_create_index_with_nulls_distinct() {
35343564
predicate: None,
35353565
index_options,
35363566
alter_options,
3567+
vector: _,
3568+
or_replace: _,
3569+
options: _,
35373570
}) => {
35383571
assert_eq_vec(&["my_index"], &name);
35393572
assert_eq_vec(&["my_table"], &table_name);

0 commit comments

Comments
 (0)