Skip to main content

reddb_rql/parser/
config.rs

1//! Parser for stable CONFIG keyed commands.
2
3use 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}