use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableSchema {
pub name: String,
pub columns: Vec<ColumnSchema>,
pub indexes: Vec<IndexSchema>,
pub foreign_keys: Vec<ForeignKeySchema>,
pub primary_key: Vec<String>,
pub migration_id: Option<String>,
}
impl TableSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
columns: Vec::new(),
indexes: Vec::new(),
foreign_keys: Vec::new(),
primary_key: Vec::new(),
migration_id: None,
}
}
pub fn add_column(&mut self, column: ColumnSchema) {
self.columns.push(column);
}
pub fn find_column(&self, name: &str) -> Option<&ColumnSchema> {
self.columns.iter().find(|c| c.name == name)
}
pub fn find_column_mut(&mut self, name: &str) -> Option<&mut ColumnSchema> {
self.columns.iter_mut().find(|c| c.name == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnSchema {
pub name: String,
pub column_type: ColumnType,
pub nullable: bool,
pub default: Option<String>,
pub unique: bool,
pub primary_key: bool,
pub auto_increment: bool,
pub indexed: bool,
pub index_name: Option<String>,
pub max_length: Option<u32>,
pub min_length: Option<u32>,
pub range: Option<RangeConstraint>,
pub soft_delete: bool,
pub renamed_from: Option<String>,
pub dropped: bool,
}
impl ColumnSchema {
pub fn new(name: impl Into<String>, column_type: ColumnType) -> Self {
Self {
name: name.into(),
column_type,
nullable: false,
default: None,
unique: false,
primary_key: false,
auto_increment: false,
indexed: false,
index_name: None,
max_length: None,
min_length: None,
range: None,
soft_delete: false,
renamed_from: None,
dropped: false,
}
}
pub fn nullable(mut self, nullable: bool) -> Self {
self.nullable = nullable;
self
}
pub fn default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self
}
pub fn primary_key(mut self, auto_increment: bool) -> Self {
self.primary_key = true;
self.auto_increment = auto_increment;
self
}
pub fn indexed(mut self) -> Self {
self.indexed = true;
self
}
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
pub fn max_length(mut self, len: u32) -> Self {
self.max_length = Some(len);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ColumnType {
Boolean,
SmallInteger,
Integer,
BigInteger,
Float,
Double,
Decimal { precision: u32, scale: u32 },
String(Option<u32>),
Text,
Binary,
Date,
Time,
DateTime,
TimestampTz,
Uuid,
Json,
JsonB,
}
impl ColumnType {
pub fn from_rust_type(type_str: &str) -> Self {
let type_str = type_str.trim();
if type_str.starts_with("Option<") && type_str.ends_with('>') {
let inner = &type_str[7..type_str.len() - 1];
return Self::from_rust_type(inner);
}
let type_str = if let Some(last) = type_str.rsplit("::").next() { last } else { type_str };
match type_str {
"bool" => Self::Boolean,
"i16" => Self::SmallInteger,
"i32" => Self::Integer,
"i64" => Self::BigInteger,
"f32" => Self::Float,
"f64" => Self::Double,
"String" => Self::String(None),
"Vec<u8>" => Self::Binary,
"Uuid" => Self::Uuid,
"NaiveDate" | "Date" => Self::Date,
"NaiveTime" | "Time" => Self::Time,
"NaiveDateTime" | "DateTime" => Self::DateTime,
"DateTimeWithTimeZone" | "DateTime<FixedOffset>" => Self::TimestampTz,
"Value" => Self::Json,
_ => Self::String(None), }
}
pub fn is_option_type(type_str: &str) -> bool {
type_str.trim().starts_with("Option<")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RangeConstraint {
pub min: Option<i64>,
pub max: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexSchema {
pub name: String,
pub columns: Vec<String>,
pub unique: bool,
}
impl IndexSchema {
pub fn new(name: impl Into<String>, columns: Vec<String>) -> Self {
Self {
name: name.into(),
columns,
unique: false,
}
}
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForeignKeySchema {
pub name: Option<String>,
pub column: String,
pub references_table: String,
pub references_column: String,
pub on_delete: OnDeleteAction,
pub on_update: OnUpdateAction,
}
impl ForeignKeySchema {
pub fn new(
column: impl Into<String>,
references_table: impl Into<String>,
references_column: impl Into<String>,
) -> Self {
Self {
name: None,
column: column.into(),
references_table: references_table.into(),
references_column: references_column.into(),
on_delete: OnDeleteAction::NoAction,
on_update: OnUpdateAction::NoAction,
}
}
pub fn on_delete(mut self, action: OnDeleteAction) -> Self {
self.on_delete = action;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OnDeleteAction {
#[default]
NoAction,
Restrict,
Cascade,
SetNull,
SetDefault,
}
impl OnDeleteAction {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"cascade" => Self::Cascade,
"restrict" => Self::Restrict,
"setnull" | "set_null" | "set null" => Self::SetNull,
"setdefault" | "set_default" | "set default" => Self::SetDefault,
_ => Self::NoAction,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OnUpdateAction {
#[default]
NoAction,
Restrict,
Cascade,
SetNull,
SetDefault,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MigrationMeta {
pub id: String,
pub after: Option<String>,
pub tables: Vec<TableSchema>,
pub data_migration: Option<String>,
}
impl MigrationMeta {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
after: None,
tables: Vec::new(),
data_migration: None,
}
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn add_table(&mut self, table: TableSchema) {
self.tables.push(table);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableDelta {
pub table: String,
pub extends: String,
pub add_columns: Vec<ColumnSchema>,
pub drop_columns: Vec<String>,
pub rename_columns: Vec<(String, String)>,
pub alter_columns: Vec<ColumnSchema>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_type_from_rust_type_primitives() {
assert_eq!(ColumnType::from_rust_type("bool"), ColumnType::Boolean);
assert_eq!(ColumnType::from_rust_type("i16"), ColumnType::SmallInteger);
assert_eq!(ColumnType::from_rust_type("i32"), ColumnType::Integer);
assert_eq!(ColumnType::from_rust_type("i64"), ColumnType::BigInteger);
assert_eq!(ColumnType::from_rust_type("f32"), ColumnType::Float);
assert_eq!(ColumnType::from_rust_type("f64"), ColumnType::Double);
assert_eq!(ColumnType::from_rust_type("String"), ColumnType::String(None));
}
#[test]
fn test_column_type_from_rust_type_datetime() {
assert_eq!(ColumnType::from_rust_type("DateTimeWithTimeZone"), ColumnType::TimestampTz);
assert_eq!(ColumnType::from_rust_type("NaiveDateTime"), ColumnType::DateTime);
assert_eq!(ColumnType::from_rust_type("NaiveDate"), ColumnType::Date);
assert_eq!(ColumnType::from_rust_type("NaiveTime"), ColumnType::Time);
assert_eq!(
ColumnType::from_rust_type("ormada::prelude::DateTimeWithTimeZone"),
ColumnType::TimestampTz
);
assert_eq!(ColumnType::from_rust_type("chrono::NaiveDateTime"), ColumnType::DateTime);
assert_eq!(ColumnType::from_rust_type("chrono::NaiveDate"), ColumnType::Date);
assert_eq!(ColumnType::from_rust_type("chrono::NaiveTime"), ColumnType::Time);
}
#[test]
fn test_column_type_from_rust_type_special() {
assert_eq!(ColumnType::from_rust_type("Uuid"), ColumnType::Uuid);
assert_eq!(ColumnType::from_rust_type("uuid::Uuid"), ColumnType::Uuid);
assert_eq!(ColumnType::from_rust_type("Vec<u8>"), ColumnType::Binary);
assert_eq!(ColumnType::from_rust_type("Value"), ColumnType::Json);
assert_eq!(ColumnType::from_rust_type("serde_json::Value"), ColumnType::Json);
}
#[test]
fn test_column_type_from_rust_type_option() {
assert_eq!(ColumnType::from_rust_type("Option<i32>"), ColumnType::Integer);
assert_eq!(ColumnType::from_rust_type("Option<String>"), ColumnType::String(None));
assert_eq!(
ColumnType::from_rust_type("Option<DateTimeWithTimeZone>"),
ColumnType::TimestampTz
);
assert_eq!(
ColumnType::from_rust_type("Option<ormada::prelude::DateTimeWithTimeZone>"),
ColumnType::TimestampTz
);
}
#[test]
fn test_column_type_from_rust_type_unknown_fallback() {
assert_eq!(ColumnType::from_rust_type("CustomType"), ColumnType::String(None));
assert_eq!(ColumnType::from_rust_type("my_module::MyType"), ColumnType::String(None));
}
#[test]
fn test_is_option_type() {
assert!(ColumnType::is_option_type("Option<i32>"));
assert!(ColumnType::is_option_type("Option<String>"));
assert!(ColumnType::is_option_type("Option<DateTimeWithTimeZone>"));
assert!(!ColumnType::is_option_type("i32"));
assert!(!ColumnType::is_option_type("String"));
assert!(!ColumnType::is_option_type("DateTimeWithTimeZone"));
}
#[test]
fn test_table_schema_builder() {
let mut table = TableSchema::new("books");
table.add_column(ColumnSchema::new("id", ColumnType::Integer).primary_key(true));
table.add_column(ColumnSchema::new("title", ColumnType::String(Some(200))).max_length(200));
table.primary_key = vec!["id".to_string()];
assert_eq!(table.name, "books");
assert_eq!(table.columns.len(), 2);
assert!(table.find_column("id").is_some());
assert!(table.find_column("title").is_some());
assert!(table.find_column("nonexistent").is_none());
}
#[test]
fn test_on_delete_from_str() {
assert_eq!(OnDeleteAction::from_str("Cascade"), OnDeleteAction::Cascade);
assert_eq!(OnDeleteAction::from_str("CASCADE"), OnDeleteAction::Cascade);
assert_eq!(OnDeleteAction::from_str("SetNull"), OnDeleteAction::SetNull);
assert_eq!(OnDeleteAction::from_str("set_null"), OnDeleteAction::SetNull);
assert_eq!(OnDeleteAction::from_str("unknown"), OnDeleteAction::NoAction);
}
}