Show HN: Rowx – SQLx for Python
https://github.com/mplemay/rowx
Hey HN, I'm Matt. I got tired of clunky ORMs when a query I wanted to write as SQL turned into select() calls and loader options. I wanted that SQL in Python, with the row type checked against Postgres. That's the part of SQLx I wanted, so I wrote rowx.SQLx checks a query when the program compiles, and offline mode stores a .sqlx directory so a later compile can skip the database. rowx check is a separate command. It prepares each inline query(...) against a live Postgres 18, and it does not write that description to a disk cache.
type UserRow = tuple[int, str, str | None]
name = "alice"
user = await query[UserRow](
t"SELECT id, name, email FROM users WHERE name = {name}"
).one(pool)
This needs Python 3.14, because the query is a PEP 750 t-string. {name} is a server parameter and is never pasted into the SQL. A table or column name cannot be a parameter on query. query_unchecked can take one after your code allowlists it, and that SQL is not described.A type checker only sees UserRow. The database comparison is:
rowx check src --database-url postgresql://app@localhost/app
The checker does not import or execute application code. The t-string has to be written on the query(...) call. If it is built somewhere else, the checker reports that and does not describe the SQL. A column annotated str and described as int is reported on the annotation. uv add "rowx[postgres]" installs the checker and the driver.SQLAlchemy's tstring() accepts a t-string. Those docs show it going through a DBAPI, and they do not describe preparing it against your schema. psycopg's typing covers the Python API and the rows that come back. It does not prepare the SQL against the schema. sqlc keeps queries in their own files, analyzes them with its own engine, and generates the functions you call. A database connection for that analysis is optional. rowx check always uses one.
rowx[embedded] starts an in-process Postgres 18 for development, checks, and tests, on macOS arm64 and Linux x86_64/aarch64. On other platforms the extra installs, and using the embedded database raises InterfaceError. It is not for production.
The driver is Rust, on tokio-postgres. 0.3.0, MIT, Postgres 18 only.
upper(name) stays nullable even when name is NOT NULL. A recursive CTE column defaults to nullable. The cases inferred NOT NULL, and the other limits, are at https://mplemay.github.io/rowx/known-limits/
https://github.com/mplemay/rowx https://mplemay.github.io/rowx/
There are no comments yet.