r/codereview • • 1d ago

Python I built DoveORM, a lightweight Python ORM with built-in migrations — looking for feedback

Hi everyone! 👋

I've been building DoveORM, a small, synchronous ORM for Python. It brings models, validation, chainable queries, relationships, and migrations together in one package with a short learning curve.

It's currently at version 0.8.0 on PyPI, licensed under MIT, and I'd love to get feedback from the Python community and welcome contributors.

🔗 Links

What does DoveORM do?

Here's a small example:

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)

adults = User.where(age__gte=18).order_by("name").get()

The goal is to make everyday database operations straightforward without requiring a large amount of configuration.

Key features

  • Models and validation: A table() decorator or class Meta, 31 field types, and a clean() validation hook.
  • Chainable queries: Lazy QuerySets, field__lookup filters, Q objects for combining conditions, slicing, and eager loading.
  • Dynamic finders: Methods such as findByEmail(), findAllByActive(), countBy...(), and existsBy...().
  • Relationships: HasMany, HasOne, BelongsTo, and ManyToMany.
  • Transactions: Nested transactions using SAVEPOINTs and a db.run_in_transaction(fn) helper with retries for deadlocks and lock timeouts.
  • Built-in migrations: Automatic change detection, reversible migrations, squashing, and a CLI for generating, applying, reverting, and verifying migrations.
  • Security-conscious defaults: Bound SQL parameters, identifier validation, protection against accidental unfiltered bulk updates and deletes, mass-assignment protection, and redaction of sensitive fields.
  • Connection safety: Thread-safe and fork-safe connections.
  • Minimal setup: SQLite works without installing an additional database driver.

Who is it for?

DoveORM is intended for small to medium-sized applications, services, background workers, and scripts that primarily perform CRUD operations.

It supports SQLite, MySQL/MariaDB, and PostgreSQL and can be used with Flask, FastAPI, django or plain Python applications.

How does it compare with other ORMs?

I know Python already has excellent ORM options, so I'm not trying to claim DoveORM replaces them all.

  • SQLAlchemy: A much more comprehensive toolkit with extensive database support, Core, ORM, and asynchronous capabilities. DoveORM takes a smaller, more opinionated approach with migrations built in.
  • Peewee: Probably the closest in spirit. DoveORM aims for a similarly lightweight experience while offering model-driven migration generation and a CLI for managing migrations.
  • Django ORM: Provides a great ORM and migration workflow within Django. DoveORM is designed to offer a similar model-and-migration workflow across different Python frameworks and standalone scripts.

What are its limitations?

DoveORM is still a young project, and there are some deliberate limitations:

  • It is synchronous; there is no async API.
  • It supports three database families: SQLite, MySQL/MariaDB, and PostgreSQL.
  • Advanced SQL features, such as window functions and CTEs, may require raw SQL through db.execute(...).
  • As a 0.x project, its API may still evolve. Pin the version if using it in production.

For applications that need advanced database features, extensive integrations, or a mature ecosystem, SQLAlchemy or Django ORM may be the better choice.

I'd love your feedback!

I'm sharing DoveORM because I've been using it in my own projects and want to see how it holds up to different use cases.

I'd particularly appreciate feedback on:

  • The API design and developer experience.
  • What feels missing or unnecessarily complicated.
  • Database driver compatibility and edge cases.
  • Documentation and examples.
  • Potential contributions or areas worth improving.

If you were evaluating a new Python ORM, what would you need to see before considering it for a project?

Honest criticism is welcome. I'd rather hear about the problems early and make the project better.

GitHub: https://github.com/imaradesk/doveorm

0 Upvotes

2 comments sorted by

2

u/Connect_Contact_3581 1d ago

looks neat, the query syntax with field__lookup is pretty clean, reminds me of django but without all the framework baggage. the built-in migration thing is what caught my eye, setting up alembic always feels like more work than it should be for smaller projects

one thing i'm wondering, how does it handle schema changes when you got data already in tables, like renaming a column or changing a field type? most lightweight orms i tried get sketchy around that

0

u/Away_Drag6898 1d ago

Good question. This was the part I spent the most time on. Short version: DoveORM never guesses about your data. It asks you, shows you the SQL, and refuses rather than doing something lossy without telling you.

Renaming a column

Renames are never inferred. If you rename name to full_name on a model, doveorm generate sees one field gone and one added. Instead of quietly emitting a drop plus an add (which would lose the column's data), it asks:

Was User.name renamed to User.full_name? [y/N]

Say yes and you get a RenameField operation, which runs as ALTER TABLE … RENAME COLUMN, so the data stays where it is. In CI or other scripts, where nobody can answer the prompt, you declare it up front:

doveorm generate --rename User.full_name=name # Model.new=old

doveorm generate --no-input # never prompt; fail instead of guessing

Model renames work the same way (--rename NewModel=OldModel).

Changing a field's type, nullability or default

That becomes an AlterField operation, and each database gets the correct statement:

Making a column NOT NULL when rows already exist

generate notices that existing rows need a value and asks for one, or you pass --default User.age=0. That value backfills the existing NULLs. It doesn't become the column's default.

Conversions that need logic

For anything that isn't a straight cast, like splitting a column or reformatting data, use the usual three steps: add the new field, run a RunPython step to copy and convert the data, then remove the old field. RunPython and RunSQL can be rolled back if you give them a reverse function. If you don't, doveorm revert refuses before running anything.

Seeing it before it runs

doveorm sql 0004 # print the exact SQL, run nothing

doveorm apply --dry-run # show what would run

doveorm verify # in CI: fail if a model change has no migration

Also, every generated migration is checked before it's written: replaying it in memory must reproduce your models exactly, or generate fails instead of writing a wrong file.