use std::sync::OnceLock;
pub trait DatabaseBackend: std::fmt::Debug + Send + Sync + 'static {
fn name(&self) -> &'static str;
fn supports(&self, feature: BackendFeature) -> bool;
fn map_type(&self, ty: crate::orm::SqlType) -> sea_query::ColumnType;
fn map_column(&self, col: &crate::migrate::Column) -> sea_query::ColumnType {
self.map_type(col.ty)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BackendFeature {
InsertReturning,
UpsertOnConflict,
ArrayColumns,
HStoreColumns,
JsonbColumns,
FullTextSearch,
CidrInet,
UuidNative,
Boolean,
}
#[derive(Debug)]
pub struct PostgresBackend;
#[derive(Debug)]
pub struct SqliteBackend;
impl DatabaseBackend for PostgresBackend {
fn name(&self) -> &'static str {
"postgres"
}
fn supports(&self, feature: BackendFeature) -> bool {
match feature {
BackendFeature::InsertReturning
| BackendFeature::UpsertOnConflict
| BackendFeature::ArrayColumns
| BackendFeature::HStoreColumns
| BackendFeature::JsonbColumns
| BackendFeature::FullTextSearch
| BackendFeature::CidrInet
| BackendFeature::UuidNative
| BackendFeature::Boolean => true,
}
}
fn map_column(&self, col: &crate::migrate::Column) -> sea_query::ColumnType {
use crate::orm::SqlType;
use sea_query::ColumnType;
if matches!(col.ty, SqlType::Text) && col.max_length > 0 {
return ColumnType::String(sea_query::StringLen::N(col.max_length));
}
self.map_type(col.ty)
}
fn map_type(&self, ty: crate::orm::SqlType) -> sea_query::ColumnType {
use crate::orm::SqlType;
use sea_query::ColumnType;
match ty {
SqlType::SmallInt => ColumnType::SmallInteger,
SqlType::Integer => ColumnType::Integer,
SqlType::BigInt => ColumnType::BigInteger,
SqlType::Real => ColumnType::Float,
SqlType::Double => ColumnType::Double,
SqlType::Boolean => ColumnType::Boolean,
SqlType::Text => ColumnType::Text,
SqlType::Date => ColumnType::Date,
SqlType::Time => ColumnType::Time,
SqlType::Timestamptz => ColumnType::TimestampWithTimeZone,
SqlType::Uuid => ColumnType::Uuid,
SqlType::Json => ColumnType::JsonBinary,
SqlType::Array(elem) => {
ColumnType::Array(std::sync::Arc::new(self.map_type(elem.to_sql_type())))
}
SqlType::Inet => ColumnType::Inet,
SqlType::Cidr => ColumnType::Cidr,
SqlType::MacAddr => ColumnType::MacAddr,
SqlType::Xml => ColumnType::custom("xml"),
SqlType::Ltree => ColumnType::custom("ltree"),
SqlType::Bit => ColumnType::custom("bit varying"),
SqlType::FullText => ColumnType::custom("tsvector"),
SqlType::ForeignKey => ColumnType::BigInteger,
SqlType::Bytes => ColumnType::Blob,
SqlType::Decimal => ColumnType::Decimal(Some((19, 4))),
}
}
}
impl DatabaseBackend for SqliteBackend {
fn name(&self) -> &'static str {
"sqlite"
}
fn supports(&self, feature: BackendFeature) -> bool {
match feature {
BackendFeature::InsertReturning
| BackendFeature::UpsertOnConflict
| BackendFeature::Boolean => true,
BackendFeature::ArrayColumns
| BackendFeature::HStoreColumns
| BackendFeature::JsonbColumns
| BackendFeature::FullTextSearch
| BackendFeature::CidrInet
| BackendFeature::UuidNative => false,
}
}
fn map_type(&self, ty: crate::orm::SqlType) -> sea_query::ColumnType {
use crate::orm::SqlType;
use sea_query::ColumnType;
match ty {
SqlType::SmallInt => ColumnType::SmallInteger,
SqlType::Integer => ColumnType::Integer,
SqlType::BigInt => ColumnType::BigInteger,
SqlType::Real => ColumnType::Float,
SqlType::Double => ColumnType::Double,
SqlType::Boolean => ColumnType::Boolean,
SqlType::Text => ColumnType::Text,
SqlType::Date => ColumnType::Date,
SqlType::Time => ColumnType::Time,
SqlType::Timestamptz => ColumnType::TimestampWithTimeZone,
SqlType::Uuid => ColumnType::Text,
SqlType::ForeignKey => ColumnType::BigInteger,
SqlType::Json => ColumnType::Text,
SqlType::Array(_) => panic!(
"umbral::backend::SqliteBackend::map_type: SqlType::Array is Postgres-only. \
The field.backend system check should have failed boot; if you reached this \
panic, either the model registry wasn't initialised before map_type ran or \
the check was disabled. For portable list storage, use SqlType::Json instead."
),
SqlType::Inet | SqlType::Cidr | SqlType::MacAddr => panic!(
"umbral::backend::SqliteBackend::map_type: SqlType::Inet/Cidr/MacAddr are \
Postgres-only. The field.backend system check should have failed boot."
),
SqlType::Xml | SqlType::Ltree | SqlType::Bit => panic!(
"umbral::backend::SqliteBackend::map_type: SqlType::Xml/Ltree/Bit are \
Postgres-only. The field.backend system check should have failed boot."
),
SqlType::FullText => panic!(
"umbral::backend::SqliteBackend::map_type: SqlType::FullText is Postgres-only. \
The field.backend system check should have failed boot."
),
SqlType::Bytes => ColumnType::Blob,
SqlType::Decimal => panic!(
"umbral::backend::SqliteBackend::map_type: SqlType::Decimal is Postgres-only. \
The field.backend system check should have failed boot."
),
}
}
}
static ACTIVE: OnceLock<&'static dyn DatabaseBackend> = OnceLock::new();
pub(crate) fn init(backend: &'static dyn DatabaseBackend) {
ACTIVE
.set(backend)
.expect("umbral::backend::init called more than once");
}
pub fn active() -> &'static dyn DatabaseBackend {
*ACTIVE
.get()
.expect("umbral: backend not initialised — did you call App::build()?")
}
pub fn detect(url: &str) -> Result<&'static dyn DatabaseBackend, BackendDetectError> {
let scheme = url
.split("://")
.next()
.and_then(|s| s.split(':').next())
.unwrap_or(url);
match scheme {
"sqlite" => Ok(&SqliteBackend),
"postgres" | "postgresql" => Ok(&PostgresBackend),
other => Err(BackendDetectError::Unsupported(other.to_owned())),
}
}
#[derive(Debug)]
pub enum BackendDetectError {
Unsupported(String),
}
impl std::fmt::Display for BackendDetectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendDetectError::Unsupported(scheme) => write!(
f,
"umbral: no backend shipped for URL scheme `{scheme}://`. \
M4 supports `sqlite://` and `postgres://`. \
MySQL, Oracle, and other backends are in the deferred backlog."
),
}
}
}
impl std::error::Error for BackendDetectError {}