1use super::ServerDatabase;
2use crate::{crypt::EncryptedString, models::database_host::DatabaseType};
3use compact_str::CompactString;
4use garde::Validate;
5use serde::{Deserialize, Serialize};
6use std::{
7 borrow::Cow,
8 collections::HashMap,
9 sync::{Arc, LazyLock},
10};
11use utoipa::ToSchema;
12
13mod mysql;
14mod postgres;
15
16pub const QUERY_MAX_LENGTH: usize = 65535;
17pub const QUERY_DEFAULT_ROWS: u32 = 100;
18pub const QUERY_MAX_ROWS: u32 = 1000;
19pub const QUERY_ACTIVITY_LENGTH: usize = 512;
20
21pub const BROWSE_DEFAULT_ROWS: u32 = 50;
22pub const BROWSE_MAX_ROWS: u32 = 500;
23pub const BROWSE_MAX_FILTERS: usize = 10;
24
25pub const MUTATE_MAX_ROWS: usize = 100;
26pub const CREATE_TABLE_MAX_COLUMNS: usize = 100;
27pub const SCHEMA_MAX_TABLES: usize = 1000;
28
29const QUERY_MAX_BYTES: usize = 4 * 1024 * 1024;
30const QUERY_MAX_VALUE_BYTES: usize = 256 * 1024;
31const QUERY_STATEMENT_TIMEOUT_MS: u64 = 10_000;
32const QUERY_CONNECTION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
33
34static TENANT_CONNECTIONS: LazyLock<Arc<tokio::sync::Semaphore>> =
35 LazyLock::new(|| Arc::new(tokio::sync::Semaphore::new(16)));
36const TENANT_PERMIT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
37
38const TENANT_POOL_MAX_CONNECTIONS: u32 = 4;
39const TENANT_POOL_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
40const TENANT_POOL_ACQUIRE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
41
42#[derive(Clone)]
43enum TenantPool {
44 Mysql(sqlx::Pool<sqlx::MySql>),
45 Postgres(sqlx::Pool<sqlx::Postgres>),
46}
47
48impl TenantPool {
49 async fn close(self) {
50 match self {
51 Self::Mysql(pool) => pool.close().await,
52 Self::Postgres(pool) => pool.close().await,
53 }
54 }
55}
56
57type TenantPoolValue = (std::time::Instant, EncryptedString, TenantPool);
58static TENANT_POOLS: LazyLock<Arc<tokio::sync::Mutex<HashMap<uuid::Uuid, TenantPoolValue>>>> =
59 LazyLock::new(|| {
60 let pools = Arc::new(tokio::sync::Mutex::new(HashMap::<
61 uuid::Uuid,
62 TenantPoolValue,
63 >::new()));
64
65 tokio::spawn({
66 let pools = Arc::clone(&pools);
67 async move {
68 loop {
69 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
70
71 let mut evicted = Vec::new();
72 let mut pools = pools.lock().await;
73 pools.retain(|_, (last_used, _, pool)| {
74 if last_used.elapsed() < TENANT_POOL_IDLE_TIMEOUT {
75 true
76 } else {
77 evicted.push(pool.clone());
78 false
79 }
80 });
81 drop(pools);
82
83 for pool in evicted {
84 tokio::spawn(pool.close());
85 }
86 }
87 }
88 });
89
90 pools
91 });
92
93pub struct TenantConnection {
94 inner: Box<dyn ExplorerConnection>,
95 _permit: tokio::sync::OwnedSemaphorePermit,
96}
97
98#[async_trait::async_trait]
99pub trait ExplorerConnection: Send {
100 fn close_on_drop(&mut self);
101 async fn set_read_only(&mut self, read_only: bool) -> Result<(), anyhow::Error>;
102
103 fn quote_ident(&self, identifier: &str) -> String;
104 fn qualified_table(&self, schema: Option<&str>, table: &str) -> String;
105 fn placeholder(&self, column: &SchemaColumn, index: usize) -> String;
106 fn like_prefix(&self, quoted_column: &str) -> String;
107 fn binary_literal(&self, hex: &str) -> String;
108 fn empty_insert_suffix(&self) -> &'static str;
109 fn auto_increment_keyword(&self) -> &'static str;
110 fn is_integer_type(&self, rendered: &str) -> bool;
111
112 async fn schema_tables(&mut self) -> Result<DatabaseSchema, anyhow::Error>;
113 async fn table_columns(
114 &mut self,
115 schema: Option<&str>,
116 table: &str,
117 ) -> Result<Vec<SchemaColumn>, anyhow::Error>;
118 async fn column_types(&mut self) -> Result<Vec<String>, anyhow::Error>;
119 async fn resolve_type(&mut self, input: &str) -> Result<String, anyhow::Error>;
120
121 async fn quote_values(&mut self, values: &[String]) -> Result<Vec<String>, anyhow::Error>;
122 async fn fetch_unprepared(
123 &mut self,
124 sql: String,
125 max_rows: usize,
126 ) -> Result<QueryResultSet, anyhow::Error>;
127 async fn run_query(
128 &mut self,
129 sql: &str,
130 max_rows: usize,
131 ) -> Result<Vec<QueryResultSet>, anyhow::Error>;
132 async fn apply_statements(
133 &mut self,
134 statements: Vec<Statement>,
135 expects_single_row: bool,
136 ) -> Result<u64, anyhow::Error>;
137 async fn execute_ddl(&mut self, sql: String) -> Result<(), anyhow::Error>;
138}
139
140fn display(message: impl Into<Cow<'static, str>>) -> anyhow::Error {
141 crate::response::DisplayError::new(message).into()
142}
143
144fn unknown_table(table: &str) -> anyhow::Error {
145 crate::response::DisplayError::new(format!("table {table} does not exist"))
146 .with_status(axum::http::StatusCode::NOT_FOUND)
147 .into()
148}
149
150fn unknown_column(column: &str) -> anyhow::Error {
151 crate::response::DisplayError::new(format!("column {column} does not exist"))
152 .with_status(axum::http::StatusCode::NOT_FOUND)
153 .into()
154}
155
156fn unsupported_engine() -> anyhow::Error {
157 crate::response::DisplayError::new("querying MongoDB databases is not supported")
158 .with_status(axum::http::StatusCode::EXPECTATION_FAILED)
159 .into()
160}
161
162fn query_error(err: sqlx::Error) -> anyhow::Error {
163 match &err {
164 sqlx::Error::Database(database_error) => {
165 crate::response::DisplayError::new(database_error.message().to_string()).into()
166 }
167 _ => err.into(),
168 }
169}
170
171impl ServerDatabase {
172 pub async fn connect_as_tenant(
175 &mut self,
176 database: &crate::database::Database,
177 read_only: bool,
178 ) -> Result<TenantConnection, anyhow::Error> {
179 let permit = tokio::time::timeout(
180 TENANT_PERMIT_TIMEOUT,
181 TENANT_CONNECTIONS.clone().acquire_owned(),
182 )
183 .await
184 .map_err(|_| {
185 crate::response::DisplayError::new("too many concurrent database connections")
186 .with_status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
187 })??;
188
189 let pool = {
190 let mut pools = TENANT_POOLS.lock().await;
191 match pools.get_mut(&self.uuid) {
192 Some((last_used, password, pool)) if *password == self.password => {
193 *last_used = std::time::Instant::now();
194 Some(pool.clone())
195 }
196 _ => None,
197 }
198 };
199
200 let pool = match pool {
201 Some(pool) => pool,
202 None => {
203 let details = self
204 .database_host
205 .credentials
206 .parse_connection_details(database)
207 .await?;
208 let password = self.password.decrypt(database).await?;
209
210 let pool = match self.database_host.r#type {
211 DatabaseType::Mysql => TenantPool::Mysql(mysql::MysqlExplorer::create_pool(
212 &details.host,
213 details.port,
214 &self.username,
215 &password,
216 &self.name,
217 )),
218 DatabaseType::Postgres => {
219 TenantPool::Postgres(postgres::PostgresExplorer::create_pool(
220 &details.host,
221 details.port,
222 &self.username,
223 &password,
224 &self.name,
225 ))
226 }
227 DatabaseType::Mongodb => return Err(unsupported_engine()),
228 };
229
230 TENANT_POOLS.lock().await.insert(
231 self.uuid,
232 (
233 std::time::Instant::now(),
234 self.password.clone(),
235 pool.clone(),
236 ),
237 );
238
239 pool
240 }
241 };
242
243 let mut inner: Box<dyn ExplorerConnection> = match pool {
244 TenantPool::Mysql(pool) => Box::new(mysql::MysqlExplorer {
245 connection: pool.acquire().await?,
246 database: self.name.to_string(),
247 }),
248 TenantPool::Postgres(pool) => Box::new(postgres::PostgresExplorer {
249 connection: pool.acquire().await?,
250 }),
251 };
252
253 inner.set_read_only(read_only).await?;
254
255 Ok(TenantConnection {
256 inner,
257 _permit: permit,
258 })
259 }
260
261 pub async fn run_query(
262 &mut self,
263 database: &crate::database::Database,
264 sql: &str,
265 max_rows: u32,
266 read_only: bool,
267 ) -> Result<Vec<QueryResultSet>, anyhow::Error> {
268 let mut connection = self.connect_as_tenant(database, read_only).await?;
269 connection.inner.close_on_drop();
270
271 let results = tokio::time::timeout(QUERY_CONNECTION_TIMEOUT, async move {
272 connection
273 .inner
274 .run_query(sql, max_rows.min(QUERY_MAX_ROWS) as usize)
275 .await
276 })
277 .await
278 .map_err(|_| {
279 crate::response::DisplayError::new("query timed out")
280 .with_status(axum::http::StatusCode::REQUEST_TIMEOUT)
281 })??;
282
283 Ok(results)
284 }
285}
286
287struct ResultCollector {
288 max_rows: usize,
289 results: Vec<QueryResultSet>,
290 columns: Vec<QueryColumn>,
291 rows: Vec<Vec<QueryValue>>,
292 bytes: usize,
293 truncated: bool,
294}
295
296impl ResultCollector {
297 fn new(max_rows: usize) -> Self {
298 Self {
299 max_rows,
300 results: Vec::new(),
301 columns: Vec::new(),
302 rows: Vec::new(),
303 bytes: 0,
304 truncated: false,
305 }
306 }
307
308 fn push(
309 &mut self,
310 columns: impl FnOnce() -> Vec<QueryColumn>,
311 row: impl FnOnce() -> Vec<QueryValue>,
312 ) {
313 if self.columns.is_empty() {
314 self.columns = columns();
315 }
316
317 if self.rows.len() >= self.max_rows || self.bytes >= QUERY_MAX_BYTES {
318 self.truncated = true;
319 return;
320 }
321
322 let row = row();
323 let bytes = row.iter().map(QueryValue::byte_len).sum::<usize>();
324
325 if self.bytes + bytes > QUERY_MAX_BYTES {
326 self.bytes = QUERY_MAX_BYTES;
327 self.truncated = true;
328 return;
329 }
330
331 self.bytes += bytes;
332 self.rows.push(row);
333 }
334
335 fn finish_set(&mut self, rows_affected: u64) {
336 self.results.push(QueryResultSet {
337 columns: std::mem::take(&mut self.columns),
338 rows: std::mem::take(&mut self.rows),
339 rows_affected,
340 truncated: std::mem::take(&mut self.truncated),
341 });
342 }
343
344 fn into_results(mut self) -> Vec<QueryResultSet> {
345 if !self.columns.is_empty() || !self.rows.is_empty() {
346 self.finish_set(0);
347 }
348
349 self.results
350 }
351}
352
353#[derive(ToSchema, Serialize, Clone)]
354pub struct QueryColumn {
355 pub name: CompactString,
356 pub type_name: CompactString,
357 #[serde(skip)]
358 pub type_oid: Option<u32>,
359 pub binary: bool,
360}
361
362#[derive(ToSchema, Serialize, Clone)]
363#[serde(tag = "type", rename_all = "snake_case")]
364pub enum QueryValue {
365 Null,
366 Text { value: String, truncated: bool },
367 Binary { value: String, truncated: bool },
368}
369
370impl QueryValue {
371 fn from_bytes(bytes: Option<&[u8]>) -> Self {
372 let Some(bytes) = bytes else {
373 return Self::Null;
374 };
375
376 match std::str::from_utf8(bytes) {
377 Ok(value) => Self::text(value),
378 Err(_) => Self::binary(bytes),
379 }
380 }
381
382 fn text(value: &str) -> Self {
383 Self::Text {
384 value: crate::utils::slice_up_to(value, QUERY_MAX_VALUE_BYTES).to_owned(),
385 truncated: value.len() > QUERY_MAX_VALUE_BYTES,
386 }
387 }
388
389 fn binary(bytes: &[u8]) -> Self {
390 let max = QUERY_MAX_VALUE_BYTES / 2;
391
392 Self::Binary {
393 value: hex::encode(&bytes[..bytes.len().min(max)]),
394 truncated: bytes.len() > max,
395 }
396 }
397
398 fn hex(hex: &[u8]) -> Self {
399 let max = QUERY_MAX_VALUE_BYTES & !1;
400 let cut = hex.len().min(max);
401
402 Self::Binary {
403 value: String::from_utf8_lossy(&hex[..cut]).into_owned(),
404 truncated: hex.len() > cut,
405 }
406 }
407
408 fn byte_len(&self) -> usize {
409 match self {
410 Self::Null => 0,
411 Self::Text { value, .. } | Self::Binary { value, .. } => value.len(),
412 }
413 }
414}
415
416#[derive(ToSchema, Serialize, Clone)]
417pub struct QueryResultSet {
418 pub columns: Vec<QueryColumn>,
419 pub rows: Vec<Vec<QueryValue>>,
420 pub rows_affected: u64,
421 pub truncated: bool,
422}
423
424#[derive(ToSchema, Serialize, Clone)]
425pub struct SchemaColumn {
426 pub name: CompactString,
427 pub type_name: CompactString,
428 #[serde(skip)]
429 pub cast_type: Option<CompactString>,
430 pub nullable: bool,
431 pub default: Option<String>,
432 pub primary_key: bool,
433 pub auto_increment: bool,
434 pub generated: bool,
435 pub binary: bool,
436 pub enum_values: Option<Vec<CompactString>>,
437 pub set_values: Option<Vec<CompactString>>,
438}
439
440#[derive(ToSchema, Serialize, Clone)]
441pub struct SchemaTable {
442 pub schema: Option<CompactString>,
443 pub name: CompactString,
444 pub view: bool,
445 pub row_estimate: Option<i64>,
446 pub columns: Vec<SchemaColumn>,
447}
448
449#[derive(ToSchema, Serialize, Clone)]
450pub struct DatabaseSchema {
451 pub tables: Vec<SchemaTable>,
452 pub truncated: bool,
453}
454
455impl ServerDatabase {
456 pub async fn get_schema(
457 &mut self,
458 database: &crate::database::Database,
459 ) -> Result<DatabaseSchema, anyhow::Error> {
460 let mut connection = self.connect_as_tenant(database, true).await?;
461
462 let result = tokio::time::timeout(QUERY_CONNECTION_TIMEOUT, async {
463 connection.inner.schema_tables().await
464 })
465 .await;
466
467 match result {
468 Ok(result) => result,
469 Err(_) => {
470 connection.inner.close_on_drop();
471
472 Err(crate::response::DisplayError::new("query timed out")
473 .with_status(axum::http::StatusCode::REQUEST_TIMEOUT)
474 .into())
475 }
476 }
477 }
478}
479
480#[derive(ToSchema, Deserialize, PartialEq, Eq, Clone, Copy)]
481#[serde(rename_all = "snake_case")]
482pub enum FilterOperator {
483 Eq,
484 Ne,
485 Lt,
486 Lte,
487 Gt,
488 Gte,
489 Contains,
490 StartsWith,
491 EndsWith,
492 IsNull,
493 NotNull,
494}
495
496impl FilterOperator {
497 fn comparison(self) -> Option<&'static str> {
498 match self {
499 Self::Eq => Some("="),
500 Self::Ne => Some("<>"),
501 Self::Lt => Some("<"),
502 Self::Lte => Some("<="),
503 Self::Gt => Some(">"),
504 Self::Gte => Some(">="),
505 _ => None,
506 }
507 }
508}
509
510#[derive(ToSchema, Validate, Deserialize)]
511pub struct BrowseFilter {
512 #[garde(length(chars, min = 1, max = 255))]
513 #[schema(min_length = 1, max_length = 255)]
514 pub column: CompactString,
515
516 #[garde(skip)]
517 pub operator: FilterOperator,
518
519 #[garde(inner(length(chars, max = 4096)))]
520 #[schema(max_length = 4096)]
521 pub value: Option<String>,
522}
523
524fn default_browse_limit() -> u32 {
525 BROWSE_DEFAULT_ROWS
526}
527
528#[derive(ToSchema, Validate, Deserialize)]
529pub struct BrowseOptions {
530 #[garde(inner(length(chars, min = 1, max = 255)))]
531 #[schema(min_length = 1, max_length = 255)]
532 pub schema: Option<CompactString>,
533
534 #[garde(length(chars, min = 1, max = 255))]
535 #[schema(min_length = 1, max_length = 255)]
536 pub table: CompactString,
537
538 #[garde(inner(length(chars, min = 1, max = 255)))]
539 #[schema(min_length = 1, max_length = 255)]
540 pub order_by: Option<CompactString>,
541
542 #[garde(skip)]
543 #[serde(default)]
544 pub descending: bool,
545
546 #[garde(range(min = 1, max = BROWSE_MAX_ROWS))]
547 #[schema(minimum = 1, maximum = 500)]
548 #[serde(default = "default_browse_limit")]
549 pub limit: u32,
550
551 #[garde(skip)]
552 #[serde(default)]
553 pub offset: u64,
554
555 #[garde(length(max = BROWSE_MAX_FILTERS), dive)]
556 #[schema(max_items = 10)]
557 #[serde(default)]
558 pub filters: Vec<BrowseFilter>,
559}
560
561struct FilterClause {
562 prefix: String,
563 value: Option<String>,
564 suffix: &'static str,
565}
566
567impl FilterClause {
568 fn complete(sql: String) -> Self {
569 Self {
570 prefix: sql,
571 value: None,
572 suffix: "",
573 }
574 }
575}
576
577fn like_pattern(operator: FilterOperator, value: &str) -> String {
578 let mut escaped = String::with_capacity(value.len() + 2);
579 for c in value.chars() {
580 if matches!(c, '%' | '_' | '!') {
581 escaped.push('!');
582 }
583 escaped.push(c);
584 }
585
586 match operator {
587 FilterOperator::Contains => format!("%{escaped}%"),
588 FilterOperator::StartsWith => format!("{escaped}%"),
589 FilterOperator::EndsWith => format!("%{escaped}"),
590 _ => unreachable!(),
591 }
592}
593
594fn filter_clause(
595 connection: &dyn ExplorerConnection,
596 columns: &[SchemaColumn],
597 filter: &BrowseFilter,
598) -> Result<FilterClause, anyhow::Error> {
599 let column = columns
600 .iter()
601 .find(|column| column.name == filter.column)
602 .ok_or_else(|| unknown_column(&filter.column))?;
603 let quoted = connection.quote_ident(&column.name);
604
605 if matches!(
606 filter.operator,
607 FilterOperator::IsNull | FilterOperator::NotNull
608 ) {
609 if filter.value.is_some() {
610 return Err(display(format!(
611 "a null check on {} does not take a value",
612 column.name
613 )));
614 }
615
616 let check = if matches!(filter.operator, FilterOperator::IsNull) {
617 "IS NULL"
618 } else {
619 "IS NOT NULL"
620 };
621
622 return Ok(FilterClause::complete(format!("{quoted} {check}")));
623 }
624
625 let value = filter
626 .value
627 .as_deref()
628 .ok_or_else(|| display(format!("a value is required to filter on {}", column.name)))?;
629
630 if column.binary {
631 if !matches!(filter.operator, FilterOperator::Eq | FilterOperator::Ne) {
632 return Err(display(format!(
633 "binary column {} only supports equality and null checks",
634 column.name
635 )));
636 }
637
638 check_hex(column, &filter.value)?;
639 let operator = if matches!(filter.operator, FilterOperator::Eq) {
640 "="
641 } else {
642 "<>"
643 };
644
645 return Ok(FilterClause::complete(format!(
646 "{quoted} {operator} {}",
647 connection.binary_literal(value)
648 )));
649 }
650
651 if let Some(comparison) = filter.operator.comparison() {
652 return Ok(FilterClause {
653 prefix: format!("{quoted} {comparison} "),
654 value: Some(value.to_string()),
655 suffix: "",
656 });
657 }
658
659 Ok(FilterClause {
660 prefix: connection.like_prefix("ed),
661 value: Some(like_pattern(filter.operator, value)),
662 suffix: " ESCAPE '!'",
663 })
664}
665
666fn assemble_where(
667 clauses: &[FilterClause],
668 literals: Vec<String>,
669) -> Result<Option<String>, anyhow::Error> {
670 if clauses.is_empty() {
671 return Ok(None);
672 }
673
674 let mut literals = literals.into_iter();
675 let mut parts = Vec::with_capacity(clauses.len());
676
677 for clause in clauses {
678 if clause.value.is_none() {
679 parts.push(clause.prefix.clone());
680 continue;
681 }
682
683 let Some(literal) = literals.next() else {
684 return Err(display("filter values and literals fell out of step"));
685 };
686
687 parts.push(format!("{}{literal}{}", clause.prefix, clause.suffix));
688 }
689
690 Ok(Some(format!(" WHERE {}", parts.join(" AND "))))
691}
692
693impl ServerDatabase {
694 pub async fn browse_rows(
695 &mut self,
696 database: &crate::database::Database,
697 options: &BrowseOptions,
698 ) -> Result<QueryResultSet, anyhow::Error> {
699 let direction = if options.descending { "DESC" } else { "ASC" };
700
701 let mut connection = self.connect_as_tenant(database, true).await?;
702
703 let result = tokio::time::timeout(QUERY_CONNECTION_TIMEOUT, async {
704 let columns = connection
705 .inner
706 .table_columns(options.schema.as_deref(), &options.table)
707 .await?;
708 if columns.is_empty() {
709 return Err(unknown_table(&options.table));
710 }
711
712 let clauses = options
713 .filters
714 .iter()
715 .map(|filter| filter_clause(&*connection.inner, &columns, filter))
716 .collect::<Result<Vec<_>, _>>()?;
717 let pending: Vec<String> = clauses
718 .iter()
719 .filter_map(|clause| clause.value.clone())
720 .collect();
721 let literals = if pending.is_empty() {
722 Vec::new()
723 } else {
724 connection.inner.quote_values(&pending).await?
725 };
726
727 let mut sql = format!(
728 "SELECT * FROM {}",
729 connection
730 .inner
731 .qualified_table(options.schema.as_deref(), &options.table)
732 );
733 if let Some(where_clause) = assemble_where(&clauses, literals)? {
734 sql.push_str(&where_clause);
735 }
736 if let Some(order_by) = &options.order_by {
737 if !columns.iter().any(|column| column.name == *order_by) {
738 return Err(unknown_column(order_by));
739 }
740
741 sql.push_str(&format!(
742 " ORDER BY {} {direction}",
743 connection.inner.quote_ident(order_by)
744 ));
745 }
746 sql.push_str(&format!(
747 " LIMIT {} OFFSET {}",
748 options.limit, options.offset
749 ));
750
751 connection
752 .inner
753 .fetch_unprepared(sql, options.limit as usize)
754 .await
755 })
756 .await;
757
758 match result {
759 Ok(result) => result,
760 Err(_) => {
761 connection.inner.close_on_drop();
762
763 Err(crate::response::DisplayError::new("query timed out")
764 .with_status(axum::http::StatusCode::REQUEST_TIMEOUT)
765 .into())
766 }
767 }
768 }
769}
770
771#[derive(ToSchema, Validate, Deserialize, Clone)]
772pub struct RowValue {
773 #[garde(length(chars, min = 1, max = 255))]
774 #[schema(min_length = 1, max_length = 255)]
775 pub column: CompactString,
776
777 #[garde(skip)]
778 pub value: Option<String>,
779}
780
781#[derive(ToSchema, Validate, Deserialize)]
782pub struct RowUpdate {
783 #[garde(dive)]
784 pub keys: Vec<RowValue>,
785 #[garde(dive)]
786 pub values: Vec<RowValue>,
787}
788
789#[derive(ToSchema, Validate, Deserialize)]
790pub struct RowInsert {
791 #[garde(dive)]
792 pub values: Vec<RowValue>,
793}
794
795#[derive(ToSchema, Validate, Deserialize)]
796pub struct RowDelete {
797 #[garde(dive)]
798 pub keys: Vec<RowValue>,
799}
800
801fn check_hex(column: &SchemaColumn, value: &Option<String>) -> Result<(), anyhow::Error> {
802 let Some(value) = value else {
803 return Ok(());
804 };
805
806 if column.binary && (value.len() % 2 != 0 || !value.chars().all(|c| c.is_ascii_hexdigit())) {
807 return Err(display(format!(
808 "value for {} is not valid hexadecimal",
809 column.name
810 )));
811 }
812
813 Ok(())
814}
815
816fn resolve<'a>(
817 columns: &'a [SchemaColumn],
818 entry: &RowValue,
819 writable: bool,
820) -> Result<&'a SchemaColumn, anyhow::Error> {
821 let column = columns
822 .iter()
823 .find(|column| column.name == entry.column)
824 .ok_or_else(|| unknown_column(&entry.column))?;
825
826 if writable && column.generated {
827 return Err(display(format!("column {} is generated", column.name)));
828 }
829
830 check_hex(column, &entry.value)?;
831
832 Ok(column)
833}
834
835fn check_keys(columns: &[SchemaColumn], keys: &[RowValue]) -> Result<(), anyhow::Error> {
836 let primary: Vec<&str> = columns
837 .iter()
838 .filter(|column| column.primary_key)
839 .map(|column| column.name.as_str())
840 .collect();
841
842 if primary.is_empty() {
843 return Err(display(
844 "this table has no primary key, so its rows cannot be addressed",
845 ));
846 }
847
848 if primary.len() != keys.len()
849 || !primary
850 .iter()
851 .all(|name| keys.iter().any(|key| key.column.as_str() == *name))
852 {
853 return Err(display("the primary key of the row must be given in full"));
854 }
855
856 Ok(())
857}
858
859fn check_batch(rows: usize) -> Result<(), anyhow::Error> {
860 if rows == 0 {
861 return Err(display("no rows were given"));
862 }
863
864 if rows > MUTATE_MAX_ROWS {
865 return Err(display(format!("at most {MUTATE_MAX_ROWS} rows at a time")));
866 }
867
868 Ok(())
869}
870
871impl ServerDatabase {
872 pub async fn mutate_rows(
873 &mut self,
874 database: &crate::database::Database,
875 schema: Option<&str>,
876 table: &str,
877 operation: RowOperation<'_>,
878 ) -> Result<u64, anyhow::Error> {
879 check_batch(operation.len())?;
880
881 let mut connection = self.connect_as_tenant(database, false).await?;
882
883 let result = tokio::time::timeout(QUERY_CONNECTION_TIMEOUT, async {
884 let columns = connection.inner.table_columns(schema, table).await?;
885 if columns.is_empty() {
886 return Err(unknown_table(table));
887 }
888
889 let statements = operation.statements(&*connection.inner, &columns, schema, table)?;
890
891 connection
892 .inner
893 .apply_statements(statements, operation.expects_single_row())
894 .await
895 })
896 .await;
897
898 match result {
899 Ok(result) => result,
900 Err(_) => {
901 connection.inner.close_on_drop();
902
903 Err(crate::response::DisplayError::new("statement timed out")
904 .with_status(axum::http::StatusCode::REQUEST_TIMEOUT)
905 .into())
906 }
907 }
908 }
909}
910
911pub enum RowOperation<'a> {
912 Insert(&'a [RowInsert]),
913 Update(&'a [RowUpdate]),
914 Delete(&'a [RowDelete]),
915}
916
917type Statement = (String, Vec<Option<String>>);
918
919impl RowOperation<'_> {
920 fn len(&self) -> usize {
921 match self {
922 Self::Insert(rows) => rows.len(),
923 Self::Update(rows) => rows.len(),
924 Self::Delete(rows) => rows.len(),
925 }
926 }
927
928 fn expects_single_row(&self) -> bool {
929 !matches!(self, Self::Delete(_))
930 }
931
932 fn statements(
933 &self,
934 connection: &dyn ExplorerConnection,
935 columns: &[SchemaColumn],
936 schema: Option<&str>,
937 table: &str,
938 ) -> Result<Vec<Statement>, anyhow::Error> {
939 let quoted = connection.qualified_table(schema, table);
940
941 match self {
942 Self::Insert(rows) => rows
943 .iter()
944 .map(|row| {
945 let mut names = Vec::new();
946 let mut placeholders = Vec::new();
947 let mut binds = Vec::new();
948
949 for entry in &row.values {
950 let column = resolve(columns, entry, true)?;
951 names.push(connection.quote_ident(&column.name));
952 placeholders.push(connection.placeholder(column, binds.len() + 1));
953 binds.push(entry.value.clone());
954 }
955
956 if names.is_empty() {
957 return Ok((
958 format!("INSERT INTO {quoted} {}", connection.empty_insert_suffix()),
959 binds,
960 ));
961 }
962
963 Ok((
964 format!(
965 "INSERT INTO {quoted} ({}) VALUES ({})",
966 names.join(", "),
967 placeholders.join(", ")
968 ),
969 binds,
970 ))
971 })
972 .collect(),
973 Self::Update(rows) => rows
974 .iter()
975 .map(|row| {
976 check_keys(columns, &row.keys)?;
977
978 let mut assignments = Vec::new();
979 let mut binds = Vec::new();
980
981 for entry in &row.values {
982 let column = resolve(columns, entry, true)?;
983 assignments.push(format!(
984 "{} = {}",
985 connection.quote_ident(&column.name),
986 connection.placeholder(column, binds.len() + 1)
987 ));
988 binds.push(entry.value.clone());
989 }
990
991 if assignments.is_empty() {
992 return Err(display("no columns were given to update"));
993 }
994
995 let (where_clause, key_binds) =
996 build_where(connection, columns, &row.keys, binds.len())?;
997 binds.extend(key_binds);
998
999 Ok((
1000 format!(
1001 "UPDATE {quoted} SET {} WHERE {where_clause}",
1002 assignments.join(", ")
1003 ),
1004 binds,
1005 ))
1006 })
1007 .collect(),
1008 Self::Delete(rows) => rows
1009 .iter()
1010 .map(|row| {
1011 check_keys(columns, &row.keys)?;
1012
1013 let (where_clause, binds) = build_where(connection, columns, &row.keys, 0)?;
1014
1015 Ok((format!("DELETE FROM {quoted} WHERE {where_clause}"), binds))
1016 })
1017 .collect(),
1018 }
1019 }
1020}
1021
1022fn build_where(
1023 connection: &dyn ExplorerConnection,
1024 columns: &[SchemaColumn],
1025 keys: &[RowValue],
1026 offset: usize,
1027) -> Result<(String, Vec<Option<String>>), anyhow::Error> {
1028 let mut clauses = Vec::new();
1029 let mut binds = Vec::new();
1030
1031 for entry in keys {
1032 let column = resolve(columns, entry, false)?;
1033 clauses.push(format!(
1034 "{} = {}",
1035 connection.quote_ident(&column.name),
1036 connection.placeholder(column, offset + binds.len() + 1)
1037 ));
1038 binds.push(entry.value.clone());
1039 }
1040
1041 Ok((clauses.join(" AND "), binds))
1042}
1043
1044#[derive(ToSchema, Validate, Deserialize, Clone)]
1045pub struct ColumnDefinition {
1046 #[garde(length(chars, min = 1, max = 255))]
1047 #[schema(min_length = 1, max_length = 255)]
1048 pub name: CompactString,
1049
1050 #[garde(length(chars, min = 1, max = 64))]
1051 #[schema(min_length = 1, max_length = 64)]
1052 pub r#type: CompactString,
1053
1054 #[garde(skip)]
1055 #[serde(default)]
1056 pub nullable: bool,
1057
1058 #[garde(skip)]
1059 #[serde(default)]
1060 pub primary_key: bool,
1061
1062 #[garde(skip)]
1063 #[serde(default)]
1064 pub auto_increment: bool,
1065}
1066
1067fn render_type(types: &[&str], input: &str) -> Result<String, anyhow::Error> {
1068 let input = input.trim().to_ascii_lowercase();
1069 let invalid = || display(format!("{input} is not a supported column type"));
1070
1071 let (raw_base, args) = match input.split_once('(') {
1072 Some((base, rest)) => {
1073 let args = rest.strip_suffix(')').ok_or_else(invalid)?;
1074 (base, Some(args))
1075 }
1076 None => (input.as_str(), None),
1077 };
1078
1079 let base = raw_base.split_whitespace().collect::<Vec<_>>().join(" ");
1080 let base = *types
1081 .iter()
1082 .find(|entry| **entry == base)
1083 .ok_or_else(invalid)?;
1084
1085 let Some(args) = args else {
1086 return Ok(base.to_string());
1087 };
1088
1089 let args: Vec<&str> = args.split(',').map(str::trim).collect();
1090 if args.len() > 2
1091 || args
1092 .iter()
1093 .any(|arg| arg.is_empty() || arg.len() > 10 || !arg.chars().all(|c| c.is_ascii_digit()))
1094 {
1095 return Err(invalid());
1096 }
1097
1098 Ok(format!("{base}({})", args.join(",")))
1099}
1100
1101fn render_column(
1102 connection: &dyn ExplorerConnection,
1103 column: &ColumnDefinition,
1104 rendered: &str,
1105) -> Result<String, anyhow::Error> {
1106 if column.auto_increment {
1107 if !column.primary_key {
1108 return Err(display(format!(
1109 "column {} must be part of the primary key to auto increment",
1110 column.name
1111 )));
1112 }
1113
1114 if !connection.is_integer_type(rendered) {
1115 return Err(display(format!(
1116 "column {} must be an integer type to auto increment",
1117 column.name
1118 )));
1119 }
1120 }
1121
1122 let name = connection.quote_ident(&column.name);
1123
1124 let mut definition = format!("{name} {rendered}");
1125 if !column.nullable {
1126 definition.push_str(" NOT NULL");
1127 }
1128 if column.auto_increment {
1129 definition.push(' ');
1130 definition.push_str(connection.auto_increment_keyword());
1131 }
1132
1133 Ok(definition)
1134}
1135
1136fn create_table_sql(
1137 connection: &dyn ExplorerConnection,
1138 qualified: &str,
1139 columns: &[ColumnDefinition],
1140 rendered_types: &[String],
1141) -> Result<String, anyhow::Error> {
1142 for (index, column) in columns.iter().enumerate() {
1143 if columns[..index]
1144 .iter()
1145 .any(|other| other.name.eq_ignore_ascii_case(&column.name))
1146 {
1147 return Err(display(format!("column {} is given twice", column.name)));
1148 }
1149 }
1150
1151 if columns
1152 .iter()
1153 .filter(|column| column.auto_increment)
1154 .count()
1155 > 1
1156 {
1157 return Err(display("only one column can auto increment"));
1158 }
1159
1160 let mut definitions = columns
1161 .iter()
1162 .zip(rendered_types)
1163 .map(|(column, rendered)| render_column(connection, column, rendered))
1164 .collect::<Result<Vec<_>, _>>()?;
1165
1166 let primary: Vec<String> = columns
1167 .iter()
1168 .filter(|column| column.primary_key)
1169 .map(|column| connection.quote_ident(&column.name))
1170 .collect();
1171 if !primary.is_empty() {
1172 definitions.push(format!("PRIMARY KEY ({})", primary.join(", ")));
1173 }
1174
1175 Ok(format!(
1176 "CREATE TABLE {qualified} ({})",
1177 definitions.join(", ")
1178 ))
1179}
1180
1181impl ServerDatabase {
1182 pub async fn column_types(
1183 &mut self,
1184 database: &crate::database::Database,
1185 ) -> Result<Vec<String>, anyhow::Error> {
1186 if matches!(self.database_host.r#type, DatabaseType::Mysql) {
1187 return Ok(mysql::MYSQL_TYPES.iter().map(ToString::to_string).collect());
1188 }
1189
1190 let mut connection = self.connect_as_tenant(database, true).await?;
1191
1192 connection.inner.column_types().await
1193 }
1194
1195 pub async fn create_table(
1196 &mut self,
1197 database: &crate::database::Database,
1198 schema: Option<&str>,
1199 table: &str,
1200 columns: &[ColumnDefinition],
1201 ) -> Result<(), anyhow::Error> {
1202 let mut connection = self.connect_as_tenant(database, false).await?;
1203 connection.inner.close_on_drop();
1204
1205 run_ddl(async move {
1206 let mut rendered = Vec::with_capacity(columns.len());
1207 for column in columns {
1208 rendered.push(connection.inner.resolve_type(&column.r#type).await?);
1209 }
1210
1211 let qualified = connection.inner.qualified_table(schema, table);
1212 let sql = create_table_sql(&*connection.inner, &qualified, columns, &rendered)?;
1213
1214 connection.inner.execute_ddl(sql).await
1215 })
1216 .await
1217 }
1218
1219 async fn run_table_ddl(
1220 &mut self,
1221 database: &crate::database::Database,
1222 schema: Option<&str>,
1223 table: &str,
1224 build: impl FnOnce(
1225 &dyn ExplorerConnection,
1226 &str,
1227 &[SchemaColumn],
1228 ) -> Result<String, anyhow::Error>,
1229 ) -> Result<(), anyhow::Error> {
1230 let mut connection = self.connect_as_tenant(database, false).await?;
1231 connection.inner.close_on_drop();
1232
1233 run_ddl(async move {
1234 let columns = connection.inner.table_columns(schema, table).await?;
1235 if columns.is_empty() {
1236 return Err(unknown_table(table));
1237 }
1238
1239 let qualified = connection.inner.qualified_table(schema, table);
1240 let sql = build(&*connection.inner, &qualified, &columns)?;
1241
1242 connection.inner.execute_ddl(sql).await
1243 })
1244 .await
1245 }
1246
1247 pub async fn rename_table(
1248 &mut self,
1249 database: &crate::database::Database,
1250 schema: Option<&str>,
1251 table: &str,
1252 new_name: &str,
1253 ) -> Result<(), anyhow::Error> {
1254 self.run_table_ddl(
1255 database,
1256 schema,
1257 table,
1258 |connection, qualified, _columns| {
1259 Ok(format!(
1260 "ALTER TABLE {qualified} RENAME TO {}",
1261 connection.quote_ident(new_name)
1262 ))
1263 },
1264 )
1265 .await
1266 }
1267
1268 pub async fn drop_table(
1269 &mut self,
1270 database: &crate::database::Database,
1271 schema: Option<&str>,
1272 table: &str,
1273 ) -> Result<(), anyhow::Error> {
1274 self.run_table_ddl(
1275 database,
1276 schema,
1277 table,
1278 |_connection, qualified, _columns| Ok(format!("DROP TABLE {qualified}")),
1279 )
1280 .await
1281 }
1282
1283 pub async fn rename_column(
1284 &mut self,
1285 database: &crate::database::Database,
1286 schema: Option<&str>,
1287 table: &str,
1288 column: &str,
1289 new_name: &str,
1290 ) -> Result<(), anyhow::Error> {
1291 self.run_table_ddl(database, schema, table, |connection, qualified, columns| {
1292 if !columns.iter().any(|entry| entry.name == column) {
1293 return Err(unknown_column(column));
1294 }
1295
1296 Ok(format!(
1297 "ALTER TABLE {qualified} RENAME COLUMN {} TO {}",
1298 connection.quote_ident(column),
1299 connection.quote_ident(new_name)
1300 ))
1301 })
1302 .await
1303 }
1304
1305 pub async fn drop_column(
1306 &mut self,
1307 database: &crate::database::Database,
1308 schema: Option<&str>,
1309 table: &str,
1310 column: &str,
1311 ) -> Result<(), anyhow::Error> {
1312 self.run_table_ddl(database, schema, table, |connection, qualified, columns| {
1313 if !columns.iter().any(|entry| entry.name == column) {
1314 return Err(unknown_column(column));
1315 }
1316
1317 Ok(format!(
1318 "ALTER TABLE {qualified} DROP COLUMN {}",
1319 connection.quote_ident(column)
1320 ))
1321 })
1322 .await
1323 }
1324
1325 pub async fn add_column(
1326 &mut self,
1327 database: &crate::database::Database,
1328 schema: Option<&str>,
1329 table: &str,
1330 column: &ColumnDefinition,
1331 ) -> Result<(), anyhow::Error> {
1332 if column.primary_key || column.auto_increment {
1333 return Err(display(
1334 "an added column cannot be part of the primary key or auto increment",
1335 ));
1336 }
1337
1338 let mut connection = self.connect_as_tenant(database, false).await?;
1339 connection.inner.close_on_drop();
1340
1341 run_ddl(async move {
1342 let columns = connection.inner.table_columns(schema, table).await?;
1343 if columns.is_empty() {
1344 return Err(unknown_table(table));
1345 }
1346 if columns.iter().any(|entry| entry.name == column.name) {
1347 return Err(display(format!("column {} already exists", column.name)));
1348 }
1349
1350 let rendered = connection.inner.resolve_type(&column.r#type).await?;
1351 let sql = format!(
1352 "ALTER TABLE {} ADD COLUMN {}",
1353 connection.inner.qualified_table(schema, table),
1354 render_column(&*connection.inner, column, &rendered)?
1355 );
1356
1357 connection.inner.execute_ddl(sql).await
1358 })
1359 .await
1360 }
1361}
1362
1363async fn run_ddl(
1367 operation: impl Future<Output = Result<(), anyhow::Error>>,
1368) -> Result<(), anyhow::Error> {
1369 tokio::time::timeout(QUERY_CONNECTION_TIMEOUT, operation)
1370 .await
1371 .map_err(|_| {
1372 crate::response::DisplayError::new("statement timed out")
1373 .with_status(axum::http::StatusCode::REQUEST_TIMEOUT)
1374 })?
1375}
1376
1377impl From<db_agent_api::QueryValue> for QueryValue {
1378 fn from(value: db_agent_api::QueryValue) -> Self {
1379 match value {
1380 db_agent_api::QueryValue::Null => Self::Null,
1381 db_agent_api::QueryValue::Text { value, truncated } => Self::Text { value, truncated },
1382 db_agent_api::QueryValue::Binary { value, truncated } => {
1383 Self::Binary { value, truncated }
1384 }
1385 }
1386 }
1387}
1388
1389impl From<db_agent_api::QueryColumn> for QueryColumn {
1390 fn from(column: db_agent_api::QueryColumn) -> Self {
1391 Self {
1392 name: column.name,
1393 type_name: column.type_name,
1394 type_oid: None,
1395 binary: column.binary,
1396 }
1397 }
1398}
1399
1400impl From<db_agent_api::QueryResultSet> for QueryResultSet {
1401 fn from(result: db_agent_api::QueryResultSet) -> Self {
1402 Self {
1403 columns: result.columns.into_iter().map(Into::into).collect(),
1404 rows: result
1405 .rows
1406 .into_iter()
1407 .map(|row| row.into_iter().map(Into::into).collect())
1408 .collect(),
1409 rows_affected: result.rows_affected,
1410 truncated: result.truncated,
1411 }
1412 }
1413}
1414
1415impl From<wings_api::QueryValue> for QueryValue {
1416 fn from(value: wings_api::QueryValue) -> Self {
1417 match value {
1418 wings_api::QueryValue::Null => Self::Null,
1419 wings_api::QueryValue::Text { value, truncated } => Self::Text { value, truncated },
1420 wings_api::QueryValue::Binary { value, truncated } => Self::Binary { value, truncated },
1421 }
1422 }
1423}
1424
1425impl From<wings_api::QueryColumn> for QueryColumn {
1426 fn from(column: wings_api::QueryColumn) -> Self {
1427 Self {
1428 name: column.name,
1429 type_name: column.type_name,
1430 type_oid: None,
1431 binary: column.binary,
1432 }
1433 }
1434}
1435
1436impl From<wings_api::QueryResultSet> for QueryResultSet {
1437 fn from(result: wings_api::QueryResultSet) -> Self {
1438 Self {
1439 columns: result.columns.into_iter().map(Into::into).collect(),
1440 rows: result
1441 .rows
1442 .into_iter()
1443 .map(|row| row.into_iter().map(Into::into).collect())
1444 .collect(),
1445 rows_affected: result.rows_affected,
1446 truncated: result.truncated,
1447 }
1448 }
1449}
1450
1451impl From<db_agent_api::SchemaColumn> for SchemaColumn {
1452 fn from(column: db_agent_api::SchemaColumn) -> Self {
1453 Self {
1454 name: column.name,
1455 type_name: column.type_name,
1456 cast_type: None,
1457 nullable: column.nullable,
1458 default: column.default.map(Into::into),
1459 primary_key: column.primary_key,
1460 auto_increment: column.auto_increment,
1461 generated: column.generated,
1462 binary: column.binary,
1463 enum_values: column.enum_values,
1464 set_values: column.set_values,
1465 }
1466 }
1467}
1468
1469impl From<db_agent_api::SchemaTable> for SchemaTable {
1470 fn from(table: db_agent_api::SchemaTable) -> Self {
1471 Self {
1472 schema: table.schema,
1473 name: table.name,
1474 view: table.view,
1475 row_estimate: table.row_estimate,
1476 columns: table.columns.into_iter().map(Into::into).collect(),
1477 }
1478 }
1479}
1480
1481impl From<FilterOperator> for db_agent_api::FilterOperator {
1482 fn from(operator: FilterOperator) -> Self {
1483 match operator {
1484 FilterOperator::Eq => Self::Eq,
1485 FilterOperator::Ne => Self::Ne,
1486 FilterOperator::Lt => Self::Lt,
1487 FilterOperator::Lte => Self::Lte,
1488 FilterOperator::Gt => Self::Gt,
1489 FilterOperator::Gte => Self::Gte,
1490 FilterOperator::Contains => Self::Contains,
1491 FilterOperator::StartsWith => Self::StartsWith,
1492 FilterOperator::EndsWith => Self::EndsWith,
1493 FilterOperator::IsNull => Self::IsNull,
1494 FilterOperator::NotNull => Self::NotNull,
1495 }
1496 }
1497}
1498
1499impl From<BrowseFilter> for db_agent_api::BrowseFilter {
1500 fn from(filter: BrowseFilter) -> Self {
1501 Self {
1502 column: filter.column,
1503 operator: filter.operator.into(),
1504 value: filter.value.map(Into::into),
1505 }
1506 }
1507}
1508
1509impl From<RowValue> for db_agent_api::RowValue {
1510 fn from(value: RowValue) -> Self {
1511 Self {
1512 column: value.column,
1513 value: value.value.map(Into::into),
1514 }
1515 }
1516}
1517
1518impl From<RowInsert> for db_agent_api::RowInsert {
1519 fn from(row: RowInsert) -> Self {
1520 Self {
1521 values: row.values.into_iter().map(Into::into).collect(),
1522 }
1523 }
1524}
1525
1526impl From<RowUpdate> for db_agent_api::RowUpdate {
1527 fn from(row: RowUpdate) -> Self {
1528 Self {
1529 keys: row.keys.into_iter().map(Into::into).collect(),
1530 values: row.values.into_iter().map(Into::into).collect(),
1531 }
1532 }
1533}
1534
1535impl From<RowDelete> for db_agent_api::RowDelete {
1536 fn from(row: RowDelete) -> Self {
1537 Self {
1538 keys: row.keys.into_iter().map(Into::into).collect(),
1539 }
1540 }
1541}
1542
1543impl From<ColumnDefinition> for db_agent_api::ColumnDefinition {
1544 fn from(column: ColumnDefinition) -> Self {
1545 Self {
1546 name: column.name,
1547 r#type: column.r#type,
1548 nullable: column.nullable,
1549 primary_key: column.primary_key,
1550 auto_increment: column.auto_increment,
1551 }
1552 }
1553}