Skip to main content

sql/
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 std::str::FromStr;
16
17use chrono::{DateTime, Utc};
18use snafu::{OptionExt, ResultExt};
19use sqlparser::ast::{Ident, Query, Value};
20use sqlparser::dialect::Dialect;
21use sqlparser::keywords::Keyword;
22use sqlparser::parser::{Parser, ParserError, ParserOptions};
23use sqlparser::tokenizer::{Token, TokenWithSpan};
24
25use crate::ast::{Expr, ObjectName};
26use crate::error::{self, InvalidSqlSnafu, Result, SyntaxSnafu};
27use crate::parsers::tql_parser;
28use crate::statements::kill::Kill;
29use crate::statements::statement::Statement;
30use crate::statements::transform_statements;
31
32pub const FLOW: &str = "FLOW";
33
34/// SQL Parser options.
35#[derive(Clone, Debug, Default)]
36pub struct ParseOptions {
37    /// If set, TQL parameter expressions containing `now()` will be evaluated
38    /// against this scheduled time instead of wall-clock time.
39    pub scheduled_time: Option<DateTime<Utc>>,
40}
41
42/// GrepTime SQL parser context, a simple wrapper for Datafusion SQL parser.
43pub struct ParserContext<'a> {
44    pub(crate) parser: Parser<'a>,
45    pub(crate) sql: &'a str,
46    /// Optional scheduled time for `now()` evaluation in TQL parameters.
47    pub(crate) scheduled_time: Option<DateTime<Utc>>,
48}
49
50impl ParserContext<'_> {
51    /// Construct a new ParserContext.
52    pub fn new<'a>(dialect: &'a dyn Dialect, sql: &'a str) -> Result<ParserContext<'a>> {
53        let parser = Parser::new(dialect)
54            .with_options(ParserOptions::new().with_trailing_commas(true))
55            .try_with_sql(sql)
56            .context(SyntaxSnafu)?;
57
58        Ok(ParserContext {
59            parser,
60            sql,
61            scheduled_time: None,
62        })
63    }
64
65    /// Parses parser context to Query.
66    pub fn parser_query(&mut self) -> Result<Box<Query>> {
67        self.parser.parse_query().context(SyntaxSnafu)
68    }
69
70    /// Parses SQL with given dialect
71    pub fn create_with_dialect(
72        sql: &str,
73        dialect: &dyn Dialect,
74        opts: ParseOptions,
75    ) -> Result<Vec<Statement>> {
76        let mut stmts: Vec<Statement> = Vec::new();
77
78        let mut parser_ctx = ParserContext::new(dialect, sql)?;
79        parser_ctx.scheduled_time = opts.scheduled_time;
80
81        let mut expecting_statement_delimiter = false;
82        loop {
83            // ignore empty statements (between successive statement delimiters)
84            while parser_ctx.parser.consume_token(&Token::SemiColon) {
85                expecting_statement_delimiter = false;
86            }
87
88            if parser_ctx.parser.peek_token() == Token::EOF {
89                break;
90            }
91            if expecting_statement_delimiter {
92                return parser_ctx.unsupported(parser_ctx.peek_token_as_string());
93            }
94
95            let statement = parser_ctx.parse_statement()?;
96            stmts.push(statement);
97            expecting_statement_delimiter = true;
98        }
99
100        transform_statements(&mut stmts)?;
101
102        Ok(stmts)
103    }
104
105    pub fn parse_table_name(sql: &str, dialect: &dyn Dialect) -> Result<ObjectName> {
106        let parser = Parser::new(dialect)
107            .with_options(ParserOptions::new().with_trailing_commas(true))
108            .try_with_sql(sql)
109            .context(SyntaxSnafu)?;
110        ParserContext {
111            parser,
112            sql,
113            scheduled_time: None,
114        }
115        .intern_parse_table_name()
116    }
117
118    pub(crate) fn intern_parse_table_name(&mut self) -> Result<ObjectName> {
119        let raw_table_name =
120            self.parser
121                .parse_object_name(false)
122                .context(error::UnexpectedSnafu {
123                    expected: "a table name",
124                    actual: self.parser.peek_token().to_string(),
125                })?;
126        Self::canonicalize_object_name(raw_table_name)
127    }
128
129    pub fn parse_function(sql: &str, dialect: &dyn Dialect) -> Result<Expr> {
130        let mut parser = Parser::new(dialect)
131            .with_options(ParserOptions::new().with_trailing_commas(true))
132            .try_with_sql(sql)
133            .context(SyntaxSnafu)?;
134
135        let function_name = parser.parse_identifier().context(SyntaxSnafu)?;
136        parser
137            .parse_function(vec![function_name].into())
138            .context(SyntaxSnafu)
139    }
140
141    /// Parses parser context to a set of statements.
142    pub fn parse_statement(&mut self) -> Result<Statement> {
143        match self.parser.peek_token().token {
144            Token::Word(w) => match w.keyword {
145                Keyword::CREATE => {
146                    let _ = self.parser.next_token();
147                    self.parse_create()
148                }
149
150                Keyword::EXPLAIN => {
151                    let _ = self.parser.next_token();
152                    self.parse_explain()
153                }
154
155                Keyword::SHOW => {
156                    let _ = self.parser.next_token();
157                    self.parse_show()
158                }
159
160                Keyword::DELETE => self.parse_delete(),
161
162                Keyword::DESCRIBE | Keyword::DESC => {
163                    let _ = self.parser.next_token();
164                    self.parse_describe()
165                }
166
167                Keyword::INSERT => self.parse_insert(),
168
169                Keyword::REPLACE => self.parse_replace(),
170
171                Keyword::SELECT | Keyword::VALUES => self.parse_query(),
172
173                Keyword::WITH => self.parse_with_tql(),
174
175                Keyword::ALTER => self.parse_alter(),
176
177                Keyword::DROP => self.parse_drop(),
178
179                Keyword::COPY => self.parse_copy(),
180
181                Keyword::TRUNCATE => self.parse_truncate(),
182
183                Keyword::COMMENT => self.parse_comment(),
184
185                Keyword::SET => self.parse_set_variables(),
186
187                Keyword::ADMIN => self.parse_admin_command(),
188
189                Keyword::NoKeyword
190                    if w.quote_style.is_none() && w.value.to_uppercase() == tql_parser::TQL =>
191                {
192                    self.parse_tql(false)
193                }
194
195                #[cfg(feature = "enterprise")]
196                Keyword::NoKeyword
197                    if w.quote_style.is_none() && w.value.eq_ignore_ascii_case("UNDROP") =>
198                {
199                    self.parse_undrop_table()
200                }
201
202                Keyword::DECLARE => self.parse_declare_cursor(),
203
204                Keyword::FETCH => self.parse_fetch_cursor(),
205
206                Keyword::CLOSE => self.parse_close_cursor(),
207
208                Keyword::USE => {
209                    let _ = self.parser.next_token();
210
211                    let database_name = self.parser.parse_identifier().with_context(|_| {
212                        error::UnexpectedSnafu {
213                            expected: "a database name",
214                            actual: self.peek_token_as_string(),
215                        }
216                    })?;
217                    Ok(Statement::Use(
218                        Self::canonicalize_identifier(database_name).value,
219                    ))
220                }
221
222                Keyword::KILL => {
223                    let _ = self.parser.next_token();
224                    let kill = if self.parser.parse_keyword(Keyword::QUERY) {
225                        // MySQL KILL QUERY <connection id> statements
226                        let connection_id_exp =
227                            self.parser.parse_number_value().with_context(|_| {
228                                error::UnexpectedSnafu {
229                                    expected: "MySQL numeric connection id",
230                                    actual: self.peek_token_as_string(),
231                                }
232                            })?;
233                        let Value::Number(s, _) = connection_id_exp.value else {
234                            return error::UnexpectedTokenSnafu {
235                                expected: "MySQL numeric connection id",
236                                actual: connection_id_exp.to_string(),
237                            }
238                            .fail();
239                        };
240
241                        let connection_id = u32::from_str(&s).map_err(|_| {
242                            error::UnexpectedTokenSnafu {
243                                expected: "MySQL numeric connection id",
244                                actual: s,
245                            }
246                            .build()
247                        })?;
248                        Kill::ConnectionId(connection_id)
249                    } else {
250                        let process_id_ident =
251                            self.parser.parse_literal_string().with_context(|_| {
252                                error::UnexpectedSnafu {
253                                    expected: "process id string literal",
254                                    actual: self.peek_token_as_string(),
255                                }
256                            })?;
257                        Kill::ProcessId(process_id_ident)
258                    };
259
260                    Ok(Statement::Kill(kill))
261                }
262
263                _ => self.unsupported(self.peek_token_as_string()),
264            },
265            Token::LParen => self.parse_query(),
266            unexpected => self.unsupported(unexpected.to_string()),
267        }
268    }
269
270    /// Parses MySQL style 'PREPARE stmt_name FROM stmt' into a (stmt_name, stmt) tuple.
271    pub fn parse_mysql_prepare_stmt(sql: &str, dialect: &dyn Dialect) -> Result<(String, String)> {
272        ParserContext::new(dialect, sql)?.parse_mysql_prepare()
273    }
274
275    /// Parses MySQL style 'EXECUTE stmt_name USING param_list' into a stmt_name string and a list of parameters.
276    pub fn parse_mysql_execute_stmt(
277        sql: &str,
278        dialect: &dyn Dialect,
279    ) -> Result<(String, Vec<Expr>)> {
280        ParserContext::new(dialect, sql)?.parse_mysql_execute()
281    }
282
283    /// Parses MySQL style 'DEALLOCATE stmt_name' into a stmt_name string.
284    pub fn parse_mysql_deallocate_stmt(sql: &str, dialect: &dyn Dialect) -> Result<String> {
285        ParserContext::new(dialect, sql)?.parse_deallocate()
286    }
287
288    /// Raises an "unsupported statement" error.
289    pub fn unsupported<T>(&self, keyword: String) -> Result<T> {
290        error::UnsupportedSnafu { keyword }.fail()
291    }
292
293    // Report unexpected token
294    pub(crate) fn expected<T>(&self, expected: &str, found: TokenWithSpan) -> Result<T> {
295        Err(ParserError::ParserError(format!(
296            "Expected {expected}, found: {found}",
297        )))
298        .context(SyntaxSnafu)
299    }
300
301    pub fn matches_keyword(&mut self, expected: Keyword) -> bool {
302        match self.parser.peek_token().token {
303            Token::Word(w) => w.keyword == expected,
304            _ => false,
305        }
306    }
307
308    pub fn consume_token(&mut self, expected: &str) -> bool {
309        if self.peek_token_as_string().to_uppercase() == *expected.to_uppercase() {
310            let _ = self.parser.next_token();
311            true
312        } else {
313            false
314        }
315    }
316
317    #[inline]
318    pub(crate) fn peek_token_as_string(&self) -> String {
319        self.parser.peek_token().to_string()
320    }
321
322    /// Canonicalize the identifier to lowercase if it's not quoted.
323    pub fn canonicalize_identifier(ident: Ident) -> Ident {
324        if ident.quote_style.is_some() {
325            ident
326        } else {
327            Ident::new(ident.value.to_lowercase())
328        }
329    }
330
331    /// Like [canonicalize_identifier] but for [ObjectName].
332    pub(crate) fn canonicalize_object_name(object_name: ObjectName) -> Result<ObjectName> {
333        object_name
334            .0
335            .into_iter()
336            .map(|x| {
337                x.as_ident()
338                    .cloned()
339                    .map(Self::canonicalize_identifier)
340                    .with_context(|| InvalidSqlSnafu {
341                        msg: format!("not an ident: '{x}'"),
342                    })
343            })
344            .collect::<Result<Vec<_>>>()
345            .map(Into::into)
346    }
347
348    /// Simply a shortcut for sqlparser's same name method `parse_object_name`,
349    /// but with constant argument "false".
350    /// Because the argument is always "false" for us (it's introduced by BigQuery),
351    /// we don't want to write it again and again.
352    pub(crate) fn parse_object_name(&mut self) -> std::result::Result<ObjectName, ParserError> {
353        self.parser.parse_object_name(false)
354    }
355}
356
357#[cfg(test)]
358mod tests {
359
360    use datatypes::prelude::ConcreteDataType;
361    use sqlparser::dialect::MySqlDialect;
362
363    use super::*;
364    use crate::dialect::GreptimeDbDialect;
365    use crate::statements::create::CreateTable;
366    use crate::statements::sql_data_type_to_concrete_data_type;
367
368    fn test_timestamp_precision(sql: &str, expected_type: ConcreteDataType) {
369        match ParserContext::create_with_dialect(
370            sql,
371            &GreptimeDbDialect {},
372            ParseOptions::default(),
373        )
374        .unwrap()
375        .pop()
376        .unwrap()
377        {
378            Statement::CreateTable(CreateTable { columns, .. }) => {
379                let ts_col = columns.first().unwrap();
380                assert_eq!(
381                    expected_type,
382                    sql_data_type_to_concrete_data_type(ts_col.data_type()).unwrap()
383                );
384            }
385            _ => unreachable!(),
386        }
387    }
388
389    #[test]
390    pub fn test_create_table_with_precision() {
391        test_timestamp_precision(
392            "create table demo (ts timestamp time index, cnt int);",
393            ConcreteDataType::timestamp_millisecond_datatype(),
394        );
395        test_timestamp_precision(
396            "create table demo (ts timestamp(0) time index, cnt int);",
397            ConcreteDataType::timestamp_second_datatype(),
398        );
399        test_timestamp_precision(
400            "create table demo (ts timestamp(3) time index, cnt int);",
401            ConcreteDataType::timestamp_millisecond_datatype(),
402        );
403        test_timestamp_precision(
404            "create table demo (ts timestamp(6) time index, cnt int);",
405            ConcreteDataType::timestamp_microsecond_datatype(),
406        );
407        test_timestamp_precision(
408            "create table demo (ts timestamp(9) time index, cnt int);",
409            ConcreteDataType::timestamp_nanosecond_datatype(),
410        );
411    }
412
413    #[test]
414    #[should_panic]
415    pub fn test_create_table_with_invalid_precision() {
416        test_timestamp_precision(
417            "create table demo (ts timestamp(1) time index, cnt int);",
418            ConcreteDataType::timestamp_millisecond_datatype(),
419        );
420    }
421
422    #[test]
423    pub fn test_parse_table_name() {
424        let table_name = "a.b.c";
425
426        let object_name =
427            ParserContext::parse_table_name(table_name, &GreptimeDbDialect {}).unwrap();
428
429        assert_eq!(object_name.0.len(), 3);
430        assert_eq!(object_name.to_string(), table_name);
431
432        let table_name = "a.b";
433
434        let object_name =
435            ParserContext::parse_table_name(table_name, &GreptimeDbDialect {}).unwrap();
436
437        assert_eq!(object_name.0.len(), 2);
438        assert_eq!(object_name.to_string(), table_name);
439
440        let table_name = "Test.\"public-test\"";
441
442        let object_name =
443            ParserContext::parse_table_name(table_name, &GreptimeDbDialect {}).unwrap();
444
445        assert_eq!(object_name.0.len(), 2);
446        assert_eq!(object_name.to_string(), table_name.to_ascii_lowercase());
447
448        let table_name = "HelloWorld";
449
450        let object_name =
451            ParserContext::parse_table_name(table_name, &GreptimeDbDialect {}).unwrap();
452
453        assert_eq!(object_name.0.len(), 1);
454        assert_eq!(object_name.to_string(), table_name.to_ascii_lowercase());
455    }
456
457    #[test]
458    pub fn test_parse_mysql_prepare_stmt() {
459        let sql = "PREPARE stmt1 FROM 'SELECT * FROM t1 WHERE id = ?';";
460        let (stmt_name, stmt) =
461            ParserContext::parse_mysql_prepare_stmt(sql, &MySqlDialect {}).unwrap();
462        assert_eq!(stmt_name, "stmt1");
463        assert_eq!(stmt, "SELECT * FROM t1 WHERE id = ?");
464
465        let sql = "PREPARE stmt2 FROM \"SELECT * FROM t1 WHERE id = ?\"";
466        let (stmt_name, stmt) =
467            ParserContext::parse_mysql_prepare_stmt(sql, &MySqlDialect {}).unwrap();
468        assert_eq!(stmt_name, "stmt2");
469        assert_eq!(stmt, "SELECT * FROM t1 WHERE id = ?");
470    }
471
472    #[test]
473    pub fn test_parse_mysql_execute_stmt() {
474        let sql = "EXECUTE stmt1 USING 1, 'hello';";
475        let (stmt_name, params) =
476            ParserContext::parse_mysql_execute_stmt(sql, &GreptimeDbDialect {}).unwrap();
477        assert_eq!(stmt_name, "stmt1");
478        assert_eq!(params.len(), 2);
479        assert_eq!(params[0].to_string(), "1");
480        assert_eq!(params[1].to_string(), "'hello'");
481
482        let sql = "EXECUTE stmt2;";
483        let (stmt_name, params) =
484            ParserContext::parse_mysql_execute_stmt(sql, &GreptimeDbDialect {}).unwrap();
485        assert_eq!(stmt_name, "stmt2");
486        assert_eq!(params.len(), 0);
487
488        let sql = "EXECUTE stmt3 USING 231, 'hello', \"2003-03-1\", NULL, ;";
489        let (stmt_name, params) =
490            ParserContext::parse_mysql_execute_stmt(sql, &GreptimeDbDialect {}).unwrap();
491        assert_eq!(stmt_name, "stmt3");
492        assert_eq!(params.len(), 4);
493        assert_eq!(params[0].to_string(), "231");
494        assert_eq!(params[1].to_string(), "'hello'");
495        assert_eq!(params[2].to_string(), "\"2003-03-1\"");
496        assert_eq!(params[3].to_string(), "NULL");
497    }
498
499    #[test]
500    pub fn test_parse_mysql_deallocate_stmt() {
501        let sql = "DEALLOCATE stmt1;";
502        let stmt_name = ParserContext::parse_mysql_deallocate_stmt(sql, &MySqlDialect {}).unwrap();
503        assert_eq!(stmt_name, "stmt1");
504
505        let sql = "DEALLOCATE stmt2";
506        let stmt_name = ParserContext::parse_mysql_deallocate_stmt(sql, &MySqlDialect {}).unwrap();
507        assert_eq!(stmt_name, "stmt2");
508    }
509
510    #[test]
511    pub fn test_parse_kill_query_statement() {
512        use crate::statements::kill::Kill;
513
514        // Test MySQL-style KILL QUERY with connection ID
515        let sql = "KILL QUERY 123";
516        let statements =
517            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
518                .unwrap();
519
520        assert_eq!(statements.len(), 1);
521        match &statements[0] {
522            Statement::Kill(Kill::ConnectionId(connection_id)) => {
523                assert_eq!(*connection_id, 123);
524            }
525            _ => panic!("Expected Kill::ConnectionId statement"),
526        }
527
528        // Test with larger connection ID
529        let sql = "KILL QUERY 999999";
530        let statements =
531            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
532                .unwrap();
533
534        assert_eq!(statements.len(), 1);
535        match &statements[0] {
536            Statement::Kill(Kill::ConnectionId(connection_id)) => {
537                assert_eq!(*connection_id, 999999);
538            }
539            _ => panic!("Expected Kill::ConnectionId statement"),
540        }
541    }
542
543    #[test]
544    pub fn test_parse_kill_process_statement() {
545        use crate::statements::kill::Kill;
546
547        // Test KILL with process ID string
548        let sql = "KILL 'process-123'";
549        let statements =
550            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
551                .unwrap();
552
553        assert_eq!(statements.len(), 1);
554        match &statements[0] {
555            Statement::Kill(Kill::ProcessId(process_id)) => {
556                assert_eq!(process_id, "process-123");
557            }
558            _ => panic!("Expected Kill::ProcessId statement"),
559        }
560
561        // Test with double quotes
562        let sql = "KILL \"process-456\"";
563        let statements =
564            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
565                .unwrap();
566
567        assert_eq!(statements.len(), 1);
568        match &statements[0] {
569            Statement::Kill(Kill::ProcessId(process_id)) => {
570                assert_eq!(process_id, "process-456");
571            }
572            _ => panic!("Expected Kill::ProcessId statement"),
573        }
574
575        // Test with UUID-like process ID
576        let sql = "KILL 'f47ac10b-58cc-4372-a567-0e02b2c3d479'";
577        let statements =
578            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
579                .unwrap();
580
581        assert_eq!(statements.len(), 1);
582        match &statements[0] {
583            Statement::Kill(Kill::ProcessId(process_id)) => {
584                assert_eq!(process_id, "f47ac10b-58cc-4372-a567-0e02b2c3d479");
585            }
586            _ => panic!("Expected Kill::ProcessId statement"),
587        }
588    }
589
590    #[test]
591    pub fn test_parse_kill_statement_errors() {
592        // Test KILL QUERY without connection ID
593        let sql = "KILL QUERY";
594        let result =
595            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
596        assert!(result.is_err());
597
598        // Test KILL QUERY with non-numeric connection ID
599        let sql = "KILL QUERY 'not-a-number'";
600        let result =
601            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
602        assert!(result.is_err());
603
604        // Test KILL without any argument
605        let sql = "KILL";
606        let result =
607            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
608        assert!(result.is_err());
609
610        // Test KILL QUERY with connection ID that's too large for u32
611        let sql = "KILL QUERY 4294967296"; // u32::MAX + 1
612        let result =
613            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
614        assert!(result.is_err());
615    }
616
617    #[test]
618    pub fn test_parse_kill_statement_edge_cases() {
619        use crate::statements::kill::Kill;
620
621        // Test KILL QUERY with zero connection ID
622        let sql = "KILL QUERY 0";
623        let statements =
624            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
625                .unwrap();
626
627        assert_eq!(statements.len(), 1);
628        match &statements[0] {
629            Statement::Kill(Kill::ConnectionId(connection_id)) => {
630                assert_eq!(*connection_id, 0);
631            }
632            _ => panic!("Expected Kill::ConnectionId statement"),
633        }
634
635        // Test KILL QUERY with maximum u32 value
636        let sql = "KILL QUERY 4294967295"; // u32::MAX
637        let statements =
638            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
639                .unwrap();
640
641        assert_eq!(statements.len(), 1);
642        match &statements[0] {
643            Statement::Kill(Kill::ConnectionId(connection_id)) => {
644                assert_eq!(*connection_id, 4294967295);
645            }
646            _ => panic!("Expected Kill::ConnectionId statement"),
647        }
648
649        // Test KILL with empty string process ID
650        let sql = "KILL ''";
651        let statements =
652            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
653                .unwrap();
654
655        assert_eq!(statements.len(), 1);
656        match &statements[0] {
657            Statement::Kill(Kill::ProcessId(process_id)) => {
658                assert_eq!(process_id, "");
659            }
660            _ => panic!("Expected Kill::ProcessId statement"),
661        }
662    }
663
664    #[test]
665    pub fn test_parse_kill_statement_case_insensitive() {
666        use crate::statements::kill::Kill;
667
668        // Test lowercase
669        let sql = "kill query 123";
670        let statements =
671            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
672                .unwrap();
673
674        assert_eq!(statements.len(), 1);
675        match &statements[0] {
676            Statement::Kill(Kill::ConnectionId(connection_id)) => {
677                assert_eq!(*connection_id, 123);
678            }
679            _ => panic!("Expected Kill::ConnectionId statement"),
680        }
681
682        // Test mixed case
683        let sql = "Kill Query 456";
684        let statements =
685            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
686                .unwrap();
687
688        assert_eq!(statements.len(), 1);
689        match &statements[0] {
690            Statement::Kill(Kill::ConnectionId(connection_id)) => {
691                assert_eq!(*connection_id, 456);
692            }
693            _ => panic!("Expected Kill::ConnectionId statement"),
694        }
695    }
696}