feat(schema): enum widening + enum→String migration (Safe path)

Add a ChangeEnumConstraint migration step and detect enum value-set
deltas in the planner before the generic prop-type-change rejection.
This commit lands the Safe (metadata-only) cases:

- widen (add allowed variants): every existing row is still valid, so
  it's a no-code, no-scan change.
- enum→String (loosen to a free string): every enum value is a valid
  String, so likewise Safe.

Enums are stored physically as Arrow String, so these are catalog-only
changes — no table rewrite, the manifest version doesn't advance, and
apply rides the metadata-only path (handled as a no-op in the step
loop). The CLI `schema plan` renderer shows the step (with code+tier
when present). The new variant's diagnostic() resolves its attached code
so apply/render derive the tier from one source of truth.

Validated tightenings (narrow, String→enum) deliberately still fall
through to UnsupportedChange here; they're enabled in the next commit
together with the apply-time row scan, so we never accept a tightening
we can't validate against existing data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-05-25 23:51:04 +01:00
parent df30aa6935
commit ae39049267
4 changed files with 273 additions and 18 deletions

View file

@ -1121,6 +1121,34 @@ fn render_schema_plan_step(step: &SchemaMigrationStep) -> String {
type_name,
drop_mode_label(*mode),
),
SchemaMigrationStep::ChangeEnumConstraint {
type_kind,
type_name,
property_name,
to_property_type,
..
} => {
// Show code + tier when attached (a Validated tightening), so
// operators see that apply will scan existing rows; a Safe
// widen / loosen carries no code.
let base = format!(
"change enum constraint on '{}.{}' -> {} on {} '{}'",
type_name,
property_name,
render_prop_type(to_property_type),
schema_type_kind_label(*type_kind),
type_name,
);
match step.diagnostic() {
Some(diag) => format!(
"{} [{}, {}]",
base,
diag.code,
schema_lint_tier_label(diag.tier),
),
None => base,
}
}
SchemaMigrationStep::UnsupportedChange { entity, reason, .. } => {
// When a schema-lint code is attached, render code + tier
// so operators see at-a-glance the kind of risk (destructive