2.1.0rc1
Pre-release2.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
Sessionis used
inside a context manager after the transaction has been rolled back due
to an exception. TheInvalidRequestErrornow 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 asjoinedload(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 theof_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 thelinecachemodule, so that
generated functions appearing on a stack trace render with their source
rather than as an opaqueFile "<string>"frame. This allows tools
likepdbandinspect.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 callsuper()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
wheresynchronize_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.Sessionpermanently 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/finallyblock that resets it, so that neither
_orm.Session.rollback()nor_orm.Session.close()would clear
it, and every subsequent flush would raiseInvalidRequestError: 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 withKeyErroror
IndexErrorif 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 thespawnorforkservermultiprocessing 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.Loadstrategy 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
ArgumentErrorwith 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.CompoundSelectconstruct, which
previously failed with an obscureAttributeErrorregarding a missing
.mapperattribute, 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_factoryaslistto a
relationship that used the_orm.WriteOnlyMappedor
_orm.DynamicMappedannotation would raise an error at mapper
configuration time, as these relationships have nocollection_class.
listis 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 atwrite_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
whereDialect.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.Existsobject, 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
Numericwhere the
Numeric.decimal_return_scaleparameter was ignored when the
DBAPI does not support native decimal objects (i.e.
dialect.supports_native_decimalisFalse). In this path the result
processor was computing the conversion scale from
Numeric.scaledirectly, bypassing
Numeric.decimal_return_scaleentirely. The behavior now
matchesFloat, which already used the correct
_effective_decimal_return_scaleproperty. 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 thebindparam.literal_execute
parameter is used with an explicitbindparam()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 ServerUuid/UNIQUEIDENTIFIERrendering which now
escapes properly, theJSONPATHtype that's currently
PostgreSQL-only, and a full family of numeric types stemming from the
_types.Floatand_types.Numericbases 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.keyof each
BindParameteris 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
Tableconstructed with a blank name, would raise
IndexErrorrather 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]
ForeignKeyConstraintnow accepts a constraint which names the
same local column more than once, such asFOREIGN 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.ForeignKeyaccessors
_schema.ForeignKey.target_tokens,
_schema.ForeignKey.target_columnand
_schema.ForeignKey.target_table_key, as well as new
ForeignKey-related datastructure_schema.ForeignKeyTarget.
_schema.ForeignKeyTargetis now accepted as a constructor
argument as well. See_schema.ForeignKeyfor 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_infowhich refers to the database server.
The implementation onDefaultDialectmakes use of a new
per-dialect methodDialect.retrieve_dbapi_version()in order to
retrieve the version from the DBAPI module and return it as a
VersionInfoobject, 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.ForeignKeywhich 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_fullnameis now a legacy accessor, and
raisesInvalidRequestErrorwhen 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_withsupport toCreateViewfor specifying
PostgreSQL view options such assecurity_invoker,security_barrier,
andcheck_option, rendered as aWITH (...)clause between the view
name and theASkeyword. Additionally, thepostgresql_withparameter
accepted by_schema.Tableand_schema.Indexnow correctly
renders Python boolean values astrue/false(lowercase), and
Nonevalues as the parameter name alone without an= valueportion.
Pull request courtesy alphavector. -
[postgresql] [usecase] The PostgreSQL dialect now reflects the schema of a schema-qualified
column or_postgresql.DOMAINcollation, populating the new
String.collation_schema/
_postgresql.DOMAIN.collation_schemaparameters so that
reflected DDL round-trips exactly. The schema is omitted from the
reflected value when the collation is visible on the current
search_pathwithout qualification.References: #6511
-
[postgresql] [usecase] Added a new parameter
String.collation_schema, as well as
_postgresql.DOMAIN.collation_schemaand
ColumnOperators.collate.collation_schema, allowing a
PostgreSQL schema-qualified collation name to be specified explicitly,
rather than embedding the schema name within thecollationstring
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 SQLLIKE
wildcard rather than a literal character. This caused user-created
schemas that happen to start with "pg" followed by any other
character, such aspgsqlorpgstats, to be silently excluded
along with actual system schemas likepg_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 aschema_translate_map
or an explicitSequence, 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
asyncpgmodule 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_infoand no version string at all, so no version was
reported; pymysql publishes__version__andversion_infoas
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 CONFLICTclauses 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 lastON CONFLICT
clause to omit its conflict target, a
_sqlite.Insert.on_conflict_do_nothing()call that omits
_sqlite.Insert.on_conflict_do_nothing.index_elementsmust 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,
UNIQUEorFOREIGN KEYconstraint would be reflected asNoneif
theCONSTRAINT <name>clause were separated from the keyword that
follows it by a newline rather than by spaces. As SQLite stores the
CREATE TABLEstatement 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,DEFERRABLEandINITIALLYoptions 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
UNIQUEcolumn
constraints during SQLiteCREATE TABLEreflection 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 theuri=trueargument also being present, and which are
not accepted by thesqlite3driver itself. SQLite URI arguments
such asmodeorcachetake effect only when URI mode is in use;
without it they were previously discarded silently, so that a URL such
assqlite:///file:mydb?mode=memorywould connect to a file on disk
namedfile:mydb. Arguments intended for the driver itself may be
passed using the_sa.create_engine.connect_argsparameter.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=truemakes 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_recyclewill discard its contents.References: #13433
-
[sqlite] [deprecated] Deprecated the selection of a single-connection pool class, i.e.
SingletonThreadPoolfor pysqlite orStaticPoolfor
aiosqlite, based on the presence of themode=memoryquery 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 ofQueuePoolor
AsyncAdaptedQueuePoolas 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
TEXTandNTEXTcolumns
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_lengthcolumn, which reports the size of the in-row LOB
pointer rather than a character length for these types. The reflected
_mssql.TEXTand_mssql.NTEXTtypes now have alength
ofNone, 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+mssqlpythondialect.
Connection-level failures such as a dropped or reset network connection
are now recognized by consulting thedriver_errorattribute 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. AsAsyncCursor.__aenter__()was added in
oracledb 2.0.1, the minimum supported oracledb version is now 2.0.1,
declared via theoracle-oracledbextra. Pull request courtesy AVRC26.References: #13420
-
[oracle] [bug] Fixed issue in the Oracle dialects where a
_types.JSONvalue 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
AUTHORSfile to the set of license files included in the
built wheel, where previously only theLICENSEfile was present. As
the text ofLICENSErefers toAUTHORSfor 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