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) {
169            self.add_error(format!(
170                "expected variable name at {}",
171                self.cur_token.position
172            ));
173            return None;
174        }
175
176        let name = Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
177
178        self.next_token();
179        // Expect '=' or 'TO'
180        let is_equals = self.cur_token_is(TokenType::Operator) && self.cur_token.literal == "=";
181        if !is_equals && !self.cur_token_is_keyword("TO") {
182            self.add_error(format!(
183                "expected '=' or 'TO' after variable name at {}",
184                self.cur_token.position
185            ));
186            return None;
187        }
188
189        self.next_token();
190        let value = self.parse_expression(Precedence::Lowest)?;
191
192        Some(SetStatement { token, name, value })
193    }
194
195    /// Parse a PRAGMA statement
196    pub(super) fn parse_pragma_statement(&mut self) -> Option<PragmaStatement> {
197        let token = self.cur_token.clone();
198
199        self.next_token();
200        // Accept both identifiers and keywords as pragma names (e.g., PRAGMA vacuum)
201        if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
202            self.add_error(format!(
203                "expected pragma name at {}",
204                self.cur_token.position
205            ));
206            return None;
207        }
208
209        let name = Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
210
211        self.next_token();
212
213        // Check for optional value
214        let value = if self.cur_token_is(TokenType::Operator) && self.cur_token.literal == "=" {
215            self.next_token();
216            Some(self.parse_expression(Precedence::Lowest)?)
217        } else {
218            None
219        };
220
221        Some(PragmaStatement { token, name, value })
222    }
223
224    /// Parse a SHOW statement
225    pub(super) fn parse_show_statement(&mut self) -> Option<Statement> {
226        let token = self.cur_token.clone();
227
228        if self.peek_token_is_keyword("TABLES") {
229            self.next_token();
230            Some(Statement::ShowTables(ShowTablesStatement { token }))
231        } else if self.peek_token_is_keyword("VIEWS") {
232            self.next_token();
233            Some(Statement::ShowViews(ShowViewsStatement { token }))
234        } else if self.peek_token_is_keyword("CREATE") {
235            self.next_token();
236            // Check for TABLE or VIEW
237            if self.peek_token_is_keyword("TABLE") {
238                self.next_token();
239                if !self.expect_peek(TokenType::Identifier) {
240                    return None;
241                }
242                let table_name =
243                    Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
244                Some(Statement::ShowCreateTable(ShowCreateTableStatement {
245                    token,
246                    table_name,
247                }))
248            } else if self.peek_token_is_keyword("VIEW") {
249                self.next_token();
250                if !self.expect_peek(TokenType::Identifier) {
251                    return None;
252                }
253                let view_name =
254                    Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
255                Some(Statement::ShowCreateView(ShowCreateViewStatement {
256                    token,
257                    view_name,
258                }))
259            } else {
260                self.add_error(format!(
261                    "expected TABLE or VIEW after SHOW CREATE at {}",
262                    self.cur_token.position
263                ));
264                None
265            }
266        } else if self.peek_token_is_keyword("INDEXES") || self.peek_token_is_keyword("INDEX") {
267            self.next_token();
268            if !self.expect_keyword("FROM") {
269                return None;
270            }
271            if !self.expect_peek(TokenType::Identifier) {
272                return None;
273            }
274            let table_name =
275                Identifier::new(self.cur_token.clone(), self.cur_token.literal.clone());
276            Some(Statement::ShowIndexes(ShowIndexesStatement {
277                token,
278                table_name,
279            }))
280        } else {
281            self.add_error(format!(
282                "unsupported SHOW statement at {}",
283                self.cur_token.position
284            ));
285            None
286        }
287    }
288
289    /// Parse a DESCRIBE statement
290    pub(super) fn parse_describe_statement(&mut self) -> Option<DescribeStatement> {
291        let token = self.cur_token.clone();
292
293        // Move past DESCRIBE/DESC keyword
294        self.next_token();
295
296        let target = if self.cur_token.literal.eq_ignore_ascii_case("DATABASE") {
297            DescribeTarget::Database
298        } else {
299            // Optional TABLE keyword (DESCRIBE TABLE t or just DESCRIBE t).
300            if self.cur_token_is_keyword("TABLE") {
301                self.next_token();
302            }
303            if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
304                self.add_error(format!(
305                    "expected TABLE name or DATABASE after DESCRIBE at {}",
306                    self.cur_token.position
307                ));
308                return None;
309            }
310            DescribeTarget::Table(Identifier::new(
311                self.cur_token.clone(),
312                self.cur_token.literal.clone(),
313            ))
314        };
315
316        let format = if self.peek_token_is_keyword("FORMAT") {
317            self.next_token();
318            self.next_token();
319            if !self.cur_token_is_keyword("JSON") {
320                self.add_error(format!(
321                    "expected JSON after DESCRIBE ... FORMAT at {}",
322                    self.cur_token.position
323                ));
324                return None;
325            }
326            DescribeFormat::Json
327        } else {
328            DescribeFormat::Tabular
329        };
330
331        if matches!(target, DescribeTarget::Database) && format != DescribeFormat::Json {
332            self.add_error(
333                "DESCRIBE DATABASE requires FORMAT JSON; the legacy tabular format is table-only"
334                    .to_string(),
335            );
336            return None;
337        }
338
339        Some(DescribeStatement {
340            token,
341            target,
342            format,
343        })
344    }
345
346    /// Parse an EXPLAIN statement
347    pub(super) fn parse_explain_statement(&mut self) -> Option<ExplainStatement> {
348        let token = self.cur_token.clone();
349
350        // Check for ANALYZE option
351        let analyze = if self.peek_token_is_keyword("ANALYZE") {
352            self.next_token();
353            true
354        } else {
355            false
356        };
357
358        // Move to the statement to explain
359        self.next_token();
360
361        // Parse the inner statement (SELECT, INSERT, UPDATE, DELETE)
362        let statement = self.parse_statement()?;
363
364        Some(ExplainStatement {
365            token,
366            statement: Box::new(statement),
367            analyze,
368        })
369    }
370
371    /// Parse an ANALYZE statement
372    /// Syntax: ANALYZE [table_name]
373    pub(super) fn parse_analyze_statement(&mut self) -> Option<AnalyzeStatement> {
374        let token = self.cur_token.clone();
375
376        // Move past ANALYZE keyword
377        self.next_token();
378
379        // Optional table name
380        let table_name = if self.cur_token_is(TokenType::Identifier)
381            || (self.cur_token_is(TokenType::Keyword)
382                && !self.cur_token.literal.eq_ignore_ascii_case("TABLE"))
383        {
384            let name = self.cur_token.literal.clone();
385            Some(name)
386        } else if self.cur_token_is(TokenType::Keyword)
387            && self.cur_token.literal.eq_ignore_ascii_case("TABLE")
388        {
389            // ANALYZE TABLE table_name syntax
390            self.next_token();
391            if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
392                let name = self.cur_token.literal.clone();
393                Some(name)
394            } else {
395                self.add_error(format!(
396                    "expected table name after ANALYZE TABLE at {}",
397                    self.cur_token.position
398                ));
399                return None;
400            }
401        } else {
402            None
403        };
404
405        Some(AnalyzeStatement { token, table_name })
406    }
407
408    /// Parse an expression statement
409    pub(super) fn parse_expression_statement(&mut self) -> Option<ExpressionStatement> {
410        let token = self.cur_token.clone();
411        let expression = self.parse_expression(Precedence::Lowest)?;
412
413        Some(ExpressionStatement { token, expression })
414    }
415
416    /// Parse an identifier list (allows keywords as identifiers for CTE column aliases)
417    pub fn parse_identifier_list(&mut self) -> Vec<Identifier> {
418        let mut list = Vec::new();
419
420        self.next_token();
421        // Accept both identifiers and keywords as column names
422        if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
423            list.push(self.cur_token_as_column_identifier());
424        }
425
426        while self.peek_token_is_punctuator(",") {
427            self.next_token(); // consume comma
428            self.next_token(); // move to identifier/keyword
429                               // Accept both identifiers and keywords as column names
430            if self.cur_token_is(TokenType::Identifier) || self.cur_token_is(TokenType::Keyword) {
431                list.push(self.cur_token_as_column_identifier());
432            } else {
433                self.add_error(format!(
434                    "expected Identifier, got {:?} at {}",
435                    self.cur_token.token_type, self.cur_token.position
436                ));
437                return list;
438            }
439        }
440
441        list
442    }
443
444    /// Parse a COPY statement
445    /// COPY table [(columns)] FROM 'file_path' [WITH (FORMAT CSV|JSON [, HEADER true|false] [, DELIMITER 'c'] [, NULL 'str'])]
446    pub(super) fn parse_copy_statement(&mut self) -> Option<CopyStatement> {
447        let token = self.cur_token.clone();
448
449        // Parse table name
450        self.next_token();
451        if !self.cur_token_is(TokenType::Identifier) && !self.cur_token_is(TokenType::Keyword) {
452            self.add_error(format!(
453                "expected table name after COPY, got {:?} at {}",
454                self.cur_token.token_type, self.cur_token.position
455            ));
456            return None;
457        }
458        let table_name = self.parse_relation_identifier_current()?;
459
460        // Parse optional column list
461        let mut columns = Vec::new();
462        if self.peek_token_is_punctuator("(") {
463            self.next_token(); // consume (
464            columns = self.parse_identifier_list();
465            if !self.expect_peek(TokenType::Punctuator) || self.cur_token.literal != ")" {
466                self.add_error(format!("expected ')' at {}", self.cur_token.position));
467                return None;
468            }
469        }
470
471        // Expect FROM keyword
472        if !self.expect_keyword("FROM") {
473            return None;
474        }
475
476        // Parse file path (string literal)
477        self.next_token();
478        if !self.cur_token_is(TokenType::String) {
479            self.add_error(format!(
480                "expected file path string after FROM, got {:?} at {}",
481                self.cur_token.token_type, self.cur_token.position
482            ));
483            return None;
484        }
485        let file_path = {
486            let lit = &self.cur_token.literal;
487            if lit.len() >= 2
488                && (lit.starts_with('\'') || lit.starts_with('"'))
489                && lit.ends_with(lit.chars().next().unwrap())
490            {
491                lit[1..lit.len() - 1].to_string()
492            } else {
493                lit.to_string()
494            }
495        };
496
497        // Default options
498        let mut format = None;
499        let mut header = true;
500        let mut delimiter = b',';
501        let mut null_string = None;
502        let mut header_specified = false;
503        let mut delimiter_specified = false;
504
505        // Parse optional WITH (options)
506        if self.peek_token_is_keyword("WITH") {
507            self.next_token(); // consume WITH
508
509            if !self.peek_token_is_punctuator("(") {
510                self.add_error(format!(
511                    "expected '(' after WITH at {}",
512                    self.peek_token.position
513                ));
514                return None;
515            }
516            self.next_token(); // consume (
517
518            // Parse key-value options
519            loop {
520                self.next_token();
521                if self.cur_token_is(TokenType::Punctuator) && self.cur_token.literal == ")" {
522                    break;
523                }
524
525                let key = self.cur_token.literal.to_uppercase();
526                match key.as_str() {
527                    "FORMAT" => {
528                        self.next_token();
529                        let fmt_str = self.cur_token.literal.to_uppercase();
530                        match fmt_str.as_str() {
531                            "CSV" => format = Some(CopyFormat::Csv),
532                            "JSON" => format = Some(CopyFormat::Json),
533                            _ => {
534                                self.add_error(format!(
535                                    "unsupported COPY format '{}', expected CSV or JSON",
536                                    fmt_str
537                                ));
538                                return None;
539                            }
540                        }
541                    }
542                    "HEADER" => {
543                        self.next_token();
544                        let val = self.cur_token.literal.to_uppercase();
545                        header = match val.as_str() {
546                            "TRUE" | "ON" | "1" => true,
547                            "FALSE" | "OFF" | "0" => false,
548                            _ => {
549                                self.add_error(format!(
550                                    "invalid HEADER value '{}', expected TRUE or FALSE",
551                                    self.cur_token.literal
552                                ));
553                                return None;
554                            }
555                        };
556                        header_specified = true;
557                    }
558                    "DELIMITER" => {
559                        self.next_token();
560                        let delim_str = &self.cur_token.literal;
561                        // Strip quotes if present
562                        let raw = if delim_str.len() >= 2
563                            && (delim_str.starts_with('\'') || delim_str.starts_with('"'))
564                        {
565                            &delim_str[1..delim_str.len() - 1]
566                        } else {
567                            delim_str.as_str()
568                        };
569                        if raw.len() != 1 {
570                            self.add_error(format!(
571                                "DELIMITER must be a single character, got '{}'",
572                                raw
573                            ));
574                            return None;
575                        }
576                        delimiter = raw.as_bytes()[0];
577                        delimiter_specified = true;
578                    }
579                    "NULL" => {
580                        self.next_token();
581                        let ns = &self.cur_token.literal;
582                        null_string = Some(
583                            if ns.len() >= 2
584                                && (ns.starts_with('\'') || ns.starts_with('"'))
585                                && ns.ends_with(ns.chars().next().unwrap())
586                            {
587                                ns[1..ns.len() - 1].to_string()
588                            } else {
589                                ns.to_string()
590                            },
591                        );
592                    }
593                    _ => {
594                        self.add_error(format!("unknown COPY option '{}'", key));
595                        return None;
596                    }
597                }
598
599                // Expect comma or closing paren
600                if self.peek_token_is_punctuator(",") {
601                    self.next_token(); // consume comma
602                } else if self.peek_token_is_punctuator(")") {
603                    self.next_token(); // consume )
604                    break;
605                } else if !self.peek_token_is(TokenType::Eof) {
606                    self.add_error(format!(
607                        "expected ',' or ')' after COPY option, got {:?} at {}",
608                        self.peek_token.token_type, self.peek_token.position
609                    ));
610                    return None;
611                }
612            }
613        }
614
615        // FORMAT is required
616        let format = match format {
617            Some(f) => f,
618            None => {
619                // Default to CSV if no WITH clause
620                CopyFormat::Csv
621            }
622        };
623
624        if format == CopyFormat::Json && (header_specified || delimiter_specified) {
625            self.add_error("COPY JSON does not support HEADER or DELIMITER options".to_string());
626            return None;
627        }
628
629        Some(CopyStatement {
630            token,
631            table_name,
632            columns,
633            file_path,
634            format,
635            header,
636            delimiter,
637            null_string,
638        })
639    }
640}