From cc6f8602a9cde2577e0019dc31adc7b894291441 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Sun, 5 Jul 2026 01:16:13 +0300 Subject: [PATCH] feat(engine): apply ExtendEnum as a metadata-only schema step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The widening is planner-verified (superset, same shape), so every committed row is already valid under the wider set: the apply arm joins the AddConstraint metadata-only family — accepted-catalog update, no table work, no version bump — and the unified validator accepts the new variants on all three write surfaces immediately. Tests: enum_widening_apply_is_metadata_only_and_accepts_new_variant (no table-version bump; new variant accepted, original still accepted, out-of-set still rejected) and enum_narrowing_apply_is_refused (OG-MF-106 surfaced, graph left writable). --- .../src/db/omnigraph/schema_apply.rs | 5 + crates/omnigraph/tests/schema_apply.rs | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/crates/omnigraph/src/db/omnigraph/schema_apply.rs b/crates/omnigraph/src/db/omnigraph/schema_apply.rs index d7e6ce5e..7fde3565 100644 --- a/crates/omnigraph/src/db/omnigraph/schema_apply.rs +++ b/crates/omnigraph/src/db/omnigraph/schema_apply.rs @@ -257,7 +257,12 @@ where // critical path by ensure_indices/optimize (iss-848), so the apply // does no table work for it — a pure metadata change like the two // metadata steps below. + // ExtendEnum is a pure widening (planner-verified superset): every + // committed row is valid under the wider set, so no table data is + // touched — the accepted catalog update alone makes the unified + // validator accept the new variants on all three write surfaces. SchemaMigrationStep::AddConstraint { .. } + | SchemaMigrationStep::ExtendEnum { .. } | SchemaMigrationStep::UpdateTypeMetadata { .. } | SchemaMigrationStep::UpdatePropertyMetadata { .. } => {} SchemaMigrationStep::DropProperty { diff --git a/crates/omnigraph/tests/schema_apply.rs b/crates/omnigraph/tests/schema_apply.rs index 508451a4..83ba0ff3 100644 --- a/crates/omnigraph/tests/schema_apply.rs +++ b/crates/omnigraph/tests/schema_apply.rs @@ -841,3 +841,100 @@ async fn index_only_constraint_apply_touches_no_table_data() { "adding an @index must not bump the table version (no inline index build)" ); } + +// Enum widening (iss-enum-widening-migration): adding variants to an enum is +// a PURE metadata change — the accepted catalog updates, no table data is +// touched, and the widened set is enforced immediately on writes. Narrowing +// stays OG-MF-106-refused. +#[tokio::test] +async fn enum_widening_apply_is_metadata_only_and_accepts_new_variant() { + let dir = tempfile::tempdir().unwrap(); + let uri = dir.path().to_str().unwrap(); + let v1 = "node Ticket {\n slug: String @key\n status: enum(todo, doing, done)\n}\n"; + let mut db = Omnigraph::init(uri, v1).await.unwrap(); + load_jsonl( + &mut db, + r#"{"type":"Ticket","data":{"slug":"t1","status":"todo"}}"#, + LoadMode::Merge, + ) + .await + .expect("load a Ticket with an original variant"); + + let before = db + .snapshot_of(ReadTarget::branch("main")) + .await + .unwrap() + .entry("node:Ticket") + .unwrap() + .table_version; + + let v2 = + "node Ticket {\n slug: String @key\n status: enum(todo, doing, done, blocked)\n}\n"; + let result = db.apply_schema(v2).await.expect("enum widening must apply"); + assert!(result.supported, "widening must be a supported plan"); + assert!(result.applied, "widening must apply"); + + let after = db + .snapshot_of(ReadTarget::branch("main")) + .await + .unwrap() + .entry("node:Ticket") + .unwrap() + .table_version; + assert_eq!( + before, after, + "enum widening must not bump the table version (metadata-only)" + ); + + // The NEW variant is accepted on the write path... + load_jsonl( + &mut db, + r#"{"type":"Ticket","data":{"slug":"t2","status":"blocked"}}"#, + LoadMode::Merge, + ) + .await + .expect("new variant must be accepted after widening"); + // ...an original variant still is... + load_jsonl( + &mut db, + r#"{"type":"Ticket","data":{"slug":"t3","status":"done"}}"#, + LoadMode::Merge, + ) + .await + .expect("original variant must remain accepted"); + // ...and an out-of-set value is still rejected (the fence didn't widen to + // free text). + let err = load_jsonl( + &mut db, + r#"{"type":"Ticket","data":{"slug":"t4","status":"bogus"}}"#, + LoadMode::Merge, + ) + .await; + assert!(err.is_err(), "out-of-set enum value must still be rejected"); +} + +#[tokio::test] +async fn enum_narrowing_apply_is_refused() { + let dir = tempfile::tempdir().unwrap(); + let uri = dir.path().to_str().unwrap(); + let v1 = "node Ticket {\n slug: String @key\n status: enum(todo, doing, done)\n}\n"; + let mut db = Omnigraph::init(uri, v1).await.unwrap(); + + let narrowed = "node Ticket {\n slug: String @key\n status: enum(todo, done)\n}\n"; + let err = db.apply_schema(narrowed).await; + assert!(err.is_err(), "narrowing must refuse at apply"); + let msg = format!("{}", err.unwrap_err()); + assert!( + msg.contains("OG-MF-106"), + "refusal must carry the stable lint code, got: {msg}" + ); + + // The graph stays healthy and writable on the original schema. + load_jsonl( + &mut db, + r#"{"type":"Ticket","data":{"slug":"t1","status":"doing"}}"#, + LoadMode::Merge, + ) + .await + .expect("graph must remain writable after a refused narrowing"); +}