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}