tusk_rs/query.rs
1use tokio_postgres::{types::ToSql, Row};
2
3pub trait IntoSyntax {
4 fn as_syntax(&self, local_table: &str) -> String;
5}
6impl IntoSyntax for &'static [&'static PostgresField] {
7 fn as_syntax(&self, local_table: &str) -> String {
8 let mut v = self.iter().map(|x| x.into_syntax(local_table)).fold(String::new(), |acc, x| acc + &x + ",");
9 v.pop();
10 v
11 }
12}
13impl IntoSyntax for &'static [&'static PostgresJoin] {
14 fn as_syntax(&self, local_table: &str) -> String {
15 let mut v = self.iter().map(|x| x.to_read(local_table)).fold(String::new(), |acc, x| acc + &x + " ");
16 v.pop();
17 v
18 }
19}
20
21/// A trait for getting a struct from a Postgres row.
22///
23/// This can be derived provided that each property also
24/// implements `FromPostgres`.
25pub trait FromPostgres {
26 fn from_postgres(row: &Row) -> Self;
27 fn try_from_postgres(row: &Row) -> Result<Self, FromPostgresError>
28 where
29 Self: Sized;
30}
31pub enum FromPostgresError {
32 InvalidType(&'static str),
33 MissingColumn(&'static str),
34}
35
36/// A struct that defines how Tusk should join
37/// tables for you.
38pub struct PostgresJoin {
39 /// The type of join to perform. Examples are INNER JOIN, LEFT JOIN, etc.
40 pub join_type: &'static str,
41 /// The table to join.
42 pub table: &'static str,
43 /// The field in the local table to join on.
44 pub local_field: &'static str,
45 /// The field in the foreign table to join on.
46 pub foreign_field: &'static str,
47 /// The condition to join on. Use SQL syntax.
48 pub condition: &'static str,
49}
50impl PostgresJoin {
51 /// Converts the join to a read statement.
52 pub fn to_read(&self, local_table: &str) -> String {
53 format!("{} {} ON {}.{} {} {}.{}", self.join_type, self.table, local_table, self.local_field, self.condition, self.table, self.foreign_field)
54 }
55 /// Tusk returns the insertered or updated row(s),
56 /// so this converts the join to a write statement.
57 pub fn to_write(&self, local_table: &str) -> String {
58 format!("FROM {} WHERE {}.{} {} {}.{}", self.table, local_table, self.local_field, self.condition, self.table, self.foreign_field)
59 }
60}
61
62/// Describes what kind of field this is.
63/// For more information, read [`PostgresField`].
64pub enum PostgresFieldLocation {
65 /// A field on the table itself.
66 Local(&'static str),
67 /// An expression that can be evaluated.
68 Expression(&'static str),
69 /// A field on a foreign table.
70 /// Syntax is (table, field).
71 Join(&'static str, &'static str),
72}
73
74/// A struct that symbolizes a field in a Postgres table.
75/// This is used for reading from tables.
76///
77/// Intializing this struct manually is frowned upon.
78/// The easiest way to construct a PostgresField is to use
79/// the macros provided.
80/// - [`local`] for a field on the table itself.
81/// - [`local_as`] for a field on the table itself with an alias.
82/// - [`expression`] for an expression.
83/// - [`foreign`] for a field on a foreign table.
84/// - [`foreign_as`] for a field on a foreign table with an alias.
85pub struct PostgresField {
86 pub alias: &'static str,
87 pub location: PostgresFieldLocation,
88}
89impl PostgresField {
90 pub fn into_syntax(&self, local_table: &str) -> String {
91 format!("{} AS {}", match &self.location {
92 PostgresFieldLocation::Local(field) => format!("{}.{}", local_table, field),
93 PostgresFieldLocation::Expression(expr) => expr.to_string().replace("{}", local_table),
94 PostgresFieldLocation::Join(table, field) => format!("{}.{}", table, field),
95 }, self.alias)
96 }
97}
98
99/// A macro for creating a local field.
100///
101/// # Arguments
102/// - `$name` - The name of the field, as a &'static str.
103#[macro_export]
104macro_rules! local {
105 ($name: literal) => {
106 &tusk_rs::PostgresField {
107 alias: $name,
108 location: tusk_rs::PostgresFieldLocation::Local($name),
109 }
110 };
111}
112
113/// A macro for creating a local field with an alias.
114///
115/// # Arguments
116/// - `$name` - The name of the field, as a &'static str.
117/// - `$alias` - The alias of the field, as a &'static str.
118#[macro_export]
119macro_rules! local_as {
120 ($name: literal, $alias: literal) => {
121 &tusk_rs::PostgresField {
122 alias: $alias,
123 location: tusk_rs::PostgresFieldLocation::Local($name),
124 }
125 };
126}
127
128/// A macro for creating an expression.
129///
130/// # Arguments
131/// - `$expr` - The expression to evaluate, as a &'static str.
132/// - `$alias` - The alias of the expression, as a &'static str.
133#[macro_export]
134macro_rules! expression {
135 ($expr: literal, $alias: literal) => {
136 &tusk_rs::PostgresField {
137 alias: $alias,
138 location: tusk_rs::PostgresFieldLocation::Expression($expr),
139 }
140 };
141}
142
143/// A macro for creating a foreign field.
144///
145/// Using [`foreign_as`] is recommended to prevent conflicts where
146/// both a local and foreign field have the same name.
147///
148/// # Arguments
149/// - `$table` - The table to join on, as a &'static str.
150/// - `$name` - The name of the field, as a &'static str.
151#[macro_export]
152macro_rules! foreign {
153 ($table: literal, $name: literal) => {
154 &tusk_rs::PostgresField {
155 alias: $name,
156 location: tusk_rs::PostgresFieldLocation::Join($table, $name),
157 }
158 };
159}
160
161/// A macro for creating a foreign field with an alias.
162///
163/// # Arguments
164/// - `$table` - The table to join on, as a &'static str.
165/// - `$name` - The name of the field, as a &'static str.
166/// - `$alias` - The alias of the field, as a &'static str.
167#[macro_export]
168macro_rules! foreign_as {
169 ($table: literal, $name: literal, $alias: literal) => {
170 &tusk_rs::PostgresField {
171 alias: $alias,
172 location: tusk_rs::PostgresFieldLocation::Join($table, $name),
173 }
174 };
175}
176
177/// A struct that contains data to write into
178/// a Postgres table.
179#[derive(Debug)]
180pub struct PostgresWrite {
181 /// The fields that will be provided.
182 pub fields: &'static [&'static str],
183 /// The arguments to insert. This supports either
184 /// a single row or multiple rows.
185 ///
186 /// arguments.len() % fields.len() must always be 0.
187 pub arguments: Vec<Box<(dyn ToSql + Sync)>>,
188}
189impl PostgresWrite {
190 /// Converts the write struct into an insert statement
191 pub fn into_insert(&self, table_name: &str) -> (String, Vec<&(dyn ToSql + Sync)>) {
192 (
193 format!(
194 "INSERT INTO {} ({}) VALUES ({})",
195 table_name,
196 self.fields.join(","),
197 (0..self.arguments.len())
198 .map(|x| format!("${}", x + 1))
199 .collect::<Vec<String>>()
200 .join(",")
201 ),
202 self.arguments
203 .iter()
204 .map(|x| x.as_ref())
205 .collect::<Vec<&(dyn ToSql + Sync)>>(),
206 )
207 }
208 /// Converts the write struct into a bulk insert statement
209 pub fn into_bulk_insert(&self, table_name: &str) -> (String, Vec<&(dyn ToSql + Sync)>) {
210 if self.arguments.len() % self.fields.len() != 0 {
211 panic!("For a bulk insert, arguments % fields must be 0.")
212 }
213 let mut arg_groups: Vec<String> = vec![];
214
215 for ix in 0..(self.arguments.len() / self.fields.len()) {
216 let mut iter_args = vec![];
217 for jx in 0..self.fields.len() {
218 iter_args.push(format!("${}", ix * self.fields.len() + jx + 1))
219 }
220 arg_groups.push(format!("({})", iter_args.join(",")));
221 }
222 (
223 format!(
224 "INSERT INTO {} ({}) VALUES {}",
225 table_name,
226 self.fields.join(","),
227 arg_groups.join(",")
228 ),
229 self.arguments
230 .iter()
231 .map(|x| x.as_ref())
232 .collect::<Vec<&(dyn ToSql + Sync)>>(),
233 )
234 }
235 /// Converts the write struct into an update statement
236 pub fn into_update(
237 &self,
238 table_name: &str,
239 arg_offset: usize,
240 ) -> (String, Vec<&(dyn ToSql + Sync)>) {
241 if self.fields.len() != self.arguments.len() {
242 panic!("Field length must equal argument length")
243 }
244 (
245 format!(
246 "UPDATE {} SET {}",
247 table_name,
248 (0..self.arguments.len())
249 .map(|x| format!("{} = ${}", self.fields[x], x + 1 + arg_offset))
250 .collect::<Vec<String>>()
251 .join(",")
252 ),
253 self.arguments
254 .iter()
255 .map(|x| x.as_ref())
256 .collect::<Vec<&(dyn ToSql + Sync)>>(),
257 )
258 }
259}
260
261/// A trait for defining a table in Postgres.
262/// This is used for determining which table
263/// to read/write from. This is required for
264/// all Tusk database operations.
265pub trait PostgresTable {
266 /// The name of the table in Postgres.
267 fn table_name() -> &'static str;
268}
269
270/// A trait for defining joins in Postgres.
271/// This is used for determining how to join
272/// tables. This is required for all Tusk
273/// database operations.
274///
275/// For more info on implementing this trait,
276/// read the documentation for [`PostgresJoin`].
277pub trait PostgresJoins {
278 /// The joins to perform.
279 fn joins() -> &'static [&'static PostgresJoin];
280}
281
282/// A trait for defining fields to read from
283/// in Postgres. This is required for all
284/// Tusk database operations.
285///
286/// This may be implemented by deriving [`PostgresJoin`],
287/// which will read all fields in the struct.
288///
289/// For more control (for example to include expression columns),
290/// implement this manually. To learn more about manual implementation,
291/// read the documentation for [`PostgresField`].
292pub trait PostgresReadFields {
293 /// The fields to read.
294 fn read_fields() -> &'static [&'static PostgresField];
295}
296
297/// A trait that declares a struct as readable.
298/// Even though there are no methods, this is
299/// a stub for future implementations.
300///
301/// For now, it may be implemented by deriving.
302pub trait PostgresReadable: PostgresReadFields + PostgresJoins {}
303
304/// A trait for defining fields to write to
305/// in Postgres. This is required for all
306/// Tusk database operations.
307///
308/// Unlike [`PostgresReadFields`], this trait
309/// returns static string slices instead of structs.
310/// Because the struct data is not needed to perform writes,
311/// this improves performance.
312pub trait PostgresWriteFields {
313 fn write_fields() -> &'static [&'static str];
314}
315
316/// A trait that declares a struct as writeable.
317/// Do not manually implement this. Instead, implement
318/// [`PostgresWriteFields`] and [`PostgresJoins`], and derive
319/// this trait.
320pub trait PostgresWriteable: PostgresWriteFields + PostgresJoins {
321 fn write(self) -> PostgresWrite;
322}
323
324/// A trait for defining a struct as bulk writeable.
325/// This is typically defined on collections of structs.
326/// Tusk includes a default implementation for Vec<T> where
327/// T implements [`PostgresWriteable`].
328pub trait PostgresBulkWriteable {
329 fn into_bulk_write(self) -> PostgresWrite;
330}
331
332impl<T: PostgresWriteable + PostgresTable> PostgresBulkWriteable for Vec<T> {
333 fn into_bulk_write(self) -> PostgresWrite {
334 PostgresWrite {
335 arguments: self.into_iter().flat_map(|x| x.write().arguments).collect(),
336 fields: T::write_fields(),
337 }
338 }
339}