Skip to main content

justorm

justorm is a lightweight, schemaless SQL builder for Python that mixes into native database cursors.

justorm 是一个轻量的、无 schema 的 Python SQL 构建器,混入原生的数据库游标。

There are no models, no sessions, no identity maps, and no migrations; justorm gives you a fluent way to write SQL against a real DB-API cursor, while leaving the cursor in charge.

没有 model、没有 session、没有 identity map、没有 migration; justorm 给你一套流畅的方式,在真实的 DB-API 游标上写 SQL, 游标始终掌控一切。

PostgreSQL (psycopg v3)

import psycopg
import justorm.psycopg

conn = psycopg.connect(
    "postgresql://localhost/mydb",
    cursor_factory=justorm.psycopg.Cursor,
)
with conn.cursor() as cur:
    rows = cur.users.where(id=1).select()
    cur.users.where(id=1).update(name="Alice")
    cur.users.append(name="Bob")

MySQL (PyMySQL)

import pymysql
import justorm.pymysql

conn = pymysql.connect(host="localhost", user="me", database="mydb")
cur = conn.cursor(justorm.pymysql.Cursor)
rows = cur.users.where(id=1).select()
cur.users.where(id=1).update(name="Alice")
cur.users.append(name="Bob")

Design principles

justorm is built around five principles.

1. Mix into the cursor, do not replace it.

Every driver entry point is a subclass of that driver's native cursor (or, for SQLite, a thin wrapper around it). The rest of your code — connection handling, transactions, error handling, fetchone() — keeps working exactly as it did before. justorm only adds methods.

with conn.cursor() as cur:
    cur.execute("SELECT 1")            # native psycopg
    cur.users.where(id=1).select()     # justorm

2. Schemaless by design.

justorm does not know your tables, your columns, or your types. It never inspects the schema and never validates column names. If you write cur.users.where(nonexistent=1).select(), the database is the one that complains.

3. Fluent, chainable, immutable.

Builders are immutable: every chained call returns a new builder, and nothing is sent to the database until a terminal method is called.

base = cur.users.where(active=True)   # nothing executed yet
adults = base.where(cur.users["age"] >= 18)
rows = adults.select()                # now it runs

4. Dialects stay visible.

justorm does not paper over the differences between PostgreSQL, MySQL, SQLite, SQL Server, and Oracle. If you are talking to MySQL, you write .on_duplicate(...); if you are talking to PostgreSQL, you write .on_conflict(...). If a feature does not exist in your database, justorm raises an error at the last possible moment rather than silently doing something surprising.

5. Escape by default, raw on request.

Structured input — keyword arguments, set / list / tuple of column names, values passed as parameters — is always escaped and quoted according to the dialect. When you need to write something the builder cannot express, reach for cur['...'], which is emitted verbatim.

cur.users.where(name="a b").select()                 # safe
cur.users.where(cur["lower(name)"] == "a").select()  # verbatim; on you

Installation

$ pip install justorm[psycopg]     # PostgreSQL via psycopg (v3)
$ pip install justorm[psycopg2]    # PostgreSQL via psycopg2
$ pip install justorm[pymysql]     # MySQL / MariaDB via PyMySQL
$ pip install justorm[sqlite]      # SQLite (sqlite3 is stdlib)
$ pip install justorm[pymssql]     # SQL Server via pymssql
$ pip install justorm[oracledb]    # Oracle via python-oracledb
$ pip install justorm[all]         # everything
$ pip install justorm[dev]         # plus the test tooling

Each driver module imports only its own driver, so installing justorm for one database does not pull in the others. The top-level import justorm imports no driver at all.

Supported drivers

Module Driver Database
justorm.psycopg psycopg (v3) PostgreSQL
justorm.psycopg2 psycopg2 PostgreSQL
justorm.pymysql PyMySQL MySQL / MariaDB
justorm.sqlite sqlite3 (standard library) SQLite 3
justorm.pymssql pymssql SQL Server
justorm.oracledb python-oracledb Oracle

A short tour

SELECT

cur.users.select()                                   # [{"id": 1, "name": "Alice"}, ...]
cur.users.select({"name"})                           # [{"name": "Alice"}, ...]
cur.users.select(("id", "name"))                     # [(1, "Alice"), ...]
cur.users.select("name")                             # ["Alice", ...]
cur.users.where(id=1).select("name", key="id")       # {1: "Alice"}
cur.users.where(id=1).get()                          # one row as a dict, or None
cur.users.orderby("name").limit(10).select()

INSERT

cur.users.append(name="Alice")                        # one row
cur.users.append({"name": "Alice", "age": 30})        # same
cur.users.insert([{"name": "A"}, {"name": "B"}])      # single statement, many rows
cur.users.insert_many(rows, columns=["name"])         # executemany
cur.users.returning("id").append(name="Alice")        # -> 1

UPDATE / DELETE

cur.users.where(id=1).update(name="Alice")
cur.users.where(id=1).delete()
cur.users.all().delete()                              # explicit, required

JOIN

u = cur.users.as_("u")
o = cur.orders.as_("o")
u.join(o, on=u["id"] == o["user_id"]).select()

Subquery

sub = cur.orders.where(cur.orders["total"] > 100).select_subquery({"user_id"})
s = sub.as_("s")
cur.users.join(s, on=cur.users["id"] == s["user_id"]).select()

UPSERT

# PostgreSQL / SQLite
cur.users.on_conflict(
    on=cur.sql.columns("id"),
    do=cur.sql.update(name=cur["EXCLUDED.name"]),
).append(id=1, name="Alice")

# MySQL
cur.users.on_duplicate(update={"name": cur["VALUES(name)"]}).append(
    id=1, name="Alice",
)

Raw SQL

# One call to execute and shape the result:
rows = cur.query("SELECT a, b FROM t WHERE c = %s", [1])   # list[dict]
n    = cur.query("UPDATE t SET a = %s WHERE b = %s", [1, 2])  # int

# fetch* also accepts an output format:
cur.execute("SELECT a, b FROM t")
cur.fetchall(format=dict)
cur.fetchone(format=namedtuple)

What justorm is not

  • Not a session manager. You own the connection, the cursor, and the transaction.
  • Not a schema tool. There are no models, no migrations, no field types.
  • Not a dialect abstraction. Dialect differences are exposed, not hidden.
  • Not asynchronous. The current release is synchronous only.
  • Not a connection pool. Use the driver's pool, or a third-party one.

Documentation

Full documentation is available in two languages:

The documentation covers:

  • Quickstart
  • API reference
  • Dialects (per-database feature matrix)
  • UPSERT
  • JOIN
  • Subqueries
  • Expressions
  • Security
  • Migration from raw SQL

Development

The test suite has two layers:

  • Pure tests (tests/test_builder.py, tests/test_renderer.py, tests/test_fetch.py) run everywhere. They exercise the builder and renderer against a fake cursor and do not need a database.
  • Integration tests (tests/test_psycopg.py, tests/test_psycopg2.py, tests/test_pymysql.py, tests/test_sqlite.py, tests/test_pymssql.py, tests/test_oracledb.py) run against a real database. They are opt-in per driver and skip automatically when the driver or the server is not available.

To run everything:

$ pip install -e ".[dev,all]"
$ JUSTORM_PG_DSN="postgresql://user:pw@localhost/test" \
  JUSTORM_MYSQL_DSN="mysql://user:pw@localhost/test" \
  pytest

SQLite tests always run. Every other driver's tests skip unless the corresponding JUSTORM_*_DSN variable is set in the environment:

  • JUSTORM_PG_DSN — PostgreSQL for psycopg (v3)
  • JUSTORM_PG2_DSN — PostgreSQL for psycopg2
  • JUSTORM_MYSQL_DSN — MySQL / MariaDB
  • JUSTORM_MSSQL_DSN — SQL Server
  • JUSTORM_ORACLE_DSN — Oracle

A convenience script, run_tests.py, runs the full pipeline (build the wheel, install it, run the tests) and writes a log file:

$ python run_tests.py

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.

Metadata

Release files for justorm 1.0.1

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

Source distribution (sdist)

Source distribution for justorm 1.0.1
File Size Uploaded
justorm-1.0.1.tar.gz 142.3 kB Details

Built distribution (wheel)

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

Total release size: 185.7 kB

Release files / justorm-1.0.1.tar.gz

Download URL justorm-1.0.1.tar.gz
Size 142.3 kB
Tags Source
SHA-256 checksum
How to use checksums
34b2549a8aa5cadfc8526f1cc28ef3838512f53735c1cc4974d51a4ee15a4900
BLAKE2b-256 checksum
How to use checksums
d9726b4da497a3b0d1646f6ce8a1fc73316a25b5e66218c66bce3872137428cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / justorm-1.0.1-py3-none-any.whl

Download URL justorm-1.0.1-py3-none-any.whl
Size 43.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a5ef312fd727230580aa8e332b38bd6511c0e43ecfed0acb8986605ad8b79e29
BLAKE2b-256 checksum
How to use checksums
b552c9d6f7a607597626db1ff199f92cf6fc3424715832381a1207efcf4ff30f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.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