Skip to content

2.1.0rc1

Pre-release
Pre-release

Choose a tag to compare

@sqla-tester sqla-tester released this 31 Aug 22:16
· 1 commit to main since this release

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 key for a particular construct, along with the
    handler that applies to each one, is now resolved once at class setup
    time into a structure that the compiled traversal consumes directly, so
    that the chain of identity comparisons that formerly rediscovered this
    per attribute, per cache key is no longer run at all. Benchmarks against
    a range of Core and ORM statements show cache key generation running
    approximately 1.5 to 2.2 times faster in a build with the Cython
    extensions compiled, and approximately 1.05 to 1.2 times faster in a
    pure Python build, with the generated cache keys themselves unchanged.

    References: #13506

  • [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.

    This change is also backported to: 2.0.52

    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.

    This change is also backported to: 2.0.52

    References: #13448

  • [sql] [bug] Fixed issue where two bound parameters whose names differ only in the
    characters listed in
    SQLCompiler.bindname_escape_characters, such as those generated
    for columns named "a.b" and "a_b", would be rendered using the
    same name in the compiled statement, as those characters are escaped only
    as the parameter is rendered. The value for one of the two parameters was
    then silently used for both, affecting SELECT criteria as well as the
    VALUES and SET clauses of INSERT and UPDATE statements, where a value
    could be written to the wrong column. Escaped parameter names are now
    disambiguated against the names already in use. The .key of each
    BindParameter is unaffected, so parameter dictionaries passed
    by the caller continue to be keyed as before.

    References: #13534

  • [sql] [bug] Fixed issue where an empty string passed to
    IdentifierPreparer.quote(), such as the name of a
    Table constructed with a blank name, would raise
    IndexError rather than being rendered. An empty identifier is now
    always quoted. While a blank name is not a legal identifier on most
    backends, SQLite accepts one, so such a table may be delivered by
    reflection; a table with a blank name can now be used in SELECT, INSERT,
    UPDATE, DELETE and DDL statements.

    References: #13535

schema

  • [schema] [usecase] ForeignKeyConstraint now accepts a constraint which names the
    same local column more than once, such as FOREIGN KEY (a, a) REFERENCES r (b, c). This form is valid SQL and constrains the referenced row so
    that two of its columns are equal; it previously raised
    ArgumentError. Such a constraint now emits and reflects like any
    other composite foreign key; the workaround added in 2.0 for
    #13525, which skipped such a constraint during reflection, is
    removed as it is no longer needed. As part of this change, the check that
    the number of constrained columns matches the number of referenced columns
    no longer counts distinct column names, so that a genuine mismatch such as
    ForeignKeyConstraint(["x", "x"], ["r.b"]), which was formerly accepted
    and silently dropped a column, is now rejected.

    References: #13526

  • [schema] [usecase] Added new _schema.ForeignKey accessors
    _schema.ForeignKey.target_tokens,
    _schema.ForeignKey.target_column and
    _schema.ForeignKey.target_table_key, as well as new
    ForeignKey-related datastructure _schema.ForeignKeyTarget.
    _schema.ForeignKeyTarget is now accepted as a constructor
    argument as well. See _schema.ForeignKey for new datamembers and
    usage patterns.

    References: #13538

  • [schema] [usecase] Added Dialect.dbapi_version, a standardized accessor for the
    version of the DBAPI module in use by a dialect, in contrast to
    Dialect.server_version_info which refers to the database server.
    The implementation on DefaultDialect makes use of a new
    per-dialect method Dialect.retrieve_dbapi_version() in order to
    retrieve the version from the DBAPI module and return it as a
    VersionInfo object, which is a tuple subclass with additional
    properties; third-party dialects should also implement the
    Dialect.retrieve_dbapi_version() method.

  • [schema] [performance] Created new reflection method _reflection.Inspector.has_multi_table()
    to check the existence of multiple tables at once, allowing for
    better performance when checking many tables. Like the other "multi"
    reflection methods, the default dialect offers a default implementation
    that just call the single method in a loop. Backends that wish to take
    advantage of this new method can implement it in their dialects.
    The PostgreSQL, Oracle and SQL Server dialects have been updated to use
    this new method.
    The implementation of _schema.MetaData.create_all() has been updated
    to make use of this new method to check the existence of the tables,
    reducing the number of round trips to the database when creating many tables.

    References: #13311

  • [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.

    This change is also backported to: 2.0.52

    References: #13481

  • [schema] [bug] Fixed issue where a _schema.ForeignKey which refers to a table or
    column whose name contains a dot would be interpreted incorrectly, as the
    dotted string form of the target could not be told apart from the
    separator between a schema, table and column name. Foreign key targets
    are now tracked as their individual schema, table and column names
    throughout, and are no longer derived by splitting a dotted string.

    References: #13538

  • [schema] [deprecated] _schema.ForeignKey.target_fullname is now a legacy accessor, and
    raises InvalidRequestError when the target has no unambiguous
    dotted string form, which is the case when the target table or column name
    contains a dot, or when a schema name is present with no column name. No
    SQLAlchemy internals make use of the attribute any longer; new code should
    use _schema.ForeignKey.target_tokens.

    References: #13538

postgresql

  • [postgresql] [usecase] Added postgresql_with support to CreateView for specifying
    PostgreSQL view options such as security_invoker, security_barrier,
    and check_option, rendered as a WITH (...) clause between the view
    name and the AS keyword. Additionally, the postgresql_with parameter
    accepted by _schema.Table and _schema.Index now correctly
    renders Python boolean values as true/false (lowercase), and
    None values as the parameter name alone without an = value portion.
    Pull request courtesy alphavector.

    References: #11122, #13432

  • [postgresql] [usecase] The PostgreSQL dialect now reflects the schema of a schema-qualified
    column or _postgresql.DOMAIN collation, populating the new
    String.collation_schema /
    _postgresql.DOMAIN.collation_schema parameters so that
    reflected DDL round-trips exactly. The schema is omitted from the
    reflected value when the collation is visible on the current
    search_path without qualification.

    References: #6511

  • [postgresql] [usecase] Added a new parameter String.collation_schema, as well as
    _postgresql.DOMAIN.collation_schema and
    ColumnOperators.collate.collation_schema, allowing a
    PostgreSQL schema-qualified collation name to be specified explicitly,
    rather than embedding the schema name within the collation string
    itself, which previously rendered incorrectly. As part of this change,
    collation name rendering across DDL and the _sql.collate()
    construct now consistently uses the dialect's identifier preparer for
    quoting, rather than several separate, inconsistent hand-quoting code
    paths; as a side effect, simple lowercase collation names such as
    "utf8" are no longer unconditionally quoted in generated DDL.

    References: #9693

  • [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.

    This change is also backported to: 2.0.53

    References: #13472

  • [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.

    This change is also backported to: 2.0.52

    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.

    This change is also backported to: 2.0.52

    References: #13429

  • [postgresql] [bug] Fixed issue in the asyncpg dialect where the version of the asyncpg
    DBAPI would always be reported as (99, 99, 99), as the version was
    looked up on the dialect's DBAPI wrapper module rather than on the
    asyncpg module itself.

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.

    This change is also backported to: 2.0.53

    References: #13523

  • [mysql] [bug] Fixed issue where the version of the DBAPI reported by the mysqldb and
    pymysql dialects was incorrect. Current mysqlclient releases publish
    MySQLdb.version_info and no version string at all, so no version was
    reported; pymysql publishes __version__ and version_info as
    mysqlclient compatibility values, so the version reported for pymysql
    was that of the mysqlclient release it emulates, e.g. (2, 2, 8)
    rather than (1, 2, 0).

sqlite

  • [sqlite] [usecase] Added support for multiple ON CONFLICT clauses within a single
    statement for the SQLite _sqlite.insert() construct; the
    _sqlite.Insert.on_conflict_do_update() and
    _sqlite.Insert.on_conflict_do_nothing() methods may now each be
    invoked more than once against the same construct, where the clauses
    render in the order in which they were established and are evaluated by
    SQLite in that order. As SQLite allows only the last ON CONFLICT
    clause to omit its conflict target, a
    _sqlite.Insert.on_conflict_do_nothing() call that omits
    _sqlite.Insert.on_conflict_do_nothing.index_elements must be
    the last clause established. Documentation is added at
    sqlite_on_conflict_multiple. Pull request courtesy Diemid
    Berozkin.

    References: #13113

  • [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.

    This change is also backported to: 2.0.53

    References: #13528

  • [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.

    This change is also backported to: 2.0.52

    References: #13419

  • [sqlite] [bug] Added a warning for query string arguments that are passed to a SQLite
    URL without the uri=true argument also being present, and which are
    not accepted by the sqlite3 driver itself. SQLite URI arguments
    such as mode or cache take effect only when URI mode is in use;
    without it they were previously discarded silently, so that a URL such
    as sqlite:///file:mydb?mode=memory would connect to a file on disk
    named file:mydb. Arguments intended for the driver itself may be
    passed using the _sa.create_engine.connect_args parameter.

    References: #13433

  • [sqlite] [bug] [documentation] Corrected the SQLite documentation regarding shared cache memory
    databases, which incorrectly indicated that the named form
    sqlite:///file:mydb?mode=memory&cache=shared&uri=true makes use of
    QueuePool; a single-connection pool is used for this form.
    Documentation has also been added noting that a shared cache database
    exists only for as long as at least one connection to it remains open,
    so that ordinary pool operations such as
    _engine.Engine.dispose() or use of
    _sa.create_engine.pool_recycle will discard its contents.

    References: #13433

  • [sqlite] [deprecated] Deprecated the selection of a single-connection pool class, i.e.
    SingletonThreadPool for pysqlite or StaticPool for
    aiosqlite, based on the presence of the mode=memory query string
    argument in a SQLite URL. Pool selection for SQLite is intended to be
    based on the database name alone, where only :memory: or an empty
    database name indicate a memory database; interpreting the query string
    additionally requires that assumptions be made regarding whether or not
    the resulting database can be shared among multiple connections. In a
    future release, such URLs will make use of QueuePool or
    AsyncAdaptedQueuePool as would any other URL. This notably
    includes the shared cache form
    sqlite:///file:mydb?mode=memory&cache=shared&uri=true, for which a
    queue pool is in fact the appropriate class, as a shared cache database
    supports multiple concurrent connections, whereas a single-connection
    pool causes such connections to share one transaction state.
    Applications that rely upon the present behavior should indicate the
    intended pool using the _sa.create_engine.poolclass
    parameter. Pull request courtesy Itachi-0xAI.

    References: #13433

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.

    This change is also backported to: 2.0.53

    References: #13451

  • [mssql] [bug] Improved disconnect detection for the mssql+mssqlpython dialect.
    Connection-level failures such as a dropped or reset network connection
    are now recognized by consulting the driver_error attribute of the
    exception, in addition to the message-based checks that were already in
    place, so that the affected connection is invalidated and the pool
    "pre ping" feature is able to recycle it. Pull request courtesy Sam
    Debruyn.

    References: #13441

oracle

  • [oracle] [bug] Updated the oracledb async dialect where the async cursor adapter invoked
    __enter__() rather than __aenter__() on the underlying cursor.
    While these are equivalent in oracledb itself, the correct async form is
    now used for correctness. As AsyncCursor.__aenter__() was added in
    oracledb 2.0.1, the minimum supported oracledb version is now 2.0.1,
    declared via the oracle-oracledb extra. Pull request courtesy AVRC26.

    References: #13420

  • [oracle] [bug] Fixed issue in the Oracle dialects where a _types.JSON value would
    be returned as an undecoded string for any JSON expression that is not a
    JSON column, such as a bound parameter, as well as for textual constructs
    with positional columns, such as _expression.text() combined with
    _expression.TextClause.columns().

    References: #13479

tests

  • [tests] [usecase] The version specifications used by testing exclusions such as
    testing.fails_if("+asyncmy<0.2.13") now support a driver name, in
    which case the comparison is against the version of the DBAPI rather
    than that of the database server. Previously this form raised
    AssertionError: DBAPI version specs not supported yet.

  • [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.

    This change is also backported to: 2.0.53

    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.

    This change is also backported to: 2.0.53

    References: #13518

  • [misc] [bug] Allowed the inspection registry to replace an existing registration with a
    reloaded callable from the same module and name. This avoids an assertion
    failure for tooling that unloads and reloads SQLAlchemy modules while still
    rejecting conflicting registrations. Pull request courtersy w-Jessamine.

    References: #10748