Skip to content

feat(databases): render, protect, and filter relationship attributes in browse#1438

Open
dawsontoth wants to merge 5 commits into
stagefrom
claude/database-relationship-issues-9312a2
Open

feat(databases): render, protect, and filter relationship attributes in browse#1438
dawsontoth wants to merge 5 commits into
stagefrom
claude/database-relationship-issues-9312a2

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

Closes #1278, closes #1202, closes #1317, closes #1286.

What

All four open relationship issues on the instance database browse page:

  • Show relationships in browse (Show relationships in browse somehow #1278) — relationship columns now resolve to the related records' primary keys and render as link chips (capped at 3, +N more) instead of a blank box. Clicking a chip deep-links to the related table with ?filters={pk: value}; the browse page now applies filters carried in the URL search, so the chip genuinely lands on "the item in the other table".
  • Relationship fields not editable / creating rows (Relationship fields should not be editable #1202, Handle relationship'd fields when creating or editing data rows #1317) — the Add-record JSON skeleton, the Edit-record JSON, and random-record generation now exclude relationship/@computed attributes. The server rejects any write whose record merely contains one of those keys ("Computed property X may not be directly assigned a value", even for null), so they must never appear in editor JSON.
  • Sub-property selection in query filters (Implement sub-property selection for complex types in query filters #1286) — relationship columns get a filter box with a sub-property picker (the related table's non-synthetic attributes). .rating >= 4 translates to search_attribute: ["reviews", "rating"], which Harper executes as a join against the related table. The existing comparator syntax (>=, * suffix, & chaining) composes with it, and values are cast by the sub-property's type.

Also fixes plain-cell rendering while in there: objects/arrays stringify as JSON and booleans render as text (previously multi-element arrays rendered as [object Object],[object Object] and false rendered as nothing).

How relationships are detected

describe_table has no explicit relationship flag. A to-one relationship reports type: "<RelatedTable>"; a to-many reports type: "array", elements: "<RelatedTable>" — so an attribute is treated as a relationship when its type/elements names a sibling table in the same database (functions/relationshipAttributes.ts). Resolution works by naming the attributes in get_attributes with nested selects ({name, select: [relatedPk]}) before '*' — a leading '*' makes the server return raw records and ignore the rest of the list.

Version behavior (verified empirically)

Server describe reports relationships outcome
Harper 4.7.28 (local instance) yes (type/elements name the table) full feature set verified end-to-end: nested selects resolve, ["rel","sub"] join conditions filter, writes containing relationship keys reject
Harper 5.1.18 (stage Anvils cluster) no (relationship attrs are runtime-only, never persisted to the attribute registry) features stay dormant; browse behaves exactly as before

Browser-verified on the worktree dev server against the Anvils cluster: chips render, chip → related-table-with-filter navigation works end to end, the sub-property picker emits the verified wire payload, the Add-record skeleton omits the relationship field, and plain tables (catalog.Product, data.Albums) render unchanged. Test tables/component used for verification were dropped afterwards.

Tests

New unit coverage for relationship detection (relationshipAttributes.test.ts), join-path filter translation (getSearchByConditions.test.ts), column construction (formatBrowseDataTableHeader.test.ts), and cell key extraction (RelationshipCell.test.ts). Full suite: 194 files / 1288 tests pass; tsc, oxlint, and dprint clean.

🤖 Generated with Claude Code

…in browse

Relationship attributes (@relationship) reported by describe_table have no
explicit flag; detect them by their type/elements naming a sibling table
(Harper 4.7 shape — 5.1 omits them entirely, so this stays dormant there).

- Browse resolves relationship columns via nested get_attributes selects
  ({name, select: [relatedPk]} before '*') and renders them as link chips
  to the related record instead of a blank box (#1278)
- Chip links deep-link with ?filters={pk: value}; the browse page applies
  filters from the URL search on arrival
- Add/edit record JSON and random-record generation exclude relationship/
  computed attributes — the server rejects writes that assign them, even
  null (#1202, #1317)
- Relationship columns are filterable by sub-property: a picker inserts
  '.property', translated to search_attribute: [column, subProperty] which
  the server runs as a join against the related table (#1286)
- Plain cells stringify objects/arrays and booleans instead of rendering
  '[object Object]' or nothing

Verified against a local Harper 4.7.28 (features work end-to-end) and the
stage 5.1.18 cluster (describe omits relationships; behavior unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dawsontoth dawsontoth requested a review from a team as a code owner July 10, 2026 15:22
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 47.51% 4543 / 9562
🔵 Statements 47.83% 4843 / 10125
🔵 Functions 39.42% 1081 / 2742
🔵 Branches 40.16% 2996 / 7459
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/databases/components/ColumnFilters.tsx 6.66% 0% 0% 6.66% 29-129
src/features/instance/databases/components/DatabaseTableView.tsx 0% 0% 0% 0% 73-657
src/features/instance/databases/components/RelationshipCell.tsx 30% 29.62% 60% 30% 41-88
src/features/instance/databases/components/TableView.tsx 75% 40% 80% 73.68% 161-169, 177, 198-204
src/features/instance/databases/functions/formatBrowseDataTableHeader.ts 73.17% 61.53% 66.66% 73.68% 23-26, 36-42, 63, 101-108, 117-122
src/features/instance/databases/functions/generateRandomRecords.ts 85% 75.47% 100% 83.92% 75, 78, 91, 94, 103, 106, 109, 112, 120
src/features/instance/databases/functions/relationshipAttributes.ts 98.59% 94.31% 100% 98.52% 77
src/features/instance/databases/functions/schemaRelationships.ts 57.89% 61.53% 20% 58.92% 40-83, 98
src/features/instance/databases/modals/AddTableRowModal.tsx 0% 0% 0% 0% 29-206
src/features/instance/databases/modals/EditTableRowModal.tsx 0% 0% 0% 0% 36-120
src/features/instance/databases/modals/ImportDataModal.tsx 3.77% 0% 0% 3.88% 67-87, 130-515
src/integrations/api/instance/database/getSearchByConditions.ts 92% 62% 66.66% 91.66% 72-123
src/integrations/api/instance/database/getSearchByValue.ts 0% 0% 0% 0% 45-101
Generated in workflow #1429 for commit 13be561 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 introduces support for relationship and computed (synthetic) attributes in the database table view, adapting to differences across Harper server versions. Key updates include stripping read-only synthetic attributes from the record add/edit modals to prevent server-side write rejections, rendering relationship values as interactive link badges via a new RelationshipCell component, and enabling sub-property filtering on related tables with a dropdown picker. Additionally, the search APIs were updated to support nested selects for resolving relationships, and comprehensive unit tests were added to verify these behaviors. There are no review comments to address, and the implementation is clean and well-tested.

…ip column

The relationship chips show the same key values the foreign key stores (the
related record's primary key), so showing both 'product' and 'productId' is
redundant. The foreign key backing a relationship is inferred by naming
convention — product→productId/product_id, pets→petIds (describe_table does
not expose the @relationship from/to mapping) — and hidden by default. It
stays listed in the Columns picker, and re-showing it stores an explicit
override that wins over the default, so unconventional pairings or raw-key
sorting are one click away. Reverse (to:) relationships have no local key
and collapse nothing. Record add/edit JSON is unaffected: the foreign key
remains the writable field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Added per follow-up feedback: the foreign-key column backing a relationship is now collapsed by default — productId no longer shows next to product; the chips carry the same key values. The pairing is inferred by naming convention (productproductId/product_id, petspetIds) since describe doesn't expose the @relationship from/to mapping. The key stays listed in the Columns picker (unchecked), and re-showing it stores a session override that wins over the default — the escape hatch for unconventional key names or raw-key sorting. Reverse (to:) relationships have no local key, so nothing collapses there. Add/edit JSON is unchanged: the foreign key remains the writable field.

…e link chips

Tables created before Harper 5 carry legacy relationship attributes whose
describe output is just {type: 'array'} with no element type (e.g.
Albums.tracks): detect those by matching the attribute name against sibling
table names (tracks → Tracks/Track). They can't be resolved via nested
selects, so the cell instead links to the related table filtered by the
reverse foreign key — inferred from a back-reference to-one relationship on
the related table (RelReview.product → productId) or the <singular owner
table>Id naming convention (Tracks.albumId) — landing on exactly this row's
related records. Resolved to-many cells reuse the same link for '+N more'.

Chips now use the app's link blue (blue / blue-300 in dark) instead of
outline's text-foreground, so they read as links in both themes instead of
blending into neighboring values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Two more follow-ups pushed:

Reverse-key links for legacy relationships. Tables created under older Harper (e.g. data.Albums on the stage Anvils cluster) kept their relationship attributes in the attribute registry, but as bare {type: 'array'} — no element type — so values can't be resolved and the cell used to render blank. Those are now detected by matching the attribute name against sibling table names (tracksTracks), and the cell renders a Tracks → chip linking to the related table filtered by the reverse foreign key (Tracks?filters={albumId: <row id>}). The reverse key is inferred from a back-reference to-one relationship on the related table when present (RelReview.productproductId), else the <singular table>Id convention (Tracks.albumId). Resolved to-many cells reuse the same link for the +N more overflow. Verified live against the Anvils cluster with no mocking: Albums → Tracks → chip → exactly the album's 3 tracks, server-side filtered.

Readable chips. The chips previously used the badge outline variant (plain text-foreground), which was indistinguishable from neighboring cell values, especially in dark mode. They now use the app's link blue (--blue #4b5eec light / --blue-300 #8f8dff dark) with hover underline, matching the existing link conventions elsewhere in the app.

@DavidCockerill DavidCockerill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The feature reviews well and I'm ready to approve — one build issue to clear first, plus one open question.

🔴 Build is red on the current head — fix before merge

Commit 6eb8b07 (one past where I first read the PR) added a required resolvable: boolean to RelationshipAttributeInfo but didn't update the fixtures that construct that interface, so tsc fails with 8 errors and Verify PR is failing. Offending fixtures: RelationshipCell.test.ts:5 and the relationshipInfo fixture in getSearchByConditions.test.ts (~lines 338–406). The PR body's "tsc … clean" note predates that commit. Fix: add resolvable (and reverseForeignKey where needed) to those fixtures, or make the field optional. Ping me when it's green and I'll approve — the rest checks out.

Open question (non-blocking): collapsedForeignKeyNames can hide a real column by naming convention

In relationshipAttributes.ts, if a table has a scalar FK productId and a to-one relationship product that's actually a reverse relationship reading from a differently-named key, relationshipForeignKeyName('product', …) matches productId purely by the product+id convention and collapses it — even though the relationship doesn't read from that column. So a real, independent stored column is hidden by default, and its relationship chips could show different values than the hidden productId.

I've graded this a suggestion, not a blocker, because it's purely a default-visibility concern: the column def/header/data are all still present, it's one click to restore via the Columns picker, the choice persists (an explicit true in storedColumnVisibility is spread after the collapsed defaults, so it wins — verified against PickColumnsDropdown + the merge useMemo), and CSV export uses raw records so the FK still exports. So: is the false-positive-hide acceptable as-is? The 6eb8b07 reverse-key inference looks like it could gate the collapse to non-reverse relationships if you want to tighten it, or a small "a column was auto-hidden" hint would remove the surprise.

Everything else I checked was clean: filter value casting (Date/Boolean sub-properties, the numeric >= 4 comparator path), useMemo deps / no render loop, per-cell cost is cheap with no N+1 (relationships resolve in the single get_attributes request), the EditTableRowModal synthetic-attr strip, and defensive scalar handling in relationshipKeyValues. One cosmetic nit: the SubPropertyPicker regex value.replace(/^\s*\.\S*\s*/, '') can eat an adjacent comparator if you type .rat mid-word with no space — self-corrects on the next keystroke, not worth changing.

@dawsontoth

Copy link
Copy Markdown
Contributor Author

There are a few more fixes and enhancements coming, but I exhausted my allotment. Will update the PR soon.

… 5.1

Harper 5.1 omits @relationship attributes from describe_table entirely (an
unintentional regression from the v5.0 rewrite: describe now reads the
persisted attribute registry, which never holds runtime-only relationship
attrs — traced to Harper's RE-7/#1183, no server flag to re-surface them).
So browse also parses the component schema.graphql files (get_components +
get_component_file) to learn relationships — the only source on 5.1, and
authoritative for the exact from/to key mappings even on 4.7.

- schemaRelationships.ts: fetch + parse schema files into database->table->
  relationship declarations (reuses the applications schema parser)
- getRelationshipInfoMap merges describe-detected and schema-declared
  relationships; foreignKeyAttribute is now exact (from:) when declared
- schema-only relationships (absent from describe) render as a synthesized
  column: to-one reads the stored foreign key from the row; to-many links to
  the related table filtered by the reverse key (to:)
- filtering a relationship by its related primary key queries the local
  foreign key directly (indexed; works on 5.1's ops API which can't join)
- clearing URL filters (nav away / back to bare table) now clears the stale
  filter inputs, keeping them in step with appliedSearchConditions

Verified end-to-end on the live 5.1.18 Anvils cluster: Product shows a
'category' chip (from stored categoryId), Category shows a 'products'
reverse-link, both navigations filter correctly. Full suite 1308 pass;
tsc/oxlint/dprint clean.

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

Copy link
Copy Markdown
Contributor Author

Relationship columns now work on Harper 5.1 (via schema files)

The reason category/products weren't showing: Harper 5.1's describe_table omits @relationship attributes entirely, so Studio had no way to know they exist. I traced this in the Harper source — it's an unintentional regression from the v5.0 "big lift" rewrite, not a deliberate change:

  • describe's attribute serializer never filtered relationships and hasn't changed.
  • v5.0 made describe read table.attributes rebuilt from the persisted attribute registry (attributesDbi). Relationship attrs are runtime-only — table()'s persistence loop continues past them, so they're never written to that registry. (Same non-persistence in 4.7, but 4.7's describe read the schema-applied in-memory list that still held them.)
  • Harper commit 3017e097c (RE-7 / fix(deps): update datadog-browser-sdk monorepo to v7 (major) #1183) already treats this as a bug but only partially fixes it, and there's no include_relationships-style flag to re-surface them.

The fix (studio-side, works regardless of Harper version)

Browse now also reads the component schema.graphql files (get_components + get_component_file, parsed with the existing applications schema parser) to learn relationships. This is the only source on 5.1, and it's better than describe — it carries the exact @relationship(from:/to:) key mappings. getRelationshipInfoMap merges describe-detected + schema-declared sources, so 4.7 and 5.1 both work.

A schema-only relationship (absent from describe) renders as a synthesized column:

  • to-one (Product.category, from: categoryId) reads the stored FK from the row → chip linking to the related record. categoryId collapses behind it.
  • to-many (Category.products, to: categoryId) → Product → reverse-link filtered by the reverse key.
  • filtering a relationship by the related PK queries the local FK directly (indexed; works on 5.1's ops API which can't execute joins).

Also fixed a stale-filter-input bug this surfaced: navigating away from / back to a table now clears the filter inputs in step with the applied conditions.

Verified end-to-end on the live 5.1.18 Anvils cluster (no mocks)

  • Product shows a category column with a cat-anvils chip (read from stored categoryId); categoryId collapsed.
  • Product category chip → Category filtered to that one category.
  • Category shows a products column; Product → → Product filtered by categoryId to exactly that category's items.
  • Fresh-tab console clean; full suite 1308 pass; tsc/oxlint/dprint clean.

Worth filing the describe regression upstream with the Harper team (it also breaks nested GraphQL queries after any reset/hot-reload, per RE-7).

Two review fixes:

- Build was red: commit 6eb8b07 added a required `resolvable` to
  RelationshipAttributeInfo but left the RelationshipCell.test.ts and
  getSearchByConditions.test.ts fixtures constructing it without the field, so
  `tsc -b` failed with 8 errors. (`npx tsc --noEmit` missed them because the
  root tsconfig excludes tests; `tsc -b`, which CI runs, includes them.) Fixed
  the fixtures and added coverage for the related-primary-key filter shortcut.

- collapsedForeignKeyNames could hide a real column by naming-convention guess
  (a to-one 'product' collapsing an independent 'productId' that the
  relationship doesn't actually read from). foreignKeyAttribute is now taken
  ONLY from an exact schema `@relationship(from:)` — never guessed — so column
  collapse, unresolved-cell rendering, and the PK filter shortcut only ever act
  on the column the relationship truly reads. Describe-detected relationships
  resolve their values via nested select, so they never needed the guess.

Full suite 1310 pass; tsc -b / oxlint / dprint clean. Live 5.1 behavior
unchanged (schema is present there, so the exact from: key still drives
collapse).

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

Copy link
Copy Markdown
Contributor Author

@DavidCockerill thanks — both addressed in 13be5613, build is green.

🔴 Build (blocker) — fixed. Commit 6eb8b07 added the required resolvable to RelationshipAttributeInfo but not to two test fixtures (RelationshipCell.test.ts:5 and the relationshipInfo fixture in getSearchByConditions.test.ts), so tsc -b failed with 8 errors. My "tsc clean" note was from tsc --noEmit, which uses the root tsconfig that excludes test filestsc -b (what CI runs) includes them, so I missed it. Fixed the fixtures, and added coverage for the related-primary-key filter shortcut while there. Reproduced tsc -b → 8 errors → now exit 0.

Open question (collapse false-positive) — tightened, not left as-is. You're right that a convention guess shouldn't hide a real column. foreignKeyAttribute is now taken only from an exact schema @relationship(from:) — never guessed by naming convention. So column collapse, unresolved-cell rendering, and the PK-filter shortcut only ever act on the column the relationship actually reads from. Describe-detected relationships resolve their values via nested get_attributes select (they're resolvable), so they never needed the guess — meaning the only thing lost is auto-collapse in the narrow "4.x and component files unreadable" case, which is the safe direction (show the column rather than hide one we're guessing at). The naming-convention helper now survives solely for reverse-key inference on to-many links, where a wrong guess just filters to nothing rather than hiding data. Note this built on the schema-parsing that landed in ad876004 (after your review of 6eb8b07): on 5.1, relationships come only from the schema files, so from: is always exact there.

Cosmetic nits (the SubPropertyPicker regex eating an adjacent comparator mid-word, self-correcting) left as-is per your call. Live 5.1 Anvils behavior re-confirmed unchanged (schema present → exact from: still drives collapse); full suite 1310 pass, tsc -b/oxlint/dprint clean.

🤖 Addressed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants