# Контракты моделей

## Связанная документация

* [`contract`](https://docs.getdbt.tech/reference/resource-configs/contract.md)
* [`columns`](https://docs.getdbt.tech/reference/resource-properties/columns.md)
* [`constraints`](https://docs.getdbt.tech/reference/resource-properties/constraints.md)

## Зачем определять контракт?

Определение модели dbt так же просто, как написание SQL-запроса `select`. Ваш запрос естественным образом создает набор данных с именами и типами столбцов на основе выбранных вами столбцов и применяемых трансформаций.

Хотя это идеально подходит для быстрого и итеративного развития, для некоторых моделей постоянное изменение структуры возвращаемого набора данных представляет риск, когда другие люди и процессы запрашивают эту модель. Лучше определить набор предварительных "гарантий", которые определяют структуру вашей модели. Мы называем этот набор гарантий "контрактом". При построении вашей модели dbt проверит, что трансформация вашей модели создаст набор данных, соответствующий ее контракту, или не сможет построить модель.

## Где поддерживаются контракты?

#### Соображения

Есть несколько моментов, которые стоит учитывать при использовании возможностей управления моделями:

* Такие функции управления моделями, как доступ к моделям (model access), контракты (contracts) и версии (versions), повышают уровень доверия и стабильности в вашем проекте dbt. Однако из‑за добавления дополнительной структуры они могут усложнить откат изменений (например, при необходимости убрать ограничения доступа к модели) и увеличить затраты на сопровождение, если внедрить их слишком рано.<br />Перед тем как добавлять функции управления, стоит оценить, готов ли ваш проект dbt извлечь из них пользу. Внедрение управления на этапе, когда модели все еще активно меняются, может усложнить будущие изменения.

* Функции управления применяются только к моделям. Они не распространяются на другие типы ресурсов, включая snapshots, seeds или sources. Это связано с тем, что такие объекты могут со временем менять свою структуру (например, snapshots фиксируют изменяющиеся исторические данные) и плохо подходят для предоставления гарантий вроде контрактов, управления доступом или версионирования.

## Предварительные требования

**Контракты моделей поддерживаются в следующих файлах и моделях, а также на следующих платформах:**

* Файл `dbt_project.yml`

* Файл `properties.yml`

* SQL-модели

* Модели с одной из следующих материализаций:

  * `table`
  * `view` — представления поддерживают имена столбцов и типы данных, но не `constraints`
  * `incremental` — с `on_schema_change: append_new_columns` или `on_schema_change: fail`

* Некоторые платформы данных, однако поддерживаемые и [принудительно применяемые `constraints`](https://docs.getdbt.tech/reference/resource-properties/constraints.md) различаются в зависимости от платформы

**Контракты моделей *НЕ* поддерживаются в следующих случаях:**

* Python-модели
* SQL-модели с материализацией `materialized view` или `ephemeral`
* Пользовательские материализации (если их автор не добавил поддержку)
* Модели с рекурсивными CTE в BigQuery
* Другие типы ресурсов, такие как `sources`, `seeds`, `snapshots` и так далее

## Определение контракта

Допустим, у вас есть модель с запросом примерно такого вида:

models/marts/dim\_customers.sql

```sql
-- много SQL

final as (

    select
        customer_id,
        customer_name,
        -- ... и многое другое ...
    from ...

)

select * from final
```

Чтобы применить контракт модели, установите `enforced: true` в конфигурации `contract`.

Когда контракт применяется, он *должен* включать `name` и `data_type` каждого столбца (где `data_type` соответствует одному из тех, которые понимает ваша платформа данных).

Если ваша модель материализована как `table` или `incremental`, и в зависимости от вашей платформы данных, вы можете дополнительно указать [constraints](https://docs.getdbt.tech/reference/resource-properties/constraints.md), такие как `not_null` (содержащий нулевые значения).

models/marts/customers.yml

```yaml
models:
  - name: dim_customers
    config:
      contract:
        enforced: true
    columns:
      - name: customer_id
        data_type: int
        constraints:
          - type: not_null
      - name: customer_name
        data_type: string
      ...
```

При построении модели с определенным контрактом dbt будет делать две вещи по-другому:

1. dbt выполнит проверку "предварительного полета", чтобы убедиться, что запрос модели вернет набор столбцов с именами и типами данных, соответствующими тем, которые вы определили. Эта проверка не зависит от порядка столбцов, указанных в вашей модели (SQL) или спецификации YAML.
2. dbt включит имена столбцов, типы данных и ограничения в DDL-запросы, которые он отправляет на платформу данных, которые будут применяться при построении или обновлении таблицы модели, и упорядочит столбцы в соответствии с контрактом, а не вашей моделью dbt.

## Поддержка ограничений на платформах

Выберите вкладку нужного адаптера, чтобы узнать больше о поддержке [ограничений](https://docs.getdbt.tech/reference/resource-properties/constraints.md) на разных платформах. Ограничения делятся на три категории в зависимости от возможности их определения и обеспечения их соблюдения платформой:

* **Можно определить, соблюдение обеспечивается** — Модель не будет построена, если она нарушает ограничение.
* **Можно определить, соблюдение не обеспечивается** — Платформа позволяет указать тип ограничения, но модель всё равно может быть построена, даже если её построение нарушает это ограничение. Такое ограничение существует только в качестве метаданных. Этот подход более характерен для облачных хранилищ данных, чем для транзакционных баз данных, где чаще обеспечивается строгое соблюдение правил.
* **Нельзя определить, соблюдение не обеспечивается** — Указать этот тип ограничения для платформы невозможно.

### Redshift

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ✅               | ❌                        |
| check           | ❌               | ❌                        |

### Snowflake

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ✅               | ❌                        |
| check           | ❌               | ❌                        |

### BigQuery

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ❌               | ❌                        |
| check           | ❌               | ❌                        |

### Postgres

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ✅                        |
| foreign\_key    | ✅               | ✅                        |
| unique          | ✅               | ✅                        |
| check           | ✅               | ✅                        |

### Spark

В настоящее время соблюдение ограничений `not_null` и `check` обеспечивается только после построения модели. Из-за этого ограничения платформы dbt считает, что эти ограничения можно определить, но их соблюдение не обеспечивается. Это означает, что они не входят в *контракт модели*, поскольку их соблюдение невозможно обеспечить во время построения. Эта таблица будет меняться по мере развития функциональности.

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ❌                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ✅               | ❌                        |
| check           | ✅               | ❌                        |

### Databricks

В настоящее время соблюдение ограничений `not_null` и `check` обеспечивается только после построения модели. Из-за этого ограничения платформы dbt считает, что эти ограничения можно определить, но их соблюдение не обеспечивается. Это означает, что они не входят в *контракт модели*, поскольку их соблюдение невозможно обеспечить во время построения. Эта таблица будет меняться по мере развития функциональности.

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ❌               | ❌                        |
| check           | ✅               | ✅                        |

### Fabric

Microsoft Fabric сначала создаёт таблицу, а затем добавляет некоторые ограничения отдельными шагами. При построении модели проверяется только `not_null`. Ограничения первичного ключа, внешнего ключа и уникальности сохраняются в определении таблицы для справки, но Fabric не проверяет их при добавлении новых данных. Определяйте `primary_key`, `foreign_key` и `unique` на уровне модели. Адаптер игнорирует эти типы ограничений на уровне столбцов.

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ✅               | ✅                        |
| primary\_key    | ✅               | ❌                        |
| foreign\_key    | ✅               | ❌                        |
| unique          | ✅               | ❌                        |
| check           | ❌               | ❌                        |

### Athena

| Тип ограничения | Можно определить | Соблюдение обеспечивается |
| --------------- | ---------------- | ------------------------- |
| not\_null       | ❌               | ❌                        |
| primary\_key    | ❌               | ❌                        |
| foreign\_key    | ❌               | ❌                        |
| unique          | ❌               | ❌                        |
| check           | ❌               | ❌                        |

### Для каких моделей следует определять контракты?

 Для каких моделей следует определять контракты?

### Чем контракты отличаются от тестов?

 Чем контракты отличаются от тестов?

[Тесты данных](https://docs.getdbt.tech/docs/build/data-tests.md) являются более гибким механизмом для проверки содержимого вашей модели *после* ее построения. Пока вы можете написать запрос, вы можете запустить тест данных. Тесты данных более настраиваемы, например, с [пользовательскими порогами серьезности](https://docs.getdbt.tech/reference/resource-configs/severity.md). Их легче отлаживать после обнаружения сбоев, потому что вы можете запросить уже построенную модель или [сохранить записи с ошибками в хранилище данных](https://docs.getdbt.tech/reference/resource-configs/store_failures.md).

В некоторых случаях вы можете заменить тест данных его эквивалентным ограничением. Это имеет преимущество гарантии проверки во время построения и, вероятно, требует меньше вычислительных ресурсов (стоимости) на вашей платформе данных. Предпосылки для замены теста данных ограничением:

* Убедитесь, что ваша платформа данных может поддерживать и применять необходимое вам ограничение. Большинство платформ применяют только `not_null`.
* Материализация вашей модели как `table` или `incremental` (**не** `view` или `ephemeral`).
* Определение полного контракта для этой модели, указав `name` и `data_type` каждого столбца.

**Почему тесты не являются частью контракта?** В аналогии с программными API, структура ответа API является контрактом. Качество и надежность ("время безотказной работы") также являются очень важными атрибутами качества API, но они не являются частью контракта как такового. Когда контракт изменяется в обратной несовместимой манере, это является критическим изменением, требующим увеличения основной версии.

### Нужно ли определять каждый столбец для контракта?

 Нужно ли определять каждый столбец для контракта?

Да. Контракты dbt распространяются на *все* столбцы, определённые в модели, и требуют явно объявлять ожидания для *каждого* из этих столбцов. Явное объявление контракта — не случайность — именно в этом и заключается замысел этой функциональности.

При этом мы понимаем, что для моделей с большим количеством столбцов это может означать *очень много* YAML. Обсуждение возможных подходов к созданию и обновлению определений контрактов моделей см. в [dbt-labs/dbt#11764](https://github.com/dbt-labs/dbt/issues/11764).

 Как обрабатываются изменения, нарушающие совместимость?

При сравнении с предыдущим состоянием проекта dbt будет искать изменения, нарушающие совместимость, которые могут повлиять на потребителей данных ниже по цепочке зависимостей. Если такие изменения обнаружены, dbt сообщит об ошибке контракта.

К критическим изменениям относятся:

* Удаление существующего столбца
* Изменение типа данных существующего столбца
* Удаление или изменение одного из `constraints` на существующем столбце (dbt версии 1.6 или выше)
* Удаление модели с контрактом путем ее удаления, переименования или отключения (dbt v1.9 или выше).
  * модели с версией вызовут ошибку. модели без версии вызовут предупреждение.

Подробнее см. в [справочнике по контрактам](https://docs.getdbt.tech/docs/mesh/govern/model-contracts.md).

 Можно ли использовать контракты моделей с вложенными полями?

Да, но только в BigQuery, поскольку вложенные поля внутри [столбцов типа struct](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type) имеют строгую типизацию. Контракты моделей полезны, когда модель содержит вложенные данные JSON или данные событий, в которых изменение типа вложенного поля может нарушить работу нижестоящих моделей.

Вложенные поля (структуры) — это столбцы внутри других столбцов. Перечислите каждое внутреннее поле в YAML, используя имя с точечной нотацией, например `b.id` или `b.double_nested.another.even_more`. Для каждого внутреннего поля необходимо указать `name` и `data_type`. При необходимости можно также объявить сам столбец типа struct (в этом примере — `b` или `b.double_nested`), чтобы добавить описание или ограничение для структуры.

models/nested\_fields.yml

```yaml
models:
  - name: double_nested_fields
    config:
      contract:
        enforced: true
      materialized: table
    columns:
      - name: a
        data_type: string
      - name: b.id
        data_type: integer
      - name: b.name
        data_type: string
      - name: b.double_nested.id
        data_type: integer
      - name: b.double_nested.another.again
        data_type: string
      - name: b.double_nested.another.even_more
        data_type: integer
```

models/double\_nested\_fields.sql

```sql
select
  'string' as a,
  struct(
    1 as id,
    'name' as name,
    struct(2 as id, struct('test' as again, 2 as even_more) as another) as double_nested
  ) as b
```

Если значения `name` и `data_type` каждого вложенного поля соответствуют SQL, модель создаётся как обычно. Если они не совпадают, проверка контракта завершается ошибкой и модель не создаётся. Например, если в SQL поле `even_more` имеет строковый тип, а в контракте указан тип `integer`, dbt возвращает:

```text
This model has an enforced contract that failed.
Please ensure the name, data_type, and number of columns in your contract match the columns in your model's definition.

| column_name                       | definition_type | contract_type | mismatch_reason    |
| --------------------------------- | --------------- | ------------- | ------------------ |
| b.double_nested.another.even_more | STRING          | INT64         | data type mismatch |
```

Массивы структур также поддерживаются. Объявите родительский столбец с `data_type: array`, затем перечислите каждое внутреннее поле:

```yaml
- name: my_array
  data_type: array
- name: my_array.id
  data_type: integer
- name: my_array.name
  data_type: string
```

О том, как добавить ограничения, например `not_null`, для вложенных полей, см. в разделе [Ограничения](https://docs.getdbt.tech/reference/resource-properties/constraints.md).
