Repository navigation
Description
Actual result
schema plan refuses to drop a property that carries @index:
supported: no
- drop property 'Doc.score' of node 'Doc'
- unsupported change on node:Doc: removing constraints from 'Doc' is not supported in schema migration v1
schema apply fails with the same message and exits 1. Nothing is written; the graph keeps the old schema.
Dropping an unindexed property from the same type plans as supported. The only difference is the property's @index. Removing the property also removes its single-property index constraint, and plan_constraints treats any removed constraint as unsupported (crates/omnigraph-compiler/src/catalog/schema_plan.rs, the removing constraints from branch, line 845 on main at 864495a).
Expected result
The drop plans as supported, and the property's own single-property constraints go with it. Three things point that way:
docs/user/schema/index.mdlists "dropping node, edge, or property declarations" among supported changes, with no exception for indexed properties.- An index is derived performance state (invariants, item 7). Losing one can change cost, but not results.
- At the storage layer the drop is already safe. The Schema evolution row of
docs/dev/lance.mdnotes that Lance'sretain_relevant_indicesremoves an index on a dropped field during the drop'sOperation::Project.
In practice, a schema that replaces an indexed field cannot remove the old one. One example is an F64 amount superseded by an exact decimal String. The old field has to stay forever as a deprecated copy, and every writer must keep both in step.
GQT reproduction or limitation
GQT cannot express this. It has no step that plans or applies a schema change; a case's schema is fixed at setup. These CLI steps reproduce it on a local graph:
# v1.pg
node Doc {
slug: String @key
score: I64 @index
note: String?
}
# drop-note.pg: removes the unindexed property
node Doc {
slug: String @key
score: I64 @index
}
# drop-score.pg: removes the indexed property
node Doc {
slug: String @key
note: String?
}
# seed.jsonl
{"type":"Doc","data":{"slug":"a","score":1,"note":"x"}}
omnigraph init --schema v1.pg g.omni
omnigraph load --data seed.jsonl --mode append g.omni
omnigraph schema plan --schema drop-note.pg g.omni
omnigraph schema plan --schema drop-score.pg g.omni
omnigraph schema apply --schema drop-score.pg g.omni
The result is the same with an empty graph.
Version
omnigraph 0.13.0 (release binary). The check is unchanged on main at 864495a.
Execution path
CLI (direct, local graph)
Environment
macOS 26.5.2, arm64; local filesystem storage.
Reproduction command and output
$ omnigraph schema plan --schema drop-note.pg g.omni
schema plan for g.omni
supported: yes
- drop property 'Doc.note' of node 'Doc'
$ omnigraph schema plan --schema drop-score.pg g.omni
schema plan for g.omni
supported: no
- drop property 'Doc.score' of node 'Doc'
- unsupported change on node:Doc: removing constraints from 'Doc' is not supported in schema migration v1
$ omnigraph schema apply --schema drop-score.pg g.omni
omnigraph schema apply → g.omni (direct, local)
removing constraints from 'Doc' is not supported in schema migration v1
command_outcome: {"execution":"unknown","effects":"unknown","action":"reconcile"}
Additional logs
Possible fix: in plan_constraints, leave out a removed constraint when every property it names is dropped in the same plan. A drop then takes its own @index and single-property @unique with it. Keep refusing removals that leave a member behind, such as @unique(a, b) when only a is dropped, and every @key change.
Open question for maintainers: should removing a bare @index from a property that stays also be supported, given that indexes are derived? That case would need the physical index dropped or left unused, so it may deserve its own decision. If the current refusal is intended, the supported-changes list in docs/user/schema/index.md should state the exception. #984 covers a related gap between the drop documentation and the planner.
Separately, the refused apply reports "execution":"unknown","effects":"unknown", although the planner refused it before any write.
Activity
ragnorc commentedon Oct 17, 2026
Triage: reproduced on latest
main; a planner-only fix is recommendedSummary
schema planrefuses to drop a property when any constraint names that property. The cause is in the planner, not in storage. A property-level@indexis also stored as a type-level@index(score)constraint. When the property is dropped, the planner sees that constraint disappear and refuses the change. It does not check that the property the constraint names is dropped in the same plan.The same refusal applies to
@unique,@rangeand@checkon a dropped property, and to edge properties. The storage path already handles the drop correctly: Lance removes the index on a dropped column. A local prototype of a planner-only fix applies cleanly end to end.Reproduction
Verdict: reproduced. The output is identical to the report.
mainat4d7536d68b1db6da34bf9cff22a90d9fdd77bed4. I checked it again at the end of triage. It is newer than the864495anamed in the report.cargo build -p omnigraph-cli --locked, debug profile, toolchain 1.97.1 fromrust-toolchain.toml. The binary reportsomnigraph 0.14.0.v1.pg,drop-note.pg,drop-score.pgandseed.jsonl, with no changes.The result is the same on an empty graph (
init, thenplanandapplywith noload).Differences from the report: I used a 0.14.0 source build, not the 0.13.0 release binary, and Linux x86_64, not macOS arm64. Neither difference changes the result. The planner code does not depend on the platform.
Scope. I planned these variants with the same build:
main@indexon a scalar, a free-text String or a Vector@unique@range(p, ..)or@check(p, ..)@unique(a, b), withaandbboth dropped@indexon an edge property@unique(a, b), with onlyadropped@keyRemoving a bare
@indexfrom a property that stays is refused twice: once by the property-level check and once by the type-level check.Minimal failing test
GQT cannot express this contract. Its step set (
Stepincrates/omnigraph-gqt-core/src/lib.rs) has query, mutate, load, branch and commit control, settings, show, branch read, restart and concurrent steps. It has no schema plan or apply step, and the schema of a case is fixed at setup.The cheapest owner is the in-source planner test module in
crates/omnigraph-compiler/src/catalog/schema_plan.rs, besideplan_emits_drop_for_removed_nullable_property. This test needs no graph and no I/O.Clean baseline first:
cargo test -p omnigraph-compiler --locked --lib catalog::schema_plan::gave 21 passed. Then, with only this test added:One test ran. It failed on the
supportedassertion. TheDropPropertyassertion passed.Root cause
The accepted
SchemaIRkeeps a property-level constraint in two places: inPropertyIR::property_constraints, and as a type-levelConstraintIRthat refers to the property by stable id. The planner compares these two places independently.plan_propertiesputs the dropped property in its leftover loop and emitsDropProperty(schema_plan.rs#L717-L730). It does not callplan_property_constraintsfor that property, so the property-level path stays quiet.plan_constraintscompares the type-level constraints by key (schema_plan.rs#L817-L850).@index(score)is in the accepted map but not in the desired map. Any removal emitsUnsupportedChange. The function has no input that tells it which properties the plan drops.supported: falsebefore any effect (schema_apply.rs#L168-L175).The apply path does not need this refusal:
DropPropertyalready sends the table to a metadata-onlyOperation::Project(schema_apply.rs#L426-L444). The doc comment onstage_schema_evolutionalready says "an index on a dropped column leaves with it" (table_store.rs#L4640-L4651).Operation::Projectcallsretain_relevant_indices. That function removes each index whose field is no longer in the schema (manifest_build.rs#L912-L931, index_maintenance.rs#L221-L238). The surface guardmetadata_only_schema_evolution_keeps_blob_files_ids_and_index_coveragepins this behavior (lance_surface_guards.rs#L811-L821). It passes on this build.declared_full_text_columns), so it gives a dropped column no analyzer segment.@unique,@rangeand@checkon that property have no value left to check.So the refusal is a gap in how the planner classifies the change. It is not a safety rule.
Recommended fix (correct by design)
Treat a constraint as dependent on the properties it names. When the plan ends the lifetime of every property a constraint names, the constraint ends with them.
plan_nodesandplan_edges, compute the set of droppedStablePropertyIds once per type. Use the same stable ids thatplan_propertiesuses. Do not compare names (invariant 6).plan_constraints. Do not refuse a removed constraint when every field it names is a property in the dropped set.@keychange, because entity ids come from the key;@src,@dst). An example is@unique(a, b)with onlyadropped;SchemaMigrationStep::DropPropertyand in the supported-changes text ofdocs/user/schema/index.md. Add achangelog.dfix note.I made a local prototype of this rule (about 40 lines in
schema_plan.rs). I did not push it. Results on the same commit:cargo test -p omnigraph-compiler --locked --libgives 484 passed.crates/omnigraph/tests/schema_apply.rsloads rows and runsensure_indices. Then it dropsscore: I64 @indexandbody: String? @indexand reads the Lance indexes of the pinned table:ensure_indicesandoptimizeafter the apply do not change that set, and a query returns every row. The complete--test schema_applybinary gives 33 passed.supported: yesandapplied: yes. The same is true for@unique,@range,@check, a free-text String, a Vector, a fully dropped@unique(a, b)and an edge property. A partial composite, a@keydrop and a bare@indexremoval are still refused.Tests to land with the fix:
@keydrop and a bare removal. Putissue_1014in the test name for the Fix Regression Gate;schema_apply.rsthat asserts the pinned table has no index on the dropped column, and thatensure_indicesdoes not build one again.I expect no change to the DST storage-cost golden. The apply reads and writes the same storage as an unindexed drop.
Alternatives
DropConstraintstep, or list the removed constraints onDropProperty@index@unique,@range,@checkand composites stay broken, and each new constraint kind needs one more special case.The quality that option A deprioritizes is plan transparency. The plan line "drop property 'Doc.score'" does not name the constraints that leave with it. The property is gone, so no constraint can still apply to it. Option B can add that detail later without a change in behavior.
The open maintainer question: removing a bare
@indexfrom a property that staysRecommendation: keep this change refused in the fix for this issue, document the exception, and decide it in its own issue or RFC. The reasons are different for each index kind.
ensure_indicesreadsnode_type.indices), so nothing refreshes it. Results stay correct, because Lance scans the fragments an index does not cover. But the index drifts, and I did not check whether the v2 planner still uses it on reads.drop_index, which is aCreateIndexcommit withremoved_indices. It must be staged detached and pinned in the same publication, with crash-window rows indetached_commit_matrix.rs.docs/rfcs/0046-index-status.md) already expectsdeclared: falseindexes whose intent "was dropped by a schema change". So this direction fits the roadmap.full_text_analyzer_survives_schema_evolution_issue_904shows thatsearch(body, "deep")then misses "Deep Learning" (schema_apply.rs#L3146-L3155). So removing@indexfrom a free-text String changes the results ofsearch,match_text,fuzzyandbm25. If apply keeps the old segment instead, the physical index becomes the only record of the query semantics. That is a shadow source of truth.@analyzed. After that change, removing a full-text@indexchanges only cost. See also Unindexed full-text search is case-sensitive #747, fuzzy() bypasses the index analyzer at a nonzero edit budget #748 and Index state changes which rows a text query matches #749.Until that decision, the supported-changes list in
docs/user/schema/index.md(line 155) should state that removing a constraint from a property that stays is refused. Line 172 says "adding or removing most constraints are rejected". That is close, but it does not name this case.Separate point:
execution: unknownon a refused applyThe engine marks this refusal as before-effect:
capture(...).map_err(OmniError::before_effect)(prepared.rs#L283-L285). The CLI does not read that evidence.Failure::classifydecidesnot_startedonly from HTTP dispatch evidence, so a direct write that opened a writable handle reportsunknown(command_outcome.rs#L207-L209).This behavior is documented as conservative: "writable open can complete earlier work" (
docs/user/operations/troubleshooting.md, line 99). Qualifying direct-mode outcomes is the scope of #466. #976 fixed only the served 401, 403 and contract-400 cases. I suggest tracking this point in #466, not here. It needs proof that the writable open itself did no work, and that proof is wider than this bug.Lance upstream assessment
The pinned version is Lance 11.0.0 (
docs/dev/lance.md). No upstream change fixes or blocks this issue. The defect is in the OmniGraph planner, and the Lance behavior that the fix needs already exists in 11.0.0.retain_relevant_indiceskeeps the same "remove indices for fields no longer in schema" rule in v13.0.0 (the latest release) and in v14.0.0-beta.19. In the v14 beta,Operation::Projectstill calls it, and also calls a newretain_relevant_overlaysfor dropped fields.retain_relevant_indiceskeeps when every segment of an index is empty. It does not touch the dropped-field rule.alter_columnswith a cast fail on an indexed column, instead of silently dropping the index. Upstream treats a silent index loss on a cast as a defect, but a column drop still removes its index by design. OmniGraph does not cast here.The Lance data-evolution guide confirms that
drop_columnsis metadata-only and reversible while old versions are retained.Related work
OG-DS-102/103/104that nothing emits. It is in the same docs section, but it is a different defect. It is not a duplicate.@indexquestion above.Supersession
No open issue supersedes this one. #984 is related, but it describes a different defect. I am not closing anything.
Limitations
docs/user/branching/index.md, "Historical reads"). An unindexed drop onmainbehaves the same way.fix(schema): drop a property together with the constraints that name …
test(cleanup): qualify sweep retries across storage and DST scheduling (