1use super::error::ParseError;
4use super::Parser;
5use crate::ast::{ConfigCommand, ConfigValueType, QueryExpr};
6use crate::lexer::Token;
7
8impl<'a> Parser<'a> {
9 pub fn parse_config_command(&mut self) -> Result<QueryExpr, ParseError> {
10 let operation = self.expect_ident_or_keyword()?.to_ascii_uppercase();
11 if operation != "PUT"
12 && operation != "GET"
13 && operation != "RESOLVE"
14 && operation != "ROTATE"
15 && operation != "DELETE"
16 && operation != "HISTORY"
17 && operation != "LIST"
18 && operation != "WATCH"
19 && operation != "INCR"
20 && operation != "DECR"
21 && operation != "ADD"
22 && operation != "INVALIDATE"
23 {
24 return Err(ParseError::expected(
25 vec![
26 "PUT",
27 "GET",
28 "RESOLVE",
29 "ROTATE",
30 "DELETE",
31 "HISTORY",
32 "LIST",
33 "WATCH",
34 "INCR",
35 "DECR",
36 "ADD",
37 "INVALIDATE",
38 ],
39 self.peek(),
40 self.position(),
41 ));
42 }
43
44 if !self.consume_ident_ci("CONFIG")? {
45 return Err(ParseError::expected(
46 vec!["CONFIG"],
47 self.peek(),
48 self.position(),
49 ));
50 }
51
52 let mut collection = self.expect_ident_or_keyword()?.to_ascii_lowercase();
53 if self.consume(&Token::Dot)? {
54 let next = self.expect_ident_or_keyword()?.to_ascii_lowercase();
55 collection = format!("{collection}.{next}");
56 }
57 let key = if operation == "LIST"
58 || (operation == "WATCH"
59 && matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("PREFIX")))
60 {
61 None
62 } else if !matches!(self.peek(), Token::Eof) {
63 Some(self.expect_ident_or_keyword()?.to_ascii_lowercase())
64 } else {
65 None
66 };
67
68 match operation.as_str() {
69 "PUT" => {
70 let key = key.ok_or_else(|| {
71 ParseError::expected(vec!["config key"], self.peek(), self.position())
72 })?;
73 self.expect(Token::Eq)?;
74 let value = self.parse_value()?;
75 let value_type = self.parse_config_value_type()?;
76 let tags = self.parse_optional_config_tags()?;
77 if self.consume_ident_ci("TTL")? || self.consume_ident_ci("EXPIRE")? {
78 self.consume_config_tail()?;
79 return Ok(QueryExpr::ConfigCommand(
80 ConfigCommand::InvalidVolatileOperation {
81 operation: "TTL/EXPIRE".to_string(),
82 collection,
83 key: Some(key),
84 },
85 ));
86 }
87 Ok(QueryExpr::ConfigCommand(ConfigCommand::Put {
88 collection,
89 key,
90 value,
91 value_type,
92 tags,
93 }))
94 }
95 "GET" => Ok(QueryExpr::ConfigCommand(ConfigCommand::Get {
96 collection,
97 key: key.ok_or_else(|| {
98 ParseError::expected(vec!["config key"], self.peek(), self.position())
99 })?,
100 })),
101 "RESOLVE" => Ok(QueryExpr::ConfigCommand(ConfigCommand::Resolve {
102 collection,
103 key: key.ok_or_else(|| {
104 ParseError::expected(vec!["config key"], self.peek(), self.position())
105 })?,
106 })),
107 "ROTATE" => {
108 let key = key.ok_or_else(|| {
109 ParseError::expected(vec!["config key"], self.peek(), self.position())
110 })?;
111 self.expect(Token::Eq)?;
112 let value = self.parse_value()?;
113 let value_type = self.parse_config_value_type()?;
114 let tags = self.parse_optional_config_tags()?;
115 if self.consume_ident_ci("TTL")? || self.consume_ident_ci("EXPIRE")? {
116 self.consume_config_tail()?;
117 return Ok(QueryExpr::ConfigCommand(
118 ConfigCommand::InvalidVolatileOperation {
119 operation: "TTL/EXPIRE".to_string(),
120 collection,
121 key: Some(key),
122 },
123 ));
124 }
125 Ok(QueryExpr::ConfigCommand(ConfigCommand::Rotate {
126 collection,
127 key,
128 value,
129 value_type,
130 tags,
131 }))
132 }
133 "DELETE" => Ok(QueryExpr::ConfigCommand(ConfigCommand::Delete {
134 collection,
135 key: key.ok_or_else(|| {
136 ParseError::expected(vec!["config key"], self.peek(), self.position())
137 })?,
138 })),
139 "HISTORY" => Ok(QueryExpr::ConfigCommand(ConfigCommand::History {
140 collection,
141 key: key.ok_or_else(|| {
142 ParseError::expected(vec!["config key"], self.peek(), self.position())
143 })?,
144 })),
145 "LIST" => {
146 if key.is_some() {
147 return Err(ParseError::expected(
148 vec!["PREFIX", "LIMIT", "OFFSET"],
149 self.peek(),
150 self.position(),
151 ));
152 }
153 let (prefix, limit, offset) = self.parse_config_list_tail()?;
154 Ok(QueryExpr::ConfigCommand(ConfigCommand::List {
155 collection,
156 prefix,
157 limit,
158 offset,
159 }))
160 }
161 "WATCH" => {
162 let (key, prefix) = if self.consume_ident_ci("PREFIX")? {
163 (self.expect_ident_or_keyword()?.to_ascii_lowercase(), true)
164 } else {
165 (
166 key.ok_or_else(|| {
167 ParseError::expected(
168 vec!["config key", "PREFIX"],
169 self.peek(),
170 self.position(),
171 )
172 })?,
173 false,
174 )
175 };
176 let from_lsn = if self.consume(&Token::From)? || self.consume_ident_ci("FROM")? {
177 if !self.consume_ident_ci("LSN")? {
178 return Err(ParseError::expected(
179 vec!["LSN"],
180 self.peek(),
181 self.position(),
182 ));
183 }
184 Some(self.parse_float()?.round() as u64)
185 } else {
186 None
187 };
188 Ok(QueryExpr::ConfigCommand(ConfigCommand::Watch {
189 collection,
190 key,
191 prefix,
192 from_lsn,
193 }))
194 }
195 _ => Ok(QueryExpr::ConfigCommand(
196 ConfigCommand::InvalidVolatileOperation {
197 operation,
198 collection,
199 key,
200 },
201 )),
202 }
203 }
204
205 fn consume_config_tail(&mut self) -> Result<(), ParseError> {
206 while !matches!(self.peek(), Token::Eof) {
207 self.advance()?;
208 }
209 Ok(())
210 }
211
212 pub(crate) fn parse_config_list_after_list(&mut self) -> Result<QueryExpr, ParseError> {
213 if !self.consume_ident_ci("CONFIG")? {
214 return Err(ParseError::expected(
215 vec!["CONFIG"],
216 self.peek(),
217 self.position(),
218 ));
219 }
220 let collection = self.parse_config_collection_name()?;
221 let (prefix, limit, offset) = self.parse_config_list_tail()?;
222 Ok(QueryExpr::ConfigCommand(ConfigCommand::List {
223 collection,
224 prefix,
225 limit,
226 offset,
227 }))
228 }
229
230 pub(crate) fn parse_config_watch_after_watch(&mut self) -> Result<QueryExpr, ParseError> {
231 if !self.consume_ident_ci("CONFIG")? {
232 return Err(ParseError::expected(
233 vec!["CONFIG"],
234 self.peek(),
235 self.position(),
236 ));
237 }
238 let collection = self.parse_config_collection_name()?;
239 let (key, prefix) = if self.consume_ident_ci("PREFIX")? {
240 (self.expect_ident_or_keyword()?.to_ascii_lowercase(), true)
241 } else {
242 (self.expect_ident_or_keyword()?.to_ascii_lowercase(), false)
243 };
244 let from_lsn = if self.consume(&Token::From)? || self.consume_ident_ci("FROM")? {
245 if !self.consume_ident_ci("LSN")? {
246 return Err(ParseError::expected(
247 vec!["LSN"],
248 self.peek(),
249 self.position(),
250 ));
251 }
252 Some(self.parse_float()?.round() as u64)
253 } else {
254 None
255 };
256 Ok(QueryExpr::ConfigCommand(ConfigCommand::Watch {
257 collection,
258 key,
259 prefix,
260 from_lsn,
261 }))
262 }
263
264 fn parse_config_list_tail(
265 &mut self,
266 ) -> Result<(Option<String>, Option<usize>, usize), ParseError> {
267 let mut prefix = None;
268 let mut limit = None;
269 let mut offset = 0usize;
270 loop {
271 if self.consume_ident_ci("PREFIX")? {
272 prefix = Some(self.expect_ident_or_keyword()?.to_ascii_lowercase());
273 } else if self.consume(&Token::Limit)? || self.consume_ident_ci("LIMIT")? {
274 limit = Some(self.parse_float()?.round().max(0.0) as usize);
275 } else if self.consume(&Token::Offset)? || self.consume_ident_ci("OFFSET")? {
276 offset = self.parse_float()?.round().max(0.0) as usize;
277 } else {
278 break;
279 }
280 }
281 Ok((prefix, limit, offset))
282 }
283
284 fn parse_config_collection_name(&mut self) -> Result<String, ParseError> {
285 let mut collection = self.expect_ident_or_keyword()?.to_ascii_lowercase();
286 if self.consume(&Token::Dot)? {
287 let next = self.expect_ident_or_keyword()?.to_ascii_lowercase();
288 collection = format!("{collection}.{next}");
289 }
290 Ok(collection)
291 }
292
293 fn parse_optional_config_tags(&mut self) -> Result<Vec<String>, ParseError> {
294 if self.consume_ident_ci("TAGS")? {
295 self.parse_kv_tag_list()
296 } else {
297 Ok(Vec::new())
298 }
299 }
300
301 fn parse_config_value_type(&mut self) -> Result<Option<ConfigValueType>, ParseError> {
302 let has_with = self.consume(&Token::With)?;
303 let has_type = self.consume_ident_ci("TYPE")?;
304 let has_schema = if !has_type {
305 self.consume(&Token::Schema)?
306 } else {
307 false
308 };
309 if !has_with && !has_type && !has_schema {
310 return Ok(None);
311 }
312 if has_with && !has_type && !has_schema {
313 return Err(ParseError::expected(
314 vec!["TYPE", "SCHEMA"],
315 self.peek(),
316 self.position(),
317 ));
318 }
319 let raw_type = self.expect_ident_or_keyword()?;
320 let Some(value_type) = ConfigValueType::parse(&raw_type) else {
321 return Err(ParseError::expected(
322 vec!["bool", "int", "string", "url", "object", "array"],
323 self.peek(),
324 self.position(),
325 ));
326 };
327 Ok(Some(value_type))
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use reddb_types::types::Value;
335
336 fn expr(input: &str) -> Result<QueryExpr, ParseError> {
337 let mut parser = Parser::new(input)?;
338 parser.parse_config_command()
339 }
340
341 #[test]
342 fn config_command_covers_dotted_collections_and_schema_type_forms() {
343 let QueryExpr::ConfigCommand(ConfigCommand::Put {
344 collection,
345 key,
346 value,
347 value_type,
348 tags,
349 }) = expr("PUT CONFIG App.Prod Feature = 'on' SCHEMA string TAGS [scope:prod]").unwrap()
350 else {
351 panic!("expected config put");
352 };
353 assert_eq!(collection, "app.prod");
354 assert_eq!(key, "feature");
355 assert_eq!(value, Value::text("on"));
356 assert_eq!(value_type, Some(ConfigValueType::String));
357 assert_eq!(tags, vec!["scope:prod".to_string()]);
358
359 let QueryExpr::ConfigCommand(ConfigCommand::Rotate {
360 collection,
361 key,
362 value_type,
363 ..
364 }) = expr("ROTATE CONFIG app feature = true WITH TYPE bool").unwrap()
365 else {
366 panic!("expected config rotate");
367 };
368 assert_eq!(collection, "app");
369 assert_eq!(key, "feature");
370 assert_eq!(value_type, Some(ConfigValueType::Bool));
371 }
372
373 #[test]
374 fn config_list_watch_and_invalid_operations_cover_optional_branches() {
375 assert!(matches!(
376 expr("LIST CONFIG App.Prod LIMIT -1 OFFSET -2").unwrap(),
377 QueryExpr::ConfigCommand(ConfigCommand::List {
378 collection,
379 prefix: None,
380 limit: Some(0),
381 offset: 0,
382 }) if collection == "app.prod"
383 ));
384 assert!(matches!(
385 expr("WATCH CONFIG App.Prod feature FROM LSN 9").unwrap(),
386 QueryExpr::ConfigCommand(ConfigCommand::Watch {
387 collection,
388 key,
389 prefix: false,
390 from_lsn: Some(9),
391 }) if collection == "app.prod" && key == "feature"
392 ));
393 assert!(matches!(
394 expr("ADD CONFIG app").unwrap(),
395 QueryExpr::ConfigCommand(ConfigCommand::InvalidVolatileOperation {
396 operation,
397 collection,
398 key: None,
399 }) if operation == "ADD" && collection == "app"
400 ));
401 assert!(matches!(
402 expr("DECR CONFIG app counter").unwrap(),
403 QueryExpr::ConfigCommand(ConfigCommand::InvalidVolatileOperation {
404 operation,
405 collection,
406 key: Some(key),
407 }) if operation == "DECR" && collection == "app" && key == "counter"
408 ));
409 }
410
411 #[test]
412 fn config_errors_are_reported_before_construction() {
413 for sql in [
414 "UPSERT CONFIG app key = 'v'",
415 "PUT app key = 'v'",
416 "PUT CONFIG app = 'v'",
417 "GET CONFIG app",
418 "WATCH CONFIG app",
419 "WATCH CONFIG app feature FROM 9",
420 "PUT CONFIG app feature = 'v' WITH",
421 "PUT CONFIG app feature = 'v' WITH TYPE nope",
422 ] {
423 assert!(expr(sql).is_err(), "{sql}");
424 }
425 assert!(crate::sql::parse_frontend("LIST CONFIG app key").is_err());
426 }
427}