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:
| Part | What it controls |
|---|---|
sources | The tables and models read, and how they join (join_type, cardinality, on). |
dimensions | The attributes a caller may group or filter by. |
measures | The values a caller may aggregate, and how each is calculated. |
filters | Fixed conditions always applied (every consumer sees the filtered view). |
rls | Row-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
cardinalityand anonclause. - Every
many_to_oneandone_to_onejoin references the joined source's declared primary key in itsonclause. 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_usdis defined once, in theproduct_performancecontract. 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_contractis 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.