prest_db_macro/
lib.rs

1mod analyze;
2mod expand;
3mod from_glue_value;
4mod into_glue_expr;
5
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::{quote as q, ToTokens};
9use syn::{
10    parse_macro_input, parse_quote, Data, DataStruct, DeriveInput, Expr, Field, Fields, Ident, Type,
11};
12
13pub(crate) use gluesql_core::ast::DataType as SqlType;
14use SqlType::*;
15
16/// Generates schema and helper functions to use struct as a table in the embedded database
17#[proc_macro_derive(Table, attributes(pkey_column, unique_column))]
18pub fn table_derive(input: TokenStream) -> TokenStream {
19    let ast = parse_macro_input!(input as DeriveInput);
20    let struct_ident = ast.ident;
21    let table_name = struct_ident.to_string() + "s";
22
23    // supports only struct with named fields
24    let fields = match ast.data {
25        Data::Struct(DataStruct {
26            fields: Fields::Named(it),
27            ..
28        }) => it,
29        _ => panic!("Expected a `struct` with named fields"),
30    };
31
32    // decompose
33    let mut columns: Vec<Column> = fields.named.into_iter().map(analyze::from_field).collect();
34
35    // can't have multiple primary keys
36    match columns.iter().filter(|c| c.pkey).count() {
37        0 => {
38            columns[0].pkey = true;
39            columns[0].unique = true;
40        }
41        1 => {}
42        _ => panic!("Table macro doesn't support more than one pkey at the moment"),
43    };
44
45    // expand
46    TokenStream::from(expand::impl_table(struct_ident, table_name, columns))
47}
48
49struct Column {
50    field_name: Ident,
51    field_name_str: String,
52    full_type: Type,
53    full_type_str: String,
54    // type inside Option or Vec
55    inner_type: Type,
56    // type in sql syntax (gluesql_core::ast::DataType)
57    sql_type: SqlType,
58    // is primary pkey
59    pkey: bool,
60    // is Option<...>
61    optional: bool,
62    // is Vec<...>
63    list: bool,
64    // should be UNIQUE
65    unique: bool,
66    // requires serialization/deserialization
67    serialized: bool,
68}
69
70impl Column {
71    fn from_row_transform(&self) -> FromRowTransform {
72        match self.sql_type {
73            Uuid => FromRowTransform::UuidFromU128,
74            _ if self.serialized => FromRowTransform::Deserialize,
75            _ => FromRowTransform::None,
76        }
77    }
78
79    fn value_variant(&self) -> &str {
80        match self.sql_type {
81            Uuid => "Uuid",
82            Text => "Str",
83            Timestamp => "Timestamp",
84            Boolean => "Bool",
85            Uint128 => "U128",
86            Uint64 => "U64",
87            Uint32 => "U32",
88            Uint16 => "U16",
89            Uint8 => "U8",
90            Int128 => "I128",
91            Int => "I64",
92            Int32 => "I32",
93            Int16 => "I16",
94            Int8 => "I8",
95            Float32 => "F32",
96            Float => "F64",
97            _ => "Str",
98        }
99    }
100}
101
102enum FromRowTransform {
103    UuidFromU128,
104    Deserialize,
105    None,
106}
107
108fn ident(name: &str) -> Ident {
109    Ident::new(name, Span::call_site())
110}
111
112trait TypeProps {
113    fn impl_into_exprnode(&self) -> bool;
114    fn int_or_smaller(&self) -> bool;
115    fn integer(&self) -> bool;
116    fn numeric(&self) -> bool;
117    fn comparable(&self) -> bool;
118    fn quoted(&self) -> bool;
119}
120
121impl TypeProps for SqlType {
122    fn impl_into_exprnode(&self) -> bool {
123        matches!(self, Boolean | Int)
124    }
125    fn int_or_smaller(&self) -> bool {
126        matches!(self, Uint32 | Uint16 | Uint8 | Int | Int32 | Int16 | Int8)
127    }
128    fn integer(&self) -> bool {
129        self.int_or_smaller() || matches!(self, Uint128 | Uint64 | Int128)
130    }
131    fn numeric(&self) -> bool {
132        self.integer() || matches!(self, Float | Float32)
133    }
134    fn comparable(&self) -> bool {
135        self.numeric() || matches!(self, Timestamp | Date | Time)
136    }
137    fn quoted(&self) -> bool {
138        !self.impl_into_exprnode() && !self.numeric()
139    }
140}
141
142impl TypeProps for Column {
143    fn impl_into_exprnode(&self) -> bool {
144        self.sql_type.impl_into_exprnode()
145    }
146    fn int_or_smaller(&self) -> bool {
147        self.sql_type.int_or_smaller()
148    }
149    fn integer(&self) -> bool {
150        self.sql_type.integer()
151    }
152    fn numeric(&self) -> bool {
153        self.sql_type.numeric()
154    }
155    fn comparable(&self) -> bool {
156        self.sql_type.comparable()
157    }
158    fn quoted(&self) -> bool {
159        self.sql_type.quoted()
160    }
161}
162
163fn column_schema(col: &Column) -> proc_macro2::TokenStream {
164    let Column {
165        field_name_str,
166        full_type_str,
167        sql_type,
168        pkey,
169        unique,
170        list,
171        optional,
172        serialized,
173        ..
174    } = col;
175    let numeric = sql_type.numeric();
176    let comparable = sql_type.comparable();
177    let sql_type = sql_type.to_string();
178    q! {
179        ColumnSchema {
180            name: #field_name_str,
181            rust_type: #full_type_str,
182            sql_type: #sql_type,
183            unique: #unique,
184            pkey: #pkey,
185            list: #list,
186            optional: #optional,
187            serialized: #serialized,
188            numeric: #numeric,
189            comparable: #comparable,
190        }
191    }
192}