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}