Skip to main content

radixdb_sql/statements/
control.rs

1// Copyright 2026 RadixDB Contributors
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 super::*;
16
17impl Parser {
18    /// Parse a BEGIN statement
19    pub(super) fn parse_begin_statement(&mut self) -> Option<BeginStatement> {
20        let token = self.cur_token.clone();
21
22        // Check for optional TRANSACTION keyword
23        if self.peek_token_is_keyword("TRANSACTION") {
24            self.next_token();
25        }
26
27        // Check for ISOLATION LEVEL
28        let isolation_level = if self.peek_token_is_keyword("ISOLATION") {
29            self.next_token();
30            if !self.expect_keyword("LEVEL") {
31                return None;
32            }
33            self.next_token();
34
35            let level = self.cur_token.literal.to_uppercase();
36            let isolation = match level.as_str() {
37                "SNAPSHOT" | "SERIALIZABLE" => level,
38                "REPEATABLE" => {
39                    if !self.expect_keyword("READ") {
40                        return None;
41                    }
42                    SmartString::const_new("REPEATABLE READ")
43                }
44                "READ" => {
45                    if self.peek_token_is_keyword("UNCOMMITTED") {
46                        self.next_token();
47                        SmartString::const_new("READ UNCOMMITTED")
48                    } else if self.peek_token_is_keyword("COMMITTED") {
49                        self.next_token();
50                        SmartString::const_new("READ COMMITTED")
51                    } else {
52                        self.add_error(format!(
53                            "expected UNCOMMITTED or COMMITTED after READ at {}",
54                            self.cur_token.position
55                        ));
56                        return None;
57                    }
58                }
59                _ => {
60                    self.add_error(format!(
61                        "invalid isolation level: {} at {}",
62                        level, self.cur_token.position
63                    ));
64                    return None;
65                }
66            };
67            Some(isolation)
68        } else {
69            None
70        };
71
72        Some(BeginStatement {
73            token,
74            isolation_level,
75        })
76    }
77
78    /// Parse a COMMIT statement
79    pub(super) fn parse_commit_statement(&mut self) -> Option<CommitStatement> {
80        let token = self.cur_token.clone();
81
82        // Check for optional TRANSACTION keyword
83        if self.peek_token_is_keyword("TRANSACTION") {
84            self.next_token();
85        }
86
87        Some(CommitStatement { token })
88    }
89
90    /// Parse a ROLLBACK statement
91    pub(super) fn parse_rollback_statement(&mut self) -> Option<RollbackStatement> {
92        let token = self.cur_token.clone();
93
94        // Check for optional TRANSACTION keyword
95        if self.peek_token_is_keyword("TRANSACTION") {
96            self.next_token();
97        }
98
99        // Check for TO SAVEPOINT clause
100        let savepoint_name = if self.peek_token_is_keyword("TO") {
101            self.next_token();
102            if self.peek_token_is_keyword("SAVEPOINT") {
103                self.next_token();
104            }
105            if !self.expect_peek(TokenType::Identifier) {
106                return None;
107            }
108            Some(Identifier::new(
109                self.cur_token.clone(),
110                self.cur_token.literal.clone(),
111            ))
112        } else {
113            None
114        };
115
116        Some(RollbackStatement {
117            token,
118            savepoint_name,
119        })
120    }
121
122    /// Parse a SAVEPOINT statement
123    pub(super) fn parse_savepoint_statement(&mut self) -> Option<SavepointStatement> {
124        let token = self.cur_token.clone();
125
126        if !self.expect_peek(TokenType::Identifier) {
127            return None;
128        }
129
130        let savepoint_name =
131            Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
132
133        Some(SavepointStatement {
134            token,
135            savepoint_name,
136        })
137    }
138
139    /// Parse a RELEASE SAVEPOINT statement
140    pub(super) fn parse_release_savepoint_statement(
141        &mut self,
142    ) -> Option<ReleaseSavepointStatement> {
143        let token = self.cur_token.clone();
144
145        // Optional SAVEPOINT keyword
146        if self.peek_token_is_keyword("SAVEPOINT") {
147            self.next_token();
148        }
149
150        if !self.expect_peek(TokenType::Identifier) {
151            return None;
152        }
153
154        let savepoint_name =
155            Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
156
157        Some(ReleaseSavepointStatement {
158            token,
159            savepoint_name,
160        })
161    }
162
163    /// Parse a SET statement
164    pub(super) fn parse_set_statement(&mut self) -> Option<SetStatement> {
165        let token = self.cur_token.clone();
166
167        self.next_token();
168        if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
169            self.add_error(format!(
170                "expected variable name at {}",
171                self.cur_token.position
172            ));
173            return None;
174        }
175
176        let name_token = self.cur_token.clone();
177        let time_zone = self.cur_token.literal.eq_ignore_ascii_case("TIME")
178            && self.peek_token.literal.eq_ignore_ascii_case("ZONE");
179
180        let name = if time_zone {
181            self.next_token();
182            Identifier::new(name_token, "TIME ZONE")
183        } else {
184            Identifier::new(name_token, self.cur_token.literal.clone())
185        };
186
187        self.next_token();
188        // PostgreSQL spells this `SET TIME ZONE '<zone>'`; the ordinary SET
189        // grammar continues to require '=' or 'TO'. Accepting an optional
190        // delimiter for TIME ZONE keeps both forms deterministic.
191        let is_equals = self.cur_token_is(TokenType::Operator) && self.cur_token.literal == "=";
192        if is_equals || self.cur_token_is_keyword("TO") {
193            self.next_token();
194        } else if !time_zone {
195            self.add_error(format!(
196                "expected '=' or 'TO' after variable name at {}",
197                self.cur_token.position
198            ));
199            return None;
200        }
201
202        let value = self.parse_expression(Precedence::Lowest)?;
203
204        Some(SetStatement { token, name, value })
205    }
206
207    /// Parse a PRAGMA statement
208    pub(super) fn parse_pragma_statement(&mut self) -> Option<PragmaStatement> {
209        let token = self.cur_token.clone();
210
211        self.next_token();
212        // Accept both identifiers and keywords as pragma names (e.g., PRAGMA vacuum)
213        if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
214            self.add_error(format!(
215                "expected pragma name at {}",
216                self.cur_token.position
217            ));
218            return None;
219        }
220
221        let name = Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
222
223        self.next_token();
224
225        // Check for optional value
226        let value = if self.cur_token_is(TokenType::Operator) && self.cur_token.literal == "=" {
227            self.next_token();
228            Some(self.parse_expression(Precedence::Lowest)?)
229        } else {
230            None
231        };
232
233        Some(PragmaStatement { token, name, value })
234    }
235
236    /// Parse a SHOW statement
237    pub(super) fn parse_show_statement(&mut self) -> Option<Statement> {
238        let token = self.cur_token.clone();
239
240        if self.peek_token.literal.eq_ignore_ascii_case("TIMEZONE")
241            || self.peek_token.literal.eq_ignore_ascii_case("TIME_ZONE")
242        {
243            self.next_token();
244            let name = Identifier::new(self.cur_token.clone(), "TIME ZONE");
245            Some(Statement::ShowVariable(ShowVariableStatement {
246                token,
247                name,
248            }))
249        } else if self.peek_token_is_keyword("TIME") {
250            self.next_token();
251            if !self.peek_token.literal.eq_ignore_ascii_case("ZONE") {
252                self.add_error(format!(
253                    "expected ZONE after SHOW TIME at {}",
254                    self.cur_token.position
255                ));
256                return None;
257            }
258            self.next_token();
259            let name = Identifier::new(self.cur_token.clone(), "TIME ZONE");
260            Some(Statement::ShowVariable(ShowVariableStatement {
261                token,
262                name,
263            }))
264        } else if self.peek_token_is_keyword("TABLES") {
265            self.next_token();
266            Some(Statement::ShowTables(ShowTablesStatement { token }))
267        } else if self.peek_token_is_keyword("VIEWS") {
268            self.next_token();
269            Some(Statement::ShowViews(ShowViewsStatement { token }))
270        } else if self.peek_token_is_keyword("CREATE") {
271            self.next_token();
272            // Check for TABLE or VIEW
273            if self.peek_token_is_keyword("TABLE") {
274                self.next_token();
275                if !self.expect_peek(TokenType::Identifier) {
276                    return None;
277                }
278                let table_name =
279                    Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
280                Some(Statement::ShowCreateTable(ShowCreateTableStatement {
281                    token,
282                    table_name,
283                }))
284            } else if self.peek_token_is_keyword("VIEW") {
285                self.next_token();
286                if !self.expect_peek(TokenType::Identifier) {
287                    return None;
288                }
289                let view_name =
290                    Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
291                Some(Statement::ShowCreateView(ShowCreateViewStatement {
292                    token,
293                    view_name,
294                }))
295            } else {
296                self.add_error(format!(
297                    "expected TABLE or VIEW after SHOW CREATE at {}",
298                    self.cur_token.position
299                ));
300                None
301            }
302        } else if self.peek_token_is_keyword("INDEXES") || self.peek_token_is_keyword("INDEX") {
303            self.next_token();
304            if !self.expect_keyword("FROM") {
305                return None;
306            }
307            if !self.expect_peek(TokenType::Identifier) {
308                return None;
309            }
310            let table_name =
311                Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
312            Some(Statement::ShowIndexes(ShowIndexesStatement {
313                token,
314                table_name,
315            }))
316        } else {
317            self.add_error(format!(
318                "unsupported SHOW statement at {}",
319                self.cur_token.position
320            ));
321            None
322        }
323    }
324
325    /// Parse a DESCRIBE statement
326    pub(super) fn parse_describe_statement(&mut self) -> Option<DescribeStatement> {
327        let token = self.cur_token.clone();
328
329        // Move past DESCRIBE/DESC keyword
330        self.next_token();
331
332        let target = if self.cur_token.literal.eq_ignore_ascii_case("DATABASE") {
333            DescribeTarget::Database
334        } else {
335            // Optional TABLE keyword (DESCRIBE TABLE t or just DESCRIBE t).
336            if self.cur_token_is_keyword("TABLE") {
337                self.next_token();
338            }
339            if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
340                self.add_error(format!(
341                    "expected TABLE name or DATABASE after DESCRIBE at {}",
342                    self.cur_token.position
343                ));
344                return None;
345            }
346            DescribeTarget::Table(Identifier::new(
347                self.cur_token.clone(),
348                self.cur_token.literal.clone(),
349            ))
350        };
351
352        let format = if self.peek_token_is_keyword("FORMAT") {
353            self.next_token();
354            self.next_token();
355            if !self.cur_token_is_keyword("JSON") {
356                self.add_error(format!(
357                    "expected JSON after DESCRIBE ... FORMAT at {}",
358                    self.cur_token.position
359                ));
360                return None;
361            }
362            DescribeFormat::Json
363        } else {
364            DescribeFormat::Tabular
365        };
366
367        if matches!(target, DescribeTarget::Database) && format != DescribeFormat::Json {
368            self.add_error(
369                "DESCRIBE DATABASE requires FORMAT JSON; the legacy tabular format is table-only"
370                    .to_string(),
371            );
372            return None;
373        }
374
375        Some(DescribeStatement {
376            token,
377            target,
378            format,
379        })
380    }
381
382    /// Parse an EXPLAIN statement
383    pub(super) fn parse_explain_statement(&mut self) -> Option<ExplainStatement> {
384        let token = self.cur_token.clone();
385
386        // Check for ANALYZE option
387        let analyze = if self.peek_token_is_keyword("ANALYZE") {
388            self.next_token();
389            true
390        } else {
391            false
392        };
393
394        // Move to the statement to explain
395        self.next_token();
396
397        // Parse the inner statement (SELECT, INSERT, UPDATE, DELETE)
398        let statement = self.parse_statement()?;
399
400        Some(ExplainStatement {
401            token,
402            statement: Box::new(statement),
403            analyze,
404        })
405    }
406
407    /// Parse an ANALYZE statement
408    /// Syntax: ANALYZE [table_name]
409    pub(super) fn parse_analyze_statement(&mut self) -> Option<AnalyzeStatement> {
410        let token = self.cur_token.clone();
411
412        // Move past ANALYZE keyword
413        self.next_token();
414
415        // Optional table name
416        let table_name = if self.cur_token_is(TokenType::Identifier)
417            || (self.cur_token_is(TokenType::Keyword)
418                && !self.cur_token.literal.eq_ignore_ascii_case("TABLE"))
419        {
420            let name = self.cur_token.literal.clone();
421            Some(name)
422        } else if self.cur_token_is(TokenType::Keyword)
423            && self.cur_token.literal.eq_ignore_ascii_case("TABLE")
424        {
425            // ANALYZE TABLE table_name syntax
426            self.next_token();
427            if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
428                let name = self.cur_token.literal.clone();
429                Some(name)
430            } else {
431                self.add_error(format!(
432                    "expected table name after ANALYZE TABLE at {}",
433                    self.cur_token.position
434                ));
435                return None;
436            }
437        } else {
438            None
439        };
440
441        Some(AnalyzeStatement { token, table_name })
442    }
443
444    /// Parse an expression statement
445    pub(super) fn parse_expression_statement(&mut self) -> Option<ExpressionStatement> {
446        let token = self.cur_token.clone();
447        let expression = self.parse_expression(Precedence::Lowest)?;
448
449        Some(ExpressionStatement { token, expression })
450    }
451
452    /// Parse an identifier list (allows keywords as identifiers for CTE column aliases)
453    pub fn parse_identifier_list(&mut self) -> Vec<Identifier> {
454        let mut list = Vec::new();
455
456        self.next_token();
457        // Accept both identifiers and keywords as column names
458        if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
459            list.push(self.cur_token_as_column_identifier());
460        }
461
462        while self.peek_token_is_punctuator(",") {
463            self.next_token(); // consume comma
464            self.next_token(); // move to identifier/keyword
465                               // Accept both identifiers and keywords as column names
466            if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
467                list.push(self.cur_token_as_column_identifier());
468            } else {
469                self.add_error(format!(
470                    "expected Identifier, got {:?} at {}",
471                    self.cur_token.token_type, self.cur_token.position
472                ));
473                return list;
474            }
475        }
476
477        list
478    }
479
480    /// Parse a COPY statement
481    /// COPY table [(columns)] FROM 'file_path' [WITH (FORMAT CSV|JSON [, HEADER true|false] [, DELIMITER 'c'] [, NULL 'str'])]
482    pub(super) fn parse_copy_statement(&mut self) -> Option<CopyStatement> {
483        let token = self.cur_token.clone();
484
485        // Parse table name
486        self.next_token();
487        if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
488            self.add_error(format!(
489                "expected table name after COPY, got {:?} at {}",
490                self.cur_token.token_type, self.cur_token.position
491            ));
492            return None;
493        }
494        let table_name = self.parse_relation_identifier_current()?;
495
496        // Parse optional column list
497        let mut columns = Vec::new();
498        if self.peek_token_is_punctuator("(") {
499            self.next_token(); // consume (
500            columns = self.parse_identifier_list();
501            if !self.expect_peek(TokenType::Punctuator) || self.cur_token.literal != ")" {
502                self.add_error(format!("expected ')' at {}", self.cur_token.position));
503                return None;
504            }
505        }
506
507        // Expect FROM keyword
508        if !self.expect_keyword("FROM") {
509            return None;
510        }
511
512        // Parse file path (string literal)
513        self.next_token();
514        if !self.cur_token_is(TokenType::String) {
515            self.add_error(format!(
516                "expected file path string after FROM, got {:?} at {}",
517                self.cur_token.token_type, self.cur_token.position
518            ));
519            return None;
520        }
521        let file_path = {
522            let lit = &self.cur_token.literal;
523            if lit.len() >= 2
524                && (lit.starts_with('\'') || lit.starts_with('"'))
525                && lit.ends_with(lit.chars().next().unwrap())
526            {
527                lit[1..lit.len() - 1].to_string()
528            } else {
529                lit.to_string()
530            }
531        };
532
533        // Default options
534        let mut format = None;
535        let mut header = true;
536        let mut delimiter = b',';
537        let mut null_string = None;
538        let mut header_specified = false;
539        let mut delimiter_specified = false;
540
541        // Parse optional WITH (options)
542        if self.peek_token_is_keyword("WITH") {
543            self.next_token(); // consume WITH
544
545            if !self.peek_token_is_punctuator("(") {
546                self.add_error(format!(
547                    "expected '(' after WITH at {}",
548                    self.peek_token.position
549                ));
550                return None;
551            }
552            self.next_token(); // consume (
553
554            // Parse key-value options
555            loop {
556                self.next_token();
557                if self.cur_token_is(TokenType::Punctuator) && self.cur_token.literal == ")" {
558                    break;
559                }
560
561                let key = self.cur_token.literal.to_uppercase();
562                match key.as_str() {
563                    "FORMAT" => {
564                        self.next_token();
565                        let fmt_str = self.cur_token.literal.to_uppercase();
566                        match fmt_str.as_str() {
567                            "CSV" => format = Some(CopyFormat::Csv),
568                            "JSON" => format = Some(CopyFormat::Json),
569                            _ => {
570                                self.add_error(format!(
571                                    "unsupported COPY format '{}', expected CSV or JSON",
572                                    fmt_str
573                                ));
574                                return None;
575                            }
576                        }
577                    }
578                    "HEADER" => {
579                        self.next_token();
580                        let val = self.cur_token.literal.to_uppercase();
581                        header = match val.as_str() {
582                            "TRUE" | "ON" | "1" => true,
583                            "FALSE" | "OFF" | "0" => false,
584                            _ => {
585                                self.add_error(format!(
586                                    "invalid HEADER value '{}', expected TRUE or FALSE",
587                                    self.cur_token.literal
588                                ));
589                                return None;
590                            }
591                        };
592                        header_specified = true;
593                    }
594                    "DELIMITER" => {
595                        self.next_token();
596                        let delim_str = &self.cur_token.literal;
597                        // Strip quotes if present
598                        let raw = if delim_str.len() >= 2
599                            && (delim_str.starts_with('\'') || delim_str.starts_with('"'))
600                        {
601                            &delim_str[1..delim_str.len() - 1]
602                        } else {
603                            delim_str.as_str()
604                        };
605                        if raw.len() != 1 {
606                            self.add_error(format!(
607                                "DELIMITER must be a single character, got '{}'",
608                                raw
609                            ));
610                            return None;
611                        }
612                        delimiter = raw.as_bytes()[0];
613                        delimiter_specified = true;
614                    }
615                    "NULL" => {
616                        self.next_token();
617                        let ns = &self.cur_token.literal;
618                        null_string = Some(
619                            if ns.len() >= 2
620                                && (ns.starts_with('\'') || ns.starts_with('"'))
621                                && ns.ends_with(ns.chars().next().unwrap())
622                            {
623                                ns[1..ns.len() - 1].to_string()
624                            } else {
625                                ns.to_string()
626                            },
627                        );
628                    }
629                    _ => {
630                        self.add_error(format!("unknown COPY option '{}'", key));
631                        return None;
632                    }
633                }
634
635                // Expect comma or closing paren
636                if self.peek_token_is_punctuator(",") {
637                    self.next_token(); // consume comma
638                } else if self.peek_token_is_punctuator(")") {
639                    self.next_token(); // consume )
640                    break;
641                } else if !self.peek_token_is(TokenType::Eof) {
642                    self.add_error(format!(
643                        "expected ',' or ')' after COPY option, got {:?} at {}",
644                        self.peek_token.token_type, self.peek_token.position
645                    ));
646                    return None;
647                }
648            }
649        }
650
651        // FORMAT is required
652        let format = match format {
653            Some(f) => f,
654            None => {
655                // Default to CSV if no WITH clause
656                CopyFormat::Csv
657            }
658        };
659
660        if format == CopyFormat::Json && (header_specified || delimiter_specified) {
661            self.add_error("COPY JSON does not support HEADER or DELIMITER options".to_string());
662            return None;
663        }
664
665        Some(CopyStatement {
666            token,
667            table_name,
668            columns,
669            file_path,
670            format,
671            header,
672            delimiter,
673            null_string,
674        })
675    }
676}