Skip to main content

shared/models/server_database/explorer/
mod.rs

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