Skip to content

Gating CI

dbprint check is the command a pipeline runs. It answers three questions in one pass — is the print well-formed, is it fresh, and does the data still satisfy the assertions you wrote — and reports the outcome in an exit code.

Offline by default

Terminal window
$ dbprint check

No connection, no credentials, no database. It reads the committed print and verifies its structure against the format, its freshness against each table's recorded threshold, and any assertions that can be evaluated from the artifact alone. That makes it a job you can run on every pull request, including one from a fork.

Terminal window
$ dbprint check --online

Adds a connection and re-extracts, so it can also report drift between the committed print and the live database. This needs credentials and costs a profiling run, so it is usually a separate, slower job — see tracking drift for when each is worth running.

Exit codes

Seven outcomes from check, ordered so a pipeline can decide how much each one deserves.

CodeMeaning
0ok
1generic — a malformed print, or a table whose rules narrow it both by a predicate and by a fraction, which the command refuses to judge
2staleness — a print is older than its own threshold
3drift (--online) — the committed print no longer matches the database
4connection (--online) — the database could not be reached
5partial extraction (--online) — the connection was reached but some tables could not be re-extracted; the ones that were are still compared and reported
6assertion — a data-quality assertion failed

Across several connections the top-level exit is the highest of the per-connection codes.

One more code exists in the family and check never returns it: 7, total failure, which dbprint generate uses when every table it touched failed. A pipeline treating any non-zero as fatal does not care; one that switches on the number should not expect 7 from a gate job.

Which severities you actually see

This is worth knowing before you rely on the output.

Errors are listed in full. Each one prints its path, its code and the specification section it was raised against.

Warnings are reported as a count. The clean line reads OK: conformance clean (3 warning(s)) and the codes are not named. So a privacy.unredacted-sensitive on a column you forgot to redact, or a manifest.orphaned-artifact on an annotation file created after the last generate, is present in the output as a number and nothing more.

To see which warnings a print carries, take the machine envelope:

Terminal window
$ dbprint check --format json | jq '.[].issues[] | select(.severity == "warning")'

--format json carries every issue at both severities, each with its path, code, detail and spec_ref. The same set is available in-process from validate_print() — see emitting a conforming print.

Only errors gate conformance; a warning never changes the exit code. Conformance codes lists every code and its severity, and SPEC 6.1 defines what each severity means.

A workflow job

name: dbprint
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install dbprint
- run: dbprint check

No extra is needed: the offline path opens no connection, so it loads no driver.

For the online job, install the extra for your engine and supply credentials through the environment. Every credential key reads from DBPRINT_<CONN>_<KEY>, upper-cased, which keeps them out of the repository:

drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install 'dbprint[postgres]'
- run: dbprint check --online
env:
DBPRINT_PRIMARY_HOST: ${{ secrets.DB_HOST }}
DBPRINT_PRIMARY_USER: ${{ secrets.DB_USER }}
DBPRINT_PRIMARY_PASSWORD: ${{ secrets.DB_PASSWORD }}

The online job needs a database reachable from the runner and the read privileges your engine's adapter page lists — duckdb, PostgreSQL, MySQL, ClickHouse, Redshift, Snowflake, Databricks, BigQuery. PostgreSQL additionally needs pg_dump on PATH, at least the server's major version.

It also needs the temporary-table privilege from that page if anything in scope resolves to a fraction. --online re-extracts, so a sample, a filter-free max_rows_scanned ceiling, or a connection-level ceiling all make the job write a temporary table. Without the privilege the job still passes, but each statistic for those tables is measured over its own draw.

Exit codes from generate

A scheduled regenerate job switches on a different set, since generate connects and writes:

CodeMeaning
0ok
1generic
3schema drift was recorded against the previous print
4connection — the database could not be reached, or pg_dump is missing
5partial — some tables failed; the rest were written
7total failure — every table it touched failed

3 reports schema movement only. A statistic that moved lands in diff.yaml without changing the exit code, so a job that gates on data movement reads the artifact rather than the status.

Output and progress

Both commands render differently to a terminal than to a pipe, and both take flags to force the choice. --no-tui gives the plain tab-separated form a log should capture; --tui forces the Rich form.

generate's progress is on stderr, the same as check and diff - merge it into the pipe to capture it. One tab-separated record per event that has something to report, with a start/done pair per preparatory phase, a pair per table, and a sketched line for each table that got a join-key sketch:

Terminal window
$ dbprint generate 2>&1 | tee generate.log
primary connecting start
primary connecting done
primary listing start
primary listing done
primary inventory start
primary inventory done
primary inventory schema public 4 objects 0.0s
primary public.accession start
primary public.accession ok 2,500 rows 0.2s
primary public.collector start
primary public.collector ok 120 rows 0.1s
primary public.taxon start
primary public.taxon ok 300 rows 0.1s
primary public.taxon_names start
primary public.taxon_names ok - rows 0.1s
primary public.accession sketched 0.0s
primary public.collector sketched 0.0s
primary public.taxon sketched 0.0s
primary summary 4 ok / 0 failed / 0 skipped 932ms

-q silences stderr progress on every command - generate, diff and check all put it there, so a pipeline reaching for -q to quieten a log never affects a command's own stdout payload.

The machine envelope

--format json returns an array with one object per connection:

KeyHolds
connectionthe connection name
manifest_presentwhether a committed manifest was found at all
exit_codethis connection's own code, before the run-wide maximum is taken
default_max_age_daysthe connection's configured threshold
issuesconformance findings
stale_entriestables past their recorded threshold
drift_issuesfindings from --online re-extraction
assertion_issuesfailed assertions
not_runtables that could not be judged, each with a severity
summaryper-category counts

Every entry in issues, drift_issues and assertion_issues carries path, code, severity, detail and spec_ref, so one jq filter works across all three.

Assertions

check also evaluates the assertions block in .dbprint.yaml, which is where a data-quality expectation lives:

.dbprint.yaml
connections:
primary:
assertions:
tables:
seedbank.collector:
columns:
email:
null_rate: 0

Offline, an assertion is evaluated against the committed print — so it tells you what was true when the print was taken. Under --online it is evaluated against a fresh extraction. A predicate over a redacted column's values is refused rather than evaluated against placeholders. The grammar, the severities and the full predicate vocabulary are in Assertions.

Failing on staleness alone

A pipeline that wants to fail only when a print has aged past a deadline of its own can override every recorded threshold for one run:

Terminal window
$ dbprint check --max-age 7d

The explicit flag governs every table directly and reads no rule to find a threshold, so it is independent of whatever the config has grown since.