SQLTy¶
Type-safe raw SQL for Python. sqlty infers the row type of each query from
its SQL, writes it into your source as SQL[T], and checks that it stays
correct.
Beta
sqlty is in beta. APIs and configuration may change before 1.0.
From a raw query to a typed one¶
Mark a query with --sql and cast its columns. Run sqlty rewrite ., and
the file becomes what the rewritten-file lines show:
query = "--sql\nSELECT id::INTEGER, name::TEXT, created_at::TIMESTAMP FROM users"
# rewritten-file: import datetime
# rewritten-file: from sqlty import SQL
# rewritten-file: query = SQL[tuple[int, str, datetime.datetime]]("--sql\nSELECT id::INTEGER, name::TEXT, created_at::TIMESTAMP FROM users")
Each cast becomes a Python type: INTEGER is int, TEXT is str,
TIMESTAMP is datetime.datetime (see the
type mapping). With a
schema file, the casts are optional.
SQL[T] is a str subclass. The query still works anywhere a string does,
and editors and type checkers now know the row type.
Three commands¶
| Command | What it does |
|---|---|
sqlty rewrite . |
Wrap --sql strings in SQL[T](...) with the inferred type |
sqlty check . |
Report drift, row type mismatches, SELECT * and unwrapped queries |
sqlty strip . |
Remove the wrappers, back to plain --sql strings |
rewrite and strip only add or remove the wrapper and its import; the
string itself is never modified. For a file of plain --sql strings,
strip(rewrite(x)) == x. See the Quickstart.
Features¶
- Row types. Use
TypedDict,NamedTuple, dataclasses or Pydantic models instead of tuples;sqlty checkvalidates field count, types and names. Closed TypedDicts (PEP 728) are matched by key, and subclasses are checked against their bases' openness. - psycopg.
execute(conn, SQL[User](...))returns aCursor[User]with the matching row factory;scalarfetches one typed value;row_formreturnsTat runtime. - Schema inference. Read column types from
CREATE TABLEstatements for PostgreSQL, SQLite and MySQL. - Magic codec. Files declaring
# coding: sqltyare rewritten when imported, without a rewrite step. - Untyped queries. Mark queries sqlty cannot type as
SQL[UNTYPED], which type checkers refuse to treat as a concrete row. - Lint rules. Row type, schema, parameter and query checks, each with a configurable severity.
- Rust engine. SQL parsing, type inference and source rewriting in Rust, exposed to Python with PyO3.
Which features to use¶
| Approach | Best for |
|---|---|
| Rewrite and check | Committed code, CI, teams |
| Row types | Named fields, domain objects |
| Schema inference | Projects with CREATE TABLE schemas |
| Magic codec | Scripts and prototypes |
| psycopg helpers | psycopg 3 code that wants typed rows at runtime |
They combine: a schema plus row types plus the psycopg helpers is the most complete setup. Use cases describes the situations sqlty is built for.
Next steps¶
- Install sqlty.
- Follow the Quickstart.
- Adding sqlty to an existing project? Read Migration and CI and pre-commit.
New to the idea? Start with Why SQLTy?. Contributing, testing and architecture are covered in DEVELOPMENT.md.
Example projects¶
Each directory in
examples/ has a
README:
01_quick_start/: rewrite, check and strip on cast queries.02_schema_inference/: types from a schema passed with--schema.03_pyproject_config/: schema, dialect and rules inpyproject.toml.04_check_and_types/: row types validated bysqlty check.05_import_style/: a customimport-style.magic_codec/: the magic codec.
Run them from a checkout:
git clone https://github.com/jirikuncar/sqlty.git
cd sqlty && uv sync --all-groups
uv run sqlty check examples/01_quick_start/
uv run sqlty check --schema examples/02_schema_inference/schema.sql examples/02_schema_inference/
uv run python -m sqlty.magic examples/magic_codec/demo.py # print the decoded source