# Consumer-Driven Contracts

Make your dependencies explicit with a consumer-driven contract.

Source: https://learn.datacontract.com/en/consumer-driven/

Your view from the [previous exercise](https://learn.datacontract.com/en/implement/) reads the producer's tables directly.
It implicitly depends on the *whole* `orders_v2` contract, but needs only five fields.
The orders team can't tell whether a change breaks you.

Make the dependency explicit: a **consumer-driven contract** for exactly the fields you need, and views that expose only those fields.

**You will learn:**
- Understand consumer-driven data contracts
- Write a contract with only what a consumer uses
- Decouple your data product from the producer's tables with access views
- Verify that your own consumers are unaffected

> **Consumer-driven contracts**
>
> The **consumer** states which subset of the data it needs and which quality it expects.
> The idea comes from API testing (e.g. Pact): the producer runs the contracts of all its consumers in its own pipeline.
> So the producer knows which fields are in use and can change everything else freely.

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

## Define what you need

### Copy the producer's contract

Copy the `orders_v2` contract and open the copy in the editor:

macOS / Linux:

```bash
cp orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml
datacontract edit orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
Copy-Item orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml
datacontract edit orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Editing: /Users/you/learn.datacontract.com/orders_v2.consumer_sku_sales.odcs.yaml
Data Contract Editor running at http://localhost:4243
Press Ctrl+C to stop
```

Set the **ID** to `orders_v2_consumer_sku_sales`. Keep the **Version** at `2.0.0`, as it is based on the v2 data.

### Strip it down to what you use

Remove all properties your view doesn't use, and the quality checks on them. Keep only:

- `orders`: `order_id`, `order_timestamp`
- `line_items`: `order_id`, `sku`, `quantity`

Keep the `quantity > 0` check. Your data product relies on it: otherwise `total_quantity` could be smaller than `order_count`.

The copy also carries the producer's ODCS 3.2 `context` and `synonyms`. Remove the contract-level `context`: its verified statements query `order_total` and the producer's tables, and its constraint is about a field you dropped. Schema-level context and synonyms on kept fields may stay.

### Make it your contract

This is *your* contract now, not the orders team's:

- set `team` and `support` to the purchasing analytics team (as in your `sku_sales_per_year` contract),
- rewrite `description.purpose`, e.g. "The fields the SKU Sales data product actually needs from orders_v2".

### Point it to the access views

Your access views will live in a new schema `sku_sales_input`:

- change the server's **Schema** to `sku_sales_input`,
- set the `physicalType` of both schema objects to `VIEW`,
- if the `quantity > 0` check is a SQL query, change `orders_v2.` to `sku_sales_input.` in it. Otherwise it still tests the old tables.

### Run the tests: red again

macOS / Linux:

```bash
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Testing orders_v2.consumer_sku_sales.odcs.yaml
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, 
schema=sku_sales_input)
╭────────┬───────────────────────────────┬────────────────────────┬────────────────────────────────╮
│ Result │ Check                         │ Field                  │ Details                        │
├────────┼───────────────────────────────┼────────────────────────┼────────────────────────────────┤
│ failed │ Check that field 'order_id'   │ line_items.order_id    │ Could not read model           │
│        │ is present                    │                        │ 'line_items': line_items       │
…
🔴 data contract is invalid, found the following errors:
1) 7 checks on orders.order_id, orders.order_timestamp: Could not read model 'orders': orders
2) 9 checks on line_items.order_id, line_items.sku, line_items.quantity: Could not read model 
'line_items': line_items
```

They fail, because the views don't exist yet. Contract-first, once more.

**Solution: orders_v2.consumer_sku_sales.odcs.yaml**

```yaml title=orders_v2.consumer_sku_sales.odcs.yaml
version: 2.0.0
kind: DataContract
apiVersion: v3.2.0
id: orders_v2_consumer_sku_sales
name: Orders (SKU Sales)
status: active
domain: ecommerce
description:
  purpose: Consumer-driven contract - the fields the SKU Sales data product actually needs from orders_v2
tags: ['orders', 'sku', 'consumer-driven']
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: sku_sales_input
schema:
- name: orders
  physicalType: view
  logicalType: object
  physicalName: orders
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
  - name: order_timestamp
    physicalType: timestamptz
    logicalType: date
    required: true
- name: line_items
  physicalType: view
  logicalType: object
  physicalName: line_items
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
  - name: sku
    physicalType: text
    logicalType: string
    required: true
  - name: quantity
    physicalType: bigint
    logicalType: integer
    quality:
    - type: sql
      description: Ensure quantity is positive
      query: SELECT COUNT(*) FROM sku_sales_input.line_items WHERE quantity <= 0;
      mustBe: 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
```

## Create the access views

### Create the views

Create `sql/sku_sales_input.sql` with views that expose only the contracted fields:

```sql title=sku_sales_input.sql
CREATE SCHEMA IF NOT EXISTS sku_sales_input;

CREATE OR REPLACE VIEW sku_sales_input.orders AS
SELECT order_id, order_timestamp FROM orders_v2.orders;

CREATE OR REPLACE VIEW sku_sales_input.line_items AS
SELECT order_id, sku, quantity FROM orders_v2.line_items;
```

Apply it:

macOS / Linux:

```bash
docker compose exec -T postgres psql -U workshop -d workshop < sql/sku_sales_input.sql
```

Windows (PowerShell):

```powershell
Get-Content sql/sku_sales_input.sql | docker compose exec -T postgres psql -U workshop -d workshop
```

Output:

```text
CREATE SCHEMA
CREATE VIEW
CREATE VIEW
```

### Run the tests: green

macOS / Linux:

```bash
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Testing orders_v2.consumer_sku_sales.odcs.yaml
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, 
schema=sku_sales_input)
╭────────┬──────────────────────────────────────────────────────┬────────────────────────┬─────────╮
│ Result │ Check                                                │ Field                  │ Details │
├────────┼──────────────────────────────────────────────────────┼────────────────────────┼─────────┤
│ 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_id has no missing values      │ line_items.order_id    │         │
│ passed │ Check that field 'quantity' is present               │ line_items.quantity    │         │
│ passed │ Check that field quantity has physical type bigint   │ line_items.quantity    │         │
│ passed │ Ensure quantity is positive                          │ line_items.quantity    │         │
…
│ passed │ Check that field 'order_timestamp' is present        │ orders.order_timestamp │         │
╰────────┴──────────────────────────────────────────────────────┴────────────────────────┴─────────╯
🟢 data contract is valid. Run 16 checks. Took 0.545395 seconds.
```

## Rebase your data product

### Read from the access views

Change `sql/sku_sales_per_year.sql` to select from `sku_sales_input.orders` and `sku_sales_input.line_items` instead of the `orders_v2` tables. Re-apply it:

macOS / Linux:

```bash
docker compose exec -T postgres psql -U workshop -d workshop < sql/sku_sales_per_year.sql
```

Windows (PowerShell):

```powershell
Get-Content sql/sku_sales_per_year.sql | docker compose exec -T postgres psql -U workshop -d workshop
```

Output:

```text
NOTICE:  schema "analytics" already exists, skipping
CREATE SCHEMA
CREATE VIEW
```

> **Note**
>
> `sql/sku_sales_per_year.sql` now depends on `sql/sku_sales_input.sql`, so apply the input views first. The CI pipeline in [Part C](https://learn.datacontract.com/en/ci-cd/) does exactly that.

**Solution: sql/sku_sales_per_year.sql**

```sql title=sku_sales_per_year.sql
-- Rebased onto the consumer-driven input views (exercise 6)
CREATE SCHEMA IF NOT EXISTS analytics;

CREATE OR REPLACE VIEW analytics.sku_sales_per_year AS
SELECT
    li.sku,
    EXTRACT(YEAR FROM o.order_timestamp)::int AS year,
    COUNT(*)::bigint                          AS order_count,
    SUM(li.quantity)::bigint                  AS total_quantity
FROM sku_sales_input.line_items li
JOIN sku_sales_input.orders o ON li.order_id = o.order_id
GROUP BY li.sku, EXTRACT(YEAR FROM o.order_timestamp);
```

### Verify your consumers are unaffected

The output of your data product must not change. Your own contract proves it:

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
…
│ passed │ Ensure year is plausible                                 │ year           │         │
╰────────┴──────────────────────────────────────────────────────────┴────────────────┴─────────╯
🟢 data contract is valid. Run 15 checks. Took 0.606872 seconds.
```

Your data product now touches only the fields in your consumer-driven contract.
Everything else in `orders_v2` may change without breaking you.
Even better: the orders team can run *your* contract in *their* CI pipeline and catch a breaking change before it ships. You build such a pipeline in the [next exercise](https://learn.datacontract.com/en/ci-cd/).

**Quick check:** The orders team wants to drop customer_email_address from orders_v2. Does that break the SKU Sales data product?

- Yes, every change to orders_v2 is breaking
- No, it is not in the consumer-driven contract, and the access views don't select it (correct)
- Only if the access views use SELECT *
- We can't know until it is deployed

Right. The consumer-driven contract documents that SKU Sales needs only five fields. Running it in the producer's pipeline proves that dropping `customer_email_address` is safe for this consumer.

## Bonus

- Compare the producer's and the consumer's contract: `datacontract changelog orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml`
- Point the input port in `sku_sales_per_year.odps.yaml` to `orders_v2_consumer_sku_sales`. Should the input port reference the producer's contract or yours? Both have good arguments.
