Skip to main content

shared/models/server_database/explorer/
mod.rs

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    /// Opens a connection to this database as the server's own database user, scoped to the
166    /// database itself.
167    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(&quoted),
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
1257/// DDL is not bounded by the session statement timeout on every engine (mysql's
1258/// `max_execution_time` only applies to reads), so the outer timeout is what guarantees the
1259/// tenant-connection permit is released
1260async 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}