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
use quote::quote;
/// Generate the `Columns` code.
#[must_use]
pub fn code_gen(
parsed: &crate::parse::columns::Parsed<'_>,
mapping_paths: crate::mapping::columns::MappingPaths,
) -> proc_macro2::TokenStream {
let runtime = &mapping_paths.runtime;
let crate::parse::columns::Parsed {
ident,
generics,
columns,
} = parsed;
let domain_model = mapping_paths.domain_model;
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
let name = ident.to_string();
let domain_fields = columns.iter().map(|column| column.domain_field.to_string());
let column_names = columns.iter().map(|column| &column.name);
// A domain field named by `#[domain_field(name)]` must exist on the
// domain model; reported at the name. (Same-named ones need no check:
// a domain field without a column is caught where the model's fields
// are used, by the `queryweave_sql` SQL helpers.)
let fields_exist = crate::checks::assert_fields_exist(
&domain_model,
columns
.iter()
.filter(|column| column.is_mapped)
.map(|column| &column.domain_field),
);
// The `SET` writer, with merge patches only (it writes their data).
let assignments = if cfg!(feature = "merge-patch") {
quote! {
/// Push `column = value` assignments of the model `M` (a `MergePatch`
/// model's `changed_values`) for an `UPDATE … SET`, comma separated,
/// returning the separated builder so more can be added.
///
/// See the `Columns` trait's `push_assignments`.
pub fn push_assignments<'qb, 'args, DB, M>(
query_builder: &'qb mut #runtime::QueryBuilder<'args, DB>,
assignments: #runtime::FieldValues<M>,
) -> #runtime::Separated<'qb, 'args, DB, &'static str>
where
DB: #runtime::BindValues,
M: #runtime::PersistedFields,
{
const { #runtime::__private::assert_model_columns::<Self, M>() };
#runtime::__private::push_assignments::<Self, DB, M>(query_builder, assignments)
}
}
} else {
proc_macro2::TokenStream::new()
};
quote! {
impl #impl_generics #runtime::Columns for #ident #type_generics #where_clause {
const NAME: &'static str = #name;
const COLUMNS: &'static [(&'static str, &'static str)] = &[
#((#domain_fields, #column_names)),*
];
}
// The SQL methods again as inherent ones, so callers use them without
// importing `queryweave_sql::Columns`. Each runs the column check
// itself, so a missing column is reported at its caller (the line
// writing the data), then calls the unchecked writer.
impl #impl_generics #ident #type_generics #where_clause {
/// Push the conditions of the model `M` (a generated filter-options
/// model's `conditions()`) to a query, joined by `separator`, each
/// against the column this entity stores its domain field in.
///
/// See the `Columns` trait's `push_conditions`.
pub fn push_conditions<DB, M>(
query_builder: &mut #runtime::QueryBuilder<'_, DB>,
conditions: #runtime::Conditions<M>,
separator: &str,
) where
DB: #runtime::BindValues,
M: #runtime::PersistedFields,
{
const { #runtime::__private::assert_model_columns::<Self, M>() };
#runtime::__private::push_conditions::<Self, DB, M>(query_builder, conditions, separator);
}
/// Push an `ORDER BY` clause for the sort keys of the model `M` (a
/// generated query model's `sort_keys()`), each by the column this
/// entity stores its domain field in; nothing when there are none.
///
/// See the `Columns` trait's `push_order_by`.
pub fn push_order_by<DB, M>(
query_builder: &mut #runtime::QueryBuilder<'_, DB>,
sort_keys: #runtime::SortKeys<M>,
) where
DB: #runtime::BindValues,
M: #runtime::PersistedFields,
{
const { #runtime::__private::assert_model_columns::<Self, M>() };
#runtime::__private::push_order_by::<Self, DB, M>(query_builder, sort_keys);
}
#assignments
}
#fields_exist
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_code_gen() {
let syn::DeriveInput {
ident,
generics,
data,
attrs,
..
} = syn::parse2(quote::quote! {
#[derive(sqlx::FromRow, Columns)]
#[columns(model = domain::models::order::OrderModel)]
struct Order {
id: i32,
#[domain_field(delivery_date)]
delivered_at: DateTime<Utc>,
#[sqlx(rename = "order_number")]
number: String,
}
})
.unwrap();
let input = crate::parse::columns::Input::new(ident, generics, data, attrs);
let mapping_paths = crate::mapping::columns::MappingPaths {
domain_model: syn::parse_str("domain::models::order::OrderModel").unwrap(),
runtime: syn::parse_quote!(::queryweave_sql),
};
let token_stream = super::code_gen(
&crate::parse::columns::parse(&input).unwrap(),
mapping_paths,
);
insta::assert_snapshot!(prettyplease::unparse(
&syn::parse2::<syn::File>(token_stream).unwrap()
));
}
}