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