Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_nEUFzhXlesutfHyTo9oo1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch"},"note":"Add migration actions","date":"2025-12-24T08:09:20.483794300Z"}
158 changes: 158 additions & 0 deletions crates/vespertide-cli/src/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,57 @@ fn format_action(action: &MigrationAction) -> String {
column.bright_cyan().bold()
)
}
MigrationAction::ModifyColumnNullable {
table,
column,
nullable,
..
} => {
let nullability = if *nullable { "NULL" } else { "NOT NULL" };
format!(
"{} {}.{} {} {}",
"Modify column nullability:".bright_yellow(),
table.bright_cyan(),
column.bright_cyan().bold(),
"->".bright_white(),
nullability.bright_cyan().bold()
)
}
MigrationAction::ModifyColumnDefault {
table,
column,
new_default,
} => {
let default_display = new_default.as_deref().unwrap_or("(none)");
format!(
"{} {}.{} {} {}",
"Modify column default:".bright_yellow(),
table.bright_cyan(),
column.bright_cyan().bold(),
"->".bright_white(),
default_display.bright_cyan().bold()
)
}
MigrationAction::ModifyColumnComment {
table,
column,
new_comment,
} => {
let comment_display = new_comment.as_deref().unwrap_or("(none)");
let truncated = if comment_display.len() > 30 {
format!("{}...", &comment_display[..27])
} else {
comment_display.to_string()
};
format!(
"{} {}.{} {} '{}'",
"Modify column comment:".bright_yellow(),
table.bright_cyan(),
column.bright_cyan().bold(),
"->".bright_white(),
truncated.bright_cyan().bold()
)
}
MigrationAction::RenameTable { from, to } => {
format!(
"{} {} {} {}",
Expand Down Expand Up @@ -393,6 +444,113 @@ mod tests {
"users".bright_cyan()
)
)]
#[case(
MigrationAction::ModifyColumnNullable {
table: "users".into(),
column: "email".into(),
nullable: false,
fill_with: None,
},
format!(
"{} {}.{} {} {}",
"Modify column nullability:".bright_yellow(),
"users".bright_cyan(),
"email".bright_cyan().bold(),
"->".bright_white(),
"NOT NULL".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnNullable {
table: "users".into(),
column: "email".into(),
nullable: true,
fill_with: None,
},
format!(
"{} {}.{} {} {}",
"Modify column nullability:".bright_yellow(),
"users".bright_cyan(),
"email".bright_cyan().bold(),
"->".bright_white(),
"NULL".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnDefault {
table: "users".into(),
column: "status".into(),
new_default: Some("'active'".into()),
},
format!(
"{} {}.{} {} {}",
"Modify column default:".bright_yellow(),
"users".bright_cyan(),
"status".bright_cyan().bold(),
"->".bright_white(),
"'active'".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnDefault {
table: "users".into(),
column: "status".into(),
new_default: None,
},
format!(
"{} {}.{} {} {}",
"Modify column default:".bright_yellow(),
"users".bright_cyan(),
"status".bright_cyan().bold(),
"->".bright_white(),
"(none)".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: Some("User email address".into()),
},
format!(
"{} {}.{} {} '{}'",
"Modify column comment:".bright_yellow(),
"users".bright_cyan(),
"email".bright_cyan().bold(),
"->".bright_white(),
"User email address".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: None,
},
format!(
"{} {}.{} {} '{}'",
"Modify column comment:".bright_yellow(),
"users".bright_cyan(),
"email".bright_cyan().bold(),
"->".bright_white(),
"(none)".bright_cyan().bold()
)
)]
#[case(
MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: Some("This is a very long comment that exceeds thirty characters and should be truncated".into()),
},
format!(
"{} {}.{} {} '{}'",
"Modify column comment:".bright_yellow(),
"users".bright_cyan(),
"email".bright_cyan().bold(),
"->".bright_white(),
"This is a very long comment...".bright_cyan().bold()
)
)]
#[serial]
fn format_action_cases(#[case] action: MigrationAction, #[case] expected: String) {
assert_eq!(format_action(&action), expected);
Expand Down
146 changes: 146 additions & 0 deletions crates/vespertide-core/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ pub enum MigrationAction {
column: ColumnName,
new_type: ColumnType,
},
ModifyColumnNullable {
table: TableName,
column: ColumnName,
nullable: bool,
/// Required when changing from nullable to non-nullable to backfill existing NULL values.
fill_with: Option<String>,
},
ModifyColumnDefault {
table: TableName,
column: ColumnName,
/// The new default value, or None to remove the default.
new_default: Option<String>,
},
ModifyColumnComment {
table: TableName,
column: ColumnName,
/// The new comment, or None to remove the comment.
new_comment: Option<String>,
},
AddConstraint {
table: TableName,
constraint: TableConstraint,
Expand Down Expand Up @@ -82,6 +101,42 @@ impl fmt::Display for MigrationAction {
MigrationAction::ModifyColumnType { table, column, .. } => {
write!(f, "ModifyColumnType: {}.{}", table, column)
}
MigrationAction::ModifyColumnNullable {
table,
column,
nullable,
..
} => {
let nullability = if *nullable { "NULL" } else { "NOT NULL" };
write!(f, "ModifyColumnNullable: {}.{} -> {}", table, column, nullability)
}
MigrationAction::ModifyColumnDefault {
table,
column,
new_default,
} => {
if let Some(default) = new_default {
write!(f, "ModifyColumnDefault: {}.{} -> {}", table, column, default)
} else {
write!(f, "ModifyColumnDefault: {}.{} -> (none)", table, column)
}
}
MigrationAction::ModifyColumnComment {
table,
column,
new_comment,
} => {
if let Some(comment) = new_comment {
let display = if comment.len() > 30 {
format!("{}...", &comment[..27])
} else {
comment.clone()
};
write!(f, "ModifyColumnComment: {}.{} -> '{}'", table, column, display)
} else {
write!(f, "ModifyColumnComment: {}.{} -> (none)", table, column)
}
}
MigrationAction::AddConstraint { table, constraint } => {
let constraint_name = match constraint {
TableConstraint::PrimaryKey { .. } => "PRIMARY KEY",
Expand Down Expand Up @@ -438,4 +493,95 @@ mod tests {
assert!(result.ends_with("..."));
assert!(result.len() > 10);
}

#[rstest]
#[case::modify_column_nullable_to_not_null(
MigrationAction::ModifyColumnNullable {
table: "users".into(),
column: "email".into(),
nullable: false,
fill_with: None,
},
"ModifyColumnNullable: users.email -> NOT NULL"
)]
#[case::modify_column_nullable_to_null(
MigrationAction::ModifyColumnNullable {
table: "users".into(),
column: "email".into(),
nullable: true,
fill_with: None,
},
"ModifyColumnNullable: users.email -> NULL"
)]
fn test_display_modify_column_nullable(
#[case] action: MigrationAction,
#[case] expected: &str,
) {
assert_eq!(action.to_string(), expected);
}

#[rstest]
#[case::modify_column_default_set(
MigrationAction::ModifyColumnDefault {
table: "users".into(),
column: "status".into(),
new_default: Some("'active'".into()),
},
"ModifyColumnDefault: users.status -> 'active'"
)]
#[case::modify_column_default_drop(
MigrationAction::ModifyColumnDefault {
table: "users".into(),
column: "status".into(),
new_default: None,
},
"ModifyColumnDefault: users.status -> (none)"
)]
fn test_display_modify_column_default(
#[case] action: MigrationAction,
#[case] expected: &str,
) {
assert_eq!(action.to_string(), expected);
}

#[rstest]
#[case::modify_column_comment_set(
MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: Some("User email address".into()),
},
"ModifyColumnComment: users.email -> 'User email address'"
)]
#[case::modify_column_comment_drop(
MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: None,
},
"ModifyColumnComment: users.email -> (none)"
)]
fn test_display_modify_column_comment(
#[case] action: MigrationAction,
#[case] expected: &str,
) {
assert_eq!(action.to_string(), expected);
}

#[test]
fn test_display_modify_column_comment_long() {
// Test truncation for long comments (> 30 chars)
let action = MigrationAction::ModifyColumnComment {
table: "users".into(),
column: "email".into(),
new_comment: Some(
"This is a very long comment that should be truncated in display".into(),
),
};
let result = action.to_string();
assert!(result.contains("..."));
assert!(result.contains("This is a very long comment"));
// Should be truncated at 27 chars + "..."
assert!(!result.contains("truncated in display"));
}
}
Loading