A virtual relation documents a dependency the database does not declare. It is drawn dashed on the canvas, saved in the diagram’s JSON, and never written to the database: no migration, SQL export, or DBML export ever turns it into a real foreign key. An inferred relation is a virtual relation Schemity read from column names, so a schema whose foreign keys were never created opens with its edges already drawn.
This is for the schemas where the real dependencies live only in naming: a dbt-built warehouse, a Rails or Django database whose keys were never declared, or a database whose keys were dropped for load performance. Without it, such a schema opens as hundreds of tables with no lines between them.
How do I draw a virtual relation?
Draw a relationship between two entities as usual (see Relationships), then switch the relation dialog from Relation to Virtual relation. The question changes: instead of naming a new foreign key column, you pick which columns already carry the dependency, one on each entity.
A virtual relation adds a foreign key to the diagram and nothing else, so no column is created and the child entity does not grow. The dialog leaves out what a virtual relation cannot honestly offer:
- No ON DELETE or ON UPDATE. Nothing enforces a virtual relation, so a referential action would claim behavior the database never performs.
- No N:N. Many-to-many would build a junction table, which is schema, not documentation.
- Cardinality is derived from the child column’s unique constraints rather than asked for.
The dialog remembers which mode you used last in that diagram, so documenting a whole warehouse does not mean re-picking Virtual relation every time.
How does Schemity infer relations from column names?
When a diagram loads - on connect, refresh, plain file open, and SQL or DBML import - Schemity scans every column for a name that points at another entity and adds a virtual relation for each match. It never runs while you type.
The matcher splits a column name into words rather than matching strings, so it follows the naming convention your codebase already uses:
articles.user_profile_id,userProfileId, anduserProfileIDall resolve touser_profiles(oruserProfiles).created_by_user_idfalls back through shorter word runs until it reachesusers.- The longest match wins, so
tenant_history_idresolves totenant_history, never totenants. superuser_iddoes not matchusers, becausesuperuseris one word.
The referenced column resolves in order: a single-column primary key, then a lone single-column unique constraint, then a column named for the suffix (id). The second and third rungs matter because a CREATE TABLE AS SELECT table has no primary key at all.
When does Schemity not infer a relation?
A candidate is skipped when:
- the column is already part of a foreign key, declared by the database or drawn by you;
- either end is a database view;
- two entities normalize to the same name, so the match is ambiguous;
- no referenced column resolves;
- the two columns’ types disagree.
Warehouse suffixes such as _key and _sk are deliberately not matched, because sort_key and api_key have the same shape and would produce confident nonsense. Composite keys are not inferred either: nothing in a naming convention says which columns group together.
How do I tell an inferred relation from one I drew?
After a load that inferred anything, a notification gives the count and says the relations are drawn dashed and are not declared in the database. The cardinality dialog of an inferred relation says it was inferred from column names and is not declared in the database, and the Context Map’s dependency drawer tags each row behind an arrow as Virtual or Inferred. The arrows themselves count both kinds, because an architectural dependency is real whether or not the database enforces it.
How do I remove inferred relations?
- One at a time - delete the relation like any other. The deletion is permanent: the pair is recorded in the diagram file, so the next load does not bring it back. Renaming the column clears that record, because the dismissal was a judgement about that column.
- All at once - choose Remove all inferred relations from the layout menu in the control bar.
Deleting a virtual relation never deletes its columns. It did not create them, and they may already belong to a primary key or a real foreign key.
What happens to virtual relations when the database changes?
They survive every rewrite that could quietly drop them. A re-sync against the live database restores saved virtual relations onto the freshly read entities, with their waypoints and cardinality. Renaming an entity or a column, pasting, and duplicating all carry them along. A saved virtual relation whose columns no longer exist is not restored, and one whose columns a real foreign key now covers is dropped, so the declared edge replaces it rather than doubling it.
How do lint and AI agents treat virtual relations?
Schema Lint separates shape from enforcement. Junction table detection counts virtual keys as ordinary edges, which is what lets it recognize junction tables on a warehouse where every relation is virtual. The not-null cycle rule ignores them, since a cycle only blocks inserts if the database checks the keys in it. A type mismatch is still reported, because mismatched types stop a join from using an index whether or not a constraint is declared.
An AI agent connected over MCP is told which relations are virtual and which were inferred, so an undeclared dependency is weighed as the guess it is.
Next
Check the schema for problems with Schema Lint.