use thiserror::Error;
use crate::drift::{DriftPropertyDoc, DriftRegistry};
use crate::migrations::Migration;
use crate::operations::Operation;
use crate::parsers::tokens::SqlTokenizer;
use crate::states::types::EntityKind;
use crate::states::{Schema, SchemaValidationError};
#[derive(Debug, Error)]
pub enum DialectError {
#[error("unsupported operation '{0}': {1}")]
Unsupported(String, String),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum DialectParseError {
#[error("database URL is empty")]
EmptyUrl,
#[error("database URL '{0}' does not contain a dialect scheme")]
MissingScheme(String),
#[error("unsupported database URL dialect scheme '{0}'")]
UnsupportedScheme(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dialect {
Postgres,
Sqlite,
Mysql,
}
pub(crate) trait DialectProcessor: Sync {
fn tokenizer(&self) -> &'static dyn SqlTokenizer;
fn migration_to_sql(
&self,
migration: &Migration,
start: &Schema,
) -> Result<Vec<String>, DialectError>;
fn finalize_diff_operations(
&self,
ops: Vec<Operation>,
previous: &Schema,
current: &Schema,
) -> Vec<Operation>;
fn should_merge(&self, table_name: &str, op: &Operation) -> bool;
fn canonicalize_schema_name(&self, object: EntityKind, schema: Option<&str>) -> Option<String>;
fn normalize_type<'a>(&self, t: &'a str) -> &'a str;
fn canonical_type(&self, t: &str) -> String;
fn type_comparison_key(&self, t: &str) -> String;
fn is_catalog_type(&self, t: &str) -> bool;
fn type_suggestions(&self, t: &str) -> Vec<String>;
fn validate_schema(&self, schema: &Schema) -> Result<(), SchemaValidationError>;
fn validate_migration(&self, migration: &Migration) -> Result<(), DialectError>;
fn validate_migration_with_state(
&self,
migration: &Migration,
start: &Schema,
) -> Result<(), DialectError>;
fn drift_registry(&self) -> &'static DriftRegistry;
fn normalize_inspected_schema(&self, schema: Schema) -> Result<Schema, SchemaValidationError>;
fn default_expressions_equal(&self, left: &str, right: &str) -> bool {
crate::parsers::tokens::expressions_equal(self.tokenizer(), left, right)
}
}
impl Dialect {
pub fn as_str(&self) -> &'static str {
match self {
Self::Postgres => "postgres",
Self::Sqlite => "sqlite",
Self::Mysql => "mysql",
}
}
pub fn parse(value: &str) -> Option<Self> {
if value.eq_ignore_ascii_case("postgres") || value.eq_ignore_ascii_case("postgresql") {
Some(Self::Postgres)
} else if value.eq_ignore_ascii_case("sqlite") || value.eq_ignore_ascii_case("sqlite3") {
Some(Self::Sqlite)
} else if value.eq_ignore_ascii_case("mysql") || value.eq_ignore_ascii_case("mariadb") {
Some(Self::Mysql)
} else {
None
}
}
pub fn parse_from_url(database_url: &str) -> Result<Self, DialectParseError> {
let value = database_url.trim();
if value.is_empty() {
return Err(DialectParseError::EmptyUrl);
}
let scheme = if value.starts_with("sqlite:") {
"sqlite"
} else if let Some((scheme, _)) = value.split_once("://") {
scheme
} else if let Some((scheme, _)) = value.split_once(':') {
scheme
} else {
return Err(DialectParseError::MissingScheme(value.to_string()));
};
Self::parse(scheme).ok_or_else(|| DialectParseError::UnsupportedScheme(scheme.to_string()))
}
pub(crate) fn processor(&self) -> &'static dyn DialectProcessor {
match self {
Dialect::Postgres => &postgres::POSTGRES,
Dialect::Sqlite => &sqlite::SQLITE,
Dialect::Mysql => &mysql::MYSQL,
}
}
pub(crate) fn tokenizer(&self) -> &'static dyn SqlTokenizer {
self.processor().tokenizer()
}
pub(crate) fn default_expressions_equal(&self, left: &str, right: &str) -> bool {
self.processor().default_expressions_equal(left, right)
}
#[doc(hidden)]
pub fn migration_to_sql(
&self,
migration: &Migration,
_start: &Schema,
) -> Result<Vec<String>, DialectError> {
self.processor().migration_to_sql(migration, _start)
}
pub fn reorder(
&self,
ops: Vec<Operation>,
previous: &Schema,
current: &Schema,
) -> Vec<Operation> {
self.processor()
.finalize_diff_operations(ops, previous, current)
}
pub fn should_merge(&self, _table_name: &str, _op: &Operation) -> bool {
self.processor().should_merge(_table_name, _op)
}
#[doc(hidden)]
pub fn canonicalize_schema_name(
&self,
object: EntityKind,
schema: Option<&str>,
) -> Option<String> {
self.processor().canonicalize_schema_name(object, schema)
}
pub fn normalize_type<'a>(&self, t: &'a str) -> &'a str {
self.processor().normalize_type(t)
}
pub fn canonical_type(&self, t: &str) -> String {
self.processor().canonical_type(t)
}
#[doc(hidden)]
pub fn type_comparison_key(&self, t: &str) -> String {
self.processor().type_comparison_key(t)
}
pub fn is_catalog_type(&self, t: &str) -> bool {
self.processor().is_catalog_type(t)
}
pub fn type_suggestions(&self, t: &str) -> Vec<String> {
self.processor().type_suggestions(t)
}
pub fn validate_schema(&self, schema: &Schema) -> Result<(), SchemaValidationError> {
self.processor().validate_schema(schema)
}
pub fn normalize_inspected_schema(
&self,
schema: Schema,
) -> Result<Schema, SchemaValidationError> {
self.processor().normalize_inspected_schema(schema)
}
pub fn drift_contract(&self) -> &'static [DriftPropertyDoc] {
crate::drift::contract_for(*self)
}
pub(crate) fn drift_registry(&self) -> &'static DriftRegistry {
self.processor().drift_registry()
}
pub fn validate_migration(&self, m: &Migration) -> Result<(), DialectError> {
self.processor().validate_migration(m)
}
#[doc(hidden)]
pub fn validate_migration_with_state(
&self,
m: &Migration,
_start: &Schema,
) -> Result<(), DialectError> {
self.processor().validate_migration_with_state(m, _start)
}
}
mod mysql;
mod postgres;
mod sqlite;
#[cfg(test)]
mod tests {
use super::{Dialect, DialectParseError};
#[test]
fn parse_from_url_accepts_postgres_urls() {
assert_eq!(
Dialect::parse_from_url("postgres://localhost/app"),
Ok(Dialect::Postgres)
);
assert_eq!(
Dialect::parse_from_url("postgresql://localhost/app"),
Ok(Dialect::Postgres)
);
}
#[test]
fn parse_from_url_accepts_sqlite_urls() {
assert_eq!(
Dialect::parse_from_url("sqlite://app.db"),
Ok(Dialect::Sqlite)
);
assert_eq!(
Dialect::parse_from_url("sqlite::memory:"),
Ok(Dialect::Sqlite)
);
}
#[test]
fn parse_from_url_accepts_mysql_urls() {
assert_eq!(
Dialect::parse_from_url("mysql://localhost/app"),
Ok(Dialect::Mysql)
);
assert_eq!(
Dialect::parse_from_url("mariadb://localhost/app"),
Ok(Dialect::Mysql)
);
}
#[test]
fn parse_from_url_rejects_unsupported_schemes() {
assert_eq!(
Dialect::parse_from_url("oracle://localhost/app"),
Err(DialectParseError::UnsupportedScheme("oracle".to_string()))
);
}
#[test]
fn parse_from_url_rejects_missing_schemes() {
assert_eq!(
Dialect::parse_from_url("localhost/app"),
Err(DialectParseError::MissingScheme(
"localhost/app".to_string()
))
);
}
}