backend/model/entity/
plantings_impl.rs

1//! Contains the implementation of [`Planting`].
2
3use chrono::{Duration, NaiveDate, Utc};
4use diesel::pg::Pg;
5use diesel::{
6    debug_query, BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl,
7    QueryResult,
8};
9use diesel_async::scoped_futures::ScopedFutureExt;
10use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
11use log::debug;
12use std::future::Future;
13use uuid::Uuid;
14
15use crate::model::dto::plantings::{DeletePlantingDto, PlantingDto, UpdatePlantingDto};
16use crate::model::entity::plantings::{NewPlanting, Planting, UpdatePlanting};
17use crate::model::entity::Map;
18use crate::model::r#enum::life_cycle::LifeCycle;
19use crate::schema::plantings::{self, layer_id, plant_id};
20use crate::schema::plants;
21use crate::schema::seeds;
22
23/// Arguments for the database layer find plantings function.
24pub struct FindPlantingsParameters {
25    /// The id of the plant to find plantings for.
26    pub plant_id: Option<i32>,
27    /// The id of the layer to find plantings for.
28    pub layer_id: Option<Uuid>,
29    /// First date in the time frame plantings are searched for.
30    pub from: NaiveDate,
31    /// Last date in the time frame plantings are searched for.
32    pub to: NaiveDate,
33}
34
35impl Planting {
36    /// Get all plantings associated with the query.
37    ///
38    /// # Errors
39    /// * Unknown, diesel doesn't say why it might error.
40    pub async fn find(
41        search_parameters: FindPlantingsParameters,
42        conn: &mut AsyncPgConnection,
43    ) -> QueryResult<Vec<PlantingDto>> {
44        let mut query = plantings::table
45            .left_join(seeds::table)
46            .select((plantings::all_columns, seeds::name.nullable()))
47            .into_boxed();
48
49        if let Some(id) = search_parameters.plant_id {
50            query = query.filter(plant_id.eq(id));
51        }
52        if let Some(id) = search_parameters.layer_id {
53            query = query.filter(layer_id.eq(id));
54        }
55
56        let from = search_parameters.from;
57        let to = search_parameters.to;
58
59        let plantings_added_before_date =
60            plantings::add_date.is_null().or(plantings::add_date.lt(to));
61        let plantings_removed_after_date = plantings::remove_date
62            .is_null()
63            .or(plantings::remove_date.gt(from));
64
65        query = query.filter(plantings_added_before_date.and(plantings_removed_after_date));
66
67        debug!("{}", debug_query::<Pg, _>(&query));
68
69        Ok(query
70            .load::<(Self, Option<String>)>(conn)
71            .await?
72            .into_iter()
73            .map(Into::into)
74            .collect())
75    }
76
77    /// Get all plantings that have a specific seed id.
78    ///
79    /// # Errors
80    /// * Unknown, diesel doesn't say why it might error.
81    pub async fn find_by_seed_id(
82        seed_id: i32,
83        conn: &mut AsyncPgConnection,
84    ) -> QueryResult<Vec<PlantingDto>> {
85        let query = plantings::table
86            .select(plantings::all_columns)
87            .filter(plantings::seed_id.eq(seed_id));
88
89        Ok(query
90            .load::<Self>(conn)
91            .await?
92            .into_iter()
93            .map(Into::into)
94            .collect())
95    }
96
97    /// Helper that sets `end_date` according to the `life_cycle`.
98    async fn set_end_date_according_to_cycle_types(
99        conn: &mut AsyncPgConnection,
100        plantings: &mut Vec<NewPlanting>,
101    ) -> QueryResult<()> {
102        /// life cycles are currently persisted as an optional list of optional values.
103        type LifeCycleType = Option<Vec<Option<LifeCycle>>>;
104
105        let plant_ids: Vec<i32> = plantings.iter().map(|p| p.plant_id).collect();
106        let life_cycles_query = plants::table
107            .filter(plants::id.eq_any(&plant_ids))
108            .select((plants::id, plants::life_cycle.nullable()));
109
110        let life_cycle_lookup = life_cycles_query.load::<(i32, LifeCycleType)>(conn).await?;
111
112        for planting in plantings {
113            if let Some(add_date) = planting.add_date {
114                let current_plant_id = planting.plant_id;
115                let life_cycle_info_opt: Option<(i32, LifeCycleType)> = life_cycle_lookup
116                    .iter()
117                    .find_map(|x| (x.0 == current_plant_id).then(|| x.clone()));
118                if let Some((_, Some(life_cycles))) = life_cycle_info_opt {
119                    if life_cycles.contains(&Some(LifeCycle::Perennial)) {
120                    } else if life_cycles.contains(&Some(LifeCycle::Biennial)) {
121                        planting.remove_date = Some(add_date + Duration::days(2 * 365));
122                    } else if life_cycles.contains(&Some(LifeCycle::Annual)) {
123                        planting.remove_date = Some(add_date + Duration::days(365));
124                    }
125                }
126            }
127        }
128        Ok(())
129    }
130
131    /// Create a new planting in the database.
132    ///
133    /// # Errors
134    /// * If the `layer_id` references a layer that is not of type `plant`.
135    /// * Unknown, diesel doesn't say why it might error.
136    pub async fn create(
137        dto_vec: Vec<PlantingDto>,
138        map_id: i32,
139        user_id: Uuid,
140        conn: &mut AsyncPgConnection,
141    ) -> QueryResult<Vec<PlantingDto>> {
142        let mut planting_creations: Vec<NewPlanting> = dto_vec
143            .into_iter()
144            .map(|dto| NewPlanting::from((dto, user_id)))
145            .collect();
146
147        Self::set_end_date_according_to_cycle_types(conn, &mut planting_creations).await?;
148
149        let query = diesel::insert_into(plantings::table).values(&planting_creations);
150
151        debug!("{}", debug_query::<Pg, _>(&query));
152
153        let query_result: Vec<Self> = query.get_results::<Self>(conn).await?;
154
155        if let Some(first) = query_result.get(0) {
156            Map::update_modified_metadata(map_id, user_id, first.created_at, conn).await?;
157        }
158
159        let seed_ids = query_result
160            .iter()
161            .map(|planting| planting.seed_id)
162            .collect::<Vec<_>>();
163
164        // There seems to be no way of retrieving the additional name using the insert query
165        // above.
166        let additional_names_query = seeds::table
167            .filter(seeds::id.nullable().eq_any(&seed_ids))
168            .select((seeds::id, seeds::name));
169
170        debug!("{}", debug_query::<Pg, _>(&additional_names_query));
171
172        let seed_ids_names: Vec<(i32, String)> = additional_names_query.get_results(conn).await?;
173
174        let seed_ids_to_names = seed_ids_names
175            .into_iter()
176            .collect::<std::collections::HashMap<_, _>>();
177
178        let result_vec = query_result
179            .into_iter()
180            .map(PlantingDto::from)
181            .map(|mut dto| {
182                if let Some(seed_id) = dto.seed_id {
183                    dto.additional_name = seed_ids_to_names.get(&seed_id).cloned();
184                }
185                dto
186            })
187            .collect::<Vec<_>>();
188
189        Ok(result_vec)
190    }
191
192    /// Partially update a planting in the database.
193    ///
194    /// # Errors
195    /// * Unknown, diesel doesn't say why it might error.
196    pub async fn update(
197        dto: UpdatePlantingDto,
198        map_id: i32,
199        user_id: Uuid,
200        conn: &mut AsyncPgConnection,
201    ) -> QueryResult<Vec<PlantingDto>> {
202        let planting_updates = Vec::from(dto);
203
204        let result = conn
205            .transaction(|transaction| {
206                async move {
207                    let futures = Self::do_update(planting_updates, user_id, transaction);
208
209                    let results = futures_util::future::try_join_all(futures).await?;
210
211                    if let Some(first) = results.get(0) {
212                        Map::update_modified_metadata(
213                            map_id,
214                            user_id,
215                            first.modified_at,
216                            transaction,
217                        )
218                        .await?;
219                    }
220
221                    Ok(results) as QueryResult<Vec<Self>>
222                }
223                .scope_boxed()
224            })
225            .await?;
226
227        Ok(result.into_iter().map(Into::into).collect())
228    }
229
230    /// Performs the actual update of the plantings using pipelined requests.
231    /// See [`diesel_async::AsyncPgConnection`] for more information.
232    /// Because the type system can not easily infer the type of futures
233    /// this helper function is needed, with explicit type annotations.
234    fn do_update(
235        updates: Vec<UpdatePlanting>,
236        user_id: Uuid,
237        conn: &mut AsyncPgConnection,
238    ) -> Vec<impl Future<Output = QueryResult<Self>>> {
239        let now = Utc::now().naive_utc();
240        // TODO: restrict concurrency
241        updates
242            .into_iter()
243            .map(|update| {
244                diesel::update(plantings::table.find(update.id))
245                    .set((
246                        update,
247                        plantings::modified_at.eq(now),
248                        plantings::modified_by.eq(user_id),
249                    ))
250                    .get_result::<Self>(conn)
251            })
252            .collect::<Vec<_>>()
253    }
254
255    /// Delete the plantings from the database.
256    ///
257    /// # Errors
258    /// * Unknown, diesel doesn't say why it might error.
259    pub async fn delete_by_ids(
260        dto: Vec<DeletePlantingDto>,
261        map_id: i32,
262        user_id: Uuid,
263        conn: &mut AsyncPgConnection,
264    ) -> QueryResult<usize> {
265        let ids: Vec<Uuid> = dto.iter().map(|&DeletePlantingDto { id }| id).collect();
266
267        conn.transaction(|transaction| {
268            Box::pin(async {
269                let query = diesel::delete(plantings::table.filter(plantings::id.eq_any(ids)));
270                debug!("{}", debug_query::<Pg, _>(&query));
271                let deleted_plantings = query.execute(transaction).await?;
272
273                Map::update_modified_metadata(map_id, user_id, Utc::now().naive_utc(), transaction)
274                    .await?;
275                Ok(deleted_plantings)
276            })
277        })
278        .await
279    }
280}