1use super::error::ParseError;
4use super::Parser;
5use crate::ast::{CreateIndexQuery, DropIndexQuery, IndexMethod, QueryExpr, DEFAULT_H3_RESOLUTION};
6use crate::lexer::Token;
7
8impl<'a> Parser<'a> {
9 pub fn parse_create_index_query(&mut self) -> Result<QueryExpr, ParseError> {
14 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 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 let method = if self.consume(&Token::Using)? {
41 self.parse_index_method()?
42 } else {
43 IndexMethod::BTree };
45
46 Ok(QueryExpr::CreateIndex(CreateIndexQuery {
47 name,
48 table,
49 columns,
50 method,
51 unique,
52 if_not_exists,
53 }))
54 }
55
56 pub fn parse_drop_index_query(&mut self) -> Result<QueryExpr, ParseError> {
60 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 fn parse_index_method(&mut self) -> Result<IndexMethod, ParseError> {
91 let peeked = self.peek().clone();
92 if matches!(peeked, Token::Hash) {
93 self.advance()?;
94 return Ok(IndexMethod::Hash);
95 }
96 match peeked {
97 Token::Ident(ref name) => {
98 let upper = name.to_ascii_uppercase();
99 if upper == "H3" {
100 self.advance()?;
101 let resolution = self.parse_h3_resolution_opt()?;
102 return Ok(IndexMethod::H3 { resolution });
103 }
104 let method = match upper.as_str() {
105 "HASH" => IndexMethod::Hash,
106 "BTREE" => IndexMethod::BTree,
107 "BITMAP" => IndexMethod::Bitmap,
108 "RTREE" => IndexMethod::RTree,
109 "SPATIAL" => IndexMethod::Spatial,
110 _ => {
111 return Err(ParseError::new(
112 format!(
113 "unknown index method '{}', expected HASH, BTREE, BITMAP, RTREE, SPATIAL, or H3",
114 name
115 ),
116 self.position(),
117 ));
118 }
119 };
120 self.advance()?;
121 Ok(method)
122 }
123 other => Err(ParseError::expected(
124 vec!["HASH", "BTREE", "BITMAP", "RTREE", "SPATIAL", "H3"],
125 &other,
126 self.position(),
127 )),
128 }
129 }
130
131 fn parse_h3_resolution_opt(&mut self) -> Result<u8, ParseError> {
135 if !self.consume(&Token::LParen)? {
136 return Ok(DEFAULT_H3_RESOLUTION);
137 }
138 let resolution = match self.peek().clone() {
139 Token::Integer(n) if (0..=15).contains(&n) => {
140 self.advance()?;
141 n as u8
142 }
143 other => {
144 return Err(ParseError::new(
145 format!("H3 resolution must be an integer 0..=15, got {other:?}"),
146 self.position(),
147 ));
148 }
149 };
150 self.expect(Token::RParen)?;
151 Ok(resolution)
152 }
153
154 fn parse_index_column_path(&mut self) -> Result<String, ParseError> {
155 let mut column = self.expect_ident_or_keyword()?;
156 while self.consume(&Token::Dot)? {
157 column.push('.');
158 column.push_str(&self.expect_ident_or_keyword()?);
159 }
160 Ok(column)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn parser(input: &str) -> Parser<'_> {
169 Parser::new(input).unwrap_or_else(|err| panic!("failed to lex {input:?}: {err:?}"))
170 }
171
172 fn parse_create_index(input: &str) -> CreateIndexQuery {
173 let mut parser = parser(input);
174 parser.expect(Token::Create).expect("CREATE");
175 let QueryExpr::CreateIndex(query) = parser
176 .parse_create_index_query()
177 .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
178 else {
179 panic!("Expected CreateIndexQuery");
180 };
181 assert!(
182 matches!(parser.peek(), Token::Eof),
183 "CREATE INDEX parse did not consume all input: {input:?}"
184 );
185 query
186 }
187
188 fn parse_drop_index(input: &str) -> DropIndexQuery {
189 let mut parser = parser(input);
190 parser.expect(Token::Drop).expect("DROP");
191 let QueryExpr::DropIndex(query) = parser
192 .parse_drop_index_query()
193 .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
194 else {
195 panic!("Expected DropIndexQuery");
196 };
197 assert!(
198 matches!(parser.peek(), Token::Eof),
199 "DROP INDEX parse did not consume all input: {input:?}"
200 );
201 query
202 }
203
204 #[test]
205 fn parse_create_index_defaults_and_options() {
206 let query = parse_create_index("CREATE INDEX IF NOT EXISTS idx ON users (email, type)");
207 assert_eq!(query.name, "idx");
208 assert_eq!(query.table, "users");
209 assert_eq!(query.columns, vec!["email", "type"]);
210 assert_eq!(query.method, IndexMethod::BTree);
211 assert!(query.if_not_exists);
212 assert!(!query.unique);
213
214 let query = parse_create_index("CREATE UNIQUE INDEX idx_orders ON orders (id) USING HASH");
215 assert!(query.unique);
216 assert_eq!(query.method, IndexMethod::Hash);
217 }
218
219 #[test]
220 fn parse_create_index_on_document_path() {
221 let query = parse_create_index("CREATE INDEX idx_docs_tier ON docs (body.service.tier)");
222 assert_eq!(query.name, "idx_docs_tier");
223 assert_eq!(query.table, "docs");
224 assert_eq!(query.columns, vec!["body.service.tier"]);
225 assert_eq!(query.method, IndexMethod::BTree);
226 }
227
228 #[test]
229 fn parse_index_method_accepts_all_ident_variants() {
230 for (method, expected) in [
231 ("BTREE", IndexMethod::BTree),
232 ("BITMAP", IndexMethod::Bitmap),
233 ("RTREE", IndexMethod::RTree),
234 ("hash", IndexMethod::Hash),
235 ] {
236 let query = parse_create_index(&format!(
237 "CREATE INDEX idx_{method} ON users (email) USING {method}"
238 ));
239 assert_eq!(query.method, expected);
240 }
241 }
242
243 #[test]
244 fn parse_create_index_using_h3_defaults_resolution() {
245 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING H3");
246 assert_eq!(query.name, "idx_loc");
247 assert_eq!(query.table, "places");
248 assert_eq!(query.columns, vec!["loc"]);
249 assert_eq!(
250 query.method,
251 IndexMethod::H3 {
252 resolution: DEFAULT_H3_RESOLUTION
253 }
254 );
255 }
256
257 #[test]
258 fn parse_create_index_using_h3_explicit_resolution() {
259 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING H3 (12)");
260 assert_eq!(query.method, IndexMethod::H3 { resolution: 12 });
261
262 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING h3 (0)");
264 assert_eq!(query.method, IndexMethod::H3 { resolution: 0 });
265 }
266
267 #[test]
268 fn parse_create_index_using_h3_rejects_out_of_range_resolution() {
269 let mut p = parser("CREATE INDEX idx ON places (loc) USING H3 (16)");
270 p.expect(Token::Create).expect("CREATE");
271 let err = p.parse_create_index_query().unwrap_err();
272 assert!(
273 err.to_string().contains("H3 resolution must be an integer"),
274 "{err}"
275 );
276 }
277
278 #[test]
279 fn parse_drop_index_body_covers_if_exists() {
280 let query = parse_drop_index("DROP INDEX IF EXISTS idx_email ON users");
281 assert_eq!(query.name, "idx_email");
282 assert_eq!(query.table, "users");
283 assert!(query.if_exists);
284
285 let query = parse_drop_index("DROP INDEX idx_email ON users");
286 assert!(!query.if_exists);
287 }
288
289 #[test]
290 fn parse_index_method_reports_unknown_ident_and_non_ident_errors() {
291 let mut p = parser("CREATE INDEX idx ON users (email) USING GIN");
292 p.expect(Token::Create).expect("CREATE");
293 let err = p.parse_create_index_query().unwrap_err();
294 assert!(
295 err.to_string().contains("unknown index method 'GIN'"),
296 "{err}"
297 );
298
299 let mut p = parser("CREATE INDEX idx ON users (email) USING 42");
300 p.expect(Token::Create).expect("CREATE");
301 let err = p.parse_create_index_query().unwrap_err();
302 assert!(
303 err.to_string()
304 .contains("expected: HASH, BTREE, BITMAP, RTREE"),
305 "{err}"
306 );
307 }
308
309 #[test]
310 fn parse_create_index_using_spatial_is_generic_spatial_method() {
311 let query = parse_create_index("CREATE INDEX gix ON places (loc) USING SPATIAL");
315 assert_eq!(query.method, IndexMethod::Spatial);
316 let query = parse_create_index("CREATE INDEX gix ON places (loc) USING spatial");
318 assert_eq!(query.method, IndexMethod::Spatial);
319 }
320}