Skip to main content

shared/models/
server_database_instance.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow, prelude::Type};
9use std::{
10    collections::BTreeMap,
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
16#[serde(rename_all = "snake_case")]
17#[sqlx(
18    type_name = "server_database_instance_status",
19    rename_all = "SCREAMING_SNAKE_CASE"
20)]
21pub enum ServerDatabaseInstanceStatus {
22    RestoringBackup,
23}
24
25#[derive(Serialize, Deserialize, Clone)]
26pub struct ServerDatabaseInstance {
27    pub uuid: uuid::Uuid,
28    pub server: Fetchable<super::server::Server>,
29    pub database_agent_host: super::database_agent_host::DatabaseAgentHost,
30    pub database_agent_template:
31        Option<Fetchable<super::database_agent_template::DatabaseAgentTemplate>>,
32    pub template_version: Option<i32>,
33
34    pub r#type: db_agent_api::DatabaseAgentType,
35
36    pub name: compact_str::CompactString,
37    pub status: Option<ServerDatabaseInstanceStatus>,
38    pub locked: bool,
39
40    pub image: Option<compact_str::CompactString>,
41    pub env: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
42
43    pub memory: Option<i64>,
44    pub swap: Option<i64>,
45    pub disk: Option<i64>,
46    pub io_weight: Option<i16>,
47    pub cpu: Option<i32>,
48
49    pub created: chrono::NaiveDateTime,
50
51    extension_data: super::ModelExtensionData,
52}
53
54pub struct ResolvedInstanceSpec {
55    pub image: compact_str::CompactString,
56    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
57
58    pub image_uid: Option<i32>,
59    pub image_gid: Option<i32>,
60    pub cmd: Option<Vec<compact_str::CompactString>>,
61    pub volumes: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
62    pub socket_path: Option<compact_str::CompactString>,
63
64    pub memory: i64,
65    pub swap: i64,
66    pub disk: i64,
67    pub io_weight: Option<i16>,
68    pub cpu: i32,
69}
70
71impl BaseModel for ServerDatabaseInstance {
72    const NAME: &'static str = "server_database_agent";
73
74    fn get_extension_list() -> &'static super::ModelExtensionList {
75        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
76            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
77
78        &EXTENSIONS
79    }
80
81    fn get_extension_data(&self) -> &super::ModelExtensionData {
82        &self.extension_data
83    }
84
85    #[inline]
86    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
87        let prefix = prefix.unwrap_or_default();
88
89        let mut columns = BTreeMap::from([
90            (
91                "server_database_instances.uuid",
92                compact_str::format_compact!("{prefix}uuid"),
93            ),
94            (
95                "server_database_instances.server_uuid",
96                compact_str::format_compact!("{prefix}server_uuid"),
97            ),
98            (
99                "server_database_instances.database_agent_template_uuid",
100                compact_str::format_compact!("{prefix}database_agent_template_uuid"),
101            ),
102            (
103                "server_database_instances.template_version",
104                compact_str::format_compact!("{prefix}template_version"),
105            ),
106            (
107                "server_database_instances.image",
108                compact_str::format_compact!("{prefix}image"),
109            ),
110            (
111                "server_database_instances.env",
112                compact_str::format_compact!("{prefix}env"),
113            ),
114            (
115                "server_database_instances.type",
116                compact_str::format_compact!("{prefix}type"),
117            ),
118            (
119                "server_database_instances.name",
120                compact_str::format_compact!("{prefix}name"),
121            ),
122            (
123                "server_database_instances.status",
124                compact_str::format_compact!("{prefix}status"),
125            ),
126            (
127                "server_database_instances.locked",
128                compact_str::format_compact!("{prefix}locked"),
129            ),
130            (
131                "server_database_instances.memory",
132                compact_str::format_compact!("{prefix}memory"),
133            ),
134            (
135                "server_database_instances.swap",
136                compact_str::format_compact!("{prefix}swap"),
137            ),
138            (
139                "server_database_instances.disk",
140                compact_str::format_compact!("{prefix}disk"),
141            ),
142            (
143                "server_database_instances.io_weight",
144                compact_str::format_compact!("{prefix}io_weight"),
145            ),
146            (
147                "server_database_instances.cpu",
148                compact_str::format_compact!("{prefix}cpu"),
149            ),
150            (
151                "server_database_instances.created",
152                compact_str::format_compact!("{prefix}created"),
153            ),
154        ]);
155
156        columns.extend(super::database_agent_host::DatabaseAgentHost::base_columns(
157            Some("database_agent_host_"),
158        ));
159
160        columns
161    }
162
163    #[inline]
164    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
165        let prefix = prefix.unwrap_or_default();
166
167        Ok(Self {
168            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
169            server: super::server::Server::get_fetchable(
170                row.try_get(compact_str::format_compact!("{prefix}server_uuid").as_str())?,
171            ),
172            database_agent_host: super::database_agent_host::DatabaseAgentHost::map(
173                Some("database_agent_host_"),
174                row,
175            )?,
176            database_agent_template: row
177                .try_get::<Option<uuid::Uuid>, _>(
178                    compact_str::format_compact!("{prefix}database_agent_template_uuid").as_str(),
179                )?
180                .map(super::database_agent_template::DatabaseAgentTemplate::get_fetchable),
181            template_version: row
182                .try_get(compact_str::format_compact!("{prefix}template_version").as_str())?,
183            r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
184            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
185            status: row.try_get(compact_str::format_compact!("{prefix}status").as_str())?,
186            locked: row.try_get(compact_str::format_compact!("{prefix}locked").as_str())?,
187            image: row.try_get(compact_str::format_compact!("{prefix}image").as_str())?,
188            env: row
189                .try_get::<Option<serde_json::Value>, _>(
190                    compact_str::format_compact!("{prefix}env").as_str(),
191                )?
192                .map(serde_json::from_value)
193                .transpose()?,
194            memory: row.try_get(compact_str::format_compact!("{prefix}memory").as_str())?,
195            swap: row.try_get(compact_str::format_compact!("{prefix}swap").as_str())?,
196            disk: row.try_get(compact_str::format_compact!("{prefix}disk").as_str())?,
197            io_weight: row.try_get(compact_str::format_compact!("{prefix}io_weight").as_str())?,
198            cpu: row.try_get(compact_str::format_compact!("{prefix}cpu").as_str())?,
199            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
200            extension_data: Self::map_extensions(prefix, row)?,
201        })
202    }
203
204    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
205        vec![compact_str::format_compact!(
206            "{}::{}",
207            Self::NAME,
208            self.uuid
209        )]
210    }
211}
212
213impl ServerDatabaseInstance {
214    pub async fn status_by_uuid(
215        database: &crate::database::Database,
216        uuid: uuid::Uuid,
217    ) -> Result<Option<ServerDatabaseInstanceStatus>, sqlx::Error> {
218        sqlx::query_scalar!(
219            r#"
220            SELECT server_database_instances.status AS "status: ServerDatabaseInstanceStatus"
221            FROM server_database_instances
222            WHERE server_database_instances.uuid = $1
223            "#,
224            uuid
225        )
226        .fetch_one(database.read())
227        .await
228    }
229
230    /// Atomically moves the database instance from the `from` status to the `to` status, returning
231    /// `false` if the instance was not in the `from` status and nothing was changed.
232    pub async fn try_set_status_by_uuid(
233        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
234        uuid: uuid::Uuid,
235        from: Option<ServerDatabaseInstanceStatus>,
236        to: Option<ServerDatabaseInstanceStatus>,
237    ) -> Result<bool, sqlx::Error> {
238        let rows_affected = sqlx::query!(
239            "UPDATE server_database_instances
240            SET status = $2
241            WHERE server_database_instances.uuid = $1 AND server_database_instances.status IS NOT DISTINCT FROM $3",
242            uuid,
243            to as Option<ServerDatabaseInstanceStatus>,
244            from as Option<ServerDatabaseInstanceStatus>
245        )
246        .execute(executor)
247        .await?
248        .rows_affected();
249
250        Ok(rows_affected > 0)
251    }
252
253    #[inline]
254    pub fn is_restoring(&self) -> bool {
255        self.status == Some(ServerDatabaseInstanceStatus::RestoringBackup)
256    }
257
258    pub fn resolve_spec(
259        &self,
260        template: Option<&super::database_agent_template::DatabaseAgentTemplate>,
261    ) -> Option<ResolvedInstanceSpec> {
262        let image = self
263            .image
264            .clone()
265            .or_else(|| template.and_then(|t| t.docker_images.values().next().cloned()))?;
266
267        let mut env = template.map(|t| t.env.clone()).unwrap_or_default();
268        if let Some(env_overrides) = &self.env {
269            env.extend(env_overrides.clone());
270        }
271
272        Some(ResolvedInstanceSpec {
273            image,
274            env,
275            image_uid: template.map(|t| t.image_uid),
276            image_gid: template.map(|t| t.image_gid),
277            cmd: template.and_then(|t| t.cmd.clone()),
278            volumes: template.map(|t| t.volumes.clone()),
279            socket_path: template.map(|t| t.socket_path.clone()),
280            memory: self.memory.or(template.map(|t| t.memory))?,
281            swap: self.swap.or(template.map(|t| t.swap))?,
282            disk: self.disk.or(template.map(|t| t.disk))?,
283            io_weight: self.io_weight.or(template.and_then(|t| t.io_weight)),
284            cpu: self.cpu.or(template.map(|t| t.cpu))?,
285        })
286    }
287
288    pub async fn set_template_version(
289        &mut self,
290        database: &crate::database::Database,
291        version: i32,
292    ) -> Result<(), sqlx::Error> {
293        sqlx::query(
294            r#"
295            UPDATE server_database_instances
296            SET template_version = $2
297            WHERE server_database_instances.uuid = $1
298            "#,
299        )
300        .bind(self.uuid)
301        .bind(version)
302        .execute(database.write())
303        .await?;
304
305        self.template_version = Some(version);
306
307        Ok(())
308    }
309
310    pub async fn by_database_agent_host_uuid_uuid(
311        database: &crate::database::Database,
312        database_agent_host_uuid: uuid::Uuid,
313        uuid: uuid::Uuid,
314    ) -> Result<Option<Self>, crate::database::DatabaseError> {
315        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
316            r#"
317            SELECT {}
318            FROM server_database_instances
319            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
320            WHERE server_database_instances.database_agent_host_uuid = $1 AND server_database_instances.uuid = $2
321            "#,
322            Self::columns_sql(None)
323        )))
324        .bind(database_agent_host_uuid)
325        .bind(uuid)
326        .fetch_optional(database.read())
327        .await?;
328
329        row.try_map(|row| Self::map(None, &row))
330    }
331
332    pub async fn by_server_uuid_uuid(
333        database: &crate::database::Database,
334        server_uuid: uuid::Uuid,
335        uuid: uuid::Uuid,
336    ) -> Result<Option<Self>, crate::database::DatabaseError> {
337        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
338            r#"
339            SELECT {}
340            FROM server_database_instances
341            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
342            WHERE server_database_instances.server_uuid = $1 AND server_database_instances.uuid = $2
343            "#,
344            Self::columns_sql(None)
345        )))
346        .bind(server_uuid)
347        .bind(uuid)
348        .fetch_optional(database.read())
349        .await?;
350
351        row.try_map(|row| Self::map(None, &row))
352    }
353
354    pub async fn by_server_uuid_with_pagination(
355        database: &crate::database::Database,
356        server_uuid: uuid::Uuid,
357        page: i64,
358        per_page: i64,
359        search: Option<&str>,
360    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
361        super::PaginatedQueryBuilder::new(
362            Self::columns_sql(None),
363            format!(
364                r#"
365                FROM server_database_instances
366                JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
367                WHERE server_database_instances.server_uuid = $1 AND {search}
368                "#,
369                search = super::search_sql(2, &["server_database_instances.name"], &["server_database_instances.uuid"]),
370            ),
371            "server_database_instances.created, server_database_instances.uuid",
372        )
373        .bind(server_uuid)
374        .bind(search)
375        .fetch(database, page, per_page)
376        .await
377    }
378
379    pub async fn by_database_agent_host_uuid_with_pagination(
380        database: &crate::database::Database,
381        database_agent_host_uuid: uuid::Uuid,
382        page: i64,
383        per_page: i64,
384        search: Option<&str>,
385    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
386        super::PaginatedQueryBuilder::new(
387            Self::columns_sql(None),
388            format!(
389                r#"
390                FROM server_database_instances
391                JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
392                WHERE server_database_instances.database_agent_host_uuid = $1 AND {search}
393                "#,
394                search = super::search_sql(2, &["server_database_instances.name"], &["server_database_instances.uuid"]),
395            ),
396            "server_database_instances.created, server_database_instances.uuid",
397        )
398        .bind(database_agent_host_uuid)
399        .bind(search)
400        .fetch(database, page, per_page)
401        .await
402    }
403
404    pub async fn by_database_agent_template_uuid_with_pagination(
405        database: &crate::database::Database,
406        database_agent_template_uuid: uuid::Uuid,
407        page: i64,
408        per_page: i64,
409        search: Option<&str>,
410    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
411        super::PaginatedQueryBuilder::new(
412            Self::columns_sql(None),
413            format!(
414                r#"
415                FROM server_database_instances
416                JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
417                WHERE server_database_instances.database_agent_template_uuid = $1 AND {search}
418                "#,
419                search = super::search_sql(2, &["server_database_instances.name"], &["server_database_instances.uuid"]),
420            ),
421            "server_database_instances.created, server_database_instances.uuid",
422        )
423        .bind(database_agent_template_uuid)
424        .bind(search)
425        .fetch(database, page, per_page)
426        .await
427    }
428
429    pub async fn all_by_server_uuid(
430        database: &crate::database::Database,
431        server_uuid: uuid::Uuid,
432    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
433        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
434            r#"
435            SELECT {}
436            FROM server_database_instances
437            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
438            WHERE server_database_instances.server_uuid = $1
439            "#,
440            Self::columns_sql(None)
441        )))
442        .bind(server_uuid)
443        .fetch_all(database.read())
444        .await?;
445
446        rows.into_iter()
447            .map(|row| Self::map(None, &row))
448            .try_collect_vec()
449    }
450
451    pub async fn all_by_database_agent_host_uuid(
452        database: &crate::database::Database,
453        database_agent_host_uuid: uuid::Uuid,
454    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
455        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
456            r#"
457            SELECT {}
458            FROM server_database_instances
459            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
460            WHERE server_database_instances.database_agent_host_uuid = $1
461            "#,
462            Self::columns_sql(None)
463        )))
464        .bind(database_agent_host_uuid)
465        .fetch_all(database.read())
466        .await?;
467
468        rows.into_iter()
469            .map(|row| Self::map(None, &row))
470            .try_collect_vec()
471    }
472
473    pub async fn count_by_server_uuid(
474        database: &crate::database::Database,
475        server_uuid: uuid::Uuid,
476    ) -> Result<i64, sqlx::Error> {
477        sqlx::query_scalar(
478            r#"
479            SELECT COUNT(*)
480            FROM server_database_instances
481            WHERE server_database_instances.server_uuid = $1
482            "#,
483        )
484        .bind(server_uuid)
485        .fetch_one(database.read())
486        .await
487    }
488
489    pub async fn count_by_database_agent_host_uuid(
490        database: &crate::database::Database,
491        database_agent_host_uuid: uuid::Uuid,
492    ) -> Result<i64, sqlx::Error> {
493        sqlx::query_scalar(
494            r#"
495            SELECT COUNT(*)
496            FROM server_database_instances
497            WHERE server_database_instances.database_agent_host_uuid = $1
498            "#,
499        )
500        .bind(database_agent_host_uuid)
501        .fetch_one(database.read())
502        .await
503    }
504}
505
506struct ResolvedAdminInstance {
507    template: Option<super::database_agent_template::DatabaseAgentTemplate>,
508    image: Option<compact_str::CompactString>,
509    env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
510    update_available: bool,
511    host: Option<compact_str::CompactString>,
512    port: i32,
513    memory: i64,
514    swap: i64,
515    disk: i64,
516    io_weight: Option<i16>,
517    cpu: i32,
518}
519
520impl ServerDatabaseInstance {
521    async fn resolve_admin(
522        &self,
523        state: &crate::State,
524    ) -> Result<ResolvedAdminInstance, crate::database::DatabaseError> {
525        let type_settings = self.database_agent_host.types.get(self.r#type);
526
527        let template = match &self.database_agent_template {
528            Some(template) => Some(template.fetch_cached(&state.database).await?),
529            None => None,
530        };
531
532        let image = self.image.clone().or_else(|| {
533            template
534                .as_ref()
535                .and_then(|t| t.docker_images.values().next().cloned())
536        });
537        let env = {
538            let mut env = template.as_ref().map(|t| t.env.clone()).unwrap_or_default();
539            if let Some(env_overrides) = &self.env {
540                env.extend(env_overrides.clone());
541            }
542
543            env
544        };
545        let update_available = match (&template, self.template_version) {
546            (Some(template), Some(template_version)) => template.version > template_version,
547            _ => false,
548        };
549
550        Ok(ResolvedAdminInstance {
551            image,
552            env,
553            update_available,
554            host: type_settings.public_host.clone().or_else(|| {
555                self.database_agent_host
556                    .url
557                    .host_str()
558                    .map(compact_str::CompactString::from)
559            }),
560            port: type_settings
561                .public_port
562                .map_or_else(|| i32::from(self.r#type.default_port()), i32::from),
563            memory: self
564                .memory
565                .or(template.as_ref().map(|t| t.memory))
566                .unwrap_or_default(),
567            swap: self
568                .swap
569                .or(template.as_ref().map(|t| t.swap))
570                .unwrap_or_default(),
571            disk: self
572                .disk
573                .or(template.as_ref().map(|t| t.disk))
574                .unwrap_or_default(),
575            io_weight: self
576                .io_weight
577                .or(template.as_ref().and_then(|t| t.io_weight)),
578            cpu: self
579                .cpu
580                .or(template.as_ref().map(|t| t.cpu))
581                .unwrap_or_default(),
582            template,
583        })
584    }
585
586    pub async fn into_admin_server_api_object(
587        self,
588        state: &crate::State,
589    ) -> Result<AdminApiServerServerDatabaseInstance, crate::database::DatabaseError> {
590        let api_object = AdminApiServerServerDatabaseInstance::init_hooks(&self, state).await?;
591        let resolved = self.resolve_admin(state).await?;
592
593        let api_object = finish_extendible!(
594            AdminApiServerServerDatabaseInstance {
595                uuid: self.uuid,
596                database_agent_host: self
597                    .database_agent_host
598                    .into_admin_api_object(state, ())
599                    .await?,
600                r#type: self.r#type,
601                host: resolved.host,
602                port: Some(resolved.port),
603                name: self.name,
604                is_locked: self.locked,
605                template_version: self.template_version,
606                update_available: resolved.update_available,
607                image: resolved.image,
608                image_override: self.image,
609                env: resolved.env,
610                env_overrides: self.env,
611                memory: resolved.memory,
612                swap: resolved.swap,
613                disk: resolved.disk,
614                io_weight: resolved.io_weight,
615                cpu: resolved.cpu,
616                memory_override: self.memory,
617                swap_override: self.swap,
618                disk_override: self.disk,
619                io_weight_override: self.io_weight,
620                cpu_override: self.cpu,
621                database_agent_template: match resolved.template {
622                    Some(template) => Some(template.into_admin_api_object(state, ()).await?),
623                    None => None,
624                },
625                created: self.created.and_utc(),
626            },
627            api_object,
628            state
629        )?;
630
631        Ok(api_object)
632    }
633}
634
635#[async_trait::async_trait]
636impl IntoAdminApiObject for ServerDatabaseInstance {
637    type AdminApiObject = AdminApiServerDatabaseInstance;
638    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
639
640    async fn into_admin_api_object<'a>(
641        self,
642        state: &crate::State,
643        storage_url_retriever: Self::ExtraArgs<'a>,
644    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
645        let api_object = AdminApiServerDatabaseInstance::init_hooks(&self, state).await?;
646        let resolved = self.resolve_admin(state).await?;
647
648        let api_object = finish_extendible!(
649            AdminApiServerDatabaseInstance {
650                uuid: self.uuid,
651                server: self
652                    .server
653                    .fetch_cached(&state.database)
654                    .await?
655                    .into_admin_api_object(state, storage_url_retriever)
656                    .await?,
657                r#type: self.r#type,
658                host: resolved.host,
659                port: Some(resolved.port),
660                name: self.name,
661                is_locked: self.locked,
662                template_version: self.template_version,
663                update_available: resolved.update_available,
664                image: resolved.image,
665                image_override: self.image,
666                env: resolved.env,
667                env_overrides: self.env,
668                memory: resolved.memory,
669                swap: resolved.swap,
670                disk: resolved.disk,
671                io_weight: resolved.io_weight,
672                cpu: resolved.cpu,
673                memory_override: self.memory,
674                swap_override: self.swap,
675                disk_override: self.disk,
676                io_weight_override: self.io_weight,
677                cpu_override: self.cpu,
678                database_agent_template: match resolved.template {
679                    Some(template) => Some(template.into_admin_api_object(state, ()).await?),
680                    None => None,
681                },
682                created: self.created.and_utc(),
683            },
684            api_object,
685            state
686        )?;
687
688        Ok(api_object)
689    }
690}
691
692#[async_trait::async_trait]
693impl IntoApiObject for ServerDatabaseInstance {
694    type ApiObject = ApiServerDatabaseInstance;
695    type ExtraArgs<'a> = ();
696
697    async fn into_api_object<'a>(
698        self,
699        state: &crate::State,
700        _args: Self::ExtraArgs<'a>,
701    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
702        let api_object = ApiServerDatabaseInstance::init_hooks(&self, state).await?;
703
704        let type_settings = self.database_agent_host.types.get(self.r#type);
705
706        let template = match &self.database_agent_template {
707            Some(template) => Some(template.fetch_cached(&state.database).await?),
708            None => None,
709        };
710
711        let update_available = match (&template, self.template_version) {
712            (Some(template), Some(template_version)) => template.version > template_version,
713            _ => false,
714        };
715
716        let api_object = finish_extendible!(
717            ApiServerDatabaseInstance {
718                uuid: self.uuid,
719                r#type: self.r#type,
720                host: type_settings.public_host.clone().or_else(|| self
721                    .database_agent_host
722                    .url
723                    .host_str()
724                    .map(compact_str::CompactString::from)),
725                port: Some(
726                    type_settings
727                        .public_port
728                        .map_or_else(|| i32::from(self.r#type.default_port()), i32::from),
729                ),
730                name: self.name,
731                is_locked: self.locked,
732                status: self.status,
733                update_available,
734                memory: self
735                    .memory
736                    .or(template.as_ref().map(|t| t.memory))
737                    .unwrap_or_default(),
738                swap: self
739                    .swap
740                    .or(template.as_ref().map(|t| t.swap))
741                    .unwrap_or_default(),
742                disk: self
743                    .disk
744                    .or(template.as_ref().map(|t| t.disk))
745                    .unwrap_or_default(),
746                io_weight: self
747                    .io_weight
748                    .or(template.as_ref().and_then(|t| t.io_weight)),
749                cpu: self
750                    .cpu
751                    .or(template.as_ref().map(|t| t.cpu))
752                    .unwrap_or_default(),
753                created: self.created.and_utc(),
754            },
755            api_object,
756            state
757        )?;
758
759        Ok(api_object)
760    }
761}
762
763#[async_trait::async_trait]
764impl ByUuid for ServerDatabaseInstance {
765    async fn by_uuid(
766        database: &crate::database::Database,
767        uuid: uuid::Uuid,
768    ) -> Result<Self, crate::database::DatabaseError> {
769        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
770            r#"
771            SELECT {}
772            FROM server_database_instances
773            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
774            WHERE server_database_instances.uuid = $1
775            "#,
776            Self::columns_sql(None)
777        )))
778        .bind(uuid)
779        .fetch_one(database.read())
780        .await?;
781
782        Self::map(None, &row)
783    }
784
785    async fn by_uuid_with_transaction(
786        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
787        uuid: uuid::Uuid,
788    ) -> Result<Self, crate::database::DatabaseError> {
789        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
790            r#"
791            SELECT {}
792            FROM server_database_instances
793            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
794            WHERE server_database_instances.uuid = $1
795            "#,
796            Self::columns_sql(None)
797        )))
798        .bind(uuid)
799        .fetch_one(&mut **transaction)
800        .await?;
801
802        Self::map(None, &row)
803    }
804}
805
806#[derive(Validate)]
807pub struct CreateServerDatabaseInstanceOptions<'a> {
808    #[garde(skip)]
809    pub uuid: uuid::Uuid,
810    #[garde(skip)]
811    pub server: &'a super::server::Server,
812    #[garde(skip)]
813    pub database_agent_host: &'a super::database_agent_host::DatabaseAgentHost,
814    #[garde(skip)]
815    pub database_agent_template: &'a super::database_agent_template::DatabaseAgentTemplate,
816
817    #[garde(length(chars, min = 1, max = 31))]
818    pub name: compact_str::CompactString,
819    #[garde(skip)]
820    pub image: Option<compact_str::CompactString>,
821}
822
823#[async_trait::async_trait]
824impl CreatableModel for ServerDatabaseInstance {
825    type CreateOptions<'a> = CreateServerDatabaseInstanceOptions<'a>;
826    type CreateResult = Self;
827
828    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
829        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerDatabaseInstance>> =
830            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
831
832        &CREATE_LISTENERS
833    }
834
835    async fn create_with_transaction(
836        state: &crate::State,
837        mut options: Self::CreateOptions<'_>,
838        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
839    ) -> Result<Self, crate::database::DatabaseError> {
840        options.validate()?;
841
842        let mut query_builder = InsertQueryBuilder::new("server_database_instances");
843
844        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
845
846        query_builder
847            .set("uuid", options.uuid)
848            .set("server_uuid", options.server.uuid)
849            .set("database_agent_host_uuid", options.database_agent_host.uuid)
850            .set(
851                "database_agent_template_uuid",
852                options.database_agent_template.uuid,
853            )
854            .set("template_version", options.database_agent_template.version)
855            .set("type", options.database_agent_template.r#type)
856            .set("name", &options.name)
857            .set("image", options.image.as_ref());
858
859        let row = query_builder
860            .returning("uuid")
861            .fetch_one(&mut **transaction)
862            .await?;
863        let uuid: uuid::Uuid = row.try_get("uuid")?;
864
865        let mut result = Self::by_uuid_with_transaction(transaction, uuid).await?;
866
867        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
868
869        Ok(result)
870    }
871}
872
873#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
874pub struct UpdateServerDatabaseInstanceOptions {
875    #[garde(length(chars, min = 1, max = 31))]
876    #[schema(min_length = 1, max_length = 31)]
877    pub name: Option<compact_str::CompactString>,
878    #[garde(skip)]
879    pub locked: Option<bool>,
880
881    #[garde(length(chars, min = 1, max = 255))]
882    #[schema(min_length = 1, max_length = 255)]
883    #[serde(
884        default,
885        skip_serializing_if = "Option::is_none",
886        with = "::serde_with::rust::double_option"
887    )]
888    pub image: Option<Option<compact_str::CompactString>>,
889    #[garde(skip)]
890    #[serde(
891        default,
892        skip_serializing_if = "Option::is_none",
893        with = "::serde_with::rust::double_option"
894    )]
895    pub env: Option<Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>>,
896
897    #[garde(inner(inner(range(min = 0))))]
898    #[schema(minimum = 0)]
899    #[serde(
900        default,
901        skip_serializing_if = "Option::is_none",
902        with = "::serde_with::rust::double_option"
903    )]
904    pub memory: Option<Option<i64>>,
905    #[garde(inner(inner(range(min = -1))))]
906    #[schema(minimum = -1)]
907    #[serde(
908        default,
909        skip_serializing_if = "Option::is_none",
910        with = "::serde_with::rust::double_option"
911    )]
912    pub swap: Option<Option<i64>>,
913    #[garde(inner(inner(range(min = 0))))]
914    #[schema(minimum = 0)]
915    #[serde(
916        default,
917        skip_serializing_if = "Option::is_none",
918        with = "::serde_with::rust::double_option"
919    )]
920    pub disk: Option<Option<i64>>,
921    #[garde(inner(inner(range(min = 0, max = 1000))))]
922    #[schema(minimum = 0, maximum = 1000)]
923    #[serde(
924        default,
925        skip_serializing_if = "Option::is_none",
926        with = "::serde_with::rust::double_option"
927    )]
928    pub io_weight: Option<Option<i16>>,
929    #[garde(inner(inner(range(min = 0))))]
930    #[schema(minimum = 0)]
931    #[serde(
932        default,
933        skip_serializing_if = "Option::is_none",
934        with = "::serde_with::rust::double_option"
935    )]
936    pub cpu: Option<Option<i32>>,
937}
938
939#[async_trait::async_trait]
940impl UpdatableModel for ServerDatabaseInstance {
941    type UpdateOptions = UpdateServerDatabaseInstanceOptions;
942
943    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
944        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerDatabaseInstance>> =
945            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
946
947        &UPDATE_LISTENERS
948    }
949
950    async fn update_with_transaction(
951        &mut self,
952        state: &crate::State,
953        mut options: Self::UpdateOptions,
954        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
955    ) -> Result<(), crate::database::DatabaseError> {
956        options.validate()?;
957
958        let mut query_builder = UpdateQueryBuilder::new("server_database_instances");
959
960        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
961            .await?;
962
963        query_builder
964            .set("name", options.name.as_ref())
965            .set("locked", options.locked)
966            .set("image", options.image.as_ref().map(|i| i.as_ref()))
967            .set(
968                "env",
969                options.env.as_ref().map(|e| e.as_ref().map(OrderedJson)),
970            )
971            .set("memory", options.memory.as_ref().map(|v| v.as_ref()))
972            .set("swap", options.swap.as_ref().map(|v| v.as_ref()))
973            .set("disk", options.disk.as_ref().map(|v| v.as_ref()))
974            .set("io_weight", options.io_weight.as_ref().map(|v| v.as_ref()))
975            .set("cpu", options.cpu.as_ref().map(|v| v.as_ref()))
976            .where_eq("uuid", self.uuid);
977
978        query_builder.execute(&mut **transaction).await?;
979
980        if let Some(name) = options.name {
981            self.name = name;
982        }
983        if let Some(locked) = options.locked {
984            self.locked = locked;
985        }
986        if let Some(image) = options.image {
987            self.image = image;
988        }
989        if let Some(env) = options.env {
990            self.env = env;
991        }
992        if let Some(memory) = options.memory {
993            self.memory = memory;
994        }
995        if let Some(swap) = options.swap {
996            self.swap = swap;
997        }
998        if let Some(disk) = options.disk {
999            self.disk = disk;
1000        }
1001        if let Some(io_weight) = options.io_weight {
1002            self.io_weight = io_weight;
1003        }
1004        if let Some(cpu) = options.cpu {
1005            self.cpu = cpu;
1006        }
1007
1008        self.run_after_update_handlers(state, transaction).await?;
1009
1010        Ok(())
1011    }
1012}
1013
1014impl ServerDatabaseInstance {
1015    pub async fn by_database_agent_template_uuid_uuid(
1016        database: &crate::database::Database,
1017        database_agent_template_uuid: uuid::Uuid,
1018        uuid: uuid::Uuid,
1019    ) -> Result<Option<Self>, crate::database::DatabaseError> {
1020        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
1021            r#"
1022            SELECT {}
1023            FROM server_database_instances
1024            JOIN database_agent_hosts ON database_agent_hosts.uuid = server_database_instances.database_agent_host_uuid
1025            WHERE server_database_instances.database_agent_template_uuid = $1 AND server_database_instances.uuid = $2
1026            "#,
1027            Self::columns_sql(None)
1028        )))
1029        .bind(database_agent_template_uuid)
1030        .bind(uuid)
1031        .fetch_optional(database.read())
1032        .await?;
1033
1034        row.try_map(|row| Self::map(None, &row))
1035    }
1036}
1037
1038#[derive(Clone, Default)]
1039pub struct DeleteServerDatabaseInstanceOptions {
1040    pub force: bool,
1041}
1042
1043#[async_trait::async_trait]
1044impl DeletableModel for ServerDatabaseInstance {
1045    type DeleteOptions = DeleteServerDatabaseInstanceOptions;
1046
1047    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
1048        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerDatabaseInstance>> =
1049            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1050
1051        &DELETE_LISTENERS
1052    }
1053
1054    async fn delete_with_transaction(
1055        &self,
1056        state: &crate::State,
1057        options: Self::DeleteOptions,
1058        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1059    ) -> Result<(), anyhow::Error> {
1060        self.run_delete_handlers(&options, state, transaction)
1061            .await?;
1062
1063        sqlx::query(
1064            r#"
1065            DELETE FROM server_database_instances
1066            WHERE server_database_instances.uuid = $1
1067            "#,
1068        )
1069        .bind(self.uuid)
1070        .execute(&mut **transaction)
1071        .await?;
1072
1073        self.run_after_delete_handlers(&options, state, transaction)
1074            .await?;
1075
1076        Ok(())
1077    }
1078
1079    async fn delete(
1080        &self,
1081        state: &crate::State,
1082        options: Self::DeleteOptions,
1083    ) -> Result<(), anyhow::Error> {
1084        let run_delete = async {
1085            self.database_agent_host
1086                .api_client(&state.database)
1087                .await?
1088                .delete_instances_instance(self.uuid)
1089                .await?;
1090
1091            Ok::<_, anyhow::Error>(())
1092        };
1093
1094        if let Err(err) = run_delete.await
1095            && !options.force
1096        {
1097            return Err(err);
1098        }
1099
1100        let mut transaction = state.database.write().begin().await?;
1101        self.delete_with_transaction(state, options, &mut transaction)
1102            .await?;
1103        transaction.commit().await?;
1104
1105        super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
1106
1107        Ok(())
1108    }
1109}
1110
1111#[derive(ToSchema, Serialize)]
1112#[schema(title = "ServerDatabaseInstanceDatabase")]
1113pub struct ApiServerDatabaseInstanceDatabase {
1114    pub uuid: uuid::Uuid,
1115
1116    pub name: compact_str::CompactString,
1117    pub created: chrono::DateTime<chrono::Local>,
1118}
1119
1120impl From<db_agent_api::StoredDatabase> for ApiServerDatabaseInstanceDatabase {
1121    fn from(database: db_agent_api::StoredDatabase) -> Self {
1122        Self {
1123            uuid: database.uuid,
1124            name: database.name,
1125            created: database.created,
1126        }
1127    }
1128}
1129
1130#[derive(ToSchema, Validate, Deserialize)]
1131pub struct ServerDatabaseInstanceUserDatabaseGrant {
1132    #[garde(skip)]
1133    pub database_uuid: uuid::Uuid,
1134    #[garde(skip)]
1135    pub permission: db_agent_api::DatabasePermission,
1136}
1137
1138impl ServerDatabaseInstanceUserDatabaseGrant {
1139    #[inline]
1140    pub fn into_api(self) -> db_agent_api::UserDatabase {
1141        db_agent_api::UserDatabase {
1142            database_uuid: self.database_uuid,
1143            permission: self.permission,
1144        }
1145    }
1146}
1147
1148#[derive(ToSchema, Serialize)]
1149#[schema(title = "ServerDatabaseInstanceUserDatabase")]
1150pub struct ApiServerDatabaseInstanceUserDatabase {
1151    pub database_uuid: uuid::Uuid,
1152
1153    pub permission: db_agent_api::DatabasePermission,
1154    pub created: chrono::DateTime<chrono::Local>,
1155}
1156
1157impl From<db_agent_api::StoredUserDatabase> for ApiServerDatabaseInstanceUserDatabase {
1158    fn from(database: db_agent_api::StoredUserDatabase) -> Self {
1159        Self {
1160            database_uuid: database.database_uuid,
1161            permission: database.permission,
1162            created: database.created,
1163        }
1164    }
1165}
1166
1167#[derive(ToSchema, Serialize)]
1168#[schema(title = "ServerDatabaseInstanceUser")]
1169pub struct ApiServerDatabaseInstanceUser {
1170    pub uuid: uuid::Uuid,
1171
1172    pub username: compact_str::CompactString,
1173    pub password: compact_str::CompactString,
1174    pub databases: Vec<ApiServerDatabaseInstanceUserDatabase>,
1175}
1176
1177impl From<db_agent_api::StoredUser> for ApiServerDatabaseInstanceUser {
1178    /// Composes the connectable username the same way the agent does (`u{short:08x}_{label}`).
1179    fn from(user: db_agent_api::StoredUser) -> Self {
1180        let short = user.uuid.as_fields().0;
1181
1182        Self {
1183            uuid: user.uuid,
1184            username: compact_str::format_compact!("u{:08x}_{}", short, user.username),
1185            password: user.password,
1186            databases: user
1187                .databases
1188                .into_iter()
1189                .map(ApiServerDatabaseInstanceUserDatabase::from)
1190                .collect(),
1191        }
1192    }
1193}
1194
1195#[schema_extension_derive::extendible]
1196#[init_args(ServerDatabaseInstance, crate::State)]
1197#[hook_args(crate::State)]
1198#[derive(ToSchema, Serialize)]
1199#[schema(title = "AdminServerServerDatabaseInstance")]
1200pub struct AdminApiServerServerDatabaseInstance {
1201    pub uuid: uuid::Uuid,
1202    pub database_agent_host: super::database_agent_host::AdminApiDatabaseAgentHost,
1203    pub database_agent_template:
1204        Option<super::database_agent_template::AdminApiDatabaseAgentTemplate>,
1205    pub template_version: Option<i32>,
1206    pub update_available: bool,
1207
1208    pub r#type: db_agent_api::DatabaseAgentType,
1209    pub host: Option<compact_str::CompactString>,
1210    pub port: Option<i32>,
1211
1212    pub name: compact_str::CompactString,
1213    pub is_locked: bool,
1214
1215    pub image: Option<compact_str::CompactString>,
1216    pub image_override: Option<compact_str::CompactString>,
1217    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1218    pub env_overrides: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
1219
1220    pub memory: i64,
1221    pub swap: i64,
1222    pub disk: i64,
1223    pub io_weight: Option<i16>,
1224    pub cpu: i32,
1225
1226    pub memory_override: Option<i64>,
1227    pub swap_override: Option<i64>,
1228    pub disk_override: Option<i64>,
1229    pub io_weight_override: Option<i16>,
1230    pub cpu_override: Option<i32>,
1231
1232    pub created: chrono::DateTime<chrono::Utc>,
1233}
1234
1235#[schema_extension_derive::extendible]
1236#[init_args(ServerDatabaseInstance, crate::State)]
1237#[hook_args(crate::State)]
1238#[derive(ToSchema, Serialize)]
1239#[schema(title = "AdminServerDatabaseInstance")]
1240pub struct AdminApiServerDatabaseInstance {
1241    pub uuid: uuid::Uuid,
1242    pub server: super::server::AdminApiServer,
1243    pub database_agent_template:
1244        Option<super::database_agent_template::AdminApiDatabaseAgentTemplate>,
1245    pub template_version: Option<i32>,
1246    pub update_available: bool,
1247
1248    pub r#type: db_agent_api::DatabaseAgentType,
1249    pub host: Option<compact_str::CompactString>,
1250    pub port: Option<i32>,
1251
1252    pub name: compact_str::CompactString,
1253    pub is_locked: bool,
1254
1255    pub image: Option<compact_str::CompactString>,
1256    pub image_override: Option<compact_str::CompactString>,
1257    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1258    pub env_overrides: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
1259
1260    pub memory: i64,
1261    pub swap: i64,
1262    pub disk: i64,
1263    pub io_weight: Option<i16>,
1264    pub cpu: i32,
1265
1266    pub memory_override: Option<i64>,
1267    pub swap_override: Option<i64>,
1268    pub disk_override: Option<i64>,
1269    pub io_weight_override: Option<i16>,
1270    pub cpu_override: Option<i32>,
1271
1272    pub created: chrono::DateTime<chrono::Utc>,
1273}
1274
1275#[schema_extension_derive::extendible]
1276#[init_args(ServerDatabaseInstance, crate::State)]
1277#[hook_args(crate::State)]
1278#[derive(ToSchema, Serialize)]
1279#[schema(title = "ServerDatabaseInstance")]
1280pub struct ApiServerDatabaseInstance {
1281    pub uuid: uuid::Uuid,
1282    pub update_available: bool,
1283
1284    pub r#type: db_agent_api::DatabaseAgentType,
1285    pub host: Option<compact_str::CompactString>,
1286    pub port: Option<i32>,
1287
1288    pub name: compact_str::CompactString,
1289    pub is_locked: bool,
1290    pub status: Option<ServerDatabaseInstanceStatus>,
1291
1292    pub memory: i64,
1293    pub swap: i64,
1294    pub disk: i64,
1295    pub io_weight: Option<i16>,
1296    pub cpu: i32,
1297
1298    pub created: chrono::DateTime<chrono::Utc>,
1299}