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