Guide · Native Data Quality

Turn existing business rules into Databricks Lakeflow expectations

Databricks2026-11-05Code ran before publishing, PDF by email

The short answer

To set up Databricks expectations on a Lakeflow pipeline for rules you already have, write each rule as one line in a Python dictionary: a name and a SQL condition that is true for a good row. Use three dictionaries to warn, drop or fail. Add the file to the pipeline as a source.

What you will have at the end

One Python file, orders_expectations.py, that you add as the source of a Lakeflow pipeline. It turns six business rules about an orders table into pipeline expectations (read 2026-11-05). Databricks describes an expectation as a SQL Boolean condition that is checked on every record passing through a pipeline, with one of three actions when a record fails: warn (the default, the record is written anyway), drop (the record is removed before it is written) or fail (the update stops).

The same file also writes a quarantine table. It holds every dropped row and names the rules that row broke, so a data steward can see what was removed and why. Lakeflow pipelines require the Premium plan (Databricks docs, read 2026-11-05).

Step 1. Write each business rule as a name and a condition

Take each rule from wherever it lives today (an ETL job, a spreadsheet, a data dictionary) and write it as a SQL condition that is true for a good row. Then decide what should happen when it is false. Put it in the dictionary for that action. The block below is the only part of the file you edit.

Python: the rules, in orders_expectations.py
# ---- EDIT THIS BLOCK: your table and column names, and your business rules --------------------------
SOURCE_TABLE = "orders_raw"        # the table the rules apply to
LOOKUP_TABLE = "customers_ref"     # the table a key must exist in
SOURCE_KEY = "o_custkey"           # the column in SOURCE_TABLE that must exist in LOOKUP_TABLE
LOOKUP_KEY = "c_custkey"           # the matching column in LOOKUP_TABLE

# Each rule is: name -> SQL condition that is TRUE for a good row. The name is what the event log shows.
# A rule that stops the update. Use it only for a problem that makes every later number untrustworthy.
RULES_FAIL = {
    "order_key_present": "o_orderkey IS NOT NULL",
}
# Rules that remove the row from the clean table. The same rules fill the quarantine table.
RULES_DROP = {
    "price_positive": "o_totalprice > 0",
    "status_valid": "o_orderstatus IN ('F', 'O', 'P')",
    "order_date_present": "o_orderdate IS NOT NULL",
    "customer_exists": "customer_found",   # set by the join below: expectations cannot hold a subquery
}
# Rules that only count: the row is kept, the failures are recorded.
RULES_WARN = {
    "priority_known": "o_orderpriority IN ('1-URGENT', '2-HIGH', '3-MEDIUM', '4-NOT SPECIFIED', '5-LOW')",
}
# ------------------------------------------------------------------------------------------------------

Databricks documents the three Python decorators this file uses to apply a whole dictionary at once: expect_all, expect_all_or_drop and expect_all_or_fail. Grouping rules in a dictionary is Python only; SQL pipelines state each rule as its own CONSTRAINT clause (Databricks docs, read 2026-11-05).

Step 2. Apply the dictionaries to three datasets

The rest of the file does not change. One dataset reads the source and carries the fail rule. A second keeps the rows that pass the drop rules and counts the warn rule. A third is the quarantine: it keeps the rows that broke at least one drop rule, with a column listing which ones.

One rule needed a workaround. Databricks says an expectation cannot contain a subquery that refers to another table (Databricks docs, read 2026-11-05), so "the customer must exist" cannot be written as IN (SELECT ...). The first dataset instead joins the orders to the customer keys and adds a customer_found flag, and the rule reads that flag. The join uses equality, so a NULL key in the lookup table cannot hide a missing customer, which is the trap that a NOT IN subquery falls into.

Python: the three datasets (nothing here is edited)
def _passes(condition):
    # A rule that evaluates to NULL is a failure, so the quarantine treats it as one.
    return f"coalesce(({condition}), false)"


QUARANTINE_FILTER = "NOT (" + " AND ".join(_passes(c) for c in RULES_DROP.values()) + ")"
FAILED_RULES = "concat_ws(', ', " + ", ".join(
    f"CASE WHEN NOT {_passes(c)} THEN '{n}' END" for n, c in RULES_DROP.items()) + ")"


@dp.materialized_view(comment="Source rows plus a customer_found flag; stops the update if a rule in RULES_FAIL breaks")
@dp.expect_all_or_fail(RULES_FAIL)
def orders_checked():
    return spark.sql(f"""
        SELECT s.*, l.{LOOKUP_KEY} IS NOT NULL AS customer_found
        FROM {SOURCE_TABLE} s
        LEFT JOIN (SELECT DISTINCT {LOOKUP_KEY} FROM {LOOKUP_TABLE}) l ON s.{SOURCE_KEY} = l.{LOOKUP_KEY}
    """)


@dp.materialized_view(comment="Rows that pass every drop rule")
@dp.expect_all_or_drop(RULES_DROP)
@dp.expect_all(RULES_WARN)
def orders_clean():
    return spark.read.table("orders_checked")


@dp.materialized_view(comment="Rows that broke at least one drop rule, with the rules they broke")
def orders_quarantine():
    return (spark.read.table("orders_checked")
            .filter(QUARANTINE_FILTER)
            .selectExpr("*", f"{FAILED_RULES} AS failed_rules"))

Databricks also documents a quarantine pattern (read 2026-11-05) that builds one table with an is_quarantined column. This file uses separate clean and quarantine tables built from the same dictionary, so a rule is written once.

To create the pipeline, use serverless compute and the Advanced product edition, which is the edition that includes expectations and which serverless pipelines require. Set the default catalog and schema to the ones that hold your two input tables, and add orders_expectations.py as the source file. Databricks says the default catalog and schema are where datasets are read from or written to when the code does not qualify them (Databricks docs, read 2026-11-05), so the bare table names in the file resolve to that schema.

Run 1. Clean data: every rule passes

The test input is 1,000 orders and the first 1,000 customers, copied from the TPC-H sample data that Azure Databricks provides. demo_data.sql builds both tables in a schema you can write to. We ran the pipeline once and read each expectation's counts from the pipeline's event log, which Databricks documents as the place to query passed_records, failed_records and dropped_records (Databricks docs, read 2026-11-05).

Output from our run: expectation counts, clean data
dataset                                                                          | expectation        | passed_records | failed_records
---------------------------------------------------------------------------------+--------------------+----------------+---------------
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_checked | order_key_present  | 1000           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | customer_exists    | 1000           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | order_date_present | 1000           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | price_positive     | 1000           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | priority_known     | 1000           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | status_valid       | 1000           | 0

Then we counted the rows in the three tables.

Output from our run: rows in, rows kept, rows quarantined
orders_raw | orders_clean | orders_quarantine
-----------+--------------+------------------
1000       | 1000         | 0

Run 2. Planted errors: five rows dropped, one kept with a warning

We added six rows with plant_errors.sql: a negative price, a status of X, an order for a customer that does not exist, an order with no date, an order with a priority the rules do not list, and one row that breaks two rules at once. Then we ran the pipeline again from a clean schema.

Output from our run: each rule counted its planted rows
dataset                                                                          | expectation        | passed_records | failed_records
---------------------------------------------------------------------------------+--------------------+----------------+---------------
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_checked | order_key_present  | 1006           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | customer_exists    | 1004           | 2
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | order_date_present | 1005           | 1
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | price_positive     | 1005           | 1
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | priority_known     | 1005           | 1
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean   | status_valid       | 1004           | 2

Each drop rule failed the rows we planted: status_valid and customer_exists each failed two, because one planted row broke both. The warn rule priority_known failed one row. The flows table shows what that did to each dataset.

Output from our run: rows written and dropped per dataset
flow                                                                                | status    | output_rows | dropped_records
------------------------------------------------------------------------------------+-----------+-------------+----------------
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_checked    | COMPLETED | 1006        | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_quarantine | COMPLETED | 5           | 0
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean      | COMPLETED | 1001        | 5

The clean table wrote 1,001 of 1,006 rows and dropped 5. The one row that broke only the warn rule is among the 1,001: a warn rule keeps the row and records the failure. The quarantine table holds the five dropped rows, and its failed_rules column says why each one is there.

Output from our run: the quarantine table
o_orderkey | failed_rules
-----------+------------------------------
900000001  | price_positive
900000002  | status_valid
900000003  | customer_exists
900000004  | order_date_present
900000006  | status_valid, customer_exists

Compare the first two runs. On clean data the quarantine table is empty. On the planted data it has one row for each planted row that broke a drop rule, and the row that broke two rules names both.

Run 3. The fail rule: one missing key stops the update

The fail rule is for a problem that makes everything after it untrustworthy. We added one order with no order key, using plant_null_key.sql, and ran the pipeline from a clean schema. The update failed.

Output from our run: the update stopped, and the error shows the record
flow                                                                                | status  | output_rows | dropped_records
------------------------------------------------------------------------------------+---------+-------------+----------------
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_checked    | FAILED  | NULL        | NULL
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_clean      | SKIPPED | NULL        | NULL
content_loop.databricks_lakeflow_expectations_from_business_rules.orders_quarantine | SKIPPED | NULL        | NULL
ERROR (pipeline): [EXPECTATION_VIOLATION.VERBOSITY_ALL] Flow 'content_loop.databricks_lakeflow_expectations_from_business_rules.orders_checked' failed to meet the expectation. Violated expectations: 'order_key_present'. Input data: '{}'. Output record: '{"o_orderkey":null,"o_custkey":1,"o_orderstatus":"F","o_totalprice":100.0,"o_orderdate":"1995-01-01","o_orderpriority":"1-URGENT","customer_found":true}'. Missing input data: false

The error names the broken expectation and prints the offending record, so the person who fixes the data can find it. The two datasets after the failing one were skipped, so neither wrote rows in this update. Databricks says that when a pipeline fails on an expectation violation, its code must be fixed to handle the invalid data before the pipeline is re-run (Databricks docs, read 2026-11-05). The same docs page says metrics are not recorded for a fail action. In our run the event log did show one failed record for order_key_present, so do not rely on a count being there.

Tested on

Tested on

Platform
Azure Databricks, Premium tier, serverless compute, Unity Catalog
Pipeline
Serverless Lakeflow pipeline, Advanced edition, one update per run
Date
2026-11-05
Dataset
samples.tpch: 1,000 orders and the first 1,000 customers, plus the planted rows
Code
orders_expectations.py, demo_data.sql, plant_errors.sql, plant_null_key.sql

Use it on your own tables

To run this on your own data, open orders_expectations.py and change only the block between the line that says EDIT THIS BLOCK and the closing dashed line. Set SOURCE_TABLE and LOOKUP_TABLE to your two tables, written as catalog.schema.table unless a table sits in the pipeline's target schema. Set SOURCE_KEY and LOOKUP_KEY to the key column in each table. Then rewrite the conditions in the three rule dictionaries as SQL that is true for a good row, using your own column names, and keep the rule named customer_exists reading customer_found. Leave everything below the closing dashed line as it is.

We checked those sentences on a differently built table: other table and column names, a mixed-case column, a NULL key in the lookup table, one planted error for each drop rule and the warn rule, and one extra order with no customer at all. The update completed, the five planted rows that broke a drop rule were dropped, the row that broke the warn rule was kept, and the NULL lookup key did not hide the order for the account that does not exist.

Download: orders_expectations.py · Get the full guide (PDF)

Get the full guide as a PDF

We'll email you a link to it. Please use your work email.

Lakeflow expectations or Great Expectations?

They are different tools, and we ran only the first. Lakeflow expectations are clauses on the datasets of a Lakeflow pipeline and are checked on each record as the pipeline writes it (Databricks docs, read 2026-11-05). That is why they can drop a row before it is written. Great Expectations is a Python framework in which expectations are testable assertions grouped into suites, run through checkpoints that can trigger actions such as alerts (Great Expectations docs, read 2026-11-05). If your data is not produced by a Lakeflow pipeline, expectations in a pipeline cannot check it, and you need a check that runs elsewhere, such as Great Expectations. If it is, the pipeline can run the checks itself, though we did not test alerts. We did not run Great Expectations for this guide.

When this is not enough

  • The rules only run when the pipeline updates. Rows that arrive in a table outside the pipeline are not checked. Our runs used one update each.
  • Expectations measure and filter; they do not repair. A dropped row is gone from the clean table until someone fixes the source and the pipeline runs again. The quarantine table keeps the row and the rules it broke, but nothing sends it to its owner. We did not test alerts.
  • A fail rule stops the datasets that read from it, not other pipelines or business processes. Databricks says that in a triggered pipeline a failed flow does not make other parallel flows fail, and that a validation table does not hold back downstream tables (Databricks docs, read 2026-11-05). If downstream work must not run, split the checks and the downstream work into separate pipelines.
  • Rules that compare other tables or earlier loads. A condition cannot contain a subquery on another table, so the customer rule needed a join. A rule over many rows, such as duplicates or row counts, is a separate validation dataset. Databricks shows validation-table examples for row counts and primary key uniqueness.
  • Deciding which rules are the business's rules. This file runs the rules you give it. Who approved each rule, and what a failure means for the business, is not decided by code.

Run your approved data quality rules natively in Databricks

Native Data Quality is a Data Meaning solution. We run your approved data quality rules natively in Snowflake or Databricks, on your own tables. See what it covers and whether it fits.

See Native Data Quality