1use quote::quote;
2use syn::__private::TokenStream;
3use syn::{Data, DeriveInput, Fields, Meta, parse_macro_input};
4
5#[proc_macro_derive(Csv, attributes(migrations))]
6pub fn csv_macro(input: TokenStream) -> TokenStream {
7 let input = parse_macro_input!(input as DeriveInput);
8 let struct_name = input.ident;
9 let expanded = quote! {
10 impl #struct_name {
11 pub async fn save_csv(
13 records: Vec<Self>,
14 file_path: &str,
15 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
16 where
17 Self: serde::Serialize,
18 {
19 let mut writer = csv::Writer::from_path(file_path)?;
20 for record in records {
21 writer.serialize(record)?;
22 }
23 writer.flush()?;
24 Ok(())
25 }
26 pub async fn load_csv(
28 file_path: &str,
29 ) -> Result<Vec<Self>, Box<dyn std::error::Error + Send + Sync>>
30 where
31 Self: for<'de> serde::Deserialize<'de>,
32 {
33 let mut reader = csv::Reader::from_path(file_path)?;
34 let mut records = Vec::new();
35 for result in reader.deserialize() {
36 let record: Self = result?;
37 records.push(record);
38 }
39 Ok(records)
40 }
41
42 pub async fn save_field<F, T>(
44 records: Vec<Self>,
45 file_path: &str,
46 field_extractor: F,
47 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
48 where
49 F: Fn(&Self) -> T,
50 T: serde::Serialize,
51 {
52 let mut writer = csv::Writer::from_path(file_path)?;
53 for record in records {
54 let field_value = field_extractor(&record);
55 writer.serialize(field_value)?;
56 }
57 writer.flush()?;
58 Ok(())
59 }
60 }
61 };
62 TokenStream::from(expanded)
63}
64
65#[proc_macro_derive(Persistency, attributes(migrations))]
66pub fn persistency_macro(input: TokenStream) -> TokenStream {
67 let input = parse_macro_input!(input as DeriveInput);
68 let struct_name = input.ident;
69
70 let mut schema = String::new();
71 let mut table = String::new();
72
73 for attr in input.attrs {
74 if attr.path().is_ident("migrations") {
75 match attr.meta {
76 Meta::List(list) => {
77 let nested = list
78 .parse_args_with(
79 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
80 )
81 .unwrap_or_else(|_| syn::punctuated::Punctuated::new());
82
83 for meta in nested {
84 match meta {
85 Meta::NameValue(nv) if nv.path.is_ident("schema") => {
86 if let syn::Expr::Lit(syn::ExprLit {
87 lit: syn::Lit::Str(lit_str),
88 ..
89 }) = nv.value
90 {
91 schema = lit_str.value();
92 }
93 }
94 Meta::NameValue(nv) if nv.path.is_ident("table") => {
95 if let syn::Expr::Lit(syn::ExprLit {
96 lit: syn::Lit::Str(lit_str),
97 ..
98 }) = nv.value
99 {
100 table = lit_str.value();
101 }
102 }
103 _ => {}
104 }
105 }
106 }
107 _ => {}
108 }
109 }
110 }
111
112 if schema.is_empty() || table.is_empty() {
113 return TokenStream::from(quote! {
114 compile_error!("Both `schema` and `table` must be provided as attributes for #[migrations(schema = \"...\", table = \"...\")].");
115 });
116 }
117
118 let expanded = quote! {
119 impl #struct_name {
120 pub async fn set_persistent(
122 pool: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
123 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
124 let client = pool.get().await?;
125 let query = format!(
126 r#"ALTER TABLE {}."{}" SET LOGGED;"#, #schema, #table
127 );
128 client.execute(&query, &[]).await?;
129 Ok(())
130 }
131
132 pub async fn set_unlogged(
134 pool: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
135 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
136 let client = pool.get().await?;
137 let query = format!(
138 r#"ALTER TABLE {}."{}" SET UNLOGGED;"#, #schema, #table
139 );
140 client.execute(&query, &[]).await?;
141 Ok(())
142 }
143 }
144 };
145
146 TokenStream::from(expanded)
147}
148
149#[proc_macro_derive(Vacuum, attributes(migrations))]
150pub fn vacuum_macro(input: TokenStream) -> TokenStream {
151 let input = parse_macro_input!(input as DeriveInput);
152 let struct_name = input.ident;
153
154 let mut schema = String::new();
155 let mut table = String::new();
156
157 for attr in input.attrs {
158 if attr.path().is_ident("migrations") {
159 match attr.meta {
160 Meta::List(list) => {
161 let nested = list
162 .parse_args_with(
163 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
164 )
165 .unwrap_or_else(|_| syn::punctuated::Punctuated::new());
166
167 for meta in nested {
168 match meta {
169 Meta::NameValue(nv) if nv.path.is_ident("schema") => {
170 if let syn::Expr::Lit(syn::ExprLit {
171 lit: syn::Lit::Str(lit_str),
172 ..
173 }) = nv.value
174 {
175 schema = lit_str.value();
176 }
177 }
178 Meta::NameValue(nv) if nv.path.is_ident("table") => {
179 if let syn::Expr::Lit(syn::ExprLit {
180 lit: syn::Lit::Str(lit_str),
181 ..
182 }) = nv.value
183 {
184 table = lit_str.value();
185 }
186 }
187 _ => {}
188 }
189 }
190 }
191 _ => {}
192 }
193 }
194 }
195
196 if schema.is_empty() || table.is_empty() {
197 return TokenStream::from(quote! {
198 compile_error!("Both `schema` and `table` must be provided as attributes for #[migrations(schema = \"...\", table = \"...\")].");
199 });
200 }
201
202 let expanded = quote! {
203 impl #struct_name {
204 pub async fn vacuum(
206 pool: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
207 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
208 let client = pool.get().await?;
209 let query = format!(r#"VACUUM FULL {}."{}";"#, #schema, #table);
210 client.execute(&query, &[]).await?;
211 Ok(())
212 }
213 }
214 };
215
216 TokenStream::from(expanded)
217}
218
219#[proc_macro_derive(Reindex, attributes(migrations))]
220pub fn reindex_macro(input: TokenStream) -> TokenStream {
221 let input = parse_macro_input!(input as DeriveInput);
222 let struct_name = input.ident;
223
224 let mut schema = String::new();
225 let mut table = String::new();
226
227 for attr in input.attrs {
228 if attr.path().is_ident("migrations") {
229 match attr.meta {
230 Meta::List(list) => {
231 let nested = list
232 .parse_args_with(
233 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
234 )
235 .unwrap_or_else(|_| syn::punctuated::Punctuated::new());
236
237 for meta in nested {
238 match meta {
239 Meta::NameValue(nv) if nv.path.is_ident("schema") => {
240 if let syn::Expr::Lit(syn::ExprLit {
241 lit: syn::Lit::Str(lit_str),
242 ..
243 }) = nv.value
244 {
245 schema = lit_str.value();
246 }
247 }
248 Meta::NameValue(nv) if nv.path.is_ident("table") => {
249 if let syn::Expr::Lit(syn::ExprLit {
250 lit: syn::Lit::Str(lit_str),
251 ..
252 }) = nv.value
253 {
254 table = lit_str.value();
255 }
256 }
257 _ => {}
258 }
259 }
260 }
261 _ => {}
262 }
263 }
264 }
265
266 if schema.is_empty() || table.is_empty() {
267 return TokenStream::from(quote! {
268 compile_error!("Both `schema` and `table` must be provided as attributes for #[migrations(schema = \"...\", table = \"...\")].");
269 });
270 }
271
272 let expanded = quote! {
273 impl #struct_name {
274 pub async fn reindex(
276 pool: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
277 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
278 let client = pool.get().await?;
279 let query = format!(r#"REINDEX TABLE {}."{}";"#, #schema, #table);
280 client.execute(&query, &[]).await?;
281 Ok(())
282 }
283 }
284 };
285
286 TokenStream::from(expanded)
287}
288
289#[proc_macro_derive(Database, attributes(migrations))]
290pub fn database_macro(input: TokenStream) -> TokenStream {
291 let input = parse_macro_input!(input as DeriveInput);
292 let struct_name = input.ident;
293
294 let mut schema = String::new();
296 let mut table = String::new();
297
298 for attr in input.attrs.iter() {
299 if attr.path().is_ident("migrations") {
300 match attr.meta {
301 Meta::List(ref list) => {
302 let nested = list
303 .parse_args_with(
304 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
305 )
306 .unwrap_or_else(|_| syn::punctuated::Punctuated::new());
307
308 for meta in nested {
309 match meta {
310 Meta::NameValue(ref nv) if nv.path.is_ident("schema") => {
311 if let syn::Expr::Lit(syn::ExprLit {
312 lit: syn::Lit::Str(ref lit_str),
313 ..
314 }) = nv.value
315 {
316 schema = lit_str.value();
317 }
318 }
319 Meta::NameValue(ref nv) if nv.path.is_ident("table") => {
320 if let syn::Expr::Lit(syn::ExprLit {
321 lit: syn::Lit::Str(ref lit_str),
322 ..
323 }) = nv.value
324 {
325 table = lit_str.value();
326 }
327 }
328 _ => {}
329 }
330 }
331 }
332 _ => {}
333 }
334 }
335 }
336
337 if schema.is_empty() || table.is_empty() {
338 return TokenStream::from(quote! {
339 compile_error!("Both `schema` and `table` must be provided as attributes for #[migrations(schema = \"...\", table = \"...\")].");
340 });
341 }
342
343 let mut field_conversions = Vec::new();
345 let mut field_names = Vec::new();
346
347 if let Data::Struct(data_struct) = &input.data {
348 if let Fields::Named(fields_named) = &data_struct.fields {
349 for field in fields_named.named.iter() {
350 if let Some(field_name) = &field.ident {
351 let mut db_field_name = field_name.to_string();
352
353 for attr in &field.attrs {
355 if attr.path().is_ident("serde") {
356 if let Meta::List(list) = &attr.meta {
357 let nested = list.parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
358 .unwrap_or_else(|_| syn::punctuated::Punctuated::new());
359
360 for meta in nested {
361 if let Meta::NameValue(nv) = meta {
362 if nv.path.is_ident("rename") {
363 if let syn::Expr::Lit(syn::ExprLit {
364 lit: syn::Lit::Str(lit_str),
365 ..
366 }) = nv.value
367 {
368 db_field_name = lit_str.value();
369 }
370 }
371 }
372 }
373 }
374 }
375 }
376
377 let field_name_ident = field_name;
379 let db_field_name_literal = db_field_name.clone();
380
381 field_conversions.push(quote! {
382 #field_name_ident: row.get(#db_field_name_literal)
383 });
384
385 field_names.push(db_field_name);
386 }
387 }
388 }
389 }
390
391 let field_names_str = field_names
393 .iter()
394 .map(|name| format!("\"{}\"", name))
395 .collect::<Vec<_>>()
396 .join(", ");
397
398 let expanded = quote! {
399 impl #struct_name {
400 pub fn convert_from_row(row: bb8_postgres::tokio_postgres::Row) -> Self {
402 Self {
403 #(#field_conversions),*
404 }
405 }
406
407 pub async fn get(
409 connection: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
410 ) -> Result<Vec<Self>, Box<dyn std::error::Error + Send + Sync>> {
411 let client = connection.get().await?;
412 let query = format!(
413 r#"SELECT {} FROM {}."{}"#,
414 #field_names_str, #schema, #table
415 );
416 let rows = client.query(&query, &[]).await?;
417 let results: Vec<Self> = rows.into_iter().map(Self::convert_from_row).collect();
418 Ok(results)
419 }
420
421 pub async fn get_filter_i64<F>(
423 connection: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
424 field_extractor: F,
425 filter_values: std::collections::HashSet<i64>,
426 ) -> Result<Vec<Self>, Box<dyn std::error::Error + Send + Sync>>
427 where
428 F: Fn(&Self) -> i64,
429 {
430 let client = connection.get().await?;
431 let query = format!(
432 r#"SELECT {} FROM {}."{}"#,
433 #field_names_str, #schema, #table
434 );
435 let rows = client.query(&query, &[]).await?;
436 let results: Vec<Self> = rows
437 .into_iter()
438 .map(Self::convert_from_row)
439 .filter(|record| filter_values.contains(&field_extractor(record)))
440 .collect();
441
442 Ok(results)
443 }
444
445 pub async fn get_filter_i64_i64<F1, F2>(
447 connection: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
448 field_extractor1: F1,
449 filter_values1: std::collections::HashSet<i64>,
450 field_extractor2: F2,
451 filter_values2: std::collections::HashSet<i64>,
452 ) -> Result<Vec<Self>, Box<dyn std::error::Error + Send + Sync>>
453 where
454 F1: Fn(&Self) -> Option<i64>,
455 F2: Fn(&Self) -> Option<i64>,
456 {
457 let client = connection.get().await?;
458 let query = format!(
459 r#"SELECT {} FROM {}."{}"#,
460 #field_names_str, #schema, #table
461 );
462 let rows = client.query(&query, &[]).await?;
463 let results: Vec<Self> = rows
464 .into_iter()
465 .map(Self::convert_from_row)
466 .filter(|record| {
467 let value1 = field_extractor1(record).unwrap_or(-1);
468 let value2 = field_extractor2(record).unwrap_or(-1);
469 filter_values1.contains(&value1) && filter_values2.contains(&value2)
470 })
471 .collect();
472
473 Ok(results)
474 }
475
476 pub async fn get_filter_uuid<F>(
478 connection: &bb8_postgres::bb8::Pool<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls>>,
479 field_extractor: F,
480 filter_values: std::collections::HashSet<uuid>,
481 ) -> Result<Vec<Self>, Box<dyn std::error::Error + Send + Sync>>
482 where
483 F: Fn(&Self) -> uuid,
484 {
485 let client = connection.get().await?;
486 let query = format!(
487 r#"SELECT {} FROM {}."{}"#,
488 #field_names_str, #schema, #table
489 );
490 let rows = client.query(&query, &[]).await?;
491 let results: Vec<Self> = rows
492 .into_iter()
493 .map(Self::convert_from_row)
494 .filter(|record| filter_values.contains(&field_extractor(record)))
495 .collect();
496
497 Ok(results)
498 }
499 }
500 };
501
502 TokenStream::from(expanded)
503}