Concept ยท OrionBelt® Analytics

Join Discovery Without Declared Keys

Most lakehouse layers declare no primary or foreign keys. Here is how OrionBelt® Analytics still finds the right joins, and how you bring your own ontology when you want the last word on them.

The Problem: Tables With No Keys

A harmonized lakehouse rarely declares its relationships. A Databricks gold layer, a ClickHouse warehouse and most BigQuery datasets hold well-named tables and no FOREIGN KEY constraints at all. People who work with the data know that orders.customer_id points at customers. The catalog does not say so.

For Text-to-SQL that gap matters twice. GraphRAG builds its join graph from keys, so without them it has no edges and no join paths to hand the model. OBQC checks joins and fan-traps against the relationships in the ontology, and without relationships it cannot tell a lookup from a join that multiplies rows. The model is left to guess, and a guessed join that runs is the kind of mistake nobody notices.

In one sentence: OrionBelt® Analytics gets relationships from three places, inferred column names, your own ontology and informational constraints, and every one of them feeds both the join paths and the query check.

Three Ways In

Approach OBQC join and fan-trap checks GraphRAG join paths Scope Effort
Inferred from column names, e.g. customer_id to customers Yes Yes, high and medium confidence, marked as inferred Everyone on the connection None, automatic
Your own ontology, uploaded in the chat or read from an import folder Yes, once it is activated Yes, marked as coming from the ontology Your session only Author it once
Informational PK/FK constraints declared in your catalog Yes Yes, as declared keys Everyone on the connection One-off DDL, no data change

The three are not equivalent. Inference is a good guess that costs nothing. An ontology is your statement of how the tables relate, plus the business meaning on top. Declared constraints are the plainest form of the truth, and every tool that reads the catalog benefits from them, not only this one.

1. Joins Inferred From Column Names

When the schema is discovered, OrionBelt® Analytics looks at every column that is not already a declared foreign key or a primary key and asks whether its name points at another table. Two kinds of match are tried, in this order:

Each inferred relationship gets a confidence. It rises when the column's type matches the target table's primary key, when the name matches the table exactly rather than through a singular or plural form, and when the column ends in id. The result is high, medium or low.

Where inferred joins go

A key the database declares always wins over an inferred one between the same two tables. Inference fills gaps. It never overrides the catalog.

2. Your Own Ontology

When the column names are too cryptic for inference, or when you already maintain a domain model, you can load your own ontology with load_my_ontology. It accepts a Turtle file dropped into the chat (passed as ontology_content), or reads the newest .ttl file from an import folder on the server, which suits a shared, curated ontology.

The activation check

OBQC and join discovery read tables, columns and joins through oba: annotations and nothing else. So an uploaded ontology replaces the active one only if it maps to the database with them, namespace https://ralforion.com/ns/oba#:

Maps Required annotations
Tablesowl:Class with oba:tableName, and oba:schemaName when the schema is not public
Columnsowl:DatatypeProperty with oba:columnName, oba:tableName and an rdfs:domain pointing at its table class
Joinsowl:ObjectProperty with oba:foreignKeyColumn, oba:referencedTable and oba:referencedColumn. Optional for activation, but these are what the join checks and join paths use

The test is the one OBQC applies to itself: at least one mapped table, and at least one table with mapped columns. The response always says how it went, with activated and an oba_requirements block that counts the mapped tables, columns and joins and lists the annotations expected.

An ontology that fails the check is not thrown away. It is still loaded into the RDF store and can be queried with SPARQL. It just does not take over: query validation and join paths keep using the ontology that was active before, and the response tells you what to add.

Conformance to the OBA SHACL shapes is reported too, as advice rather than a gate. The shapes describe a generated ontology in full, so a hand-written one that also models business classes with no table behind them can fail them and still work perfectly well for OBQC.

Scope and precedence

3. Informational Constraints in the Catalog

Several lakehouse and warehouse platforms let you declare PRIMARY KEY and FOREIGN KEY constraints that they record but do not enforce. Databricks Unity Catalog and Snowflake both work this way. Declaring them is a one-off DDL statement per table. No data is validated, rewritten or moved.

-- Databricks Unity Catalog: recorded, not enforced
ALTER TABLE gold.customers ADD CONSTRAINT customers_pk PRIMARY KEY (customer_id);
ALTER TABLE gold.orders ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id) REFERENCES gold.customers (customer_id);

Where the OrionBelt® Analytics driver reads the catalog's constraints, as it does for Databricks and Snowflake, these become ordinary declared keys. They feed OBQC and GraphRAG alike, and they win over anything inferred. The platform's own rules apply: on Databricks, for example, primary-key columns must be NOT NULL and the tables must live in Unity Catalog.

Not every driver reads constraints today. The BigQuery and Dremio connections report no keys, so on those, inference and your own ontology are the way in. Run discover_schema once after declaring keys and check that it reports them before you rely on it.

Checking a Join Against the Data

A join inferred from a column name, or written into an uploaded ontology, is a claim about the data. validate_relationship tests it. Give it the table and the key column, and the referenced table if the column points at more than one. It works the same for declared, inferred and uploaded relationships, and refuses one the active ontology does not state.

It runs two read-only queries: how many non-null values of the key column exist in the referenced column, and whether the referenced column is unique. Both tables are scanned. The verdict:

VerdictMeaning
confirmedAt least 99% of non-null keys match: treat it as a key.
partialAt least 90% match. Joins drop the rest.
refutedFewer match. Probably not a relationship.
target_not_uniqueThe referenced column repeats values, so the join multiplies rows.
no_dataThe key column holds no values.

The verdict does not stay in the conversation. It is written onto the relationship in the active ontology as a new version, with oba:validationStatus, oba:validationMatchRatio, oba:validationCheckedRows and oba:validationCheckedAt; it goes into the RDF store; and it is kept in the workspace, so a regenerated ontology gets it back. From then on graphrag_find_join_path shows each join's verdict. A confirmed inferred join loses its "check this" note, and a refuted, partial or non-unique one carries a warning, so the model stops relying on it.

The queries are built as syntax trees, with table and column names emitted as quoted identifiers in each engine's own syntax. A name taken from an uploaded ontology cannot turn into extra SQL.

Recommended Setup

Declare the keys in your catalog where the platform allows it, then add business meaning on top, either by applying business names to the generated ontology or by loading your own. Keys give every tool the joins. The ontology gives the model the vocabulary. Where you rely on inferred or uploaded joins, check the ones that matter with validate_relationship.

Workflow With Your Own Ontology

  1. Start. Download the generated ontology with download_artifact (type ontology). It already carries every oba: mapping. Or start from one you maintain.
  2. Edit. Add the relationships the database does not declare, rename classes and properties to business terms, and write descriptions. OrionBelt® Ontology Builder does this in the browser; Protégé or any OWL editor works too.
  3. Upload. Drop the .ttl file into the chat, or put it in the import folder.
  4. Activate. load_my_ontology checks the mapping and either activates the ontology or lists what is missing.
  5. Ask. Join paths now include your relationships, and OBQC validates every query against your model, fan-traps included.

The smallest useful upload maps two tables, the columns the queries touch, and the join between them:

@prefix oba:  <https://ralforion.com/ns/oba#> .
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix :     <http://example.com/sales#> .

:Customer a owl:Class ;
    rdfs:label "Customer" ;
    oba:tableName "customers" ;
    oba:schemaName "gold" .

:Order a owl:Class ;
    rdfs:label "Order" ;
    oba:tableName "orders" ;
    oba:schemaName "gold" .

:order_net_amt a owl:DatatypeProperty ;
    rdfs:label "Net revenue" ;
    rdfs:domain :Order ;
    rdfs:range xsd:decimal ;
    oba:tableName "orders" ;
    oba:columnName "net_amt" .

:order_customer_id a owl:DatatypeProperty ;
    rdfs:domain :Order ;
    oba:tableName "orders" ;
    oba:columnName "customer_id" .

:customer_customer_id a owl:DatatypeProperty ;
    rdfs:domain :Customer ;
    oba:tableName "customers" ;
    oba:columnName "customer_id" .

:placedBy a owl:ObjectProperty ;
    rdfs:label "placed by" ;
    rdfs:domain :Order ;
    rdfs:range :Customer ;
    oba:foreignKeyColumn "customer_id" ;
    oba:referencedTable "customers" ;
    oba:referencedColumn "customer_id" ;
    oba:relationshipType "many_to_one" .

The full vocabulary, with SQL types, primary-key flags, join conditions and the business annotations, is documented at the oba: namespace. The machine-readable files are oba.ttl, the shapes in oba-shacl.ttl, and a complete annotated example in oba-example.ttl.

Related OrionBelt® Concepts

Frequently Asked Questions

Can OrionBelt Analytics find joins when the database declares no foreign keys?

Yes. It infers likely foreign keys from column naming patterns such as customer_id, id_customer, fk_customer or customer_sk, each with a high, medium or low confidence. The generated ontology records them, so OBQC checks joins and fan-traps against them, and the high- and medium-confidence ones also become GraphRAG join edges, marked as inferred.

What does my own ontology need so it is used for query checks?

oba:tableName on each owl:Class, and oba:columnName plus oba:tableName with an rdfs:domain on each owl:DatatypeProperty. Joins need oba:foreignKeyColumn, oba:referencedTable and oba:referencedColumn on an owl:ObjectProperty. Without table and column mappings the file is loaded for SPARQL only and the previously active ontology stays in force.

How do I know an inferred join is correct?

Check it against the data with validate_relationship. It counts how many non-null key values exist in the referenced column and whether that column is unique, then records confirmed, partial, refuted, target_not_unique or no_data on the relationship in the ontology. Join paths show the verdict from then on, and a regenerated ontology keeps it. See Checking a Join Against the Data.

Does uploading an ontology change the database or affect other users?

No. The database is never written to. An uploaded ontology applies to the session that loaded it: its relationships extend that session's join discovery, and colleagues on the same database keep their own ontology.

What are informational constraints?

PRIMARY KEY and FOREIGN KEY constraints that a platform records in its catalog but does not enforce, as Databricks Unity Catalog and Snowflake do. Declaring them touches no data. Where the driver reads them, they count as declared keys and feed both OBQC and GraphRAG.

Correct joins, with or without keys

Join discovery ships with OrionBelt® Analytics, the ontology-based MCP server for Text-to-SQL across 8 database connectors.

OrionBelt® Analytics Docs GitHub Contact RALFORION