qail_core/migrate/
parser.rs1use super::schema::{Column, Index, MigrationHint, Schema, Table};
21
22pub fn parse_qail(input: &str) -> Result<Schema, String> {
24 let mut schema = Schema::new();
25 let mut lines = input.lines().peekable();
26
27 while let Some(line) = lines.next() {
28 let line = line.trim();
29
30 if line.is_empty() || line.starts_with('#') || line.starts_with("--") {
32 continue;
33 }
34
35 if line.starts_with("table ") {
36 let (table, consumed) = parse_table(line, &mut lines)?;
37 schema.add_table(table);
38 let _ = consumed;
40 }
41 else if line.starts_with("unique index ") || line.starts_with("index ") {
42 let index = parse_index(line)?;
43 schema.add_index(index);
44 }
45 else if line.starts_with("rename ") {
46 let hint = parse_rename(line)?;
47 schema.add_hint(hint);
48 } else if line.starts_with("transform ") {
49 let hint = parse_transform(line)?;
50 schema.add_hint(hint);
51 } else if line.starts_with("drop ") {
52 let hint = parse_drop(line)?;
53 schema.add_hint(hint);
54 } else {
55 return Err(format!("Unknown statement: {}", line));
56 }
57 }
58
59 Ok(schema)
60}
61
62fn parse_table<'a, I>(
64 first_line: &str,
65 lines: &mut std::iter::Peekable<I>,
66) -> Result<(Table, usize), String>
67where
68 I: Iterator<Item = &'a str>,
69{
70 let rest = first_line.strip_prefix("table ").unwrap();
71 let name = rest.trim_end_matches('{').trim().to_string();
72
73 if name.is_empty() {
74 return Err("Table name required".to_string());
75 }
76
77 let mut table = Table::new(&name);
78 let mut consumed = 0;
79
80 for line in lines.by_ref() {
81 consumed += 1;
82 let line = line.trim();
83
84 if line == "}" || line.starts_with('}') {
85 break;
86 }
87
88 if line.is_empty() || line.starts_with('#') {
89 continue;
90 }
91
92 let col = parse_column(line)?;
93 table.columns.push(col);
94 }
95
96 Ok((table, consumed))
97}
98
99fn parse_column(line: &str) -> Result<Column, String> {
101 let parts: Vec<&str> = line.split_whitespace().collect();
102
103 if parts.len() < 2 {
104 return Err(format!("Invalid column: {}", line));
105 }
106
107 let name = parts[0].to_string();
108 let type_str = parts[1];
109
110 let data_type: super::types::ColumnType = type_str
111 .parse()
112 .map_err(|_| format!("Unknown column type: {}", type_str))?;
113
114 let mut col = Column::new(&name, data_type);
115
116 let mut i = 2;
117 while i < parts.len() {
118 match parts[i] {
119 "primary_key" => {
120 col.primary_key = true;
121 col.nullable = false;
122 }
123 "not_null" => {
124 col.nullable = false;
125 }
126 "nullable" => {
127 col.nullable = true;
128 }
129 "unique" => {
130 col.unique = true;
131 }
132 "default" => {
133 if i + 1 < parts.len() {
134 col.default = Some(parts[i + 1].to_string());
135 i += 1;
136 }
137 }
138 s if s.starts_with("references") => {
139 let fk_str = if s.contains('(') {
140 s.strip_prefix("references").unwrap_or(s)
142 } else if i + 1 < parts.len() {
143 i += 1;
145 parts[i]
146 } else {
147 ""
148 };
149
150 if let Some(paren_start) = fk_str.find('(')
151 && let Some(paren_end) = fk_str.find(')')
152 {
153 let table = &fk_str[..paren_start];
154 let column = &fk_str[paren_start + 1..paren_end];
155 col = col.references(table, column);
156 }
157 }
158 _ => {
159 }
161 }
162 i += 1;
163 }
164
165 Ok(col)
166}
167
168fn parse_index(line: &str) -> Result<Index, String> {
170 let is_unique = line.starts_with("unique ");
171 let rest = if is_unique {
172 line.strip_prefix("unique index ").unwrap()
173 } else {
174 line.strip_prefix("index ").unwrap()
175 };
176
177 let parts: Vec<&str> = rest.splitn(2, " on ").collect();
178 if parts.len() != 2 {
179 return Err(format!("Invalid index: {}", line));
180 }
181
182 let name = parts[0].trim().to_string();
183 let rest = parts[1];
184
185 let paren_start = rest.find('(').ok_or("Missing ( in index")?;
186 let paren_end = rest.find(')').ok_or("Missing ) in index")?;
187
188 let table = rest[..paren_start].trim().to_string();
189 let cols_str = &rest[paren_start + 1..paren_end];
190 let columns: Vec<String> = cols_str.split(',').map(|s| s.trim().to_string()).collect();
191
192 let mut index = Index::new(&name, &table, columns);
193 if is_unique {
194 index.unique = true;
195 }
196
197 Ok(index)
198}
199
200fn parse_rename(line: &str) -> Result<MigrationHint, String> {
202 let rest = line.strip_prefix("rename ").unwrap();
204 let parts: Vec<&str> = rest.split(" -> ").collect();
205
206 if parts.len() != 2 {
207 return Err(format!("Invalid rename: {}", line));
208 }
209
210 Ok(MigrationHint::Rename {
211 from: parts[0].trim().to_string(),
212 to: parts[1].trim().to_string(),
213 })
214}
215
216fn parse_transform(line: &str) -> Result<MigrationHint, String> {
218 let rest = line.strip_prefix("transform ").unwrap();
220 let parts: Vec<&str> = rest.split(" -> ").collect();
221
222 if parts.len() != 2 {
223 return Err(format!("Invalid transform: {}", line));
224 }
225
226 Ok(MigrationHint::Transform {
227 expression: parts[0].trim().to_string(),
228 target: parts[1].trim().to_string(),
229 })
230}
231
232fn parse_drop(line: &str) -> Result<MigrationHint, String> {
234 let rest = line.strip_prefix("drop ").unwrap();
236 let confirmed = rest.ends_with(" confirm");
237 let target = if confirmed {
238 rest.strip_suffix(" confirm").unwrap().trim().to_string()
239 } else {
240 rest.trim().to_string()
241 };
242
243 Ok(MigrationHint::Drop { target, confirmed })
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_parse_simple_table() {
252 let input = r#"
253table users {
254 id serial primary_key
255 name text not_null
256 email text nullable unique
257}
258"#;
259 let schema = parse_qail(input).unwrap();
260 assert!(schema.tables.contains_key("users"));
261 let table = &schema.tables["users"];
262 assert_eq!(table.columns.len(), 3);
263 assert!(table.columns[0].primary_key);
264 assert!(!table.columns[1].nullable);
265 assert!(table.columns[2].unique);
266 }
267
268 #[test]
269 fn test_parse_index() {
270 let input = "unique index idx_users_email on users (email)";
271 let schema = parse_qail(input).unwrap();
272 assert_eq!(schema.indexes.len(), 1);
273 assert!(schema.indexes[0].unique);
274 assert_eq!(schema.indexes[0].name, "idx_users_email");
275 }
276
277 #[test]
278 fn test_parse_rename() {
279 let input = "rename users.username -> users.name";
280 let schema = parse_qail(input).unwrap();
281 assert_eq!(schema.migrations.len(), 1);
282 assert!(matches!(
283 &schema.migrations[0],
284 MigrationHint::Rename { from, to } if from == "users.username" && to == "users.name"
285 ));
286 }
287
288 #[test]
289 fn test_parse_full_schema() {
290 let input = r#"
291# User table
292table users {
293 id serial primary_key
294 name text not_null
295 email text unique
296 created_at timestamptz default now()
297}
298
299unique index idx_users_email on users (email)
300
301rename users.username -> users.name
302"#;
303 let schema = parse_qail(input).unwrap();
304 assert_eq!(schema.tables.len(), 1);
305 assert_eq!(schema.indexes.len(), 1);
306 assert_eq!(schema.migrations.len(), 1);
307 }
308}