> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rafflesia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Joins & join safety

> Reciprocal, typed relationships that are checked against the ontology before a single row is read.

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](/ontology/identity).

## Naive vs. typed

The same intent, written two ways:

```sql theme={"dark"}
-- Naive SQL join — runs, returns rows, silently wrong
SELECT *
FROM   variants v
JOIN   residues r
  ON   v.residue_number = r.position
```

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.

```json theme={"dark"}
// Rafflesia relationship — declared once, checked every time
{
  "relation": "residue_features",
  "cardinality": "many_to_one",
  "local_columns": [
    "structure_id",
    "structure_chain_id",
    "structure_residue_number"
  ],
  "remote_columns": ["structure_id", "chain_id", "position"],
  "is_lossy": false,
  "is_range_mapping": false
}
```

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](/ontology/equivalences)).
* **`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:

```bash theme={"dark"}
rafflesia query databases --json
rafflesia query releases --database uniprot --json
rafflesia query releases --database sifts --json
```

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](/ontology/rql) 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.

<Warning>
  **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.
</Warning>

## 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:

```bash theme={"dark"}
rafflesia query ontology paths source_sequence_records sifts_chain_mapping \
  --from-database uniprot --from-release 2026_07 \
  --to-database sifts --to-release 2026_07 \
  --evidence cardinality_consistent --safety strict --max-hops 3
```

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:

```bash theme={"dark"}
rafflesia query ontology context \
  --source source_sequence_records=uniprot,2026_07 \
  --source sifts_chain_mapping=sifts,2026_07 \
  --evidence cardinality_consistent --safety strict \
  --max-paths 3 --max-path-hops 4 --max-bytes 32768 --json
```

`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`:

```bash theme={"dark"}
rafflesia query joins requirements \
  --source records=uniprot,2026_07,source_sequence_records \
  --source mappings=sifts,2026_07,sifts_chain_mapping \
  --sql 'select records.accession, mappings.pdb_id from records join mappings on records.accession = mappings.uniprot_accession order by records.accession, mappings.pdb_id' \
  --join-evidence-policy cardinality_consistent --json
```

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](/ontology/rql) — how relationships, dimensions,
and measurements come together into one reproducible question that compiles to
portable SQL.
