Skip to main content

justorm

A lightweight, schemaless SQL builder that mixes into native database cursors.

import psycopg
import justorm.psycopg

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

justorm is not an ORM in the "hide the database" sense. There are no models, no sessions, no identity maps, no migrations. What it does is give you a fluent, Pythonic way to write SQL against a real DB-API cursor, while leaving the cursor — and the database it talks to — fully in charge.

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(justorm.psycopg.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.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 justorm 1.0.0
File Size Uploaded
justorm-1.0.0.tar.gz 139.3 kB Details

Built distribution (wheel)

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

Total release size: 182.3 kB

Release files / justorm-1.0.0.tar.gz

Download URL justorm-1.0.0.tar.gz
Size 139.3 kB
Tags Source
SHA-256 checksum
How to use checksums
36e437d29cf2fcc84775a754728d0dbe11c77622e16f3b861b24070000e7814b
BLAKE2b-256 checksum
How to use checksums
234d380ab602625b9382e570067c59a9c8768ecfa07e85fd43808e924db66eb2
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 8, 2026.

Transparency log

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

Download URL justorm-1.0.0-py3-none-any.whl
Size 43.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0ee2f665a672eededfc8a01db5adfd73060af9b929361a145e44f6c47e50f770
BLAKE2b-256 checksum
How to use checksums
0ec8ebbc673ee5e80d65b594b235dc51c973a2d36800b4d98f79ee008dae3c56
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 8, 2026.

Transparency log

Release history Release notifications | RSS feed

1.0.1

2 release files

This release

1.0.0 This release

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