use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum ColumnType {
Integer,
BigInteger,
String(Option<usize>),
Text,
Boolean,
Float,
Decimal {
precision: u32,
scale: u32,
},
DateTime,
Date,
Time,
Binary,
Json,
Uuid,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ColumnDef {
pub name: String,
pub col_type: ColumnType,
pub nullable: bool,
pub default: Option<Value>,
pub primary_key: bool,
pub auto_increment: bool,
pub unique: bool,
pub index: bool,
}
impl ColumnDef {
pub fn new(name: &str, col_type: ColumnType) -> Self {
Self {
name: name.to_owned(),
col_type,
nullable: false,
default: None,
primary_key: false,
auto_increment: false,
unique: false,
index: false,
}
}
pub fn nullable(mut self) -> Self {
self.nullable = true;
self
}
pub fn default(mut self, val: Value) -> Self {
self.default = Some(val);
self
}
pub fn primary_key(mut self) -> Self {
self.primary_key = true;
self
}
pub fn auto_increment(mut self) -> Self {
self.auto_increment = true;
self
}
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableDef {
pub name: String,
pub columns: Vec<ColumnDef>,
pub timestamps: bool,
}
impl TableDef {
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
columns: Vec::new(),
timestamps: false,
}
}
pub fn column(mut self, col: ColumnDef) -> Self {
self.columns.push(col);
self
}
pub fn timestamps(mut self) -> Self {
self.timestamps = true;
Self::ensure_timestamps(&mut self.columns);
self
}
pub fn build(mut self) -> Self {
if self.timestamps {
Self::ensure_timestamps(&mut self.columns);
}
self
}
fn ensure_timestamps(columns: &mut Vec<ColumnDef>) {
if !columns.iter().any(|column| column.name == "created_at") {
columns.push(ColumnDef::new("created_at", ColumnType::DateTime));
}
if !columns.iter().any(|column| column.name == "updated_at") {
columns.push(ColumnDef::new("updated_at", ColumnType::DateTime));
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{ColumnDef, ColumnType, TableDef};
#[tokio::test]
async fn column_definition_uses_expected_defaults() {
let column = ColumnDef::new("name", ColumnType::String(Some(255)));
assert_eq!(column.name, "name");
assert_eq!(column.col_type, ColumnType::String(Some(255)));
assert!(!column.nullable);
assert!(column.default.is_none());
assert!(!column.primary_key);
assert!(!column.auto_increment);
assert!(!column.unique);
}
#[tokio::test]
async fn column_builder_methods_set_flags() {
let column = ColumnDef::new("id", ColumnType::BigInteger)
.primary_key()
.auto_increment()
.unique();
assert!(column.primary_key);
assert!(column.auto_increment);
assert!(column.unique);
}
#[tokio::test]
async fn nullable_and_default_are_recorded() {
let column = ColumnDef::new("published", ColumnType::Boolean)
.nullable()
.default(json!(true));
assert!(column.nullable);
assert_eq!(column.default, Some(json!(true)));
}
#[tokio::test]
async fn table_builder_collects_columns() {
let table = TableDef::new("posts")
.column(ColumnDef::new("id", ColumnType::Integer).primary_key())
.column(ColumnDef::new("title", ColumnType::String(Some(255))))
.build();
assert_eq!(table.name, "posts");
assert_eq!(table.columns.len(), 2);
assert_eq!(table.columns[0].name, "id");
assert_eq!(table.columns[1].name, "title");
}
#[tokio::test]
async fn timestamps_add_created_and_updated_columns() {
let table = TableDef::new("posts").timestamps().build();
assert!(table.timestamps);
assert!(
table
.columns
.iter()
.any(|column| column.name == "created_at")
);
assert!(
table
.columns
.iter()
.any(|column| column.name == "updated_at")
);
assert_eq!(table.columns.len(), 2);
}
#[test]
fn column_definition_defaults_index_to_false() {
let column = ColumnDef::new("email", ColumnType::String(None));
assert!(!column.index);
}
#[test]
fn table_definition_starts_without_columns_or_timestamps() {
let table = TableDef::new("accounts");
assert_eq!(table.name, "accounts");
assert!(table.columns.is_empty());
assert!(!table.timestamps);
}
#[test]
fn integer_column_type_is_retained() {
let column = ColumnDef::new("age", ColumnType::Integer);
assert_eq!(column.col_type, ColumnType::Integer);
}
#[test]
fn boolean_column_type_is_retained() {
let column = ColumnDef::new("published", ColumnType::Boolean);
assert_eq!(column.col_type, ColumnType::Boolean);
}
#[test]
fn text_column_type_is_retained() {
let column = ColumnDef::new("body", ColumnType::Text);
assert_eq!(column.col_type, ColumnType::Text);
}
#[test]
fn datetime_column_type_is_retained() {
let column = ColumnDef::new("published_at", ColumnType::DateTime);
assert_eq!(column.col_type, ColumnType::DateTime);
}
#[test]
fn float_column_type_is_retained() {
let column = ColumnDef::new("rating", ColumnType::Float);
assert_eq!(column.col_type, ColumnType::Float);
}
#[test]
fn decimal_column_type_retains_precision_and_scale() {
let column = ColumnDef::new(
"amount",
ColumnType::Decimal {
precision: 12,
scale: 4,
},
);
assert_eq!(
column.col_type,
ColumnType::Decimal {
precision: 12,
scale: 4,
}
);
}
#[test]
fn default_value_can_store_strings() {
let column = ColumnDef::new("status", ColumnType::String(Some(20))).default(json!("draft"));
assert_eq!(column.default, Some(json!("draft")));
}
#[test]
fn default_value_can_store_integers() {
let column = ColumnDef::new("retries", ColumnType::Integer).default(json!(3));
assert_eq!(column.default, Some(json!(3)));
}
#[test]
fn default_value_can_store_objects() {
let column = ColumnDef::new("settings", ColumnType::Json).default(json!({"theme": "dark"}));
assert_eq!(column.default, Some(json!({"theme": "dark"})));
}
#[test]
fn build_without_timestamps_does_not_add_timestamp_columns() {
let table = TableDef::new("posts")
.column(ColumnDef::new("title", ColumnType::String(Some(255))))
.build();
assert_eq!(table.columns.len(), 1);
assert!(
table
.columns
.iter()
.all(|column| column.name != "created_at")
);
assert!(
table
.columns
.iter()
.all(|column| column.name != "updated_at")
);
}
#[test]
fn timestamps_preserve_existing_created_at_column() {
let table = TableDef::new("posts")
.column(ColumnDef::new("created_at", ColumnType::DateTime).nullable())
.timestamps()
.build();
assert_eq!(
table
.columns
.iter()
.filter(|column| column.name == "created_at")
.count(),
1
);
assert!(
table
.columns
.iter()
.find(|column| column.name == "created_at")
.expect("created_at should exist")
.nullable
);
}
#[test]
fn timestamps_preserve_existing_updated_at_column() {
let table = TableDef::new("posts")
.column(ColumnDef::new("updated_at", ColumnType::DateTime).nullable())
.timestamps()
.build();
assert_eq!(
table
.columns
.iter()
.filter(|column| column.name == "updated_at")
.count(),
1
);
assert!(
table
.columns
.iter()
.find(|column| column.name == "updated_at")
.expect("updated_at should exist")
.nullable
);
}
#[test]
fn timestamps_add_only_missing_timestamp_column() {
let table = TableDef::new("posts")
.column(ColumnDef::new("created_at", ColumnType::DateTime))
.timestamps()
.build();
let names = table
.columns
.iter()
.map(|column| column.name.as_str())
.collect::<Vec<_>>();
assert_eq!(names, vec!["created_at", "updated_at"]);
}
#[test]
fn repeated_timestamps_calls_do_not_duplicate_columns() {
let table = TableDef::new("posts").timestamps().timestamps().build();
assert_eq!(
table
.columns
.iter()
.filter(|column| column.name == "created_at")
.count(),
1
);
assert_eq!(
table
.columns
.iter()
.filter(|column| column.name == "updated_at")
.count(),
1
);
}
#[test]
fn timestamps_append_columns_after_existing_definitions() {
let table = TableDef::new("posts")
.column(ColumnDef::new("id", ColumnType::Integer).primary_key())
.column(ColumnDef::new("title", ColumnType::String(Some(255))))
.timestamps()
.build();
let names = table
.columns
.iter()
.map(|column| column.name.as_str())
.collect::<Vec<_>>();
assert_eq!(names, vec!["id", "title", "created_at", "updated_at"]);
}
#[test]
fn timestamp_columns_use_datetime_type_and_non_nullable_defaults() {
let table = TableDef::new("posts").timestamps().build();
let created_at = table
.columns
.iter()
.find(|column| column.name == "created_at")
.expect("created_at should exist");
let updated_at = table
.columns
.iter()
.find(|column| column.name == "updated_at")
.expect("updated_at should exist");
assert_eq!(created_at.col_type, ColumnType::DateTime);
assert_eq!(updated_at.col_type, ColumnType::DateTime);
assert!(!created_at.nullable);
assert!(!updated_at.nullable);
}
}