Design Considerations¶
This page explains design decisions behind vowl's internals: how checks are executed, how queries are derived, and how results flow into output.
The two-query model¶
Every SQL check derives two queries from the single query: you write:
| Query | Purpose | When it runs |
|---|---|---|
| Scalar query | Produces the number that decides pass/fail | Always |
| Failed-rows query | Returns the actual offending rows | Only on failure, and only when rows are requested |
You only write one of them. vowl derives the other automatically:
This means a check like COUNT(*) mustBe 0 is not limited to a single number.
vowl can always recover the individual rows behind that number by running the
derived failed-rows query. Consider a referential-integrity check:
SELECT COUNT(*)
FROM demo_employee_payroll payroll
LEFT JOIN demo_employee_list ref
ON payroll.employee_id = ref.employee_id
WHERE ref.employee_id IS NULL
The scalar query tells you some payroll rows have no matching master record.
The derived failed-rows query (COUNT(*) rewritten to SELECT *) tells you
exactly which ones. The join is how you express the condition; it does not
prevent vowl from recovering individual offending rows.
This is what makes it possible to map a cross-table failure back onto a single table's annotated output (next section).
How the rewrite works: sqlglot, not string manipulation
vowl does not regex or string-replace COUNT(*) with SELECT *. It parses
your query into an AST using sqlglot,
a SQL transpiler, and performs the substitution structurally. This means it
correctly handles nested subqueries, CTEs, and dialect-specific syntax
without mangling your SQL.
The failed-rows query is lazy
The scalar query always runs because vowl needs it to decide the verdict.
The failed-rows query only runs when a check fails and something
requests the rows (e.g. get_annotated_output(), show_failed_rows(), or
output_mode="failed_rows"). Passing checks cost a single query.
A note on query performance
The syntactic complexity vowl adds (wrapping queries in subqueries,
rewriting between COUNT(*) and SELECT *) does not degrade execution
plans. Query engines flatten these standard shapes during planning. The
LEFT JOIN ... WHERE ref.key IS NULL anti-join pattern is typically
executed as a hash anti-join over the join key, not a row-by-row
comparison. Additionally, the failed-rows query only runs on failure and
is capped at max_failed_rows, so its cost is bounded regardless of table
size.
Making a cross-table check annotate onto a table¶
For background on annotated output and residues, see Annotated Output: Not All Checks Can Be Merged.
By default a cross-table check becomes a residue: its failed rows carry
columns from both tables, so they don't match any single schema (see
condition 3).
The residue still contains the offending rows and the check_name, just as a
separate entry rather than a column on the annotated table.
If you want those failures to appear in a table's check_info column, shape
the failed-rows query so it returns exactly the columns of the table you want
to annotate. Wrap the join in a subquery that selects only the anchor table's
columns:
- name: employee_id_exists_in_master_list
description: >-
Referential integrity check: ensures every employee ID in the payroll
table exists in the reference employee list.
type: sql
dimension: consistency
query: >-
SELECT COUNT(*)
FROM (
SELECT payroll.*
FROM demo_employee_payroll payroll
LEFT JOIN demo_employee_list ref
ON payroll.employee_id = ref.employee_id
WHERE ref.employee_id IS NULL
) AS orphaned_payroll
mustBe: 0
tags:
- Cross-Table Validation
When vowl rewrites the outer COUNT(*) into SELECT *, it only swaps the
outer select list. The subquery is left untouched:
SELECT * FROM (
SELECT payroll.*
FROM demo_employee_payroll payroll
LEFT JOIN demo_employee_list ref
ON payroll.employee_id = ref.employee_id
WHERE ref.employee_id IS NULL
) AS orphaned_payroll
The inner SELECT payroll.* decides the columns, so the failed rows come back
with only the payroll columns using their bare names (employee_id, name,
salary, ...). payroll.* qualifies which table to expand; it does not
prefix the column names. That column set matches demo_employee_payroll, so the
rows map straight back onto its annotated table.
| Query shape | Failed-rows columns | Annotates onto payroll? |
|---|---|---|
Bare SELECT COUNT(*) FROM payroll LEFT JOIN ref ... |
Both tables' columns (ref.* all NULL) |
No, column set doesn't match |
Subquery with SELECT payroll.* (above) |
Payroll columns only | Yes |
Mergeability is decided by column structure, not intent
vowl decides mergeability by column structure. Any failed-rows result
whose columns match the anchor table will be merged onto it. It is on you
to ensure the subquery projects the right columns (payroll.*, not a
partial column list, and not both tables' columns). Get this wrong and the
check either won't merge (column mismatch = residue) or could merge rows
you didn't intend. When in doubt, run get_annotated_output() and inspect
both annotated and residues.
NOT EXISTS is an equivalent, often cleaner form
The same result with no subquery wrapper. The reference table lives only
inside the WHERE NOT EXISTS (...), so SELECT * naturally resolves to
payroll columns:
```sql
SELECT COUNT(*)
FROM demo_employee_payroll payroll
WHERE NOT EXISTS (
SELECT 1 FROM demo_employee_list ref
WHERE ref.employee_id = payroll.employee_id
)
```