Skip to main content

tysql

tysql is an experimental, type-level query builder for PostgreSQL. A SQL statement is written as a type — Select[User] — and tysql

  • statically infers the result type of every statement and rejects ill-typed statements with a type error, using the type operators from PEP 827;
  • renders the statement to PostgreSQL text you could hand to a driver; and
  • executes it on a psycopg connection you own — with the tysql[psycopg] extra — returning rows in the inferred shape.

It is a research prototype: the API may change and SQL coverage is narrow (see what works)

The idea

PEP 827 lets a type checker evaluate small type-level programs. tysql uses that to make the shape of a query a static fact:

import psycopg
from typing import Literal
from tysql import Col, Cols, SerialPrimaryKey, Select, Table, run


class User(Table):
    id: SerialPrimaryKey[int]
    age: int
    email: str


conn = psycopg.connect("postgresql://localhost/mydb")

# SELECT id, email FROM "user" — run on the connection you own
rows = run(
    Select[User, Cols[Col[User, Literal["id"]], Col[User, Literal["email"]]]],
    data=None,
    conn=conn,
)
rows[0]["id"]     # int   — inferred
rows[0]["email"]  # str   — inferred
rows[0]["age"]    # type error: "age" is not in the projected row

run's signature is the product: data is typed to the statement's parameters and the return type to its rows — both computed from the statement type by the combinators in tysql/shapes.py. Pass a psycopg conn (the tysql[psycopg] extra) and it renders and executes the statement, returning those rows.

Until PEP 827 lands in a released type checker, the same evaluation is done two ways: at runtime by typemap, and statically by a mypy fork.

Install

Install the core library from PyPI with uv or pip:

uv add tysql 
# or pip install tysql

That includes the runtime side: build statement types and render them to SQL. To also execute statements against PostgreSQL, add the psycopg driver extra:

uv add "tysql[psycopg]"
# or pip install tysql[psycopg]

Enable type-checking

The static rejection of ill-typed statements runs on a mypy fork that evaluates the PEP 827 combinators

uv add "mypy @ git+https://github.com/iliyasone/mypy-typemap.git"
uv run mypy .

Runnable demo snippets ship inside the package under tysql/examples/.

What works

Type inference below holds on both tracks (the mypy fork and runtime eval_typing); SQL rendering is runtime (tysql.render.render).

Capability Type inference SQL rendering
CREATE TABLE (incl. SERIAL PRIMARY KEY, REFERENCES) — ✅
INSERT — params inferred, RETURNING the primary key ✅ ✅
SELECT * — full row, primary key unwrapped (SerialPrimaryKey[int] → int) ✅ ✅
SELECT projection — Cols[...] picks columns ✅ ✅
Column alias — As[col, Literal["name"]] ✅ ✅
WHERE — Params collected into the inferred parameter mapping ✅ ✅
INNER JOIN with an explicit On predicate ✅ ✅
Aggregate Count (result typed int) ✅ ✅
GROUP BY / ORDER BY ✅ ✅

What it rejects

The parameter and row types are exact TypedDicts, so a type checker rejects, at check time: a column that doesn't exist on its table (Col: no such column); a projected column whose table isn't in the FROM/JOIN (Col: table is not in the FROM clause — the "map the column to the table" check); reading a result column that wasn't selected; and an INSERT/WHERE data payload with a missing, extra, mis-named or wrong-typed key (the primary key may not be supplied on insert).

Column checks are per reference site. A ✅ is enforced on both tracks; a ❌ is currently accepted — a known false negative, not a guarantee:

Column reference site exists on its table belongs to the FROM operand types compatible
SELECT projection ✅ ✅ n/a
join ON predicate ✅ ❌ ❌
WHERE ❌ ❌ ❌
GROUP BY / ORDER BY ❌ ❌ n/a

Not implemented

Out of scope for this prototype — the SQL surface tysql does not cover yet:

Not implemented Notes
LEFT / RIGHT / FULL / CROSS JOIN INNER JOIN only
OR / NOT / nested boolean in WHERE flat conjunction (AND) of Eq only
Comparison operators other than = (<, >, LIKE, IN, BETWEEN) Eq only
Aggregates other than Count (SUM, AVG, MIN, MAX) —
HAVING, LIMIT, OFFSET, DISTINCT —
Subqueries, CTEs (WITH), set operations (UNION) —
UPDATE / DELETE statements CREATE TABLE, INSERT, SELECT only
WHERE param type inferred from its column declared explicitly via Param[name, T], not cross-checked
GROUP BY functional-dependency check not enforced (unlike PostgreSQL)

Examples

from typing import Literal
from tysql import (
    As, Col, Cols, Count, Eq, GroupBy, InnerJoin, On, OrderBy, Param, Select, Where, run,
)

# WHERE, with parameters inferred from the clause
Select[User, Cols[Col[User, Literal["id"]]],
       Where[Eq[Col[User, Literal["age"]], Param[Literal["min_age"], int]]]]
# rows: {"id": int};  data: {"min_age": int}

# explicit INNER JOIN — columns from both tables in one row
Select[InnerJoin[User, Post, On[Eq[Col[User, Literal["id"]], Col[Post, Literal["author"]]]]],
       Cols[Col[User, Literal["email"]], Col[Post, Literal["text"]]]]
# rows: {"email": str, "text": str}

# aggregate + GROUP BY + ORDER BY
Select[InnerJoin[User, Post, On[Eq[Col[User, Literal["id"]], Col[Post, Literal["author"]]]]],
       Cols[Col[User, Literal["id"]], As[Count[Col[Post, Literal["id"]]], Literal["n_posts"]]],
       GroupBy[Col[User, Literal["id"]]],
       OrderBy[Col[User, Literal["id"]], Literal["asc"]]]
# rows: {"id": int, "n_posts": int}

Rendering the last statement with tysql.render.render yields:

SELECT "user"."id", count("post"."id") AS "n_posts"
FROM "user" INNER JOIN "post" ON "user"."id" = "post"."author"
GROUP BY "user"."id" ORDER BY "user"."id" ASC;

A larger example schema lives in src/tysql/examples/users.py, alongside the numbered demo snippets the playground serves.

Execute

Everything above is type-level; with the tysql[psycopg] extra, run also executes a statement on a connection you own and returns the rows in the inferred shape — SELECT as a list of dict rows, INSERT the RETURNING primary key, CREATE TABLE None:

import psycopg
from tysql import CreateTable, Insert, Select, run

with psycopg.connect("postgresql://localhost/mydb") as conn:
    run(CreateTable[User], data=None, conn=conn)
    new_id = run(Insert[User], data={"age": 30, "email": "a@b.c"}, conn=conn)
    rows = run(Select[User], data=None, conn=conn)   # [{"id": int, "age": int, "email": str}]

run never commits — you own the connection, its transaction and pooling. For asyncio, arun is the same contract on a psycopg AsyncConnection:

from tysql import arun

rows = await arun(Select[User], data=None, conn=aconn)

Development

uv sync --all-groups

uv run ruff check .   # lint
uv run mypy .         # static type-check — the primary type-level test layer
uv run pytest         # runtime tests

mypy is part of the test contract. Two conventions make it load-bearing:

  • mypy_test_* functions (bodies under if TYPE_CHECKING:) are not collected by pytest but are checked by the fork — they assert inferred types with assert_type.
  • --warn-unused-ignores is on, so every # type: ignore[code] is a negative assertion: if the fork stops emitting that error, the run fails.

PostgreSQL integration tests (the postgres marker) run against a real server via Docker/testcontainers; their dependencies are in the postgres dependency group.

Metadata

Release files for tysql 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tysql 0.3.0
File Size Uploaded
tysql-0.3.0.tar.gz 17.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tysql 0.3.0
File Interpreter ABI Platform
tysql-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.7 kB

Release files / tysql-0.3.0.tar.gz

Download URL tysql-0.3.0.tar.gz
Size 17.0 kB
Tags Source
SHA-256 checksum
How to use checksums
87541d8330060fda492b4be952386993a73fb91998965bd4d2364f0b2bfc2bcd
BLAKE2b-256 checksum
How to use checksums
7723719812846ba5a09ff2426e6e3ea758f6cc2431547dc174f27cd0c2dfad62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / tysql-0.3.0-py3-none-any.whl

Download URL tysql-0.3.0-py3-none-any.whl
Size 23.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c16da0d8135e17d2620fe02983dce9edbc1e1518b692ced005ced2663df78ee1
BLAKE2b-256 checksum
How to use checksums
c61c4fc64be36bcb97994bc24d90726af9813ca535dbe5072022b8f2a69ce8f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page