1use 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
25impl 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 !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 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 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(); let next_peek = self.parser.peek_token().token; if matches!(next_peek, Token::EOF | Token::SemiColon)
121 || matches!(next_peek, Token::Word(word) if word.keyword == Keyword::WITH)
122 {
123 break; }
125 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}