1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! Contains the implementation of [`Planting`].

use chrono::{Duration, NaiveDate, Utc};
use diesel::pg::Pg;
use diesel::{
    debug_query, BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl,
    QueryResult,
};
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
use futures_util::Future;
use log::debug;
use uuid::Uuid;

use crate::model::dto::plantings::{DeletePlantingDto, PlantingDto, UpdatePlantingDto};
use crate::model::entity::plantings::{NewPlanting, Planting, UpdatePlanting};
use crate::model::entity::Map;
use crate::model::r#enum::life_cycle::LifeCycle;
use crate::schema::plantings::{self, layer_id, plant_id};
use crate::schema::plants;
use crate::schema::seeds;

/// Arguments for the database layer find plantings function.
pub struct FindPlantingsParameters {
    /// The id of the plant to find plantings for.
    pub plant_id: Option<i32>,
    /// The id of the layer to find plantings for.
    pub layer_id: Option<Uuid>,
    /// First date in the time frame plantings are searched for.
    pub from: NaiveDate,
    /// Last date in the time frame plantings are searched for.
    pub to: NaiveDate,
}

impl Planting {
    /// Get all plantings associated with the query.
    ///
    /// # Errors
    /// * Unknown, diesel doesn't say why it might error.
    pub async fn find(
        search_parameters: FindPlantingsParameters,
        conn: &mut AsyncPgConnection,
    ) -> QueryResult<Vec<PlantingDto>> {
        let mut query = plantings::table
            .left_join(seeds::table)
            .select((plantings::all_columns, seeds::name.nullable()))
            .into_boxed();

        if let Some(id) = search_parameters.plant_id {
            query = query.filter(plant_id.eq(id));
        }
        if let Some(id) = search_parameters.layer_id {
            query = query.filter(layer_id.eq(id));
        }

        let from = search_parameters.from;
        let to = search_parameters.to;

        let plantings_added_before_date =
            plantings::add_date.is_null().or(plantings::add_date.lt(to));
        let plantings_removed_after_date = plantings::remove_date
            .is_null()
            .or(plantings::remove_date.gt(from));

        query = query.filter(plantings_added_before_date.and(plantings_removed_after_date));

        debug!("{}", debug_query::<Pg, _>(&query));

        Ok(query
            .load::<(Self, Option<String>)>(conn)
            .await?
            .into_iter()
            .map(Into::into)
            .collect())
    }

    /// Get all plantings that have a specific seed id.
    ///
    /// # Errors
    /// * Unknown, diesel doesn't say why it might error.
    pub async fn find_by_seed_id(
        seed_id: i32,
        conn: &mut AsyncPgConnection,
    ) -> QueryResult<Vec<PlantingDto>> {
        let query = plantings::table
            .select(plantings::all_columns)
            .filter(plantings::seed_id.eq(seed_id));

        Ok(query
            .load::<Self>(conn)
            .await?
            .into_iter()
            .map(Into::into)
            .collect())
    }

    /// Helper that sets `end_date` according to the `life_cycle`.
    async fn set_end_date_according_to_cycle_types(
        conn: &mut AsyncPgConnection,
        plantings: &mut Vec<NewPlanting>,
    ) -> QueryResult<()> {
        /// life cycles are currently persisted as an optional list of optional values.
        type LifeCycleType = Option<Vec<Option<LifeCycle>>>;

        let plant_ids: Vec<i32> = plantings.iter().map(|p| p.plant_id).collect();
        let life_cycles_query = plants::table
            .filter(plants::id.eq_any(&plant_ids))
            .select((plants::id, plants::life_cycle.nullable()));

        let life_cycle_lookup = life_cycles_query.load::<(i32, LifeCycleType)>(conn).await?;

        for planting in plantings {
            if let Some(add_date) = planting.add_date {
                let current_plant_id = planting.plant_id;
                let life_cycle_info_opt: Option<(i32, LifeCycleType)> = life_cycle_lookup
                    .iter()
                    .find_map(|x| (x.0 == current_plant_id).then(|| x.clone()));
                if let Some((_, Some(life_cycles))) = life_cycle_info_opt {
                    if life_cycles.contains(&Some(LifeCycle::Perennial)) {
                        continue;
                    } else if life_cycles.contains(&Some(LifeCycle::Biennial)) {
                        planting.remove_date = Some(add_date + Duration::days(2 * 365));
                    } else if life_cycles.contains(&Some(LifeCycle::Annual)) {
                        planting.remove_date = Some(add_date + Duration::days(365));
                    }
                };
            };
        }
        Ok(())
    }

    /// Create a new planting in the database.
    ///
    /// # Errors
    /// * If the `layer_id` references a layer that is not of type `plant`.
    /// * Unknown, diesel doesn't say why it might error.
    pub async fn create(
        dto_vec: Vec<PlantingDto>,
        map_id: i32,
        user_id: Uuid,
        conn: &mut AsyncPgConnection,
    ) -> QueryResult<Vec<PlantingDto>> {
        let mut planting_creations: Vec<NewPlanting> = dto_vec
            .into_iter()
            .map(|dto| NewPlanting::from((dto, user_id)))
            .collect();

        Self::set_end_date_according_to_cycle_types(conn, &mut planting_creations).await?;

        let query = diesel::insert_into(plantings::table).values(&planting_creations);

        debug!("{}", debug_query::<Pg, _>(&query));

        let query_result: Vec<Self> = query.get_results::<Self>(conn).await?;

        if let Some(first) = query_result.get(0) {
            Map::update_modified_metadata(map_id, user_id, first.created_at, conn).await?;
        }

        let seed_ids = query_result
            .iter()
            .map(|planting| planting.seed_id)
            .collect::<Vec<_>>();

        // There seems to be no way of retrieving the additional name using the insert query
        // above.
        let additional_names_query = seeds::table
            .filter(seeds::id.nullable().eq_any(&seed_ids))
            .select((seeds::id, seeds::name));

        debug!("{}", debug_query::<Pg, _>(&additional_names_query));

        let seed_ids_names: Vec<(i32, String)> = additional_names_query.get_results(conn).await?;

        let seed_ids_to_names = seed_ids_names
            .into_iter()
            .collect::<std::collections::HashMap<_, _>>();

        let result_vec = query_result
            .into_iter()
            .map(PlantingDto::from)
            .map(|mut dto| {
                if let Some(seed_id) = dto.seed_id {
                    dto.additional_name = seed_ids_to_names.get(&seed_id).cloned();
                }
                dto
            })
            .collect::<Vec<_>>();

        Ok(result_vec)
    }

    /// Partially update a planting in the database.
    ///
    /// # Errors
    /// * Unknown, diesel doesn't say why it might error.
    pub async fn update(
        dto: UpdatePlantingDto,
        map_id: i32,
        user_id: Uuid,
        conn: &mut AsyncPgConnection,
    ) -> QueryResult<Vec<PlantingDto>> {
        let planting_updates = Vec::from(dto);

        let result = conn
            .transaction(|transaction| {
                Box::pin(async {
                    let futures = Self::do_update(planting_updates, user_id, transaction);

                    let results = futures_util::future::try_join_all(futures).await?;

                    if let Some(first) = results.get(0) {
                        Map::update_modified_metadata(
                            map_id,
                            user_id,
                            first.modified_at,
                            transaction,
                        )
                        .await?;
                    }

                    Ok(results) as QueryResult<Vec<Self>>
                })
            })
            .await?;

        Ok(result.into_iter().map(Into::into).collect())
    }

    /// Performs the actual update of the plantings using pipelined requests.
    /// See [`diesel_async::AsyncPgConnection`] for more information.
    /// Because the type system can not easily infer the type of futures
    /// this helper function is needed, with explicit type annotations.
    fn do_update(
        updates: Vec<UpdatePlanting>,
        user_id: Uuid,
        conn: &mut AsyncPgConnection,
    ) -> Vec<impl Future<Output = QueryResult<Self>>> {
        let mut futures = Vec::with_capacity(updates.len());
        let now = Utc::now().naive_utc();

        for update in updates {
            let updated_planting = diesel::update(plantings::table.find(update.id))
                .set((
                    update,
                    plantings::modified_at.eq(now),
                    plantings::modified_by.eq(user_id),
                ))
                .get_result::<Self>(conn);

            futures.push(updated_planting);
        }

        futures
    }

    /// Delete the plantings from the database.
    ///
    /// # Errors
    /// * Unknown, diesel doesn't say why it might error.
    pub async fn delete_by_ids(
        dto: Vec<DeletePlantingDto>,
        map_id: i32,
        user_id: Uuid,
        conn: &mut AsyncPgConnection,
    ) -> QueryResult<usize> {
        let ids: Vec<Uuid> = dto.iter().map(|&DeletePlantingDto { id }| id).collect();

        conn.transaction(|transaction| {
            Box::pin(async {
                let query = diesel::delete(plantings::table.filter(plantings::id.eq_any(ids)));
                debug!("{}", debug_query::<Pg, _>(&query));
                let deleted_plantings = query.execute(transaction).await?;

                Map::update_modified_metadata(map_id, user_id, Utc::now().naive_utc(), transaction)
                    .await?;
                Ok(deleted_plantings)
            })
        })
        .await
    }
}