Skip to content

Configuration

sqlty reads [tool.sqlty] from pyproject.toml. The CLI uses, for each file, the nearest pyproject.toml above it, unless --config-file or --isolated picks one file or none; the magic codec searches from the current directory. Command-line flags override the file. An unknown key, rule or severity is an error (exit 2) that names the file and suggests the closest match.

In every case a pyproject.toml without [tool.sqlty] is skipped, so a package's packaging-only pyproject.toml in a monorepo does not hide the root config. The closest table wins; settings are not merged with tables in parent directories. To change one setting for a nested project, give it its own complete [tool.sqlty] table. Command-line flags (--dialect, --schema, --untyped, --exclude, --extend-exclude, --error/--warn/--ignore) and -c KEY=VALUE assignments override the table for every path.

[tool.sqlty]
dialect = "postgres"
schema = "schema.sql"
import-style = "from sqlty import SQL"
untyped = "skip"

[tool.sqlty.rules]
select-star = "ignore"
stale-type = "error"

[[tool.sqlty.functions]]
import-path = "db.sql"
query-param = "query"
Key Type Default Used by Summary
dialect "postgres", "sqlite", "mysql" "postgres" rewrite, check SQL dialect of queries and schema
schema string or list of strings none rewrite, check Schema .sql files, directories or globs, relative to pyproject.toml
rules table of rule = severity per-rule defaults check Severity per lint rule: "error", "warn" or "ignore"
overrides array of tables [] check Rule severities for the files include matches and exclude does not, on top of rules
import-style string "from sqlty import SQL" rewrite, strip, check, magic codec How rewritten code imports and spells the wrapper
untyped "skip", "sentinel" "skip" rewrite Whether queries of unknown row type are wrapped as SQL[UNTYPED]
json-types "typing.Any", "typing.TypedDict" "typing.Any" rewrite, check --fix How a JSON column of known shape is typed: typing.Any, or a generated TypedDict
param-types true, false false rewrite, check --fix Whether SQL[R] calls get the type of their parameters, SQL[R, P]
preview boolean false check Turn on features still in preview: query variants
max-query-variants positive integer 64 check With preview, the most variants of a query built in branches that check checks
functions array of tables [] check Helper functions whose string argument is a query
types table of SQL type or table.column = Python type {} rewrite, check Python types a converter above the driver gives a SQL type or a column
format-command list of strings none rewrite, check --fix Formatter run on each file rewrite or check --fix changes, with the path appended
exclude list of globs see Default excludes rewrite, strip, check Globs skipped while walking a directory; replaces the default list
extend-exclude list of globs [] rewrite, strip, check Globs skipped in addition to exclude or the defaults
respect-gitignore boolean true rewrite, strip, check Skip the files .gitignore ignores while walking a directory

dialect

SQL dialect for queries and the schema: "postgres" (default), "sqlite", "mysql", or another dialect sqlparser-rs knows. CLI: --dialect (the first three only), which overrides this key. See Dialects.

schema

A .sql file with CREATE TABLE statements, or a list of them, relative to the directory of pyproject.toml. Each entry is a file, a directory (every *.sql below it) or a glob (tables/*.sql, **/*.sql). The files are joined in the order of the list; directory and glob matches are sorted by path, and a file named twice is read once. CLI: --schema, repeatable (relative to the current directory). See Schema inference.

[tool.sqlty]
schema = ["extensions.sql", "tables/"]

rules

Severity per lint rule: "error", "warn" or "ignore". An unknown rule name or severity is an error.

[tool.sqlty.rules]
select-star = "ignore"
column-name-mismatch = "error"

overrides

Rule severities for some paths only, such as tests or migrations:

[tool.sqlty.rules]
select-star = "error"

[[tool.sqlty.overrides]]
include = ["tests/**", "conftest.py"]

[tool.sqlty.overrides.rules]
select-star = "ignore"

The table follows ty's [[tool.ty.overrides]]: include and exclude are lists of globs, and rules takes the same severities as rules. An entry applies to the files include matches (every file when it is left out) and exclude does not:

[[tool.sqlty.overrides]]
include = ["generated/**"]
exclude = ["generated/important.py"]

[tool.sqlty.overrides.rules]
select-star = "ignore"

Globs are relative to the pyproject.toml directory and match like exclude: a glob without a slash matches a file or directory name at any depth, and a matching directory covers every file in it. Every entry that matches a file is merged over rules, later entries winning. --error, --warn, --ignore and -c rules.X=... still win over both.

An override changes severities only; dialect, schema and the other keys apply to the whole project. check and check --fix read it.

import-style

How rewritten code imports and spells the wrapper. Default: "from sqlty import SQL".

Value Wrapper
"from sqlty import SQL" SQL[T](...)
"from sqlty import SQL as Q" (any alias) Q[T](...)
"import sqlty" sqlty.SQL[T](...)
"import sqlty as sy" (any alias) sy.SQL[T](...)

strip and check use the same setting to recognize wrappers, so change it only on stripped sources. With untyped = "sentinel", UNTYPED follows the style: from sqlty import UNTYPED, or sqlty.UNTYPED (sy.UNTYPED) for module imports. Example: examples/05_import_style/.

preview

true turns on features that may still change before they are on by default:

  • Query variants: a query built from names bound in branches is checked once per path that builds it, instead of being skipped as dynamic-query.
[tool.sqlty]
preview = true

max-query-variants

With preview, the most variants of one query that check checks, 64 by default. A query with more is reported as dynamic-query, or unchecked-query for a psycopg.sql composition, with the count: it is never checked on part of its variants.

[tool.sqlty]
preview = true
max-query-variants = 256

untyped

"skip" (default) leaves row-returning queries of unknown type as plain strings. "sentinel" wraps them as SQL[UNTYPED](...). CLI: sqlty rewrite --untyped. See Untyped queries.

json-types

"typing.Any" (default) types a JSON column typing.Any. "typing.TypedDict" makes rewrite and check --fix type a JSON column whose shape the query spells out with generated TypedDicts, written after the imports under a # sqlty: generated line. strip removes them. See Generated TypedDicts.

[tool.sqlty]
json-types = "typing.TypedDict"

param-types

false (default) leaves the parameters of a query untyped, SQL[R]. true makes rewrite and check --fix write their type, SQL[R, P]: a tuple, or a generated TypedDict for named parameters, written as json-types writes its classes. strip removes them. See Typed parameters.

[tool.sqlty]
param-types = true

Type checkers become stricter than the driver

A type checker then rejects calls that psycopg runs: a list for a tuple, text for another type, a dict[str, Any] or extra keys for a TypedDict. That is why it is off by default.

functions

An array of tables registering helper functions. sqlty check checks their calls like those of an annotated helper without editing the signature (a method is "pkg.db.Db.fetch_all"):

[[tool.sqlty.functions]]
import-path = "db.fetch_all"
query-param = "query"
Key Required Description
import-path yes Module path and function name, for example "app.db.sql"; a method is "pkg.db.Db.fetch_all". Use "__main__.sql" or the file name for a script run directly
query-param yes Name of the parameter that holds the query
return-type-param no Parameter whose argument, such as return_tp=int, is the row type of that call instead of an inferred one; check compares it with the query columns

Entries without import-path or query-param are skipped with a warning.

types

The Python type a converter above the driver gives a SQL type or a column. sqlty types the value the reference driver returns; when your code reads values through something that converts them (an ORM, sqlite3 with detect_types), say what it returns:

[tool.sqlty.types]
DATE = "datetime.date"
TIMESTAMP = "datetime.datetime"
BOOLEAN = "bool"
"events.duration" = "datetime.timedelta"

A key with a dot is table.column; any other is a SQL type name, read in the project's dialect (NUMERIC covers DECIMAL(10, 2)). The type applies to schema columns and casts; an expression over a column keeps its type when it returns the column's values (MAX(day)). See Drivers and ORMs.

rewrite and check --fix import the module a type names (import app.models for app.models.Status), as they import datetime. strip leaves that import, since it reads no configuration.

format-command

A formatter rewrite and check --fix run on each file they change, with the file's path appended to the command. A long SQL[tuple[...]] wrapper then leaves the file formatted, instead of failing your formatter's check until it runs again.

[tool.sqlty]
format-command = ["ruff", "format", "--quiet"]

Files that are not changed are never formatted, and --check and --diff write nothing, so they run no formatter. A formatter that fails is reported as a warning and the file keeps sqlty's changes. rewrite stays idempotent with a formatter: a wrapper the formatter split over several lines is recognized as already written.

rewrite adds its imports (from sqlty import SQL, import datetime) at the top of the import block, below the module docstring, without sorting them into your groups. To keep an import sorter's check passing, sort them as part of the command, for example with a script:

#!/bin/sh
# scripts/format.sh: sort imports, then format
ruff check --select I --fix --quiet "$@" && ruff format --quiet "$@"
[tool.sqlty]
format-command = ["sh", "scripts/format.sh"]

exclude and extend-exclude

Directories the CLI walks skip these globs. exclude replaces the default list; extend-exclude adds to whichever list applies.

The default excludes are:

.bzr, .direnv, .eggs, .git, .git-rewrite, .hg, .ipynb_checkpoints, .mypy_cache, .nox, .pants.d, .pyenv, .pytest_cache, .pytype, .ruff_cache, .svn, .tox, .venv, .vscode, __pypackages__, _build, buck-out, dist, node_modules, site-packages, venv

A glob without a slash matches a file or directory name at any depth; one with a slash matches the path relative to the pyproject.toml that holds it, and ** recurses. Files named on the command line are always processed, unless --force-exclude is given. The CLI's --exclude and --extend-exclude take the same globs, relative to the current directory.

[tool.sqlty]
extend-exclude = ["migrations", "gen/*.py"]

respect-gitignore

Directories the CLI walks also skip what git ignores: every .gitignore from the repository root down, and .git/info/exclude, with negations (!keep.py) and nested files read as git reads them. Build output and generated code are usually listed there already, so they need no exclude entry. Outside a git repository nothing is skipped this way; the global core.excludesFile is not read.

[tool.sqlty]
respect-gitignore = false

Files named on the command line are always processed. The CLI's --respect-gitignore and --no-respect-gitignore override the setting.