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.
cardinality — one_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.