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