Skip to main content

reddb_server/storage/query/parser/
migration.rs

1//! Parser for migration SQL statements.
2
3use super::super::ast::{
4    ApplyMigrationQuery, ApplyMigrationTarget, CreateMigrationQuery, ExplainMigrationQuery,
5    QueryExpr, RollbackMigrationQuery,
6};
7use super::super::lexer::Token;
8use super::error::ParseError;
9use super::Parser;
10
11impl<'a> Parser<'a> {
12    /// Parse: CREATE MIGRATION name [DEPENDS ON dep1, dep2] [BATCH n ROWS] [NO ROLLBACK] body_sql
13    ///
14    /// Called after CREATE has been consumed and MIGRATION ident detected.
15    pub fn parse_create_migration_body(&mut self) -> Result<QueryExpr, ParseError> {
16        let name = self.expect_ident()?;
17
18        let mut depends_on: Vec<String> = Vec::new();
19        let mut batch_size: Option<u64> = None;
20        let mut no_rollback = false;
21
22        // Parse optional clauses in any order before the body
23        loop {
24            if self.consume_ident_ci("DEPENDS")? {
25                // `ON` is lexed as `Token::On` (reserved keyword), not as
26                // an identifier — `consume_ident_ci("ON")` would silently
27                // miss it and the next `expect_ident()` would surface
28                // "expected identifier, got ON". Require the typed
29                // keyword so the dependency list actually parses.
30                self.expect(Token::On)?;
31                loop {
32                    depends_on.push(self.expect_ident()?);
33                    if !self.consume(&Token::Comma)? {
34                        break;
35                    }
36                }
37            } else if self.consume_ident_ci("BATCH")? {
38                if let Token::Integer(n) = self.peek().clone() {
39                    self.advance()?;
40                    batch_size = Some(n as u64);
41                }
42                self.consume_ident_ci("ROWS")?;
43            } else if self.consume_ident_ci("NO")? {
44                let _ = self.consume(&Token::Rollback)? || self.consume_ident_ci("ROLLBACK")?;
45                no_rollback = true;
46            } else {
47                break;
48            }
49        }
50
51        // Optional `AS` keyword separating the metadata clauses from the
52        // body. SQL convention; without consuming it the body string
53        // begins with the literal "AS " token, which then doesn't
54        // round-trip through the query-mode detector when the engine
55        // re-executes the body in `apply_batched`. `AS` is lexed as
56        // `Token::As`, not as an identifier, so use `consume(&Token::As)`.
57        let _ = self.consume(&Token::As)?;
58
59        // Everything remaining until EOF is the body
60        let body = self.collect_remaining_input();
61
62        Ok(QueryExpr::CreateMigration(CreateMigrationQuery {
63            name,
64            body,
65            depends_on,
66            batch_size,
67            no_rollback,
68        }))
69    }
70
71    /// Parse: APPLY MIGRATION name | APPLY MIGRATION * [FOR TENANT id]
72    pub fn parse_apply_migration(&mut self) -> Result<QueryExpr, ParseError> {
73        // APPLY has already been consumed. The `MIGRATION` keyword is
74        // mandatory — `consume_ident_ci` returns `Ok(false)` on a
75        // miss, which previously let `APPLY m1` silently succeed by
76        // treating `m1` as the migration name. Require it strictly.
77        if !self.consume_ident_ci("MIGRATION")? {
78            return Err(ParseError::expected(
79                vec!["MIGRATION"],
80                self.peek(),
81                self.position(),
82            ));
83        }
84
85        let target = if self.consume(&Token::Star)? {
86            ApplyMigrationTarget::All
87        } else {
88            let name = self.expect_ident()?;
89            ApplyMigrationTarget::Named(name)
90        };
91
92        // `FOR` is lexed as `Token::For` (reserved keyword), not as an
93        // identifier — `consume_ident_ci("FOR")` never matched it and
94        // the suffix was silently dropped, so the `for_tenant` slot
95        // stayed `None` while `Token::For` leaked back to the
96        // top-level parser as "Unexpected token after query".
97        let for_tenant = if self.consume(&Token::For)? {
98            // Once FOR is committed, TENANT must follow — bail
99            // explicitly if it doesn't, instead of silently accepting
100            // arbitrary identifiers as the tenant id.
101            if !self.consume_ident_ci("TENANT")? {
102                return Err(ParseError::expected(
103                    vec!["TENANT"],
104                    self.peek(),
105                    self.position(),
106                ));
107            }
108            Some(self.expect_string_or_ident()?)
109        } else {
110            None
111        };
112
113        Ok(QueryExpr::ApplyMigration(ApplyMigrationQuery {
114            target,
115            for_tenant,
116        }))
117    }
118
119    /// Parse: ROLLBACK MIGRATION name  (called after ROLLBACK is consumed)
120    pub fn parse_rollback_migration_after_keyword(&mut self) -> Result<QueryExpr, ParseError> {
121        self.consume_ident_ci("MIGRATION")?;
122        let name = self.expect_ident()?;
123        Ok(QueryExpr::RollbackMigration(RollbackMigrationQuery {
124            name,
125        }))
126    }
127
128    /// Parse: EXPLAIN MIGRATION name  (called after EXPLAIN is consumed)
129    pub fn parse_explain_migration_after_keyword(&mut self) -> Result<QueryExpr, ParseError> {
130        self.consume_ident_ci("MIGRATION")?;
131        let name = self.expect_ident()?;
132        Ok(QueryExpr::ExplainMigration(ExplainMigrationQuery { name }))
133    }
134
135    /// Collect all remaining tokens into a single string (joined with spaces).
136    /// Used to capture the raw SQL body of a migration.
137    pub fn collect_remaining_input(&mut self) -> String {
138        let mut parts: Vec<String> = Vec::new();
139        loop {
140            if self.check(&Token::Eof) {
141                break;
142            }
143            parts.push(self.current.token.to_string());
144            // Advance, ignoring errors (at worst we stop early)
145            if self.advance().is_err() {
146                break;
147            }
148        }
149        parts.join(" ")
150    }
151
152    /// Try to consume a bare identifier or a single-quoted string literal.
153    pub fn expect_string_or_ident(&mut self) -> Result<String, ParseError> {
154        match self.peek().clone() {
155            Token::String(s) => {
156                self.advance()?;
157                Ok(s)
158            }
159            Token::Ident(_) => self.expect_ident(),
160            other => Err(ParseError::expected(
161                vec!["string or identifier"],
162                &other,
163                self.position(),
164            )),
165        }
166    }
167}