Topic: Part 12. Data Contracts: ODCS and dbt Contract Checks
Difficulty level: Medium
Estimated study time: 3-4 hours (theory ~1.5 hours, practice ~2 hours, case studies ~0.5 hours)
Prerequisites: Confident command of SQL (SELECT, JOIN, GROUP BY, window functions)
Basic understanding of dbt (models, materializations, sources, refs)
Knowledge of data mart design principles (grain, slowly changing dimensions)
Understanding of the software development lifecycle (PR, review, CI/CD)
Basic familiarity with YAML syntax
Concept of data quality and data testing
Learning objectives: Distinguish two levels of data contract: product-level (ODCS) and model-level (dbt schema.yml) and understand what each one is responsible for
Identify three types of contract drift (schema, semantic, process) and articulate the consequences for the consumer
Compose a minimum contract impact note when modifying a model using a given template
Link ODCS obligations to specific dbt tests, singular tests, and manual review facts, evaluating coverage
Formulate a prompt for an LLM assistant (e.g., Qwen Code) to search for drift and gaps in test coverage without modifying files
Overview: A data contract is a verifiable layer between the product promise of a data mart and its SQL implementation. Within the SDD Data specification, a contract is not a decorative YAML file but a formal agreement about schema, quality, SLA, and breaking-change policy. The chapter examines two levels of contract: ODCS (Open Data Contract Standard) as the technical contract of a data product and the dbt model contract as the form of a specific model. Particular attention is given to contract drift — the situation where the model code changes while the contract stays the same, leading to a silent divergence of data meaning for the consumer. Three types of drift (schema, semantic, process) are studied, along with detection methodology and the role of dbt tests as the evidentiary base of the contract. The practical part is built around a Qwen query that compares the ODCS specification, model description, schema.yml, and SQL code, generating a minimum contract impact note for use in the PR description.
Key concepts: ODCS (Open Data Contract Standard): A standard for a machine-readable description of a data product's technical contract. Within the course, it is presented as the file specs/customer_360_contract.odcs.yaml. It contains a promise about schema (fields, types, required/optional), quality (SLA on freshness, completeness, uniqueness), PII policy, and description of breaking changes. This is a product-level contract: it answers the question "what does the mart promise to the outside world."
dbt model contract: A description of the shape of a specific dbt model in models/schema.yml: name, description, columns with types, tests, and links to documentation. This is a model-level contract: it answers the question "how exactly is the promise implemented in this project." In the learning example, contract.enforced: true is not enabled so as not to mix the first pass with adapter constraints; in production, it is enabled where the materialization supports it.
Two levels of contract and their separation: ODCS describes the product, schema.yml and tests describe the specific model. Neither layer replaces the other. If you keep only dbt tests, you can get a green model with the wrong meaning. If you keep only ODCS, you can get a beautiful contract with no evidence of its fulfillment.
Schema drift: The first type of contract drift. Occurs when fields are added, removed, or renamed in the model, while the ODCS specification is not updated. Example: in mart_customer_360, the product_code column was added, but ODCS continues to list the previous set of fields. dbt may pass all tests, and the consumer will receive a structure that is not described in the contract.
Semantic drift: The second type of drift. A field remains in the schema, but its meaning changes. For example, the metric risk_event_count_7d starts being counted over a different time interval or includes events of a different type. Technically the schema is intact, but the business meaning has changed. This is the most dangerous type of drift, because automated tests usually do not catch it.
Process drift: The third type of drift. validation.md, a reviewer report, or another process artifact does not reflect the actual model change. For example, a reviewer approved a change but forgot to update the test coverage matrix or a changelog entry. Formally the process passed, but in fact the evidentiary base of the contract is lost.
Contract evidentiary base: The principle: if ODCS says a field is required (e.g., not_null), then there must be at least one dbt test, singular test, or manual reviewer fact confirming this property. A contract without evidence remains a promise that can neither be confirmed nor refuted.
Minimum contract impact note: A structured markdown block that should appear in the description of every PR that touches a model with a contract. Contains six fields: changed fields, breaking change, PII impact, SLA impact, test coverage, whether human confirmation is needed. Serves as proof that the PR author understood the contract implications.
Grain as part of the interface: The principle that grain (e.g., "one row per customer_id") is part of the public interface of the data mart just as much as the list of columns. Changing grain is a breaking change, even if the schema stays the same. This is a central notion for understanding semantic drift.
Qwen query for drift detection: A defined prompt for an LLM assistant that compares four artifacts: specs/customer_360_contract.odcs.yaml, specs/models/mart_customer_360.md, models/schema.yml, and models/marts/mart_customer_360.sql. The assistant must find drift and gaps in test coverage, while being forbidden from modifying files. The result is an analytical note for the reviewer.
Important dates: Emergence of the ODCS standard: The ODCS (Open Data Contract Standard) specification has been actively developed by the community since 2023 as an attempt to unify machine-readable data contracts by analogy with OpenAPI for REST.
Introduction of dbt contracts: The contract.enforced feature in dbt appeared in version 1.5 (late 2023) and stabilized in 1.6+, supporting table and incremental materializations for a number of adapters.
Data mesh concept (Zhamak Dehghani): From 2019–2020, the data mesh concept elevated data contracts as a mandatory element of data-as-a-product, which influenced the emergence of ODCS and dbt contracts.
Evolution of the data contract notion: Before 2022, the term "data contract" was used mostly in a marketing sense; from 2023 it became a formalized engineering artifact in large companies (GoCardless, Shopify, Netflix).
Practice exercises: Name: Matching ODCS Obligations to dbt Tests
Problem: You are given two fragments. Fragment A is an excerpt from specs/customer_360_contract.odcs.yaml:
columns:
- name: customer_id
dataType: integer
required: true
primaryKey: true
- name: risk_event_count_7d
dataType: integer
required: true
- name: email
dataType: string
required: false
classification: PII
Fragment B is an excerpt from models/schema.yml:
models:
- name: mart_customer_360
columns:
- name: customer_id
tests:
- unique
- not_null
- name: risk_event_count_7d
tests:
- not_null
- name: email
# no tests
List the required fields and indicate which dbt test, singular test, or manual reviewer fact covers each of them. For fields without coverage, formulate a singular test or a reviewer action.
Solution: Step 1. Identify required fields from ODCS: customer_id (required, primaryKey) and risk_event_count_7d (required). The email field is not required.
Step 2. Match with the tests in schema.yml:
- customer_id: covered by unique and not_null tests in schema.yml. Both ODCS obligations (required + primaryKey) are closed by automated tests.
- risk_event_count_7d: covered by the not_null test. The required obligation is closed, but it is not proven that the values are reasonable (e.g., ≥ 0). An additional singular test on range is required.
Step 3. For the email field (classified as PII in ODCS, but not covered by tests): either a singular test on the absence of email in unexpected places, or an explicit reviewer fact is required: "email is not used in mart, the column is reserved." This fact is recorded in validation.md.
Step 4. Final coverage table:
| Field | ODCS Obligation | Coverage |
|---|---|---|
| customer_id | required + primaryKey | unique + not_null ✅ |
| risk_event_count_7d | required | not_null ✅, but no range check ⚠️ |
| PII | no tests ⚠️, singular test or reviewer fact required |
Complexity: intermediate
Name: Classifying the Type of Drift
Problem: For each of the four situations, determine the type of drift (schema, semantic, process) and justify whether the change is breaking.
Situation 1: A nullable column product_code has been added to mart_customer_360. Situation 2: The metric total_revenue in mart_finance has been renamed to net_revenue and now subtracts refunds. Situation 3: A reviewer approved a PR with a grain change, but did not update validation.md. Situation 4: In the ODCS specification the field email is marked as PII, but in schema.yml it has been removed, while in SQL it remains.
Solution: Situation 1: schema drift. The column appeared — ODCS and schema.yml disagree. The change is NOT breaking for existing consumers (they simply ignore the new column), but it is potentially semantically significant: product_code may be perceived as a new business metric. An explicit description in the PR is required.
Situation 2: semantic drift + schema drift. The field was renamed (schema) and its meaning changed (subtraction of refunds, semantic). This is a BREAKING change: any dashboard that referenced total_revenue will either break or start showing different numbers without a visible error.
Situation 3: process drift. The change went into the code, the reviewer approved it, but the process artifact (validation.md) was not updated. The change itself can be anything, but the evidentiary base is lost. It is considered breaking if the change is contractually significant but the validation was not recorded.
Situation 4: schema + process drift. ODCS says the PII field exists; schema.yml says it does not; SQL says it does. Three sources disagree. This is a breaking change from a process perspective: a consumer reading schema.yml will conclude that email is absent and may not apply masking.
Complexity: intermediate
Name: Composing the Minimum Impact Note
Problem: You are the author of a PR. The change: a nullable column middle_name (patronymic) has been added to mart_customer_360, grain was not changed, customer_id remains the primary key, no new tests were added, and the PII policy was not affected (middle_name is classified as PII in ODCS). Fill in the minimum contract impact note according to the template from the chapter.
Solution: ```markdown Changed fields: a nullable column middle_name (PII) was added Breaking change: no (nullable, does not change grain, existing consumers are not affected) PII impact: a PII column middle_name was added to the mart; it is necessary to verify that downstream consumers take the classification into account; in schema.yml the field must have classification: PII and a test or singular rule prohibiting propagation without masking SLA impact: none (freshness and completeness are not affected) Test coverage: for middle_name a singular test was added that checks the column is present only in marts with confidential access level; customer_id continues to be covered by unique + not_null Confirmation required: yes, explicit confirmation from the data steward is required, since a new PII column was added
Key point: even a non-breaking change requires human confirmation if it touches PII or potentially changes consumer expectations.
Complexity: intermediate
Name: Formulating a Qwen Prompt for a Specific Model
Problem: You have the model mart_orders with a contract in specs/orders_contract.odcs.yaml. Formulate a prompt for Qwen Code that will check contract drift for this model. The prompt must: list all four artifacts for comparison, explicitly state that files must not be changed, and request a structured output.
Solution: ```text
Compare specs/orders_contract.odcs.yaml,
specs/models/mart_orders.md, models/schema.yml and
models/marts/mart_orders.sql.
Find contract drift and gaps in test coverage.
Do not modify the files.
Return the result as:
1. A list of required fields per ODCS and their test coverage.
2. A list of fields present in SQL but not described in ODCS.
3. A list of fields described in ODCS but missing in SQL.
4. Detected semantic inconsistencies (grain change, metric renaming, change in calculation logic).
5. A minimum contract impact note per the template from the chapter.
Key elements of a correct prompt: an explicit list of files, an explicit prohibition on modification, structured output, and anchoring to the chapter's concepts (drift, coverage, grain).
Complexity: intermediate
Name: Recognizing a Breaking Change
Problem: Five changes in models with contracts are given. For each, determine whether it is breaking and whether confirmation from the data steward is required.
- Renaming the column status to order_status.
- Adding a nullable column discount_amount.
- Changing the grain from "one row per customer_id" to "one row per (customer_id, month)".
- Deleting the column legacy_score, which has not been used for 6 months.
- Changing the ltv_90d calculation formula from sum(amount) to sum(amount) - sum(refund).
Solution: 1. BREAKING. Renaming breaks any dashboard that references status. Steward confirmation and a migration period are required.
- NOT breaking (formally). A nullable column does not break existing consumers. But if discount_amount is perceived as a new business metric, notification is required and possibly product owner confirmation.
- BREAKING. Changing grain is the most dangerous kind of breaking change: the number of rows changes, downstream aggregates produce different numbers. Mandatory steward confirmation and product sign-off are required.
- CONDITIONALLY BREAKING. If legacy_score truly was not used, its deletion is safe. But "not used for 6 months" is weak evidence: dashboards may have been archived rather than deleted. Product owner confirmation is required.
- BREAKING. Semantic change: the numbers change without renaming the column. This is the worst case — tests can pass, and consumers see different data. Steward confirmation is required, mandatory notification of consumers, and updating ODCS and range tests.
Complexity: advanced
Case studies: Name: Adding a "Safe" Column Broke Reporting
Scenario: A fintech company, ~200 employees, 12 marts in dbt. The mart mart_customer_360 is used by the risk management team for a daily report on high-risk customers. The ODCS contract describes the grain as "one row per customer_id" and lists 14 fields, among them risk_event_count_7d (required). A Tableau dashboard aggregates by customer_id and computes the share of customers with risk_event_count_7d > 5.
Challenge: An analyst added a nullable column product_code (main product code of the customer) to the model, considered the change safe, and opened a PR without updating ODCS. dbt tests passed, the reviewer approved without questions. Three weeks later, the risk management team discovered that the "high-risk customers" report now included 2.3x more records. The investigation showed: in the Tableau calculation, product_code was accidentally used as an additional grouping key, and the report's grain implicitly shifted from customer_id to (customer_id, product_code).
Solution: The team implemented three process changes. First, they added a mandatory "Contract Impact Note" block to the PR template, following the example from the chapter. Second, they set up Qwen Code as an automatic check in CI: for every PR that touches a model with ODCS, the bot compares four files and leaves a comment with suspected drifts. Third, they introduced a rule: if a PR adds a nullable column to a model with a contract, the reviewer must explicitly confirm that the new column does not change the grain and is not perceived as a business metric.
Result: In the first quarter after implementation, the bot detected 7 potential drifts before merge, of which 2 turned out to be real semantic issues that would have been missed by ordinary dbt tests. Average PR review time decreased by 18% because authors began filling out the impact note themselves and reviewers did not spend time clarifying "what does this column mean."
Lessons learned: A nullable column does not equal a safe change: if it is perceived as a new grouping key, the grain changes implicitly
Automated dbt tests do not catch grain changes — for this you need either a separate test on key uniqueness or an explicit reviewer fact
The contract impact note works as a checklist: the very fact of filling it out forces the PR author to think about the consumer
Related concepts: Schema drift
Semantic drift
Grain as part of the interface
Minimum impact note
Qwen query for drift detection
Name: A Silent Change in a Metric's Formula
Scenario: An e-commerce platform, the mart mart_finance_daily is used by the finance team to compute revenue. The metric gross_revenue is described in ODCS as "the sum of the cost of all successfully completed orders per day." SQL implements this as SUM(amount) FROM fct_orders WHERE status = 'completed'. The mart is consumed by three Looker dashboards and one ML pipeline for revenue forecasting.
Challenge: An engineer changed the formula by adding a filter on NOT refunded_at IS NULL, in order to "not account for refunds in gross revenue." The change was reviewed as a "small logic improvement." ODCS was not updated, schema.yml was untouched, dbt tests passed. A month later, the finance team discovered a 12% discrepancy between the mart's report and the report from the ERP system. The investigation revealed that some refunds had previously been included in gross_revenue, but now they were not.
Solution: The team rolled back the change, reverted the SQL to the previous formula, updated ODCS, and added a new metric net_revenue with an explicit description "the sum of the cost of successfully completed and non-refunded orders." In schema.yml they added a separate test that checks gross_revenue ≥ net_revenue for any day. They added a warning to the PR template: "If a SQL change affects the calculation of metrics described in ODCS, the change is considered breaking and requires data steward confirmation."
Result: In the six months after the incident, there was not a single case of silent metric changes. The revenue-forecasting ML pipeline continued to work without failures. The team formalized the policy: "any change to a metric's formula = a breaking change = requires a major contract version and notification of all consumers two weeks in advance."
Lessons learned: Semantic drift is more dangerous than schema drift: the schema is intact, tests are green, numbers change silently
Improving logic ≠ a safe change if the logic is described in the contract
A contract must explicitly list metrics and their formulas, otherwise any "optimization" is drift
Related concepts: Semantic drift
Breaking change
ODCS as an obligation
Contract evidentiary base
Name: ODCS Without Tests: A Beautiful Contract for a Dead Product
Scenario: An insurance company, a legacy project. An analyst from the data governance team created ODCS specifications for 30 data marts, ceremoniously committed them to the repository, and reported to management on "the implementation of data contracts." The marts continued to be developed as before: with no link to ODCS, schema.yml described only a small portion of columns, and there were few tests.
Challenge: Half a year later, an audit showed that 18 of the 30 ODCS specifications were completely outdated: fields had been renamed, metrics recalculated, grain changed. At the same time, the contracts formally existed, and reporting to the regulator looked "green." Legal and compliance departments used ODCS as proof that "everything is under control," but in fact consumers received data that did not match the contract.
Solution: The team introduced the linkage "ODCS obligation → dbt test or singular test or reviewer fact." For every required field in ODCS at least one test was created. They added a check to CI: if ODCS specifies required: true, schema.yml must contain a not_null test. If ODCS specifies classification: PII, schema.yml or a singular test must contain an explicit rule for handling this field. All 18 outdated specifications were either updated to the actual state of the marts, or marked as deprecated with an explicit SLA until shutdown.
Result: Within a quarter, 100% of required fields in current ODCS specifications received test coverage. The regulator's audit passed without remarks on data contracts. The team formulated the principle: "ODCS without an evidentiary base is not a contract, it is marketing."
Lessons learned: ODCS by itself has no value: value appears when every obligation is verifiable
The contract must be synchronized with the implementation, otherwise it becomes a source of false confidence
The linkage ODCS ↔ schema.yml ↔ SQL must be automated in CI, otherwise drift is inevitable
Related concepts: Contract evidentiary base
Two levels of contract
Process drift
Linkage of contract and tests
Study tips: Read changes as a reviewer, not as an author. The author sees "did the SQL get better," the reviewer sees "did the promise stay the same." Switch perspectives on every PR with a model that has a contract.
Memorize the six fields of the minimum impact note and keep them in mind as a checklist: changed fields, breaking change, PII impact, SLA impact, test coverage, confirmation required. This is the most practical skill of the chapter.
Distinguish "the test passed" from "the contract is upheld." A test checks one property of one column at one point in time. A contract is the totality of obligations over the lifecycle of the data mart. Green tests do not mean the contract is alive.
Practice formulating Qwen prompts: take a working model and ask the LLM assistant to compare ODCS, model description, schema.yml, and SQL. Evaluate what drifts it found, what it missed, and refine the prompt. This skill scales.
Do not confuse schema drift with semantic drift. Schema drift is about the list of columns. Semantic drift is about the meaning of the columns that remain. Semantic drift is 5–10x more dangerous, because automated tests usually do not catch it.
When adding a nullable column, ask yourself: "If I were a consumer, would I perceive this column as a new business metric?" If yes, this is not a "safe" change, even if it is nullable.
Remember grain as part of the interface. Changing grain is always a breaking change, even if all columns remain the same. A unique test on the grain key is mandatory hygiene for any model with a contract.
Do not enable contract.enforced: true in the first learning pass. First learn to read drift with your eyes and through Qwen Code. Mechanical adapter constraints are the second step, once the team understands why this is needed.
Additional resources: ODCS official specification: https://bitol-io.github.io/data-contract-specification/ — the primary source of the Open Data Contract Standard used in the course
dbt documentation on contracts: https://docs.getdbt.com/docs/mesh/contracts — the section of the official dbt documentation on contract.enforced and supported materializations
Zhamak Dehghani's article on data mesh: https://martinfowler.com/articles/data-mesh-principles.html — the conceptual foundation of data-as-a-product and data contracts
dbt-utils package: https://github.com/dbt-labs/dbt-utils — standard tests and macros that extend contract check coverage
Great Expectations: https://greatexpectations.io/ — a framework for data quality assertions that can serve as an additional layer of checks beyond dbt tests
Базовый Data: https://www.elementary-data.com/ — open-source data observability for dbt, including anomaly detection and tests
'Data Contracts' book (Andrew Jones): https://datacontract.com/ — a practical guide to implementing data contracts in organizations
Qwen Code documentation: https://qwenlm.github.io/ — materials on working with the LLM assistant for analyzing code and specifications
Summary: A data contract is a verifiable layer between the product promise of a data mart and its SQL implementation. In SDD Data, two levels are distinguished: ODCS describes the product (schema, quality, SLA, PII policy), while schema.yml and dbt tests describe the specific model. Neither layer replaces the other: tests without ODCS give a green model with the wrong meaning, ODCS without tests gives a beautiful contract with no evidence. Contract drift occurs when the code changes while the contract stays the same, and comes in three types: schema (fields added/removed/renamed), semantic (the meaning of a field changed without a schema change), and process (process artifacts do not reflect the change). Semantic drift is the most dangerous, because automated tests usually do not catch it. The main practical skill is filling out the minimum contract impact note on every PR and formulating a Qwen prompt for automatic drift detection. Remember the six note fields: changed fields, breaking change, PII impact, SLA impact, test coverage, confirmation required. And remember the main principle: adding a nullable column does not equal a safe change if it is perceived as a new business metric or changes the grain.