Skip to main content
A join is a claim that two rows describe the same biology. In plain SQL that claim is never checked — the engine compares whatever columns you name and trusts you got it right. The ontology’s job is to type-check the claim. A join is only allowed along a registered relationship whose columns agree on kind, namespace, and coordinate system.

Naive vs. typed

The same intent, written two ways:
A variant’s structure_residue_number and a residue’s protein_sequence_position are different coordinate systems. The query executes without error and returns the wrong rows.
The key is a fully typed composite. Because the relationship is registered on the relation (and its reciprocal on the other side), the compiler knows exactly which columns may join and refuses anything else — refused, not guessed.

Anatomy of a relationship

Every relation declares its joins as reciprocal relationships:
  • local_columns / remote_columns — the composite key, column for column.
  • cardinalityone_to_one, many_to_one, one_to_many, or many_to_many. The compiler uses it to reason about fanout.
  • is_lossy — the mapping drops or approximates rows (see Lossy & range mappings).
  • is_range_mapping — the join is an interval containment, not an equality.
  • coordinate_notes — human-readable caveats, e.g. “confirm wildtype_matches before trusting the mapping.”
Because relationships are reciprocal, adding a join means declaring it on both relations — the loader rejects a one-sided relationship.

Declaration is not release evidence

A registered relationship says which columns are semantically allowed to join. It does not prove that both relations exist—or that their keys overlap—in the two database releases you intend to query. Safe discovery therefore starts with immutable database and release selectors:
Use those values for every release-backed join operation. The engine retains the release digest and relation digest on both sides, so evidence from an older release cannot silently authorize a new one.

strict_joins

Each RQL recipe declares how tolerant its joins are:
  • strict_joins: true — refuse any join that is not a registered, type-checked relationship. Use it when correctness matters more than coverage; an unmapped identity fails loudly instead of dropping or mismatching rows.
  • strict_joins: false — the join still type-checks, but unresolved keys are tolerated rather than raised. Useful for exploratory recipes.
Strict is the safe default for pipelines. Exploratory recipes often run strict_joins: false to see what connects. Anything feeding a model or a decision should run strict_joins: true, so a missing relationship surfaces as a failed run rather than a quietly incomplete result.

Finding safe paths

You don’t have to know the join graph by heart. query ontology paths performs bounded traversal over the registered relationships and reports every candidate with its exact column pairs, cardinality, fanout, lossy/range flags, release pins, and retained evidence:
The identity_safe and strict safety filters let the caller decide which mapping risks are admissible. query ontology context materializes the smallest useful release-bound schema and path slice for a join-heavy task:
max_bytes is a hard limit on the serialized data object. Required keys and pinned source relations are preserved; lower-priority relations, optional columns, generated Markdown, and extra paths are removed deterministically. The response reports its exact byte count, estimated tokens, omissions, and warnings. Bridge releases are retained only while a returned path still references them. For three or more sources, source-pair counters distinguish pairs represented by a returned path from unresolved, unevaluated, and bound-omitted pairs; the path budget is allocated across source pairs before alternative paths are added.

Explain and unblock a join

Before executing SQL, run the aggregate preflight with the same --source and --sql flags used by query run:
Each row gives a mechanical admitted or refused decision, both exact source selectors and digests, the declared key/cardinality/risk facts, available and required evidence, and a stable proof digest. A measurement-blocked refusal includes the typed request and exact rafflesia query joins measure ... arguments needed to collect that evidence. A lossy, range, or many-to-many declaration remains refused regardless of measurement and is labeled unsafe_declaration. Joining two releases of the same logical database is reported separately as release_mismatch. query explain is a preflight operation: a strict-join or missing-evidence refusal does not hide the plan. Valid SQL and exact sources still produce a plan whose join explanations contain the same selectors, decision reason, remediation, and proof digest. Invalid SQL, missing sources, and malformed evidence references remain request errors. Next: RQL recipes — how relationships, dimensions, and measurements come together into one reproducible question that compiles to portable SQL.