
Example—An SQL test runs a query to check for rows where the workspace is null. If any rows match the test, it will throw an error
Test types
Alongside a query you write yourself, a test can be one of the following types. Each takes the columns and values it needs, so a test of a given type is the same assertion wherever it is defined — in the app, over the API, or in a YAML configuration, where the name in the first column is thetype: value.
Creating a SQL test
- Head to the Health overview
-
In the SQL tests section, click
Add sql test - Select the connection to execute your SQL query
-
Specify a SQL query. The test is considered a success if it returns zero records. If any records are returned, the test will trigger an error, and the failed records will be stored in an audit table for investigation. To have the query return metrics and grade them individually instead, add evaluators.

-
Choose a schedule (
hourly/daily) and a time (hh: mm) when you want the test to run.
Running an excessive amount of tests or running a test too often will impact your data warehouse costs. Avoid running tests more often than needed
-
The confirmation page will show you a summary of the setup. To make it easier to locate in the UI, you can give the test a human-friendly name.

Editing a SQL test
- Head to the Health overview
- Click on SQL tests to see all your SQL tests
- Select the SQL test you want to edit by clicking on it
-
In the popout, navigate to the
settingstab. ClickEditunder SQL test configurations to edit the name, run schedule, or SQL code. To delete the test, clickDelete SQL test
Managing SQL tests programmatically
SQL tests can be created and changed outside the app, one at a time or in bulk across every table matching a selection.- YAML and the CLI — tests live under an entity’s
tests:in a monitors-as-code configuration and are deployed withsynqcli deploy. To cover many tables by a selection instead of listing each one, put the tests under atype: sql_testsdeployment rule. - Public API —
synq.datachecks.sqltests.v1.SqlTestsServicefor individual tests andsynq.datachecks.sqltests.v1.SqlTestsDeploymentRulesServicefor rules that deploy a set of tests onto every table a selection matches. Reading needs a token withSCOPE_DATACHECKS_SQLTESTS_READ, writing needsSCOPE_DATACHECKS_SQLTESTS_EDIT. See the API reference and API scopes. - MCP — the SQL test tools create tests from a template (not-null, unique, accepted values, relationships, freshness, range, business rules) or from raw SQL, and the deployment rule tools do the same across a selection of tables. Listing tests is covered by read-only consent; writing needs the Deploy permission, and every write is confirmation-gated.
The table placeholder
Abusiness_rule and a business_query carry SQL you write yourself, so nothing in them names the table unless you do. Write {{ table }} and it is replaced with the fully qualified name of the table the test is anchored to, quoted for your warehouse. It is the only supported placeholder — any other {{ name }} token is rejected when the test is saved.
The substitution is textual, so the token is replaced everywhere it appears, inside a string constant or a comment as well as in a FROM clause. There is no escape sequence; write the name out yourself if you need the literal text in a result.
On a single test it saves repeating a name you already gave. On a sql_tests deployment rule it is the difference between a test and a copy: the rule deploys its tests onto every table the selection matches, so without the placeholder all of them run the same query against whichever table you typed.
business_query you own the whole statement and can alias it, which is what a correlated subquery needs. In a business_rule the FROM clause is generated for you and carries no alias, so the placeholder is for a subquery of your own rather than for qualifying a column:
{{ table }}.column is not portable: on BigQuery the fully qualified name is a single quoted identifier and cannot qualify a column. Reach for business_query when you need to name the anchor’s columns.
Evaluators
Abusiness_query with no evaluators is a single verdict: every row the query returns is a failure, so the query itself has to encode what “wrong” means and the whole test is one pass or one fail. Evaluators split that in two. The query returns the numbers, and each evaluator is a named condition over those numbers with its own severity. One scan of the warehouse, several checks, each of them named in the alert.
Reach for them when one rollup answers several independent questions, when those questions deserve different severities, or when you want the alert to say which condition broke rather than how many rows came back.
Daily order health by channel
The query groups yesterday’s orders by sales channel and computes four numbers per channel. Each evaluator reads one of them. SQL
The same test in the app — the query, then one card per evaluator carrying its own name, severity and expression. The first two of the four are shown.
WHERE clause and its own scan of orders, or one test that returns a row when anything at all is wrong and cannot say what.
Writing an expression
- Refer to the columns of the query’s select list by the alias they were given.
refund_rateworks because the query aliased it. - The expression is a fail condition. The row is flagged when the expression is
TRUE, so write what you do not want to see rather than what you expect. NULLpasses. A metric that can be null and should count as a failure has to say so:revenue IS NULL OR revenue < 0.- Aggregate in the query and compare in the evaluator. The expression is evaluated one result row at a time, so it cannot total across rows.
- The expression is sent to your warehouse as written, in that warehouse’s SQL dialect.
Severity
Each evaluator carries its own severity, and the test reports the worst severity among the evaluators that flagged a row.
An
Info evaluator records its outcome without moving the test’s status, which suits a threshold you are still calibrating or a number you want in the run history without paging anyone. A test created over the API or from a YAML configuration can leave an evaluator’s severity unset, and it takes the test’s own.
Failed rows
With Save failures on, the rows an evaluator flagged are written to the audit table, alongside how each evaluator did on that run. You can go from a failing test to the channels behind it rather than to a count. Rows are written whenever any evaluator flags one, including when onlyInfo evaluators did and the test itself reports OK. A threshold you are still calibrating therefore collects real rows before you promote it to a warning.
Editing evaluators
Adding, removing or changing an evaluator updates the test in place, so its history, its issues and its past runs are kept. A test on a schedule runs again straight away instead of waiting for its next slot, so the change shows up in the run view immediately.Evaluators belong to a
business_query, because the evaluator reads the columns the query returns and only a business_query gives you the whole SELECT.Audit table schema
When a test has Save failures on, the rows it flagged are written to an audit table in your data warehouse, so you can look at the records themselves. The table belongs to the warehouse integration rather than to any one test: set its fully qualified name in the integration’s Audit table FQN field and Coalesce Quality creates it if it does not already exist. While that field is empty a test writes nothing, whatever its Save failures setting.Naming the table
The name is yours to choose. Give the integration a fully qualified name the connection can create and write to; the placeholder in that field shows the shape your platform expects, from a bare table name on Redshift and PostgreSQL tocatalog.schema.table on Databricks. One audit table serves every SQL test on that integration, and the statements below use quality_sql_test__audit as the name.
Schema by data warehouse
The audit table schema varies slightly by data warehouse. Below are the CREATE TABLE statements for each supported platform:BigQuery
BigQuery
Snowflake
Snowflake
Databricks
Databricks
PostgreSQL
PostgreSQL
MySQL
MySQL
Redshift
Redshift
ClickHouse
ClickHouse
Trino
Trino
SQL Server
SQL Server
Oracle
Oracle
DuckDB
DuckDB
Column descriptions
The table is created when you save the integration with an Audit table FQN set, so the connection needs permission to create tables in that schema or database. Without the privilege the integration reports the failure on its status; you can also create the table yourself with the statement above and grant
INSERT on it instead.sql_test_name and evaluators columns keeps its original shape and those two
values are dropped from the rows written to it. Add the two columns yourself to start
recording them.