quick_oxibooks_sql_macro/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 Ident, LitInt, Token, Type,
5 parse::{Parse, ParseStream},
6 punctuated::Punctuated,
7};
8
9#[proc_macro]
95pub fn qb_sql(input: TokenStream) -> TokenStream {
96 let query = syn::parse_macro_input!(input as SqlQuery);
97 let expanded = query.expand();
98 TokenStream::from(expanded)
99}
100
101struct SqlQuery {
103 fields: FieldSelection,
104 item_type: Type,
105 conditions: Vec<Condition>,
106 order_by: Option<OrderBy>,
107 limit: Option<LimitClause>,
108}
109
110enum FieldSelection {
112 All,
113 Specific(Vec<Ident>),
114}
115
116struct Condition {
118 field: Ident,
119 operator: Operator,
120 values: Vec<syn::Expr>,
121}
122
123enum Operator {
125 Equal,
126 Less,
127 Greater,
128 LessEqual,
129 GreaterEqual,
130 In,
131 Like,
132}
133
134struct OrderBy {
136 orders: Vec<OrderField>,
137}
138
139struct OrderField {
140 field: Ident,
141 direction: Option<OrderDirection>,
142}
143
144enum OrderDirection {
145 Asc,
146 Desc,
147}
148
149struct LimitClause {
151 number: LitInt,
152 offset: Option<syn::Expr>,
153}
154
155impl Parse for SqlQuery {
156 fn parse(input: ParseStream) -> syn::Result<Self> {
157 input.parse::<kw::select>()?;
159
160 let fields = if input.peek(Token![*]) {
162 input.parse::<Token![*]>()?;
163 FieldSelection::All
164 } else {
165 let field_list = Punctuated::<Ident, Token![,]>::parse_separated_nonempty(input)?;
166 FieldSelection::Specific(field_list.into_iter().collect())
167 };
168
169 input.parse::<kw::from>()?;
171 let item_type: Type = input.parse()?;
172
173 let mut conditions = vec![];
174
175 if input.peek(Token![where]) {
176 input.parse::<Token![where]>()?;
178 conditions.push(Condition::parse(input)?);
180 while input.peek(kw::and) {
182 input.parse::<kw::and>()?;
183 conditions.push(Condition::parse(input)?);
184 }
185 }
186
187 let order_by = if input.peek(kw::order) {
189 Some(OrderBy::parse(input)?)
190 } else {
191 None
192 };
193
194 let limit = if input.peek(kw::limit) {
196 Some(LimitClause::parse(input)?)
197 } else {
198 None
199 };
200
201 Ok(SqlQuery {
202 fields,
203 item_type,
204 conditions,
205 order_by,
206 limit,
207 })
208 }
209}
210
211impl Parse for Condition {
212 fn parse(input: ParseStream) -> syn::Result<Self> {
213 let field: Ident = input.parse()?;
214 let operator = Operator::parse(input)?;
215
216 let values = if matches!(operator, Operator::In) {
217 let content;
219 syn::parenthesized!(content in input);
220 let exprs = Punctuated::<syn::Expr, Token![,]>::parse_separated_nonempty(&content)?;
221 exprs.into_iter().collect()
222 } else {
223 vec![input.parse()?]
225 };
226
227 Ok(Condition {
228 field,
229 operator,
230 values,
231 })
232 }
233}
234
235impl Parse for Operator {
236 fn parse(input: ParseStream) -> syn::Result<Self> {
237 let lookahead = input.lookahead1();
238
239 if lookahead.peek(Token![=]) {
240 input.parse::<Token![=]>()?;
241 Ok(Operator::Equal)
242 } else if lookahead.peek(Token![<]) {
243 input.parse::<Token![<]>()?;
244 if input.peek(Token![=]) {
245 input.parse::<Token![=]>()?;
246 Ok(Operator::LessEqual)
247 } else {
248 Ok(Operator::Less)
249 }
250 } else if lookahead.peek(Token![>]) {
251 input.parse::<Token![>]>()?;
252 if input.peek(Token![=]) {
253 input.parse::<Token![=]>()?;
254 Ok(Operator::GreaterEqual)
255 } else {
256 Ok(Operator::Greater)
257 }
258 } else if lookahead.peek(Token![in]) {
259 input.parse::<Token![in]>()?;
260 Ok(Operator::In)
261 } else if lookahead.peek(kw::like) {
262 input.parse::<kw::like>()?;
263 Ok(Operator::Like)
264 } else {
265 Err(lookahead.error())
266 }
267 }
268}
269
270impl Parse for OrderBy {
271 fn parse(input: ParseStream) -> syn::Result<Self> {
272 input.parse::<kw::order>()?;
273 input.parse::<kw::by>()?;
274
275 let orders = Punctuated::<OrderField, Token![,]>::parse_separated_nonempty(input)?;
276
277 Ok(OrderBy {
278 orders: orders.into_iter().collect(),
279 })
280 }
281}
282
283impl Parse for OrderField {
284 fn parse(input: ParseStream) -> syn::Result<Self> {
285 let field: Ident = input.parse()?;
286
287 let direction = if input.peek(kw::asc) {
288 input.parse::<kw::asc>()?;
289 Some(OrderDirection::Asc)
290 } else if input.peek(kw::desc) {
291 input.parse::<kw::desc>()?;
292 Some(OrderDirection::Desc)
293 } else {
294 None
295 };
296
297 Ok(OrderField { field, direction })
298 }
299}
300
301impl Parse for LimitClause {
302 fn parse(input: ParseStream) -> syn::Result<Self> {
303 input.parse::<kw::limit>()?;
304 let number: LitInt = input.parse()?;
305
306 let offset = if input.peek(kw::offset) {
307 input.parse::<kw::offset>()?;
308 Some(input.parse()?)
309 } else {
310 None
311 };
312
313 Ok(LimitClause { number, offset })
314 }
315}
316
317impl SqlQuery {
318 fn expand(&self) -> proc_macro2::TokenStream {
319 let item_type = &self.item_type;
320
321 let all_fields: Vec<&Ident> = {
323 let mut fields = Vec::new();
324
325 if let FieldSelection::Specific(ref select_fields) = self.fields {
326 fields.extend(select_fields.iter());
327 }
328
329 fields.extend(self.conditions.iter().map(|c| &c.field));
330
331 if let Some(ref order_by) = self.order_by {
332 fields.extend(order_by.orders.iter().map(|o| &o.field));
333 }
334
335 fields
336 };
337
338 let type_check = if !all_fields.is_empty() {
340 quote! {
341 const _: () = {
342 fn _check_fields(v: #item_type) {
343 #(let _ = v.#all_fields;)*
344 }
345 };
346 }
347 } else {
348 quote! {}
349 };
350
351 let field_code = match &self.fields {
353 FieldSelection::All => quote! {},
354 FieldSelection::Specific(fields) => {
355 let field_names: Vec<_> = fields
356 .iter()
357 .map(|f| {
358 let name = to_camel_case(&f.to_string());
359 quote! { stringify!(#name) }
360 })
361 .collect();
362
363 quote! {
364 #(
365 unsafe {
366 query = query.field(#field_names);
367 }
368 )*
369 }
370 }
371 };
372
373 let condition_code: Vec<_> = self
375 .conditions
376 .iter()
377 .map(|c| {
378 let field = &c.field;
379 let field_name = to_camel_case(&field.to_string());
380 let operator = c.operator.to_tokens();
381 let values = &c.values;
382
383 let values_code = if matches!(c.operator, Operator::In) && values.len() == 1 {
385 let expr = &values[0];
386 quote! {
387 #expr.into_iter().map(|v| v.to_string()).collect::<Vec<String>>()
388 }
389 } else {
390 quote! { vec![#(#values.to_string()),*] }
392 };
393
394 quote! {
395 let clause = WhereClause {
396 field: stringify!(#field_name),
397 operator: #operator,
398 values: #values_code,
399 };
400 unsafe {
401 query = query.condition(clause);
402 }
403 }
404 })
405 .collect();
406
407 let order_code = if let Some(ref order_by) = self.order_by {
409 let orders: Vec<_> = order_by
410 .orders
411 .iter()
412 .map(|o| {
413 let field = &o.field;
414 let field_name = to_camel_case(&field.to_string());
415 let direction = match &o.direction {
416 Some(OrderDirection::Asc) => quote! { Order::Asc },
417 Some(OrderDirection::Desc) => quote! { Order::Desc },
418 None => quote! { Order::Asc },
419 };
420
421 quote! {
422 unsafe {
423 query = query.order(stringify!(#field_name), #direction);
424 }
425 }
426 })
427 .collect();
428
429 quote! { #(#orders)* }
430 } else {
431 quote! {}
432 };
433
434 let limit_code = if let Some(ref limit) = self.limit {
436 let number = &limit.number;
437 let offset_code = if let Some(ref offset) = limit.offset {
438 quote! { Some(#offset) }
439 } else {
440 quote! { None }
441 };
442
443 quote! {
444 query = query.limit(#number, #offset_code);
445 }
446 } else {
447 quote! {}
448 };
449
450 quote! {
451 {
452 #type_check
453
454 let mut query = Query::<#item_type>::new();
455
456 #field_code
457 #(#condition_code)*
458 #order_code
459 #limit_code
460
461 query
462 }
463 }
464 }
465}
466
467impl Operator {
468 fn to_tokens(&self) -> proc_macro2::TokenStream {
469 match self {
470 Operator::Equal => quote! { Operator::Equal },
471 Operator::Less => quote! { Operator::Less },
472 Operator::Greater => quote! { Operator::Greater },
473 Operator::LessEqual => quote! { Operator::LessEqual },
474 Operator::GreaterEqual => quote! { Operator::GreaterEqual },
475 Operator::In => quote! { Operator::In },
476 Operator::Like => quote! { Operator::Like },
477 }
478 }
479}
480
481fn to_camel_case(s: &str) -> syn::Ident {
483 let camel = s
484 .split('_')
485 .map(|word| {
486 let mut chars = word.chars();
487 match chars.next() {
488 None => String::new(),
489 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
490 }
491 })
492 .collect::<String>();
493
494 syn::Ident::new(&camel, proc_macro2::Span::call_site())
495}
496
497mod kw {
499 syn::custom_keyword!(select);
500 syn::custom_keyword!(from);
501 syn::custom_keyword!(and);
502 syn::custom_keyword!(order);
503 syn::custom_keyword!(by);
504 syn::custom_keyword!(limit);
505 syn::custom_keyword!(offset);
506 syn::custom_keyword!(asc);
507 syn::custom_keyword!(desc);
508 syn::custom_keyword!(like);
509}