alopex_sql/catalog/
index.rs

1//! Index metadata definitions for the Alopex SQL catalog.
2//!
3//! This module defines [`IndexMetadata`] which stores schema information
4//! for indexes on tables.
5
6use crate::ast::ddl::IndexMethod;
7
8/// Metadata for an index in the catalog.
9///
10/// Contains the index ID, name, target table, columns, uniqueness flag,
11/// index method, and optional parameters.
12///
13/// # Examples
14///
15/// ```
16/// use alopex_sql::catalog::IndexMetadata;
17/// use alopex_sql::ast::ddl::IndexMethod;
18///
19/// // Create a B-tree index on a single column
20/// let btree_idx = IndexMetadata::new(1, "idx_users_name", "users", vec!["name".into()])
21///     .with_column_indices(vec![1])
22///     .with_method(IndexMethod::BTree);
23///
24/// assert_eq!(btree_idx.index_id, 1);
25/// assert_eq!(btree_idx.name, "idx_users_name");
26/// assert_eq!(btree_idx.table, "users");
27/// assert_eq!(btree_idx.columns, vec!["name"]);
28/// assert!(!btree_idx.unique);
29///
30/// // Create a unique composite index
31/// let unique_idx = IndexMetadata::new(2, "idx_orders_composite", "orders", vec!["user_id".into(), "order_date".into()])
32///     .with_column_indices(vec![0, 2])
33///     .with_unique(true);
34///
35/// assert!(unique_idx.unique);
36/// assert_eq!(unique_idx.columns.len(), 2);
37/// ```
38#[derive(Debug, Clone)]
39pub struct IndexMetadata {
40    /// Unique index ID assigned by the catalog.
41    pub index_id: u32,
42    /// Index name.
43    pub name: String,
44    /// Catalog name.
45    pub catalog_name: String,
46    /// Namespace name.
47    pub namespace_name: String,
48    /// Target table name.
49    pub table: String,
50    /// Target column names (supports composite indexes).
51    pub columns: Vec<String>,
52    /// Column indices within the table (for IndexStorage).
53    pub column_indices: Vec<usize>,
54    /// Whether this is a UNIQUE index.
55    pub unique: bool,
56    /// Index method (BTree, Hnsw, etc.).
57    pub method: Option<IndexMethod>,
58    /// Index options (e.g., HNSW parameters: m, ef_construction).
59    pub options: Vec<(String, String)>,
60}
61
62impl IndexMetadata {
63    /// Create a new index metadata with the given ID, name, table, and columns.
64    ///
65    /// The index defaults to non-unique, with no method specified,
66    /// empty column_indices, and no options.
67    ///
68    /// # Note
69    ///
70    /// `column_indices` should be set via `with_column_indices()` after creation,
71    /// typically resolved by the Executor when the table schema is available.
72    pub fn new(
73        index_id: u32,
74        name: impl Into<String>,
75        table: impl Into<String>,
76        columns: Vec<String>,
77    ) -> Self {
78        Self {
79            index_id,
80            name: name.into(),
81            catalog_name: "default".to_string(),
82            namespace_name: "default".to_string(),
83            table: table.into(),
84            columns,
85            column_indices: Vec::new(),
86            unique: false,
87            method: None,
88            options: Vec::new(),
89        }
90    }
91
92    /// Set the column indices (positions within the table).
93    pub fn with_column_indices(mut self, indices: Vec<usize>) -> Self {
94        self.column_indices = indices;
95        self
96    }
97
98    /// Set whether this is a UNIQUE index.
99    pub fn with_unique(mut self, unique: bool) -> Self {
100        self.unique = unique;
101        self
102    }
103
104    /// Set the index method.
105    pub fn with_method(mut self, method: IndexMethod) -> Self {
106        self.method = Some(method);
107        self
108    }
109
110    /// Add an index option.
111    pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
112        self.options.push((key.into(), value.into()));
113        self
114    }
115
116    /// Set multiple options at once.
117    pub fn with_options(mut self, options: Vec<(String, String)>) -> Self {
118        self.options = options;
119        self
120    }
121
122    /// Get an option value by key.
123    ///
124    /// Returns `None` if the option doesn't exist.
125    pub fn get_option(&self, key: &str) -> Option<&str> {
126        self.options
127            .iter()
128            .find(|(k, _)| k == key)
129            .map(|(_, v)| v.as_str())
130    }
131
132    /// Check if this index covers the given column name.
133    pub fn covers_column(&self, column: &str) -> bool {
134        self.columns.iter().any(|c| c == column)
135    }
136
137    /// Check if this is a single-column index.
138    pub fn is_single_column(&self) -> bool {
139        self.columns.len() == 1
140    }
141
142    /// Get the first (or only) column name.
143    ///
144    /// Returns `None` if no columns are defined.
145    pub fn first_column(&self) -> Option<&str> {
146        self.columns.first().map(|s| s.as_str())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_index_metadata_new() {
156        let index = IndexMetadata::new(1, "idx_users_id", "users", vec!["id".into()]);
157
158        assert_eq!(index.index_id, 1);
159        assert_eq!(index.name, "idx_users_id");
160        assert_eq!(index.table, "users");
161        assert_eq!(index.columns, vec!["id"]);
162        assert!(index.column_indices.is_empty());
163        assert!(!index.unique);
164        assert!(index.method.is_none());
165        assert!(index.options.is_empty());
166    }
167
168    #[test]
169    fn test_index_metadata_with_column_indices() {
170        let index =
171            IndexMetadata::new(1, "idx", "table", vec!["col".into()]).with_column_indices(vec![2]);
172
173        assert_eq!(index.column_indices, vec![2]);
174    }
175
176    #[test]
177    fn test_index_metadata_with_unique() {
178        let index = IndexMetadata::new(1, "idx", "table", vec!["col".into()]).with_unique(true);
179
180        assert!(index.unique);
181    }
182
183    #[test]
184    fn test_index_metadata_composite() {
185        let index = IndexMetadata::new(
186            5,
187            "idx_composite",
188            "orders",
189            vec!["user_id".into(), "order_date".into()],
190        )
191        .with_column_indices(vec![0, 3])
192        .with_unique(true);
193
194        assert_eq!(index.index_id, 5);
195        assert_eq!(index.columns.len(), 2);
196        assert_eq!(index.column_indices, vec![0, 3]);
197        assert!(index.unique);
198        assert!(!index.is_single_column());
199    }
200
201    #[test]
202    fn test_index_metadata_with_method() {
203        let index = IndexMetadata::new(1, "idx_users_name", "users", vec!["name".into()])
204            .with_method(IndexMethod::BTree);
205
206        assert_eq!(index.method, Some(IndexMethod::BTree));
207    }
208
209    #[test]
210    fn test_index_metadata_hnsw_with_options() {
211        let index = IndexMetadata::new(1, "idx_items_embedding", "items", vec!["embedding".into()])
212            .with_method(IndexMethod::Hnsw)
213            .with_option("m", "16")
214            .with_option("ef_construction", "200");
215
216        assert_eq!(index.method, Some(IndexMethod::Hnsw));
217        assert_eq!(index.options.len(), 2);
218        assert_eq!(index.get_option("m"), Some("16"));
219        assert_eq!(index.get_option("ef_construction"), Some("200"));
220        assert_eq!(index.get_option("nonexistent"), None);
221    }
222
223    #[test]
224    fn test_index_metadata_with_options_bulk() {
225        let options = vec![
226            ("m".to_string(), "32".to_string()),
227            ("ef_construction".to_string(), "400".to_string()),
228        ];
229        let index = IndexMetadata::new(1, "idx", "table", vec!["col".into()]).with_options(options);
230
231        assert_eq!(index.options.len(), 2);
232        assert_eq!(index.get_option("m"), Some("32"));
233    }
234
235    #[test]
236    fn test_covers_column() {
237        let index = IndexMetadata::new(1, "idx", "table", vec!["a".into(), "b".into()]);
238
239        assert!(index.covers_column("a"));
240        assert!(index.covers_column("b"));
241        assert!(!index.covers_column("c"));
242    }
243
244    #[test]
245    fn test_is_single_column() {
246        let single = IndexMetadata::new(1, "idx", "table", vec!["a".into()]);
247        let composite = IndexMetadata::new(2, "idx", "table", vec!["a".into(), "b".into()]);
248
249        assert!(single.is_single_column());
250        assert!(!composite.is_single_column());
251    }
252
253    #[test]
254    fn test_first_column() {
255        let index = IndexMetadata::new(1, "idx", "table", vec!["first".into(), "second".into()]);
256        let empty = IndexMetadata::new(2, "idx", "table", vec![]);
257
258        assert_eq!(index.first_column(), Some("first"));
259        assert_eq!(empty.first_column(), None);
260    }
261}