dinoco_engine 0.0.7

Database adapters, query execution, and migration engine components for Dinoco.
Documentation
use async_trait::async_trait;
use dinoco_compiler::Database;
use std::future::Future;
use std::pin::Pin;

use crate::{
    AdapterDialect, ColumnDefinition, DatabaseColumn, DatabaseEnumRaw, DatabaseForeignKey, DatabaseIndex,
    DatabaseParsedTable, DinocoAdapter, DinocoAdapterHandler, DinocoClientConfig, DinocoError, DinocoResult, DinocoRow,
    DinocoTransactionAdapter, DinocoValue, ExecutionResult, MigrationExecutor, MigrationStep, MySqlAdapter,
    PostgresAdapter, SqliteAdapter,
};

#[derive(Clone)]
pub enum UniversalAdapter {
    Mysql(MySqlAdapter),
    Postgresql(PostgresAdapter),
    Sqlite(SqliteAdapter),
}

#[derive(Clone, Copy)]
pub enum UniversalDialect {
    Mysql,
    Postgresql,
    Sqlite,
}

static MYSQL_DIALECT: UniversalDialect = UniversalDialect::Mysql;
static POSTGRESQL_DIALECT: UniversalDialect = UniversalDialect::Postgresql;
static SQLITE_DIALECT: UniversalDialect = UniversalDialect::Sqlite;

impl UniversalAdapter {
    pub async fn connect_for_database(
        database: &Database,
        url: String,
        config: DinocoClientConfig,
    ) -> DinocoResult<Self> {
        match database {
            Database::Mysql => Ok(Self::Mysql(MySqlAdapter::connect(url, config).await?)),
            Database::Postgresql => Ok(Self::Postgresql(PostgresAdapter::connect(url, config).await?)),
            Database::Sqlite => Ok(Self::Sqlite(SqliteAdapter::connect(url, config).await?)),
        }
    }
}

fn detect_database(url: &str) -> Option<Database> {
    if url.starts_with("postgres://") || url.starts_with("postgresql://") {
        return Some(Database::Postgresql);
    }

    if url.starts_with("mysql://") {
        return Some(Database::Mysql);
    }

    if url.starts_with("file:") {
        return Some(Database::Sqlite);
    }

    None
}

impl AdapterDialect for UniversalDialect {
    fn bind_param(&self, index: usize) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.bind_param(index)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.bind_param(index)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.bind_param(index)
            }
        }
    }

    fn bind_value(&self, index: usize, value: &DinocoValue) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.bind_value(index, value)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.bind_value(index, value)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.bind_value(index, value)
            }
        }
    }

    fn cast_numeric_for_division(&self, expr: &str) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.cast_numeric_for_division(expr)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.cast_numeric_for_division(expr)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.cast_numeric_for_division(expr)
            }
        }
    }

    fn identifier(&self, value: &str) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.identifier(value)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.identifier(value)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.identifier(value)
            }
        }
    }

    fn literal_string(&self, value: &str) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.literal_string(value)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.literal_string(value)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.literal_string(value)
            }
        }
    }

    fn column_type(&self, definition: &ColumnDefinition, is_primary: bool, auto_increment: bool) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.column_type(definition, is_primary, auto_increment)
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.column_type(definition, is_primary, auto_increment)
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.column_type(definition, is_primary, auto_increment)
            }
        }
    }

    fn offset_without_limit(&self) -> String {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.offset_without_limit()
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.offset_without_limit()
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.offset_without_limit()
            }
        }
    }

    fn supports_native_enums(&self) -> bool {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.supports_native_enums()
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.supports_native_enums()
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.supports_native_enums()
            }
        }
    }

    fn supports_insert_returning(&self) -> bool {
        match self {
            Self::Mysql => {
                let dialect = crate::MySqlDialect;
                dialect.supports_insert_returning()
            }
            Self::Postgresql => {
                let dialect = crate::PostgresDialect;
                dialect.supports_insert_returning()
            }
            Self::Sqlite => {
                let dialect = crate::SqliteDialect;
                dialect.supports_insert_returning()
            }
        }
    }
}

#[async_trait]
impl DinocoAdapter for UniversalAdapter {
    type Dialect = UniversalDialect;

    fn dialect(&self) -> &Self::Dialect {
        match self {
            Self::Mysql(_) => &MYSQL_DIALECT,
            Self::Postgresql(_) => &POSTGRESQL_DIALECT,
            Self::Sqlite(_) => &SQLITE_DIALECT,
        }
    }

    async fn connect(url: String, config: DinocoClientConfig) -> DinocoResult<Self> {
        let Some(database) = detect_database(&url) else {
            return Err(DinocoError::ConnectionError(
                "Could not infer database type from URL. Use mysql://, postgresql://, postgres:// or file:."
                    .to_string(),
            ));
        };

        Self::connect_for_database(&database, url, config).await
    }

    async fn execute_result(&self, query: &str, params: &[DinocoValue]) -> DinocoResult<ExecutionResult> {
        match self {
            Self::Mysql(adapter) => adapter.execute_result(query, params).await,
            Self::Postgresql(adapter) => adapter.execute_result(query, params).await,
            Self::Sqlite(adapter) => adapter.execute_result(query, params).await,
        }
    }

    async fn execute_script(&self, sql_content: &str) -> DinocoResult<()> {
        match self {
            Self::Mysql(adapter) => adapter.execute_script(sql_content).await,
            Self::Postgresql(adapter) => adapter.execute_script(sql_content).await,
            Self::Sqlite(adapter) => adapter.execute_script(sql_content).await,
        }
    }

    async fn query_as<T: DinocoRow>(&self, query: &str, params: &[DinocoValue]) -> DinocoResult<Vec<T>> {
        match self {
            Self::Mysql(adapter) => adapter.query_as(query, params).await,
            Self::Postgresql(adapter) => adapter.query_as(query, params).await,
            Self::Sqlite(adapter) => adapter.query_as(query, params).await,
        }
    }
}

#[async_trait]
impl DinocoAdapterHandler for UniversalAdapter {
    async fn fetch_tables(&self) -> DinocoResult<Vec<DatabaseParsedTable>> {
        match self {
            Self::Mysql(adapter) => adapter.fetch_tables().await,
            Self::Postgresql(adapter) => adapter.fetch_tables().await,
            Self::Sqlite(adapter) => adapter.fetch_tables().await,
        }
    }

    async fn fetch_columns(&self, table_name: String) -> DinocoResult<Vec<DatabaseColumn>> {
        match self {
            Self::Mysql(adapter) => adapter.fetch_columns(table_name).await,
            Self::Postgresql(adapter) => adapter.fetch_columns(table_name).await,
            Self::Sqlite(adapter) => adapter.fetch_columns(table_name).await,
        }
    }

    async fn fetch_foreign_keys(&self) -> DinocoResult<Vec<DatabaseForeignKey>> {
        match self {
            Self::Mysql(adapter) => adapter.fetch_foreign_keys().await,
            Self::Postgresql(adapter) => adapter.fetch_foreign_keys().await,
            Self::Sqlite(adapter) => adapter.fetch_foreign_keys().await,
        }
    }

    async fn fetch_enums(&self) -> DinocoResult<Vec<DatabaseEnumRaw>> {
        match self {
            Self::Mysql(adapter) => adapter.fetch_enums().await,
            Self::Postgresql(adapter) => adapter.fetch_enums().await,
            Self::Sqlite(adapter) => adapter.fetch_enums().await,
        }
    }

    async fn fetch_indexes(&self) -> DinocoResult<Vec<DatabaseIndex>> {
        match self {
            Self::Mysql(adapter) => adapter.fetch_indexes().await,
            Self::Postgresql(adapter) => adapter.fetch_indexes().await,
            Self::Sqlite(adapter) => adapter.fetch_indexes().await,
        }
    }

    async fn reset_database(&self) -> DinocoResult<()> {
        match self {
            Self::Mysql(adapter) => adapter.reset_database().await,
            Self::Postgresql(adapter) => adapter.reset_database().await,
            Self::Sqlite(adapter) => adapter.reset_database().await,
        }
    }
}

impl MigrationExecutor for UniversalAdapter {
    fn build_step(&self, step: &MigrationStep, schema: &dinoco_compiler::ParsedSchema) -> Vec<String> {
        match self {
            Self::Mysql(adapter) => <MySqlAdapter as MigrationExecutor>::build_step(adapter, step, schema),
            Self::Postgresql(adapter) => <PostgresAdapter as MigrationExecutor>::build_step(adapter, step, schema),
            Self::Sqlite(adapter) => <SqliteAdapter as MigrationExecutor>::build_step(adapter, step, schema),
        }
    }

    fn build_reverse_step(&self, step: &MigrationStep, schema: &dinoco_compiler::ParsedSchema) -> Vec<String> {
        match self {
            Self::Mysql(adapter) => <MySqlAdapter as MigrationExecutor>::build_reverse_step(adapter, step, schema),
            Self::Postgresql(adapter) => {
                <PostgresAdapter as MigrationExecutor>::build_reverse_step(adapter, step, schema)
            }
            Self::Sqlite(adapter) => <SqliteAdapter as MigrationExecutor>::build_reverse_step(adapter, step, schema),
        }
    }

    fn build_migration(
        &self,
        steps: &[MigrationStep],
        schema: &dinoco_compiler::ParsedSchema,
        reverse: bool,
    ) -> Vec<String> {
        match self {
            Self::Mysql(adapter) => {
                <MySqlAdapter as MigrationExecutor>::build_migration(adapter, steps, schema, reverse)
            }
            Self::Postgresql(adapter) => {
                <PostgresAdapter as MigrationExecutor>::build_migration(adapter, steps, schema, reverse)
            }
            Self::Sqlite(adapter) => {
                <SqliteAdapter as MigrationExecutor>::build_migration(adapter, steps, schema, reverse)
            }
        }
    }
}

impl DinocoTransactionAdapter for UniversalAdapter {
    fn with_transaction<'a, T, F>(&'a self, operation: F) -> Pin<Box<dyn Future<Output = DinocoResult<T>> + Send + 'a>>
    where
        T: Send + 'a,
        F: FnOnce() -> Pin<Box<dyn Future<Output = DinocoResult<T>> + Send + 'a>> + Send + 'a,
    {
        match self {
            Self::Mysql(adapter) => adapter.with_transaction(operation),
            Self::Postgresql(adapter) => adapter.with_transaction(operation),
            Self::Sqlite(adapter) => adapter.with_transaction(operation),
        }
    }
}