#[cfg(feature = "migrations")]
use std::{collections::BTreeMap, path::PathBuf};
#[cfg(feature = "migrations")]
use thiserror::Error;
#[cfg(feature = "migrations")]
pub use gaman::EmbeddedMigrations;
pub use gaman::core::Dialect;
pub use gaman::schema::{
Column, ColumnDesc, ColumnRef, ColumnType, Constraint, FunctionDef, Index, IntoTable,
SchemaLoadError, Table,
};
pub use gaman::schema::{Schema, SchemaBuilder, TableBuilder};
#[cfg(feature = "migrations")]
pub use mool_macros::embedded_migrations;
#[cfg(feature = "migrations")]
#[derive(Clone, Copy)]
pub struct MigrationSource {
namespace: Option<&'static str>,
migrations: &'static EmbeddedMigrations,
}
#[cfg(feature = "migrations")]
#[derive(Clone, Copy)]
pub struct SchemaSource {
namespace: Option<&'static str>,
build: fn() -> Schema,
}
#[cfg(feature = "migrations")]
impl SchemaSource {
pub fn namespace(&self) -> Option<&'static str> {
self.namespace
}
pub fn build(&self) -> Schema {
(self.build)()
}
}
#[cfg(feature = "migrations")]
impl MigrationSource {
pub fn namespace(&self) -> Option<&'static str> {
self.namespace
}
pub fn embedded(&self) -> &'static EmbeddedMigrations {
self.migrations
}
pub fn dir(&self) -> PathBuf {
PathBuf::from(self.migrations.dir)
}
}
#[cfg(feature = "migrations")]
pub fn root_migration(migrations: &'static EmbeddedMigrations) -> MigrationSource {
MigrationSource {
namespace: None,
migrations,
}
}
#[cfg(feature = "migrations")]
pub fn crate_migration(
namespace: &'static str,
migrations: &'static EmbeddedMigrations,
) -> MigrationSource {
MigrationSource {
namespace: Some(namespace),
migrations,
}
}
#[cfg(feature = "migrations")]
pub fn root_schema(build: fn() -> Schema) -> SchemaSource {
SchemaSource {
namespace: None,
build,
}
}
#[cfg(feature = "migrations")]
pub fn crate_schema(namespace: &'static str, build: fn() -> Schema) -> SchemaSource {
SchemaSource {
namespace: Some(namespace),
build,
}
}
#[cfg(feature = "migrations")]
#[derive(Debug, Error)]
pub enum MigrationError {
#[error("duplicate root migration source")]
DuplicateRoot,
#[error("duplicate migration namespace '{0}'")]
DuplicateNamespace(String),
#[error("invalid migration namespace '{namespace}': {reason}")]
InvalidNamespace { namespace: String, reason: String },
#[error("no root migration source is registered")]
MissingRoot,
#[error("no migration source registered for namespace '{0}'")]
MissingNamespace(String),
#[error("migration engine error: {0}")]
Engine(#[from] gaman::EngineError),
#[error("schema merge error: {0}")]
Schema(String),
}
#[cfg(feature = "migrations")]
#[derive(Clone, Default)]
pub struct MigrationRegistry {
root: Option<MigrationSource>,
crates: BTreeMap<&'static str, MigrationSource>,
root_schema: Vec<SchemaSource>,
crate_schema: BTreeMap<&'static str, Vec<SchemaSource>>,
}
#[cfg(feature = "migrations")]
impl MigrationRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, source: MigrationSource) -> Result<(), MigrationError> {
validate_namespace(source.namespace)?;
match source.namespace {
Some(ns) => self.register_crate(ns, source),
None => self.register_root(source),
}
}
pub fn register_schema(&mut self, source: SchemaSource) -> Result<(), MigrationError> {
validate_namespace(source.namespace)?;
match source.namespace {
Some(ns) => self.crate_schema.entry(ns).or_default().push(source),
None => self.root_schema.push(source),
}
Ok(())
}
pub fn merge(&mut self, other: MigrationRegistry) -> Result<(), MigrationError> {
if let Some(source) = other.root {
self.register(source)?;
}
for source in other.crates.into_values() {
self.register(source)?;
}
for source in other.root_schema {
self.register_schema(source)?;
}
for sources in other.crate_schema.into_values() {
for source in sources {
self.register_schema(source)?;
}
}
Ok(())
}
pub fn root(&self) -> Option<MigrationSource> {
self.root
}
pub fn get(&self, namespace: &str) -> Option<MigrationSource> {
self.crates.get(namespace).copied()
}
pub fn crates(&self) -> impl Iterator<Item = (&'static str, MigrationSource)> + '_ {
self.crates.iter().map(|(ns, source)| (*ns, *source))
}
pub fn schema_for(&self, namespace: Option<&str>) -> Result<Schema, MigrationError> {
let sources = self.schema_sources(namespace);
merge_schema(sources)
}
fn register_root(&mut self, source: MigrationSource) -> Result<(), MigrationError> {
if self.root.is_some() {
return Err(MigrationError::DuplicateRoot);
}
self.root = Some(source);
Ok(())
}
fn register_crate(
&mut self,
ns: &'static str,
source: MigrationSource,
) -> Result<(), MigrationError> {
match self.crates.entry(ns) {
std::collections::btree_map::Entry::Occupied(_) => {
Err(MigrationError::DuplicateNamespace(ns.to_string()))
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(source);
Ok(())
}
}
}
}
#[cfg(feature = "migrations")]
fn merge_schema(sources: Vec<SchemaSource>) -> Result<Schema, MigrationError> {
let mut schema = Schema::default();
for source in sources {
schema = schema
.merge(source.build())
.map_err(|e| MigrationError::Schema(e.to_string()))?;
}
Ok(schema)
}
#[cfg(feature = "migrations")]
impl MigrationRegistry {
fn schema_sources(&self, namespace: Option<&str>) -> Vec<SchemaSource> {
match namespace {
Some(ns) => self
.crate_schema
.get(ns)
.map(|items| items.to_vec())
.unwrap_or_default(),
None => self.root_schema.clone(),
}
}
}
#[cfg(feature = "migrations")]
fn validate_namespace(namespace: Option<&str>) -> Result<(), MigrationError> {
let Some(namespace) = namespace else {
return Ok(());
};
if namespace.is_empty() {
return Err(invalid_ns(namespace, "namespace cannot be empty"));
}
if namespace.contains('/') {
return Err(invalid_ns(namespace, "namespace cannot contain '/'"));
}
if !namespace
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
return Err(invalid_ns(
namespace,
"use lowercase letters, digits, and underscores only",
));
}
Ok(())
}
#[cfg(feature = "migrations")]
fn invalid_ns(namespace: &str, reason: &str) -> MigrationError {
MigrationError::InvalidNamespace {
namespace: namespace.to_string(),
reason: reason.to_string(),
}
}