use super::{FieldState, ModelState, ProjectState};
use crate::backends::schema::BaseDatabaseSchemaEditor;
use crate::backends::types::DatabaseType;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationError {
EmptyCompositePrimaryKey {
table_name: String,
},
NonExistentField {
field_name: String,
table_name: String,
available_fields: Vec<String>,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::EmptyCompositePrimaryKey { table_name } => {
write!(
f,
"Composite primary key for table '{}' cannot be empty",
table_name
)
}
ValidationError::NonExistentField {
field_name,
table_name,
available_fields,
} => {
write!(
f,
"Field '{}' does not exist in table '{}'. Available fields: [{}]",
field_name,
table_name,
available_fields.join(", ")
)
}
}
}
}
impl std::error::Error for ValidationError {}
pub type ValidationResult<T> = Result<T, ValidationError>;
pub fn quote_identifier(identifier: &str, database_type: DatabaseType) -> String {
match database_type {
DatabaseType::Postgres | DatabaseType::Sqlite => {
format!("\"{}\"", identifier.replace('"', "\"\""))
}
DatabaseType::Mysql => {
format!("`{}`", identifier.replace('`', "``"))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FieldDefinition {
pub name: String,
pub field_type: crate::migrations::FieldType,
pub primary_key: bool,
pub unique: bool,
pub default: Option<String>,
pub null: bool,
pub generated: Option<String>,
pub generated_stored: Option<bool>,
#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
pub generated_virtual: Option<bool>,
#[cfg(feature = "db-postgres")]
pub identity_always: Option<bool>,
#[cfg(feature = "db-postgres")]
pub identity_by_default: Option<bool>,
#[cfg(feature = "db-mysql")]
pub auto_increment: Option<bool>,
#[cfg(feature = "db-sqlite")]
pub autoincrement: Option<bool>,
pub collate: Option<String>,
#[cfg(feature = "db-mysql")]
pub character_set: Option<String>,
#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
pub comment: Option<String>,
#[cfg(feature = "db-postgres")]
pub storage: Option<String>,
#[cfg(feature = "db-postgres")]
pub compression: Option<String>,
#[cfg(feature = "db-mysql")]
pub on_update_current_timestamp: Option<bool>,
#[cfg(feature = "db-mysql")]
pub invisible: Option<bool>,
#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
pub fulltext: Option<bool>,
#[cfg(feature = "db-mysql")]
pub unsigned: Option<bool>,
#[cfg(feature = "db-mysql")]
pub zerofill: Option<bool>,
}
impl FieldDefinition {
pub fn new(
name: impl Into<String>,
field_type: crate::migrations::FieldType,
primary_key: bool,
unique: bool,
default: Option<impl Into<String>>,
) -> Self {
Self {
name: name.into(),
field_type,
primary_key,
unique,
default: default.map(|d| d.into()),
null: false,
generated: None,
generated_stored: None,
#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
generated_virtual: None,
#[cfg(feature = "db-postgres")]
identity_always: None,
#[cfg(feature = "db-postgres")]
identity_by_default: None,
#[cfg(feature = "db-mysql")]
auto_increment: None,
#[cfg(feature = "db-sqlite")]
autoincrement: None,
collate: None,
#[cfg(feature = "db-mysql")]
character_set: None,
#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
comment: None,
#[cfg(feature = "db-postgres")]
storage: None,
#[cfg(feature = "db-postgres")]
compression: None,
#[cfg(feature = "db-mysql")]
on_update_current_timestamp: None,
#[cfg(feature = "db-mysql")]
invisible: None,
#[cfg(any(feature = "db-postgres", feature = "db-mysql"))]
fulltext: None,
#[cfg(feature = "db-mysql")]
unsigned: None,
#[cfg(feature = "db-mysql")]
zerofill: None,
}
}
pub fn nullable(mut self, null: bool) -> Self {
self.null = null;
self
}
pub fn to_sql_definition(&self) -> String {
let mut parts = vec![self.field_type.to_sql_string()];
if let Some(ref generated_expr) = self.generated {
parts.push(format!("GENERATED ALWAYS AS ({})", generated_expr));
let is_stored = self.generated_stored.unwrap_or(false);
#[cfg(any(feature = "db-mysql", feature = "db-sqlite"))]
let is_virtual = self.generated_virtual.unwrap_or(false);
#[cfg(not(any(feature = "db-mysql", feature = "db-sqlite")))]
let is_virtual = false;
if is_stored {
parts.push("STORED".to_string());
} else if is_virtual {
parts.push("VIRTUAL".to_string());
}
}
#[cfg(feature = "db-postgres")]
if self.identity_always.unwrap_or(false) {
parts.push("GENERATED ALWAYS AS IDENTITY".to_string());
}
#[cfg(feature = "db-postgres")]
if self.identity_by_default.unwrap_or(false) {
parts.push("GENERATED BY DEFAULT AS IDENTITY".to_string());
}
#[cfg(feature = "db-mysql")]
if self.auto_increment.unwrap_or(false) {
parts.push("AUTO_INCREMENT".to_string());
}
#[cfg(feature = "db-sqlite")]
if self.autoincrement.unwrap_or(false) {
parts.push("AUTOINCREMENT".to_string());
}
if self.primary_key {
parts.push("PRIMARY KEY".to_string());
}
if self.unique && !self.primary_key {
parts.push("UNIQUE".to_string());
}
if !self.null && !self.primary_key {
parts.push("NOT NULL".to_string());
}
if let Some(ref default) = self.default {
parts.push(format!("DEFAULT {}", default));
}
#[cfg(feature = "db-mysql")]
if let Some(ref character_set) = self.character_set {
parts.push(format!("CHARACTER SET {}", character_set));
}
if let Some(ref collate) = self.collate {
parts.push(format!("COLLATE {}", collate));
}
#[cfg(feature = "db-mysql")]
if let Some(ref comment) = self.comment {
parts.push(format!("COMMENT '{}'", comment.replace('\'', "''")));
}
#[cfg(feature = "db-postgres")]
if let Some(ref storage) = self.storage {
parts.push(format!("STORAGE {}", storage.to_uppercase()));
}
#[cfg(feature = "db-postgres")]
if let Some(ref compression) = self.compression {
parts.push(format!("COMPRESSION {}", compression));
}
#[cfg(feature = "db-mysql")]
if self.on_update_current_timestamp.unwrap_or(false) {
parts.push("ON UPDATE CURRENT_TIMESTAMP".to_string());
}
#[cfg(feature = "db-mysql")]
if self.invisible.unwrap_or(false) {
parts.push("INVISIBLE".to_string());
}
#[cfg(feature = "db-mysql")]
if self.unsigned.unwrap_or(false) {
parts.push("UNSIGNED".to_string());
}
#[cfg(feature = "db-mysql")]
if self.zerofill.unwrap_or(false) {
parts.push("ZEROFILL".to_string());
}
parts.join(" ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateModel {
pub name: String,
pub fields: Vec<FieldDefinition>,
pub options: HashMap<String, String>,
pub bases: Vec<String>,
pub composite_primary_key: Option<Vec<String>>,
}
impl CreateModel {
pub fn new(name: impl Into<String>, fields: Vec<FieldDefinition>) -> Self {
Self {
name: name.into(),
fields,
options: HashMap::new(),
bases: vec![],
composite_primary_key: None,
}
}
pub fn with_options(mut self, options: HashMap<String, String>) -> Self {
self.options = options;
self
}
pub fn with_bases(mut self, bases: Vec<String>) -> Self {
self.bases = bases;
self
}
pub fn with_composite_primary_key(mut self, fields: Vec<String>) -> ValidationResult<Self> {
if fields.is_empty() {
return Err(ValidationError::EmptyCompositePrimaryKey {
table_name: self.name.clone(),
});
}
let available_field_names: Vec<String> =
self.fields.iter().map(|f| f.name.clone()).collect();
for field_name in &fields {
if !available_field_names.contains(field_name) {
return Err(ValidationError::NonExistentField {
field_name: field_name.clone(),
table_name: self.name.clone(),
available_fields: available_field_names.clone(),
});
}
}
self.composite_primary_key = Some(fields);
Ok(self)
}
pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
let mut model = ModelState::new(app_label, &self.name);
for field_def in &self.fields {
let field = FieldState::new(
field_def.name.clone(),
field_def.field_type.clone(),
field_def.primary_key,
);
model.add_field(field);
}
state.add_model(model);
}
pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
let mut sql_statements = Vec::new();
let has_composite_pk = self.composite_primary_key.is_some();
let column_defs: Vec<String> = self
.fields
.iter()
.map(|f| {
if has_composite_pk && f.primary_key {
let mut parts = vec![f.field_type.to_sql_string()];
parts.push("NOT NULL".to_string());
if f.unique {
parts.push("UNIQUE".to_string());
}
if let Some(ref default) = f.default {
parts.push(format!("DEFAULT {}", default));
}
parts.join(" ")
} else {
f.to_sql_definition()
}
})
.collect();
let columns: Vec<(&str, &str)> = self
.fields
.iter()
.zip(column_defs.iter())
.map(|(f, def)| (f.name.as_str(), def.as_str()))
.collect();
let stmt = schema_editor.create_table_statement(&self.name, &columns);
let mut create_sql = schema_editor.build_create_table_sql(&stmt);
if let Some(ref pk_fields) = self.composite_primary_key {
let db_type = schema_editor.database_type();
let pk_name = format!("{}_pkey", self.name);
let quoted_pk_name = quote_identifier(&pk_name, db_type);
let quoted_pk_fields = pk_fields
.iter()
.map(|f| quote_identifier(f, db_type))
.collect::<Vec<_>>()
.join(", ");
let constraint_sql = format!(
"CONSTRAINT {} PRIMARY KEY ({})",
quoted_pk_name, quoted_pk_fields
);
if create_sql.ends_with(");") {
let insert_pos = create_sql.len() - 2; create_sql.insert_str(insert_pos, &format!(", {}", constraint_sql));
} else if create_sql.ends_with(")") {
let insert_pos = create_sql.len() - 1; create_sql.insert_str(insert_pos, &format!(", {}", constraint_sql));
}
}
#[cfg(feature = "db-sqlite")]
{
let mut table_options = Vec::new();
if let Some(strict_val) = self.options.get("strict")
&& strict_val == "true"
{
table_options.push("STRICT");
}
if let Some(without_rowid_val) = self.options.get("without_rowid")
&& without_rowid_val == "true"
{
table_options.push("WITHOUT ROWID");
}
if !table_options.is_empty() {
if create_sql.ends_with(';') {
create_sql.pop();
}
create_sql.push(' ');
create_sql.push_str(&table_options.join(" "));
create_sql.push(';');
}
}
sql_statements.push(create_sql);
sql_statements
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteModel {
pub name: String,
}
impl DeleteModel {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
state.remove_model(app_label, &self.name);
}
pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
let stmt = schema_editor.drop_table_statement(&self.name, false);
vec![schema_editor.build_drop_table_sql(&stmt)]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameModel {
pub old_name: String,
pub new_name: String,
}
impl RenameModel {
pub fn new(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
Self {
old_name: old_name.into(),
new_name: new_name.into(),
}
}
pub fn state_forwards(&self, app_label: &str, state: &mut ProjectState) {
state.rename_model(app_label, &self.old_name, self.new_name.clone());
}
pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
let db_type = schema_editor.database_type();
let old_name = quote_identifier(&self.old_name, db_type);
let new_name = quote_identifier(&self.new_name, db_type);
vec![format!("ALTER TABLE {} RENAME TO {}", old_name, new_name)]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoveModel {
pub model_name: String,
pub from_app: String,
pub to_app: String,
pub rename_table: bool,
pub old_table_name: Option<String>,
pub new_table_name: Option<String>,
}
impl MoveModel {
pub fn new(
model_name: impl Into<String>,
from_app: impl Into<String>,
to_app: impl Into<String>,
) -> Self {
Self {
model_name: model_name.into(),
from_app: from_app.into(),
to_app: to_app.into(),
rename_table: false,
old_table_name: None,
new_table_name: None,
}
}
pub fn with_table_rename(
mut self,
old_table: impl Into<String>,
new_table: impl Into<String>,
) -> Self {
self.rename_table = true;
self.old_table_name = Some(old_table.into());
self.new_table_name = Some(new_table.into());
self
}
pub fn state_forwards(&self, _app_label: &str, state: &mut ProjectState) {
if let Some(model) = state
.models
.remove(&(self.from_app.clone(), self.model_name.clone()))
{
let mut new_model = model;
new_model.app_label = self.to_app.clone();
state
.models
.insert((self.to_app.clone(), self.model_name.clone()), new_model);
}
}
pub fn state_backwards(&self, _app_label: &str, state: &mut ProjectState) {
if let Some(model) = state
.models
.remove(&(self.to_app.clone(), self.model_name.clone()))
{
let mut original_model = model;
original_model.app_label = self.from_app.clone();
state.models.insert(
(self.from_app.clone(), self.model_name.clone()),
original_model,
);
}
}
pub fn database_forwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
if self.rename_table {
if let (Some(old_table), Some(new_table)) = (&self.old_table_name, &self.new_table_name)
{
let db_type = schema_editor.database_type();
let old_name = quote_identifier(old_table, db_type);
let new_name = quote_identifier(new_table, db_type);
vec![format!("ALTER TABLE {} RENAME TO {}", old_name, new_name)]
} else {
vec![]
}
} else {
vec![]
}
}
pub fn database_backwards(&self, schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
if self.rename_table {
if let (Some(old_table), Some(new_table)) = (&self.old_table_name, &self.new_table_name)
{
let db_type = schema_editor.database_type();
let old_name = quote_identifier(old_table, db_type);
let new_name = quote_identifier(new_table, db_type);
vec![format!("ALTER TABLE {} RENAME TO {}", new_name, old_name)]
} else {
vec![]
}
} else {
vec![]
}
}
}
use crate::migrations::operation_trait::MigrationOperation;
impl MigrationOperation for CreateModel {
fn migration_name_fragment(&self) -> Option<String> {
Some(self.name.to_lowercase())
}
fn describe(&self) -> String {
format!("Create model {}", self.name)
}
}
impl MigrationOperation for DeleteModel {
fn migration_name_fragment(&self) -> Option<String> {
Some(format!("delete_{}", self.name.to_lowercase()))
}
fn describe(&self) -> String {
format!("Delete model {}", self.name)
}
}
impl MigrationOperation for RenameModel {
fn migration_name_fragment(&self) -> Option<String> {
Some(format!(
"rename_{}_to_{}",
self.old_name.to_lowercase(),
self.new_name.to_lowercase()
))
}
fn describe(&self) -> String {
format!("Rename model {} to {}", self.old_name, self.new_name)
}
}
impl MigrationOperation for MoveModel {
fn migration_name_fragment(&self) -> Option<String> {
Some(format!(
"move_{}_to_{}",
self.model_name.to_lowercase(),
self.to_app.to_lowercase()
))
}
fn describe(&self) -> String {
format!(
"Move model {} from {} to {}",
self.model_name, self.from_app, self.to_app
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrations::FieldType;
#[test]
fn test_field_definition_to_sql() {
let field = FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>);
let sql = field.to_sql_definition();
assert_eq!(sql, "INTEGER PRIMARY KEY");
let field2 =
FieldDefinition::new("email", FieldType::VarChar(255), false, true, Some("''"));
let sql2 = field2.to_sql_definition();
assert_eq!(sql2, "VARCHAR(255) UNIQUE NOT NULL DEFAULT ''");
}
#[test]
fn test_create_model_state_forwards() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![
FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
FieldDefinition::new(
"name",
FieldType::VarChar(100),
false,
false,
None::<String>,
),
],
);
create.state_forwards("myapp", &mut state);
let model = state.get_model("myapp", "User").unwrap();
assert_eq!(model.name, "User");
assert_eq!(model.fields.len(), 2);
assert_eq!(model.fields.get("id").unwrap().name, "id");
assert_eq!(model.fields.get("name").unwrap().name, "name");
}
#[test]
fn test_delete_model_state_forwards() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
create.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_some());
let delete = DeleteModel::new("User");
delete.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_none());
}
#[test]
fn test_rename_model_state_forwards() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
create.state_forwards("myapp", &mut state);
let rename = RenameModel::new("User", "Customer");
rename.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_none());
let model = state.get_model("myapp", "Customer").unwrap();
assert_eq!(model.name, "Customer");
}
#[cfg(feature = "db-postgres")]
#[test]
fn test_delete_model_database_forwards() {
use crate::backends::schema::test_utils::MockSchemaEditor;
let delete = DeleteModel::new("users");
let editor = MockSchemaEditor::new();
let sql = delete.database_forwards(&editor);
assert_eq!(sql.len(), 1);
assert_eq!(sql[0], "DROP TABLE IF EXISTS \"users\"");
}
#[cfg(feature = "db-postgres")]
#[test]
fn test_rename_model_database_forwards() {
use crate::backends::schema::test_utils::MockSchemaEditor;
let rename = RenameModel::new("users", "customers");
let editor = MockSchemaEditor::new();
let sql = rename.database_forwards(&editor);
assert_eq!(sql.len(), 1);
assert_eq!(sql[0], "ALTER TABLE \"users\" RENAME TO \"customers\"");
}
#[test]
fn test_field_definition_nullable() {
let field = FieldDefinition::new(
"email",
FieldType::VarChar(255),
false,
false,
None::<String>,
)
.nullable(true);
assert!(field.null);
let sql = field.to_sql_definition();
assert_eq!(sql, "VARCHAR(255)");
}
#[test]
fn test_create_model_with_options() {
let mut options = HashMap::new();
options.insert("db_table".to_string(), "custom_users".to_string());
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
)
.with_options(options.clone());
assert_eq!(create.options, options);
assert_eq!(
create.options.get("db_table"),
Some(&"custom_users".to_string())
);
}
#[test]
fn test_create_model_with_bases() {
let bases = vec!["BaseModel".to_string(), "Timestamped".to_string()];
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
)
.with_bases(bases.clone());
assert_eq!(create.bases, bases);
assert_eq!(create.bases.len(), 2);
}
#[test]
fn test_create_model_multiple_fields() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![
FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
FieldDefinition::new(
"username",
FieldType::VarChar(50),
false,
true,
None::<String>,
),
FieldDefinition::new(
"email",
FieldType::VarChar(255),
false,
true,
None::<String>,
),
FieldDefinition::new("is_active", FieldType::Boolean, false, false, Some("true")),
],
);
create.state_forwards("myapp", &mut state);
let model = state.get_model("myapp", "User").unwrap();
assert_eq!(model.fields.len(), 4);
assert_eq!(model.fields.get("id").unwrap().name, "id");
assert_eq!(model.fields.get("username").unwrap().name, "username");
assert_eq!(model.fields.get("email").unwrap().name, "email");
assert_eq!(model.fields.get("is_active").unwrap().name, "is_active");
}
#[test]
fn test_field_definition_with_default() {
let field = FieldDefinition::new(
"status",
FieldType::VarChar(20),
false,
false,
Some("'pending'"),
);
assert_eq!(field.default, Some("'pending'".to_string()));
let sql = field.to_sql_definition();
assert_eq!(sql, "VARCHAR(20) NOT NULL DEFAULT 'pending'");
}
#[test]
fn test_delete_model_removes_from_state() {
let mut state = ProjectState::new();
let create1 = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
let create2 = CreateModel::new(
"Post",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
create1.state_forwards("myapp", &mut state);
create2.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_some());
assert!(state.get_model("myapp", "Post").is_some());
let delete = DeleteModel::new("User");
delete.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_none());
assert!(state.get_model("myapp", "Post").is_some());
}
#[test]
fn test_rename_model_preserves_fields() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![
FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
FieldDefinition::new(
"name",
FieldType::VarChar(100),
false,
false,
None::<String>,
),
],
);
create.state_forwards("myapp", &mut state);
let rename = RenameModel::new("User", "Account");
rename.state_forwards("myapp", &mut state);
let model = state.get_model("myapp", "Account").unwrap();
assert_eq!(model.fields.len(), 2);
assert_eq!(model.fields.get("id").unwrap().name, "id");
assert_eq!(model.fields.get("name").unwrap().name, "name");
}
#[test]
fn test_move_model_basic() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
create.state_forwards("myapp", &mut state);
assert!(state.get_model("myapp", "User").is_some());
let move_op = MoveModel::new("User", "myapp", "auth");
move_op.state_forwards("auth", &mut state);
assert!(state.get_model("myapp", "User").is_none());
assert!(state.get_model("auth", "User").is_some());
let model = state.get_model("auth", "User").unwrap();
assert_eq!(model.app_label, "auth");
}
#[test]
fn test_move_model_preserves_fields() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![
FieldDefinition::new("id", FieldType::Integer, true, false, None::<String>),
FieldDefinition::new(
"email",
FieldType::VarChar(255),
false,
false,
None::<String>,
),
FieldDefinition::new(
"name",
FieldType::VarChar(100),
false,
false,
None::<String>,
),
],
);
create.state_forwards("myapp", &mut state);
let move_op = MoveModel::new("User", "myapp", "auth");
move_op.state_forwards("auth", &mut state);
let model = state.get_model("auth", "User").unwrap();
assert_eq!(model.fields.len(), 3);
assert_eq!(model.fields.get("id").unwrap().name, "id");
assert_eq!(model.fields.get("email").unwrap().name, "email");
assert_eq!(model.fields.get("name").unwrap().name, "name");
}
#[test]
fn test_move_model_backwards() {
let mut state = ProjectState::new();
let create = CreateModel::new(
"User",
vec![FieldDefinition::new(
"id",
FieldType::Integer,
true,
false,
None::<String>,
)],
);
create.state_forwards("myapp", &mut state);
let move_op = MoveModel::new("User", "myapp", "auth");
move_op.state_forwards("auth", &mut state);
assert!(state.get_model("auth", "User").is_some());
move_op.state_backwards("myapp", &mut state);
assert!(state.get_model("auth", "User").is_none());
assert!(state.get_model("myapp", "User").is_some());
let model = state.get_model("myapp", "User").unwrap();
assert_eq!(model.app_label, "myapp");
}
#[cfg(feature = "db-postgres")]
#[test]
fn test_move_model_without_table_rename() {
use crate::backends::schema::test_utils::MockSchemaEditor;
let move_op = MoveModel::new("User", "myapp", "auth");
let editor = MockSchemaEditor::new();
let sql = move_op.database_forwards(&editor);
assert_eq!(sql.len(), 0);
}
#[cfg(feature = "db-postgres")]
#[test]
fn test_move_model_with_table_rename() {
use crate::backends::schema::test_utils::MockSchemaEditor;
let move_op =
MoveModel::new("User", "myapp", "auth").with_table_rename("myapp_user", "auth_user");
let editor = MockSchemaEditor::new();
let sql = move_op.database_forwards(&editor);
assert_eq!(sql.len(), 1);
assert_eq!(sql[0], "ALTER TABLE \"myapp_user\" RENAME TO \"auth_user\"");
}
#[cfg(feature = "db-postgres")]
#[test]
fn test_move_model_backward_sql() {
use crate::backends::schema::test_utils::MockSchemaEditor;
let move_op =
MoveModel::new("User", "myapp", "auth").with_table_rename("myapp_user", "auth_user");
let editor = MockSchemaEditor::new();
let sql = move_op.database_backwards(&editor);
assert_eq!(sql.len(), 1);
assert_eq!(sql[0], "ALTER TABLE \"auth_user\" RENAME TO \"myapp_user\"");
}
}