SQL Quality Rules
A type: sql rule runs a custom SQL query against the server and compares the single returned value to a threshold. It is the most flexible rule type — use it whenever a check can be expressed as a query. The query must be read-only, and is written in the dialect of the selected server.
Property-level example
schema:
- name: orders
properties:
- name: order_total
logicalType: integer
physicalType: bigint
required: true
quality:
- type: sql
description: 95% of all order total values are expected to be between 10 and 499 EUR.
query: |
SELECT quantile_cont(order_total, 0.95) AS percentile_95
FROM orders
mustBeBetween:
- 1000
- 99900
Schema-level example
schema:
- name: orders
quality:
- type: sql
description: The maximum duration between two orders should be less than 3600 seconds.
query: |
SELECT MAX(duration) AS max_duration
FROM (
SELECT EXTRACT(EPOCH FROM (order_timestamp - LAG(order_timestamp)
OVER (ORDER BY order_timestamp))) AS duration
FROM orders
) subquery
mustBeLessThan: 3600
Placeholders
Instead of hard-coding names, the query can reference the schema, the property, and the location the server names:
| Placeholder | Replaced with |
|---|---|
${model} / ${table} / ${object} | the name of the schema object the rule belongs to |
${field} / ${column} / ${property} | the name of the property the rule belongs to (property-level rules only) |
${schema} | the server's schema |
${dataset} | the server's dataset (BigQuery) |
${project} | the server's project (BigQuery) |
${catalog} | the server's catalog (Databricks, Trino) |
${database} | the server's database (Postgres, SQL Server, Snowflake) |
The $ is optional: {schema} works the same as ${schema}. A placeholder the server has no value for falls back to the name of the schema object.
quality:
- type: sql
description: The orders table is not empty.
query: |
SELECT COUNT(*) FROM ${project}.${dataset}.${table}
mustBeGreaterThan: 0
Comparators
The query must return a single value, which is compared using exactly one of:
| Comparator | Passes when the result… |
|---|---|
mustBe | equals the value |
mustNotBe | does not equal the value |
mustBeGreaterThan / mustBeGreaterOrEqualTo | is above a lower bound |
mustBeLessThan / mustBeLessOrEqualTo | is below an upper bound |
mustBeBetween / mustNotBeBetween | is inside / outside a [min, max] range |
The query must be a query
A rule computes a single value from the data, so the query must be a single read-only statement — a SELECT, a WITH … SELECT, a UNION, or a parenthesized query. Anything else is reported as a failed check and never sent to the data source: DDL (CREATE, DROP, ALTER), DML (INSERT, UPDATE, DELETE), COPY, ATTACH, INSTALL/LOAD, SET, PRAGMA, CALL, and a second statement appended behind a legitimate one.
This applies to every data source, and to every way the CLI is run. A data contract is not always written by the person whose credentials execute it.
SQL dialect
There is no dialect field on a quality rule. The dialect is derived from the type of the server the rule runs against, so a query is read the same way the data source will read it — BigQuery's backtick-quoted table names, Snowflake's SAMPLE, SQL Server's TOP and Postgres' ->> are all understood without any declaration:
| Server type | SQL dialect |
|---|---|
local, s3, gcs, azure, kafka, api, duckdb | duckdb |
postgres | postgres |
redshift | redshift |
mysql | mysql |
oracle | oracle |
sqlserver | tsql |
snowflake | snowflake |
bigquery | bigquery |
databricks | databricks |
athena | athena |
trino | trino |
impala | hive |
dataframe | spark |
The ODCS synonyms resolve to the spelling above before the dialect is looked up, so postgresql is read as postgres. A server declared as type: custom with customType: mssql is read as tsql, like sqlserver.
Files, Kafka topics and API responses are read through DuckDB, so a rule on those server types is written in DuckDB SQL — including its functions, such as read_parquet or list_contains.
If a rule fails with "could not be read as one", the query is not valid for the server type the contract declares; the message names the dialect it was read as.
Notes
- Referencing the data — reference the schema/table by its name in the
FROMclause (e.g.FROM orders). severity— setseverity: warningto report a failing rule without failing the run (see--fail-on).