Skip to content

Releases: sqlalchemy/sqlalchemy

2.1.2

Choose a tag to compare

@sqla-tester sqla-tester released this 02 Oct 04:16

2.1.2

Released: October 2, 2026

orm

  • [orm] [bug] [regression] Fixed regression caused by #5987 where the selectinload()
    loader strategy would ignore additional criteria present in the
    _orm.relationship.primaryjoin of a many-to-many relationship,
    such as a comparison against a column on the association table or on the
    parent table, loading related rows that should have been excluded. The
    omit_join optimization for many-to-many relationships is now only used
    when the primaryjoin consists solely of comparisons between the parent's
    primary key columns and the association table.

    References: #13626

engine

  • [engine] [bug] [regression] Fixed regression where result rows delivered by the DBAPI as a subclass of
    tuple would fail to be processed when the Cython extensions were in
    use, raising TypeError: Expected tuple. Such rows are now converted to
    a plain tuple, as was the case in 2.0. Rows delivered as a plain
    tuple, or as a sequence that is not a tuple at all such as
    asyncpg's Record, were not affected. Drivers such as the Databricks
    SQL connector return rows that are tuple subclasses.

    References: #13619

sql

  • [sql] [bug] The plain string portions of a _sql.tstring() construct are no
    longer scanned for bound parameter names in the :name format, and no
    longer interpret the \: escape sequence; the text is now rendered
    exactly as given. Previously, each string portion was parsed in the same
    way as _sql.text(), so that a colon within a quoted SQL string, such
    as 'time is :now', would be rendered as a bound parameter, failing at
    execution time.

    This change is a breaking change in the behavior of the newly introduced
    _sql.tstring() feature, as the former use of text()
    inadvertently provided a behavior that wasn't intended. In order to embed
    an explicit parameter in the SQL generated by _sql.tstring(),
    _sql.bindparam() may be used directly, e.g.
    tstring(t"SELECT {bindparam('p')}").

    References: #13616

schema

  • [schema] [bug] [regression] Fixed issue where the internal copies of Enum,
    _postgresql.DOMAIN and other SchemaType types produced
    when the type is adapted to a dialect-specific implementation would
    register themselves with the MetaData collection that is queried
    by MetaData.get_schema_objects(), in addition to the original type.
    As a new dialect-level copy is produced for each new Dialect
    instance, such as each time str() is called on a statement referring
    to a column using Enum, the collection would grow without bound
    and lead to excessive memory use. Only the type constructed by the user
    is now registered.

    References: #13625

2.1.1

Choose a tag to compare

@sqla-tester sqla-tester released this 25 Sep 14:59

2.1.1

Released: September 25, 2026

platform

  • [platform] [bug] Removed the legacy underscore-separated extra names such as
    mssql_pymssql and postgresql_psycopg from pyproject.toml.
    They normalize to the same names as the existing dash-separated extras,
    which is disallowed by PEP 685, and caused the 2.1.0 source
    distribution to fail to build with installers that enforce this rule,
    such as uv. The underscore spellings continue to work when installing,
    as installers normalize extra names before matching them.

    References: #13604

2.1.0

Choose a tag to compare

@sqla-tester sqla-tester released this 24 Sep 20:13

2.1.0

Released: September 24, 2026

orm

  • [orm] [feature] Added _orm.composite.column_template parameter to
    _orm.composite(). When the composite class is a dataclass, this
    parameter accepts a string template such as "person_%s", containing
    exactly one %s placeholder, that's used to generate column names for
    dataclass fields that don't otherwise have an explicit name, rather than
    using the bare field name. This removes the need to hand-write a
    _orm.mapped_column() for each field when the same composite dataclass
    is mapped multiple times on the same class with different column-name
    prefixes. Pull request courtesy Leonardo Rosa.

    References: #12575

  • [orm] [bug] Fixed issue where pickling an ORM object that had an instance level lazy
    loader established, such as when the _orm.raiseload() option is used,
    would emit a spurious warning regarding the loader containing additional
    criteria, if the object had itself been unpickled from a previous
    serialization. This would occur for objects that cross more than one
    serialization boundary, such as when using multiprocessing.

    This change is also backported to: 2.0.53

    References: #13574

  • [orm] [bug] Fixed issue where calling _orm.aliased() against an existing
    _orm.aliased() construct, without passing an explicit selectable,
    would disregard the selectable of the existing construct and produce an
    alias of the mapped table instead, if that selectable were anything other
    than a table or a plain subquery, leading to incorrect results and/or
    non-working queries.

    This includes _orm.aliased() against a _orm.with_polymorphic()
    construct, which would previously produce an alias of the base mapped
    class only, discarding the polymorphic selectable and additional mappers.
    The new construct now retains these, so that criteria against subclass
    attributes and the innerjoin and selectable parameters of
    _orm.with_polymorphic() take effect, and subclass columns are loaded
    up front. The SQL rendered for these constructs now includes the
    polymorphic selectable.

    This change is also backported to: 2.0.53

    References: #13583, #13584

  • [orm] [bug] Fixed issue where using _orm.aliased() with both the
    _orm.aliased.name and _orm.aliased.flat
    parameters, against an entity that is mapped to a join which includes
    anonymously named aliases, would embed the anonymous name symbol within
    the names generated for each element of the join, producing unusable
    SQL. An anonymously named element of the join is now aliased
    anonymously.

    This change is also backported to: 2.0.53

    References: #13583

sql

  • [sql] [bug] Fixed issue where calling _sql.CTE.alias() on a _sql.CTE
    that was itself produced by _sql.CTE.alias() would render SQL that
    referred to the name of the intermediate alias, which is not present in
    the WITH clause, rather than to the name of the original CTE, producing
    invalid SQL.

    This change is also backported to: 2.0.53

    References: #13583

postgresql

  • [postgresql] [usecase] PostgreSQL index reflection now reports so-called "invalid" indexes with
    postgresql_invalid=True in the Inspector's dialect_options.
    Reflected Index objects expose this state in the new
    read-only Index.reflect_only_elements mapping, separate from
    DDL options. Added new marker DialectKWArgConst.REFLECTED_ONLY
    which may be used by third party dialects for similar features within
    the DefaultDialect.construct_arguments registry. Pull request
    courtesy Max Azatian.

    References: #13577

mssql

  • [mssql] [bug] Adjusted the mssql+mssqlpython dialect for mssql-python version 1.15,
    which now binds all Python Decimal values as SQL_NUMERIC. The
    conversion of very large and very small Decimal values to strings,
    carried over from the pyodbc dialect, is no longer applied for this
    version of the driver, as the string values would otherwise be converted
    by SQL Server to the narrower numeric type of other Decimal values in
    the same statement, such as within a multiple-row INSERT, raising an
    arithmetic overflow error.

    References: #13585

  • [mssql] [bug] Fixed issue where Dialect.dbapi_version would raise
    AttributeError for the mssql+mssqlpython dialect, as the version
    was looked up using a version attribute that is not present on the
    mssql_python module, rather than __version__.

    References: #13585

oracle

  • [oracle] [usecase] Added support for oracledb's new terminate() feature, which allows for
    clean termination of an Oracle database connection in an asyncio context
    where the connection's state has fallen out of the event loop, and needs to
    be garbage collected. The feature is enabled automatically when using
    oracledb 26.0.0 or greater.

    This change is also backported to: 2.0.53

    References: #13578

2.0.54

Choose a tag to compare

@sqla-tester sqla-tester released this 15 Sep 21:07

2.0.54

Released: September 15, 2026

platform

  • [platform] [change] Binary wheels are no longer built for Python 3.7. PyPI now rejects wheel
    files whose filename does not begin with the normalized project name, and
    the packaging tools that can be installed on Python 3.7 do not produce
    such a filename. As a result, SQLAlchemy 2.0.44 was the last release to
    publish Python 3.7 wheels to PyPI, and releases 2.0.45 and later have
    been available on Python 3.7 only as a source distribution; the wheel
    builds for Python 3.7 are now removed. Python 3.7 remains supported by
    the 2.0 series.

  • [platform] [bug] Fixed issue where the Cython extensions were compiled without the
    freethreading_compatible directive, so that they did not declare
    themselves as safe to run without the GIL. On a free-threaded Python
    interpreter such as Python 3.13t or 3.14t, importing SQLAlchemy would
    cause the interpreter to re-enable the GIL, emitting a
    RuntimeWarning. The directive is now set when building for Python
    3.13 and above, and a test has been added which confirms that importing
    SQLAlchemy on a free-threaded build does not enable the GIL.

    References: #13592

2.0.53

Choose a tag to compare

@sqla-tester sqla-tester released this 14 Sep 20:37

2.0.53

Released: September 14, 2026

orm

  • [orm] [bug] Fixed issue where an expression passed to _orm.with_expression()
    that embedded a _sql.select(), such as a correlated
    _sql.exists(), would fail to populate the attribute correctly on
    the second and subsequent executions of an otherwise identical
    statement, when the _orm.query_expression() attribute was loaded
    by a relationship loader that emits a second query, i.e.
    _orm.selectinload(), _orm.lazyload() or
    _orm.immediateload().

    References: #13560

  • [orm] [bug] Fixed memory issue where mapped classes, along with their
    Table and _orm.Mapper objects, would not be garbage
    collected after the _orm.registry in which they were mapped had
    been disposed and dereferenced. The issue would occur for mappings that
    made use of _orm.relationship() together with constructs such as an
    Index established against an ORM-annotated expression.

    References: #13566

  • [orm] [bug] Fixed issue where pickling an ORM object that had an instance level lazy
    loader established, such as when the _orm.raiseload() option is used,
    would emit a spurious warning regarding the loader containing additional
    criteria, if the object had itself been unpickled from a previous
    serialization. This would occur for objects that cross more than one
    serialization boundary, such as when using multiprocessing.

    References: #13574

  • [orm] [bug] Fixed issue where calling _orm.aliased() against an existing
    _orm.aliased() construct, without passing an explicit selectable,
    would disregard the selectable of the existing construct and produce an
    alias of the mapped table instead, if that selectable were anything other
    than a table or a plain subquery, leading to incorrect results and/or
    non-working queries.

    This includes _orm.aliased() against a _orm.with_polymorphic()
    construct, which would previously produce an alias of the base mapped
    class only, discarding the polymorphic selectable and additional mappers.
    The new construct now retains these, so that criteria against subclass
    attributes and the innerjoin and selectable parameters of
    _orm.with_polymorphic() take effect, and subclass columns are loaded
    up front. The SQL rendered for these constructs now includes the
    polymorphic selectable.

    References: #13583, #13584

  • [orm] [bug] Fixed issue where using _orm.aliased() with both the
    _orm.aliased.name and _orm.aliased.flat
    parameters, against an entity that is mapped to a join which includes
    anonymously named aliases, would embed the anonymous name symbol within
    the names generated for each element of the join, producing unusable
    SQL. An anonymously named element of the join is now aliased
    anonymously.

    References: #13583

engine

  • [engine] [bug] [asyncio] [pool] Fixed issue where a DBAPI connection would be left open and unreachable
    if an exception were raised within the _events.PoolEvents.connect()
    or _events.PoolEvents.first_connect() event handlers, which is
    where Dialect.initialize() runs. The connection had been created
    but not yet associated with anything that could close it, so it was
    neither returned to the pool nor closed. For an asyncio driver in
    particular this could leak a server-side session for the life of the
    process, as the garbage collector is not able to close a connection that
    requires the event loop.

    References: #13548

sql

  • [sql] [bug] Fixed issue where calling _sql.CTE.alias() on a _sql.CTE
    that was itself produced by _sql.CTE.alias() would render SQL that
    referred to the name of the intermediate alias, which is not present in
    the WITH clause, rather than to the name of the original CTE, producing
    invalid SQL.

    References: #13583

schema

  • [schema] [bug] Fixed issue where reflecting a table with a foreign key constraint that
    names the same source column more than once, such as FOREIGN KEY (a, a) REFERENCES r (b, c) which is accepted by backends such as PostgreSQL,
    would raise ArgumentError and fail the reflection of the entire
    table. As ForeignKeyConstraint has no representation for this
    form, the constraint is now skipped with a warning, in the same way as one
    that names columns which are not present in the table, so that the
    remainder of the table is still reflected.

    Note that in the SQLAlchemy 2.1 series, full support for reflecting
    and constructing foreign key constraints with duplicated source columns
    has been added, with no warnings or skips.

    References: #13525

postgresql

  • [postgresql] [bug] Fixed bug in _reflection.Inspector.get_schema_names() for
    PostgreSQL where the query used to exclude system schemas relied on
    NOT LIKE 'pg_%', which treats the underscore as a SQL LIKE
    wildcard rather than a literal character. This caused user-created
    schemas that happen to start with "pg" followed by any other
    character, such as pgsql or pgstats, to be silently excluded
    along with actual system schemas like pg_catalog. Pull request
    courtesy Evan Rusackas.

    References: #13472

mysql

  • [mysql] [bug] Ensure that CREATE TABLE DDL statements for MySQL and MariaDB dialects
    render the table options in a deterministic order. Previously the order
    could change depending on the Python seed.

    References: #13523

sqlite

  • [sqlite] [usecase] [reflection] Implemented _engine.Inspector.get_table_options() for the SQLite
    dialect, which previously raised NotImplementedError. The method
    returns the sqlite_with_rowid and sqlite_strict dialect options
    for a table that was created using the WITHOUT ROWID and / or
    STRICT keywords. As a result, these keywords are now also present in
    _schema.Table.kwargs for a _schema.Table that is
    reflected using autoload, so that a table which is recreated from its
    reflected form, such as by Alembic's batch migration mode, renders the
    keywords again rather than silently dropping them.

    References: #13543

  • [sqlite] [bug] [reflection] Fixed issue in SQLite reflection where the name of a PRIMARY KEY,
    UNIQUE or FOREIGN KEY constraint would be reflected as None if
    the CONSTRAINT <name> clause were separated from the keyword that
    follows it by a newline rather than by spaces. As SQLite stores the
    CREATE TABLE statement as it was originally typed, this affected
    tables created from hand-written DDL that spans multiple lines. The
    regular expressions used to recover constraint names, as well as the
    ON UPDATE / ON DELETE, DEFERRABLE and INITIALLY options of
    a foreign key constraint, now accept any whitespace between tokens.

    References: #13528

  • [sqlite] [bug] [reflection] Reworked the regular expression that parses FOREIGN KEY constraints
    during SQLite CREATE TABLE reflection so that the referred column list
    is matched unambiguously avoiding potential exponential backtracking.
    Pull request courtesy of Javid Khan.

    References: #13530

mssql

  • [mssql] [bug] [reflection] Fixed issue in SQL Server reflection where TEXT and NTEXT columns
    would be reflected with a spurious length of 16 and 8, respectively. These
    are unlengthed LOB datatypes; the value originates from the
    sys.columns.max_length column, which reports the size of the in-row LOB
    pointer rather than a character length for these types. The reflected
    _mssql.TEXT and _mssql.NTEXT types now have a length
    of None, so that a reflected table emits valid DDL when re-created,
    which previously failed with "Cannot specify a column width on data type
    text". Pull request courtesy Sam Debruyn.

    References: #13451

oracle

  • [oracle] [usecase] Added support for oracledb's new terminate() feature, which allows for
    clean termination of an Oracle database connection in an asyncio context
    where the connection's state has fallen out of the event loop, and needs to
    be garbage collected. The feature is enabled automatically when using
    oracledb 26.0.0 or greater.

    References: #13578

tests

  • [tests] [bug] Altered the dialect reflection test
    test_check_constraint_parenthesized_expressions() so that it does not
    convert the reflected constraint to lowercase, which interferes with some
    third party dialect's representation of reflected check constraints.

    References: #13521

misc

  • [bug] [installation] Added the AUTHORS file to the set of license files included in the
    built wheel, where previously only the LICENSE file was present. As
    the text of LICENSE refers to AUTHORS for the list of copyright
    holders, the reference would not resolve for tools that inspect an
    installed distribution.

    R...

Read more

2.1.0rc2

2.1.0rc2 Pre-release
Pre-release

Choose a tag to compare

@sqla-tester sqla-tester released this 08 Sep 18:48

2.1.0rc2

Released: September 8, 2026

orm

  • [orm] [usecase] The collection of per-mapper / per-table binds established by the
    _orm.Session.binds parameter, as well as by the
    _orm.Session.bind_mapper() and _orm.Session.bind_table()
    methods, is now available publicly as _orm.Session.binds; the
    collection was previously stored under a private, name-mangled attribute.
    It is an immutable dictionary, and is replaced rather than mutated when a
    new bind is added, so a reference to it will not observe subsequent
    changes. The collection is also proxied by
    _orm.scoping.scoped_session.

    References: #13563

  • [orm] [bug] Fixed issue where an expression passed to _orm.with_expression()
    that embedded a _sql.select(), such as a correlated
    _sql.exists(), would fail to populate the attribute correctly on
    the second and subsequent executions of an otherwise identical
    statement, when the _orm.query_expression() attribute was loaded
    by a relationship loader that emits a second query, i.e.
    _orm.selectinload(), _orm.lazyload() or
    _orm.immediateload().

    This change is also backported to: 2.0.53

    References: #13560

  • [orm] [bug] Fixed memory issue where mapped classes, along with their
    Table and _orm.Mapper objects, would not be garbage
    collected after the _orm.registry in which they were mapped had
    been disposed and dereferenced. The issue would occur for mappings that
    made use of _orm.relationship() together with constructs such as an
    Index established against an ORM-annotated expression.

    This change is also backported to: 2.0.53

    References: #13566

orm declarative

  • [bug] [orm declarative] Fixed memory issue where a declarative class making use of the
    __declare_first__() or __declare_last__() hooks could never be
    garbage collected, as each such class established its own permanent
    _orm.Mapper-wide event listener referring to it. These hooks
    are now invoked by _orm.registry-local RegistryEvents
    listeners which refer to the classes weakly, and which are invoked only
    for the _orm.registry being configured. As the hooks are now
    located as the class is added to the _orm.registry, they also
    take effect for classes mapped using
    _orm.registry.map_imperatively().

    References: #9147

engine

  • [engine] [changed] Improved the implementation of _pool.Pool to use
    weakref.finalize() instead of a weakref.ref() finalizer to handle
    GC cleanup of non-detached, pooled connections that were not explicitly
    closed.

asyncio

  • [asyncio] [usecase] _asyncio.AsyncSession.bind and
    _asyncio.AsyncSession.binds are now always present and are
    derived from the underlying _asyncio.AsyncSession.sync_session,
    translated back into their asyncio equivalents. Added
    _asyncio.AsyncSession.get_async_bind(),
    _asyncio.AsyncSession.bind_mapper() and
    _asyncio.AsyncSession.bind_table() as the asyncio-facing
    counterparts to the _orm.Session methods of the same name.
    Both attributes, along with
    _asyncio.AsyncSession.get_async_bind(), are proxied by
    _asyncio.async_scoped_session.

    References: #13563

sqlite

  • [sqlite] [usecase] [reflection] Implemented _engine.Inspector.get_table_options() for the SQLite
    dialect, which previously raised NotImplementedError. The method
    returns the sqlite_with_rowid and sqlite_strict dialect options
    for a table that was created using the WITHOUT ROWID and / or
    STRICT keywords. As a result, these keywords are now also present in
    _schema.Table.kwargs for a _schema.Table that is
    reflected using autoload, so that a table which is recreated from its
    reflected form, such as by Alembic's batch migration mode, renders the
    keywords again rather than silently dropping them.

    This change is also backported to: 2.0.53

    References: #13543

  • [sqlite] [bug] [reflection] Reworked the regular expression that parses FOREIGN KEY constraints
    during SQLite CREATE TABLE reflection so that the referred column list
    is matched unambiguously avoiding potential exponential backtracking.
    Pull request courtesy of Javid Khan.

    This change is also backported to: 2.0.53

    References: #13530

2.1.0rc1

2.1.0rc1 Pre-release
Pre-release

Choose a tag to compare

@sqla-tester sqla-tester released this 31 Aug 22:16

2.1.0rc1

Released: August 31, 2026

platform

  • [platform] [change] Python 3.11 or above is now required; support for Python 3.10 is dropped,
    in addition to the drop of versions Python 3.9, 3.8 and 3.7 introduced
    in 2.1.0b1. Python 3.10 reaches EOL in October of 2026, so dropping
    support now gives the SQLAlchemy 2.1 series an extra year of space to
    remain on current Python versions.

  • [platform] [bug] Python 3.15 support has been added and tested, including minimal changes
    for full compatibility.

    This change is also backported to: 2.0.52

    References: #13477

orm

  • [orm] [usecase] Improved the error message raised when a Session is used
    inside a context manager after the transaction has been rolled back due
    to an exception. The InvalidRequestError now includes the original
    exception that triggered the rollback, making it clearer why the
    transaction is no longer active. Pull request courtesy Ilan Keshet.

    References: #11297

  • [orm] [usecase] Improved error messages raised when ORM loader strategy options cannot be
    applied to a query. Messages now render the offending option in a
    user-friendly form such as joinedload(User.orders) rather than exposing
    internal class and path representations, and the "does not apply to root
    entities" message now includes the option that triggered the error. The
    same user-friendly rendering is also applied to the "conflicting loader
    strategy" message and to the of_type() representation in "does not
    link" messages. Originating pull request courtesy Jan Vollmer.

    References: #12398

  • [orm] [usecase] Python source generated at runtime is now compiled against a descriptive
    filename which is registered with the linecache module, so that
    generated functions appearing on a stack trace render with their source
    rather than as an opaque File "<string>" frame. This allows tools
    like pdb and inspect.getsource() to work with these generated
    source blocks as well. The new feature is applied to the instrumentation
    applied to an ORM object's __init__ method, as well as throughout
    SQLAlchemy functions that are internally instrumented.

    References: #13505

  • [orm] [usecase] When a subclass overrides a _orm.validates() method using the
    same method name as the parent class, only the subclass validator is
    now invoked for instances of the subclass. The subclass validator
    may call super() to also invoke the parent class validator.
    Previously, the parent validator was always used regardless of
    whether the subclass provided an override. Pull request courtesy
    Indivar Mishra.

    References: #2943

  • [orm] [bug] Fixed a result-column misalignment bug in ORM-enabled UPDATE statements
    where synchronize_session="fetch" is in use, either explicitly or
    because the statement uses constructs such as CTEs that implicitly select
    for it. Columns in rows returned by .returning() could be returned
    under incorrect keys (e.g. row[SomeClass.a] returning the value of
    a different column), a problem most likely to manifest under concurrent
    workloads. ORM DELETE statements were not affected.

    This change is also backported to: 2.0.52

    References: #13439

  • [orm] [bug] Fixed bug where a failed _orm.Session.bulk_insert_mappings(),
    _orm.Session.bulk_update_mappings() or
    _orm.Session.bulk_save_objects() call could leave the
    _orm.Session permanently in a "flushing" state, such as when the
    transaction could not be begun because a previous flush had left it
    needing a rollback. Unlike _orm.Session.flush(), the bulk methods
    set the internal flushing flag and began the transaction outside of the
    try/finally block that resets it, so that neither
    _orm.Session.rollback() nor _orm.Session.close() would clear
    it, and every subsequent flush would raise InvalidRequestError: Session is already flushing. Pull request courtesy Hamody We.

    This change is also backported to: 2.0.52

    References: #13485

  • [orm] [bug] Fixed issue where unpickling an ORM object that were loaded using loader
    options making use of wildcard tokens, such as _orm.load_only() or
    _orm.raiseload() with "*", would fail with KeyError or
    IndexError if the process doing the unpickling had not yet constructed
    a loader path making use of that same token. This would typically be
    observed when the object were unpickled in a separate process, such as
    with the spawn or forkserver multiprocessing start methods, the
    latter of which became the default on POSIX platforms as of Python 3.14.
    The internal collection of these tokens is now established up front, so
    that it is identical in every process.

    This change is also backported to: 2.0.52

    References: #13493

  • [orm] [bug] Fixed issue where a string ending in "*" passed to a
    _orm.Load strategy method, such as
    Load(A).joinedload("bs.*"), would bypass the check which rejects
    string attribute names in loader options, silently producing a loader
    path that matched nothing. Such a string now raises
    ArgumentError with the same message given for any other
    string attribute name. The bare wildcard "*", as in
    Load(A).lazyload("*"), continues to be accepted.

    This change is also backported to: 2.0.52

    References: #13493

  • [orm] [bug] Calling _orm.aliased() against a _sql.select() or
    _sql.union() / _sql.CompoundSelect construct, which
    previously failed with an obscure AttributeError regarding a missing
    .mapper attribute, now raises when using SQLAlchemy 2.1, and emits a
    deprecation warning under SQLAlchemy 2.0 as it coerces the construct into a
    subquery instead. This matches the behavior of other similar implicit
    SELECT-to-FROM coercions. Pull request courtesy Rens Groothuijsen.

    This change is also backported to: 2.0.52

    References: #6274

  • [orm] [bug] [regression] Fixed regression caused by the dataclasses change in #12168 where
    passing _orm.relationship.default_factory as list to a
    relationship that used the _orm.WriteOnlyMapped or
    _orm.DynamicMapped annotation would raise an error at mapper
    configuration time, as these relationships have no collection_class.
    list is now accepted for these relationships, which behave the same
    as ordinary collections in this regard; the factory itself is never
    invoked, and a newly constructed object begins with an empty
    collection. Documentation is added at write_only_dataclasses
    illustrating the use of write only and dynamic relationships with ORM
    mapped dataclasses.

    References: #13227

  • [orm] [bug] Fixed long-standing issue where an object that was loaded at more than one
    path within a single query, such as when a chain of _orm.joinedload()
    options leads back to an entity that was also loaded at the top level of
    the query, would retain the loader options of whichever path the query
    happened to see last, which varied with the loader strategy in use. The
    options an object retains are applied to all forms of :term:lazy loading
    for that object, so an otherwise identical set of options could behave
    differently depending on the loader strategy. The shallowest path is now
    favored, which is deterministic.

    Unknown interpreted text role "term".

    References: #13507

engine

  • [engine] [bug] [asyncio] [pool] Fixed issue where a DBAPI connection would be left open and unreachable
    if an exception were raised within the _events.PoolEvents.connect()
    or _events.PoolEvents.first_connect() event handlers, which is
    where Dialect.initialize() runs. The connection had been created
    but not yet associated with anything that could close it, so it was
    neither returned to the pool nor closed. For an asyncio driver in
    particular this could leak a server-side session for the life of the
    process, as the garbage collector is not able to close a connection that
    requires the event loop.

    This change is also backported to: 2.0.53

    References: #13548

sql

  • [sql] [usecase] Added new methods _sql.Exists.with_hint() and
    _sql.Exists.with_statement_hint(), which apply a table hint or a
    statement hint to the SELECT statement that's enclosed by the EXISTS
    expression, in the same way as _sql.Select.with_hint() and
    _sql.Select.with_statement_hint(). As ORM constructs such as
    _orm.PropComparator.any() and _orm.PropComparator.has()
    produce an _sql.Exists object, hints may now be applied to the
    subqueries which these constructs generate. Pull request courtesy
    Abhinav Gorrepati.

    References: #8311

  • [sql] [performance] Improved the performance of SQL cache key generation by moving the
    traversal into the Cython extension modules. The set of attributes that
    participate in the cache k...

Read more

2.0.52

Choose a tag to compare

@sqla-tester sqla-tester released this 11 Aug 19:07

2.0.52

Released: August 11, 2026

platform

  • [platform] [bug] Python 3.15 support has been added and tested, including minimal changes
    for full compatibility.

    References: #13477

orm

  • [orm] [bug] Fixed a result-column misalignment bug in ORM-enabled UPDATE statements
    where synchronize_session="fetch" is in use, either explicitly or
    because the statement uses constructs such as CTEs that implicitly select
    for it. Columns in rows returned by .returning() could be returned
    under incorrect keys (e.g. row[SomeClass.a] returning the value of
    a different column), a problem most likely to manifest under concurrent
    workloads. ORM DELETE statements were not affected.

    References: #13439

  • [orm] [bug] Fixed bug where a failed _orm.Session.bulk_insert_mappings(),
    _orm.Session.bulk_update_mappings() or
    _orm.Session.bulk_save_objects() call could leave the
    _orm.Session permanently in a "flushing" state, such as when the
    transaction could not be begun because a previous flush had left it
    needing a rollback. Unlike _orm.Session.flush(), the bulk methods
    set the internal flushing flag and began the transaction outside of the
    try/finally block that resets it, so that neither
    _orm.Session.rollback() nor _orm.Session.close() would clear
    it, and every subsequent flush would raise InvalidRequestError: Session is already flushing. Pull request courtesy Hamody We.

    References: #13485

  • [orm] [bug] Fixed issue where unpickling an ORM object that were loaded using loader
    options making use of wildcard tokens, such as _orm.load_only() or
    _orm.raiseload() with "*", would fail with KeyError or
    IndexError if the process doing the unpickling had not yet constructed
    a loader path making use of that same token. This would typically be
    observed when the object were unpickled in a separate process, such as
    with the spawn or forkserver multiprocessing start methods, the
    latter of which became the default on POSIX platforms as of Python 3.14.
    The internal collection of these tokens is now established up front, so
    that it is identical in every process.

    References: #13493

  • [orm] [bug] Fixed issue where a string ending in "*" passed to a
    _orm.Load strategy method, such as
    Load(A).joinedload("bs.*"), would bypass the check which rejects
    string attribute names in loader options, silently producing a loader
    path that matched nothing. Such a string now raises
    ArgumentError with the same message given for any other
    string attribute name. The bare wildcard "*", as in
    Load(A).lazyload("*"), continues to be accepted.

    References: #13493

  • [orm] [bug] Calling _orm.aliased() against a _sql.select() or
    _sql.union() / _sql.CompoundSelect construct, which
    previously failed with an obscure AttributeError regarding a missing
    .mapper attribute, now raises when using SQLAlchemy 2.1, and emits a
    deprecation warning under SQLAlchemy 2.0 as it coerces the construct into a
    subquery instead. This matches the behavior of other similar implicit
    SELECT-to-FROM coercions. Pull request courtesy Rens Groothuijsen.

    References: #6274

orm declarative

  • [bug] [orm declarative] Fixed issue where using PEP 593 Annotated wrapping a PEP 695
    type alias, such as Annotated[SomeTypeAlias, mapped_column()],
    would crash with AttributeError: __value__. The internal
    is_pep695() check incorrectly identified the Annotated type as a
    PEP 695 type alias due to a quirk in Annotated.__origin__ returning
    the first type argument rather than Annotated itself.

    References: #13386

sql

  • [sql] [bug] Fixed issue where _sql.Select.get_final_froms() would emit a
    deprecation warning when the statement made use of the PostgreSQL-specific
    expression argument to _sql.Select.distinct(); the same spurious
    warning would be emitted when stringifying such a statement without
    explicitly using a PostgreSQL dialect. The fix ensures that this 1.4-era
    warning is suppressed under both 2.0 and 2.1.

    Note that under SQLAlchemy 2.1, passing an expression to
    _sql.Select.distinct() is deprecated overall, and is replaced by a
    new PostgreSQL-specific construct (see #12342).

    References: #13396

  • [sql] [bug] Fixed an issue in Numeric where the
    Numeric.decimal_return_scale parameter was ignored when the
    DBAPI does not support native decimal objects (i.e.
    dialect.supports_native_decimal is False). In this path the result
    processor was computing the conversion scale from
    Numeric.scale directly, bypassing
    Numeric.decimal_return_scale entirely. The behavior now
    matches Float, which already used the correct
    _effective_decimal_return_scale property. Pull request courtesy Kadir
    Can Ozden.

    References: #13424

  • [sql] [bug] Added auditing to the test suite which exercises the literal execute
    processors across all datatypes and dialects to ensure that string input is
    either appropriately rejected or correctly escaped. Literal execute
    processors are invoked when the bindparam.literal_execute
    parameter is used with an explicit bindparam() object, which
    overrides DBAPI-native bind handling to render the value inline with the
    statement instead. Datatypes that were updated include the originally
    reported SQL Server Uuid / UNIQUEIDENTIFIER rendering which now
    escapes properly, the JSONPATH type that's currently
    PostgreSQL-only, and a full family of numeric types stemming from the
    _types.Float and _types.Numeric bases which now coerce
    the value to a number, rejecting non-numeric input. Thanks to Javid Khan
    for helping to identify the issue.

    References: #13448

schema

  • [schema] [bug] Fixed an issue where _schema.Table.to_metadata() reused column
    default and on-update objects, causing the defaults on the original
    columns to refer to the copied columns. Default generators, including
    sequences, and server-side defaults are now copied and remain associated
    with their respective columns and metadata collections. Applications that
    inspected these objects will now see distinct defaults on the copied table
    instead of the objects owned by the original table. Pull request courtesy
    Goutam Adwant.

    References: #13481

postgresql

  • [postgresql] [bug] [reflection] Fixed reflection of PostgreSQL CHECK constraints where an expression made
    up of multiple parenthesized sub-expressions, such as (x IS NULL OR y IS NULL) AND (x IS NULL OR y IS NULL), would have its leading and trailing
    parentheses incorrectly stripped, producing an unbalanced and
    syntactically invalid reflected expression. Pull request courtesy
    Shaurya Singh.

    References: #13157

  • [postgresql] [bug] Fixed bug in the PostgreSQL dialect where a single quote in a sequence,
    table, or schema name, such as one supplied via a schema_translate_map
    or an explicit Sequence, could result in a malformed
    nextval() statement. The quote is now properly escaped. Pull request
    courtesy dxbjavid.

    References: #13429

sqlite

  • [sqlite] [bug] Reworked the regular expression that detects inline UNIQUE column
    constraints during SQLite CREATE TABLE reflection so that the
    whitespace separating a column's type from a following clause is matched
    unambiguously. The previous pattern had three overlapping quantifiers
    that could each consume a space character, so a column definition
    carrying a long run of whitespace in the stored schema made
    _reflection.Inspector.get_unique_constraints() spend cubic time
    backtracking before returning. Fix courtesy of Javid Khan.

    References: #13419

mssql

  • [mssql] [bug] Tightened the construction of the ODBC connection string in the pyodbc
    connector (as well as the mssql-python connector in 2.1) so that the
    driver name, the names of pass-through connection parameters, and values
    containing } are brace-quoted. Previously a } in the driver name
    or in a pass-through value, or a ; in the name of a pass-through
    parameter, could close the surrounding token early and allow the
    remainder of the string to be interpreted as additional connection
    attributes. Pull request courtesy dxbjavid.

    References: #13380

tests

  • [tests] [bug] Fixed class-scoped pytest fixtures that were defined as instance methods
    using self, which is deprecated as of pytest 9.1 and will be removed in
    pytest 10. Fixtures are now decorated with a compatibility @classmethod
    decorator and use cls as the first parameter.

    References: #13392

2.1.0b3

2.1.0b3 Pre-release
Pre-release

Choose a tag to compare

@sqla-tester sqla-tester released this 27 Jun 18:52

2.1.0b3

Released: June 27, 2026

orm

  • [orm] [feature] Added selectinload.chunksize parameter to selectinload()
    allowing users to configure the number of primary keys sent per IN clause
    when loading relationships. Pull request courtesy bekapono.

    References: #11450

  • [orm] [usecase] The populate_existing execution option is now honored when passed in the
    Session.get.execution_options dict by the method
    Session.get() and analogous in other session kinds. The current
    Session.get.populate_existing parameter will takes precedence if
    specified, overriding the value of the execution options.

    References: #10610

  • [orm] [usecase] Updated the attribute _orm.ORMExecuteState.user_defined_options to
    include options that were added to the statement before calling
    Select.with_only_columns() or _orm.Query.with_entities().

    References: #13309

  • [orm] [usecase] [performance] Optimized _orm.selectinload() to skip the .unique() call on inner
    result sets when no nested _orm.joinedload() on a collection is
    present. The uniquing pass is only required when a joined eager load
    inflates rows due to a one-to-many or many-to-many JOIN; in the common case
    of a leaf selectin load, rows are already unique by construction and the
    per-row hashing overhead can be avoided. As a side effect, yield_per
    set in a do_orm_execute event for a _orm.selectinload()
    relationship load no longer raises InvalidRequestError when no nested
    collection joinedload is in effect, since .unique() is no longer called
    in that path. Pull request courtesy Oliver Parker.

    References: #13339

  • [orm] [usecase] Session level _orm.Session.execution_options now take
    effect for Core level SQL emitted by unit of work operations, in
    addition to their existing use within ORM statement executions.
    This is to provide for Core options such as
    _engine.Connection.execution_options.schema_translate_map
    to be applicable to a Session overall.

    References: #13346

  • [orm] [performance] ORM result row fetching now processes rows as plain tuples rather than
    constructing Row objects, as ORM loaders use position-based
    access and do not require the Row interface. Row
    construction is still used when engine-level debug logging is enabled so
    that individual rows can be logged. Benchmarks show a 3-16% improvement in
    ORM entity load times depending on query shape. Pull request courtesy
    Oliver Parker.

    References: #13363

  • [orm] [performance] Improved performance of _orm.selectinload() and
    _orm.subqueryload() result handling:

    -   in selectinloader, the primary key columns used to correlate related
        rows are now selected directly rather than being wrapped in a
        `Bundle`, and are read from positional slices of each result
        row.  This removes the per-row `Row` construction that the
        `Bundle` introduced, including for the common single-column
        primary key case.
    
    -   removed use of `groupby()` + `lambda` against `Row` objects
        in subqueryloader; rows are converted to plain tuples and the result
        lists are built via `append()`.
    
    -   many-to-one selectinload reads foreign key values directly from the
        parent instance dictionary when present, falling back to attribute-level
        access only for expired or deferred attributes.
    

    Pull request courtesy Oliver Parker.

    References: #13363

  • [orm] [performance] The selectinload() loader strategy now selects the omit_join
    optimization for many-to-many non-self-referential relationships, reducing
    the number of joins in the secondary SELECT by selecting from the secondary
    table directly rather than joining back to the parent entity. omit_join
    is enabled automatically when the join condition determines that the
    secondary table's foreign keys fully cover the parent's primary key. As
    always, omit_join can be disabled by setting
    relationship.omit_join to False. Pull request courtesy
    bekapono.

    References: #5987

  • [orm] [bug] Fixed issue where the declarative class registry would not consider
    class-level MetaData objects set on abstract mixin classes when
    resolving string-based table references in relationship()
    configurations. The registry now uses the same metadata resolution logic
    as table creation, first checking for a class-specific metadata
    attribute before falling back to registry.metadata.

    References: #13291

  • [orm] [bug] Fixed issue where the _engine.Result.unique() filter was not properly
    validated against the _engine.Result.yield_per() method when both
    were called as methods on the result object, such as
    result.unique().yield_per(N) or result.yield_per(N).unique(). The
    uniquing filter was previously only checked when yield_per was set via
    _engine.Connection.execution_options.yield_per. Since these two
    features are fundamentally incompatible for ORM results, an
    InvalidRequestError is now raised in all cases.

    References: #13293

  • [orm] [bug] A warning is now emitted when a Declarative attribute name is named
    metadata or registry. Previously, no warning was emitted for
    registry, and using the name metadata would raise an
    InvalidRequestError. Since these names can be used for attributes
    that are mapped as backrefs or using imperative mappings, usage
    under Declarative has been relaxed for metadata but also warns
    for both names as they may have unintended interactions with the
    Declarative reserved names.

    References: #13333

  • [orm] [bug] Fixed issue where the declarative class resolver would not consider
    the MetaData.schema default schema when resolving a
    string table name for the relationship.secondary parameter
    as well as within string-based
    relationship.primaryjoin and
    relationship.secondaryjoin expressions. The resolution now
    matches the behavior of ForeignKey, where an unqualified
    table name is implicitly resolved under the default schema. A
    deprecation warning is emitted when an unqualified name resolves to a
    :data:.BLANK_SCHEMA table in a MetaData that has a default
    schema set, as this implicit resolution will change in a future version.

    Unknown interpreted text role "data".

    References: #8068

engine

  • [engine] [bug] Expanded try/except error handling to encompass the
    _events.ConnectionEvents.before_cursor_execute() and
    _events.ConnectionEvents.after_cursor_execute() event hooks, so that
    exceptions raised within these hooks, including BaseException
    subclasses such as asyncio.CancelledError, are properly handled via the
    error handling path used for DBAPI errors. This ensures proper connection
    invalidation and pool notification when exit-type exceptions are raised in
    event hooks. As part of this change, DBAPI errors raised from within these
    event hooks will now be wrapped as SQLAlchemy exceptions.

    References: #13381

  • [engine] [reflection] Removed the legacy include_columns key from the dictionary returned
    by the index reflection methods of some dialects.
    This information is now part of the dialect_options dictionary under the key
    {dialect_name}_include, such as postgresql_include or mssql_include.

    References: #13350

sql

  • [sql] [usecase] Added _sql.Delete.using(), allowing explicit FROM expressions such as
    joins to be rendered in backend-specific multiple-table DELETE forms
    including MySQL/MariaDB DELETE .. USING. Pull request courtesy
    cjc0013.

    References: #8130

  • [sql] [bug] Fixed issue where negation of comparison expressions involving
    func.any(), func.all(), and func.some() SQL functions would
    incorrectly flip the comparison operator (e.g. = to !=) rather
    than wrapping the expression with NOT. These functions are now
    registered as collection aggregate functions that prevent operator
    flipping on negation, consistent with the behavior of the standalone
    _expression.any_() and _expression.all_() constructs.

    References: #13343

postgresql

  • [postgresql] [usecase] Changed the default backslash escape value in the PostgreSQL dialect to
    False to align it with the default value of
    standard_conforming_strings=on. This change should not affect most users
    since the value is set at driver initialization on first connect.

    References: #13268

mysql

  • [mysql] [bug] Improved the regular expression used to parse index COMMENT clauses
    in MySQL SHOW CREATE TABLE reflection to use an unambiguous
    single-quoted-string pattern; the previous pattern was theoretically
    subject to backtracking on malformed input, though such input ...
Read more

2.0.51

Choose a tag to compare

@sqla-tester sqla-tester released this 15 Jun 15:41

2.0.51

Released: June 15, 2026

orm

  • [orm] [bug] Fixed issue where _orm.subqueryload() combined with
    PropComparator.of_type() and PropComparator.and_() would
    silently drop the additional filter criteria, causing all related objects
    to be loaded instead of only those matching the filter. The
    LoaderCriteriaOption was being constructed against the base
    entity rather than the effective entity indicated by
    PropComparator.of_type(). Pull request courtesy Arya Rizky.

    References: #13207

  • [orm] [bug] Fixed bug where a failure during tpc_prepare() within
    _orm.Session.commit() for a two-phase session would raise
    IllegalStateChangeError instead of the original database
    exception. The internal _prepare_impl() method's error handler
    was unable to invoke _orm.SessionTransaction.rollback() due
    to a state-change guard, preventing proper cleanup and masking the
    underlying error.

    References: #13356

engine

  • [engine] [bug] Fixed issue where Result.freeze() would lose track of ambiguous
    column names present in the original CursorResult, causing
    key-based access on the thawed result to silently return a value instead of
    raising InvalidRequestError. The
    SimpleResultMetaData now accepts and propagates ambiguous key
    information so that frozen, thawed, and pickled results raise consistently
    for duplicate column names. Pull request courtesy Saurabh Kohli.

    References: #9427

sql

  • [sql] [bug] Fixed issue where _sql.StatementLambdaElement would proxy
    attribute access through the cached "expected" expression rather than the
    resolved expression, causing stale closure-bound parameter values to be
    used when a lambda statement was extended with non-lambda criteria such as
    an additional .where() clause. Courtesy cjc0013.

    References: #10827

postgresql

  • [postgresql] [bug] Repaired bug introduced in #13229 where a two-phase
    transaction recovery would not return the correct transaction
    identifier when generating the identifiers using the xid()
    method of the psycopg connection.

    References: #13355

  • [postgresql] [bug] Fixed regular expression in the pure Python hstore result processor,
    used when use_native_hstore=False is set, which could hang on
    malformed hstore text containing unterminated quoted segments with
    backslashes. Pull request courtesy dxbjavid.

    References: #13370