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 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 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}