Skip to main content

sql/parsers/
truncate_parser.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use snafu::{ResultExt, ensure};
16use sqlparser::keywords::Keyword;
17use sqlparser::tokenizer::Token;
18
19use crate::error::{self, InvalidSqlSnafu, InvalidTableNameSnafu, Result, UnexpectedTokenSnafu};
20use crate::parser::ParserContext;
21use crate::parsers::utils;
22use crate::statements::statement::Statement;
23use crate::statements::truncate::TruncateTable;
24
25/// `TRUNCATE [TABLE] table_name [FILE RANGE ...] [WITH (wait = ..., timeout = ...)];`
26impl ParserContext<'_> {
27    pub(crate) fn parse_truncate(&mut self) -> Result<Statement> {
28        let _ = self.parser.next_token();
29        let _ = self.parser.parse_keyword(Keyword::TABLE);
30
31        let raw_table_ident =
32            self.parse_object_name()
33                .with_context(|_| error::UnexpectedSnafu {
34                    expected: "a table name",
35                    actual: self.peek_token_as_string(),
36                })?;
37        let table_ident = Self::canonicalize_object_name(raw_table_ident)?;
38
39        ensure!(
40            !table_ident.0.is_empty(),
41            InvalidTableNameSnafu {
42                name: table_ident.to_string()
43            }
44        );
45
46        let have_range = self.parser.parse_keywords(&[Keyword::FILE, Keyword::RANGE]);
47
48        // if no range is specified, we just truncate the table
49        if !have_range {
50            let options = utils::parse_ddl_options(&mut self.parser)?;
51            return Ok(Statement::TruncateTable(
52                TruncateTable::new(table_ident).with_options(options),
53            ));
54        }
55
56        // parse a list of time ranges consist of (Timestamp, Timestamp),?
57        let mut time_ranges = vec![];
58        loop {
59            let _ = self
60                .parser
61                .expect_token(&sqlparser::tokenizer::Token::LParen)
62                .with_context(|_| error::UnexpectedSnafu {
63                    expected: "a left parenthesis",
64                    actual: self.peek_token_as_string(),
65                })?;
66
67            // parse to values here, no need to valid in parser
68            let start = self
69                .parser
70                .parse_value()
71                .map(|x| x.value)
72                .with_context(|e| error::UnexpectedSnafu {
73                    expected: "a timestamp value",
74                    actual: e.to_string(),
75                })?;
76
77            let _ = self
78                .parser
79                .expect_token(&sqlparser::tokenizer::Token::Comma)
80                .with_context(|_| error::UnexpectedSnafu {
81                    expected: "a comma",
82                    actual: self.peek_token_as_string(),
83                })?;
84
85            let end = self
86                .parser
87                .parse_value()
88                .map(|x| x.value)
89                .with_context(|_| error::UnexpectedSnafu {
90                    expected: "a timestamp",
91                    actual: self.peek_token_as_string(),
92                })?;
93
94            let _ = self
95                .parser
96                .expect_token(&sqlparser::tokenizer::Token::RParen)
97                .with_context(|_| error::UnexpectedSnafu {
98                    expected: "a right parenthesis",
99                    actual: self.peek_token_as_string(),
100                })?;
101
102            time_ranges.push((start, end));
103
104            let peek = self.parser.peek_token().token;
105
106            match peek {
107                Token::Word(word) if word.keyword == Keyword::WITH => break,
108                sqlparser::tokenizer::Token::EOF | Token::SemiColon => {
109                    if time_ranges.is_empty() {
110                        return Err(InvalidSqlSnafu {
111                            msg: "TRUNCATE TABLE RANGE must have at least one range".to_string(),
112                        }
113                        .build());
114                    }
115                    break;
116                }
117                Token::Comma => {
118                    self.parser.next_token(); // Consume the comma
119                    let next_peek = self.parser.peek_token().token; // Peek the token after the comma
120                    if matches!(next_peek, Token::EOF | Token::SemiColon)
121                        || matches!(next_peek, Token::Word(word) if word.keyword == Keyword::WITH)
122                    {
123                        break; // Trailing comma, end of statement
124                    }
125                    // Otherwise, continue to parse next range
126                    continue;
127                }
128                _ => UnexpectedTokenSnafu {
129                    expected: "a comma, WITH or end of statement",
130                    actual: self.peek_token_as_string(),
131                }
132                .fail()?,
133            }
134        }
135
136        let options = utils::parse_ddl_options(&mut self.parser)?;
137        Ok(Statement::TruncateTable(
138            TruncateTable::new_with_ranges(table_ident, time_ranges).with_options(options),
139        ))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use sqlparser::ast::{Ident, ObjectName};
146
147    use super::*;
148    use crate::dialect::GreptimeDbDialect;
149    use crate::parser::ParseOptions;
150
151    #[test]
152    fn test_parse_truncate_ddl_options() {
153        for prefix in [
154            "TRUNCATE foo",
155            "TRUNCATE TABLE foo",
156            "TRUNCATE TABLE foo FILE RANGE (0, 20)",
157            "TRUNCATE TABLE foo FILE RANGE (0, 20), (30, 40),",
158        ] {
159            let sql = format!("{prefix} WITH (WAIT = false, TIMEOUT = '30s');");
160            let statements = ParserContext::create_with_dialect(
161                &sql,
162                &GreptimeDbDialect {},
163                ParseOptions::default(),
164            )
165            .unwrap();
166            let Statement::TruncateTable(truncate) = &statements[0] else {
167                unreachable!()
168            };
169            assert_eq!(truncate.options().get("wait"), Some("false"));
170            assert_eq!(truncate.options().get("timeout"), Some("30s"));
171
172            let formatted = truncate.to_string();
173            let reparsed = ParserContext::create_with_dialect(
174                &formatted,
175                &GreptimeDbDialect {},
176                ParseOptions::default(),
177            )
178            .unwrap();
179            let Statement::TruncateTable(reparsed) = &reparsed[0] else {
180                unreachable!()
181            };
182            assert_eq!(truncate.table_name(), reparsed.table_name());
183            assert_eq!(truncate.time_ranges(), reparsed.time_ranges());
184            assert_eq!(
185                truncate.options().to_str_map(),
186                reparsed.options().to_str_map()
187            );
188        }
189
190        for options in [
191            "ttl = '1d'",
192            "unknown = true",
193            "wait = NULL",
194            "timeout = ['30s']",
195        ] {
196            let sql = format!("TRUNCATE TABLE foo WITH ({options})");
197            assert!(
198                ParserContext::create_with_dialect(
199                    &sql,
200                    &GreptimeDbDialect {},
201                    ParseOptions::default(),
202                )
203                .is_err(),
204                "{sql}"
205            );
206        }
207    }
208
209    #[test]
210    pub fn test_parse_truncate_with_ranges() {
211        let sql = r#"TRUNCATE foo FILE RANGE (0, 20)"#;
212        let mut stmts =
213            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
214                .unwrap();
215        assert_eq!(
216            stmts.pop().unwrap(),
217            Statement::TruncateTable(TruncateTable::new_with_ranges(
218                ObjectName::from(vec![Ident::new("foo")]),
219                vec![(
220                    sqlparser::ast::Value::Number("0".to_string(), false),
221                    sqlparser::ast::Value::Number("20".to_string(), false)
222                )]
223            ))
224        );
225
226        let sql = r#"TRUNCATE TABLE foo FILE RANGE ("2000-01-01 00:00:00+00:00", "2000-01-01 00:00:00+00:00"), (2,33)"#;
227        let mut stmts =
228            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
229                .unwrap();
230        assert_eq!(
231            stmts.pop().unwrap(),
232            Statement::TruncateTable(TruncateTable::new_with_ranges(
233                ObjectName::from(vec![Ident::new("foo")]),
234                vec![
235                    (
236                        sqlparser::ast::Value::DoubleQuotedString(
237                            "2000-01-01 00:00:00+00:00".to_string()
238                        ),
239                        sqlparser::ast::Value::DoubleQuotedString(
240                            "2000-01-01 00:00:00+00:00".to_string()
241                        )
242                    ),
243                    (
244                        sqlparser::ast::Value::Number("2".to_string(), false),
245                        sqlparser::ast::Value::Number("33".to_string(), false)
246                    )
247                ]
248            ))
249        );
250
251        let sql = "TRUNCATE TABLE my_schema.foo FILE RANGE (1, 2), (3, 4),";
252        let mut stmts =
253            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
254                .unwrap();
255        assert_eq!(
256            stmts.pop().unwrap(),
257            Statement::TruncateTable(TruncateTable::new_with_ranges(
258                ObjectName::from(vec![Ident::new("my_schema"), Ident::new("foo")]),
259                vec![
260                    (
261                        sqlparser::ast::Value::Number("1".to_string(), false),
262                        sqlparser::ast::Value::Number("2".to_string(), false)
263                    ),
264                    (
265                        sqlparser::ast::Value::Number("3".to_string(), false),
266                        sqlparser::ast::Value::Number("4".to_string(), false)
267                    )
268                ]
269            ))
270        );
271
272        let sql = "TRUNCATE my_schema.foo FILE RANGE (1,2),";
273        let mut stmts =
274            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
275                .unwrap();
276        assert_eq!(
277            stmts.pop().unwrap(),
278            Statement::TruncateTable(TruncateTable::new_with_ranges(
279                ObjectName::from(vec![Ident::new("my_schema"), Ident::new("foo")]),
280                vec![(
281                    sqlparser::ast::Value::Number("1".to_string(), false),
282                    sqlparser::ast::Value::Number("2".to_string(), false)
283                )]
284            ))
285        );
286
287        let sql = "TRUNCATE TABLE my_catalog.my_schema.foo FILE RANGE (1,2);";
288        let mut stmts =
289            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
290                .unwrap();
291        assert_eq!(
292            stmts.pop().unwrap(),
293            Statement::TruncateTable(TruncateTable::new_with_ranges(
294                ObjectName::from(vec![
295                    Ident::new("my_catalog"),
296                    Ident::new("my_schema"),
297                    Ident::new("foo")
298                ]),
299                vec![(
300                    sqlparser::ast::Value::Number("1".to_string(), false),
301                    sqlparser::ast::Value::Number("2".to_string(), false)
302                )]
303            ))
304        );
305
306        let sql = "TRUNCATE drop FILE RANGE (1,2)";
307        let mut stmts =
308            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
309                .unwrap();
310        assert_eq!(
311            stmts.pop().unwrap(),
312            Statement::TruncateTable(TruncateTable::new_with_ranges(
313                ObjectName::from(vec![Ident::new("drop")]),
314                vec![(
315                    sqlparser::ast::Value::Number("1".to_string(), false),
316                    sqlparser::ast::Value::Number("2".to_string(), false)
317                )]
318            ))
319        );
320
321        let sql = "TRUNCATE `drop`";
322        let mut stmts =
323            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
324                .unwrap();
325        assert_eq!(
326            stmts.pop().unwrap(),
327            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
328                Ident::with_quote('`', "drop"),
329            ])))
330        );
331
332        let sql = "TRUNCATE \"drop\" FILE RANGE (\"1\", \"2\")";
333        let mut stmts =
334            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
335                .unwrap();
336        assert_eq!(
337            stmts.pop().unwrap(),
338            Statement::TruncateTable(TruncateTable::new_with_ranges(
339                ObjectName::from(vec![Ident::with_quote('"', "drop")]),
340                vec![(
341                    sqlparser::ast::Value::DoubleQuotedString("1".to_string()),
342                    sqlparser::ast::Value::DoubleQuotedString("2".to_string())
343                )]
344            ))
345        );
346    }
347
348    #[test]
349    pub fn test_parse_truncate() {
350        let sql = "TRUNCATE foo";
351        let mut stmts =
352            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
353                .unwrap();
354        assert_eq!(
355            stmts.pop().unwrap(),
356            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
357                "foo"
358            )])))
359        );
360
361        let sql = "TRUNCATE TABLE foo";
362        let mut stmts =
363            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
364                .unwrap();
365        assert_eq!(
366            stmts.pop().unwrap(),
367            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
368                "foo"
369            )])))
370        );
371
372        let sql = "TRUNCATE TABLE my_schema.foo";
373        let mut stmts =
374            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
375                .unwrap();
376        assert_eq!(
377            stmts.pop().unwrap(),
378            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
379                Ident::new("my_schema"),
380                Ident::new("foo")
381            ])))
382        );
383
384        let sql = "TRUNCATE my_schema.foo";
385        let mut stmts =
386            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
387                .unwrap();
388        assert_eq!(
389            stmts.pop().unwrap(),
390            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
391                Ident::new("my_schema"),
392                Ident::new("foo")
393            ])))
394        );
395
396        let sql = "TRUNCATE TABLE my_catalog.my_schema.foo";
397        let mut stmts =
398            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
399                .unwrap();
400        assert_eq!(
401            stmts.pop().unwrap(),
402            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
403                Ident::new("my_catalog"),
404                Ident::new("my_schema"),
405                Ident::new("foo")
406            ])))
407        );
408
409        let sql = "TRUNCATE drop";
410        let mut stmts =
411            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
412                .unwrap();
413        assert_eq!(
414            stmts.pop().unwrap(),
415            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
416                "drop"
417            )])))
418        );
419
420        let sql = "TRUNCATE `drop`";
421        let mut stmts =
422            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
423                .unwrap();
424        assert_eq!(
425            stmts.pop().unwrap(),
426            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
427                Ident::with_quote('`', "drop"),
428            ])))
429        );
430
431        let sql = "TRUNCATE \"drop\"";
432        let mut stmts =
433            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
434                .unwrap();
435        assert_eq!(
436            stmts.pop().unwrap(),
437            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
438                Ident::with_quote('"', "drop"),
439            ])))
440        );
441    }
442
443    #[test]
444    pub fn test_parse_invalid_truncate() {
445        let sql = "TRUNCATE SCHEMA foo";
446        let result =
447            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
448        assert!(result.is_err(), "result is: {result:?}");
449
450        let sql = "TRUNCATE";
451        let result =
452            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
453        assert!(result.is_err(), "result is: {result:?}");
454
455        let sql = "TRUNCATE TABLE foo RANGE";
456        let result =
457            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
458        assert!(
459            result.is_err()
460                && format!("{result:?}").contains("SQL statement is not supported, keyword: RANGE"),
461            "result is: {result:?}"
462        );
463
464        let sql = "TRUNCATE TABLE foo FILE";
465        let result =
466            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
467        assert!(
468            result.is_err()
469                && format!("{result:?}").contains("SQL statement is not supported, keyword: FILE"),
470            "result is: {result:?}"
471        );
472
473        let sql = "TRUNCATE TABLE foo FILE RANGE";
474        let result =
475            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
476        assert!(
477            result.is_err() && format!("{result:?}").contains("expected: 'a left parenthesis'"),
478            "result is: {result:?}"
479        );
480
481        let sql = "TRUNCATE TABLE foo FILE RANGE (";
482        let result =
483            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
484        assert!(
485            result.is_err() && format!("{result:?}").contains("expected: 'a timestamp value'"),
486            "result is: {result:?}"
487        );
488
489        let sql = "TRUNCATE TABLE foo FILE RANGE ()";
490        let result =
491            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
492        assert!(
493            result.is_err() && format!("{result:?}").contains("expected: 'a timestamp value'"),
494            "result is: {result:?}"
495        );
496
497        let sql = "TRUNCATE TABLE foo FILE RANGE (1 2) (3 4)";
498        let result =
499            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
500        assert!(
501            result.is_err() && format!("{result:?}").contains("expected: 'a comma'"),
502            "result is: {result:?}"
503        );
504
505        let sql = "TRUNCATE TABLE foo FILE RANGE (,),(3,4)";
506        let result =
507            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
508        assert!(
509            result.is_err() && format!("{result:?}").contains("Expected: a value, found: ,"),
510            "result is: {result:?}"
511        );
512
513        let sql = "TRUNCATE TABLE foo FILE RANGE (1,2) (3,4)";
514        let result =
515            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
516        assert!(
517            result.is_err()
518                && format!("{result:?}")
519                    .contains("expected: 'a comma, WITH or end of statement', found: ("),
520            "result is: {result:?}"
521        );
522
523        let sql = "TRUNCATE TABLE foo FILE RANGE (1,2),,,,,,,,,(3,4)";
524        let result =
525            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
526        assert!(
527            result.is_err()
528                && format!("{result:?}").contains("expected: 'a left parenthesis', found: ,"),
529            "result is: {result:?}"
530        );
531    }
532}