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