# Daten unter Vertrag nehmen

Schreibe deinen ersten ODCS-Datenkontrakt und teste ihn gegen PostgreSQL.

Source: https://learn.datacontract.com/de/contract/

Du bist Owner der **Orders**-Daten in der E-Commerce-Plattform deines Unternehmens.
Sie liegen in PostgreSQL in zwei Tabellen: `orders` (Zeitstempel, Summen, Kundeninformationen) und `line_items` (die Positionen jeder Bestellung, verknüpft über `order_id`).
Deine Konsumenten fragen immer wieder: *Auf welche Spalten kann ich mich verlassen? Was bedeutet `order_total`? Wen frage ich, wenn etwas komisch aussieht?*
Beantworte das einmal, in einem Datenkontrakt.

**You will learn:**
- Einen ODCS-Datenkontrakt mit dem Data Contract Editor erstellen
- Den Kontrakt gegen die echte Datenbank testen und Tests fehlschlagen sehen
- Nutzungsbedingungen, Klassifizierung, Beziehungen und Qualitätschecks ergänzen
- KI-Agenten Kontext geben: Anweisungen, verifizierte Antworten und Verbote (neu in ODCS 3.2)
- Ownership, Support-Kanäle und Service Levels ergänzen

_Scenario: the Orders data product (output ports orders_v1 and orders_v2) is consumed by the SKU Sales data product, which the purchasing team uses._

## Die Daten erkunden

### Daten ansehen

Stell sicher, dass die Datenbank läuft (siehe [Setup](https://learn.datacontract.com/de/setup/)), und öffne einen SQL-Prompt:

macOS / Linux:

```bash
docker compose exec postgres psql -U workshop -d workshop
```

Windows (PowerShell):

```powershell
docker compose exec postgres psql -U workshop -d workshop
```

Output:

```text
psql (17.10 (Debian 17.10-1.pgdg13+1))
Type "help" for help.

workshop=#
```

Liste die Tabellen auf und sieh dir ein paar Zeilen an:

```sql
\dt orders_v1.*
SELECT * FROM orders_v1.orders LIMIT 5;
SELECT * FROM orders_v1.line_items LIMIT 5;
```

Das Ergebnis sieht etwa so aus:

```text
               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           | test394@example.org
 9e44da97-4f72-4bcf-821a-9d9500d06651 | 2020-01-01 10:37:00+00 |       55156 | ZN661MOMVMQXRJ       | test4757@example.org
 8fc4621c-66ae-4031-91f1-5313beb9f541 | 2020-01-01 20:14:00+00 |       85365 | NF0PRHKQP9W9Q0MTC87P | test1991@example.org
```

Keine Ausgabe? Prüfe auf Tippfehler: `psql` bleibt bei falsch geschriebenen Schema- oder Tabellennamen stumm.
Verlasse den Prompt mit `\q`.

## Den Kontrakt erstellen

### Kontrakt-Datei anlegen

Lege im Root des Repositorys einen Kontrakt an und öffne ihn im Data Contract Editor:

macOS / Linux:

```bash
datacontract edit orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract edit orders_v1.odcs.yaml
```

Output:

```text
File '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 stop
```

Die CLI fragt, ob sie die Datei anlegen soll. Bestätige mit `y`. Die neue Datei nutzt ODCS `apiVersion: v3.2.0`.
Der Editor öffnet sich im Browser. **Speichern schreibt direkt in die Datei auf der Festplatte.**

Setze in der **Form**-Ansicht die Grunddaten:

| Feld | Wert |
|---|---|
| Name | `Orders` |
| ID | `orders_v1` |
| Version | `1.0.0` (so lassen) |
| Status | `draft` (so lassen) |

![Data Contract Editor: Formularansicht mit den Fundamentals](https://learn.datacontract.com/screenshots/editor-fundamentals.webp)

> **Warum Version 1.0.0 und Status draft?**
>
> Kontrakte nutzen Semantic Versioning: Eine neue **Major**-Version signalisiert einen Breaking Change.
> Der `status` sagt Konsumenten, ob sie sich auf den Kontrakt verlassen können. `draft` heißt „in Arbeit, bau noch nicht darauf auf“.

### Server hinzufügen

Gehe zu **Servers** und füge einen Server hinzu. Er sagt den Werkzeugen, *wo* die Daten liegen, damit sie sie testen können:

| Feld | Wert |
|---|---|
| Server | `Orders` |
| Type | `postgres` |
| Host | `localhost` |
| Port | `5433` |
| Database | `workshop` |
| Schema | `orders_v1` |

![Der Server sagt Werkzeugen, wo die Daten liegen](https://learn.datacontract.com/screenshots/editor-server.webp)

> **Variablen (ODCS 3.2)**
>
> Kontrakte dürfen Variablen enthalten, etwa `host: ${DB_HOST:-localhost}`. Werkzeuge lösen sie zur Laufzeit aus der Umgebung auf, der Wert nach `:-` ist der Default.
> So funktioniert ein Kontrakt in Dev, Test und Produktion, ohne Secrets in Git.

### Schema beschreiben

Gehe zu **Schemas** und lege zwei Schemas mit ihren Properties an.

**`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` |

![Das Schema orders mit seinen Properties, rechts die Vorschau](https://learn.datacontract.com/screenshots/editor-schema.webp)

> **Logischer vs. physischer Typ**
>
> Der **logische Typ** ist die technologieunabhängige Bedeutung (`integer`, `string`, `date`). Damit arbeiten Konsumenten.
> Der **physische Typ** ist, wie die Datenbank speichert (`BIGINT`, `TEXT`, `TIMESTAMPTZ`). Dagegen prüfen die Tests.

Klicke auf **Save** (oben rechts). Lass den Editor offen.

## Den Kontrakt testen

Der Test prüft deinen Kontrakt gegen die *echte* Datenbank: Gibt es die beschriebenen Tabellen, Spalten und Typen?
So fällt auf, wenn Dokumentation und Realität auseinanderlaufen.

### Tests ausführen

Die `.env`-Datei des Repositorys enthält die Zugangsdaten (`workshop` / `workshop`). Die CLI liest sie im Root des Repositorys automatisch.

macOS / Linux:

```bash
datacontract test orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v1.odcs.yaml
```

Output:

```text
Testing 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.
```

Alle Checks sollten grün sein. Schlagen **alle** fehl, läuft die Datenbank vermutlich nicht: `docker compose up -d`.

### Einen Test fehlschlagen lassen

Tests nützen nur, wenn sie fehlschlagen können. Ändere in `orders_v1.odcs.yaml` den `physicalType` von `customer_email_address` auf `integer` und teste erneut:

macOS / Linux:

```bash
datacontract test orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v1.odcs.yaml
```

Output:

```text
Testing 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'
```

Welcher Check schlägt fehl, und warum? Probier andere Fehler aus: eine falsch geschriebene Spalte, eine fehlende Tabelle.
**Mach die Änderungen danach rückgängig**, bis alle Tests wieder grün sind.

## Den Kontrakt anreichern

Wähle, wie du arbeitest:

- **Data Contract Editor**: ideal, um Felder zu entdecken. Öffne ihn wieder mit `datacontract edit orders_v1.odcs.yaml`.
- **Deine IDE** (z. B. VS Code): schneller, wenn du die Felder kennst. Jedes Formularfeld ist ein YAML-Key, siehe die [ODCS-Referenz](https://bitol-io.github.io/open-data-contract-standard/latest/).

> **Warning**
>
> Datei in der IDE geändert, während der Editor offen ist? Lade die Editor-Seite neu, bevor du dort speicherst, sonst überschreibt er deine Änderungen.

### Nutzungsbedingungen und Tags ergänzen

- Ergänze unter **Terms of Use** eine **Description**, einen **Purpose** und **Limitations**. Die ✨-AI-Buttons schreiben Entwürfe.
- Ergänze unter **Fundamentals** **Tags** wie `orders` und `ecommerce`.

### E-Mail-Adresse klassifizieren

Bearbeite unter **Schemas** das Feld `orders.customer_email_address`:

- ergänze **Examples** aus den Daten, z. B. `test394@example.org`,
- setze **Classification & Security → Classification** auf `confidential` (personenbezogene Daten),
- setze **Constraints → Required** auf `true`.

Speichere und finde die neuen Keys in der YAML-Datei.

![Diagrammansicht: Ein Klick auf eine Property öffnet Klassifizierung und Quality Rules](https://learn.datacontract.com/screenshots/editor-property-email.webp)

### Erneut testen und den neuen Check finden

macOS / Linux:

```bash
datacontract test orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v1.odcs.yaml
```

Output:

```text
Testing 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.
```

Findest du den neuen Check? Ein `required`-Feld ergibt einen Check auf fehlende Werte.

Optional: Rendere den Kontrakt als HTML und öffne `orders_v1.odcs.html` im Browser:

macOS / Linux:

```bash
datacontract export html orders_v1.odcs.yaml --output orders_v1.odcs.html
```

Windows (PowerShell):

```powershell
datacontract export html orders_v1.odcs.yaml --output orders_v1.odcs.html
```

Output:

```text
Written result to orders_v1.odcs.html
```

## Beziehungen und Qualität

### Beziehung definieren

Drücke aus, dass `line_items.order_id` auf `orders.order_id` verweist. So wissen Konsumenten, dass der Join sicher ist.
Ergänze das an der **Property** `order_id` im Schema `line_items`:

```yaml
schema:
  # ...
  - name: line_items
    properties:
      # ...
      - name: order_id
        logicalType: string
        physicalType: TEXT
        relationships:
          - type: foreignKey
            to: orders.order_id
```

![Die Diagrammansicht zeigt die Beziehung zwischen line_items und orders](https://learn.datacontract.com/screenshots/editor-diagram.webp)

### Qualitätscheck ergänzen

Schema-Checks prüfen die *Struktur*, Qualitätschecks den *Inhalt*.
Ergänze an `customer_email_address` einen SQL-Check, dass jede E-Mail ein `@` enthält:

```yaml
schema:
  - name: orders
    properties:
      # ...
      - name: customer_email_address
        # ... (Typen, Beispiele, Klassifizierung)
        quality:
          - type: sql
            description: Ensure email addresses are valid
            query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_email_address NOT LIKE '%@%';
            mustBe: 0
```

Die Abfrage zählt *ungültige* Zeilen, `mustBe: 0` stellt sicher, dass es keine gibt. Führe die Tests aus und finde den neuen Check:

macOS / Linux:

```bash
datacontract test orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v1.odcs.yaml
```

Output:

```text
Testing 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.
```

> **Tip**
>
> „Zähle die fehlerhaften Zeilen, erwarte null“ funktioniert für fast jede Regel. Schlägt der Check fehl, hilft dir die Abfrage beim Debuggen.

### Weitere Qualitätschecks ergänzen

Ergänze weitere Regeln und teste nach jeder:

- `order_total` ist nie negativ
- jede `order_id` in `line_items` existiert in `orders`
- die Tabelle `orders` ist nicht leer (Tipp: `mustBeGreaterThan: 0`)
- deine eigenen Ideen

Für Regeln, die sich (noch) nicht automatisieren lassen, nimm einen Text-Check:

```yaml
quality:
  - type: text
    description: Order total is in cents
  - type: sql
    description: Ensure that ...
    query: SELECT COUNT(*) FROM ... WHERE ...;
    mustBe: 0
```

Alle Optionen: [ODCS-Referenz zur Datenqualität](https://bitol-io.github.io/open-data-contract-standard/latest/data-quality/#sql).

## Kontext für KI

KI-Agenten, LLM-Chats und BI-Assistenten fragen Daten zunehmend selbst ab.
Sie lesen das Schema, wissen aber nicht, dass `order_total` in Cent ist, welche Fragen eine geprüfte Antwort haben oder was sie nie tun dürfen.
Genau dafür gibt es in ODCS 3.2 den Block `context`.

> **Der context-Block (ODCS 3.2)**
>
> `context` gibt es auf Kontrakt-Ebene (der Datensatz als Ganzes) und an jedem Schema-Objekt (eine Tabelle). Er hat drei Teile:
>
> - **instructions**: Anleitung in natürlicher Sprache, wie ein System-Prompt für genau diese Daten.
> - **verifiedStatements**: zentrale fachliche Fragen. Mit `answer` sollen Agenten die kuratierte Antwort liefern, wenn eine Frage ähnlich ist. Ohne Antwort sind es Beispielfragen, die den Agenten vorbereiten.
> - **constraints**: Verbote. Was Agenten **nicht** tun dürfen, z. B. personenbezogene Daten ausgeben.

### Kontext für KI-Agenten ergänzen

Öffne im Editor links **Context** (oder bearbeite das YAML) und ergänze:

- **Instructions**: was die Daten sind, Einheiten und Zeitzone.
- **Verified statements**: eine Frage mit einer SQL-Antwort, die du selbst geprüft hast.
- **Constraints**: niemals personenbezogene Daten ausgeben.

```yaml
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']
```

![Der Bereich Context: Instructions, Verified Statements und Constraints](https://learn.datacontract.com/screenshots/editor-context.webp)

Prüfe deine verifizierte Antwort, bevor du sie veröffentlichst. Führe die Abfrage aus:

macOS / Linux:

```bash
docker compose exec postgres psql -U workshop -d workshop -c "SELECT COUNT(*) FROM orders_v1.orders WHERE EXTRACT(YEAR FROM order_timestamp) = 2023;"
```

Windows (PowerShell):

```powershell
docker compose exec postgres psql -U workshop -d workshop -c "SELECT COUNT(*) FROM orders_v1.orders WHERE EXTRACT(YEAR FROM order_timestamp) = 2023;"
```

Output:

```text
 count
-------
   876
(1 row)
```

### Kontext an der Tabelle orders ergänzen

Kontext an einem Schema-Objekt erklärt, wie man genau diese Tabelle nutzt: Granularität, Joins, Filter.
Ergänze unter **Schemas → orders**:

```yaml
schema:
  - name: orders
    context:
      instructions: One row per order. Join line_items on order_id to get the purchased SKUs.
    # ...
```

Mach dasselbe für `line_items`, z. B. „One row per purchased SKU in an order.“

### Synonyme ergänzen

Menschen suchen mit ihren eigenen Worten: „purchases“, „Bestellungen“, „Artikelnummer“. ODCS 3.2 hält sie als `synonyms` an Schema-Objekten und Properties fest, damit Kataloge und Agenten die richtige Tabelle finden.
Ergänze Synonyme im Editor oder im YAML. Die `locale` (ein BCP-47-Tag) markiert ein Synonym in einer anderen Sprache:

```yaml
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: de
```

Validiere den Kontrakt gegen das Schema von ODCS 3.2:

macOS / Linux:

```bash
datacontract lint orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract lint orders_v1.odcs.yaml
```

Output:

```text
╭────────┬────────────────────────────────────────────┬───────┬─────────╮
│ 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.
```

![Synonyme und Kontext auf Schema-Ebene unter Advanced Metadata](https://learn.datacontract.com/screenshots/editor-synonyms.webp)

### Optional: frag deinen KI-Agenten

Starte deinen KI-Coding-Agenten (Claude Code, Codex, Copilot, ...) im Repository und frag:

> 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.

Beobachte, was er tut:

- Nutzt er die verifizierte Antwort? (Erwartetes Ergebnis: **876**.)
- Verweigert er die E-Mail-Adressen wegen des Verbots?

**Quick check:** Ein KI-Agent wird gefragt: „Wie viele Bestellungen gab es 2023?“ Der Kontrakt hat ein verifiziertes Statement mit dieser Frage und einer SQL-Antwort. Was sollte der Agent tun?

- Eigenes SQL schreiben, er kennt ja das Schema
- Die kuratierte Antwort des verifizierten Statements wiederverwenden (correct)
- Ablehnen, weil der Kontrakt Constraints hat
- Das Statement nur nutzen, wenn die Frage wortgleich ist

Verifizierte Statements mit `answer` sind kuratierte Antworten, die Agenten bei sinngemäß ähnlichen Fragen wiederverwenden sollen. Statements ohne Antwort dienen nur als Beispielfragen.

## Ownership und Service Levels

### Team und Support ergänzen

Konsumenten müssen wissen, wem die Daten gehören und wo sie Hilfe bekommen. Ergänze diese Keys auf **oberster Ebene**:

```yaml
team:
  name: order_data_team
  members:
    - username: owner@example.com
      role: Owner

support:
  - channel: "#order-data-help"
    url: https://example.slack.com/archives/order-data-help
    tool: slack
```

### Service Levels ergänzen

Wie lange werden die Daten aufbewahrt, und wie aktuell sind sie? Ergänze auf oberster Ebene `slaProperties`:

```yaml
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 daily
```

> **Welche Service Levels werden getestet?**
>
> Die CLI macht aus zwei SLA-Eigenschaften Checks, wenn sie in `element` eine Zeitstempel-Spalte nennen (`schema.property`):
>
> - `retention`: Der **älteste** Wert (MIN) muss jünger als der Zeitraum sein. Alte Daten werden also wirklich gelöscht.
> - `freshness`: Der **neueste** Wert (MAX) muss jünger als die Schwelle sein. Neue Daten kommen also weiterhin an.
>
> Andere Eigenschaften wie `frequency` oder `latency` sind reine Dokumentation. Achtung: `unit: m` heißt bei freshness Minuten, bei retention Monate. Mehr in der [Doku zu Service Levels](https://docs.datacontract.com/service-levels).

Führe nur die Service-Level-Checks aus:

macOS / Linux:

```bash
datacontract test --checks slaProperties orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test --checks slaProperties orders_v1.odcs.yaml
```

Output:

```text
Testing 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.
```

### Einen Freshness-Check ausprobieren

Ergänze ein `freshness`-Service-Level: Neue Bestellungen sollen innerhalb von 24 Stunden ankommen.

```yaml
slaProperties:
  - property: freshness
    value: 24
    unit: h
    element: orders.order_timestamp
    description: New orders arrive within 24 hours
  # ... retention and frequency as before
```

Führe die Service-Level-Checks erneut aus:

macOS / Linux:

```bash
datacontract test --checks slaProperties orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test --checks slaProperties orders_v1.odcs.yaml
```

Output:

```text
Testing 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 
86400s
```

Der Check schlägt fehl: Die neueste Bestellung ist vom September 2025. Die Workshop-Daten sind ein fester Stand, also nie aktuell. In Produktion meldet genau dieser Check, wenn eine Pipeline keine Daten mehr lädt.
**Entferne den `freshness`-Eintrag wieder**, damit dein Kontrakt (und später deine CI-Pipeline) grün bleibt.

### Kontrakt auf active setzen

Dein Kontrakt ist fertig. Setze den `status` auf `active`: Ab jetzt können sich Konsumenten darauf verlassen.
Führe die Tests ein letztes Mal aus:

macOS / Linux:

```bash
datacontract test orders_v1.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v1.odcs.yaml
```

Output:

```text
Testing 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.
```

**Solution: orders_v1.odcs.yaml**

```yaml title=orders_v1.odcs.yaml
version: 1.0.0
kind: DataContract
apiVersion: v3.2.0
id: orders_v1
name: Orders
status: active
domain: ecommerce
description:
  purpose: Order data for analytics and reporting
  limitations: Contains PII (customer email addresses)
tags: ['orders', 'ecommerce']
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']
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: orders_v1
schema:
- name: orders
  synonyms:
  - synonym: purchases
  - synonym: Bestellungen
    locale: de
  context:
    instructions: One row per order. Join line_items on order_id to get the purchased SKUs.
  physicalType: table
  logicalType: object
  physicalName: orders
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
    quality:
    - type: text
      description: Must be a valid UUID
    - type: sql
      description: Ensure order_id is unique
      query: SELECT COUNT(*) FROM (SELECT order_id FROM orders_v1.orders GROUP BY order_id HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: order_timestamp
    physicalType: timestamptz
    logicalType: date
    quality:
    - type: text
      description: Must not be in the future
    - type: sql
      description: Ensure order_timestamp is not in the future
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE order_timestamp > CURRENT_TIMESTAMP;
      mustBe: 0
  - name: order_total
    physicalType: bigint
    logicalType: integer
    quality:
    - type: text
      description: Order total in cents, must be non-negative
    - type: sql
      description: Ensure order_total is non-negative
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE order_total < 0;
      mustBe: 0
  - name: customer_id
    physicalType: text
    logicalType: string
    quality:
    - type: text
      description: Must be a non-empty alphanumeric identifier
    - type: sql
      description: Ensure customer_id is not empty
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_id IS NULL OR customer_id = '';
      mustBe: 0
  - name: customer_email_address
    physicalType: text
    logicalType: string
    required: true
    classification: confidential
    examples: ['test394@example.org', 'test4757@example.org']
    quality:
    - type: text
      description: Must be a valid email address
    - type: sql
      description: Ensure email addresses contain @
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_email_address NOT LIKE '%@%';
      mustBe: 0
  quality:
  - type: text
    description: Orders table must not be empty
  - type: sql
    description: Ensure orders table has data
    query: SELECT COUNT(*) FROM orders_v1.orders;
    mustBeGreaterThan: 0
  - type: sql
    description: Ensure every order has at least one line item
    query: SELECT COUNT(*) FROM orders_v1.orders o LEFT JOIN orders_v1.line_items li ON o.order_id = li.order_id WHERE li.order_id IS NULL;
    mustBe: 0
- name: line_items
  synonyms:
  - synonym: order lines
  - synonym: Bestellpositionen
    locale: de
  context:
    instructions: One row per purchased SKU in an order.
  physicalType: table
  logicalType: object
  physicalName: line_items
  properties:
  - name: lines_item_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
    quality:
    - type: text
      description: Must be a valid UUID
    - type: sql
      description: Ensure lines_item_id is unique
      query: SELECT COUNT(*) FROM (SELECT lines_item_id FROM orders_v1.line_items GROUP BY lines_item_id HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    relationships:
    - type: foreignKey
      to: orders.order_id
  - name: sku
    synonyms:
    - synonym: article number
    - synonym: Artikelnummer
      locale: de
    physicalType: text
    logicalType: string
    quality:
    - type: text
      description: Must be a non-empty product SKU
    - type: sql
      description: Ensure sku is not empty
      query: SELECT COUNT(*) FROM orders_v1.line_items WHERE sku IS NULL OR sku = '';
      mustBe: 0
  quality:
  - type: text
    description: Line items table must not be empty
  - type: sql
    description: Ensure line_items table has data
    query: SELECT COUNT(*) FROM orders_v1.line_items;
    mustBeGreaterThan: 0
  - type: sql
    description: Ensure every line_items.order_id exists in orders
    query: SELECT COUNT(*) FROM orders_v1.line_items li LEFT JOIN orders_v1.orders o ON li.order_id = o.order_id WHERE o.order_id IS NULL;
    mustBe: 0
team:
  name: order_data_team
  members:
  - username: owner@example.com
    role: Owner
support:
- channel: "#order-data-help"
  url: https://example.slack.com/archives/order-data-help
  tool: slack
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 daily
```

**Quick check:** Du hast customer_email_address als required markiert. Was passiert bei datacontract test?

- Nichts, required ist nur Dokumentation
- Es prüft, dass die Spalte keine fehlenden Werte hat (correct)
- Es legt einen NOT-NULL-Constraint in der Datenbank an
- Der Test schlägt fehl, bis die Datenbank einen NOT-NULL-Constraint hat

`required: true` wird zu einem Check „keine fehlenden Werte“ auf den echten Daten. Die CLI ändert deine Datenbank nie, sie liest nur.

## Bonus

- SQL-DDL erzeugen: `datacontract export sql orders_v1.odcs.yaml` (alle [Export-Formate](https://cli.datacontract.com/#export))
- Gegen das ODCS-Schema prüfen: `datacontract lint orders_v1.odcs.yaml`
- HTML-Katalog aller Kontrakte im Ordner erzeugen: `datacontract catalog`
