Skip to content

Commit 244a1c8

Browse files
committed
Update Removed value
1 parent cbbd1e1 commit 244a1c8

2 files changed

Lines changed: 338 additions & 0 deletions

File tree

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

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,4 +1537,168 @@ mod tests {
15371537
fn test_wrap_if_spaces_multiple_spaces() {
15381538
assert_eq!(wrap_if_spaces("a b c".to_string()), "'a b c'");
15391539
}
1540+
1541+
// ── enum fill_with tests ───────────────────────────────────────────
1542+
1543+
#[test]
1544+
fn test_collect_enum_fill_with_values_single_removal() {
1545+
use vespertide_planner::EnumFillWithRequired;
1546+
1547+
let missing = vec![EnumFillWithRequired {
1548+
action_index: 0,
1549+
table: "orders".to_string(),
1550+
column: "status".to_string(),
1551+
removed_values: vec!["cancelled".to_string()],
1552+
remaining_values: vec!["pending".to_string(), "shipped".to_string()],
1553+
}];
1554+
1555+
// Mock prompt: always select first remaining value
1556+
let mock_enum = |_prompt: &str, values: &[String]| -> Result<String> {
1557+
Ok(format!("'{}'", values[0]))
1558+
};
1559+
1560+
let result = collect_enum_fill_with_values(&missing, mock_enum);
1561+
assert!(result.is_ok());
1562+
let collected = result.unwrap();
1563+
assert_eq!(collected.len(), 1);
1564+
assert_eq!(collected[0].0, 0); // action_index
1565+
assert_eq!(collected[0].1.get("cancelled"), Some(&"'pending'".to_string()));
1566+
}
1567+
1568+
#[test]
1569+
fn test_collect_enum_fill_with_values_multiple_removals() {
1570+
use vespertide_planner::EnumFillWithRequired;
1571+
1572+
let missing = vec![EnumFillWithRequired {
1573+
action_index: 0,
1574+
table: "orders".to_string(),
1575+
column: "status".to_string(),
1576+
removed_values: vec!["cancelled".to_string(), "draft".to_string()],
1577+
remaining_values: vec!["pending".to_string(), "shipped".to_string()],
1578+
}];
1579+
1580+
// Mock prompt: always select second remaining value
1581+
let mock_enum = |_prompt: &str, values: &[String]| -> Result<String> {
1582+
Ok(format!("'{}'", values[1]))
1583+
};
1584+
1585+
let result = collect_enum_fill_with_values(&missing, mock_enum);
1586+
assert!(result.is_ok());
1587+
let collected = result.unwrap();
1588+
assert_eq!(collected[0].1.len(), 2);
1589+
assert_eq!(collected[0].1.get("cancelled"), Some(&"'shipped'".to_string()));
1590+
assert_eq!(collected[0].1.get("draft"), Some(&"'shipped'".to_string()));
1591+
}
1592+
1593+
#[test]
1594+
fn test_apply_enum_fill_with_to_plan() {
1595+
use vespertide_core::{ColumnType, ComplexColumnType, EnumValues};
1596+
1597+
let mut plan = MigrationPlan {
1598+
id: String::new(),
1599+
comment: None,
1600+
created_at: None,
1601+
version: 2,
1602+
actions: vec![MigrationAction::ModifyColumnType {
1603+
table: "orders".into(),
1604+
column: "status".into(),
1605+
new_type: ColumnType::Complex(ComplexColumnType::Enum {
1606+
name: "order_status".into(),
1607+
values: EnumValues::String(vec!["pending".into(), "shipped".into()]),
1608+
}),
1609+
fill_with: None,
1610+
}],
1611+
};
1612+
1613+
let mut mappings = BTreeMap::new();
1614+
mappings.insert("cancelled".to_string(), "'pending'".to_string());
1615+
let collected = vec![(0usize, mappings)];
1616+
1617+
apply_enum_fill_with_to_plan(&mut plan, &collected);
1618+
1619+
if let MigrationAction::ModifyColumnType { fill_with, .. } = &plan.actions[0] {
1620+
let fw = fill_with.as_ref().expect("fill_with should be set");
1621+
assert_eq!(fw.get("cancelled"), Some(&"'pending'".to_string()));
1622+
} else {
1623+
panic!("Expected ModifyColumnType");
1624+
}
1625+
}
1626+
1627+
#[test]
1628+
fn test_handle_missing_enum_fill_with_collects_and_applies() {
1629+
use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, EnumValues};
1630+
1631+
let mut plan = MigrationPlan {
1632+
id: String::new(),
1633+
comment: None,
1634+
created_at: None,
1635+
version: 2,
1636+
actions: vec![MigrationAction::ModifyColumnType {
1637+
table: "orders".into(),
1638+
column: "status".into(),
1639+
new_type: ColumnType::Complex(ComplexColumnType::Enum {
1640+
name: "order_status".into(),
1641+
values: EnumValues::String(vec!["pending".into(), "shipped".into()]),
1642+
}),
1643+
fill_with: None,
1644+
}],
1645+
};
1646+
1647+
let baseline = vec![TableDef {
1648+
name: "orders".into(),
1649+
description: None,
1650+
columns: vec![ColumnDef {
1651+
name: "status".into(),
1652+
r#type: ColumnType::Complex(ComplexColumnType::Enum {
1653+
name: "order_status".into(),
1654+
values: EnumValues::String(vec![
1655+
"pending".into(),
1656+
"shipped".into(),
1657+
"cancelled".into(),
1658+
]),
1659+
}),
1660+
nullable: false,
1661+
default: None,
1662+
comment: None,
1663+
primary_key: None,
1664+
unique: None,
1665+
index: None,
1666+
foreign_key: None,
1667+
}],
1668+
constraints: vec![],
1669+
}];
1670+
1671+
// Mock: always select first remaining value
1672+
let mock_enum = |_prompt: &str, values: &[String]| -> Result<String> {
1673+
Ok(format!("'{}'", values[0]))
1674+
};
1675+
1676+
let result = handle_missing_enum_fill_with(&mut plan, &baseline, mock_enum);
1677+
assert!(result.is_ok());
1678+
1679+
if let MigrationAction::ModifyColumnType { fill_with, .. } = &plan.actions[0] {
1680+
let fw = fill_with.as_ref().expect("fill_with should be populated");
1681+
assert_eq!(fw.get("cancelled"), Some(&"'pending'".to_string()));
1682+
} else {
1683+
panic!("Expected ModifyColumnType");
1684+
}
1685+
}
1686+
1687+
#[test]
1688+
fn test_handle_missing_enum_fill_with_no_missing() {
1689+
let mut plan = MigrationPlan {
1690+
id: String::new(),
1691+
comment: None,
1692+
created_at: None,
1693+
version: 2,
1694+
actions: vec![],
1695+
};
1696+
1697+
let mock_enum = |_prompt: &str, _values: &[String]| -> Result<String> {
1698+
panic!("Should not be called when nothing is missing");
1699+
};
1700+
1701+
let result = handle_missing_enum_fill_with(&mut plan, &[], mock_enum);
1702+
assert!(result.is_ok());
1703+
}
15401704
}

crates/vespertide-planner/src/validate.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,4 +2186,178 @@ mod tests {
21862186
"Bool primary key should not trigger auto_increment validation"
21872187
);
21882188
}
2189+
2190+
// ── find_missing_enum_fill_with tests ──────────────────────────────
2191+
2192+
fn string_enum(name: &str, values: Vec<&str>) -> ColumnType {
2193+
ColumnType::Complex(ComplexColumnType::Enum {
2194+
name: name.into(),
2195+
values: EnumValues::String(values.into_iter().map(|s| s.to_string()).collect()),
2196+
})
2197+
}
2198+
2199+
#[test]
2200+
fn find_missing_enum_fill_with_detects_removed_values() {
2201+
let plan = MigrationPlan {
2202+
id: String::new(),
2203+
comment: None,
2204+
created_at: None,
2205+
version: 2,
2206+
actions: vec![MigrationAction::ModifyColumnType {
2207+
table: "orders".into(),
2208+
column: "status".into(),
2209+
new_type: string_enum("order_status", vec!["pending", "shipped"]),
2210+
fill_with: None,
2211+
}],
2212+
};
2213+
let baseline = vec![table(
2214+
"orders",
2215+
vec![col("status", string_enum("order_status", vec!["pending", "shipped", "cancelled"]))],
2216+
vec![],
2217+
)];
2218+
2219+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2220+
assert_eq!(missing.len(), 1);
2221+
assert_eq!(missing[0].table, "orders");
2222+
assert_eq!(missing[0].column, "status");
2223+
assert_eq!(missing[0].removed_values, vec!["cancelled"]);
2224+
assert_eq!(missing[0].remaining_values, vec!["pending", "shipped"]);
2225+
}
2226+
2227+
#[test]
2228+
fn find_missing_enum_fill_with_ignores_additions_only() {
2229+
let plan = MigrationPlan {
2230+
id: String::new(),
2231+
comment: None,
2232+
created_at: None,
2233+
version: 2,
2234+
actions: vec![MigrationAction::ModifyColumnType {
2235+
table: "orders".into(),
2236+
column: "status".into(),
2237+
new_type: string_enum("order_status", vec!["pending", "shipped", "delivered"]),
2238+
fill_with: None,
2239+
}],
2240+
};
2241+
let baseline = vec![table(
2242+
"orders",
2243+
vec![col("status", string_enum("order_status", vec!["pending", "shipped"]))],
2244+
vec![],
2245+
)];
2246+
2247+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2248+
assert!(missing.is_empty(), "Adding values should not trigger fill_with");
2249+
}
2250+
2251+
#[test]
2252+
fn find_missing_enum_fill_with_skips_already_covered() {
2253+
let mut fw = std::collections::BTreeMap::new();
2254+
fw.insert("cancelled".to_string(), "'pending'".to_string());
2255+
2256+
let plan = MigrationPlan {
2257+
id: String::new(),
2258+
comment: None,
2259+
created_at: None,
2260+
version: 2,
2261+
actions: vec![MigrationAction::ModifyColumnType {
2262+
table: "orders".into(),
2263+
column: "status".into(),
2264+
new_type: string_enum("order_status", vec!["pending", "shipped"]),
2265+
fill_with: Some(fw),
2266+
}],
2267+
};
2268+
let baseline = vec![table(
2269+
"orders",
2270+
vec![col("status", string_enum("order_status", vec!["pending", "shipped", "cancelled"]))],
2271+
vec![],
2272+
)];
2273+
2274+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2275+
assert!(missing.is_empty(), "All removed values are covered by fill_with");
2276+
}
2277+
2278+
#[test]
2279+
fn find_missing_enum_fill_with_reports_partially_covered() {
2280+
let mut fw = std::collections::BTreeMap::new();
2281+
fw.insert("cancelled".to_string(), "'pending'".to_string());
2282+
2283+
let plan = MigrationPlan {
2284+
id: String::new(),
2285+
comment: None,
2286+
created_at: None,
2287+
version: 2,
2288+
actions: vec![MigrationAction::ModifyColumnType {
2289+
table: "orders".into(),
2290+
column: "status".into(),
2291+
new_type: string_enum("order_status", vec!["pending"]),
2292+
fill_with: Some(fw),
2293+
}],
2294+
};
2295+
let baseline = vec![table(
2296+
"orders",
2297+
vec![col("status", string_enum("order_status", vec!["pending", "shipped", "cancelled"]))],
2298+
vec![],
2299+
)];
2300+
2301+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2302+
assert_eq!(missing.len(), 1);
2303+
assert_eq!(missing[0].removed_values, vec!["shipped"], "Only uncovered value should be reported");
2304+
}
2305+
2306+
#[test]
2307+
fn find_missing_enum_fill_with_ignores_integer_enums() {
2308+
let old_type = ColumnType::Complex(ComplexColumnType::Enum {
2309+
name: "priority".into(),
2310+
values: EnumValues::Integer(vec![
2311+
NumValue { name: "low".into(), value: 0 },
2312+
NumValue { name: "high".into(), value: 1 },
2313+
]),
2314+
});
2315+
let new_type = ColumnType::Complex(ComplexColumnType::Enum {
2316+
name: "priority".into(),
2317+
values: EnumValues::Integer(vec![
2318+
NumValue { name: "high".into(), value: 1 },
2319+
]),
2320+
});
2321+
2322+
let plan = MigrationPlan {
2323+
id: String::new(),
2324+
comment: None,
2325+
created_at: None,
2326+
version: 2,
2327+
actions: vec![MigrationAction::ModifyColumnType {
2328+
table: "tasks".into(),
2329+
column: "priority".into(),
2330+
new_type,
2331+
fill_with: None,
2332+
}],
2333+
};
2334+
let baseline = vec![table("tasks", vec![col("priority", old_type)], vec![])];
2335+
2336+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2337+
assert!(missing.is_empty(), "Integer enum changes should not trigger fill_with");
2338+
}
2339+
2340+
#[test]
2341+
fn find_missing_enum_fill_with_ignores_non_enum_type_change() {
2342+
let plan = MigrationPlan {
2343+
id: String::new(),
2344+
comment: None,
2345+
created_at: None,
2346+
version: 2,
2347+
actions: vec![MigrationAction::ModifyColumnType {
2348+
table: "users".into(),
2349+
column: "age".into(),
2350+
new_type: ColumnType::Simple(SimpleColumnType::BigInt),
2351+
fill_with: None,
2352+
}],
2353+
};
2354+
let baseline = vec![table(
2355+
"users",
2356+
vec![col("age", ColumnType::Simple(SimpleColumnType::Integer))],
2357+
vec![],
2358+
)];
2359+
2360+
let missing = find_missing_enum_fill_with(&plan, &baseline);
2361+
assert!(missing.is_empty(), "Non-enum type changes should not trigger fill_with");
2362+
}
21892363
}

0 commit comments

Comments
 (0)