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
//! Database init helper functions to create a database from scratch with all required tables initialized

use std::collections::HashMap;

use super::linked_resource::PsqlType as LinkedPsqlType;
use super::simple_resource::PsqlType as SimplePsqlType;
use super::{get_psql_client, ArrErr, PsqlFieldType};
use crate::grpc::server::*;
use crate::resources::{
    base::FieldDefinition,
    base::{Resource, ResourceObject},
};

/// If the database is fresh, we need to create all tables.
/// This function makes sure the tables will be created in the correct order
pub async fn create_db() -> Result<(), ArrErr> {
    psql_info!("(create_db) Creating database tables.");
    ResourceObject::<group::Data>::init_table().await?;
    ResourceObject::<user::Data>::init_table().await?;
    ResourceObject::<user_group::Data>::init_table().await?;
    ResourceObject::<vertiport::Data>::init_table().await?;
    ResourceObject::<vertiport_group::Data>::init_table().await?;
    ResourceObject::<vertipad::Data>::init_table().await?;
    ResourceObject::<vertipad_group::Data>::init_table().await?;
    ResourceObject::<vehicle::Data>::init_table().await?;
    ResourceObject::<vehicle_group::Data>::init_table().await?;
    ResourceObject::<pilot::Data>::init_table().await?;
    ResourceObject::<adsb::Data>::init_table().await?;
    ResourceObject::<flight_plan::Data>::init_table().await?;
    ResourceObject::<itinerary::Data>::init_table().await?;
    ResourceObject::<itinerary_flight_plan::Data>::init_table().await?;
    ResourceObject::<parcel::Data>::init_table().await?;
    ResourceObject::<flight_plan_parcel::Data>::init_table().await?;
    ResourceObject::<scanner::Data>::init_table().await?;
    ResourceObject::<parcel_scan::Data>::init_table().await?;
    Ok(())
}

/// If we want to recreate the database tables created by this module, we will want to drop the existing tables first.
/// This function makes sure the tables will be dropped in the correct order
pub async fn drop_db() -> Result<(), ArrErr> {
    psql_warn!("(drop_db) Dropping database tables.");
    // Drop our tables (in the correct order)
    ResourceObject::<parcel_scan::Data>::drop_table().await?;
    ResourceObject::<scanner::Data>::drop_table().await?;
    ResourceObject::<flight_plan_parcel::Data>::drop_table().await?;
    ResourceObject::<parcel::Data>::drop_table().await?;
    ResourceObject::<itinerary_flight_plan::Data>::drop_table().await?;
    ResourceObject::<itinerary::Data>::drop_table().await?;
    ResourceObject::<flight_plan::Data>::drop_table().await?;
    ResourceObject::<adsb::Data>::drop_table().await?;
    ResourceObject::<pilot::Data>::drop_table().await?;
    ResourceObject::<vehicle_group::Data>::drop_table().await?;
    ResourceObject::<vehicle::Data>::drop_table().await?;
    ResourceObject::<vertipad_group::Data>::drop_table().await?;
    ResourceObject::<vertipad::Data>::drop_table().await?;
    ResourceObject::<vertiport_group::Data>::drop_table().await?;
    ResourceObject::<vertiport::Data>::drop_table().await?;
    ResourceObject::<user_group::Data>::drop_table().await?;
    ResourceObject::<user::Data>::drop_table().await?;
    ResourceObject::<group::Data>::drop_table().await?;
    Ok(())
}

/// Recreate the database by dropping all tables first (if they exist) and recreating them again
pub async fn recreate_db() -> Result<(), ArrErr> {
    psql_warn!("(recreate_db) Re-creating database tables.");
    drop_db().await?;
    create_db().await?;

    psql_debug!("(recreate_db) Clearing caches.");
    // Make sure to clear any cached statements
    let pool = super::pool::DB_POOL
        .get()
        .ok_or(ArrErr::Error(String::from(
            "(recreate_db) Could not get pool.",
        )))?;
    pool.manager().statement_caches.clear();

    Ok(())
}

/// Generic PostgreSQL trait to provide table init functions for `Resource` struct
#[tonic::async_trait]
pub trait PsqlInitResource
where
    Self: Resource + Clone,
{
    /// Internal function called by [init_table](PsqlInitResource::init_table) to run table index creation queries if any indices
    /// are defined for the resource
    async fn _init_table_indices() -> Result<(), ArrErr> {
        let queries = Self::get_table_indices();
        if queries.is_empty() {
            // Nothing to do
            return Ok(());
        }

        let mut client = get_psql_client().await?;
        let transaction = client.transaction().await?;
        for index_query in queries {
            psql_debug!("(_init_table_indices) [{}].", index_query);
            if let Err(e) = transaction.execute(&index_query, &[]).await {
                psql_error!(
                    "(_init_table_indices) Failed to create indices for table [{}]: {}",
                    Self::get_psql_table(),
                    e
                );
                return transaction.rollback().await.map_err(ArrErr::from);
            }
        }
        transaction.commit().await.map_err(ArrErr::from)
    }

    /// Create table with specified columns using the resource's `psql_definition`
    async fn init_table() -> Result<(), ArrErr> {
        let mut client = get_psql_client().await?;
        let transaction = client.transaction().await?;
        let create_table = Self::_get_create_table_query();

        psql_debug!("(init_table) [{}].", create_table);
        if let Err(e) = transaction.execute(&create_table, &[]).await {
            psql_error!("(init_table) Failed to create table: {}", e);
            return transaction.rollback().await.map_err(ArrErr::from);
        }
        transaction.commit().await?;
        Self::_init_table_indices().await
    }

    /// Drops the entire table for the resource
    async fn drop_table() -> Result<(), ArrErr> {
        let definition = Self::get_definition();
        let mut client = get_psql_client().await?;
        let transaction = client.transaction().await?;

        let drop_query = format!(r#"DROP TABLE IF EXISTS "{}""#, definition.psql_table);
        psql_debug!("(drop_table) [{}].", drop_query);

        psql_info!("(drop_table) Dropping table [{}].", definition.psql_table);
        if let Err(e) = transaction.execute(&drop_query, &[]).await {
            psql_error!(
                "(drop_table) Failed to drop table [{}]: {}",
                e,
                definition.psql_table
            );
            return transaction.rollback().await.map_err(ArrErr::from);
        }
        transaction.commit().await.map_err(ArrErr::from)
    }

    /// Internal function to get the query that should be used to create the resource's table.
    /// Should be overwritten by the implementor.
    fn _get_create_table_query() -> String;
}

/// Generic PostgreSQL trait to provide table init functions for `Resource` struct
#[tonic::async_trait]
pub trait PsqlInitSimpleResource
where
    Self: SimplePsqlType + Clone,
{
    /// Constructs the create table query for the resource
    /// for internal use
    fn _get_create_table_query() -> String {
        let definition = Self::get_definition();
        psql_info!(
            "(_get_create_table_query) Composing create table query for [{}].",
            definition.psql_table
        );
        let id_field = match Self::try_get_id_field() {
            Ok(field) => field,
            Err(e) => {
                // Panic here, we should -always- have an id_field configured for our simple resources.
                // If we hit this scenario, we should fix our code, so we need to let this know with a hard crash.
                panic!(
                    "(_get_create_table_query) Can't convert Object into ResourceObject<Data>: {e}"
                )
            }
        };
        let mut fields = vec![];
        fields.push(format!(
            r#""{}" UUID DEFAULT uuid_generate_v4() PRIMARY KEY"#,
            id_field
        ));

        fields.append(&mut get_create_table_fields_sql(&definition.fields));

        format!(
            r#"CREATE TABLE IF NOT EXISTS "{}" ({})"#,
            definition.psql_table,
            fields.join(", ")
        )
    }
}

/// Generic PostgreSQL trait to provide table init functions for `Resource` struct
#[tonic::async_trait]
pub trait PsqlInitLinkedResource
where
    Self: LinkedPsqlType + Clone,
{
    /// Constructs the create table query for the resource
    /// for internal use
    fn _get_create_table_query() -> String {
        let definition = Self::get_definition();
        psql_info!(
            "(_get_create_table_query) Composing create table query for [{}].",
            definition.psql_table
        );

        let mut fields = vec![];
        let mut ids = vec![];
        for id in definition.psql_id_cols {
            fields.push(format!(r#""{}" UUID NOT NULL"#, id));
            ids.push(format!(r#""{}""#, id))
        }

        fields.append(&mut get_create_table_fields_sql(&definition.fields));

        format!(
            r#"CREATE TABLE IF NOT EXISTS "{}" ({}, PRIMARY KEY({}) )"#,
            definition.psql_table,
            fields.join(", "),
            ids.join(", ")
        )
    }
}

fn get_create_table_fields_sql(fields: &HashMap<String, FieldDefinition>) -> Vec<String> {
    let mut result: Vec<String> = vec![];
    for (key, field) in fields {
        let mut field_sql = format!(r#""{}""#, key);

        match field.field_type {
            PsqlFieldType::TIMESTAMPTZ => field_sql.push_str(" TIMESTAMP WITH TIME ZONE"),
            PsqlFieldType::ANYENUM => field_sql.push_str(" TEXT"),
            PsqlFieldType::INT2 => field_sql.push_str(" SMALLINT"),
            PsqlFieldType::INT4 => field_sql.push_str(" INTEGER"),
            PsqlFieldType::INT8 => field_sql.push_str(" BIGINT"),
            PsqlFieldType::NUMERIC => field_sql.push_str(" DOUBLE PRECISION"),
            PsqlFieldType::BYTEA => field_sql.push_str(" BYTEA"),
            PsqlFieldType::PATH => field_sql.push_str(" GEOMETRY"),
            PsqlFieldType::POINT => field_sql.push_str(" GEOMETRY"),
            PsqlFieldType::POLYGON => field_sql.push_str(" GEOMETRY"),
            _ => field_sql.push_str(&format!(" {}", field.field_type.name().to_uppercase())),
        }

        if field.has_default() {
            field_sql.push_str(&format!(" DEFAULT {}", field.get_default()));
        }

        if field.is_mandatory() {
            field_sql.push_str(" NOT NULL");
        }
        result.push(field_sql);
    }
    result
}