Skip to content

Releases: ormar-orm/ormar

Massive performance update, dumping data to ids and not nested models and other

07 May 10:54
3574031

Choose a tag to compare

0.25.0

⚡ Performance

A broad performance pass across model construction, row hydration, relation
resolution, alias lookups, and result merging — combining per-class caching,
lazy initialization of relation machinery, and porting the hottest helpers to
Rust via the new required ormar-utils
dependency. End-to-end speedups vs 0.24.0 range from ~12 % on select_related,
through ~30 % on iterate and first, to ~60 - 80 % on initialization of models, and up to x2.5 speedup in getting all.

✨ Features

  • Add flatten_fields() queryset method and model_dump(flatten_fields=...) /
    model_dump(flatten_all=True) arguments to render related models as their
    primary key value instead of nested dicts — see docs #1641

    cars = await Car.objects.flatten_fields("manufacturer").all()
    assert cars[0].model_dump() == {"id": 1, "name": "Corolla", "manufacturer": 1} # Note the id 1 and not the nested model
    
    car.model_dump(flatten_all=True)
    car.model_dump(flatten_fields={"manufacturer": ...})
  • Add proxy model inheritance (OrmarConfig(proxy=True)) — child class
    shares the parent's table and only adds methods / computed fields / a custom
    queryset_class — thanks @SepehrBazyar — see docs #760

    class User(Human):
        ormar_config = base_ormar_config.copy(proxy=True)
    
        def full_name(self) -> str:
            return f"{self.first_name} {self.last_name}"
  • Add per-table schema= support on OrmarConfig so models can live in
    non-default database schemas (PostgreSQL full, MySQL with caveats, SQLite
    not supported for cross-schema FKs) — thanks @hk3dva — see docs #1493

  • Add Python-style indexing and slicing on QuerySetTrack.objects[:10],
    Track.objects[-5:], Track.objects[5] — thanks @SepehrBazyar — see docs #765

  • Add nulls_ordering argument to .asc() / .desc() (ormar.NullsOrdering.FIRST
    / LAST) with MySQL emulation — thanks @SepehrBazyar — see docs #769

  • Allow filtering by a foreign-key column directly (Book.objects.filter(Book.author == 5)
    or with a model instance) without adding a JOIN — thanks @djalouehz — see docs #854

  • Add through_relation_nullable and through_reverse_relation_nullable to
    ormar.ManyToMany to enforce NOT NULL on the through table's FK columns —
    thanks @evadev-eva — see docs #852

  • Add last() / last_or_none() on QuerySet and expose first_or_none() /
    last() / last_or_none() on QuerysetProxy — see docs #765

  • Forward unrecognized **kwargs from ormar.ForeignKey / ormar.ManyToMany
    to BaseField — fixes comment= (and other BaseField kwargs) being
    silently dropped on relation fields — thanks @booqoffsky #1239

🐛 Fixes

  • Clearing a nullable ForeignKey with instance.fk = None now also clears the
    in-memory relation, so subsequent reads return None without a reload —
    originally @amit12297, rebased — see docs #1230
  • Drop the unnecessary post-INSERT pk reload in save(), and raise
    ModelPersistenceError on backends that cannot return a generated pk
    (Oracle MySQL with non-AUTO_INCREMENT server-default pk) instead of
    silently assigning the row count to Model.pk — thanks @vnicolaichuk #919
  • SelectAction aggregate identifiers now go through sqlalchemy.column()
    instead of string concatenation #1637
  • Fix the mkdocs deployment workflow #1638

Restore pre databases drop performance - add on_update on fields, possibility to name fks etc.

22 Apr 11:57
881fedb

Choose a tag to compare

0.24.0

✨ Features

  • Add on_update option to fields to customize SQL ON UPDATE behavior - thanks @vvanglro #1273
  • Allow overriding auto-generated foreign key constraint names via fk_name parameter on ormar.ForeignKey, useful when the generated name exceeds the database's identifier length limit - thanks @nikelborm #849
  • Forward autoincrement property from ormar field to the underlying SQLAlchemy column, enabling DDL with manually managed integer primary keys - thanks @smuething #674

🐛 Fixes

  • Run standalone queries under AUTOCOMMIT isolation to remove the per-query BEGIN/COMMIT round-trip introduced in 0.22.0. Restores (and exceeds) 0.21-era performance on per-row CRUD and single-row reads while keeping explicit database.transaction() and savepoints working. bulk_update and iterate are internally wrapped in an explicit transaction so their batch-commit and server-side-cursor semantics are preserved #1629 #1635

Fix high vulnerability with kwargs injection

19 Mar 14:57
7f22aa2

Choose a tag to compare

0.23.1

‼️🚨 High vulnerability fixed – please upgrade ASAP

  • In this version of ormar a high severity vulnerability (CVE-2026-27953) in model initialization was patched. The vulnerability allowed injection of __pk_only__ and __excluded__ parameters through user-supplied **kwargs (e.g. JSON request bodies). Passing __pk_only__=True bypassed all Pydantic validation, and __excluded__ could nullify arbitrary fields. Thanks @Mistz1 for reporting!
  • Affected versions:
    • All versions prior to 0.23.1

Fix critical vulnerability, drop Python 3.9, bugfixes

22 Feb 13:51
a03bae1

Choose a tag to compare

0.23.0

‼️🚨 Critical vulnerability fixed – please upgrade ASAP

  • In this version of ormar the critical vulnerability (CVE-2026-26198) in aggregate functions was patched - thanks @AAtomical
    for reporting. The vulnerability was caused by the way ormar generated SQL queries for aggregate functions, allowing arbitrary SQL execution through user input.
  • Affected versions:
    • 0.9.9 - 0.12.2
    • 0.20.0b1 - 0.22.0 (latest)

✨ Breaking changes

  • Drop support for Python 3.9

🐛 Fixes

  • Fix selecting data with nested models with json fields #1530
  • Fix prefetching JSON list field throwing TypeError - thanks @jannyware-inc #1402

Drop databases package and use sqlalchemy async instead

08 Feb 08:53
7e92b72

Choose a tag to compare

0.22.0

🐛 Breaking changes

  • Migration from databases library to native async SQLAlchemy

    Version 0.22.0 migrates from the databases library to native async SQLAlchemy using ormar's DatabaseConnection wrapper. This provides better integration with SQLAlchemy's async ecosystem and improved transaction handling and avoid dependency on archived databases library.

  • Import changes

    Replace databases import with DatabaseConnection from ormar:

    # ormar < 0.22
    import databases
    database = databases.Database("sqlite:///db.sqlite")
    
    # ormar >= 0.22
    from ormar import DatabaseConnection
    database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
  • Database URLs require async drivers

    Database connection strings must now use async-compatible drivers:

    • SQLite: sqlite+aiosqlite:// (not sqlite://)
    • PostgreSQL: postgresql+asyncpg:// (not postgresql://)
    • MySQL: mysql+aiomysql:// (not mysql://)
    # ormar < 0.22
    database = databases.Database("sqlite:///db.sqlite")
    
    # ormar >= 0.22
    database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
  • Engine parameter removed from OrmarConfig

    The engine parameter is no longer needed in OrmarConfig - it's created internally by DatabaseConnection:

    # ormar < 0.22
    import databases
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    engine = sqlalchemy.create_engine("sqlite:///db.sqlite")
    
    base_ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=sqlalchemy.MetaData(),
        engine=engine,  # <- No longer needed
    )
    
    # ormar >= 0.22
    from ormar import DatabaseConnection
    import sqlalchemy
    
    database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
    
    base_ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=sqlalchemy.MetaData(),
        # engine is created internally
    )
  • Table creation requires sync engine

    When using metadata.create_all(), you must create a separate sync engine:

    # ormar >= 0.22
    import sqlalchemy
    from ormar import DatabaseConnection
    
    DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
    database = DatabaseConnection(DATABASE_URL)
    metadata = sqlalchemy.MetaData()
    
    # Create a sync engine for table creation
    sync_engine = sqlalchemy.create_engine(
        DATABASE_URL.replace('+aiosqlite', '')
    )
    metadata.create_all(sync_engine)

✨ Features

  • Improved transaction handling

    Transactions now use context variables and SQLAlchemy savepoints for better nested transaction support:

    # Nested transactions with automatic savepoints
    async with database.transaction():
        await Model1.objects.create(...)
    
        async with database.transaction():  # Uses savepoint
            await Model2.objects.create(...)
  • Enhanced testing support

    The force_rollback parameter for transactions makes testing easier.
    Like in databases force_rollback can also be used with DatabaseConnection directly to use one global transaction.

    async with database.transaction(force_rollback=True):
        # Your test code - will rollback even on success
        await Model.objects.create(...)

💬 Other

Add support for sqlalchemy 2.0 and pydantic 2.11

09 Jan 13:34
c09209a

Choose a tag to compare

0.21.0

🐛 Breaking changes

  • Drop support for Python 3.8
  • Remove the possibility to exclude parents' fields in children models (discouraged as bad practice anyway)
  • Add support for Sqlalchemy 2.0 and drop for 1.4

💬 Other

  • Bump dependencies to newer versions: among others pydantic, databases and fastapi
  • Move setuptools to dev dependencies
  • Solve vulnerabilities in dependencies

Bug fixes and bump pydantic & python supported versions

06 Dec 10:47
409ae5a

Choose a tag to compare

0.20.2

🐛 Fixes

💬 Other

Fixes for prefetch related and saving related

09 Jun 16:24
45d81b2

Choose a tag to compare

0.20.1

✨ Breaking changes

🐛 Fixes

  • Fix merging same target models when using select_related with prefetch_related #906
  • Fix saving related with pk only models #812
  • Fix adding the same relation multiple times corrupting relation cache #1335

✨ Features

  • Allow adding indexed on foreign keys by @cmflynn, thanks! #1276

💬 Other

BETA - Support for pydantic v2

16 Mar 19:49

Choose a tag to compare

Pre-release

BETA

0.20.0

You can find updated documentation at: https://collerek.github.io/ormar/0.20.0b1/

✨ Breaking changes

  • ormar Model configuration

    Instead of defining a Meta class now each of the ormar models require an ormar_config parameter that is an instance of the OrmarConfig class.
    Note that the attribute must be named ormar_config and be an instance of the config class.

    import databases
    import ormar
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    # ormar < 0.20
    class Album(ormar.Model):
        class Meta:
            database = database
            metadata = metadata
            tablename = "albums"
        
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        favorite: bool = ormar.Boolean(default=False)
    
    # ormar >= 0.20
    class AlbumV20(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
            tablename="albums_v20"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        favorite: bool = ormar.Boolean(default=False)
  • OrmarConfig api/ parameters

    The ormar_config expose the same set of settings as Meta class used to provide.
    That means that you can use any of the following parameters initializing the config:

    metadata: Optional[sqlalchemy.MetaData]
    database: Optional[databases.Database]
    engine: Optional[sqlalchemy.engine.Engine]
    tablename: Optional[str]
    order_by: Optional[List[str]]
    abstract: bool
    exclude_parent_fields: Optional[List[str]]
    queryset_class: Type[QuerySet]
    extra: Extra
    constraints: Optional[List[ColumnCollectionConstraint]]
  • BaseMeta equivalent - best practice

    Note that to reduce the duplication of code and ease of development it's still recommended to create a base config and provide each of the models with a copy.
    OrmarConfig provides a convenient copy method for that purpose.

    The copy method accepts the same parameters as OrmarConfig init, so you can overwrite if needed, but by default it will return already existing attributes, except for: tablename, order_by and constraints which by default are cleared.

    import databases
    import ormar
    import sqlalchemy
    
    base_ormar_config = ormar.OrmarConfig(
        database=databases.Database("sqlite:///db.sqlite"),
        metadata=sqlalchemy.MetaData()
    )
    
    class AlbumV20(ormar.Model):
        ormar_config = base_ormar_config.copy(
            tablename="albums_v20"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
    
        
    class TrackV20(ormar.Model):
        ormar_config = base_ormar_config.copy(
            tablename="tracks_v20"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
  • choices Field parameter is no longer supported.

    Before version 0.20 you could provide choices parameter to any existing ormar Field to limit the accepted values.
    This functionality was dropped, and you should use ormar.Enum field that was designed for this purpose.
    If you want to keep the database field type (i.e. an Integer field) you can always write a custom validator.

    import databases
    import ormar
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    # ormar < 0.20
    class Artist(ormar.Model):
        class Meta:
            database = database
            metadata = metadata
        
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        country: str = ormar.String(default=False, max_length=50, choices=["UK", "US", "Vietnam", "Colombia"])
    
    # ormar >= 0.20
    from enum import Enum
    
    class Country(str, Enum):
        UK = "UK"
        US = "US"
        VIETNAM = "Vietnam"
        COLOMBIA = "Colombia"
    
    class ArtistV20(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
            tablename="artists_v20"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        country: Country = ormar.Enum(enum_class=Country)
  • pydantic_only Field parameter is no longer supported

    pydantic_only fields were already deprecated and are removed in v 0.20. Ormar allows defining pydantic fields as in ordinary pydantic model.

    import databases
    import ormar
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    # ormar < 0.20
    class Dish(ormar.Model):
        class Meta:
            database = database
            metadata = metadata
            tablename = "dishes"
        
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        cook: str = ormar.String(max_length=40, pydantic_only=True, default="sam")
    
    # ormar >= 0.20
    class DishV20(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
            tablename="dishes_v20"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        cook: str = "sam"  # this is normal pydantic field
  • property_field decorator is no longer supported

    property_field decorator was used to provide a way to pass calculated fields that were included in dictionary/ serialized json representation of the model.
    Version 2.X of pydantic introduced such a possibility, so you should now switch to the one native to the pydantic.

    import databases
    import ormar
    import sqlalchemy
    import pydantic
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    # ormar < 0.20
    class Employee(ormar.Model):
        class Meta:
            database = database
            metadata = metadata
        
    
        id: int = ormar.Integer(primary_key=True)
        first_name: str = ormar.String(max_length=100)
        last_name: str = ormar.String(max_length=100)
        
        @ormar.property_field()
        def full_name(self) -> str:
            return f"{self.first_name} {self.last_name}"
    
    # ormar >= 0.20
    class EmployeeV20(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
        )
    
        id: int = ormar.Integer(primary_key=True)
        first_name: str = ormar.String(max_length=100)
        last_name: str = ormar.String(max_length=100)
    
        @pydantic.computed_field()
        def full_name(self) -> str:
            return f"{self.first_name} {self.last_name}"
  • Deprecated methods

    All methods listed below are deprecated and will be removed in version 0.30 of ormar.

    • dict() becomes the model_dump()
    import databases
    import ormar
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    class Album(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
            tablename="albums"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        favorite: bool = ormar.Boolean(default=False)
    
    album = Album(name="Dark Side of the Moon")
        
    # ormar < 0.20
    album_dict = album.dict()
    
    # ormar >= 0.20
    new_album_dict = album.model_dump() 

    Note that parameters remain the same i.e. include, exclude etc.

    • json() becomes the model_dump_json()

      import databases
      import ormar
      import sqlalchemy
      
      database = databases.Database("sqlite:///db.sqlite")
      metadata = sqlalchemy.MetaData()
      
      class Album(ormar.Model):
          ormar_config = ormar.OrmarConfig(
              database=database,
              metadata=metadata,
              tablename="albums"
          )
      
          id: int = ormar.Integer(primary_key=True)
          name: str = ormar.String(max_length=100)
          favorite: bool = ormar.Boolean(default=False)
      
      album = Album(name="Dark Side of the Moon")
        
      # ormar < 0.20
      album_json= album.json()
      
      # ormar >= 0.20
      new_album_dict = album.model_dump_json() 

      Note that parameters remain the same i.e. include, exclude etc.

    • construct() becomes the model_construct()

    import databases
    import ormar
    import sqlalchemy
    
    database = databases.Database("sqlite:///db.sqlite")
    metadata = sqlalchemy.MetaData()
    
    class Album(ormar.Model):
        ormar_config = ormar.OrmarConfig(
            database=database,
            metadata=metadata,
            tablename="albums"
        )
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=100)
        favorite: bool = ormar.Boolean(default=False)
        
    params = {
        "n...
Read more

Bump fastapi and python

18 Jun 17:05
c793d66

Choose a tag to compare

0.12.2

✨ Features

  • Bump support for FastAPI up to the newest version (0.97.0) #1110
  • Add support and tests for Python 3.11 #1110