clickhouse_client_macros/
lib.rs

1//! Macros for `clickhouse-client`
2//!
3//! # [derive(AsChRecord)]
4//!
5//! This macro parses a struct and implements the trait `clickhouse-client::orm::ChRecord`
6
7use proc_macro::TokenStream;
8use proc_macro_error::{abort, proc_macro_error, OptionExt};
9use quote::quote;
10use syn::{parse_macro_input, spanned::Spanned, Field, Ident, ItemStruct, LitBool, LitStr};
11
12/// A macro to derive the trait `ChRecord`
13///
14/// # Prerequisites
15///
16/// - each field type must implement the trait `ChValue` to map to a DB type
17/// - The following types must be in scope:
18///     - `Record`
19///     - `ChValue`
20///     - `Value
21///     - `Type`
22///     - `TableSchema`
23///     - `Error`
24///
25/// # Attributes
26///
27/// This macro accepts struct and field level attribute called `ch`.
28///
29/// ## Struct level attributes:
30/// - **table**: table name (mandatory)
31///
32/// ## Field level attributes:
33/// - **name**: column name (optional)
34/// - **primary_key**: indicates a primary key (optional)
35/// - **skip**: field is skipped (optional)
36///
37/// # Example
38///
39/// ```ignore
40/// #[derive(AsChRecord)]
41/// #[ch(table = "my_table")]
42/// struct MyRecord {
43///   #[ch(primary_key)]
44///   id: u32,
45///   #[ch(name = "id")]
46///   id: u32,
47///   #[ch(skip)]
48///   other: String,
49/// }
50/// ```
51#[proc_macro_error]
52#[proc_macro_derive(AsChRecord, attributes(ch))]
53pub fn derive_db_record(input: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(input as ItemStruct);
55
56    let ident = &input.ident;
57    let fields = &input.fields;
58    // let attrs = &input.attrs;
59    // let vis = &input.vis;
60    // let generics = &input.generics;
61    // let semi_token = &input.semi_token;
62
63    // parse struct attributes
64    let attrs = StructAttrs::parse("ch", &input);
65    // eprintln!("struct_attrs= {:#?}", struct_attrs);
66    let table_name = attrs.table_name;
67
68    // parse fields
69    let mut schema_entries = vec![];
70    let mut into_record_entries = vec![];
71    let mut from_record_entries = vec![];
72
73    let mut has_primary_key = false;
74    for field in fields {
75        let attrs = FieldAttrs::parse("ch", field);
76        // eprintln!("field_attrs= {:#?}", field_attrs);
77
78        let field_id = &attrs.field_id;
79        let field_type = &field.ty;
80        let col_name = &attrs.col_name;
81        let col_primary = &attrs.primary;
82
83        if !attrs.skip.value {
84            // .column("id", Uuid::ch_type(), true)
85            schema_entries.push(quote! {
86                .column(#col_name, <#field_type>::ch_type(), #col_primary)
87            });
88
89            // .field("id", true, Uuid::ch_type(), self.id.into_ch_value())
90            into_record_entries.push(quote! {
91                .field(#col_name, #col_primary, <#field_type>::ch_type(), self.#field_id.into_ch_value())
92            });
93
94            // id: match record.remove_field("id") {
95            //     Some(field) => ::uuid::Uuid::from_ch_value(field.value)?,
96            //     None => return Err(Error::new("Missing field 'id'")),
97            // },
98            from_record_entries.push(quote! {
99                #field_id: match record.remove_field(#col_name) {
100                    Some(field) => <#field_type>::from_ch_value(field.value)?,
101                    None => return Err(Error::new(format!("Missing field '{}'", #col_name).as_str())),
102                }
103            });
104
105            if col_primary.value {
106                has_primary_key = true;
107            }
108        }
109    }
110
111    // check that there is at least 1 primary key
112    if !has_primary_key {
113        abort!(input.span(), "There must at least 1 primary key");
114    }
115
116    quote! {
117        impl ChRecord for #ident {
118            fn ch_schema() -> TableSchema {
119                TableSchema::new(#table_name)
120                    #(#schema_entries)*
121            }
122
123            fn into_ch_record(self) -> Record {
124                Record::new(#table_name)
125                    #(#into_record_entries)*
126            }
127
128            fn from_ch_record(mut record: Record) -> Result<Self, Error> {
129                Ok(Self {
130                    #(#from_record_entries),*
131                })
132            }
133        }
134    }
135    .into()
136}
137
138/// Struct attributes
139struct StructAttrs {
140    table_name: LitStr,
141}
142
143impl StructAttrs {
144    /// Parses the struct attribute
145    fn parse(attr_key: &str, item: &ItemStruct) -> Self {
146        let mut table_name: Option<LitStr> = None;
147
148        let attrs = &item.attrs;
149        for attr in attrs.iter() {
150            // eprintln!("ATTR: {:#?}", attr);
151            match &attr.meta {
152                syn::Meta::Path(_) => continue,
153                syn::Meta::NameValue(_) => continue,
154                syn::Meta::List(list) => {
155                    if list.path.is_ident(attr_key) {
156                        let tokens = list.tokens.to_string();
157                        for part in tokens.split(',') {
158                            match part.trim().split_once('=') {
159                                Some((key, val)) => {
160                                    let key = key.trim();
161                                    let val = val.trim();
162                                    // eprintln!("XXXX val={:?}", val);
163                                    if val.is_empty() {
164                                        list.tokens.span();
165                                        abort!(list.tokens.span(), "missing value");
166                                    }
167
168                                    match key {
169                                        "table" => {
170                                            let val_lit = match syn::parse_str::<LitStr>(val) {
171                                                Ok(ok) => ok,
172                                                Err(_) => {
173                                                    abort!(
174                                                        list.tokens.span(),
175                                                        "value must be quoted"
176                                                    );
177                                                }
178                                            };
179                                            if val_lit.value().is_empty() {
180                                                abort!(list.tokens.span(), "value is empty");
181                                            }
182                                            table_name = Some(val_lit);
183                                        }
184                                        _ => {
185                                            abort!(
186                                                list.tokens.span(),
187                                                "invalid key (valid: table)"
188                                            );
189                                        }
190                                    }
191                                }
192                                None => {
193                                    abort!(
194                                        list.tokens.span(),
195                                        "invalid attribute (must be key=\"val\")"
196                                    );
197                                }
198                            };
199                        }
200                    }
201                }
202            }
203        }
204
205        Self {
206            table_name: table_name.expect_or_abort("missing attribute 'table'"),
207        }
208    }
209}
210
211/// Field attributes
212struct FieldAttrs {
213    field_id: Ident,
214    col_name: LitStr,
215    skip: LitBool,
216    primary: LitBool,
217}
218
219impl FieldAttrs {
220    /// Parses a field
221    fn parse(attr_key: &str, field: &Field) -> Self {
222        let field_id = field.ident.clone().expect_or_abort("missing struct field");
223        let mut col_name = LitStr::new(field_id.to_string().as_str(), field.span());
224        let mut skip = LitBool::new(false, field.span());
225        let mut primary = LitBool::new(false, field.span());
226
227        for attr in field.attrs.iter() {
228            // eprintln!("ATTR: {:#?}", attr);
229            match &attr.meta {
230                syn::Meta::Path(_) => continue,
231                syn::Meta::NameValue(_) => continue,
232                syn::Meta::List(list) => {
233                    if list.path.is_ident(attr_key) {
234                        let tokens = list.tokens.to_string();
235                        for part in tokens.split(',') {
236                            match part {
237                                "skip" => {
238                                    skip = LitBool::new(true, list.tokens.span());
239                                    continue;
240                                }
241                                "primary_key" => {
242                                    primary = LitBool::new(true, list.tokens.span());
243                                    continue;
244                                }
245                                _ => {}
246                            }
247
248                            match part.trim().split_once('=') {
249                                Some((key, val)) => {
250                                    let key = key.trim();
251                                    let val = val.trim();
252                                    // eprintln!("YYYY val={:?}", val);
253                                    if val.is_empty() {
254                                        list.tokens.span();
255                                        abort!(list.tokens.span(), "missing value");
256                                    }
257
258                                    match key {
259                                        "name" => {
260                                            let val_lit = match syn::parse_str::<LitStr>(val) {
261                                                Ok(ok) => ok,
262                                                Err(_) => {
263                                                    abort!(
264                                                        list.tokens.span(),
265                                                        "value must be quoted"
266                                                    );
267                                                }
268                                            };
269                                            if val_lit.value().is_empty() {
270                                                abort!(list.tokens.span(), "value is empty");
271                                            }
272                                            col_name = val_lit;
273                                        }
274                                        _ => {
275                                            abort!(list.tokens.span(), "invalid key (valid: name)");
276                                        }
277                                    }
278                                }
279                                None => {
280                                    abort!(
281                                        list.tokens.span(),
282                                        "invalid attribute (must be key=\"val\")"
283                                    );
284                                }
285                            };
286                        }
287                    }
288                }
289            }
290        }
291
292        // end of 'db' attribute
293        Self {
294            field_id,
295            col_name,
296            skip,
297            primary,
298        }
299    }
300}