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}