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::{ensure, ResultExt};
16use sqlparser::keywords::Keyword;
17use sqlparser::tokenizer::Token;
18
19use crate::error::{self, InvalidSqlSnafu, InvalidTableNameSnafu, Result, UnexpectedTokenSnafu};
20use crate::parser::ParserContext;
21use crate::statements::statement::Statement;
22use crate::statements::truncate::TruncateTable;
23
24/// `TRUNCATE [TABLE] table_name;`
25impl ParserContext<'_> {
26    pub(crate) fn parse_truncate(&mut self) -> Result<Statement> {
27        let _ = self.parser.next_token();
28        let _ = self.parser.parse_keyword(Keyword::TABLE);
29
30        let raw_table_ident =
31            self.parse_object_name()
32                .with_context(|_| error::UnexpectedSnafu {
33                    expected: "a table name",
34                    actual: self.peek_token_as_string(),
35                })?;
36        let table_ident = Self::canonicalize_object_name(raw_table_ident);
37
38        ensure!(
39            !table_ident.0.is_empty(),
40            InvalidTableNameSnafu {
41                name: table_ident.to_string()
42            }
43        );
44
45        let have_range = self.parser.parse_keywords(&[Keyword::FILE, Keyword::RANGE]);
46
47        // if no range is specified, we just truncate the table
48        if !have_range {
49            return Ok(Statement::TruncateTable(TruncateTable::new(table_ident)));
50        }
51
52        // parse a list of time ranges consist of (Timestamp, Timestamp),?
53        let mut time_ranges = vec![];
54        loop {
55            let _ = self
56                .parser
57                .expect_token(&sqlparser::tokenizer::Token::LParen)
58                .with_context(|_| error::UnexpectedSnafu {
59                    expected: "a left parenthesis",
60                    actual: self.peek_token_as_string(),
61                })?;
62
63            // parse to values here, no need to valid in parser
64            let start = self
65                .parser
66                .parse_value()
67                .map(|x| x.value)
68                .with_context(|e| error::UnexpectedSnafu {
69                    expected: "a timestamp value",
70                    actual: e.to_string(),
71                })?;
72
73            let _ = self
74                .parser
75                .expect_token(&sqlparser::tokenizer::Token::Comma)
76                .with_context(|_| error::UnexpectedSnafu {
77                    expected: "a comma",
78                    actual: self.peek_token_as_string(),
79                })?;
80
81            let end = self
82                .parser
83                .parse_value()
84                .map(|x| x.value)
85                .with_context(|_| error::UnexpectedSnafu {
86                    expected: "a timestamp",
87                    actual: self.peek_token_as_string(),
88                })?;
89
90            let _ = self
91                .parser
92                .expect_token(&sqlparser::tokenizer::Token::RParen)
93                .with_context(|_| error::UnexpectedSnafu {
94                    expected: "a right parenthesis",
95                    actual: self.peek_token_as_string(),
96                })?;
97
98            time_ranges.push((start, end));
99
100            let peek = self.parser.peek_token().token;
101
102            match peek {
103                sqlparser::tokenizer::Token::EOF | Token::SemiColon => {
104                    if time_ranges.is_empty() {
105                        return Err(InvalidSqlSnafu {
106                            msg: "TRUNCATE TABLE RANGE must have at least one range".to_string(),
107                        }
108                        .build());
109                    }
110                    break;
111                }
112                Token::Comma => {
113                    self.parser.next_token(); // Consume the comma
114                    let next_peek = self.parser.peek_token().token; // Peek the token after the comma
115                    if matches!(next_peek, Token::EOF | Token::SemiColon) {
116                        break; // Trailing comma, end of statement
117                    }
118                    // Otherwise, continue to parse next range
119                    continue;
120                }
121                _ => UnexpectedTokenSnafu {
122                    expected: "a comma or end of statement",
123                    actual: self.peek_token_as_string(),
124                }
125                .fail()?,
126            }
127        }
128
129        Ok(Statement::TruncateTable(TruncateTable::new_with_ranges(
130            table_ident,
131            time_ranges,
132        )))
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use sqlparser::ast::{Ident, ObjectName};
139
140    use super::*;
141    use crate::dialect::GreptimeDbDialect;
142    use crate::parser::ParseOptions;
143
144    #[test]
145    pub fn test_parse_truncate_with_ranges() {
146        let sql = r#"TRUNCATE foo FILE RANGE (0, 20)"#;
147        let mut stmts =
148            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
149                .unwrap();
150        assert_eq!(
151            stmts.pop().unwrap(),
152            Statement::TruncateTable(TruncateTable::new_with_ranges(
153                ObjectName::from(vec![Ident::new("foo")]),
154                vec![(
155                    sqlparser::ast::Value::Number("0".to_string(), false),
156                    sqlparser::ast::Value::Number("20".to_string(), false)
157                )]
158            ))
159        );
160
161        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)"#;
162        let mut stmts =
163            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
164                .unwrap();
165        assert_eq!(
166            stmts.pop().unwrap(),
167            Statement::TruncateTable(TruncateTable::new_with_ranges(
168                ObjectName::from(vec![Ident::new("foo")]),
169                vec![
170                    (
171                        sqlparser::ast::Value::DoubleQuotedString(
172                            "2000-01-01 00:00:00+00:00".to_string()
173                        ),
174                        sqlparser::ast::Value::DoubleQuotedString(
175                            "2000-01-01 00:00:00+00:00".to_string()
176                        )
177                    ),
178                    (
179                        sqlparser::ast::Value::Number("2".to_string(), false),
180                        sqlparser::ast::Value::Number("33".to_string(), false)
181                    )
182                ]
183            ))
184        );
185
186        let sql = "TRUNCATE TABLE my_schema.foo FILE RANGE (1, 2), (3, 4),";
187        let mut stmts =
188            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
189                .unwrap();
190        assert_eq!(
191            stmts.pop().unwrap(),
192            Statement::TruncateTable(TruncateTable::new_with_ranges(
193                ObjectName::from(vec![Ident::new("my_schema"), Ident::new("foo")]),
194                vec![
195                    (
196                        sqlparser::ast::Value::Number("1".to_string(), false),
197                        sqlparser::ast::Value::Number("2".to_string(), false)
198                    ),
199                    (
200                        sqlparser::ast::Value::Number("3".to_string(), false),
201                        sqlparser::ast::Value::Number("4".to_string(), false)
202                    )
203                ]
204            ))
205        );
206
207        let sql = "TRUNCATE my_schema.foo FILE RANGE (1,2),";
208        let mut stmts =
209            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
210                .unwrap();
211        assert_eq!(
212            stmts.pop().unwrap(),
213            Statement::TruncateTable(TruncateTable::new_with_ranges(
214                ObjectName::from(vec![Ident::new("my_schema"), Ident::new("foo")]),
215                vec![(
216                    sqlparser::ast::Value::Number("1".to_string(), false),
217                    sqlparser::ast::Value::Number("2".to_string(), false)
218                )]
219            ))
220        );
221
222        let sql = "TRUNCATE TABLE my_catalog.my_schema.foo FILE RANGE (1,2);";
223        let mut stmts =
224            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
225                .unwrap();
226        assert_eq!(
227            stmts.pop().unwrap(),
228            Statement::TruncateTable(TruncateTable::new_with_ranges(
229                ObjectName::from(vec![
230                    Ident::new("my_catalog"),
231                    Ident::new("my_schema"),
232                    Ident::new("foo")
233                ]),
234                vec![(
235                    sqlparser::ast::Value::Number("1".to_string(), false),
236                    sqlparser::ast::Value::Number("2".to_string(), false)
237                )]
238            ))
239        );
240
241        let sql = "TRUNCATE drop FILE RANGE (1,2)";
242        let mut stmts =
243            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
244                .unwrap();
245        assert_eq!(
246            stmts.pop().unwrap(),
247            Statement::TruncateTable(TruncateTable::new_with_ranges(
248                ObjectName::from(vec![Ident::new("drop")]),
249                vec![(
250                    sqlparser::ast::Value::Number("1".to_string(), false),
251                    sqlparser::ast::Value::Number("2".to_string(), false)
252                )]
253            ))
254        );
255
256        let sql = "TRUNCATE `drop`";
257        let mut stmts =
258            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
259                .unwrap();
260        assert_eq!(
261            stmts.pop().unwrap(),
262            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
263                Ident::with_quote('`', "drop"),
264            ])))
265        );
266
267        let sql = "TRUNCATE \"drop\" FILE RANGE (\"1\", \"2\")";
268        let mut stmts =
269            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
270                .unwrap();
271        assert_eq!(
272            stmts.pop().unwrap(),
273            Statement::TruncateTable(TruncateTable::new_with_ranges(
274                ObjectName::from(vec![Ident::with_quote('"', "drop")]),
275                vec![(
276                    sqlparser::ast::Value::DoubleQuotedString("1".to_string()),
277                    sqlparser::ast::Value::DoubleQuotedString("2".to_string())
278                )]
279            ))
280        );
281    }
282
283    #[test]
284    pub fn test_parse_truncate() {
285        let sql = "TRUNCATE foo";
286        let mut stmts =
287            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
288                .unwrap();
289        assert_eq!(
290            stmts.pop().unwrap(),
291            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
292                "foo"
293            )])))
294        );
295
296        let sql = "TRUNCATE TABLE foo";
297        let mut stmts =
298            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
299                .unwrap();
300        assert_eq!(
301            stmts.pop().unwrap(),
302            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
303                "foo"
304            )])))
305        );
306
307        let sql = "TRUNCATE TABLE my_schema.foo";
308        let mut stmts =
309            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
310                .unwrap();
311        assert_eq!(
312            stmts.pop().unwrap(),
313            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
314                Ident::new("my_schema"),
315                Ident::new("foo")
316            ])))
317        );
318
319        let sql = "TRUNCATE my_schema.foo";
320        let mut stmts =
321            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
322                .unwrap();
323        assert_eq!(
324            stmts.pop().unwrap(),
325            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
326                Ident::new("my_schema"),
327                Ident::new("foo")
328            ])))
329        );
330
331        let sql = "TRUNCATE TABLE my_catalog.my_schema.foo";
332        let mut stmts =
333            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
334                .unwrap();
335        assert_eq!(
336            stmts.pop().unwrap(),
337            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
338                Ident::new("my_catalog"),
339                Ident::new("my_schema"),
340                Ident::new("foo")
341            ])))
342        );
343
344        let sql = "TRUNCATE drop";
345        let mut stmts =
346            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
347                .unwrap();
348        assert_eq!(
349            stmts.pop().unwrap(),
350            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![Ident::new(
351                "drop"
352            )])))
353        );
354
355        let sql = "TRUNCATE `drop`";
356        let mut stmts =
357            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
358                .unwrap();
359        assert_eq!(
360            stmts.pop().unwrap(),
361            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
362                Ident::with_quote('`', "drop"),
363            ])))
364        );
365
366        let sql = "TRUNCATE \"drop\"";
367        let mut stmts =
368            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
369                .unwrap();
370        assert_eq!(
371            stmts.pop().unwrap(),
372            Statement::TruncateTable(TruncateTable::new(ObjectName::from(vec![
373                Ident::with_quote('"', "drop"),
374            ])))
375        );
376    }
377
378    #[test]
379    pub fn test_parse_invalid_truncate() {
380        let sql = "TRUNCATE SCHEMA foo";
381        let result =
382            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
383        assert!(result.is_err(), "result is: {result:?}");
384
385        let sql = "TRUNCATE";
386        let result =
387            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
388        assert!(result.is_err(), "result is: {result:?}");
389
390        let sql = "TRUNCATE TABLE foo RANGE";
391        let result =
392            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
393        assert!(
394            result.is_err()
395                && format!("{result:?}").contains("SQL statement is not supported, keyword: RANGE"),
396            "result is: {result:?}"
397        );
398
399        let sql = "TRUNCATE TABLE foo FILE";
400        let result =
401            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
402        assert!(
403            result.is_err()
404                && format!("{result:?}").contains("SQL statement is not supported, keyword: FILE"),
405            "result is: {result:?}"
406        );
407
408        let sql = "TRUNCATE TABLE foo FILE RANGE";
409        let result =
410            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
411        assert!(
412            result.is_err() && format!("{result:?}").contains("expected: 'a left parenthesis'"),
413            "result is: {result:?}"
414        );
415
416        let sql = "TRUNCATE TABLE foo FILE RANGE (";
417        let result =
418            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
419        assert!(
420            result.is_err() && format!("{result:?}").contains("expected: 'a timestamp value'"),
421            "result is: {result:?}"
422        );
423
424        let sql = "TRUNCATE TABLE foo FILE RANGE ()";
425        let result =
426            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
427        assert!(
428            result.is_err() && format!("{result:?}").contains("expected: 'a timestamp value'"),
429            "result is: {result:?}"
430        );
431
432        let sql = "TRUNCATE TABLE foo FILE RANGE (1 2) (3 4)";
433        let result =
434            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
435        assert!(
436            result.is_err() && format!("{result:?}").contains("expected: 'a comma'"),
437            "result is: {result:?}"
438        );
439
440        let sql = "TRUNCATE TABLE foo FILE RANGE (,),(3,4)";
441        let result =
442            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
443        assert!(
444            result.is_err() && format!("{result:?}").contains("Expected: a value, found: ,"),
445            "result is: {result:?}"
446        );
447
448        let sql = "TRUNCATE TABLE foo FILE RANGE (1,2) (3,4)";
449        let result =
450            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
451        assert!(
452            result.is_err()
453                && format!("{result:?}")
454                    .contains("expected: 'a comma or end of statement', found: ("),
455            "result is: {result:?}"
456        );
457
458        let sql = "TRUNCATE TABLE foo FILE RANGE (1,2),,,,,,,,,(3,4)";
459        let result =
460            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
461        assert!(
462            result.is_err()
463                && format!("{result:?}").contains("expected: 'a left parenthesis', found: ,"),
464            "result is: {result:?}"
465        );
466    }
467}