sui_sql_macro/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
Error, Expr, LitStr, Result, Token, Type,
};
use crate::{
lexer::Lexer,
parser::{Format, Parser},
};
mod lexer;
mod parser;
/// Rust syntax for `sql!(as T, "format", binds,*)`
struct SqlInput {
return_: Type,
format_: LitStr,
binds: Punctuated<Expr, Token![,]>,
}
/// Rust syntax for `query!("format", binds,*)`.
struct QueryInput {
format_: LitStr,
binds: Punctuated<Expr, Token![,]>,
}
impl Parse for SqlInput {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Token![as]>()?;
let return_ = input.parse()?;
input.parse::<Token![,]>()?;
let format_ = input.parse()?;
if input.is_empty() {
return Ok(Self {
return_,
format_,
binds: Punctuated::new(),
});
}
input.parse::<Token![,]>()?;
let binds = Punctuated::parse_terminated(input)?;
Ok(Self {
return_,
format_,
binds,
})
}
}
impl Parse for QueryInput {
fn parse(input: ParseStream) -> Result<Self> {
let format_ = input.parse()?;
if input.is_empty() {
return Ok(Self {
format_,
binds: Punctuated::new(),
});
}
input.parse::<Token![,]>()?;
let binds = Punctuated::parse_terminated(input)?;
Ok(Self { format_, binds })
}
}
/// The `sql!` macro is used to construct a `diesel::SqlLiteral<T>` using a format string to
/// describe the SQL snippet with the following syntax:
///
/// ```rust,ignore
/// sql!(as T, "format", binds,*)
/// ```
///
/// `T` is the `SqlType` that the literal will be interpreted as, as a Rust expression. The format
/// string introduces binders with curly braces, surrounding the `SqlType` of the bound value. This
/// type is given as a string which must correspond to a type in the `diesel::sql_types` module.
/// Bound values follow in the order matching their binders in the string:
///
/// ```rust,ignore
/// sql!(as Bool, "{BigInt} <= foo AND foo < {BigInt}", 5, 10)
/// ```
///
/// The above macro invocation will generate the following code:
///
/// ```rust,ignore
/// sql::<Bool>("")
/// .bind::<BigInt, _>(5)
/// .sql(" <= foo AND foo < ")
/// .bind::<BigInt, _>(10)
/// .sql("")
/// ```
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let SqlInput {
return_,
format_,
binds,
..
} = parse_macro_input!(input as SqlInput);
let format_str = format_.value();
let lexemes: Vec<_> = Lexer::new(&format_str).collect();
let Format { head, tail } = match Parser::new(&lexemes).format() {
Ok(format) => format,
Err(err) => {
return Error::new(format_.span(), err).into_compile_error().into();
}
};
let mut tokens = quote! {
::diesel::dsl::sql::<#return_>(#head)
};
for (expr, (ty, suffix)) in binds.iter().zip(tail.into_iter()) {
tokens.extend(if let Some(ty) = ty {
quote! {
.bind::<::diesel::sql_types::#ty, _>(#expr)
.sql(#suffix)
}
} else {
// No type was provided for the bind parameter, so we use `Untyped` which will report
// an error because it doesn't implement `SqlType`.
quote! {
.bind::<::diesel::sql_types::Untyped, _>(#expr)
.sql(#suffix)
}
});
}
tokens.into()
}
/// The `query!` macro constructs a value that implements `diesel::query_builder::Query` -- a full
/// SQL query, defined by a format string and binds with the following syntax:
///
/// ```rust,ignore
/// query!("format", binds,*)
/// ```
///
/// The format string introduces binders with curly braces. An empty binder interpolates another
/// query at that position, otherwise the binder is expected to contain a `SqlType` for a value
/// that will be bound into the query, given a a string which must correspond to a type in the
/// `diesel::sql_types` module. Bound values or queries to interpolate follow in the order matching
/// their binders in the string:
///
/// ```rust,ignore
/// query!("SELECT * FROM foo WHERE {BigInt} <= cursor AND {}", 5, query!("cursor < {BigInt}", 10))
/// ```
///
/// The above macro invocation will generate the following SQL query:
///
/// ```sql
/// SELECT * FROM foo WHERE $1 <= cursor AND cursor < $2 -- binds [5, 10]
/// ```
#[proc_macro]
pub fn query(input: TokenStream) -> TokenStream {
let QueryInput { format_, binds } = parse_macro_input!(input as QueryInput);
let format_str = format_.value();
let lexemes: Vec<_> = Lexer::new(&format_str).collect();
let Format { head, tail } = match Parser::new(&lexemes).format() {
Ok(format) => format,
Err(err) => {
return Error::new(format_.span(), err).into_compile_error().into();
}
};
let mut tokens = quote! {
::sui_pg_db::query::Query::new(#head)
};
for (expr, (ty, suffix)) in binds.iter().zip(tail.into_iter()) {
tokens.extend(if let Some(ty) = ty {
// If there is a type, this interpolation is for a bind.
quote! {
.bind::<::diesel::sql_types::#ty, _>(#expr)
.sql(#suffix)
}
} else {
// Otherwise, we are interpolating another query.
quote! {
.query(#expr)
.sql(#suffix)
}
});
}
tokens.into()
}