Skip to content

CI and pre-commit

Run sqlty where it blocks bad queries: before a commit and on every pull request. sqlty rewrite --check and sqlty strip --check exit 1 when files would change; sqlty check exits 1 when a rule with severity error fires. Any CI system can use them.

Pre-commit

The sqlty repository publishes two hooks. Both receive the staged Python files as arguments:

Hook id Runs
sqlty-rewrite sqlty rewrite <files>
sqlty-check sqlty check <files>
# .pre-commit-config.yaml
default_language_version:
  python: python3.14  # sqlty requires Python >= 3.14
repos:
  - repo: https://github.com/jirikuncar/sqlty
    rev: v0.2.2
    hooks:
      - id: sqlty-rewrite
      - id: sqlty-check

pre-commit autoupdate moves rev to the latest release. pre-commit builds the hook environment with the Python it runs on unless told otherwise, and sqlty needs 3.14 or newer: keep default_language_version, or set language_version on each hook. pre-commit installs sqlty from source into that environment, which compiles the Rust extension, so the first run needs a Rust toolchain (cargo) and takes a few minutes. Pass options with args, for example args: [--dialect, sqlite].

sqlty-check fails the commit only for rules with severity error. Drift that can break the code, narrow-type, is one by default; an annotation only wider than the rows, stale-type, is a warning unless you promote it (see below). The sqlty hook id is a deprecated alias of sqlty-rewrite: it rewrites the staged files.

To skip the build or reuse the sqlty already installed in your environment, use local hooks instead:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: sqlty-rewrite
        name: sqlty rewrite
        entry: sqlty rewrite .
        language: system
        pass_filenames: false
        types: [python]
      - id: sqlty-check
        name: sqlty check
        entry: sqlty check .
        language: system
        pass_filenames: false
        types: [python]

sqlty rewrite edits files in place, which makes pre-commit fail the commit and leave the rewritten files for you to stage. Use uv run sqlty ... as the entry if sqlty lives in a uv-managed environment.

Each file uses the nearest pyproject.toml above it that has a [tool.sqlty] table, so one command covers several projects with their own settings. In a monorepo, a package pyproject.toml without [tool.sqlty] is skipped and its files use the root config; one with its own table replaces the root config entirely (tables are not merged).

Fail on unwrapped queries

sqlty rewrite --check changes nothing and exits with 1 if any file would be rewritten:

$ sqlty rewrite --check .
Would rewrite queries.py
1 file(s) would be rewritten

--diff prints the changes as a unified diff instead. Add --untyped sentinel (or set untyped = "sentinel") to also require wrapping queries whose row type is unknown; see Untyped queries.

Fail on drift and mismatches

sqlty check exits with 1 only when a rule with severity error fires. By default that covers row type mismatches, names the schema doesn't have, SELECT * without a schema, and annotations that don't admit the rows (narrow-type); annotations only wider than the rows (stale-type) and unwrapped strings (stale-rewrite) are warnings. Promote the rules you want to enforce:

[tool.sqlty]
schema = "schema.sql"

[tool.sqlty.rules]
stale-type = "error"
untyped-query = "error"

stale-rewrite reports exactly the strings rewrite would wrap, so promoting it to error is equivalent to rewrite --check.

Keep the schema file current, for example by regenerating it from the database in the same pull request as the migration (pg_dump --schema-only, sqlite3 app.db .schema, mysqldump --no-data).

GitHub Actions

# .github/workflows/sqlty.yml
name: sqlty
on: [pull_request]
jobs:
  sqlty:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # sqlty breaking reads the base branch
      - uses: astral-sh/setup-uv@v7
      - run: uv sync
      - run: uv run sqlty rewrite --check .
      - run: uv run sqlty check --output-format github .
      - run: uv run sqlty breaking --output-format github --against origin/${{ github.base_ref }}

Each step catches something the others don't:

Step Fails when
sqlty rewrite --check a --sql string is not wrapped yet
sqlty check a rule with severity error fires: a row type mismatch, an unknown table or column, or an annotation that doesn't admit the rows (narrow-type) by default; see Fail on drift and mismatches
sqlty breaking the pull request's schema breaks the base branch's queries, or its queries break on the base branch's schema: a dropped column, a column made nullable

A wider annotation is only a warning

A migration that makes a column nullable leaves SQL[tuple[str]] too narrow, and the code can now get None: sqlty check fails with narrow-type. A migration that makes a column NOT NULL leaves SQL[tuple[str | None]] wider than it needs to be: stale-type warns and sqlty check exits 0.

--output-format github prints each diagnostic, and each breaking change, as a GitHub Actions annotation, so it shows on its line in the pull request's diff. See Schema changes in CI for the --direction options.

Keeping sources stripped

If you commit plain --sql strings and rely on the magic codec instead, sqlty strip --check . exits with 1 when a file contains SQL[T](...) wrappers.