use std::collections::BTreeMap;
use crate::{Database, DatabaseError, DatabaseValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataType {
Text,
VarChar(u16),
Char(u16),
TinyInt,
SmallInt,
Int,
BigInt,
Serial,
BigSerial,
Real,
Double,
Decimal(u8, u8),
Money,
Bool,
Date,
Time,
DateTime,
Timestamp,
Blob, Binary(Option<u32>),
Json, Jsonb,
Uuid, Xml, Array(Box<Self>), Inet, MacAddr,
Custom(String), }
#[derive(Debug, Clone)]
pub struct Column {
pub name: String,
pub nullable: bool,
pub auto_increment: bool,
pub data_type: DataType,
pub default: Option<DatabaseValue>,
}
pub struct CreateTableStatement<'a> {
pub table_name: &'a str,
pub if_not_exists: bool,
pub columns: Vec<Column>,
pub primary_key: Option<&'a str>,
pub foreign_keys: Vec<(&'a str, &'a str)>,
}
#[must_use]
pub const fn create_table(table_name: &str) -> CreateTableStatement<'_> {
CreateTableStatement {
table_name,
if_not_exists: false,
columns: vec![],
primary_key: None,
foreign_keys: vec![],
}
}
impl<'a> CreateTableStatement<'a> {
#[must_use]
pub const fn if_not_exists(mut self, if_not_exists: bool) -> Self {
self.if_not_exists = if_not_exists;
self
}
#[must_use]
pub fn column(mut self, column: Column) -> Self {
self.columns.push(column);
self
}
#[must_use]
pub fn columns(mut self, columns: Vec<Column>) -> Self {
self.columns.extend(columns);
self
}
#[must_use]
pub const fn primary_key(mut self, primary_key: &'a str) -> Self {
self.primary_key = Some(primary_key);
self
}
#[must_use]
pub fn foreign_key(mut self, foreign_key: (&'a str, &'a str)) -> Self {
self.foreign_keys.push(foreign_key);
self
}
#[must_use]
pub fn foreign_keys(mut self, foreign_keys: Vec<(&'a str, &'a str)>) -> Self {
self.foreign_keys = foreign_keys;
self
}
pub async fn execute(self, db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_create_table(&self).await
}
}
#[cfg(feature = "cascade")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropBehavior {
Default,
Cascade,
Restrict,
}
pub struct DropTableStatement<'a> {
pub table_name: &'a str,
pub if_exists: bool,
#[cfg(feature = "cascade")]
pub behavior: DropBehavior,
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn drop_table(table_name: &str) -> DropTableStatement<'_> {
DropTableStatement {
table_name,
if_exists: false,
#[cfg(feature = "cascade")]
behavior: DropBehavior::Default,
}
}
impl DropTableStatement<'_> {
#[must_use]
pub const fn if_exists(mut self, if_exists: bool) -> Self {
self.if_exists = if_exists;
self
}
#[cfg(feature = "cascade")]
#[must_use]
pub const fn cascade(mut self) -> Self {
self.behavior = DropBehavior::Cascade;
self
}
#[cfg(feature = "cascade")]
#[must_use]
pub const fn restrict(mut self) -> Self {
self.behavior = DropBehavior::Restrict;
self
}
pub async fn execute(self, db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_drop_table(&self).await
}
}
pub struct CreateIndexStatement<'a> {
pub index_name: &'a str,
pub table_name: &'a str,
pub columns: Vec<&'a str>,
pub unique: bool,
pub if_not_exists: bool,
}
#[must_use]
pub const fn create_index(index_name: &str) -> CreateIndexStatement<'_> {
CreateIndexStatement {
index_name,
table_name: "",
columns: vec![],
unique: false,
if_not_exists: false,
}
}
impl<'a> CreateIndexStatement<'a> {
#[must_use]
pub const fn table(mut self, table_name: &'a str) -> Self {
self.table_name = table_name;
self
}
#[must_use]
pub fn column(mut self, column: &'a str) -> Self {
self.columns.push(column);
self
}
#[must_use]
pub fn columns(mut self, columns: Vec<&'a str>) -> Self {
self.columns = columns;
self
}
#[must_use]
pub const fn unique(mut self, unique: bool) -> Self {
self.unique = unique;
self
}
#[must_use]
pub const fn if_not_exists(mut self, if_not_exists: bool) -> Self {
self.if_not_exists = if_not_exists;
self
}
pub async fn execute(self, db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_create_index(&self).await
}
}
pub struct DropIndexStatement<'a> {
pub index_name: &'a str,
pub table_name: &'a str,
pub if_exists: bool,
}
#[must_use]
pub const fn drop_index<'a>(index_name: &'a str, table_name: &'a str) -> DropIndexStatement<'a> {
DropIndexStatement {
index_name,
table_name,
if_exists: false,
}
}
impl DropIndexStatement<'_> {
#[must_use]
pub const fn if_exists(mut self) -> Self {
self.if_exists = true;
self
}
pub async fn execute(self, db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_drop_index(&self).await
}
}
#[derive(Debug, Clone)]
pub enum AlterOperation {
AddColumn {
name: String,
data_type: DataType,
nullable: bool,
default: Option<DatabaseValue>,
},
DropColumn {
name: String,
#[cfg(feature = "cascade")]
behavior: DropBehavior,
},
RenameColumn {
old_name: String,
new_name: String,
},
ModifyColumn {
name: String,
new_data_type: DataType,
new_nullable: Option<bool>,
new_default: Option<DatabaseValue>,
},
}
pub struct AlterTableStatement<'a> {
pub table_name: &'a str,
pub operations: Vec<AlterOperation>,
}
#[must_use]
pub const fn alter_table(table_name: &str) -> AlterTableStatement<'_> {
AlterTableStatement {
table_name,
operations: vec![],
}
}
impl AlterTableStatement<'_> {
#[must_use]
pub fn add_column(
mut self,
name: String,
data_type: DataType,
nullable: bool,
default: Option<DatabaseValue>,
) -> Self {
self.operations.push(AlterOperation::AddColumn {
name,
data_type,
nullable,
default,
});
self
}
#[must_use]
pub fn drop_column(mut self, name: String) -> Self {
self.operations.push(AlterOperation::DropColumn {
name,
#[cfg(feature = "cascade")]
behavior: DropBehavior::Default,
});
self
}
#[cfg(feature = "cascade")]
#[must_use]
pub fn drop_column_cascade(mut self, name: String) -> Self {
self.operations.push(AlterOperation::DropColumn {
name,
behavior: DropBehavior::Cascade,
});
self
}
#[cfg(feature = "cascade")]
#[must_use]
pub fn drop_column_restrict(mut self, name: String) -> Self {
self.operations.push(AlterOperation::DropColumn {
name,
behavior: DropBehavior::Restrict,
});
self
}
#[must_use]
pub fn rename_column(mut self, old_name: String, new_name: String) -> Self {
self.operations
.push(AlterOperation::RenameColumn { old_name, new_name });
self
}
#[must_use]
pub fn modify_column(
mut self,
name: String,
new_data_type: DataType,
new_nullable: Option<bool>,
new_default: Option<DatabaseValue>,
) -> Self {
self.operations.push(AlterOperation::ModifyColumn {
name,
new_data_type,
new_nullable,
new_default,
});
self
}
pub async fn execute(self, db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_alter_table(&self).await
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ColumnInfo {
pub name: String,
pub data_type: DataType,
pub nullable: bool,
pub is_primary_key: bool,
pub auto_increment: bool,
pub default_value: Option<DatabaseValue>,
pub ordinal_position: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexInfo {
pub name: String,
pub unique: bool,
pub columns: Vec<String>,
pub is_primary: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForeignKeyInfo {
pub name: String,
pub column: String,
pub referenced_table: String,
pub referenced_column: String,
pub on_update: Option<String>,
pub on_delete: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableInfo {
pub name: String,
pub columns: BTreeMap<String, ColumnInfo>,
pub indexes: BTreeMap<String, IndexInfo>,
pub foreign_keys: BTreeMap<String, ForeignKeyInfo>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drop_table_builder_default() {
let statement = drop_table("test_table");
assert_eq!(statement.table_name, "test_table");
assert!(!statement.if_exists);
}
#[test]
fn test_drop_table_builder_with_if_exists() {
let statement = drop_table("test_table").if_exists(true);
assert_eq!(statement.table_name, "test_table");
assert!(statement.if_exists);
}
#[test]
fn test_drop_table_builder_chain() {
let statement = drop_table("users").if_exists(true);
assert_eq!(statement.table_name, "users");
assert!(statement.if_exists);
}
#[test]
fn test_drop_table_builder_if_exists_false() {
let statement = drop_table("test_table").if_exists(true).if_exists(false);
assert_eq!(statement.table_name, "test_table");
assert!(!statement.if_exists);
}
#[test]
fn test_create_index_builder_default() {
let statement = create_index("test_index");
assert_eq!(statement.index_name, "test_index");
assert_eq!(statement.table_name, "");
assert!(statement.columns.is_empty());
assert!(!statement.unique);
assert!(!statement.if_not_exists);
}
#[test]
fn test_create_index_builder_single_column() {
let statement = create_index("idx_name").table("users").column("name");
assert_eq!(statement.index_name, "idx_name");
assert_eq!(statement.table_name, "users");
assert_eq!(statement.columns, vec!["name"]);
assert!(!statement.unique);
assert!(!statement.if_not_exists);
}
#[test]
fn test_create_index_builder_multi_column() {
let statement = create_index("idx_multi")
.table("users")
.columns(vec!["first_name", "last_name"]);
assert_eq!(statement.index_name, "idx_multi");
assert_eq!(statement.table_name, "users");
assert_eq!(statement.columns, vec!["first_name", "last_name"]);
assert!(!statement.unique);
assert!(!statement.if_not_exists);
}
#[test]
fn test_create_index_builder_unique() {
let statement = create_index("idx_email")
.table("users")
.column("email")
.unique(true);
assert_eq!(statement.index_name, "idx_email");
assert_eq!(statement.table_name, "users");
assert_eq!(statement.columns, vec!["email"]);
assert!(statement.unique);
assert!(!statement.if_not_exists);
}
#[test]
fn test_create_index_builder_if_not_exists() {
let statement = create_index("idx_test")
.table("test")
.column("col")
.if_not_exists(true);
assert_eq!(statement.index_name, "idx_test");
assert_eq!(statement.table_name, "test");
assert_eq!(statement.columns, vec!["col"]);
assert!(!statement.unique);
assert!(statement.if_not_exists);
}
#[test]
fn test_create_index_builder_method_chaining() {
let statement = create_index("idx_complex")
.table("products")
.column("category_id")
.column("price")
.unique(true)
.if_not_exists(true);
assert_eq!(statement.index_name, "idx_complex");
assert_eq!(statement.table_name, "products");
assert_eq!(statement.columns, vec!["category_id", "price"]);
assert!(statement.unique);
assert!(statement.if_not_exists);
}
#[test]
fn test_create_index_builder_columns_overwrite() {
let statement = create_index("idx_test")
.table("test")
.column("col1")
.column("col2")
.columns(vec!["col3", "col4"]);
assert_eq!(statement.index_name, "idx_test");
assert_eq!(statement.table_name, "test");
assert_eq!(statement.columns, vec!["col3", "col4"]);
assert!(!statement.unique);
assert!(!statement.if_not_exists);
}
#[test]
fn test_drop_index_builder_default() {
let statement = drop_index("test_index", "test_table");
assert_eq!(statement.index_name, "test_index");
assert_eq!(statement.table_name, "test_table");
assert!(!statement.if_exists);
}
#[test]
fn test_drop_index_builder_with_if_exists() {
let statement = drop_index("idx_email", "users").if_exists();
assert_eq!(statement.index_name, "idx_email");
assert_eq!(statement.table_name, "users");
assert!(statement.if_exists);
}
#[test]
fn test_drop_index_builder_if_exists_chaining() {
let statement = drop_index("idx_complex", "products").if_exists();
assert_eq!(statement.index_name, "idx_complex");
assert_eq!(statement.table_name, "products");
assert!(statement.if_exists);
}
#[test]
fn test_alter_table_builder_default() {
let statement = alter_table("test_table");
assert_eq!(statement.table_name, "test_table");
assert!(statement.operations.is_empty());
}
#[test]
fn test_alter_table_add_column() {
let statement = alter_table("users").add_column(
"email".to_string(),
DataType::VarChar(255),
false,
None,
);
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::AddColumn {
name,
data_type,
nullable,
default,
} => {
assert_eq!(name, "email");
assert!(matches!(data_type, DataType::VarChar(255)));
assert!(!nullable);
assert!(default.is_none());
}
_ => panic!("Expected AddColumn operation"),
}
}
#[test]
fn test_alter_table_add_column_with_default() {
let statement = alter_table("users").add_column(
"active".to_string(),
DataType::Bool,
true,
Some(DatabaseValue::Bool(true)),
);
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::AddColumn {
name,
data_type,
nullable,
default,
} => {
assert_eq!(name, "active");
assert!(matches!(data_type, DataType::Bool));
assert!(nullable);
assert!(matches!(default, Some(DatabaseValue::Bool(true))));
}
_ => panic!("Expected AddColumn operation"),
}
}
#[test]
fn test_alter_table_drop_column() {
let statement = alter_table("users").drop_column("old_column".to_string());
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::DropColumn {
name,
#[cfg(feature = "cascade")]
behavior,
} => {
assert_eq!(name, "old_column");
#[cfg(feature = "cascade")]
assert_eq!(*behavior, DropBehavior::Default);
}
_ => panic!("Expected DropColumn operation"),
}
}
#[test]
fn test_alter_table_rename_column() {
let statement =
alter_table("users").rename_column("old_name".to_string(), "new_name".to_string());
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::RenameColumn { old_name, new_name } => {
assert_eq!(old_name, "old_name");
assert_eq!(new_name, "new_name");
}
_ => panic!("Expected RenameColumn operation"),
}
}
#[test]
fn test_alter_table_modify_column() {
let statement = alter_table("users").modify_column(
"age".to_string(),
DataType::Int,
Some(false),
Some(DatabaseValue::Int64(0)),
);
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::ModifyColumn {
name,
new_data_type,
new_nullable,
new_default,
} => {
assert_eq!(name, "age");
assert!(matches!(new_data_type, DataType::Int));
assert_eq!(new_nullable, &Some(false));
assert!(matches!(new_default, Some(DatabaseValue::Int64(0))));
}
_ => panic!("Expected ModifyColumn operation"),
}
}
#[test]
fn test_alter_table_modify_column_partial() {
let statement =
alter_table("users").modify_column("name".to_string(), DataType::Text, None, None);
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::ModifyColumn {
name,
new_data_type,
new_nullable,
new_default,
} => {
assert_eq!(name, "name");
assert!(matches!(new_data_type, DataType::Text));
assert_eq!(new_nullable, &None);
assert_eq!(new_default, &None);
}
_ => panic!("Expected ModifyColumn operation"),
}
}
#[test]
fn test_alter_table_multiple_operations() {
let statement = alter_table("users")
.add_column("email".to_string(), DataType::VarChar(255), false, None)
.drop_column("old_field".to_string())
.rename_column("first_name".to_string(), "given_name".to_string())
.modify_column("age".to_string(), DataType::SmallInt, Some(true), None);
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 4);
match &statement.operations[0] {
AlterOperation::AddColumn {
name,
data_type,
nullable,
default,
} => {
assert_eq!(name, "email");
assert!(matches!(data_type, DataType::VarChar(255)));
assert!(!nullable);
assert!(default.is_none());
}
_ => panic!("Expected AddColumn operation at index 0"),
}
match &statement.operations[1] {
AlterOperation::DropColumn {
name,
#[cfg(feature = "cascade")]
behavior,
} => {
assert_eq!(name, "old_field");
#[cfg(feature = "cascade")]
assert_eq!(*behavior, DropBehavior::Default);
}
_ => panic!("Expected DropColumn operation at index 1"),
}
match &statement.operations[2] {
AlterOperation::RenameColumn { old_name, new_name } => {
assert_eq!(old_name, "first_name");
assert_eq!(new_name, "given_name");
}
_ => panic!("Expected RenameColumn operation at index 2"),
}
match &statement.operations[3] {
AlterOperation::ModifyColumn {
name,
new_data_type,
new_nullable,
new_default,
} => {
assert_eq!(name, "age");
assert!(matches!(new_data_type, DataType::SmallInt));
assert_eq!(new_nullable, &Some(true));
assert_eq!(new_default, &None);
}
_ => panic!("Expected ModifyColumn operation at index 3"),
}
}
#[test]
fn test_alter_table_builder_chaining() {
let statement = alter_table("products")
.add_column("sku".to_string(), DataType::VarChar(50), false, None)
.add_column(
"created_at".to_string(),
DataType::DateTime,
false,
Some(DatabaseValue::Now),
);
assert_eq!(statement.table_name, "products");
assert_eq!(statement.operations.len(), 2);
for (i, operation) in statement.operations.iter().enumerate() {
match operation {
AlterOperation::AddColumn { .. } => {
}
_ => panic!("Expected AddColumn operation at index {i}"),
}
}
}
#[test]
fn test_alter_operation_clone() {
let operation = AlterOperation::AddColumn {
name: "test_col".to_string(),
data_type: DataType::Int,
nullable: true,
default: Some(DatabaseValue::Int64(42)),
};
let cloned = operation.clone();
match (&operation, &cloned) {
(
AlterOperation::AddColumn {
name: n1,
data_type: d1,
nullable: null1,
default: def1,
},
AlterOperation::AddColumn {
name: n2,
data_type: d2,
nullable: null2,
default: def2,
},
) => {
assert_eq!(n1, n2);
assert!(matches!((d1, d2), (DataType::Int, DataType::Int)));
assert_eq!(null1, null2);
assert!(matches!(
(def1, def2),
(
Some(DatabaseValue::Int64(42)),
Some(DatabaseValue::Int64(42))
)
));
}
_ => panic!("Clone should match original"),
}
}
#[test]
#[cfg(feature = "cascade")]
fn test_drop_column_cascade() {
let statement = alter_table("users").drop_column_cascade("old_column".to_string());
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::DropColumn { name, behavior } => {
assert_eq!(name, "old_column");
assert_eq!(*behavior, DropBehavior::Cascade);
}
_ => panic!("Expected DropColumn operation with CASCADE behavior"),
}
}
#[test]
#[cfg(feature = "cascade")]
fn test_drop_column_restrict() {
let statement = alter_table("users").drop_column_restrict("old_column".to_string());
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 1);
match &statement.operations[0] {
AlterOperation::DropColumn { name, behavior } => {
assert_eq!(name, "old_column");
assert_eq!(*behavior, DropBehavior::Restrict);
}
_ => panic!("Expected DropColumn operation with RESTRICT behavior"),
}
}
#[test]
#[cfg(feature = "cascade")]
fn test_multiple_drop_column_behaviors() {
let statement = alter_table("users")
.drop_column("default_col".to_string())
.drop_column_cascade("cascade_col".to_string())
.drop_column_restrict("restrict_col".to_string());
assert_eq!(statement.table_name, "users");
assert_eq!(statement.operations.len(), 3);
match &statement.operations[0] {
AlterOperation::DropColumn { name, behavior } => {
assert_eq!(name, "default_col");
assert_eq!(*behavior, DropBehavior::Default);
}
_ => panic!("Expected DropColumn operation with Default behavior"),
}
match &statement.operations[1] {
AlterOperation::DropColumn { name, behavior } => {
assert_eq!(name, "cascade_col");
assert_eq!(*behavior, DropBehavior::Cascade);
}
_ => panic!("Expected DropColumn operation with CASCADE behavior"),
}
match &statement.operations[2] {
AlterOperation::DropColumn { name, behavior } => {
assert_eq!(name, "restrict_col");
assert_eq!(*behavior, DropBehavior::Restrict);
}
_ => panic!("Expected DropColumn operation with RESTRICT behavior"),
}
}
}
#[cfg(feature = "schema")]
pub mod dependencies;
#[cfg(feature = "schema")]
pub use dependencies::{
ColumnDependencies, CycleError, DependencyGraph, DropPlan, get_column_dependencies,
};
#[cfg(feature = "auto-reverse")]
pub mod auto_reversible;
#[cfg(feature = "auto-reverse")]
pub use auto_reversible::AutoReversible;