SQLAlchemy rel_2_1_0rc1

rel_2_1_0rc1Pre-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

View original

Upgraded? How did it go?

Discussion