GaurGaur docs

The contract model

Why Gaur uses declarative contracts instead of views, and the trust chain that makes them safe to expose.

A contract is a JSON declaration of what callers may ask of a dataset, and how every value they can ask for is calculated. Once a contract is published, applications query it, agents call it, dashboards bind to it; none of them write SQL.

This page is the reasoning behind that design, and the validation chain that makes it work. If you've ever wondered why Gaur rejects a contract that "the SQL is fine for", this is where the answer lives.

What goes wrong without a contract

Three failure modes follow you everywhere raw SQL is the interface:

Calculation drift. Two teams compute "revenue" in two ways. One uses order_total, the other uses subtotal + tax_paid. Both look right; they disagree by single-digit percents that nobody notices for months.

Silent fan-out. Someone joins raw_orders to raw_items to get "revenue with product breakdown" and writes sum(o.order_total). The one-to-many join multiplies orders by their line-item count. Revenue triples. The dashboard still ships.

No row-level enforcement. A customer-facing dashboard authenticates the user and then trusts the frontend to filter to their data. One missed WHERE clause and customer A sees customer B's numbers.

Views and stored procedures help with calculation drift. They don't help with the other two. A view that joins raw_orders to raw_items still inflates revenue if you SUM(order_total) from it. A view doesn't know who's calling.

A contract is the smallest abstraction that addresses all three.

What a contract declares

Five things, in one document:

PartWhat it controls
sourcesThe tables and models read, and how they join (join_type, cardinality, on).
dimensionsThe attributes a caller may group or filter by.
measuresThe values a caller may aggregate, and how each is calculated.
filtersFixed conditions always applied (every consumer sees the filtered view).
rlsRow-level security, evaluated per-call against auth or request context.

Each part is enforced. A query naming a dimension or measure the contract doesn't declare is rejected. A query that would require a join the contract didn't specify is rejected. RLS is injected into every query against the contract; there's no opt-out.

The trust chain

Three things have to be true for "every caller gets the same correct answer" to actually hold. Gaur enforces each one at a different point.

1. Source keys are real

When a table is ingested, Gaur enforces the declared primary key at the storage layer. If the data has duplicates on that key, ingestion fails. By the time a contract joins a table, its declared key is verified data, not a schema hint.

This is the foundation of the next two links. Without it, "many_to_one on the customer id" is a wish.

2. Joins do what they claim

When you create a contract, Gaur walks its sources and checks:

  • Every joined source (every source after the first) declares a cardinality and an on clause.
  • Every many_to_one and one_to_one join references the joined source's declared primary key in its on clause. If it doesn't, the join isn't actually unique on the join key, so calling it many-to-one would lie.
  • The whole join topology is walkable from one fact through dimension lookups without creating fan-out or chasm. A contract that would inflate or multiply rows is rejected, with a message that names the join responsible.

For Jaffle Shop's product_performance, this means: raw_items is the fact; orders_clean, products_clean, sku_costs, and stores_enriched are dimension lookups joined many-to-one on their primary keys. Five sources, zero ways to over-count.

See Rules and limitations for the full ruleset.

3. Queries can only ask what the contract allows

When a query arrives, it's validated against the contract's vocabulary: every dimension and measure named in the query must exist in the contract; every filter operator must match the field's type; every requested time grain must be one the contract declared.

Then the query is compiled into SQL, with the contract's filters appended and the contract's rls clause injected. The compiled SQL runs against Gaur's copy of the data and rows come back.

The caller never wrote SQL. They never named a column the contract didn't expose. They never escaped their RLS scope.

What this gets you in practice

  • A wrong number can't be quietly served. Either the contract is wrong (and the validator caught it at publish time) or the query is wrong (and the validator caught it at query time). There isn't a path from "I asked the system for revenue" to "the system returned an inflated value".
  • One definition for every consumer. revenue_usd is defined once, in the product_performance contract. A web dashboard and an AI agent both read the same measure. They can't drift.
  • AI safety isn't a separate layer. An agent calling query_contract is bound by exactly the same validator and RLS as a curl against the REST endpoint. There's no permissive path for agents.
  • Authors think about meaning, not SQL safety. Declare the cardinality honestly and the cardinality safety is automatic. Write the measure once and additivity rules tell Gaur how it can be rolled up.

Where the model has limits

Gaur guarantees the contract layer. It does not validate the intent of a model's SQL. A model that does the wrong thing in its SQL will faithfully feed a contract the wrong rows, and a contract on top of it will faithfully serve them. This is the same boundary every SQL-based analytics tool has; verify model SQL with exploration before you build contracts on it.

Some analytical patterns aren't expressible in a single contract today and need a model-first remediation: multi-fact contracts (chasm), last-value semi-additive measures, period-over-period comparisons, and timezone normalization.

Where to next

On this page