Skip to main content

reddb_rql/parser/
index_ddl.rs

1//! DDL Parser for CREATE INDEX and DROP INDEX
2
3use super::error::ParseError;
4use super::Parser;
5use crate::ast::{CreateIndexQuery, DropIndexQuery, IndexMethod, QueryExpr};
6use crate::lexer::Token;
7
8impl<'a> Parser<'a> {
9    /// Parse: CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col1, ...) [USING method]
10    ///
11    /// Called after `Token::Create` has been consumed and we've peeked `Token::Index`
12    /// or `Token::Unique`.
13    pub fn parse_create_index_query(&mut self) -> Result<QueryExpr, ParseError> {
14        // CREATE has already been consumed by the dispatcher
15
16        let unique = self.consume(&Token::Unique)?;
17
18        self.expect(Token::Index)?;
19
20        let if_not_exists = self.match_if_not_exists()?;
21
22        let name = self.expect_ident()?;
23
24        self.expect(Token::On)?;
25
26        let table = self.expect_ident()?;
27
28        // Parse column list: (col1, col2, ...)
29        self.expect(Token::LParen)?;
30        let mut columns = Vec::new();
31        loop {
32            columns.push(self.parse_index_column_path()?);
33            if !self.consume(&Token::Comma)? {
34                break;
35            }
36        }
37        self.expect(Token::RParen)?;
38
39        // Parse optional USING method
40        let method = if self.consume(&Token::Using)? {
41            self.parse_index_method()?
42        } else {
43            IndexMethod::BTree // default
44        };
45
46        Ok(QueryExpr::CreateIndex(CreateIndexQuery {
47            name,
48            table,
49            columns,
50            method,
51            unique,
52            if_not_exists,
53        }))
54    }
55
56    /// Parse: DROP INDEX [IF EXISTS] name ON table
57    ///
58    /// Called after `Token::Drop` has been consumed and we've peeked `Token::Index`.
59    pub fn parse_drop_index_query(&mut self) -> Result<QueryExpr, ParseError> {
60        // DROP has already been consumed by the dispatcher
61
62        self.expect(Token::Index)?;
63
64        let if_exists = self.match_if_exists()?;
65
66        let name = self.expect_ident()?;
67
68        self.expect(Token::On)?;
69
70        let table = self.expect_ident()?;
71
72        Ok(QueryExpr::DropIndex(DropIndexQuery {
73            name,
74            table,
75            if_exists,
76        }))
77    }
78
79    /// Parse index method identifier: HASH | BTREE | BITMAP | RTREE.
80    /// `HASH` is also a reserved keyword token, so we match both the
81    /// keyword form and the ident form — otherwise `USING HASH`
82    /// fails with "Unexpected token: HASH" even though the parser
83    /// lists HASH as an expected option.
84    fn parse_index_method(&mut self) -> Result<IndexMethod, ParseError> {
85        let peeked = self.peek().clone();
86        if matches!(peeked, Token::Hash) {
87            self.advance()?;
88            return Ok(IndexMethod::Hash);
89        }
90        match peeked {
91            Token::Ident(ref name) => {
92                let method = match name.to_ascii_uppercase().as_str() {
93                    "HASH" => IndexMethod::Hash,
94                    "BTREE" => IndexMethod::BTree,
95                    "BITMAP" => IndexMethod::Bitmap,
96                    "RTREE" => IndexMethod::RTree,
97                    _ => {
98                        return Err(ParseError::new(
99                            format!(
100                                "unknown index method '{}', expected HASH, BTREE, BITMAP, or RTREE",
101                                name
102                            ),
103                            self.position(),
104                        ));
105                    }
106                };
107                self.advance()?;
108                Ok(method)
109            }
110            other => Err(ParseError::expected(
111                vec!["HASH", "BTREE", "BITMAP", "RTREE"],
112                &other,
113                self.position(),
114            )),
115        }
116    }
117
118    fn parse_index_column_path(&mut self) -> Result<String, ParseError> {
119        let mut column = self.expect_ident_or_keyword()?;
120        while self.consume(&Token::Dot)? {
121            column.push('.');
122            column.push_str(&self.expect_ident_or_keyword()?);
123        }
124        Ok(column)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    fn parser(input: &str) -> Parser<'_> {
133        Parser::new(input).unwrap_or_else(|err| panic!("failed to lex {input:?}: {err:?}"))
134    }
135
136    fn parse_create_index(input: &str) -> CreateIndexQuery {
137        let mut parser = parser(input);
138        parser.expect(Token::Create).expect("CREATE");
139        let QueryExpr::CreateIndex(query) = parser
140            .parse_create_index_query()
141            .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
142        else {
143            panic!("Expected CreateIndexQuery");
144        };
145        assert!(
146            matches!(parser.peek(), Token::Eof),
147            "CREATE INDEX parse did not consume all input: {input:?}"
148        );
149        query
150    }
151
152    fn parse_drop_index(input: &str) -> DropIndexQuery {
153        let mut parser = parser(input);
154        parser.expect(Token::Drop).expect("DROP");
155        let QueryExpr::DropIndex(query) = parser
156            .parse_drop_index_query()
157            .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
158        else {
159            panic!("Expected DropIndexQuery");
160        };
161        assert!(
162            matches!(parser.peek(), Token::Eof),
163            "DROP INDEX parse did not consume all input: {input:?}"
164        );
165        query
166    }
167
168    #[test]
169    fn parse_create_index_defaults_and_options() {
170        let query = parse_create_index("CREATE INDEX IF NOT EXISTS idx ON users (email, type)");
171        assert_eq!(query.name, "idx");
172        assert_eq!(query.table, "users");
173        assert_eq!(query.columns, vec!["email", "type"]);
174        assert_eq!(query.method, IndexMethod::BTree);
175        assert!(query.if_not_exists);
176        assert!(!query.unique);
177
178        let query = parse_create_index("CREATE UNIQUE INDEX idx_orders ON orders (id) USING HASH");
179        assert!(query.unique);
180        assert_eq!(query.method, IndexMethod::Hash);
181    }
182
183    #[test]
184    fn parse_create_index_on_document_path() {
185        let query = parse_create_index("CREATE INDEX idx_docs_tier ON docs (body.service.tier)");
186        assert_eq!(query.name, "idx_docs_tier");
187        assert_eq!(query.table, "docs");
188        assert_eq!(query.columns, vec!["body.service.tier"]);
189        assert_eq!(query.method, IndexMethod::BTree);
190    }
191
192    #[test]
193    fn parse_index_method_accepts_all_ident_variants() {
194        for (method, expected) in [
195            ("BTREE", IndexMethod::BTree),
196            ("BITMAP", IndexMethod::Bitmap),
197            ("RTREE", IndexMethod::RTree),
198            ("hash", IndexMethod::Hash),
199        ] {
200            let query = parse_create_index(&format!(
201                "CREATE INDEX idx_{method} ON users (email) USING {method}"
202            ));
203            assert_eq!(query.method, expected);
204        }
205    }
206
207    #[test]
208    fn parse_drop_index_body_covers_if_exists() {
209        let query = parse_drop_index("DROP INDEX IF EXISTS idx_email ON users");
210        assert_eq!(query.name, "idx_email");
211        assert_eq!(query.table, "users");
212        assert!(query.if_exists);
213
214        let query = parse_drop_index("DROP INDEX idx_email ON users");
215        assert!(!query.if_exists);
216    }
217
218    #[test]
219    fn parse_index_method_reports_unknown_ident_and_non_ident_errors() {
220        let mut p = parser("CREATE INDEX idx ON users (email) USING GIN");
221        p.expect(Token::Create).expect("CREATE");
222        let err = p.parse_create_index_query().unwrap_err();
223        assert!(
224            err.to_string().contains("unknown index method 'GIN'"),
225            "{err}"
226        );
227
228        let mut p = parser("CREATE INDEX idx ON users (email) USING 42");
229        p.expect(Token::Create).expect("CREATE");
230        let err = p.parse_create_index_query().unwrap_err();
231        assert!(
232            err.to_string()
233                .contains("expected: HASH, BTREE, BITMAP, RTREE"),
234            "{err}"
235        );
236    }
237}