docs(schema): document enum migration (widen/narrow/String↔enum)

Add an "Enum evolution" section to schema-language.md covering the four
supported shapes and their tiers, plus the unsupported cases (non-String
scalar change, interface enums, in-place variant rename). Record the new
ChangeEnumConstraint migration step. Add OG-MF-105 / OG-MF-107 to the
schema-lint code table and clarify OG-MF-106 as a genuine scalar change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-05-25 23:56:51 +01:00
parent cf18d9b600
commit 5e7b1aad78
2 changed files with 17 additions and 1 deletions

View file

@ -75,10 +75,24 @@ Edge bodies only allow `@unique` and `@index`.
- `AddConstraint { type_kind, type_name, constraint }`
- `UpdateTypeMetadata { … annotations }`
- `UpdatePropertyMetadata { … annotations }`
- `ChangeEnumConstraint { type_kind, type_name, property_name, to_property_type, code }` — evolve an enum-typed property's value-set (see below)
- `UnsupportedChange { entity, reason }` (forces `supported=false`)
`apply_schema()` returns `SchemaApplyResult { supported, applied, manifest_version, steps }` and is gated by an internal `__schema_apply_lock__` system branch so concurrent schema applies serialize.
## Enum evolution
Enums are stored physically as `Utf8`; the allowed value-set lives only in the schema, not in the column. So enum migrations change catalog metadata, never the data — no table rewrite, and the manifest version does not advance. Four shapes are supported on **node and edge** properties (interface enum changes are not supported in v1):
| Change | Example | Tier | Behavior |
|---|---|---|---|
| **Widen** (add variants) | `enum(open, closed)``enum(open, closed, archived)` | Safe | Metadata-only; applies unconditionally. No existing row can be invalid. |
| **`enum``String`** (loosen) | `enum(open, closed)``String` | Safe | Metadata-only; every enum value is a valid `String`. |
| **Narrow** (remove variants) | `enum(open, closed, archived)``enum(open, closed)` | Validated (`OG-MF-105`) | Apply scans existing rows; if any holds a removed value it **aborts** before publish, naming the offending value. No data is dropped — fix or migrate the rows, then re-apply. |
| **`String``enum`** (constrain) | `String``enum(open, closed)` | Validated (`OG-MF-107`) | Apply scans existing rows; aborts on the first out-of-set value. |
Reordering variants is a no-op (the value-set is sorted + deduped, so `enum(b, a)` and `enum(a, b)` are identical). Changing an enum to a non-`String` scalar (e.g. `enum(...)``I32`), or changing nullability/list-ness alongside the value-set, is a genuine type change and stays `UnsupportedChange` (`OG-MF-106`). Renaming a variant in place (a value remap, e.g. `closed``done`) is not yet supported — model it as add-then-narrow with a data migration in between.
## Destructive drops — `--allow-data-loss`
`DropProperty` and `DropType` steps default to `Soft` mode: the catalog tombstones the entry but the prior column / dataset remains time-travel-reachable via `snapshot_at_version(prev)` until `omnigraph cleanup` runs. Soft drops are reversible.

View file

@ -35,7 +35,9 @@ The chassis defines ten families. Today only DS and MF have emitted codes. The r
| `OG-DS-105` | Destructive | destructive | error | drop populated vector column (reserved) |
| `OG-MF-103` | Maybe-fail | validated | error | add required property without `@default` to populated type |
| `OG-MF-104` | Maybe-fail | validated | error | tighten nullable to non-nullable (reserved) |
| `OG-MF-106` | Maybe-fail | destructive | error | narrowing scalar type |
| `OG-MF-105` | Maybe-fail | validated | error | narrow enum value set (remove allowed variants) |
| `OG-MF-106` | Maybe-fail | destructive | error | narrowing scalar type (genuine scalar change only) |
| `OG-MF-107` | Maybe-fail | validated | error | constrain `String` to `enum` |
The full code catalog source of truth lives in `crates/omnigraph-compiler/src/lint/codes.rs`. CI-level invariants (uniqueness, format, family coverage) are unit-tested in the same module.