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