1#![cfg_attr(
5 test,
6 expect(
7 unsafe_code,
8 reason = "serial_test serializes Rust 2024 std::env var setters used by macro tests"
9 )
10)]
11use std::env;
18use std::fmt::Write;
19use std::path::PathBuf;
20
21use proc_macro::TokenStream;
22#[cfg(not(test))]
23use proc_macro_crate::{FoundCrate, crate_name};
24use proc_macro2::{Span, TokenStream as TokenStream2};
25use quote::quote;
26use syn::parse::{Parse, ParseStream};
27use syn::{Ident, Token};
28use vespertide_loader::{load_config_or_default, load_migrations_at_compile_time};
29use vespertide_planner::apply_action;
30use vespertide_query::{DatabaseBackend, build_plan_queries};
31
32struct MacroInput {
33 pool: proc_macro2::TokenStream,
34 version_table: Option<String>,
35 verbose: bool,
36}
37
38impl Parse for MacroInput {
39 fn parse(input: ParseStream) -> syn::Result<Self> {
40 let mut pool_tokens = Vec::new();
41 while !input.is_empty() && !input.peek(Token![,]) {
42 pool_tokens.push(input.parse::<proc_macro2::TokenTree>()?);
43 }
44 let pool: proc_macro2::TokenStream = pool_tokens.into_iter().collect();
45 let mut version_table = None;
46 let mut verbose = false;
47
48 while !input.is_empty() {
49 input.parse::<Token![,]>()?;
50 if input.is_empty() {
51 break;
52 }
53
54 let key: Ident = input.parse()?;
55 if key == "version_table" {
56 input.parse::<Token![=]>()?;
57 let value: syn::LitStr = input.parse()?;
58 version_table = Some(value.value());
59 } else if key == "verbose" {
60 verbose = true;
61 } else {
62 return Err(syn::Error::new(
63 key.span(),
64 "unsupported option for vespertide_migration!",
65 ));
66 }
67 }
68
69 Ok(MacroInput {
70 pool,
71 version_table,
72 verbose,
73 })
74 }
75}
76
77#[cfg(not(test))]
82fn vespertide_crate_path() -> syn::Result<TokenStream2> {
83 match crate_name("vespertide").map_err(|e| {
84 syn::Error::new(
85 Span::call_site(),
86 format!("vespertide crate not found in Cargo.toml dependencies: {e}"),
87 )
88 })? {
89 FoundCrate::Itself => Ok(quote! { crate }),
90 FoundCrate::Name(name) => {
91 let ident = Ident::new(&name, Span::call_site());
92 Ok(quote! { ::#ident })
93 }
94 }
95}
96
97#[cfg(test)]
98#[expect(
99 clippy::unnecessary_wraps,
100 reason = "test stub mirrors the production syn::Result<TokenStream2> helper so macro codegen call sites stay uniform"
101)]
102fn vespertide_crate_path() -> syn::Result<TokenStream2> {
103 Ok(quote! { ::vespertide })
104}
105
106#[derive(Debug)]
108pub(crate) struct MigrationBlock {
109 pub version: u32,
111 pub migration_id: String,
113 pub comment: String,
115 pub pg_sqls: Vec<String>,
117 pub mysql_sqls: Vec<String>,
119 pub sqlite_sqls: Vec<String>,
121}
122
123pub(crate) fn build_migration_block(
124 migration: &vespertide_core::MigrationPlan,
125 baseline_schema: &mut Vec<vespertide_core::TableDef>,
126) -> Result<MigrationBlock, String> {
127 let version = migration.version;
128
129 let queries = build_plan_queries(migration, baseline_schema)
131 .map_err(|e| format!("Failed to build queries for migration version {version}: {e}"))?;
132
133 for action in &migration.actions {
135 let _ = apply_action(baseline_schema, action);
136 }
137
138 let mut pg_sqls = Vec::with_capacity(queries.iter().map(|q| q.postgres.len()).sum());
141 let mut mysql_sqls = Vec::with_capacity(queries.iter().map(|q| q.mysql.len()).sum());
142 let mut sqlite_sqls = Vec::with_capacity(queries.iter().map(|q| q.sqlite.len()).sum());
143
144 for q in &queries {
145 for stmt in &q.postgres {
146 pg_sqls.push(stmt.build(DatabaseBackend::Postgres));
147 }
148 for stmt in &q.mysql {
149 mysql_sqls.push(stmt.build(DatabaseBackend::MySql));
150 }
151 for stmt in &q.sqlite {
152 sqlite_sqls.push(stmt.build(DatabaseBackend::Sqlite));
153 }
154 }
155
156 let comment = migration.comment.as_deref().unwrap_or("").to_string();
157
158 Ok(MigrationBlock {
159 version,
160 migration_id: migration.id.clone(),
161 comment,
162 pg_sqls,
163 mysql_sqls,
164 sqlite_sqls,
165 })
166}
167
168fn generate_migration_code(
169 pool: &TokenStream2,
170 version_table: &str,
171 migration_blocks: &[MigrationBlock],
172 verbose: bool,
173 lock_timeout_ms: Option<u64>,
174 statement_timeout_ms: Option<u64>,
175) -> syn::Result<TokenStream2> {
176 let pool_str = pool.to_string();
179 let krate = vespertide_crate_path()?;
180 let krate_str = krate.to_string();
181 let mut code = String::with_capacity(1_048_576);
182
183 code.push_str("{\n");
184
185 for b in migration_blocks {
187 write_sql_blob(&mut code, &format!("__V{}_PG", b.version), &b.pg_sqls)?;
188 write_sql_blob(&mut code, &format!("__V{}_MYSQL", b.version), &b.mysql_sqls)?;
189 write_sql_blob(
190 &mut code,
191 &format!("__V{}_SQLITE", b.version),
192 &b.sqlite_sqls,
193 )?;
194 }
195
196 writeln!(
197 code,
198 "static __VESPERTIDE_MIGRATIONS: &[{krate_str}::runtime::EmbeddedMigration] = &["
199 )
200 .map_err(write_codegen_error)?;
201 for b in migration_blocks {
202 writeln!(code, "{krate_str}::runtime::EmbeddedMigration::new({}u32, {:?}, {:?}, __V{}_PG, __V{}_MYSQL, __V{}_SQLITE),", b.version, b.migration_id, b.comment, b.version, b.version, b.version).map_err(write_codegen_error)?;
203 }
204 code.push_str("];\n");
205
206 code.push_str("async {\n");
207 writeln!(code, "let __pool = &{pool_str};").map_err(write_codegen_error)?;
208 if lock_timeout_ms.is_none() && statement_timeout_ms.is_none() {
212 writeln!(code, "{krate_str}::runtime::run_embedded_migrations(__pool, {version_table:?}, {verbose}, __VESPERTIDE_MIGRATIONS).await").map_err(write_codegen_error)?;
213 } else {
214 let lock = render_opt_u64(lock_timeout_ms);
215 let stmt = render_opt_u64(statement_timeout_ms);
216 writeln!(code, "{krate_str}::runtime::run_embedded_migrations_with_options(__pool, {version_table:?}, {verbose}, __VESPERTIDE_MIGRATIONS, {krate_str}::runtime::MigrationRuntimeOptions::from_millis({lock}, {stmt})).await").map_err(write_codegen_error)?;
217 }
218 code.push_str("}\n"); code.push_str("}\n"); code.parse::<TokenStream2>().map_err(|e| {
223 syn::Error::new(
224 Span::call_site(),
225 format!("vespertide_migration codegen produced invalid Rust: {e}"),
226 )
227 })
228}
229
230fn render_opt_u64(value: Option<u64>) -> String {
232 value.map_or_else(|| "None".to_string(), |v| format!("Some({v}u64)"))
233}
234
235fn write_codegen_error(e: std::fmt::Error) -> syn::Error {
236 syn::Error::new(
237 Span::call_site(),
238 format!("vespertide_migration codegen failed while writing Rust: {e}"),
239 )
240}
241
242fn write_sql_blob(code: &mut String, ident: &str, sqls: &[String]) -> syn::Result<()> {
244 let mut blob = String::new();
245 for sql in sqls {
246 blob.push_str(sql);
247 blob.push('\0');
248 }
249
250 writeln!(code, "static {ident}: &str = {blob:?};").map_err(write_codegen_error)
251}
252
253pub(crate) fn vespertide_migration_impl(input: TokenStream2) -> syn::Result<TokenStream2> {
255 let input: MacroInput = syn::parse2(input)?;
256 let pool = &input.pool;
257 let verbose = input.verbose;
258
259 let project_root = match env::var("CARGO_MANIFEST_DIR") {
261 Ok(dir) => Some(PathBuf::from(dir)),
262 Err(_) => None,
263 };
264
265 let config = match load_config_or_default(project_root) {
267 Ok(config) => config,
268 Err(e) => {
269 return Err(syn::Error::new(
270 proc_macro2::Span::call_site(),
271 format!("Failed to load config at compile time: {e}"),
272 ));
273 }
274 };
275 let prefix = config.prefix();
276
277 let version_table = input.version_table.map_or_else(
279 || config.apply_prefix("vespertide_version"),
280 |vt| config.apply_prefix(&vt),
281 );
282
283 let migrations = match load_migrations_at_compile_time() {
285 Ok(migrations) => migrations,
286 Err(e) => {
287 return Err(syn::Error::new(
288 proc_macro2::Span::call_site(),
289 format!("Failed to load migrations at compile time: {e}"),
290 ));
291 }
292 };
293 let mut baseline_schema = Vec::new();
295 let mut migration_blocks = Vec::new();
296
297 for migration in &migrations {
298 let prefixed_migration = migration.clone().with_prefix(prefix);
300 match build_migration_block(&prefixed_migration, &mut baseline_schema) {
301 Ok(block) => migration_blocks.push(block),
302 Err(e) => {
303 return Err(syn::Error::new(proc_macro2::Span::call_site(), e));
304 }
305 }
306 }
307
308 generate_migration_code(
309 pool,
310 &version_table,
311 &migration_blocks,
312 verbose,
313 config.lock_timeout_ms(),
314 config.statement_timeout_ms(),
315 )
316}
317
318#[cfg(not(tarpaulin_include))]
328#[proc_macro]
329pub fn vespertide_migration(input: TokenStream) -> TokenStream {
330 let input2 = TokenStream2::from(input);
331 match vespertide_migration_impl(input2) {
332 Ok(ts) => ts.into(),
333 Err(e) => e.to_compile_error().into(),
334 }
335}
336
337#[cfg(test)]
338mod tests;