Skip to main content

google_cloud_bigquery_derive/
lib.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Derive macros for the Google Cloud BigQuery client.
16
17extern crate proc_macro;
18
19use proc_macro::TokenStream;
20use quote::quote;
21use syn::{Data, DeriveInput, Fields, parse_macro_input};
22
23/// Derives standard library [TryFrom] for converting a BigQuery `Row` into a struct.
24///
25/// Supports renaming attributes via `#[bigquery(rename = "new_name")]`.
26///
27/// [TryFrom]: std::convert::TryFrom
28#[proc_macro_derive(FromRow, attributes(bigquery))]
29pub fn derive_from_row(input: TokenStream) -> TokenStream {
30    let input = parse_macro_input!(input as DeriveInput);
31    let name = input.ident;
32
33    let fields = match input.data {
34        Data::Struct(data) => match data.fields {
35            Fields::Named(fields) => fields.named,
36            _ => {
37                return syn::Error::new_spanned(
38                    name,
39                    "FromRow can only be derived for structs with named fields",
40                )
41                .to_compile_error()
42                .into();
43            }
44        },
45        _ => {
46            return syn::Error::new_spanned(name, "FromRow can only be derived for structs")
47                .to_compile_error()
48                .into();
49        }
50    };
51    let value_extractions = fields.iter().map(|f| {
52        let field_name = f.ident.as_ref().expect("named field must have identifier");
53        let db_column_name = get_field_name(f);
54        quote! {
55            let #field_name = row.take(#db_column_name)?;
56        }
57    });
58
59    let field_idents = fields
60        .iter()
61        .map(|f| f.ident.as_ref().expect("named field must have identifier"));
62
63    // TODO(#5592): check that the schema and this struct have same columns/attributes count.
64
65    let expanded = quote! {
66        impl std::convert::TryFrom<google_cloud_bigquery::query::Row> for #name {
67            type Error = google_cloud_bigquery::error::RowError;
68
69            fn try_from(mut row: google_cloud_bigquery::query::Row) -> std::result::Result<Self, Self::Error> {
70                #( #value_extractions )*
71
72                std::result::Result::Ok(Self {
73                    #( #field_idents, )*
74                })
75            }
76        }
77    };
78
79    expanded.into()
80}
81
82/// Derives `FromSql` for converting a BigQuery value into a struct.
83///
84/// Supports renaming attributes via `#[bigquery(rename = "new_name")]`.
85#[proc_macro_derive(FromSql, attributes(bigquery))]
86pub fn derive_from_sql(input: TokenStream) -> TokenStream {
87    let input = parse_macro_input!(input as DeriveInput);
88    let name = input.ident;
89
90    let fields = match input.data {
91        Data::Struct(data) => match data.fields {
92            Fields::Named(fields) => fields.named,
93            _ => {
94                return syn::Error::new_spanned(
95                    name,
96                    "FromSql can only be derived for structs with named fields",
97                )
98                .to_compile_error()
99                .into();
100            }
101        },
102        _ => {
103            return syn::Error::new_spanned(name, "FromSql can only be derived for structs")
104                .to_compile_error()
105                .into();
106        }
107    };
108
109    let field_idents_struct_array = fields
110        .iter()
111        .map(|f| f.ident.as_ref().expect("named field must have identifier"));
112    let field_idents_struct_obj = fields
113        .iter()
114        .map(|f| f.ident.as_ref().expect("named field must have identifier"));
115
116    let field_extractions_array = fields.iter().map(|f| {
117        let field_name = f.ident.as_ref().expect("named field must have identifier");
118        let db_column_name = get_field_name(f);
119        quote! {
120            let #field_name = iter.next()
121                .ok_or_else(|| google_cloud_bigquery::error::ConvertError::MissingField(#db_column_name.to_string()))?;
122            let #field_name = google_cloud_bigquery::query::FromSql::from_sql(#field_name)?;
123        }
124    });
125
126    let field_extractions_obj = fields.iter().map(|f| {
127        let field_name = f.ident.as_ref().expect("named field must have identifier");
128        let db_column_name = get_field_name(f);
129        quote! {
130            let #field_name = obj.remove(#db_column_name)
131                .ok_or_else(|| google_cloud_bigquery::error::ConvertError::MissingField(#db_column_name.to_string()))?;
132            let #field_name = google_cloud_bigquery::query::FromSql::from_sql(#field_name)?;
133        }
134    });
135
136    let expanded = quote! {
137        impl google_cloud_bigquery::query::FromSql for #name {
138            fn from_sql(value: wkt::Value) -> std::result::Result<Self, google_cloud_bigquery::error::ConvertError> {
139                match value {
140                    wkt::Value::Array(arr) => {
141                        let mut iter = arr.into_iter();
142                        #( #field_extractions_array )*
143                        std::result::Result::Ok(Self {
144                            #( #field_idents_struct_array, )*
145                        })
146                    }
147                    wkt::Value::Object(mut obj) => {
148                        #( #field_extractions_obj )*
149                        std::result::Result::Ok(Self {
150                            #( #field_idents_struct_obj, )*
151                        })
152                    }
153                    other => std::result::Result::Err(google_cloud_bigquery::error::ConvertError::TypeMismatch {
154                        expected: "array or object",
155                        got: other,
156                    }),
157                }
158            }
159        }
160    };
161
162    expanded.into()
163}
164
165fn get_field_name(field: &syn::Field) -> String {
166    for attr in &field.attrs {
167        if attr.path().is_ident("bigquery") {
168            let mut renamed = None;
169            let _ = attr.parse_nested_meta(|meta| {
170                if meta.path.is_ident("rename") {
171                    let value = meta.value()?;
172                    let lit: syn::LitStr = value.parse()?;
173                    renamed = Some(lit.value());
174                    Ok(())
175                } else {
176                    Err(meta.error("unsupported bigquery attribute"))
177                }
178            });
179            if let Some(name) = renamed {
180                return name;
181            }
182        }
183    }
184    field
185        .ident
186        .as_ref()
187        .expect("named field must have identifier")
188        .to_string()
189}