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}