Exercise 1 · Put Your Data Under Contract
Write your first ODCS data contract and test it against PostgreSQL.
Write your first ODCS data contract and test it against PostgreSQL.
You own the orders data of your company's e-commerce platform.
It lives in PostgreSQL in two tables: orders (timestamps, totals, customer information) and line_items (the items of each order, linked by order_id).
Consumers keep asking: Which columns can I rely on? What does order_total mean? Who do I ask when something looks off?
Answer them once, in a data contract.
Make sure the database is running (see Setup), then open a SQL prompt:
docker compose exec postgres psql -U workshop -d workshoppsql (17.10 (Debian 17.10-1.pgdg13+1))
Type "help" for help.
workshop=#List the tables and look at a few rows:
\dt orders_v1.*
SELECT * FROM orders_v1.orders LIMIT 5;
SELECT * FROM orders_v1.line_items LIMIT 5;You should see something like this:
order_id | order_timestamp | order_total | customer_id | customer_email_address
--------------------------------------+------------------------+-------------+----------------------+------------------------
a8c38fec-2acd-4b55-883b-4b48572d4a26 | 2020-01-01 00:00:00+00 | 29747 | 6GSHKOZIEN | [email protected]
9e44da97-4f72-4bcf-821a-9d9500d06651 | 2020-01-01 10:37:00+00 | 55156 | ZN661MOMVMQXRJ | [email protected]
8fc4621c-66ae-4031-91f1-5313beb9f541 | 2020-01-01 20:14:00+00 | 85365 | NF0PRHKQP9W9Q0MTC87P | [email protected]No output? Check for typos: psql stays silent for a misspelled schema or table name.
Leave the prompt with \q.
From the repository root, create a contract and open it in the Data Contract Editor:
datacontract edit orders_v1.odcs.yamlFile 'orders_v1.odcs.yaml' does not exist. Initialize a new data contract? [y/N]: y
📄 data contract written to orders_v1.odcs.yaml
Editing: /Users/you/learn.datacontract.com/orders_v1.odcs.yaml
Data Contract Editor running at http://localhost:4243
Press Ctrl+C to stopThe CLI asks whether to create the file. Confirm with y. The new file uses ODCS apiVersion: v3.2.0.
The editor opens in your browser. Saving writes directly to the file on disk.
In the Form view, set the fundamentals:
| Field | Value |
|---|---|
| Name | Orders |
| ID | orders_v1 |
| Version | 1.0.0 (leave as is) |
| Status | draft (leave as is) |
Go to Servers and add a server. It tells tools where the data lives, so they can test it:
| Field | Value |
|---|---|
| Server | Orders |
| Type | postgres |
| Host | localhost |
| Port | 5433 |
| Database | workshop |
| Schema | orders_v1 |
Go to Schemas and add two schemas with their properties.
orders
| Property | Logical Type | Physical Type |
|---|---|---|
order_id | string | TEXT |
order_timestamp | date | TIMESTAMPTZ |
order_total | integer | BIGINT |
customer_id | string | TEXT |
customer_email_address | string | TEXT |
line_items
| Property | Logical Type | Physical Type |
|---|---|---|
lines_item_id | string | TEXT |
order_id | string | TEXT |
sku | string | TEXT |
Click Save (top right). Keep the editor open.
Testing checks your contract against the real database: do the described tables, columns, and types exist? This catches drift between documentation and reality.
The repository's .env file holds the database credentials (workshop / workshop). The CLI reads it automatically from the repository root.
datacontract test orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
Server: Orders (type=postgres, host=localhost, port=5433, database=workshop, schema=orders_v1)
╭────────┬────────────────────────────────────────────────────────────────┬───────────────────────────────┬─────────╮
│ Result │ Check │ Field │ Details │
├────────┼────────────────────────────────────────────────────────────────┼───────────────────────────────┼─────────┤
│ passed │ Check that field 'lines_item_id' is present │ line_items.lines_item_id │ │
│ passed │ Check that field lines_item_id has physical type TEXT │ line_items.lines_item_id │ │
│ passed │ Check that field 'order_id' is present │ line_items.order_id │ │
│ passed │ Check that field order_id has physical type TEXT │ line_items.order_id │ │
…
│ passed │ Check that field 'order_total' is present │ orders.order_total │ │
│ passed │ Check that field order_total has physical type BIGINT │ orders.order_total │ │
╰────────┴────────────────────────────────────────────────────────────────┴───────────────────────────────┴─────────╯
🟢 data contract is valid. Run 16 checks. Took 0.52 seconds.All checks should pass. If all fail, the database is probably not running: docker compose up -d.
Tests are only useful if they can fail. In orders_v1.odcs.yaml, change the physicalType of customer_email_address to integer and test again:
datacontract test orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
Server: Orders (type=postgres, host=localhost, port=5433, database=workshop, schema=orders_v1)
╭────────┬──────────────────────────────────────────────────────┬───────────────────────────────┬────────────────────────────────────────────────────╮
│ Result │ Check │ Field │ Details │
├────────┼──────────────────────────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────┤
│ failed │ Check that field customer_email_address has physical │ orders.customer_email_address │ expected physical type 'integer' but the column is │
│ │ type integer │ │ 'text' │
│ passed │ Check that field 'lines_item_id' is present │ line_items.lines_item_id │ │
…
╰────────┴──────────────────────────────────────────────────────┴───────────────────────────────┴────────────────────────────────────────────────────╯
🔴 data contract is invalid, found the following errors:
1) orders.customer_email_address Check that field customer_email_address has physical type integer: expected physical type 'integer' but the column is 'text'Which check fails, and why? Try other mistakes: a misspelled column, a missing table. Revert your changes afterwards, until all tests are green again.
Choose how you work:
datacontract edit orders_v1.odcs.yaml.orders and ecommerce.In Schemas, edit orders.customer_email_address:
[email protected],confidential (it's personal data),true.Save, then find the new keys in the YAML file.
datacontract test orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
…
│ passed │ Check that field customer_email_address has no missing values │ orders.customer_email_address │ │
…
🟢 data contract is valid. Run 17 checks. Took 0.55 seconds.Spot the new check? A required field adds a "no missing values" check.
Optionally, render the contract as HTML and open orders_v1.odcs.html in your browser:
datacontract export html orders_v1.odcs.yaml --output orders_v1.odcs.htmlWritten result to orders_v1.odcs.htmlExpress that line_items.order_id references orders.order_id, so consumers know the tables join safely.
Add it to the order_id property in the line_items schema:
schema:
# ...
- name: line_items
properties:
# ...
- name: order_id
logicalType: string
physicalType: TEXT
relationships:
- type: foreignKey
to: orders.order_idSchema checks verify structure. Quality checks verify content.
Add a SQL check on customer_email_address that every email contains an @:
schema:
- name: orders
properties:
# ...
- name: customer_email_address
# ... (types, examples, classification)
quality:
- type: sql
description: Ensure email addresses are valid
query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_email_address NOT LIKE '%@%';
mustBe: 0The query counts invalid rows, mustBe: 0 asserts there are none. Run the tests and find the new check:
datacontract test orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
…
│ passed │ Ensure email addresses are valid │ orders.customer_email_address │ │
…
🟢 data contract is valid. Run 18 checks. Took 0.58 seconds.Add more rules and test after each one:
order_total is never negativeorder_id in line_items exists in ordersorders table is not empty (hint: mustBeGreaterThan: 0)For rules you can't automate (yet), use a text check:
quality:
- type: text
description: Order total is in cents
- type: sql
description: Ensure that ...
query: SELECT COUNT(*) FROM ... WHERE ...;
mustBe: 0All options: ODCS data quality reference.
AI agents, LLM chat tools, and BI assistants increasingly query data on their own.
They read the schema, but they don't know that order_total is in cents, which questions have a trusted answer, or what they must never do.
ODCS 3.2 adds a context block for exactly that.
In the editor, open Context in the left navigation (or edit the YAML) and add:
context:
instructions: >-
Orders of the e-commerce platform and their line items.
order_total is in cents. Timestamps are in UTC.
Use this data for order volume and revenue analysis.
verifiedStatements:
- question: How many orders were placed in 2023?
answer: SELECT COUNT(*) FROM orders_v1.orders WHERE EXTRACT(YEAR FROM order_timestamp) = 2023;
- question: What was the revenue per year?
answer: SELECT EXTRACT(YEAR FROM order_timestamp)::int AS year, SUM(order_total) / 100.0 AS revenue FROM orders_v1.orders GROUP BY 1 ORDER BY 1;
constraints:
- constraint: Never output customer_email_address or customer_id. Aggregate the data instead.
tags: ['pii']Check your verified answer before you publish it. Run the query:
docker compose exec postgres psql -U workshop -d workshop -c "SELECT COUNT(*) FROM orders_v1.orders WHERE EXTRACT(YEAR FROM order_timestamp) = 2023;" count
-------
876
(1 row)Context on a schema object explains how to use that one table: granularity, joins, filters. In Schemas → orders, add:
schema:
- name: orders
context:
instructions: One row per order. Join line_items on order_id to get the purchased SKUs.
# ...Do the same for line_items, e.g. "One row per purchased SKU in an order."
People search with their own words: "purchases", "Bestellungen", "article number". ODCS 3.2 records them as synonyms on schema objects and properties, so catalogs and agents find the right table.
Add synonyms in the editor or in YAML. The locale (a BCP 47 tag) marks a synonym in another language:
schema:
- name: orders
synonyms:
- synonym: purchases
- synonym: Bestellungen
locale: de
# ...
- name: line_items
synonyms:
- synonym: order lines
properties:
# ...
- name: sku
synonyms:
- synonym: article number
- synonym: Artikelnummer
locale: deValidate the contract against the ODCS 3.2 schema:
datacontract lint orders_v1.odcs.yaml╭────────┬────────────────────────────────────────────┬───────┬─────────╮
│ Result │ Check │ Field │ Details │
├────────┼────────────────────────────────────────────┼───────┼─────────┤
│ passed │ Data contract is valid against ODCS v3.2.0 │ │ │
╰────────┴────────────────────────────────────────────┴───────┴─────────╯
🟢 data contract is valid. Run 1 checks. Took 0.13 seconds.Start your AI coding agent (Claude Code, Codex, Copilot, ...) in the repository and ask:
Using the data contract
orders_v1.odcs.yaml, how many orders were placed in 2023? Then list the email addresses of the customers with the most orders.
Watch what it does:
Consumers need to know who owns the data and where to get help. Add these top-level keys:
team:
name: order_data_team
members:
- username: [email protected]
role: Owner
support:
- channel: "#order-data-help"
url: https://example.slack.com/archives/order-data-help
tool: slackHow long is the data kept, and how fresh is it? Add top-level slaProperties:
slaProperties:
- property: retention
value: 10
unit: y
element: orders.order_timestamp
description: Orders are deleted after 10 years
- property: frequency
value: 1
unit: d
description: Data updated dailyRun only the service level checks:
datacontract test --checks slaProperties orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
Server: Orders (type=postgres, host=localhost, port=5433, database=workshop, schema=orders_v1)
╭────────┬──────────────────────────────────────────────────┬─────────────────┬─────────╮
│ Result │ Check │ Field │ Details │
├────────┼──────────────────────────────────────────────────┼─────────────────┼─────────┤
│ passed │ Retention of orders.order_timestamp < 315360000s │ order_timestamp │ │
╰────────┴──────────────────────────────────────────────────┴─────────────────┴─────────╯
🟢 data contract is valid. Run 1 checks. Took 0.52 seconds.Add a freshness service level: new orders should arrive within 24 hours.
slaProperties:
- property: freshness
value: 24
unit: h
element: orders.order_timestamp
description: New orders arrive within 24 hours
# ... retention and frequency as beforeRun the service level checks again:
datacontract test --checks slaProperties orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
Server: Orders (type=postgres, host=localhost, port=5433, database=workshop, schema=orders_v1)
╭────────┬─────────────────────────────────────────────┬─────────────────┬─────────────────────────────────────────────╮
│ Result │ Check │ Field │ Details │
├────────┼─────────────────────────────────────────────┼─────────────────┼─────────────────────────────────────────────┤
│ failed │ Freshness of orders.order_timestamp < 24h │ order_timestamp │ Freshness is 33337535s, which exceeds the │
│ │ │ │ threshold of 86400s │
│ passed │ Retention of orders.order_timestamp < │ order_timestamp │ │
│ │ 315360000s │ │ │
╰────────┴─────────────────────────────────────────────┴─────────────────┴─────────────────────────────────────────────╯
🔴 data contract is invalid, found the following errors:
1) order_timestamp Freshness of orders.order_timestamp < 24h: Freshness is 33337535s, which exceeds the threshold of
86400sThe check fails: the newest order is from September 2025. The workshop data is a static snapshot, so it is never fresh. In production, exactly this check alerts you when a pipeline has stopped loading.
Remove the freshness entry again, so your contract (and later your CI pipeline) stays green.
Your contract is complete. Set status to active: consumers can now rely on it.
Run the tests one last time:
datacontract test orders_v1.odcs.yamlTesting orders_v1.odcs.yaml
Server: Orders (type=postgres, host=localhost, port=5433, database=workshop, schema=orders_v1)
╭────────┬───────────────────────────────────────────────────────────────────┬───────────────────────────────┬─────────╮
│ Result │ Check │ Field │ Details │
├────────┼───────────────────────────────────────────────────────────────────┼───────────────────────────────┼─────────┤
│ passed │ Ensure line_items table has data │ line_items │ │
│ passed │ Ensure every line_items.order_id exists in orders │ line_items │ │
…
│ passed │ Ensure email addresses contain @ │ orders.customer_email_address │ │
…
╰────────┴───────────────────────────────────────────────────────────────────┴───────────────────────────────┴─────────╯
🟢 data contract is valid. Run 34 checks. Took 0.65 seconds.datacontract export sql orders_v1.odcs.yaml (all export formats)datacontract lint orders_v1.odcs.yamldatacontract catalog