1#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11
12mod describe;
13
14use proc_macro::TokenStream;
15use quote::quote;
16use syn::{
17 Expr, Lit, Token, Type,
18 parse::{Parse, ParseStream},
19 parse_macro_input,
20 punctuated::Punctuated,
21};
22
23struct QueryInput {
24 sql: Expr,
25 args: Punctuated<Expr, Token![,]>,
26}
27
28impl Parse for QueryInput {
29 fn parse(input: ParseStream) -> syn::Result<Self> {
30 let sql: Expr = input.parse()?;
31 let args = parse_trailing_args(input)?;
32 Ok(QueryInput {
33 sql,
34 args,
35 })
36 }
37}
38
39struct QueryAsInput {
40 ty: Type,
41 sql: Expr,
42 args: Punctuated<Expr, Token![,]>,
43}
44
45impl Parse for QueryAsInput {
46 fn parse(input: ParseStream) -> syn::Result<Self> {
47 let ty: Type = input.parse()?;
48 input.parse::<Token![,]>()?;
49 let sql: Expr = input.parse()?;
50 let args = parse_trailing_args(input)?;
51 Ok(QueryAsInput {
52 ty,
53 sql,
54 args,
55 })
56 }
57}
58
59fn parse_trailing_args(input: ParseStream) -> syn::Result<Punctuated<Expr, Token![,]>> {
60 if input.is_empty() {
61 return Ok(Punctuated::new());
62 }
63 input.parse::<Token![,]>()?;
64 if input.is_empty() {
65 return Ok(Punctuated::new());
66 }
67 Punctuated::parse_terminated(input)
68}
69
70#[proc_macro]
72pub fn query(input: TokenStream) -> TokenStream {
73 let input = parse_macro_input!(input as QueryInput);
74 match expand(input.sql, input.args, None) {
75 Ok(tokens) => tokens.into(),
76 Err(err) => err.to_compile_error().into(),
77 }
78}
79
80#[proc_macro]
82pub fn query_as(input: TokenStream) -> TokenStream {
83 let input = parse_macro_input!(input as QueryAsInput);
84 match expand(input.sql, input.args, Some(input.ty)) {
85 Ok(tokens) => tokens.into(),
86 Err(err) => err.to_compile_error().into(),
87 }
88}
89
90fn checking_enabled() -> Option<String> {
91 if let Ok(flag) = std::env::var("STDX_POSTGRESQL_CHECK") {
92 if flag.eq_ignore_ascii_case("false") {
93 return None;
94 }
95 }
96 std::env::var("DATABASE_URL").ok().filter(|url| !url.is_empty())
97}
98
99fn sql_literal(sql: &Expr) -> Option<String> {
100 if let Expr::Lit(lit) = sql {
101 if let Lit::Str(s) = &lit.lit {
102 return Some(s.value());
103 }
104 }
105 None
106}
107
108fn expand(sql: Expr, args: Punctuated<Expr, Token![,]>, target: Option<Type>) -> syn::Result<proc_macro2::TokenStream> {
109 let described = match (checking_enabled(), sql_literal(&sql)) {
110 (Some(url), Some(text)) => Some(
111 describe::describe(&url, &text).map_err(|e| syn::Error::new_spanned(&sql, format!("postgresql: {e}")))?,
112 ),
113 _ => None,
114 };
115
116 if let Some(described) = &described {
117 if described.param_oids.len() != args.len() {
118 return Err(syn::Error::new_spanned(
119 &sql,
120 format!(
121 "postgresql: query expects {} parameter(s) but {} were provided",
122 described.param_oids.len(),
123 args.len()
124 ),
125 ));
126 }
127 }
128
129 let params: Vec<proc_macro2::TokenStream> = args
130 .iter()
131 .enumerate()
132 .map(|(index, arg)| match &described {
133 Some(described) => {
134 let oid = described.param_oids[index];
135 quote! {
136 ::postgresql::__private::param::<#oid, _>(&(#arg)) as &dyn ::postgresql::ToSql
137 }
138 }
139 None => quote! {
140 &(#arg) as &dyn ::postgresql::ToSql
141 },
142 })
143 .collect();
144
145 let params_slice = quote! { &[ #(#params),* ] };
146
147 let tokens = match target {
148 Some(ty) => {
149 let base = quote! {
150 ::postgresql::QueryAs::<#ty>::new(#sql, #params_slice)
151 };
152 match &described {
153 Some(described) => {
154 let specs = described.columns.iter().map(|column| {
155 let name = column.name.as_str();
156 let oid = column.type_oid;
157 let nullable = column.nullable;
158 let known = column.nullable_known;
159 quote! {
160 ::postgresql::ColumnSpec {
161 name: #name,
162 type_oid: #oid,
163 nullable: #nullable,
164 known: #known,
165 },
166 }
167 });
168 quote! {
169 (
170 const {
171 ::postgresql::__private::verify_shape(
172 <#ty as ::postgresql::RowShape>::SHAPE,
173 &[ #(#specs)* ],
174 )
175 },
176 #base,
177 ).1
178 }
179 }
180 None => base,
181 }
182 }
183 None => quote! {
184 ::postgresql::Query::new(#sql, #params_slice)
185 },
186 };
187 Ok(tokens)
188}
189
190struct CopyInput {
191 conn: Expr,
192 ty: Type,
193 sql: Expr,
194}
195
196impl Parse for CopyInput {
197 fn parse(input: ParseStream) -> syn::Result<Self> {
198 let conn: Expr = input.parse()?;
199 input.parse::<Token![,]>()?;
200 let ty: Type = input.parse()?;
201 input.parse::<Token![,]>()?;
202 let sql: Expr = input.parse()?;
203 Ok(CopyInput {
204 conn,
205 ty,
206 sql,
207 })
208 }
209}
210
211#[proc_macro]
215pub fn copy_in(input: TokenStream) -> TokenStream {
216 let input = parse_macro_input!(input as CopyInput);
217 match expand_copy(input, false) {
218 Ok(tokens) => tokens.into(),
219 Err(err) => err.to_compile_error().into(),
220 }
221}
222
223#[proc_macro]
227pub fn copy_out(input: TokenStream) -> TokenStream {
228 let input = parse_macro_input!(input as CopyInput);
229 match expand_copy(input, true) {
230 Ok(tokens) => tokens.into(),
231 Err(err) => err.to_compile_error().into(),
232 }
233}
234
235fn expand_copy(input: CopyInput, out: bool) -> syn::Result<proc_macro2::TokenStream> {
236 let columns = match (checking_enabled(), sql_literal(&input.sql)) {
237 (Some(url), Some(text)) => Some(
238 describe::copy_columns(&url, &text)
239 .map_err(|e| syn::Error::new_spanned(&input.sql, format!("postgresql: {e}")))?,
240 ),
241 _ => None,
242 };
243
244 let ty = &input.ty;
245 let conn = &input.conn;
246 let sql = &input.sql;
247
248 let check = columns.as_ref().map(|columns| {
249 let specs = columns.iter().map(|column| {
250 let name = column.name.as_str();
251 let oid = column.type_oid;
252 let nullable = column.nullable;
253 let known = column.nullable_known;
254 quote! {
255 ::postgresql::ColumnSpec {
256 name: #name,
257 type_oid: #oid,
258 nullable: #nullable,
259 known: #known,
260 },
261 }
262 });
263 quote! {
264 const {
265 ::postgresql::__private::verify_shape(
266 <#ty as ::postgresql::RowShape>::SHAPE,
267 &[ #(#specs)* ],
268 )
269 }
270 }
271 });
272
273 let call = if out {
274 let oids: Vec<u32> = columns
275 .as_ref()
276 .map(|columns| columns.iter().map(|c| c.type_oid).collect())
277 .unwrap_or_default();
278 quote! {
279 ::postgresql::__private::copy_out::<#ty>(&(#conn), #sql, &[ #(#oids),* ])
280 }
281 } else {
282 quote! {
283 ::postgresql::__private::copy_in::<#ty>(&(#conn), #sql)
284 }
285 };
286
287 Ok(match check {
288 Some(check) => quote! { ( #check, #call ).1 },
289 None => call,
290 })
291}