Skip to content

Removing an @relationship on a populated table breaks select() over that field: transient phantom edges, then a permanent 500 (Resource.ts:867) #1788

Open

Description

@kriszyp

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

  1. Deploy a component with two related tables and a @relationship, e.g. Author.tagBooks: [Book] @relationship(to: tag) (with Book.tag: ID @indexed), and populate it (e.g. 20 authors × 25 books).
  2. Confirm GET /Author/<id>?select(tagBooks{id}) resolves the related books.
  3. Evolve the component schema to remove the @relationship (drop Author.tagBooks and the @relationship directive), and apply it via set_component_file + restart_service http_workers (i.e. an in-place schema change without a full restart).
  4. 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 select over 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 @indexed annotation (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

  1. Invalidate / rebuild the per-worker relationship-resolver metadata when an @relationship is removed (Leg A — the workers should not keep resolving a dropped relationship).
  2. Guard transformForSelect against an undefined target resource so a select() 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 on 7d10daf7d; the resources/Resource.ts / resources/tables/Table.ts relationship-resolver path is unchanged through current 56f8891b9 (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)

Activity

  1. added
    bugSomething isn't working
    area:clusteringMulti-thread/multi-process coordination, ITC, worker management
    area:rest-apiREST API, resources, routing
    on Jul 14, 2026
  2. added this to the v5.2 milestone on Aug 7, 2026
  3. added theissue type on Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:clusteringMulti-thread/multi-process coordination, ITC, worker managementarea:rest-apiREST API, resources, routingbugSomething isn't working

    Type

    Fields

    Priority

    P1

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions