Schema

The other table's data, without writing the join

A foreign key holds a number. Point a column at the relationship and the grid shows the related row's name, or a count of the rows on the far side, or several values at once — and it will walk more than one hop to get there.

SchemaStack team23 Aug 2026Verified working · 27 Aug 2026Docs

Your orders table has a customer_id column containing 601. Useful to a database, useless to a person.

Every tool in this category solves that, and most solve it by copying the customer's name into the orders table and hoping the two stay in step. SchemaStack doesn't copy anything — it resolves the relationship at query time, and gives you three ways to see it.

WHAT THE DATABASE HOLDSorders.customer_id601WHAT THE GRID SHOWSAcme Ltda display field you pick — the label mode14display field left blank — becomes COUNT(*) over the far sideblue · large · gift-wrapmany-to-many, rendered as chipsAND ACROSS MORE THAN ONE HOPOrder LineOrderCustomerCustomer.countryThe walk refuses rather than guesses: a repeated entity is a cycle, a null part-way along ends the path, and depth is capped.
A foreign key holds a number. What you see depends on what you asked for: the related row's label, a count of the rows on the other side, or several values at once — all from the same column definition.

One column, three faces

Pick a display field and you get a label. 601 becomes Acme Ltd. The foreign key is untouched; the grid renders what the related row says.

Leave the display field blank and it becomes a count. The column turns into SELECT COUNT(*) over the far side of the relationship, so a customer row shows how many orders it has. You can attach a filter to that count too — how many paid orders, rather than all of them.

A many-to-many renders as chips. Several values from the other side in one cell, resolved through the junction table.

All three are the same column definition, differing only in what you asked it to show. Nothing is denormalised into your tables to make it work.

More than one hop

A relationship column can reach through several relationships: from an order line to its order, to that order's customer, to that customer's country. You choose the path in the picker; the query is assembled from it.

The interesting part is what happens when the path is wrong, because the walk refuses rather than guesses:

  • A null part-way along ends the walk (NullMidPathException) rather than producing a misleading blank.
  • Depth is capped, with MaxDepthExceededException if a path somehow grows past it. That one is defensive — real paths are three or four hops — and it exists so a malformed path fails fast instead of walking forever.

There used to be a third refusal here: an entity appearing twice on the path raised CycleDetectedException. This post named it as a virtue, and it was the wrong assertion — a category's parent.parent.name legitimately visits the same table three times, and the walk was never at risk of looping anyway, because it consumes a stored, finite path with the depth cap as the real bound. The check is gone, self-referential paths resolve, and the depth cap keeps doing the actual protecting.

A feature whose exception types name its own failure modes is usually a feature someone has actually operated. Occasionally that includes retiring one.

Both directions

Foreign keys are one-directional in the database and bidirectional in how people think. Import reads the reverse side too, so a customer knows about its orders and not only the other way round — which is what makes the count mode possible without you defining anything extra.

What it doesn't do (yet)

  • Filtering a relationship column was broken until two days ago. The value was converted against the type the cell displays — the related row's label, so text — while the filter applies to the numeric key underneath, and the database refused the mismatch with a server error. Fixed, with 90 tests over every column type, but it was live for a while and worth admitting rather than quietly repairing.
  • Filtering across a collection relationship only searches text columns on the far side. If the target has none there is nothing to search — that case used to drop the condition, leaving a filter that silently matched everything, and is now refused with a 400 naming the column instead.
  • Junction tables import as ordinary tables. A pure link table with two foreign keys arrives as a table, not an inferred many-to-many; you add the relationship column deliberately, which is also what lets you choose the display field.
  • A new or dropped foreign key isn't reported as drift — only cascade-rule changes on columns already known. Schema drift covers what is and isn't watched.
  • Counts are computed per row, per query. A count column over a large collection is a subquery for every visible row; it is paginated, but it is not free.
  • No aggregate other than count on the collection side. Summing the far side's values is a formula column rather than a relationship setting.
  • The path picker doesn't warn about expensive paths. A four-hop lookup through large tables is offered exactly as readily as a one-hop one.

The relationships guide covers the picker, and the generated REST API exposes the same relationships as expand, so what you see in the grid is reachable programmatically without rebuilding the joins by hand.

Verified 27 Aug 2026: 178 tests across the relationship suites — path walking and its exception cases, edit modes, reverse relationships, foreign-key discovery, many-to-many association, relationship options, column deletion, and expansion through the generated API — all green at the time of writing. Re-verified on 2026-08-26, when the collection-filter limitation below stopped being silent — CollectionFilterWhereClauseTest now asserts that a filter with nothing searchable on the far side is refused with a 400, and that asking whether the collection is empty still answers, in a workspace-api suite of 560 tests run green that day. Re-verified 2026-08-27 for the retirement of the cycle guard — RelationshipPathWalkerTest gained self-referential single- and two-hop cases plus a depth-cap case, and SelfReferentialRelationshipQueryTest proves the generated self-join SQL end to end against a real workspace database, 91 tests across the touched suites green..