use crate::model_manager::{
Error, Result,
adapters::DatabaseAdapter,
definitions::{Constraint, ModelDescriptor},
errors::sql_generation_error,
};
pub struct ModelGenerator {
adapter: Box<dyn DatabaseAdapter<Error = Error>>,
}
impl ModelGenerator {
pub fn new(adapter: Box<dyn DatabaseAdapter<Error = Error>>) -> Self {
Self { adapter }
}
pub fn generate_create_table_script(&self, model: &ModelDescriptor) -> Result<String> {
let mut script = String::new();
let create_table_sql = self.adapter.generate_create_table_sql(model)?;
script.push_str(&create_table_sql);
script.push_str(";\n\n");
for index in &model.indexes {
let index_sql = self.adapter.generate_index_sql(index, &model.name)?;
script.push_str(&index_sql);
script.push_str(";\n");
}
Ok(script)
}
pub fn generate_drop_table_script(
&self,
table_name: &str,
schema_name: Option<&str>,
) -> Result<String> {
self.adapter
.generate_drop_table_sql(table_name, schema_name)
}
pub fn generate_add_column_script(
&self,
table_name: &str,
column: &crate::model_manager::definitions::ColumnDescriptor,
schema_name: Option<&str>,
) -> Result<String> {
self.adapter
.generate_add_column_sql(table_name, column, schema_name)
}
pub fn generate_drop_column_script(
&self,
table_name: &str,
column_name: &str,
schema_name: Option<&str>,
) -> Result<String> {
self.adapter
.generate_drop_column_sql(table_name, column_name, schema_name)
}
pub fn generate_foreign_key_script(&self, table_name: &str, fk: &Constraint) -> Result<String> {
match fk {
Constraint::ForeignKey { .. } => self.adapter.generate_foreign_key_sql(fk, table_name),
_ => Err(sql_generation_error("Not a foreign key constraint")),
}
}
pub fn adapter(&self) -> &dyn DatabaseAdapter<Error = Error> {
&*self.adapter
}
pub fn adapter_mut(&mut self) -> &mut dyn DatabaseAdapter<Error = Error> {
&mut *self.adapter
}
pub fn generate_migration_script(
&self,
from: &ModelDescriptor,
to: &ModelDescriptor,
) -> Result<String> {
let mut script = String::new();
for col in &to.columns {
if !from.columns.iter().any(|c| c.name == col.name) {
let stmt =
self.adapter
.generate_add_column_sql(&to.name, col, to.schema.as_deref())?;
script.push_str(&stmt);
script.push_str(";\n");
}
}
for col in &from.columns {
if !to.columns.iter().any(|c| c.name == col.name) {
let stmt = self.adapter.generate_drop_column_sql(
&from.name,
&col.name,
from.schema.as_deref(),
)?;
script.push_str(&stmt);
script.push_str(";\n");
}
}
for constraint in &to.constraints {
if !from.constraints.contains(constraint) {
if let Some(sql) = self.adapter.generate_constraint_sql(constraint, &to.name)? {
script.push_str(&sql);
script.push_str(";\n");
}
}
}
for constraint in &from.constraints {
if !to.constraints.contains(constraint) {
let base_name = match constraint {
Constraint::PrimaryKey { name, .. }
| Constraint::Unique { name, .. }
| Constraint::Check { name, .. }
| Constraint::ForeignKey { name, .. } => name.as_deref().unwrap_or_default(),
_ => continue,
};
let constraint_name = format!("{}_{}", from.name, base_name);
let stmt = self.adapter.generate_drop_constraint_sql(
&from.name,
&constraint_name,
from.schema.as_deref(),
)?;
script.push_str(&stmt);
script.push_str(";\n");
}
}
for index in &to.indexes {
if !from.indexes.contains(index) {
let stmt = self.adapter.generate_index_sql(index, &to.name)?;
script.push_str(&stmt);
script.push_str(";\n");
}
}
for index in &from.indexes {
if !to.indexes.contains(index) {
let index_name = index
.name
.clone()
.unwrap_or_else(|| format!("{}_{}_idx", from.name, index.columns.join("_")));
let stmt = self.adapter.generate_drop_index_sql(
&from.name,
&index_name,
from.schema.as_deref(),
)?;
script.push_str(&stmt);
script.push_str(";\n");
}
}
Ok(script)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model_manager::adapters::SqliteAdapter;
use crate::model_manager::definitions::{
ColumnDescriptor, Constraint, DataType, IndexDescriptor, IntegerSize, ModelDescriptor,
};
#[test]
fn test_generate_create_table_script_simple() {
let adapter = Box::new(SqliteAdapter::new());
let generator = ModelGenerator::new(adapter);
let model = ModelDescriptor {
name: "test".to_string(),
schema: None,
columns: vec![
ColumnDescriptor {
name: "id".to_string(),
data_type: DataType::Integer(IntegerSize::I64),
is_nullable: false,
is_primary_key: true,
is_unique: false,
default_value: None,
auto_increment: true,
comment: None,
constraints: vec![],
},
ColumnDescriptor {
name: "name".to_string(),
data_type: DataType::Text(None),
is_nullable: true,
is_primary_key: false,
is_unique: false,
default_value: None,
auto_increment: false,
comment: None,
constraints: vec![],
},
],
primary_key: None,
indexes: vec![],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let sql = generator.generate_create_table_script(&model).unwrap();
assert!(sql.contains("CREATE TABLE \"test\""));
assert!(sql.contains("\"id\" INTEGER PRIMARY KEY AUTOINCREMENT"));
assert!(sql.contains("\"name\" TEXT"));
}
#[test]
fn test_generate_drop_table_script_simple() {
let adapter = Box::new(SqliteAdapter::new());
let generator = ModelGenerator::new(adapter);
let sql = generator.generate_drop_table_script("test", None).unwrap();
assert_eq!(sql, "DROP TABLE IF EXISTS \"test\"");
}
#[test]
fn test_generate_migration_script_add_and_drop() {
let adapter = Box::new(SqliteAdapter::new());
let generator = ModelGenerator::new(adapter);
let from = ModelDescriptor {
name: "items".to_string(),
schema: None,
columns: vec![ColumnDescriptor {
name: "id".to_string(),
data_type: DataType::Integer(IntegerSize::I64),
is_nullable: false,
is_primary_key: true,
is_unique: false,
default_value: None,
auto_increment: true,
comment: None,
constraints: vec![],
}],
primary_key: None,
indexes: vec![],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let to = ModelDescriptor {
name: "items".to_string(),
schema: None,
columns: vec![
ColumnDescriptor {
name: "id".to_string(),
data_type: DataType::Integer(IntegerSize::I64),
is_nullable: false,
is_primary_key: true,
is_unique: false,
default_value: None,
auto_increment: true,
comment: None,
constraints: vec![],
},
ColumnDescriptor {
name: "name".to_string(),
data_type: DataType::Text(None),
is_nullable: true,
is_primary_key: false,
is_unique: false,
default_value: None,
auto_increment: false,
comment: None,
constraints: vec![],
},
],
primary_key: None,
indexes: vec![],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let migration_sql = generator.generate_migration_script(&from, &to).unwrap();
assert!(migration_sql.contains("ADD COLUMN \"name\" TEXT"));
assert!(!migration_sql.contains("DROP COLUMN \"id\""));
match generator.generate_migration_script(&to, &from) {
Ok(_) => panic!("Expected an error for DROP COLUMN with SQLite"),
Err(err) => {
assert!(
err.to_string()
.contains("SQLite doesn't support DROP COLUMN")
);
}
}
}
#[test]
fn test_generate_migration_script_constraints() {
let adapter = Box::new(SqliteAdapter::new());
let generator = ModelGenerator::new(adapter);
let from = ModelDescriptor {
name: "users".to_string(),
schema: None,
columns: vec![],
primary_key: None,
indexes: vec![],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let to = ModelDescriptor {
name: "users".to_string(),
schema: None,
columns: vec![],
primary_key: None,
indexes: vec![],
constraints: vec![Constraint::Unique {
name: Some("u1".to_string()),
columns: vec!["email".to_string()],
}],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let sql = generator.generate_migration_script(&from, &to).unwrap();
assert!(sql.contains("CONSTRAINT \"users_u1\" UNIQUE"));
match generator.generate_migration_script(&to, &from) {
Ok(unexpected_sql) => {
panic!(
"Expected an error when trying to generate DROP CONSTRAINT for SQLite, but got SQL: {}",
unexpected_sql
);
}
Err(err) => {
assert!(
err.to_string().contains(
"SQLite does not support DROP CONSTRAINT. Table must be recreated."
),
"Error message mismatch, got: {}",
err
);
}
}
}
#[test]
fn test_generate_migration_script_indexes() {
let adapter = Box::new(SqliteAdapter::new());
let generator = ModelGenerator::new(adapter);
let from = ModelDescriptor {
name: "users".to_string(),
schema: None,
columns: vec![],
primary_key: None,
indexes: vec![],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let to = ModelDescriptor {
name: "users".to_string(),
schema: None,
columns: vec![],
primary_key: None,
indexes: vec![IndexDescriptor {
name: None,
columns: vec!["email".to_string()],
is_unique: true,
index_type: None,
condition: None,
}],
constraints: vec![],
comment: None,
engine: None,
charset: None,
collation: None,
options: Default::default(),
};
let sql = generator.generate_migration_script(&from, &to).unwrap();
assert!(sql.contains("CREATE UNIQUE INDEX \"users_email_idx\""));
let reverse_sql = generator.generate_migration_script(&to, &from).unwrap();
assert!(reverse_sql.contains("DROP INDEX \"users_email_idx\""));
}
}