# Data contracts (ODCS)

A data contract is a team's written promise about a dataset: its columns, the quality rules it
keeps and how fresh it stays. DataQ reads contracts in the
[Open Data Contract Standard](https://bitol-io.github.io/open-data-contract-standard/) (ODCS) v3
and turns them into ordinary suites and checks that run, alert and score like any other. This is
the **consumer** side: DataQ checks data that has already landed against the contract. It does not
enforce anything inside the producer's pipeline.

## Import a contract

In the app, open **Contracts** in the sidebar and choose **Import contract**: pick the connection
the data lives on and paste the document or choose the file. Over the API, send the ODCS document, as YAML text or JSON, with the connection the data lives on:

```bash
curl -X POST "$DATAQ/api/v1/contracts/import" \
  -H "Authorization: Bearer $DATAQ_TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -n --arg yaml "$(cat orders.odcs.yaml)" --arg conn "$CONNECTION_ID" \
        '{connection_id: $conn, document_yaml: $yaml}')"
```

DataQ validates the document against the ODCS JSON Schema for its `apiVersion` (v3.0.0 to
v3.2.0; a newer v3 minor is accepted with a warning; v2 and v4 are refused), stores it exactly as
sent, and creates **one suite per schema object**, targeted at the object's `physicalName`
(`schema.table`, `catalog.schema.table` on Unity Catalog, a path on object stores). The response
lists the suites it created, any object whose name did not resolve to a table on that connection
(its suite exists but cannot run until you set a target), and every clause it could not enforce.

Importing requires the Member role. Each version of a contract is stored once; importing the same
`id` and `version` again is refused unless you send `replace: true`.

## What becomes a check

| In the contract | In DataQ |
|---|---|
| `required: true` | not-null check |
| `unique: true`, a single `primaryKey` | uniqueness check |
| several `primaryKey` properties | compound uniqueness |
| `logicalTypeOptions` minimum / maximum (exclusive too), length, pattern | range, length and regex checks |
| `enum` / `invalidValues` with `validValues` or `pattern` | value-set or regex check |
| `nullValues`, `missingValues`, `duplicateValues` | not-null, not-in-set, uniqueness checks; a `percent` bound becomes the fail threshold |
| `rowCount` | a volume monitor; a one-sided bound stays one-sided |
| the object's property list | a schema-drift check whose baseline is the declared columns |
| `type: sql` rules | a custom SQL check that fails when the query's result breaks the operator, or is empty |
| `type: custom` with `engine: great-expectations` | that expectation, if DataQ allows it |
| `slaProperties` `latency` on a timestamp column | a freshness monitor |
| `classification` of `confidential`, `restricted`, `pii`, `sensitive` or `secret` | the column joins the suite's PII columns, so failing samples mask it |
| `severity` `info` / `warning` / `error` / `critical` | warn / warn / fail / critical; no severity means fail |
| `dimension` | DataQ's dimension (`conformity` reads as validity; `coverage` stays unclassified) |

A schedule for an object's suite comes from a `dataq.schedule` cron in the object's
`customProperties`, and `dataq.maxAgeHours` (with `dataq.freshnessColumn`) adds a freshness
ceiling.

### Not enforced

Everything else is kept on the contract as **not enforced**, with the reason, and never silently
dropped. That includes `frequency` (a refresh cadence is not a staleness limit; state the limit as
`latency`), `retention` and the other service-level properties, `text` rules, rules for another
engine such as Soda, a `rows` bound on `nullValues`, `multipleOf`, and any check DataQ's own
validators refuse for that datasource.

## A new version

Import the new version with `suite_ids` set to the suites the old version created. DataQ re-points
them and reconciles their checks: clauses that are new get a check, a check nobody has edited since
the import is updated to the new clause, a check whose clause disappeared is **switched off** (not
deleted, so its history stays), and checks people added to the suite themselves are left alone.
Without `suite_ids`, the new version creates fresh suites.

## Compliance

A contract's page in the app shows the same rollup, with a warning when a suite's latest run has
not finished or some suites are hidden from you, and a **Download ODCS** button. Over the API,
`GET /api/v1/contracts/{id}` carries a `compliance` block: for every clause a check enforces, the
state that check reached in its suite's latest run — `pass`, `warn`, `fail`, `critical`,
`unverified` (the check errored or was skipped), `not_run` (that run has no result for it),
`disabled`, or `missing` (the suite has no check for the clause at all, because it was deleted
or the suite was linked by hand) — and a `drifted` flag when someone has edited the check since the import, so it may
no longer say what the clause says. Each suite reports its latest run and `results_final`, which
is false while that run is still going: the states are partial until then. The clauses DataQ
does not enforce are counted as `not_enforced`.

When a contract-derived check opens an incident, the incident's evidence names the contract and
the clause it broke, and alerts lead with it.

## Link a suite by hand

To attach a suite you already have to a contract's schema object, send
`PATCH /api/v1/suites/{id}` with `{"contract": {"contract_id": "…", "contract_object": "orders"}}`,
or `{"contract": null}` to detach it. Linking adds no checks; importing the contract does.

## Export

`GET /api/v1/contracts/{id}/export` returns the contract as an ODCS document (JSON, or YAML with
`?format=yaml`): the stored document plus `dataq.*` custom properties naming the check behind each
rule, and every check people added to the suites as a `type: custom, engine: dataq` rule.
Importing the export recreates every check.

## Who sees a contract

You see a contract when you can view at least one of its suites; the response counts the linked
suites you cannot see without naming them. Workspace admins also see contracts with no linked
suite. Deleting a contract needs edit on all of its suites, and only unlinks them: no suite, check
or result is deleted.

## Contracts and comparison checks

A [comparison check](datasources-checks.md) reconciles two datasets against each other. A contract
describes one dataset against a written promise. They answer different questions and can be used
together.
