Skip to main content

Sustained.py

Sustained is a Python query builder, lightweight ORM, and schema migration tool, originally inspired by Objection.js.

You describe your tables in one set of model classes, and Sustained uses those classes both to build and run queries and to keep the schema in step.

The syntax will look familiar if you have worked with Objection, Kysely, or even Knex before:

adults = User.query().where(User.c.age >= 18).orderBy('name').run()

Managing queries through Sustained

With Sustained, you can:

  • Build SQL programmatically. Selects, aggregates, window functions, CASE expressions, every join type, CTEs (including recursive), unions, INTERSECT and EXCEPT, and subqueries in SELECT, FROM, WHERE, and JOIN clauses.
  • Target seven dialects. ANSI (default), PostgreSQL, MySQL and MariaDB, MSSQL, Presto, AWS Athena, and DuckDB. Quoting, placeholders, upsert syntax, LIMIT/OFFSET spelling, and function names all follow the dialect. Unsupported features raise DialectError at build time instead of failing in the database. Migrating queries between dialects is a one-line change.
  • Execute queries safely. Every statement runs parameterized against any DB-API 2.0 connection or a ConnectionPool. Transactions nest through savepoints, and update() and delete() refuse to run without a WHERE clause.
  • Write data. insert(), update(), delete(), upserts through onConflict(), INSERT ... SELECT, CREATE TABLE AS, and RETURNING.
  • Hydrate results. Rows become model instances, plain dicts, pandas DataFrames, or pyarrow Tables. Relations eager load with withGraphFetched(). A type checker reads Show.query().run() as List[Show].
  • Run queries async. The same queries run through driver adapters, including asyncpg and aiosqlite, with await query.arun(). AsyncConnectionPool pools those adapters, so concurrent queries do not queue behind one connection.

Schema management with Sustained

Sustained also manages schema changes. It generates migrations from your models, tests each change before it runs, and rolls a migration back when you ask. Schema and Migrations describes these features in detail.

With Sustained, schema migrations are:

  • Generated from your models. Migrator.up(models=[...]) diffs the live database against your models, generates the migration, records it, and applies it. If you run it again after a model change, it applies only the difference. down() rolls it back.
  • Rehearsed before they land. sustained rehearse applies every pending migration, runs the downgrade steps to test the revert plan, and rolls the whole thing back. If a migration fails to run or fails to reverse, the rehearsal reports it before the migration reaches the real schema. A config module can send the rehearsal to a scratch database instead.
  • Planned in one screen. sustained plan shows your pending migrations, outstanding problems that validate would report, and any gap between your models and the database's current state.
  • Read for their impact on a live database. sustained impact reports, for each statement a run would apply on PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, or DuckDB, the tables it locks, whether reads or writes wait, whether it scans, rebuilds an index, or rewrites the table, and how long the lock is held. It flags a lock held until commit across a later backfill, and names the safer form, such as CREATE INDEX CONCURRENTLY or NOT VALID followed by VALIDATE CONSTRAINT. Statement impact lists the rules.
  • Verified before every run. Sustained keeps a per-database tracking table that records a sequence number, a SHA-256 checksum, an apply timestamp, execution time, and a success flag per migration. validate refuses a run when a migration was edited after it ran, arrives out of order, or left a failed attempt behind. After manual corrections, repair deletes failed runs from the tracking table and updates script checksums.
  • Gated by custom safeguards. A guard is a built-in rule such as no_drops(), index_must_be_concurrent(), or max_statements(n), or a function you write. Guards read every statement a run would apply and block the deployment when a rule fails.
  • Safe by default. Drops need an explicit allow_drops=True, renames need explicit hints, and NOT NULL changes need a default or backfill, so destructive changes never run by default.
  • Written your way. Migrations can be Python Migration objects, <id>.up.sql and <id>.down.sql files with ${placeholders}, or <id>.repeat.sql files for views and seed data, which re-run whenever their contents change.
  • Ready for deploys. The sustained console script runs plan, impact, status, rehearse, migrate, down, validate, repair, script, and baseline, with exit codes for pipelines and before_migrate, after_migrate, and on_error callbacks around a run. Concurrent deploys queue on an advisory lock. baseline adopts a database whose schema already matches the migrations. script('up') renders the SQL for a DBA instead of running it. AsyncMigrator does all of this on an async adapter.

Installation

python3 -m pip install sustained

Usage

from sustained import Model, RelationType

class Venue(Model):
    tableName = 'venues'

class Show(Model):
    tableName = 'shows'
    relationMappings = {
        'venue': {
            'relation': RelationType.BelongsToOneRelation,
            'modelClass': Venue,
            'join': {
                'from': 'shows.venue_id',
                'to': 'venues.id'
            }
        }
    }

# Build a query
query = Show.query().select('shows.title', 'venues.name').leftOuterJoinRelated('venue')

print(query)
# SELECT shows.title, venues.name
# FROM shows
# LEFT OUTER JOIN venues
#   ON shows.venue_id = venues.id


# Execute against any DB-API 2.0 connection
import sqlite3

conn = sqlite3.connect('app.db')
Animal.bind(conn)

# Parameterized execution with model hydration
animals = Animal.query().where('species', '=', 'dog').orderBy('name').run()

# Or take the SQL and parameters and execute them yourself
sql, params = Animal.query().where('species', '=', 'dog').to_sql()
# sql:    "SELECT * FROM animals WHERE species = ?"
# params: ('dog',)

Models define their own schema, so a column change is a migration:

from sustained.migrations import Migrator
from sustained.schema import Integer, String, Text

class User(Model):
    tableName = 'users'
    tableColumns = {
        'id': Integer(primary_key=True, autoincrement=True),
        'email': String(120, unique=True, nullable=False),
    }

migrator = Migrator(conn, [])
migrator.up(models=[User])         # creates the users table

User.tableColumns['bio'] = Text()
migrator.plan([User])              # the migration the next run would generate
migrator.up(models=[User])         # adds only the bio column
migrator.down()                    # rolls it back

From the shell, a config module names the connection, the migrations directory, and the models:

# sustained_config.py
import sqlite3

def get_connection():
    return sqlite3.connect('app.db')

migrations_dir = 'migrations'
models = [User]
$ sustained plan        # pending migrations, validation problems, model drift
$ sustained rehearse    # run it all, forwards and back, then roll back
$ sustained migrate     # apply it for real
$ sustained down        # --steps N (0 or more) or --to ID

See Schema and Migrations for SQL file migrations, repeatables, checksum validation, baseline, and the Athena rules.

Documentation

The documentation includes:

The support policy lists the supported databases and Python versions and states the deprecation policy. The changelog lists released versions.

Development

To install from source:

git clone https://github.com/wetherc/sustained.git
cd sustained
python3 -m pip install -e .

This project uses pre-commit to format code, lint, type check, and run the test suite before each commit:

pip install pre-commit
pre-commit install

Metadata

Release files for sustained 2.28.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 sustained 2.28.1
File Size Uploaded
sustained-2.28.1.tar.gz 890.6 kB Details

Built distribution (wheel)

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

Total release size: 1.5 MB

Release files / sustained-2.28.1.tar.gz

Download URL sustained-2.28.1.tar.gz
Size 890.6 kB
Tags Source
SHA-256 checksum
How to use checksums
73f925ba096ab30b9cd5ebf651fb7e291397db696899e1a477f65ead3370cfd3
BLAKE2b-256 checksum
How to use checksums
8535908d020f22213529273c3b7f47439fd7ce3687c7a881962fc1ed3513c58e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / sustained-2.28.1-py3-none-any.whl

Download URL sustained-2.28.1-py3-none-any.whl
Size 572.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
daf4fa5d64b12f36ecbb60e6031a1fa127abdff8c81252b4a969fc249e808e9c
BLAKE2b-256 checksum
How to use checksums
05d28b387e7404cf22b0a851ea8b7d4ce69a5e1ed4cdf0a8488e8e8a392d3b70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

2.28.1 This release

2 release files

2.26.0

2 release files

2.25.0

2 release files

2.24.2

2 release files

2.23.1

2 release files

2.23.0

2 release files

2.22.0

2 release files

2.21.0

2 release files

2.20.0

2 release files

2.19.0

2 release files

2.18.0

2 release files

2.17.0

2 release files

2.16.1

2 release files

2.16.0

2 release files

2.15.0

2 release files

2.14.0

2 release files

2.13.0

2 release files

2.12.0

2 release files

2.11.0

2 release files

2.10.0

2 release files

2.9.0

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

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