Repository navigation
Removing an @relationship on a populated table breaks select() over that field: transient phantom edges, then a permanent 500 (Resource.ts:867) #1788
Description
Bug Summary
Removing an @relationship from a populated table's schema breaks select() over that field in two distinct ways that share one trigger: first a transient data leak (removed-relationship edges keep resolving), then a permanent 500 crash that survives a full process restart. There is no clean "the field no longer exists" behavior at any point — a select() referencing the removed relationship should return an absent/4xx result, never stale edges and never an unhandled 500.
Found via exploratory QA (adjudicated from a demoted schema-evolution test). No data loss — the full-scan path is unaffected.
Steps to Reproduce
- Deploy a component with two related tables and a
@relationship, e.g.Author.tagBooks: [Book] @relationship(to: tag)(withBook.tag: ID @indexed), and populate it (e.g. 20 authors × 25 books). - Confirm
GET /Author/<id>?select(tagBooks{id})resolves the related books. - Evolve the component schema to remove the
@relationship(dropAuthor.tagBooksand the@relationshipdirective), and apply it viaset_component_file+restart_service http_workers(i.e. an in-place schema change without a full restart). - Immediately issue
GET /Author/<id>?select(tagBooks{id})again, repeatedly, on a multi-worker instance (threads.count: 4).
Expected Behavior
After the relationship is removed, a select(tagBooks{...}) referencing it should return a clean result — the field is absent (empty/omitted) or a 4xx — deterministically, on all workers.
Actual Behavior
Two separable bugs, one trigger:
Leg A — transient phantom edges (stale-data exposure). Immediately after the schema change is applied via restart_service http_workers, stale in-memory relationship-resolver metadata still resolves all the removed relationship's edges at HTTP 200. In single-worker mode the window is narrow (~1s). In 4-worker mode it persisted across every request in the test with no decay — effectively indefinite under an http-worker-only restart, since the workers hold stale resolver state. Clients receive data for a relationship that no longer exists in the schema.
Leg B — permanent crash (deterministic). The resolver state then flips to a hard failure:
TypeError: Cannot read properties of undefined (reading 'propertyResolvers')
at transformForSelect (resources/Resource.ts:867)
transformForSelect(select, resource) does const propertyResolvers = resource.propertyResolvers, but resource (the removed relationship's target resource) is now undefined. Every subsequent request to that select returns 500, including after a full process restart (killHarper + startHarper on the same dataRootDir). Waiting never converges to a correct state; a full restart only swaps Leg A → Leg B.
Blast radius / notes
- Affects REST reverse-expansion and GraphQL nested
selectover the removed field.search_by_conditions(full scan) never regressed — no data loss. - Reproduced ×3, deterministic (not flaky). No storage-engine sensitivity — identical on RocksDB and LMDB. Worker count only affects how long the Leg A leak window lasts, not the eventual broken state.
- The adjacent case of removing an
@indexedannotation (without a relationship) is not a defect — it correctly falls back to a full scan (the underlying attribute data was never removed).
Suggested fix direction
- Invalidate / rebuild the per-worker relationship-resolver metadata when an
@relationshipis removed (Leg A — the workers should not keep resolving a dropped relationship). - Guard
transformForSelectagainst anundefinedtarget resource so aselect()over a removed relationship yields a clean absent/4xx result instead of an unhandled 500 (Leg B).
Likely related to the relationship/index family (#1485).
Environment
- harper
main— confirmed on7d10daf7d; theresources/Resource.ts/resources/tables/Table.tsrelationship-resolver path is unchanged through current56f8891b9(v5.2.0-alpha.4), so it reproduces on current main. - A local integration repro exists (
integrationTests/qa-scratch/qa-phantom-edge-adjudicate.test.ts) exercising both legs across rocksdb + lmdb and single/multi-worker; it can be promoted as a regression anchor once fixed.
Filed from exploratory QA · characterized with Claude (Fable 5)
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity