diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index 260cfea..eb8ea95 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -1035,6 +1035,29 @@ fn render_schema_plan_step(step: &SchemaMigrationStep) -> String { type_name, render_annotations(annotations) ), + SchemaMigrationStep::DropType { + type_kind, + name, + mode, + } => format!( + "drop {} type '{}' ({} mode)", + schema_type_kind_label(*type_kind), + name, + drop_mode_label(*mode), + ), + SchemaMigrationStep::DropProperty { + type_kind, + type_name, + property_name, + mode, + } => format!( + "drop property '{}.{}' of {} '{}' ({} mode)", + type_name, + property_name, + schema_type_kind_label(*type_kind), + type_name, + drop_mode_label(*mode), + ), SchemaMigrationStep::UnsupportedChange { entity, reason, .. } => { @@ -1073,6 +1096,13 @@ fn schema_lint_tier_label(tier: omnigraph_compiler::SafetyTier) -> &'static str } } +fn drop_mode_label(mode: omnigraph_compiler::DropMode) -> &'static str { + match mode { + omnigraph_compiler::DropMode::Soft => "soft", + omnigraph_compiler::DropMode::Hard => "hard", + } +} + fn render_prop_type(prop_type: &omnigraph_compiler::PropType) -> String { let base = if let Some(values) = &prop_type.enum_values { format!("Enum({})", values.join("|")) diff --git a/crates/omnigraph-compiler/src/catalog/schema_plan.rs b/crates/omnigraph-compiler/src/catalog/schema_plan.rs index a06a036..f9c708d 100644 --- a/crates/omnigraph-compiler/src/catalog/schema_plan.rs +++ b/crates/omnigraph-compiler/src/catalog/schema_plan.rs @@ -16,6 +16,29 @@ pub enum SchemaTypeKind { Edge, } +/// How a drop step interacts with data. +/// +/// - **`Soft`** — catalog tombstone only. The type / property is hidden +/// from queries but the underlying Lance column / dataset is retained +/// on disk. Reversible via `omnigraph schema unhide` (forthcoming). +/// Tier: `safe`. +/// - **`Hard`** — actual data removal. The Lance column is rewritten +/// without the property, or the Lance dataset is dropped. Irreversible +/// short of branch / snapshot restore. Tier: `destructive`; requires +/// `--allow-data-loss` to apply. +/// +/// The planner emits `Soft` by default; `--allow-data-loss` on the apply +/// CLI promotes drops to `Hard`. This is the dimension orthogonal to +/// `SafetyTier` from the schema-lint chassis (`crate::lint`): tier +/// describes the rule's class; mode describes the operator's intent for +/// data treatment. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DropMode { + Soft, + Hard, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SchemaMigrationPlan { pub supported: bool, @@ -62,6 +85,28 @@ pub enum SchemaMigrationStep { property_name: String, annotations: Vec, }, + /// Remove a node or edge type. Soft mode tombstones in the catalog + /// and retains data on disk; Hard mode drops the Lance dataset and + /// requires `--allow-data-loss`. + /// + /// Dormant in this commit — emitted by the planner in a later + /// commit (see `docs/schema-lint-v1-plan.md`). + DropType { + type_kind: SchemaTypeKind, + name: String, + mode: DropMode, + }, + /// Remove a property from an existing type. Soft mode tombstones + /// the property in the catalog and retains the Lance column; Hard + /// mode rewrites the column out and requires `--allow-data-loss`. + /// + /// Dormant in this commit. + DropProperty { + type_kind: SchemaTypeKind, + type_name: String, + property_name: String, + mode: DropMode, + }, UnsupportedChange { entity: String, reason: String, @@ -953,4 +998,56 @@ node Person @description("new") { }], })); } + + #[test] + fn drop_steps_round_trip_through_serde() { + // The DropType / DropProperty variants are dormant in this + // commit — the planner doesn't emit them yet — but their + // serde shape needs to be stable from day one. A future + // SchemaIR JSON containing one of these must deserialize + // back to the same value. This test pins the wire format + // so a v0 schema-ir consumer never sees a surprise variant + // shape after v1 ships. + let steps = vec![ + SchemaMigrationStep::DropType { + type_kind: SchemaTypeKind::Node, + name: "Person".to_string(), + mode: DropMode::Soft, + }, + SchemaMigrationStep::DropType { + type_kind: SchemaTypeKind::Edge, + name: "Knows".to_string(), + mode: DropMode::Hard, + }, + SchemaMigrationStep::DropProperty { + type_kind: SchemaTypeKind::Node, + type_name: "Person".to_string(), + property_name: "age".to_string(), + mode: DropMode::Soft, + }, + SchemaMigrationStep::DropProperty { + type_kind: SchemaTypeKind::Interface, + type_name: "Named".to_string(), + property_name: "alias".to_string(), + mode: DropMode::Hard, + }, + ]; + + for step in steps { + let json = serde_json::to_string(&step).expect("serialize"); + let round_trip: SchemaMigrationStep = + serde_json::from_str(&json).expect("deserialize"); + assert_eq!(step, round_trip, "round-trip mismatch on {json}"); + } + } + + #[test] + fn drop_mode_serde_uses_snake_case() { + // External tools may write SchemaIR JSON by hand. Pin the + // wire form so we don't silently break them later. + assert_eq!(serde_json::to_string(&DropMode::Soft).unwrap(), "\"soft\""); + assert_eq!(serde_json::to_string(&DropMode::Hard).unwrap(), "\"hard\""); + let soft: DropMode = serde_json::from_str("\"soft\"").unwrap(); + assert_eq!(soft, DropMode::Soft); + } } diff --git a/crates/omnigraph-compiler/src/lib.rs b/crates/omnigraph-compiler/src/lib.rs index 102b479..7ebc09a 100644 --- a/crates/omnigraph-compiler/src/lib.rs +++ b/crates/omnigraph-compiler/src/lib.rs @@ -16,7 +16,7 @@ pub use catalog::schema_ir::{ schema_ir_pretty_json, }; pub use catalog::schema_plan::{ - SchemaMigrationPlan, SchemaMigrationStep, SchemaTypeKind, plan_schema_migration, + DropMode, SchemaMigrationPlan, SchemaMigrationStep, SchemaTypeKind, plan_schema_migration, }; pub use lint::{DiagnosticCode, Family, SafetyTier, Severity}; pub use ir::ParamMap; diff --git a/crates/omnigraph/src/db/omnigraph/schema_apply.rs b/crates/omnigraph/src/db/omnigraph/schema_apply.rs index a01cbc8..8d2eb28 100644 --- a/crates/omnigraph/src/db/omnigraph/schema_apply.rs +++ b/crates/omnigraph/src/db/omnigraph/schema_apply.rs @@ -138,6 +138,17 @@ pub(super) async fn apply_schema_with_lock( } SchemaMigrationStep::UpdateTypeMetadata { .. } | SchemaMigrationStep::UpdatePropertyMetadata { .. } => {} + SchemaMigrationStep::DropType { .. } | SchemaMigrationStep::DropProperty { .. } => { + // Dormant — variants exist on the IR but the planner + // doesn't emit them yet. Implementation lands in a + // subsequent commit (see docs/schema-lint-v1-plan.md). + // If a SchemaIR JSON containing one of these arrives + // (e.g. round-tripped from a future tool), fail + // explicitly rather than silently misclassifying. + return Err(OmniError::manifest_internal( + "drop-step variant is not yet implemented in apply_schema", + )); + } step @ SchemaMigrationStep::UnsupportedChange { .. } => { return Err(OmniError::manifest( step.unsupported_error_message()