Skip to content

Commit 647677e

Browse files
authored
Merge pull request #80 from dev-five-git/fix-utf8-enum
Fix unicode, enum default
2 parents e25c702 + c46336a commit 647677e

9 files changed

Lines changed: 161 additions & 34 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch"},"note":"Fix unicode issue and enum default issue","date":"2026-01-23T05:43:59.168949900Z"}

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespertide-cli/src/commands/diff.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,11 @@ fn format_action(action: &MigrationAction) -> String {
126126
new_comment,
127127
} => {
128128
let comment_display = new_comment.as_deref().unwrap_or("(none)");
129-
let truncated = if comment_display.len() > 30 {
130-
format!("{}...", &comment_display[..27])
129+
let truncated = if comment_display.chars().count() > 30 {
130+
format!(
131+
"{}...",
132+
comment_display.chars().take(27).collect::<String>()
133+
)
131134
} else {
132135
comment_display.to_string()
133136
};

crates/vespertide-cli/src/commands/revision.rs

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs;
44
use anyhow::{Context, Result};
55
use chrono::Utc;
66
use colored::Colorize;
7-
use dialoguer::Input;
7+
use dialoguer::{Input, Select};
88
use serde_json::Value;
99
use vespertide_config::FileFormat;
1010
use vespertide_core::{MigrationAction, MigrationPlan};
@@ -118,15 +118,32 @@ fn prompt_fill_with_value(prompt: &str) -> Result<String> {
118118
Ok(wrap_if_spaces(value))
119119
}
120120

121+
/// Prompt the user to select an enum value using dialoguer Select.
122+
/// Returns the selected value wrapped in single quotes for SQL.
123+
#[cfg(not(tarpaulin_include))]
124+
fn prompt_enum_value(prompt: &str, enum_values: &[String]) -> Result<String> {
125+
let selection = Select::new()
126+
.with_prompt(prompt)
127+
.items(enum_values)
128+
.default(0)
129+
.interact()
130+
.context("failed to read selection")?;
131+
// Return the selected value with single quotes for SQL enum literal
132+
Ok(format!("'{}'", enum_values[selection]))
133+
}
134+
121135
/// Collect fill_with values interactively for missing columns.
122136
/// The `prompt_fn` parameter allows injecting a mock for testing.
123-
fn collect_fill_with_values<F>(
137+
/// The `enum_prompt_fn` parameter handles enum type columns with selection UI.
138+
fn collect_fill_with_values<F, E>(
124139
missing: &[vespertide_planner::FillWithRequired],
125140
fill_values: &mut HashMap<(String, String), String>,
126141
prompt_fn: F,
142+
enum_prompt_fn: E,
127143
) -> Result<()>
128144
where
129145
F: Fn(&str) -> Result<String>,
146+
E: Fn(&str, &[String]) -> Result<String>,
130147
{
131148
print_fill_with_header();
132149

@@ -139,7 +156,13 @@ where
139156
item.action_type,
140157
);
141158

142-
let value = prompt_fn(&prompt)?;
159+
let value = if let Some(enum_values) = &item.enum_values {
160+
// Use selection UI for enum types
161+
enum_prompt_fn(&prompt, enum_values)?
162+
} else {
163+
// Use text input for other types
164+
prompt_fn(&prompt)?
165+
};
143166
fill_values.insert((item.table.clone(), item.column.clone()), value);
144167
}
145168

@@ -184,18 +207,20 @@ fn apply_fill_with_to_plan(
184207

185208
/// Handle interactive fill_with collection if there are missing values.
186209
/// Returns the updated fill_values map after collecting from user.
187-
fn handle_missing_fill_with<F>(
210+
fn handle_missing_fill_with<F, E>(
188211
plan: &mut MigrationPlan,
189212
fill_values: &mut HashMap<(String, String), String>,
190213
prompt_fn: F,
214+
enum_prompt_fn: E,
191215
) -> Result<()>
192216
where
193217
F: Fn(&str) -> Result<String>,
218+
E: Fn(&str, &[String]) -> Result<String>,
194219
{
195220
let missing = find_missing_fill_with(plan);
196221

197222
if !missing.is_empty() {
198-
collect_fill_with_values(&missing, fill_values, prompt_fn)?;
223+
collect_fill_with_values(&missing, fill_values, prompt_fn, enum_prompt_fn)?;
199224

200225
// Apply the collected fill_with values
201226
apply_fill_with_to_plan(plan, fill_values);
@@ -228,7 +253,12 @@ pub fn cmd_revision(message: String, fill_with_args: Vec<String>) -> Result<()>
228253
apply_fill_with_to_plan(&mut plan, &fill_values);
229254

230255
// Handle any missing fill_with values interactively
231-
handle_missing_fill_with(&mut plan, &mut fill_values, prompt_fill_with_value)?;
256+
handle_missing_fill_with(
257+
&mut plan,
258+
&mut fill_values,
259+
prompt_fill_with_value,
260+
prompt_enum_value,
261+
)?;
232262

233263
plan.comment = Some(message);
234264
if plan.created_at.is_none() {
@@ -838,6 +868,11 @@ mod tests {
838868
print_fill_with_footer();
839869
}
840870

871+
// Mock enum prompt function for tests - returns first enum value quoted
872+
fn mock_enum_prompt(_prompt: &str, values: &[String]) -> Result<String> {
873+
Ok(format!("'{}'", values[0]))
874+
}
875+
841876
#[test]
842877
fn test_collect_fill_with_values_single_item() {
843878
use vespertide_planner::FillWithRequired;
@@ -849,6 +884,7 @@ mod tests {
849884
action_type: "AddColumn",
850885
column_type: Some("text".to_string()),
851886
default_value: Some("''".to_string()),
887+
enum_values: None,
852888
}];
853889

854890
let mut fill_values = HashMap::new();
@@ -857,7 +893,8 @@ mod tests {
857893
let mock_prompt =
858894
|_prompt: &str| -> Result<String> { Ok("'test@example.com'".to_string()) };
859895

860-
let result = collect_fill_with_values(&missing, &mut fill_values, mock_prompt);
896+
let result =
897+
collect_fill_with_values(&missing, &mut fill_values, mock_prompt, mock_enum_prompt);
861898
assert!(result.is_ok());
862899
assert_eq!(fill_values.len(), 1);
863900
assert_eq!(
@@ -878,6 +915,7 @@ mod tests {
878915
action_type: "AddColumn",
879916
column_type: Some("text".to_string()),
880917
default_value: Some("''".to_string()),
918+
enum_values: None,
881919
},
882920
FillWithRequired {
883921
action_index: 1,
@@ -886,6 +924,7 @@ mod tests {
886924
action_type: "ModifyColumnNullable",
887925
column_type: None,
888926
default_value: None,
927+
enum_values: None,
889928
},
890929
];
891930

@@ -903,7 +942,8 @@ mod tests {
903942
}
904943
};
905944

906-
let result = collect_fill_with_values(&missing, &mut fill_values, mock_prompt);
945+
let result =
946+
collect_fill_with_values(&missing, &mut fill_values, mock_prompt, mock_enum_prompt);
907947
assert!(result.is_ok());
908948
assert_eq!(fill_values.len(), 2);
909949
assert_eq!(
@@ -930,7 +970,8 @@ mod tests {
930970

931971
// Note: The function still prints header/footer even for empty list
932972
// This is a design choice - in practice, cmd_revision won't call this with empty list
933-
let result = collect_fill_with_values(&missing, &mut fill_values, mock_prompt);
973+
let result =
974+
collect_fill_with_values(&missing, &mut fill_values, mock_prompt, mock_enum_prompt);
934975
assert!(result.is_ok());
935976
assert!(fill_values.is_empty());
936977
}
@@ -946,6 +987,7 @@ mod tests {
946987
action_type: "AddColumn",
947988
column_type: Some("text".to_string()),
948989
default_value: Some("''".to_string()),
990+
enum_values: None,
949991
}];
950992

951993
let mut fill_values = HashMap::new();
@@ -954,7 +996,8 @@ mod tests {
954996
let mock_prompt =
955997
|_prompt: &str| -> Result<String> { Err(anyhow::anyhow!("input cancelled")) };
956998

957-
let result = collect_fill_with_values(&missing, &mut fill_values, mock_prompt);
999+
let result =
1000+
collect_fill_with_values(&missing, &mut fill_values, mock_prompt, mock_enum_prompt);
9581001
assert!(result.is_err());
9591002
assert!(fill_values.is_empty());
9601003
}
@@ -998,7 +1041,8 @@ mod tests {
9981041
let mock_prompt =
9991042
|_prompt: &str| -> Result<String> { Ok("'test@example.com'".to_string()) };
10001043

1001-
let result = handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt);
1044+
let result =
1045+
handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt, mock_enum_prompt);
10021046
assert!(result.is_ok());
10031047

10041048
// Verify fill_with was applied to the plan
@@ -1049,7 +1093,8 @@ mod tests {
10491093
panic!("Should not be called when no missing fill_with values");
10501094
};
10511095

1052-
let result = handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt);
1096+
let result =
1097+
handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt, mock_enum_prompt);
10531098
assert!(result.is_ok());
10541099
assert!(fill_values.is_empty());
10551100
}
@@ -1085,7 +1130,8 @@ mod tests {
10851130
let mock_prompt =
10861131
|_prompt: &str| -> Result<String> { Err(anyhow::anyhow!("user cancelled")) };
10871132

1088-
let result = handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt);
1133+
let result =
1134+
handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt, mock_enum_prompt);
10891135
assert!(result.is_err());
10901136

10911137
// Plan should not be modified on error
@@ -1144,7 +1190,8 @@ mod tests {
11441190
}
11451191
};
11461192

1147-
let result = handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt);
1193+
let result =
1194+
handle_missing_fill_with(&mut plan, &mut fill_values, mock_prompt, mock_enum_prompt);
11481195
assert!(result.is_ok());
11491196

11501197
// Verify both actions were updated
@@ -1163,6 +1210,46 @@ mod tests {
11631210
}
11641211
}
11651212

1213+
#[test]
1214+
fn test_collect_fill_with_values_enum_column() {
1215+
use vespertide_planner::FillWithRequired;
1216+
1217+
let missing = vec![FillWithRequired {
1218+
action_index: 0,
1219+
table: "orders".to_string(),
1220+
column: "status".to_string(),
1221+
action_type: "AddColumn",
1222+
column_type: Some("enum<order_status>".to_string()),
1223+
default_value: None,
1224+
enum_values: Some(vec![
1225+
"pending".to_string(),
1226+
"confirmed".to_string(),
1227+
"shipped".to_string(),
1228+
]),
1229+
}];
1230+
1231+
let mut fill_values = HashMap::new();
1232+
1233+
// Mock prompt function that should NOT be called for enum columns
1234+
let mock_prompt = |_prompt: &str| -> Result<String> {
1235+
panic!("Should not be called for enum columns");
1236+
};
1237+
1238+
// Mock enum prompt that selects the second value
1239+
let mock_enum = |_prompt: &str, values: &[String]| -> Result<String> {
1240+
// Select "confirmed" (index 1)
1241+
Ok(format!("'{}'", values[1]))
1242+
};
1243+
1244+
let result = collect_fill_with_values(&missing, &mut fill_values, mock_prompt, mock_enum);
1245+
assert!(result.is_ok());
1246+
assert_eq!(fill_values.len(), 1);
1247+
assert_eq!(
1248+
fill_values.get(&("orders".to_string(), "status".to_string())),
1249+
Some(&"'confirmed'".to_string())
1250+
);
1251+
}
1252+
11661253
#[test]
11671254
fn test_wrap_if_spaces_empty() {
11681255
assert_eq!(wrap_if_spaces("".to_string()), "");

crates/vespertide-core/src/action.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ impl fmt::Display for MigrationAction {
252252
new_comment,
253253
} => {
254254
if let Some(comment) = new_comment {
255-
let display = if comment.len() > 30 {
256-
format!("{}...", &comment[..27])
255+
let display = if comment.chars().count() > 30 {
256+
format!("{}...", comment.chars().take(27).collect::<String>())
257257
} else {
258258
comment.clone()
259259
};

0 commit comments

Comments
 (0)