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#[derive(Default)]
10pub struct IVFIndexOptions {
11    pub nlist: Option<usize>,
12    pub nprobe: Option<usize>,
13    pub train_threshold: Option<usize>,
14}
15#[derive(Default)]
16pub struct HNSWIndexOptions {
17    pub m: Option<usize>,
18    pub ef_construction: Option<usize>,
19    pub ef_search: Option<usize>,
20    pub rebuild_threshold: Option<usize>,
21    pub seed: Option<u64>,
22}
23pub fn index_access_method(statement: &crate::ast::CreateIndex) -> Result<String, SQLError> {
24    let am = statement.access_method.to_ascii_lowercase();
25    if !matches!(am.as_str(), "" | "btree" | "gin" | "ivf" | "hnsw") {
26        return Err(SQLError::Unsupported(format!(
27            "CREATE INDEX access method `{}` is not supported",
28            statement.access_method
29        )));
30    }
31
32    Ok(am)
33}
34pub fn parse_ivf_index_options(options: &[(String, String)]) -> Result<IVFIndexOptions, SQLError> {
35    let mut params = IVFIndexOptions::default();
36    let mut seen = std::collections::BTreeSet::new();
37    for (key, value) in options {
38        if key.eq_ignore_ascii_case("lists") || key.eq_ignore_ascii_case("nlist") {
39            claim_index_option(&mut seen, "nlist", "ivf", key)?;
40            params.nlist = Some(parse_positive_usize_option("ivf", key, value)?);
41        } else if key.eq_ignore_ascii_case("probes") || key.eq_ignore_ascii_case("nprobe") {
42            claim_index_option(&mut seen, "nprobe", "ivf", key)?;
43            params.nprobe = Some(parse_positive_usize_option("ivf", key, value)?);
44        } else if key.eq_ignore_ascii_case("train_threshold")
45            || key.eq_ignore_ascii_case("train-threshold")
46            || key.eq_ignore_ascii_case("min_train")
47        {
48            claim_index_option(&mut seen, "train_threshold", "ivf", key)?;
49            params.train_threshold = Some(parse_positive_usize_option("ivf", key, value)?);
50        } else {
51            return Err(SQLError::Unsupported(format!(
52                "CREATE INDEX USING ivf option `{key}` is not supported"
53            )));
54        }
55    }
56    Ok(params)
57}
58
59pub fn parse_hnsw_index_options(
60    options: &[(String, String)],
61) -> Result<HNSWIndexOptions, SQLError> {
62    let mut params = HNSWIndexOptions::default();
63    let mut seen = std::collections::BTreeSet::new();
64    for (key, value) in options {
65        if key.eq_ignore_ascii_case("m") {
66            claim_index_option(&mut seen, "m", "hnsw", key)?;
67            params.m = Some(parse_positive_usize_option("hnsw", key, value)?);
68        } else if key.eq_ignore_ascii_case("ef_construction")
69            || key.eq_ignore_ascii_case("ef-construction")
70        {
71            claim_index_option(&mut seen, "ef_construction", "hnsw", key)?;
72            params.ef_construction = Some(parse_positive_usize_option("hnsw", key, value)?);
73        } else if key.eq_ignore_ascii_case("ef_search") || key.eq_ignore_ascii_case("ef-search") {
74            claim_index_option(&mut seen, "ef_search", "hnsw", key)?;
75            params.ef_search = Some(parse_positive_usize_option("hnsw", key, value)?);
76        } else if key.eq_ignore_ascii_case("rebuild_threshold")
77            || key.eq_ignore_ascii_case("rebuild-threshold")
78        {
79            claim_index_option(&mut seen, "rebuild_threshold", "hnsw", key)?;
80            params.rebuild_threshold = Some(parse_positive_usize_option("hnsw", key, value)?);
81        } else if key.eq_ignore_ascii_case("seed") {
82            claim_index_option(&mut seen, "seed", "hnsw", key)?;
83            params.seed = Some(value.parse::<u64>().map_err(|_| {
84                SQLError::TypeMismatch(format!(
85                    "CREATE INDEX USING hnsw option `{key}` must be an unsigned integer"
86                ))
87            })?);
88        } else {
89            return Err(SQLError::Unsupported(format!(
90                "CREATE INDEX USING hnsw option `{key}` is not supported"
91            )));
92        }
93    }
94    Ok(params)
95}
96
97fn claim_index_option(
98    seen: &mut std::collections::BTreeSet<&'static str>,
99    canonical: &'static str,
100    access_method: &str,
101    source: &str,
102) -> Result<(), SQLError> {
103    if !seen.insert(canonical) {
104        return Err(SQLError::Unsupported(format!(
105            "CREATE INDEX USING {access_method} option `{source}` duplicates `{canonical}`"
106        )));
107    }
108    Ok(())
109}
110
111fn parse_positive_usize_option(
112    access_method: &str,
113    key: &str,
114    value: &str,
115) -> Result<usize, SQLError> {
116    let parsed = value.parse::<usize>().map_err(|_| {
117        SQLError::TypeMismatch(format!(
118            "CREATE INDEX USING {access_method} option `{key}` must be a positive integer"
119        ))
120    })?;
121    if parsed == 0 {
122        return Err(SQLError::TypeMismatch(format!(
123            "CREATE INDEX USING {access_method} option `{key}` must be a positive integer"
124        )));
125    }
126    Ok(parsed)
127}