# Contract-first entwerfen

Ein abgeleitetes Datenprodukt entwerfen, bevor du eine Zeile SQL schreibst.

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

Das **Einkaufsteam** will wissen, wie oft jede SKU pro Jahr gekauft wird, um bessere Konditionen mit Lieferanten auszuhandeln.
Dafür baust du ein neues Datenprodukt auf Orders auf: **SKU Sales**.

Diesmal arbeitest du **contract-first**: Du entwirfst Datenkontrakt und Datenproduktbeschreibung, bevor du SQL schreibst.
Der Kontrakt ist die Spezifikation. Implementiert wird er in der [nächsten Übung](https://learn.datacontract.com/de/implement/).

**You will learn:**
- Einen Datenkontrakt für ein Datenprodukt entwerfen, das es noch nicht gibt
- Die Semantik einer View als Quality Checks ausdrücken
- Measures und Dimensionen markieren und KI-Agenten Kontext geben (ODCS 3.2)
- Ein consumer-aligned Datenprodukt mit Input- und Output-Ports beschreiben
- Verstehen, warum fehlschlagende Tests der erwartete Startpunkt sind

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

> **Contract-first**
>
> Die Schnittstelle kommt vor der Implementierung, wie eine OpenAPI-Spezifikation vor der API.
> Consumer prüfen den Kontrakt, *bevor* Arbeit investiert wird, und seine Tests zeigen dir, wann die Implementierung fertig ist: **rot → grün**.

## Den Kontrakt entwerfen

### Kontraktdatei anlegen

Lege einen neuen Kontrakt an und öffne ihn im Editor. Bestätige, wenn die CLI fragt, ob die Datei angelegt werden soll.

macOS / Linux:

```bash
datacontract edit sku_sales_per_year.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract edit sku_sales_per_year.odcs.yaml
```

Output:

```text
File 'sku_sales_per_year.odcs.yaml' does not exist. Initialize a new data contract? [y/N]: y
📄 data contract written to sku_sales_per_year.odcs.yaml
Editing: /Users/you/learn.datacontract.com/sku_sales_per_year.odcs.yaml
Data Contract Editor running at http://localhost:4243
Press Ctrl+C to stop
```

Setze die Grunddaten:

| Feld | Wert |
|---|---|
| Name | `SKU Sales per Year` |
| ID | `sku_sales_per_year` |
| Version | `1.0.0` |
| Status | `draft` |

### Server hinzufügen

Die View liegt in einem neuen Schema `analytics` in derselben Datenbank. Füge einen **Server** hinzu:

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

### Schema beschreiben

Füge ein **Schema** `sku_sales_per_year` hinzu, setze **Advanced Metadata → Physical Type** auf `VIEW` und lege diese Properties an:

| Property | Logical Type | Physical Type | Bedeutung |
|---|---|---|---|
| `sku` | `string` | `TEXT` | Die Produkt-SKU |
| `year` | `integer` | `INTEGER` | Jahr der Bestellung |
| `order_count` | `integer` | `BIGINT` | In wie vielen Bestellungen die SKU vorkam |
| `total_quantity` | `integer` | `BIGINT` | Insgesamt gekaufte Stückzahl |

Schreib die Bedeutung in die **Description** jeder Property. Genau das liest das Einkaufsteam.

### Semantik als Quality Checks festhalten

Das Schema sagt, *welche* Spalten es gibt. Quality Checks sagen, was über sie *wahr* sein muss. Ergänze Checks wie:

- Die Kombination aus `sku` und `year` ist eindeutig
- `total_quantity` ist nie kleiner als `order_count`
- Die View ist nicht leer

Nutze „fehlerhafte Zeilen zählen, null erwarten“, wie in [Übung 1](https://learn.datacontract.com/de/contract/):

```yaml
quality:
  - type: sql
    description: Ensure total_quantity is at least order_count
    query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE total_quantity < order_count;
    mustBe: 0
```

> **Tip**
>
> Eindeutigkeit über zwei Spalten: `GROUP BY ... HAVING COUNT(*) > 1` in einer Subquery. Checks über mehrere Spalten gehören auf Schema-Ebene (`schema[].quality`), nicht an eine einzelne Property.

### Measures und Dimensionen markieren

Mit ODCS 3.2 legst du per `semanticType` fest, welche Rolle eine Property spielt.
Öffne im Editor unter **Schemas** eine Property und setze **Semantic Type** und **Transform Logic**, oder bearbeite das YAML:

```yaml
properties:
  - name: sku
    semanticType: dimension
  - name: year
    semanticType: dimension
  - name: order_count
    semanticType: measure
    transformLogic: COUNT(*)
  - name: total_quantity
    semanticType: measure
    transformLogic: SUM(line_items.quantity)
```

![Semantic Type im Property-Editor (Diagrammansicht, Klick auf eine Property)](https://learn.datacontract.com/screenshots/editor-semantic-type.webp)

> **Measures und Dimensionen**
>
> Eine **Dimension** ist ein Merkmal zum Gruppieren und Filtern (`sku`, `year`).
> Ein **Measure** ist ein aggregierter Wert (`order_count`, `total_quantity`); `transformLogic` sagt, wie er berechnet wird.
> Semantic Layer, BI-Tools und KI-Agenten nutzen diese Rollen für korrekte Abfragen, etwa um Measures zu summieren, aber nie ein Jahr.

### Kontext für KI-Agenten hinzufügen

Analysten werden KI-Assistenten zu diesem Datenprodukt befragen. Sag ihnen mit einem `context`-Block, wie es zu nutzen ist (neu in ODCS 3.2, siehe [Übung 1](https://learn.datacontract.com/de/contract/)).
Öffne im Editor **Context** in der Navigation:

```yaml
context:
  instructions: >-
    One row per SKU and year. Sum order_count or total_quantity across years for totals.
  verifiedStatements:
    - question: Which three SKUs sold the most units in 2024?
      answer: SELECT sku, total_quantity FROM analytics.sku_sales_per_year WHERE year = 2024 ORDER BY total_quantity DESC LIMIT 3;
```

![Kontext des SKU-Sales-Kontrakts](https://learn.datacontract.com/screenshots/editor-sku-context.webp)

Das Verified Statement ist eine Frage mit geprüfter Antwort. Du kontrollierst sie, sobald die View existiert.

### Tests ausführen und scheitern sehen

Speichere und führe die Tests aus:

macOS / Linux:

```bash
datacontract test sku_sales_per_year.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test sku_sales_per_year.odcs.yaml
```

Output:

```text
Testing sku_sales_per_year.odcs.yaml
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, schema=analytics)
╭────────┬────────────────────────────────────┬────────────────┬───────────────────────────────────╮
│ Result │ Check                              │ Field          │ Details                           │
├────────┼────────────────────────────────────┼────────────────┼───────────────────────────────────┤
│ failed │ Ensure the view has data           │                │ Could not read model              │
│        │                                    │                │ 'sku_sales_per_year':             │
│        │                                    │                │ sku_sales_per_year                │
│ failed │ Check that field 'order_count' is  │ order_count    │ Could not read model              │
│        │ present                            │                │ 'sku_sales_per_year':             │
│        │                                    │                │ sku_sales_per_year                │
…
╰────────┴────────────────────────────────────┴────────────────┴───────────────────────────────────╯
🔴 data contract is invalid, found the following errors:
1) 15 checks on sku, year, order_count, total_quantity: Could not read model 'sku_sales_per_year': 
sku_sales_per_year
```

Die Tests **schlagen fehl**, natürlich: Es ist noch nichts implementiert. Genau darum geht es. Das Einkaufsteam kann die Schnittstelle schon prüfen, während du die Tests in der nächsten Übung grün machst.

**Solution: sku_sales_per_year.odcs.yaml**

```yaml title=sku_sales_per_year.odcs.yaml
version: 1.0.0
kind: DataContract
apiVersion: v3.2.0
id: sku_sales_per_year
name: SKU Sales per Year
status: draft
domain: ecommerce
description:
  purpose: Shows how often each SKU is bought, grouped by year. Built for the purchasing team to support supplier negotiations.
  limitations: Aggregated data only, no PII. Based on the orders data product.
tags: ['sku', 'sales', 'analytics']
context:
  instructions: >-
    One row per SKU and year. Sum order_count or total_quantity across years for totals.
  verifiedStatements:
  - question: Which three SKUs sold the most units in 2024?
    answer: SELECT sku, total_quantity FROM analytics.sku_sales_per_year WHERE year = 2024 ORDER BY total_quantity DESC LIMIT 3;
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: analytics
schema:
- name: sku_sales_per_year
  physicalType: view
  logicalType: object
  physicalName: sku_sales_per_year
  properties:
  - name: sku
    semanticType: dimension
    physicalType: text
    logicalType: string
    required: true
    quality:
    - type: sql
      description: Ensure sku and year combination is unique
      query: SELECT COUNT(*) FROM (SELECT sku, year FROM analytics.sku_sales_per_year GROUP BY sku, year HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: year
    semanticType: dimension
    physicalType: integer
    logicalType: integer
    required: true
    quality:
    - type: sql
      description: Ensure year is plausible
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE year < 2020 OR year > 2100;
      mustBe: 0
  - name: order_count
    semanticType: measure
    transformLogic: COUNT(*)
    physicalType: bigint
    logicalType: integer
    quality:
    - type: text
      description: Number of orders containing the SKU in that year
    - type: sql
      description: Ensure order_count is positive
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE order_count <= 0;
      mustBe: 0
  - name: total_quantity
    semanticType: measure
    transformLogic: SUM(line_items.quantity)
    physicalType: bigint
    logicalType: integer
    quality:
    - type: text
      description: Total units bought, never less than the number of orders
    - type: sql
      description: Ensure total_quantity is at least order_count
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE total_quantity < order_count;
      mustBe: 0
  quality:
  - type: sql
    description: Ensure the view has data
    query: SELECT COUNT(*) FROM analytics.sku_sales_per_year;
    mustBeGreaterThan: 0
team:
  name: purchasing_analytics_team
  members:
  - username: purchasing@example.com
    role: Owner
support:
- channel: "#purchasing-analytics"
  url: https://example.slack.com/archives/purchasing-analytics
  tool: slack
```

## Das Datenprodukt beschreiben

> **Source-aligned vs. consumer-aligned**
>
> **Orders** ist *source-aligned*: Es stellt Daten nah am System bereit, in dem sie entstehen.
> **SKU Sales** ist *consumer-aligned*: gebaut für einen bestimmten Anwendungsfall eines bestimmten Consumers.
> Ein consumer-aligned Produkt deklariert seine Datenquellen über **Input-Ports**, jeder verweist auf den Kontrakt, auf den es sich verlässt.

### Datenprodukt-Datei anlegen

Lege `sku_sales_per_year.odps.yaml` an, mit derselben Struktur wie in [Übung 3](https://learn.datacontract.com/de/data-product/):

| Feld | Wert |
|---|---|
| `id` | `sku_sales` |
| `name` | `SKU Sales` |
| `version` | `1.0.0` |
| `status` | `draft` |
| `domain` | `ecommerce` |

Ergänze eine `description` mit `purpose` sowie `team` und `support` für das Purchasing-Analytics-Team (z. B. `purchasing_analytics_team`).

### Output-Port hinzufügen

Das Produkt bietet einen Output-Port an, beschrieben durch deinen neuen Kontrakt:

```yaml
outputPorts:
  - name: sku_sales_per_year
    description: Aggregated SKU sales per year as a PostgreSQL view
    version: 1.0.0
    contractId: sku_sales_per_year
```

### Input-Port hinzufügen

Deklariere, auf welchen Daten (und Garantien) dein Produkt aufbaut. Du konsumierst `orders_v2`, denn nur v2 hat `quantity`:

```yaml
inputPorts:
  - name: orders
    version: 2.0.0
    contractId: orders_v2
```

### Datenprodukt prüfen

Validiere gegen den ODPS-Standard:

macOS / Linux:

```bash
dataproduct lint sku_sales_per_year.odps.yaml
```

Windows (PowerShell):

```powershell
dataproduct lint sku_sales_per_year.odps.yaml
```

Output:

```text
✅ Data product is valid against ODPS v1.1.0
🟢 Data product is valid.
```

**Solution: sku_sales_per_year.odps.yaml**

```yaml title=sku_sales_per_year.odps.yaml
apiVersion: v1.1.0
kind: DataProduct
id: sku_sales
name: SKU Sales
version: 1.0.0
status: draft
type: consumerAligned
domain: ecommerce
description:
  purpose: Consumer-aligned data product showing how often each SKU is bought per year, used by the purchasing team for supplier negotiations.
tags: ['sku', 'sales', 'purchasing']
inputPorts:
- name: orders
  version: 2.0.0
  contractId: orders_v2
outputPorts:
- name: sku_sales_per_year
  description: Aggregated SKU sales per year as a PostgreSQL view
  version: 1.0.0
  contractId: sku_sales_per_year
team:
  name: purchasing_analytics_team
  members:
  - username: purchasing@example.com
    role: Owner
support:
- channel: "#purchasing-analytics"
  url: https://example.slack.com/archives/purchasing-analytics
  tool: slack
```

**Quick check:** Du hast gerade den Kontrakt sku_sales_per_year entworfen, und datacontract test schlägt fehl. Was sagt dir das?

- Der Kontrakt ist falsch und muss korrigiert werden
- Es ist noch nichts implementiert: Die roten Tests sind deine To-do-Liste (correct)
- Die Datenbank läuft nicht
- Du solltest die Quality Checks lockern, bis die Tests grün sind

Genau. Bei Contract-first sind rote Tests der erwartete Startpunkt. Sie werden grün, sobald die Implementierung die Spezifikation erfüllt.
