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" => {
109 return Err(ParseError::new(
110 "USING RTREE was removed: the in-RAM R-tree indexed nothing and served no queries. Use USING H3 — same SEARCH SPATIAL surface, disk-resident, maintained on every write. Example: CREATE INDEX idx_loc ON events (gpsLocation) USING H3",
111 self.position(),
112 ));
113 }
114 "SPATIAL" => IndexMethod::Spatial,
115 _ => {
116 return Err(ParseError::new(
117 format!(
118 "unknown index method '{}', expected HASH, BTREE, BITMAP, SPATIAL, or H3",
119 name
120 ),
121 self.position(),
122 ));
123 }
124 };
125 self.advance()?;
126 Ok(method)
127 }
128 other => Err(ParseError::expected(
129 vec!["HASH", "BTREE", "BITMAP", "SPATIAL", "H3"],
130 &other,
131 self.position(),
132 )),
133 }
134 }
135
136 fn parse_h3_resolution_opt(&mut self) -> Result<u8, ParseError> {
140 if !self.consume(&Token::LParen)? {
141 return Ok(DEFAULT_H3_RESOLUTION);
142 }
143 let resolution = match self.peek().clone() {
144 Token::Integer(n) if (0..=15).contains(&n) => {
145 self.advance()?;
146 n as u8
147 }
148 other => {
149 return Err(ParseError::new(
150 format!("H3 resolution must be an integer 0..=15, got {other:?}"),
151 self.position(),
152 ));
153 }
154 };
155 self.expect(Token::RParen)?;
156 Ok(resolution)
157 }
158
159 fn parse_index_column_path(&mut self) -> Result<String, ParseError> {
160 let mut column = self.expect_ident_or_keyword()?;
161 while self.consume(&Token::Dot)? {
162 column.push('.');
163 column.push_str(&self.expect_ident_or_keyword()?);
164 }
165 Ok(column)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn parser(input: &str) -> Parser<'_> {
174 Parser::new(input).unwrap_or_else(|err| panic!("failed to lex {input:?}: {err:?}"))
175 }
176
177 fn parse_create_index(input: &str) -> CreateIndexQuery {
178 let mut parser = parser(input);
179 parser.expect(Token::Create).expect("CREATE");
180 let QueryExpr::CreateIndex(query) = parser
181 .parse_create_index_query()
182 .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
183 else {
184 panic!("Expected CreateIndexQuery");
185 };
186 assert!(
187 matches!(parser.peek(), Token::Eof),
188 "CREATE INDEX parse did not consume all input: {input:?}"
189 );
190 query
191 }
192
193 fn parse_drop_index(input: &str) -> DropIndexQuery {
194 let mut parser = parser(input);
195 parser.expect(Token::Drop).expect("DROP");
196 let QueryExpr::DropIndex(query) = parser
197 .parse_drop_index_query()
198 .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"))
199 else {
200 panic!("Expected DropIndexQuery");
201 };
202 assert!(
203 matches!(parser.peek(), Token::Eof),
204 "DROP INDEX parse did not consume all input: {input:?}"
205 );
206 query
207 }
208
209 #[test]
210 fn parse_create_index_defaults_and_options() {
211 let query = parse_create_index("CREATE INDEX IF NOT EXISTS idx ON users (email, type)");
212 assert_eq!(query.name, "idx");
213 assert_eq!(query.table, "users");
214 assert_eq!(query.columns, vec!["email", "type"]);
215 assert_eq!(query.method, IndexMethod::BTree);
216 assert!(query.if_not_exists);
217 assert!(!query.unique);
218
219 let query = parse_create_index("CREATE UNIQUE INDEX idx_orders ON orders (id) USING HASH");
220 assert!(query.unique);
221 assert_eq!(query.method, IndexMethod::Hash);
222 }
223
224 #[test]
225 fn parse_create_index_on_document_path() {
226 let query = parse_create_index("CREATE INDEX idx_docs_tier ON docs (body.service.tier)");
227 assert_eq!(query.name, "idx_docs_tier");
228 assert_eq!(query.table, "docs");
229 assert_eq!(query.columns, vec!["body.service.tier"]);
230 assert_eq!(query.method, IndexMethod::BTree);
231 }
232
233 #[test]
234 fn parse_index_method_accepts_all_ident_variants() {
235 for (method, expected) in [
236 ("BTREE", IndexMethod::BTree),
237 ("BITMAP", IndexMethod::Bitmap),
238 ("hash", IndexMethod::Hash),
239 ] {
240 let query = parse_create_index(&format!(
241 "CREATE INDEX idx_{method} ON users (email) USING {method}"
242 ));
243 assert_eq!(query.method, expected);
244 }
245 }
246
247 #[test]
248 fn parse_create_index_using_rtree_reports_didactic_removal() {
249 let mut p = parser("CREATE INDEX idx_loc ON events (gpsLocation) USING RTREE");
250 p.expect(Token::Create).expect("CREATE");
251 let err = p.parse_create_index_query().unwrap_err();
252 let msg = err.to_string();
253 assert!(msg.contains("USING RTREE was removed"), "{msg}");
254 assert!(msg.contains("Use USING H3"), "{msg}");
255 assert!(
256 msg.contains("CREATE INDEX idx_loc ON events (gpsLocation) USING H3"),
257 "{msg}"
258 );
259 }
260
261 #[test]
262 fn parse_create_index_using_h3_defaults_resolution() {
263 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING H3");
264 assert_eq!(query.name, "idx_loc");
265 assert_eq!(query.table, "places");
266 assert_eq!(query.columns, vec!["loc"]);
267 assert_eq!(
268 query.method,
269 IndexMethod::H3 {
270 resolution: DEFAULT_H3_RESOLUTION
271 }
272 );
273 }
274
275 #[test]
276 fn parse_create_index_using_h3_explicit_resolution() {
277 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING H3 (12)");
278 assert_eq!(query.method, IndexMethod::H3 { resolution: 12 });
279
280 let query = parse_create_index("CREATE INDEX idx_loc ON places (loc) USING h3 (0)");
282 assert_eq!(query.method, IndexMethod::H3 { resolution: 0 });
283 }
284
285 #[test]
286 fn parse_create_index_using_h3_rejects_out_of_range_resolution() {
287 let mut p = parser("CREATE INDEX idx ON places (loc) USING H3 (16)");
288 p.expect(Token::Create).expect("CREATE");
289 let err = p.parse_create_index_query().unwrap_err();
290 assert!(
291 err.to_string().contains("H3 resolution must be an integer"),
292 "{err}"
293 );
294 }
295
296 #[test]
297 fn parse_drop_index_body_covers_if_exists() {
298 let query = parse_drop_index("DROP INDEX IF EXISTS idx_email ON users");
299 assert_eq!(query.name, "idx_email");
300 assert_eq!(query.table, "users");
301 assert!(query.if_exists);
302
303 let query = parse_drop_index("DROP INDEX idx_email ON users");
304 assert!(!query.if_exists);
305 }
306
307 #[test]
308 fn parse_index_method_reports_unknown_ident_and_non_ident_errors() {
309 let mut p = parser("CREATE INDEX idx ON users (email) USING GIN");
310 p.expect(Token::Create).expect("CREATE");
311 let err = p.parse_create_index_query().unwrap_err();
312 assert!(
313 err.to_string().contains("unknown index method 'GIN'"),
314 "{err}"
315 );
316
317 let mut p = parser("CREATE INDEX idx ON users (email) USING 42");
318 p.expect(Token::Create).expect("CREATE");
319 let err = p.parse_create_index_query().unwrap_err();
320 assert!(
321 err.to_string()
322 .contains("expected: HASH, BTREE, BITMAP, SPATIAL"),
323 "{err}"
324 );
325 }
326
327 #[test]
328 fn parse_create_index_using_spatial_is_generic_spatial_method() {
329 let query = parse_create_index("CREATE INDEX gix ON places (loc) USING SPATIAL");
333 assert_eq!(query.method, IndexMethod::Spatial);
334 let query = parse_create_index("CREATE INDEX gix ON places (loc) USING spatial");
336 assert_eq!(query.method, IndexMethod::Spatial);
337 }
338}