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