Skip to content

fix(browse): handle rows with an undefined primary key (#1199)#1529

Open
dawsontoth wants to merge 3 commits into
stagefrom
claude/github-issue-1199-76982c
Open

fix(browse): handle rows with an undefined primary key (#1199)#1529
dawsontoth wants to merge 3 commits into
stagefrom
claude/github-issue-1199-76982c

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

Closes #1199.

The state (yes, it's reproducible)

A record can end up with no value for its declared primary key. The path: change a table's @primaryKey on a populated table (or formalize a previously-dynamic table with a natural key, e.g. id: IDemail: String @primaryKey, then redeploy). Harper accepts the change but does not re-key the existing rows — they keep their original stored key (the old id), and describe now reports the new primary-key attribute the old rows have no value for.

So rowData.original[primaryKey] is undefined. Clicking the row did:

setSelectedIds([undefined]) → search_by_id { ids: [null] }   // JSON.stringify([undefined]) === "[null]"
→ 500 'hash_values' must be strings or numbers

…and the edit modal just spun on <Loading /> forever (the query has retry: false and no error toast).

What this PR does

  • onRowClick detects the missing primary key, skips the doomed search_by_id, and opens the modal on the row the list query already returned (answering the issue's "does it even need to re-fetch?" — no).
  • EditTableRowModal shows a warning banner ("This row has no primary key value… usually means the table's primary key was changed after the row was created"), renders the row read-only, and hides the Save/Delete actions.
  • getSearchByIdOptions filters null/undefined ids so a null hash value is never sent (belt-and-suspenders for any caller).
  • The Delete guard now uses != null instead of a truthy check, so a record with a falsy-but-valid key (0, "") can still be deleted.

Unit test added for the id filtering; full suite (1688 tests) green.

The scarier thing I found

While tracing the delete path I found that a Harper delete with hash_values: [null] deletes the entire table while reporting "1 of 1 record successfully deleted" (verified on 4.7.23 and 5.0.15). Studio's delete button happens to guard against it, but that's core data-loss — fixed here: HarperFast/harper#1837.

Full investigation and reproduction steps: #1199.

🤖 Generated with Claude Code

When a table's @PrimaryKey is changed on a populated table, Harper does not
re-key the existing rows, so `describe` reports a primary key attribute the old
rows have no value for. Clicking such a row sent `ids: [null]` to search_by_id
(JSON.stringify turns `[undefined]` into `[null]`), which Harper rejects with a
500 'hash_values' must be strings or numbers — leaving the edit modal spinning
on a loading spinner forever.

- onRowClick detects the missing primary key and shows the row read-only using
  the data the list query already returned, instead of firing a doomed lookup.
- EditTableRowModal shows a warning banner explaining the row can't be looked
  up, edited, or deleted individually, and hides the Save/Delete actions.
- getSearchByIdOptions filters null/undefined ids so a null hash value is never
  sent (belt-and-suspenders for any caller).
- Delete guard uses `!= null` so a record with a falsy-but-valid key (0, "")
  can still be deleted.

The root-cause data-loss bug (a `delete` with a null hash value wiped the whole
table in Harper) is fixed in HarperFast/harper#1837.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 51.21% 5402 / 10547
🔵 Statements 51.8% 5786 / 11169
🔵 Functions 43.38% 1325 / 3054
🔵 Branches 44.31% 3623 / 8176
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/databases/components/DatabaseTableView.tsx 0% 0% 0% 0% 71-571
src/features/instance/databases/modals/EditTableRowModal.tsx 52% 65.78% 37.5% 52% 59, 66-67, 79-169
src/integrations/api/instance/database/getSearchById.ts 100% 100% 100% 100%
Generated in workflow #1535 for commit 750d405 by the Vitest Coverage Report Action

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue where rows with missing primary key values could not be viewed or managed in the database table view. The changes introduce logic to detect missing primary keys, stash the row data locally, and render the edit modal in a read-only state with an informative warning. Additionally, the getSearchByIdOptions utility and its corresponding tests were updated to filter out null or undefined IDs, preventing invalid API requests that previously caused server errors. I have no feedback to provide as there were no review comments.

Covers the new behavior surfaced in the coverage report: with a missing primary
key the modal warns, goes read-only, and hides Save/Delete; a normal row still
edits and deletes. Monaco is stubbed since it can't load in jsdom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/features/instance/databases/modals/EditTableRowModal.tsx Outdated
…rimary key (#1199)

A table whose primary key was changed after rows existed reports one primary key
via describe but is still keyed by the original attribute. Two consequences in the
edit modal, both now handled by falling back to the row the list already gave us:

- The edit fetch hardcoded `onlyIfCached: true`; on a key miss Harper answers
  `{message: "Entry is not cached"}`, which the modal rendered as the record. It now
  sends `onlyIfCached: false`, so a genuine miss returns an empty result and a real
  record loads for editing regardless of cache state.
- A row whose declared primary-key value exists but resolves to no stored record
  (e.g. `email` present but the table is really keyed by `id`) now shows a read-only
  "couldn't be loaded" banner instead of a raw error/empty editor.

The underlying Harper bug (changing @PrimaryKey on a populated table leaves the real
hash attribute unchanged, so describe reports the wrong key) is fixed in
HarperFast/harper#1837.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dawsontoth added a commit to HarperFast/harper that referenced this pull request Jul 16, 2026
Changing which attribute is the `@primaryKey` (via GraphQL schema, defineTable, or
create_table) on a table that already contains records left the table corrupt: the
storage/hash key (`Table.primaryKey`) was never re-pointed, so `describe_table` began
reporting the newly-declared attribute while every record — old and newly inserted,
even via the raw Operations API — stayed keyed by the original attribute. search_by_id,
update, and delete by the declared key then all miss.

`table()` is the single factory every front-end funnels through, so guard there: on the
existing-table branch, if a schema-authored declaration names a different primary key and
the table has any records, throw a ClientError instead of silently diverging describe from
storage. Empty tables and settings-only reasserts are unaffected.

This is the root cause behind HarperFast/studio#1199 (the studio side handles the
resulting orphaned rows defensively in HarperFast/studio#1529).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth requested a review from cb1kenobi July 16, 2026 20:58
kriszyp pushed a commit to HarperFast/harper that referenced this pull request Jul 17, 2026
Changing which attribute is the `@primaryKey` (via GraphQL schema, defineTable, or
create_table) on a table that already contains records left the table corrupt: the
storage/hash key (`Table.primaryKey`) was never re-pointed, so `describe_table` began
reporting the newly-declared attribute while every record — old and newly inserted,
even via the raw Operations API — stayed keyed by the original attribute. search_by_id,
update, and delete by the declared key then all miss.

`table()` is the single factory every front-end funnels through, so guard there: on the
existing-table branch, if a schema-authored declaration names a different primary key and
the table has any records, throw a ClientError instead of silently diverging describe from
storage. Empty tables and settings-only reasserts are unaffected.

This is the root cause behind HarperFast/studio#1199 (the studio side handles the
resulting orphaned rows defensively in HarperFast/studio#1529).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

500 'hash_values' must be strings or numbers

2 participants