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}