Skip to main content

reddb_rql/parser/
migration.rs

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