Skip to main content

uqa_sql/schema/indexes/
options.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL vector-index option aliases, value parsing, and duplicate checks.
8use crate::SQLError;
9
10mod diskann;
11mod relational;
12pub use diskann::{parse_diskann_index_options, DiskANNIndexOptions};
13
14#[derive(Default)]
15pub struct IVFIndexOptions {
16    pub nlist: Option<usize>,
17    pub nprobe: Option<usize>,
18    pub train_threshold: Option<usize>,
19}
20#[derive(Default)]
21pub struct HNSWIndexOptions {
22    pub m: Option<usize>,
23    pub ef_construction: Option<usize>,
24    pub ef_search: Option<usize>,
25    pub rebuild_threshold: Option<usize>,
26    pub seed: Option<u64>,
27}
28pub fn index_access_method(statement: &crate::ast::CreateIndex) -> Result<String, SQLError> {
29    let am = statement.access_method.to_ascii_lowercase();
30    if !matches!(
31        am.as_str(),
32        "" | "btree" | "gin" | "ivf" | "hnsw" | "diskann"
33    ) {
34        return Err(SQLError::Unsupported(format!(
35            "CREATE INDEX access method `{}` is not supported",
36            statement.access_method
37        )));
38    }
39
40    Ok(am)
41}
42
43/// Validate declared options before namespace collisions and physical index work.
44pub fn validate_index_options(statement: &crate::ast::CreateIndex) -> Result<(), SQLError> {
45    if let Some(namespace) = statement.option_namespaces.first() {
46        return Err(SQLError::Routine {
47            sqlstate: "22023".into(),
48            message: format!("unrecognized parameter namespace \"{namespace}\""),
49        });
50    }
51    match index_access_method(statement)?.as_str() {
52        "" | "btree" => relational::validate(&statement.options, false),
53        "gin" => relational::validate(&statement.options, true),
54        "ivf" => parse_ivf_index_options(&statement.options).map(|_| ()),
55        "hnsw" => parse_hnsw_index_options(&statement.options).map(|_| ()),
56        "diskann" => parse_diskann_index_options(&statement.options).map(|_| ()),
57        _ => unreachable!("validated access method"),
58    }
59}
60pub fn parse_ivf_index_options(options: &[(String, String)]) -> Result<IVFIndexOptions, SQLError> {
61    let mut params = IVFIndexOptions::default();
62    let mut seen = std::collections::BTreeSet::new();
63    for (key, value) in options {
64        if key.eq_ignore_ascii_case("lists") || key.eq_ignore_ascii_case("nlist") {
65            claim_index_option(&mut seen, "nlist", "ivf", key)?;
66            params.nlist = Some(parse_positive_usize_option("ivf", key, value)?);
67        } else if key.eq_ignore_ascii_case("probes") || key.eq_ignore_ascii_case("nprobe") {
68            claim_index_option(&mut seen, "nprobe", "ivf", key)?;
69            params.nprobe = Some(parse_positive_usize_option("ivf", key, value)?);
70        } else if key.eq_ignore_ascii_case("train_threshold")
71            || key.eq_ignore_ascii_case("train-threshold")
72            || key.eq_ignore_ascii_case("min_train")
73        {
74            claim_index_option(&mut seen, "train_threshold", "ivf", key)?;
75            params.train_threshold = Some(parse_positive_usize_option("ivf", key, value)?);
76        } else {
77            return Err(SQLError::Unsupported(format!(
78                "CREATE INDEX USING ivf option `{key}` is not supported"
79            )));
80        }
81    }
82    Ok(params)
83}
84
85pub fn parse_hnsw_index_options(
86    options: &[(String, String)],
87) -> Result<HNSWIndexOptions, SQLError> {
88    let mut params = HNSWIndexOptions::default();
89    let mut seen = std::collections::BTreeSet::new();
90    for (key, value) in options {
91        if key.eq_ignore_ascii_case("m") {
92            claim_index_option(&mut seen, "m", "hnsw", key)?;
93            params.m = Some(parse_positive_usize_option("hnsw", key, value)?);
94        } else if key.eq_ignore_ascii_case("ef_construction")
95            || key.eq_ignore_ascii_case("ef-construction")
96        {
97            claim_index_option(&mut seen, "ef_construction", "hnsw", key)?;
98            params.ef_construction = Some(parse_positive_usize_option("hnsw", key, value)?);
99        } else if key.eq_ignore_ascii_case("ef_search") || key.eq_ignore_ascii_case("ef-search") {
100            claim_index_option(&mut seen, "ef_search", "hnsw", key)?;
101            params.ef_search = Some(parse_positive_usize_option("hnsw", key, value)?);
102        } else if key.eq_ignore_ascii_case("rebuild_threshold")
103            || key.eq_ignore_ascii_case("rebuild-threshold")
104        {
105            claim_index_option(&mut seen, "rebuild_threshold", "hnsw", key)?;
106            params.rebuild_threshold = Some(parse_positive_usize_option("hnsw", key, value)?);
107        } else if key.eq_ignore_ascii_case("seed") {
108            claim_index_option(&mut seen, "seed", "hnsw", key)?;
109            params.seed = Some(value.parse::<u64>().map_err(|_| {
110                SQLError::TypeMismatch(format!(
111                    "CREATE INDEX USING hnsw option `{key}` must be an unsigned integer"
112                ))
113            })?);
114        } else {
115            return Err(SQLError::Unsupported(format!(
116                "CREATE INDEX USING hnsw option `{key}` is not supported"
117            )));
118        }
119    }
120    Ok(params)
121}
122
123fn claim_index_option(
124    seen: &mut std::collections::BTreeSet<&'static str>,
125    canonical: &'static str,
126    access_method: &str,
127    source: &str,
128) -> Result<(), SQLError> {
129    if !seen.insert(canonical) {
130        return Err(SQLError::Unsupported(format!(
131            "CREATE INDEX USING {access_method} option `{source}` duplicates `{canonical}`"
132        )));
133    }
134    Ok(())
135}
136
137fn parse_positive_usize_option(
138    access_method: &str,
139    key: &str,
140    value: &str,
141) -> Result<usize, SQLError> {
142    let parsed = value.parse::<usize>().map_err(|_| {
143        SQLError::TypeMismatch(format!(
144            "CREATE INDEX USING {access_method} option `{key}` must be a positive integer"
145        ))
146    })?;
147    if parsed == 0 {
148        return Err(SQLError::TypeMismatch(format!(
149            "CREATE INDEX USING {access_method} option `{key}` must be a positive integer"
150        )));
151    }
152    Ok(parsed)
153}