DoveORM: A Small, Security-Hardened Python ORM with Built-In Migrations

DoveORM: A Small, Security-Hardened Python ORM with Built-In Migrations

# python# opensource# database# showdev
DoveORM: A Small, Security-Hardened Python ORM with Built-In MigrationsTitus Eddy

Most Python apps I build need the same few things from a database layer. They need models that...

Most Python apps I build need the same few things from a database layer. They need models that validate their data, readable queries, a handful of relationships, and migrations I can trust in production. I wanted that in one small package with a short learning curve and safe defaults, so I built DoveORM.

It's open source (MIT), on PyPI at version 0.8.0, and supports SQLite, MySQL/MariaDB and PostgreSQL.

📖 Docs: https://doveorm.imaradesk.com
💻 GitHub: https://github.com/imaradesk/doveorm

Hello, World

pip install doveorm             # SQLite needs nothing else
pip install "doveorm[postgres]" # or [mysql]
Enter fullscreen mode Exit fullscreen mode
from doveorm import Model, configure, CharField, IntegerField

configure(DATABASE_PATH="app.db")

class User(Model):
    name = CharField(max_length=100, nullable=False)
    age  = IntegerField(default=0)

User.create_table()

user = User.create(name="Ada", age=36)
user.age = 37
user.save()

adults = User.where(age__gte=18).order_by("name").get()
Enter fullscreen mode Exit fullscreen mode

It works the same way in a Flask app, a FastAPI service, a background worker or a plain script. No web framework is required.

Queries that read like the question

The QuerySet is lazy and chainable, uses field__lookup filters, and supports Q objects for OR, AND and NOT. For the most common lookups there are also dynamic finders:

user  = User.findByUsername("ada")    # first match or None
posts = Post.findAllByAuthorId(1)      # list
count = User.countByActive(True)       # int
found = Tag.existsByName("python")     # bool
Enter fullscreen mode Exit fullscreen mode

Relationships cover HasMany, HasOne, BelongsTo and ManyToMany, and eager loading avoids N+1 queries.

Migrations are built in, not bolted on

This was the part I cared about most. DoveORM compares your models with the migration history and writes the migration for you. The migrations are reversible and squashable, and a single doveorm command runs them:

doveorm generate        # detect model changes → migrations/0001_initial.py
doveorm apply           # apply everything pending
doveorm sql 0002        # print the SQL, run nothing
doveorm revert 2        # unapply the last two
doveorm verify          # CI: exit 1 if a model change has no migration
doveorm squash 0007     # fold 0001…0007 into one migration
Enter fullscreen mode Exit fullscreen mode

doveorm verify is the one I'd recommend to everyone. Put it in CI and a pull request can no longer change a model without the matching migration. For tables your code reads but doesn't own, such as database views or another service's tables, set migrate = False and DoveORM will leave them alone.

Safe by default

An ORM sits between user input and your data, so I made the safe path the default one:

  • SQL injection: every value is a bound parameter. Table, column and index names are checked as valid identifiers, and limit and offset must be integers.
  • Destructive queries: an unfiltered bulk update() or delete() is refused unless you write Model.all() explicitly.
  • Mass assignment: from_input() only sets fields that are editable and on the allowed list.
  • Data exposure: sensitive=True fields are left out of to_dict() and redacted in error messages and query logs.
  • Integrity: strict MySQL sql_mode, optimistic locking, and atomic migrations.

Transactions can be nested using SAVEPOINTs, and db.run_in_transaction(fn) retries automatically on deadlocks and lock timeouts. Connections are thread-safe and fork-safe.

When not to use it

Being clear about limits saves everyone time. DoveORM is synchronous only and supports three databases. Window functions, CTEs and heavy reporting queries are best written as raw SQL with db.execute(...). Many-to-many relationships need a join-table model you define yourself. It's also a young 0.x project, so pin the version and read the changelog before you upgrade.

If you need async or a much broader feature set today, a larger, more established ORM is the better choice. If you want something small, predictable and safe for CRUD-heavy apps, give DoveORM a try.

Try it and tell me what breaks

The docs are at doveorm.imaradesk.com, and the code and changelog are on GitHub. Issues, pull requests and blunt feedback on the API are all welcome. I'm especially interested in hearing what would stop you from using it on your next project. 👇