2.1 Changelog

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 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 Session.bulk_insert_mappings(), Session.bulk_update_mappings() or Session.bulk_save_objects() call could leave the 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 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 Session.rollback() nor 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 load_only() or 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 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 aliased() against a select() or union() / 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 relationship.default_factory as list to a relationship that used the WriteOnlyMapped or 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 Using Write Only Relationships with ORM 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 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 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.

    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 PoolEvents.connect() or 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 Exists.with_hint() and 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 Select.with_hint() and Select.with_statement_hint(). As ORM constructs such as PropComparator.any() and PropComparator.has() produce an 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 Float and 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 ForeignKey accessors ForeignKey.target_tokens, ForeignKey.target_column and ForeignKey.target_table_key, as well as new ForeignKey-related datastructure ForeignKeyTarget. ForeignKeyTarget is now accepted as a constructor argument as well. See 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 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 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 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 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]

    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 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 Table and 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 DOMAIN collation, populating the new String.collation_schema / 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 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 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 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 insert() construct; the Insert.on_conflict_do_update() and 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 Insert.on_conflict_do_nothing() call that omits Insert.on_conflict_do_nothing.index_elements must be the last clause established. Documentation is added at Specifying Multiple ON CONFLICT Clauses. 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 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 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.dispose() or use of 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 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 TEXT and 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 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 text() combined with 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

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 ORMExecuteState.user_defined_options to include options that were added to the statement before calling Select.with_only_columns() or Query.with_entities().

    References: #13309

  • [orm] [usecase]

    Session level 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 Connection.execution_options.schema_translate_map to be applicable to a Session overall.

    References: #13346

  • [orm] [performance] [usecase]

    Optimized selectinload() to skip the .unique() call on inner result sets when no nested 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 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] [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 selectinload() and 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 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.

    This change is also backported to: 2.0.51

    References: #13207

  • [orm] [bug]

    Fixed bug where a failure during tpc_prepare() within 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 SessionTransaction.rollback() due to a state-change guard, preventing proper cleanup and masking the underlying error.

    This change is also backported to: 2.0.51

    References: #13356

  • [orm] [bug]

    Fixed issue where using joinedload() with PropComparator.of_type() targeting a joined-table subclass combined with PropComparator.and_() referencing a column on that subclass would generate invalid SQL, where the subclass column was not adapted to the subquery alias. Pull request courtesy Joaquin Hui Gomez.

    This change is also backported to: 2.0.50

    References: #13203

  • [orm] [bug]

    Fixed issue where the presence of a SessionEvents.do_orm_execute() event hook would cause internal execution options such as yield_per and loader-specific state from the first orm_pre_session_exec pass to leak into the second pass, leading to errors when using relationship loaders such as selectinload() and immediateload(). The execution options passed to the second compilation pass are now based on the original options plus only the explicit updates made via ORMExecuteState.update_execution_options() within the event hook.

    This change is also backported to: 2.0.50

    References: #13301

  • [orm] [bug]

    Fixed issue where using with_polymorphic() on a leaf class (a subclass with no further descendants) or a non-inherited class would fail with an AttributeError when used in an ORM statement, due to configure_mappers() not being triggered implicitly. The fix ensures that AliasedInsp participates in the _post_inspect hook, triggering mapper configuration during ORM statement compilation.

    This change is also backported to: 2.0.50

    References: #13319

  • [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 Result.unique() filter was not properly validated against the 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 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 BLANK_SCHEMA table in a MetaData that has a default schema set, as this implicit resolution will change in a future version.

    References: #8068

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.

    This change is also backported to: 2.0.52

    References: #13386

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.

    This change is also backported to: 2.0.51

    References: #9427

  • [engine] [bug]

    Expanded try/except error handling to encompass the ConnectionEvents.before_cursor_execute() and 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 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 Select.get_final_froms() would emit a deprecation warning when the statement made use of the PostgreSQL-specific expression argument to 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 Select.distinct() is deprecated overall, and is replaced by a new PostgreSQL-specific construct (see #12342).

    This change is also backported to: 2.0.52

    References: #13396

  • [sql] [bug]

    Fixed issue where 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.

    This change is also backported to: 2.0.51

    References: #10827

  • [sql] [bug]

    Fixed issue where floor division (//) between a Float or Numeric numerator and an Integer denominator would omit the FLOOR() SQL wrapper on dialects where Dialect.div_is_floordiv is True (the default, including PostgreSQL and SQLite). FLOOR() is now applied if either the denominator or the numerator is a non-integer, so that expressions such as float_col // int_col render as FLOOR(float_col / int_col) instead of the incorrect float_col / int_col. Pull request courtesy r266-tech.

    This change is also backported to: 2.0.50

    References: #10528

  • [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 any_() and 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

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

    This change is also backported to: 2.0.51

    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.

    This change is also backported to: 2.0.51

    References: #13370

  • [postgresql] [bug]

    Fixed issue where the asyncpg driver could throw an insufficiently-handled exception InternalClientError under some circumstances, leading to connections not being properly marked as invalidated.

    This change is also backported to: 2.0.50

    References: #13241

  • [postgresql] [bug]

    Fixed issue where the ExcludeConstraint construct did not correctly forward the ExcludeConstraint.info parameter to the superclass, causing user-defined metadata to be lost. Pull request courtesy Wiktor Byrka.

    This change is also backported to: 2.0.50

    References: #13317

mysql

  • [mysql] [bug] [reflection]

    Narrowed the scope of the internal workaround for MySQL bugs #88718 and #96365 so that it is only applied where needed: MySQL 8.0.1 through 8.0.13 (where bug 88718 is present), and on systems with lower_case_table_names=2 (where bug 96365 applies, typically macOS). Previously the workaround was applied unconditionally for all MySQL 8.0+ versions, which caused a KeyError during foreign key reflection when the database user lacked SELECT privileges on referred tables.

    This change is also backported to: 2.0.50

    References: #13243

  • [mysql] [bug]

    Fixed issue in aiomysql and asyncmy dialects that appears as of using pymysql 1.2.0; the dialects were not properly taking into account logic that detects the argument signature of pymysql’s ping() method which was added as part of #10492.

    This change is also backported to: 2.0.50

    References: #13306

  • [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 is not producible by MySQL itself. Fix courtesy of Javid Khan.

    References: #13393

sqlite

  • [sqlite] [feature]

    Added JSONB type for SQLite’s binary JSON storage format, available as of SQLite version 3.45.0. Values are stored via the jsonb() SQL function and retrieved via json(), while the Python-side behavior remains identical to JSON. Pull request courtesy Shamil Abdulaev.

    See also

    JSONB

    References: #13260

mssql

  • [mssql] [performance] [reflection]

    Implemented native multi-table reflection methods for the SQL Server dialect, providing MSDialect.get_multi_columns(), MSDialect.get_multi_pk_constraint(), MSDialect.get_multi_foreign_keys(), MSDialect.get_multi_indexes() and MSDialect.get_multi_table_comment(). Previously the SQL Server dialect relied on the default dialect default implementation which calls the per-table methods in a loop; the new implementations issue a single bulk query per object type against the sys.* catalog views, avoiding the per-table round trips. The single-table reflection methods are now thin wrappers over the multi-table ones, matching the pattern used by the PostgreSQL and Oracle dialects. Pull request courtesy Gaurav Sharma.

    References: #8430

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

    This change is also backported to: 2.0.52

    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.

    This change is also backported to: 2.0.52

    References: #13392

2.1.0b2

Released: April 16, 2026

orm

  • [orm] [usecase]

    The metadata, type_annotation_map, or registry can now be set up in a declarative base also via a mixin class, not only by directly setting them on the subclass like before. The declarative class setup now uses getattr() to look for these attributes, instead of relying only on the class __dict__.

    References: #13198

  • [orm] [bug]

    Fixed issue where Session.get() would bypass the identity map and emit unnecessary SQL when with_for_update=False was passed, rather than treating it equivalently to the default of None. Pull request courtesy of Joshua Swanson.

    This change is also backported to: 2.0.49

    References: #13176

  • [orm] [bug]

    Fixed issue where chained joinedload() options would not be applied correctly when the final relationship in the chain is declared on a base mapper and accessed through a subclass mapper in a with_polymorphic() query. The path registry now correctly computes the natural path when a property declared on a base class is accessed through a path containing a subclass mapper, ensuring the loader option can be located during query compilation.

    This change is also backported to: 2.0.49

    References: #13193

  • [orm] [bug] [inheritance]

    Fixed issue where using Load.options() to apply a chained loader option such as joinedload() or selectinload() with PropComparator.of_type() for a polymorphic relationship would not generate the necessary clauses for the polymorphic subclasses. The polymorphic loading strategy is now correctly propagated when using a call such as joinedload(A.b).options(joinedload(B.c.of_type(poly))) to match the behavior of direct chaining e.g. joinedload(A.b).joinedload(B.c.of_type(poly)).

    This change is also backported to: 2.0.49

    References: #13202

  • [orm] [bug] [inheritance]

    Fixed issue where using chained loader options such as selectinload() after joinedload() with PropComparator.of_type() for a polymorphic relationship would not properly apply the chained loader option. The loader option is now correctly applied when using a call such as joinedload(A.b.of_type(poly)).selectinload(poly.SubClass.c) to eagerly load related objects.

    This change is also backported to: 2.0.49

    References: #13209

  • [orm] [bug]

    Fixed issue when using ORM mappings with Python 3.14’s PEP 649 feature that no longer requires “future annotations”, where the ORM’s introspection of the __init__ method of mapped classes would fail if non-present identifiers in annotations were present. The vendored getfullargspec() method has been amended to use Format.FORWARDREF under Python 3.14 to prevent resolution of names that aren’t present.

    This change is also backported to: 2.0.47

    References: #13104

engine

  • [engine] [usecase]

    The connection object returned by Engine.raw_connection() now supports the context manager protocol, automatically returning the connection to the pool when exiting the context.

    This change is also backported to: 2.0.47

    References: #13116

  • [engine] [bug]

    Fixed a critical issue in Engine where connections created in conjunction with the DialectEvents.do_connect() event listeners would receive shared, mutable collections for the connection arguments, leading to a variety of potential issues including unlimited growth of the argument list as well as elements within the parameter dictionary being shared among concurrent connection calls. In particular this could impact do_connect routines making use of complex mutable authentication structures.

    This change is also backported to: 2.0.48

    References: #13144

sql

  • [sql] [usecase]

    Added new parameter over.exclude to over() and related methods, enabling SQL standard frame exclusion clauses EXCLUDE CURRENT ROW, EXCLUDE GROUP, EXCLUDE TIES, EXCLUDE NO OTHERS in window functions. Pull request courtesy of Varun Chawla.

    References: #11671

  • [sql] [usecase]

    The ColumnCollection class hierarchy has been refactored to allow column names such as add, remove, update, extend, and clear to be used without conflicts. ColumnCollection is now an abstract base class, with mutation operations moved to WriteableColumnCollection and DedupeColumnCollection subclasses. The ReadOnlyColumnCollection exposed as attributes such as Table.c no longer includes mutation methods that raised NotImplementedError, allowing these common column names to be accessed naturally, e.g. table.c.add, table.c.remove, table.c.update, etc.

  • [sql] [bug]

    A warning is emitted when using the standalone distinct() function in a select() columns list outside of an aggregate function; this function is not intended as a replacement for the use of Select.distinct(). Pull request courtesy bekapono.

    References: #11526

  • [sql] [bug]

    Improved the ability for TypeDecorator to produce a correct repr() for “schema” types such as Enum and Boolean. This is mostly to support the Alembic autogenerate use case so that custom types render with relevant arguments present. Improved the architecture used by TypeEngine to produce repr() strings to be more modular for compound types like TypeDecorator.

    References: #13140

schema

typing

  • [typing] [bug]

    Fixed a typing issue where the typed members of func would return the appropriate class of the same name, however this creates an issue for typecheckers such as Zuban and pyrefly that assume PEP 749 style typechecking even if the file states that it’s a PEP 563 file; they see the returned name as indicating the method object and not the class object. These typecheckers are actually following along with an upcoming test harness that insists on PEP 749 style name resolution for this case unconditionally. Since PEP 749 is the way of the future regardless, differently-named type aliases have been added for these return types.

    This change is also backported to: 2.0.49

    References: #13167

  • [typing] [bug]

    Fixed issue in new PEP 646 support for result sets where an issue in the mypy type checker prevented “scalar” methods including Connection.scalar(), Result.scalar(), Session.scalar(), as well as async versions of these methods from applying the correct type to the scalar result value, when the columns in the originating select() were typed as Any. Pull request courtesy Yurii Karabas.

    References: #13091

  • [typing] [bug]

    Improved typing of JSON as well as dialect specific variants like JSON to include generic capabilities, so that the types may be parameterized to indicate any specific type of contents expected, e.g. JSONB[list[str]]().

    References: #13131

postgresql

  • [postgresql] [bug]

    Improve handling of two phase transaction identifiers for PostgreSQL when the identifier is provided by the user. As part of this change the psycopg dialect was updated to use the DBAPI two phase transaction API instead of executing the SQL directly.

    This change is also backported to: 2.0.50

    References: #13229

  • [postgresql] [bug]

    Fixed regular expression used when reflecting foreign keys in PostgreSQL to support escaped quotes in table names. Pull request courtesy of Austin Graham

    This change is also backported to: 2.0.49

    References: #10902

  • [postgresql] [bug]

    Fixed an issue in the PostgreSQL dialect where foreign key constraint reflection would incorrectly swap or fail to capture onupdate and ondelete values when these clauses appeared in a different order than expected in the constraint definition. This issue primarily affected PostgreSQL-compatible databases such as CockroachDB, which may return ON DELETE before ON UPDATE in the constraint definition string. The reflection logic now correctly parses both clauses regardless of their ordering.

    This change is also backported to: 2.0.47

    References: #13105

  • [postgresql] [bug]

    Fixed issue in the “Insert Many Values” Behavior for INSERT statements feature where using PostgreSQL’s ON CONFLICT clause with Insert.returning.sort_by_parameter_order enabled would generate invalid SQL when the insert used an implicit sentinel (server-side autoincrement primary key). The generated SQL would incorrectly declare a sentinel counter column in the imp_sen table alias without providing corresponding values in the VALUES clause, leading to a ProgrammingError indicating column count mismatch. The fix allows batch execution mode when embed_values_counter is active, as the embedded counter provides the ordering capability needed even with upsert behaviors, rather than unnecessarily downgrading to row-at-a-time execution.

    This change is also backported to: 2.0.47

    References: #13107

  • [postgresql] [bug]

    Fixed issue where Insert.on_conflict_do_update() parameters were not respecting compilation options such as literal_binds=True. Pull request courtesy Loïc Simon.

    This change is also backported to: 2.0.47

    References: #13110

  • [postgresql] [bug]

    Fixed issue where Insert.on_conflict_do_update() using parametrized bound parameters in the set_ clause would fail when used with executemany batching. For dialects that use the use_insertmanyvalues_wo_returning optimization (psycopg2), insertmanyvalues is now disabled when there is an ON CONFLICT clause. For cases with RETURNING, row-at-a-time mode is used when the SET clause contains parametrized bindparams (bindparams that receive values from the parameters dict), ensuring each row’s parameters are correctly applied. ON CONFLICT statements using expressions like excluded.<column> continue to batch normally.

    This change is also backported to: 2.0.47

    References: #13130

mysql

  • [mysql] [bug]

    Fixed issue where DDL compilation options were registered to the hard-coded dialect name mysql. This made it awkward for MySQL-derived dialects like MariaDB, StarRocks, etc. to work with such options when different sets of options exist for different platforms. Options are now registered under the actual dialect name, and a fallback was added to help avoid errors when an option does not exist for that dialect.

    To maintain backwards compatibility, when using the MariaDB dialect with the options mysql_with_parser or mysql_using without also specifying the corresponding mariadb_ prefixed options, a deprecation warning will be emitted. The mysql_ prefixed options will continue to work during the deprecation period. Users should update their code to additionally specify mariadb_with_parser and mariadb_using when using the mariadb:// dialect, or specify both options to support both dialects.

    Pull request courtesy Tiansu Yu.

    This change is also backported to: 2.0.47

    References: #13134

sqlite

  • [sqlite] [bug]

    Escape key and pragma values when utilizing the pysqlcipher dialect.

    This change is also backported to: 2.0.50

    References: #13230

  • [sqlite] [bug]

    Fixed issue where Insert.on_conflict_do_update() parameters were not respecting compilation options such as literal_binds=True. Pull request courtesy Loïc Simon.

    This change is also backported to: 2.0.47

    References: #13110

  • [sqlite] [bug]

    Fixed issue where Insert.on_conflict_do_update() using parametrized bound parameters in the set_ clause would fail when used with executemany batching. Row-at-a-time mode is now used for ON CONFLICT statements with RETURNING that contain parametrized bindparams, ensuring each row’s parameters are correctly applied. ON CONFLICT statements using expressions like excluded.<column> continue to batch normally.

    This change is also backported to: 2.0.47

    References: #13130

mssql

  • [mssql] [feature]

    Added support for the mssql-python driver, Microsoft’s official Python driver for SQL Server.

    See also

    mssql-python - Documentation for the mssql-python dialect

    References: #12869

  • [mssql] [usecase]

    Enhanced the aioodbc dialect to expose the fast_executemany attribute of the pyodbc cursor. This allows the fast_executemany parameter to work with the mssql+aioodbc dialect. Pull request courtesy Georg Sieber.

    This change is also backported to: 2.0.49

    References: #13152

  • [mssql] [usecase]

    Remove warning for SQL Server dialect when a new version is detected. The warning was originally added more than 15 years ago due to an unexpected value returned when using an old version of FreeTDS. The assumption is that since then the issue has been resolved, so make the SQL Server dialect behave like the other ones that don’t have an upper bound check on the version number.

    This change is also backported to: 2.0.49

    References: #13185

  • [mssql] [bug] [reflection]

    Fixed regression from version 2.0.42 caused by #12654 where the updated column reflection query would receive SQL Server “type alias” names for special types such as sysname, whereas previously the base name would be received (e.g. nvarchar for sysname), leading to warnings that such types could not be reflected and resulting in NullType, rather than the expected NVARCHAR for a type like sysname. The column reflection query now joins sys.types a second time to look up the base type when the user type name is not present in MSDialect.ischema_names, and both names are checked in MSDialect.ischema_names for a match. Pull request courtesy Carlos Serrano.

    This change is also backported to: 2.0.49

    References: #13181, #13182

oracle

  • [oracle] [feature]

    Added support for the JSON datatype when using the Oracle database with the oracledb dialect. JSON values are serialized and deserialized using configurable strategies that accommodate Oracle’s native JSON type available as of Oracle 21c. Pull request courtesy Abdallah Alhadad.

    See also

    JSON - Oracle-specific JSON class that includes implementation and platform notes.

    JSON Support

    References: #10375

  • [oracle] [bug]

    Fixed issue in Oracle dialect where the RAW datatype would not reflect the length parameter. Pull request courtesy Daniel Sullivan.

    This change is also backported to: 2.0.49

    References: #13150

2.1.0b1

Released: January 21, 2026

platform

  • [platform] [feature]

    Free-threaded Python versions are now supported in wheels released on Pypi. This integrates with overall free-threaded support added as part of #12881 for the 2.0 and 2.1 series, which includes new test suites as well as a few improvements to race conditions observed under freethreading.

    References: #12881

  • [platform] [change]

    The greenlet dependency used for asyncio support no longer installs by default. This dependency does not publish wheel files for every architecture and is not needed for applications that aren’t using asyncio features. Use the sqlalchemy[asyncio] install target to include this dependency.

    References: #10197

  • [platform] [change]

    Updated the setup manifest definition to use PEP 621-compliant pyproject.toml. Also updated the extra install dependency to comply with PEP-685. Thanks for the help of Matt Oberle and KOLANICH on this change.

  • [platform] [change]

    Python 3.10 or above is now required; support for Python 3.9, 3.8 and 3.7 is dropped as these versions are EOL.

    Note

    as of 2.0.0rc1 3.10 is also dropped

    ..seealso:

    :ref:`change_python_versions`

    References: #10357, #12029, #12819

orm

  • [orm] [feature]

    The relationship.back_populates argument to relationship() may now be passed as a Python callable, which resolves to either the direct linked ORM attribute, or a string value as before. ORM attributes are also accepted directly by relationship.back_populates. This change allows type checkers and IDEs to confirm the argument for relationship.back_populates is valid. Thanks to Priyanshu Parikh for the help on suggesting and helping to implement this feature.

    References: #10050

  • [orm] [feature]

    Added new hybrid method hybrid_property.bulk_dml() which works in a similar way as hybrid_property.update_expression() for bulk ORM operations. A user-defined class method can now populate a bulk insert mapping dictionary using the desired hybrid mechanics. New documentation is added showing how both of these methods can be used including in combination with the new from_dml_column() construct.

    References: #12496

  • [orm] [feature]

    Added new parameter composite.return_none_on to composite(), which allows control over if and when this composite attribute should resolve to None when queried or retrieved from the object directly. By default, a composite object is always present on the attribute, including for a pending object which is a behavioral change since 2.0. When composite.return_none_on is specified, a callable is passed that returns True or False to indicate if the given arguments indicate the composite should be returned as None. This parameter may also be set automatically when ORM Annotated Declarative is used; if the annotation is given as Mapped[SomeClass|None], a composite.return_none_on rule is applied that will return None if all contained columns are themselves None.

    References: #12570

  • [orm] [feature]

    Added support for per-session execution options that are merged into all queries executed within that session. The Session, sessionmaker, scoped_session, AsyncSession, and async_sessionmaker constructors now accept an Session.execution_options parameter that will be applied to all explicit query executions (e.g. using Session.execute(), Session.get(), Session.scalars()) for that session instance.

    References: #12659

  • [orm] [feature]

    Session autoflush behavior has been simplified to unconditionally flush the session each time an execution takes place, regardless of whether an ORM statement or Core statement is being executed. This change eliminates the previous conditional logic that only flushed when ORM-related statements were detected, which had become difficult to define clearly with the unified v2 syntax that allows both Core and ORM execution patterns. The change provides more consistent and predictable session behavior across all types of SQL execution.

    References: #9809

  • [orm] [feature]

    Added RegistryEvents event class that allows event listeners to be established on a registry object. The new class provides three events: RegistryEvents.resolve_type_annotation() which allows customization of type annotation resolution that can supplement or replace the use of the registry.type_annotation_map dictionary, including that it can be helpful with custom resolution for complex types such as those of PEP 695, as well as RegistryEvents.before_configured() and RegistryEvents.after_configured(), which are registry-local forms of the mapper-wide version of these hooks.

    References: #9832

  • [orm] [usecase]

    The Session.flush.objects parameter is now deprecated.

    References: #10816

  • [orm] [usecase]

    Added the utility method Session.merge_all() and Session.delete_all() that operate on a collection of instances.

    References: #11776

  • [orm] [usecase]

    Added support for using with_expression() to populate a query_expression() attribute that is also configured as the polymorphic_on discriminator column. The ORM now detects when a query expression column is serving as the polymorphic discriminator and updates it to use the column provided via with_expression(), enabling polymorphic loading to work correctly in this scenario. This allows for patterns such as where the discriminator value is computed from a related table.

    References: #12631

  • [orm] [usecase]

    Added default implementations of ColumnOperators.desc(), ColumnOperators.asc(), ColumnOperators.nulls_first(), ColumnOperators.nulls_last() to composite() attributes, by default applying the modifier to all contained columns. Can be overridden using a custom comparator.

    References: #12769

  • [orm] [usecase]

    The aliased() object now emits warnings when an attribute is accessed on an aliased class that cannot be located in the target selectable, for those cases where the aliased() is against a different FROM clause than the regular mapped table (such as a subquery). This helps users identify cases where column names don’t match between the aliased class and the underlying selectable. When aliased.adapt_on_names is True, the warning suggests checking the column name; when False, it suggests using the adapt_on_names parameter for name-based matching.

    References: #12838

  • [orm] [usecase]

    Improvements to the use case of using Declarative Dataclass Mapping with intermediary classes that are unmapped. As was the existing behavior, classes can subclass MappedAsDataclass alone without a declarative base to act as mixins, or along with a declarative base as well as __abstract__ = True to define an abstract base. However, the improved behavior scans ORM attributes like mapped_column() in this case to create correct dataclasses.field() constructs based on their arguments, allowing for more natural ordering of fields without dataclass errors being thrown. Additionally, added a new unmapped_dataclass() decorator function, which may be used to create unmapped mixins in a mapped hierarchy that is using the mapped_dataclass() decorator to create mapped dataclasses.

    References: #12854

  • [orm] [usecase]

    Added DictBundle as a subclass of Bundle that returns dict objects.

    References: #12960

  • [orm] [change]

    A sweep through class and function names in the ORM renames many classes and functions that have no intent of public visibility to be underscored. This is to reduce ambiguity as to which APIs are intended to be targeted by third party applications and extensions. Third parties are encouraged to propose new public APIs in Discussions to the extent they are needed to replace those that have been clarified as private.

    References: #10497

  • [orm] [change]

    The first_init ORM event has been removed. This event was non-functional throughout the 1.4 and 2.0 series and could not be invoked without raising an internal error, so it is not expected that there is any real-world use of this event hook.

    References: #10500

  • [orm] [change]

    Removed legacy signatures dating back to 0.9 release from the SessionEvents.after_bulk_update() and SessionEvents.after_bulk_delete().

    References: #10721

  • [orm] [changed]

    The “non primary” mapper feature, long deprecated in SQLAlchemy since version 1.3, has been removed. The sole use case for “non primary” mappers was that of using relationship() to link to a mapped class against an alternative selectable; this use case is now suited by the Relationship to Aliased Class feature.

    References: #12437

  • [orm] [bug]

    The relationship.secondary parameter no longer uses Python eval() to evaluate the given string. This parameter when passed a string should resolve to a table name that’s present in the local MetaData collection only, and never needs to be any kind of Python expression otherwise. To use a real deferred callable based on a name that may not be locally present yet, use a lambda instead.

    References: #10564

  • [orm] [bug]

    Fixed issue where joined eager loading would fail to use the “nested” form of the query when GROUP BY or DISTINCT were present if the eager joins being added were many-to-ones, leading to additional columns in the columns clause which would then cause errors. The check for “nested” is tuned to be enabled for these queries even for many-to-one joined eager loaders, and the “only do nested if it’s one to many” aspect is now localized to when the query only has LIMIT or OFFSET added.

    References: #11226

  • [orm] [bug]

    Revised the set “binary” operators for the association proxy set() interface to correctly raise TypeError for invalid use of the |, &, ^, and - operators, as well as the in-place mutation versions of these methods, to match the behavior of standard Python set() as well as SQLAlchemy ORM’s “instrumented” set implementation.

    References: #11349

  • [orm] [bug]

    A significant behavioral change has been made to the behavior of the mapped_column.default and relationship.default parameters, as well as the relationship.default_factory parameter with collection-based relationships, when used with SQLAlchemy’s Declarative Dataclass Mapping feature introduced in 2.0, where the given value (assumed to be an immutable scalar value for mapped_column.default and a simple collection class for relationship.default_factory) is no longer passed to the @dataclass API as a real default, instead a token that leaves the value un-set in the object’s __dict__ is used, in conjunction with a descriptor-level default. This prevents an un-set default value from overriding a default that was actually set elsewhere, such as in relationship / foreign key assignment patterns as well as in Session.merge() scenarios. See the full writeup in the What’s New in SQLAlchemy 2.1? document which includes guidance on how to re-enable the 2.0 version of the behavior if needed.

    References: #12168

  • [orm] [bug]

    The behavior of with_polymorphic() when used with a single inheritance mapping has been changed such that its behavior should match as closely as possible to that of an equivalent joined inheritance mapping. Specifically this means that the base class specified in the with_polymorphic() construct will be the basemost class that is loaded, as well as all descendant classes of that basemost class. The change includes that the descendant classes named will no longer be exclusively indicated in “WHERE polymorphic_col IN” criteria; instead, the whole hierarchy starting with the given basemost class will be loaded. If the query indicates that rows should only be instances of a specific subclass within the polymorphic hierarchy, an error is raised if an incompatible superclass is loaded in the result since it cannot be made to match the requested class; this behavior is the same as what joined inheritance has done for many years. The change also allows a single result set to include column-level results from multiple sibling classes at once which was not previously possible with single table inheritance.

    References: #12395

  • [orm] [bug]

    Improved the behavior of standalone “operators” like desc(), asc(), all_(), etc. so that they consult the given expression object for an overriding method for that operator, even if the object is not itself a ClauseElement, such as if it’s an ORM attribute. This allows custom comparators for things like composite() to provide custom implementations of methods like desc(), asc(), etc.

    References: #12769

  • [orm] [bug]

    ORM entities can now be involved within the SQL expressions used within relationship.primaryjoin and relationship.secondaryjoin parameters without the ORM entity information being implicitly sanitized, allowing ORM-specific features such as single-inheritance criteria in subqueries to continue working even when used in this context. This is made possible by overall ORM simplifications that occurred as of the 2.0 series. The changes here also provide a performance boost (up to 20%) for certain query compilation scenarios.

    References: #12843

  • [orm] [bug]

    The SessionEvents.do_orm_execute() event now allows direct mutation or replacement of the ORMExecuteState.parameters dictionary or list, which will take effect when the the statement is executed. Previously, changes to this collection were not accommodated by the event hook. Pull request courtesy Shamil.

    References: #12921

  • [orm] [bug]

    A change in the mechanics of how Python dataclasses are applied to classes that use MappedAsDataclass or registry.mapped_as_dataclass() to apply __annotations__ that are as identical as is possible to the original __annotations__ given, while also adding attributes that SQLAlchemy considers to be part of dataclass __annotations__, then restoring the previous annotations in exactly the same format as they were, using patterns that work with PEP 649 as closely as possible.

    References: #13021

  • [orm] [bug]

    Removed the ORDER BY clause from queries generated by selectin_polymorphic() and the Mapper.polymorphic_load parameter set to "selectin". The ORDER BY clause appears to have been an unnecessary implementation artifact.

    References: #13060

  • [orm] [bug]

    A significant change to the ORM mechanics involved with both with_loader_criteria() as well as single table inheritance, to more aggressively locate WHERE criteria which should be augmented by either the custom criteria or single-table inheritance criteria; SELECT statements that do not include the entity within the columns clause or as an explicit FROM, but still reference the entity within the WHERE clause, are now covered, in particular this will allow subqueries using EXISTS (SELECT 1) such as those rendered by Comparator.any() and Comparator.has().

    References: #13070

  • [orm]

    The noload() relationship loader option and related lazy='noload' setting is deprecated and will be removed in a future release. This option was originally intended for custom loader patterns that are no longer applicable in modern SQLAlchemy.

    References: #11045

  • [orm]

    Ignore Session.join_transaction_mode in all cases when the bind provided to the Session is an Engine. Previously if an event that executed before the session logic, like ConnectionEvents.engine_connect(), left the connection with an active transaction, the Session.join_transaction_mode behavior took place, leading to a surprising behavior.

    References: #11163

engine

  • [engine] [usecase]

    Added new execution option Connection.execution_options.driver_column_names. This option disables the “name normalize” step that takes place against the DBAPI cursor.description for uppercase-default backends like Oracle, and will cause the keys of a result set (e.g. named tuple names, dictionary keys in Row._mapping, etc.) to be exactly what was delivered in cursor.description. This is mostly useful for plain textual statements using text() or Connection.exec_driver_sql().

    References: #10789

  • [engine] [change]

    An empty sequence passed to any execute() method now raised a deprecation warning, since such an executemany is invalid. Pull request courtesy of Carlos Sousa.

    References: #9647

  • [engine] [change]

    The private method Connection._execute_compiled is removed. This method may have been used for some special purposes however the SQLCompiler object has lots of special state that should be set up for an execute call, which we don’t support.

  • [engine] [bug]

    Fixed issue in “insertmanyvalues” feature where an INSERT..RETURNING that also made use of a sentinel column to track results would fail to filter out the additional column when Result.unique() were used to uniquify the result set.

    References: #10802

  • [engine] [bug]

    Adjusted URL parsing and stringification to apply url quoting to the “database” portion of the URL. This allows a URL where the “database” portion includes special characters such as question marks to be accommodated.

    References: #11234

  • [engine] [bug]

    Fixed issue in the ConnectionEvents.after_cursor_execute() method where the SQL statement and parameter list for an “insertmanyvalues” operation sent to the event would not be the actual SQL / parameters just emitted on the cursor, instead being the non-batched form of the statement that’s used as a template to generate the batched statements.

    References: #13018

sql

  • [sql] [feature]

    Added the ability to create custom SQL constructs that can define new clauses within SELECT, INSERT, UPDATE, and DELETE statements without needing to modify the construction or compilation code of of Select, Insert, Update, or Delete directly. Support for testing these constructs, including caching support, is present along with an example test suite. The use case for these constructs is expected to be third party dialects for analytical SQL (so-called NewSQL) or other novel styles of database that introduce new clauses to these statements. A new example suite is included which illustrates the QUALIFY SQL construct used by several NewSQL databases which includes a cacheable implementation as well as a test suite.

    References: #12195

  • [sql] [feature] [core]

    The Core operator system now includes the matmul operator, i.e. the @ operator in Python as an optional operator. In addition to the __matmul__ and __rmatmul__ operator support this change also adds the missing __rrshift__ and __rlshift__. Pull request courtesy Aramís Segovia.

    References: #12479

  • [sql] [feature]

    Added new Core feature from_dml_column() that may be used in expressions inside of UpdateBase.values() for INSERT or UPDATE; this construct will copy whatever SQL expression is used for the given target column in the statement to be used with additional columns. The construct is mostly intended to be a helper with ORM hybrid_property within DML hooks.

    References: #12496

  • [sql] [feature]

    Added support for Python 3.14+ template strings (t-strings) via the new tstring() construct. This feature makes use of Python 3.14 template strings as defined in PEP 750, allowing for ergonomic SQL statement construction by automatically interpolating Python values and SQLAlchemy expressions within template strings.

    References: #12548

  • [sql] [usecase]

    Added new generalized aggregate function ordering to functions via the aggregate_order_by() method, which receives an expression and generates the appropriate embedded “ORDER BY” or “WITHIN GROUP (ORDER BY)” phrase depending on backend database. This new function supersedes the use of the PostgreSQL aggregate_order_by() function, which remains present for backward compatibility. To complement the new parameter, the aggregate_strings.order_by which adds ORDER BY capability to the aggregate_strings dialect-agnostic function which works for all included backends. Thanks much to Reuven Starodubski with help on this patch.

    References: #12853

  • [sql] [usecase]

    Changed the query style for ORM queries emitted by Session.get() as well as many-to-one lazy load queries to use the default labeling style, SelectLabelStyle.LABEL_STYLE_DISAMBIGUATE_ONLY, which normally does not apply labels to columns in a SELECT statement. Previously, the older style SelectLabelStyle.LABEL_STYLE_TABLENAME_PLUS_COL that labels columns as <tablename>_<columname> was used for Session.get() to maintain compatibility with Query. The change allows the string representation of ORM queries to be less verbose in all cases outside of legacy Query use. Pull request courtesy Inada Naoki.

    References: #12932

  • [sql] [usecase]

    Added method TableClause.insert_column() to complement TableClause.append_column(), which inserts the given column at a specific index. This can be helpful for prepending primary key columns to tables, etc.

    References: #7910

  • [sql] [usecase]

    Added support for the pow operator (**), with a default SQL implementation of the POW() function. On Oracle Database, PostgreSQL and MSSQL it renders as POWER(). As part of this change, the operator routes through a new first class func member pow, which renders on Oracle Database, PostgreSQL and MSSQL as POWER().

    References: #8579

  • [sql] [usecase] [orm]

    The Select.filter_by(), Update.filter_by() and Delete.filter_by() methods now search across all entities present in the statement, rather than limiting their search to only the last joined entity or the first FROM entity. This allows these methods to locate attributes unambiguously across multiple joined tables, resolving issues where changing the order of operations such as Select.with_only_columns() would cause the method to fail.

    If an attribute name exists in more than one FROM clause entity, an AmbiguousColumnError is now raised, indicating that Select.filter() (or Select.where()) should be used instead with explicit table-qualified column references.

    References: #8601

  • [sql] [change]

    The .c and .columns attributes on the Select and TextualSelect constructs, which are not instances of FromClause, have been removed completely, in addition to the .select() method as well as other codepaths which would implicitly generate a subquery from a Select without the need to explicitly call the Select.subquery() method.

    In the case of .c and .columns, these attributes were never useful in practice and have caused a great deal of confusion, hence were deprecated back in version 1.4, and have emitted warnings since that version. Accessing the columns that are specific to a Select construct is done via the Select.selected_columns attribute, which was added in version 1.4 to suit the use case that users often expected .c to accomplish. In the larger sense, implicit production of subqueries works against SQLAlchemy’s modern practice of making SQL structure as explicit as possible.

    Note that this is not related to the usual FromClause.c and FromClause.columns attributes, common to objects such as Table and Subquery, which are unaffected by this change.

    References: #10236

  • [sql] [change]

    the Numeric and Float SQL types have been separated out so that Float no longer inherits from Numeric; instead, they both extend from a common mixin NumericCommon. This corrects for some architectural shortcomings where numeric and float types are typically separate, and establishes more consistency with Integer also being a distinct type. The change should not have any end-user implications except for code that may be using isinstance() to test for the Numeric datatype; third party dialects which rely upon specific implementation types for numeric and/or float may also require adjustment to maintain compatibility.

    References: #5252

  • [sql] [change]

    Added new implementation for the Select.params() method and that of similar statements, via a new statement-only ExecutableStatement.params() method which works more efficiently and correctly than the previous implementations available from ClauseElement, by associating the given parameter dictionary with the statement overall rather than cloning the statement and rewriting its bound parameters. The ClauseElement.params() and ClauseElement.unique_params() methods, when called on an object that does not implement ExecutableStatement, will continue to work the old way of cloning the object, and will emit a deprecation warning. This issue both resolves the architectural / performance concerns of #7066 and also provides correct ORM compatibility for functions like aliased(), reported by #12915.

    References: #12915, #7066

  • [sql] [bug]

    The Double type is now used when a Python float value is detected as a literal value to be sent as a bound parameter, rather than the Float type. Double has the same implementation as Float, but when rendered in a CAST, produces DOUBLE or DOUBLE PRECISION rather than FLOAT. The former better matches Python’s float datatype which uses 8-byte double-precision storage. Third party dialects which don’t support the Double type directly may need adjustment so that they render an appropriate keyword (e.g. FLOAT) when the Double datatype is encountered.

    References: #10300

  • [sql] [bug]

    Fixed issue in name normalization (e.g. “uppercase” backends like Oracle) where using a TextualSelect would not properly maintain as uppercase column names that were quoted as uppercase, even though the TextualSelect includes a Column that explicitly holds this uppercase name.

    References: #10788

  • [sql] [bug]

    Enhanced the caching structure of the over.rows and over.range so that different numerical values for the rows / range fields are cached on the same cache key, to the extent that the underlying SQL does not actually change (i.e. “unbounded”, “current row”, negative/positive status will still change the cache key). This prevents the use of many different numerical range/rows value for a query that is otherwise identical from filling up the SQL cache.

    Note that the semi-private compiler method _format_frame_clause() is removed by this fix, replaced with a new method visit_frame_clause(). Third party dialects which may have referred to this method will need to change the name and revise the approach to rendering the correct SQL for that dialect.

    References: #11515

  • [sql] [bug]

    Updated the over() clause to allow non integer values in over.range_ clause. Previously, only integer values were allowed and any other values would lead to a failure. To specify a non-integer value, use the new FrameClause construct along with the new FrameClauseType enum to specify the frame boundaries. For example:

    from sqlalchemy import FrameClause, FrameClauseType
    
    select(
        func.sum(table.c.value).over(
            range_=FrameClause(
                3.14,
                2.71,
                FrameClauseType.PRECEDING,
                FrameClauseType.FOLLOWING,
            )
        )
    )

    References: #12596

  • [sql] [bug]

    Added a new concept of “operator classes” to the SQL operators supported by SQLAlchemy, represented within the enum OperatorClass. The purpose of this structure is to provide an extra layer of validation when a particular kind of SQL operation is used with a particular datatype, to catch early the use of an operator that does not have any relevance to the datatype in use; a simple example is an integer or numeric column used with a “string match” operator.

    References: #12736

  • [sql] [bug]

    Fixed an issue in Select.join_from() where the join condition between the left and right tables specified in the method call could be incorrectly determined based on an intermediate table already present in the FROM clause, rather than matching the foreign keys between the immediate left and right arguments. The join condition is now determined by matching primary keys between the two tables explicitly passed to Select.join_from(), ensuring consistent and predictable join behavior regardless of the order of join operations or other tables present in the query. The fix is applied to both the Core and ORM implementations of Select.join_from().

    References: #12931

  • [sql] [bug]

    Fixed issue where anonymous label generation for CTE constructs could produce name collisions when Python’s garbage collector reused memory addresses during complex query compilation. The anonymous name generation for CTE and other aliased constructs like Alias, Subquery and others now use os.urandom() to generate unique identifiers instead of relying on object id(), ensuring uniqueness even in cases of aggressive garbage collection and memory reuse.

    References: #12990

  • [sql]

    Removed the automatic coercion of executable objects, such as Query, when passed into Session.execute(). This usage raised a deprecation warning since the 1.4 series.

    References: #12218

schema

  • [schema] [feature]

    Added support for the SQL CREATE VIEW statement via the new CreateView DDL class. The new class allows creating database views from SELECT statements, with support for options such as TEMPORARY, IF NOT EXISTS, and MATERIALIZED where supported by the target database. Views defined with CreateView integrate with MetaData for automated DDL generation and provide a Table object for querying.

    References: #181

  • [schema] [feature]

    Added support for the SQL CREATE TABLE ... AS SELECT construct via the new CreateTableAs DDL construct and the Select.into() method. The new construct allows creating a table directly from the results of a SELECT statement, with support for options such as TEMPORARY and IF NOT EXISTS where supported by the target database. Tables defined with CreateTableAs integrate with MetaData for automated DDL generation and provide a Table object for querying. Pull request courtesy Greg Jarzab.

    References: #4950

  • [schema] [usecase]

    The the parameter DropConstraint.isolate_from_table was deprecated since it has no effect on the drop table behavior. Its default values was also changed to False.

    References: #13006

  • [schema] [bug]

    The Float and Numeric types are no longer automatically considered as auto-incrementing columns when the Column.autoincrement parameter is left at its default of "auto" on a Column that is part of the primary key. When the parameter is set to True, a Numeric type will be accepted as an auto-incrementing datatype for primary key columns, but only if its scale is explicitly given as zero; otherwise, an error is raised. This is a change from 2.0 where all numeric types including floats were automatically considered as “autoincrement” for primary key columns.

    References: #11811

  • [schema]

    Deprecate Oracle only parameters Sequence.order, Identity.order and Identity.on_null. They should be configured using the dialect kwargs oracle_order and oracle_on_null.

    References: #10247

typing

  • [typing] [feature]

    The Row object now no longer makes use of an intermediary Tuple in order to represent its individual element types; instead, the individual element types are present directly, via new PEP 646 integration, now available in more recent versions of Mypy. Mypy 1.7 or greater is now required for statements, results and rows to be correctly typed. Pull request courtesy Yurii Karabas.

    References: #10635

  • [typing] [bug]

    Fixed typing issues where ORM mapped classes and aliased entities could not be used as keys in result row mappings or as join targets in select statements. Patterns such as row._mapping[User], row._mapping[aliased(User)], row._mapping[with_polymorphic(...)] (rejected by both mypy and Pylance), and .join(aliased(User)) (rejected by Pylance) are documented and fully supported at runtime but were previously rejected by type checkers. The type definitions for _KeyType and _FromClauseArgument have been updated to accept these ORM entity types.

    This change is also backported to: 2.0.46

    References: #13075

  • [typing]

    The default implementation of TypeEngine.python_type now returns object instead of NotImplementedError, since that’s the base for all types in Python3. The python_type of JSON no longer returns dict, but instead fallbacks to the generic implementation.

    References: #10646

  • [typing] [orm]

    Removed the deprecated mypy plugin. The plugin was non-functional with newer version of mypy and it’s no longer needed with modern SQLAlchemy declarative style.

    References: #12293

  • [typing] [orm]

    Deprecated the declarative_mixin decorator since it was used only by the now removed mypy plugin.

    References: #12346

asyncio

  • [asyncio] [feature]

    The “emulated” exception hierarchies for the asyncio drivers such as asyncpg, aiomysql, aioodbc, etc. have been standardized on a common base EmulatedDBAPIException, which is now what’s available from the StatementException.orig attribute on a SQLAlchemy DBAPIError object. Within EmulatedDBAPIException and the subclasses in its hierarchy, the original driver-level exception is also now available via the EmulatedDBAPIException.orig attribute, and is also available from DBAPIError directly using the DBAPIError.driver_exception attribute.

    References: #8047

  • [asyncio] [change]

    Added an initialize step to the import of sqlalchemy.ext.asyncio so that greenlet will be imported only when the asyncio extension is first imported. Alternatively, the greenlet library is still imported lazily on first use to support use case that don’t make direct use of the SQLAlchemy asyncio extension.

    References: #10296

  • [asyncio] [change]

    Adapted all asyncio dialects, including aiosqlite, aiomysql, asyncmy, psycopg, asyncpg to use the generic asyncio connection adapter first added in #6521 for the aioodbc DBAPI, allowing these dialects to take advantage of a common framework.

    References: #10415

  • [asyncio] [change]

    Removed the compatibility async_fallback mode for async dialects, since it’s no longer used by SQLAlchemy tests. Also removed the internal function await_fallback() and renamed the internal function await_only() to await_(). No change is expected to user code.

  • [asyncio] [bug]

    Refactored all asyncio dialects so that exceptions which occur on failed connection attempts are appropriately wrapped with SQLAlchemy exception objects, allowing for consistent error handling.

    References: #11956

postgresql

  • [postgresql] [feature]

    Adds a new str subclass BitString representing PostgreSQL bitstrings in python, that includes functionality for converting to and from int and bytes, in addition to implementing utility methods and operators for dealing with bits.

    This new class is returned automatically by the postgresql.BIT type.

    References: #10556

  • [postgresql] [feature]

    Support for storage parameters in CREATE TABLE using the WITH clause has been added. The postgresql_with dialect option of Table accepts a mapping of key/value options.

    See also

    WITH - in the PostgreSQL dialect documentation

    References: #10909

  • [postgresql] [feature]

    Added syntax extension distinct_on() to build DISTINCT ON clauses. The old api, that passed columns to Select.distinct(), is now deprecated.

    References: #12342

  • [postgresql] [feature]

    Support for VIRTUAL computed columns on PostgreSQL 18 and later has been added. The default behavior when Computed.persisted is not specified has been changed to align with PostgreSQL 18’s default of VIRTUAL. When Computed.persisted is not specified, no keyword is rendered on PostgreSQL 18 and later; on older versions a warning is emitted and STORED is used as the default. To explicitly request STORED behavior on all PostgreSQL versions, specify persisted=True.

    References: #12866

  • [postgresql] [feature] [sql]

    Added support for monotonic server-side functions such as PostgreSQL 18’s uuidv7() to work with the “Insert Many Values” Behavior for INSERT statements feature. By passing monotonic=True to any Function, the function can be used as a sentinel for tracking row order in batched INSERT operations with RETURNING, allowing the ORM and Core to efficiently batch INSERT statements while maintaining deterministic row ordering.

    References: #13014

  • [postgresql] [feature]

    Added additional emulated error classes for the subclasses of asyncpg.exception.IntegrityError including RestrictViolationError, NotNullViolationError, ForeignKeyViolationError, UniqueViolationError CheckViolationError, ExclusionViolationError. These exceptions are not directly thrown by SQLAlchemy’s asyncio emulation, however are available from the newly added DBAPIError.driver_exception attribute when a IntegrityError is caught.

    References: #8047

  • [postgresql] [usecase]

    Added new parameter Enum.create_type to the Core Enum class. This parameter is automatically passed to the corresponding ENUM native type during DDL operations, allowing control over whether the PostgreSQL ENUM type is implicitly created or dropped within DDL operations that are otherwise targeting tables only. This provides control over the ENUM.create_type behavior without requiring explicit creation of a ENUM object.

    References: #10604

  • [postgresql] [usecase]

    The PostgreSQL dialect now support reflection of table options, including the storage parameters, table access method and table spaces. These options are automatically reflected when autoloading a table, and are also available via the Inspector.get_table_options() and Inspector.get_multi_table_optionsmethod() methods.

    References: #10909

  • [postgresql] [usecase]

    Added support for PostgreSQL 14+ HSTORE subscripting syntax. When connected to PostgreSQL 14 or later, HSTORE columns now automatically use the native subscript notation hstore_col['key'] instead of the arrow operator hstore_col -> 'key' for both read and write operations. This provides better compatibility with PostgreSQL’s native HSTORE subscripting feature while maintaining backward compatibility with older PostgreSQL versions.

    Warning

    Indexes in existing PostgreSQL databases which were indexed on an HSTORE subscript expression would need to be updated in order to match the new SQL syntax.

    References: #12948

  • [postgresql] [usecase]

    The default DBAPI driver for the PostgreSQL dialect has been changed to psycopg (psycopg version 3) instead of psycopg2. The psycopg2 driver remains fully supported and can be explicitly specified in the connection URL using postgresql+psycopg2://.

    The psycopg (version 3) driver includes improvements over psycopg2 including better performance when using C extensions and native support for async operations.

    References: #13010

  • [postgresql] [change]

    The Comparator.any() and Comparator.all() methods for the ARRAY type are now deprecated for removal; these two methods along with Any() and All() have been legacy for some time as they are superseded by the any_() and all_() functions, which feature more intuitive use.

    References: #10821

  • [postgresql] [change]

    Named types such as ENUM and DOMAIN (as well as the dialect-agnostic Enum version) are now more strongly associated with the MetaData at the top of the table hierarchy and are de-associated with any particular Table they may be a part of. This better represents how PostgreSQL named types exist independently of any particular table, and that they may be used across many tables simultaneously. The change impacts the behavior of the “default schema” for a named type, as well as the CREATE/DROP behavior in relationship to the MetaData and Table construct. The change also includes a new CheckFirst enumeration which allows fine grained control over “check” queries during DDL operations, as well as that the SchemaType.inherit_schema parameter is deprecated and will emit a deprecation warning when used. See the migration notes for full details.

    See also

    Changes to Named Type Handling in PostgreSQL - Complete details on PostgreSQL named type changes

    References: #10594, #12690

  • [postgresql] [bug]

    Fixed issue where PostgreSQL JSONB operators Comparator.path_match() and Comparator.path_exists() were applying incorrect VARCHAR casts to the right-hand side operand when used with newer PostgreSQL drivers such as psycopg. The operators now indicate the right-hand type as JSONPATH, which currently results in no casting taking place, but is also compatible with explicit casts if the implementation were require it at a later point.

    This change is also backported to: 2.0.46

    References: #13059

  • [postgresql] [bug]

    Fixed regression in PostgreSQL dialect where JSONB subscription syntax would generate incorrect SQL for cast() expressions returning JSONB, causing syntax errors. The dialect now properly wraps cast expressions in parentheses when using the [] subscription syntax, generating (CAST(...))[index] instead of CAST(...)[index] to comply with PostgreSQL syntax requirements. This extends the fix from #12778 which addressed the same issue for function calls.

    This change is also backported to: 2.0.46

    References: #13067

  • [postgresql] [bug]

    Improved the foreign key reflection regular expression pattern used by the PostgreSQL dialect to be more permissive in matching identifier characters, allowing it to correctly handle unicode characters in table and column names. This change improves compatibility with PostgreSQL variants such as CockroachDB that may use different quoting patterns in combination with unicode characters in their identifiers. Pull request courtesy Gord Thompson.

    This change is also backported to: 2.0.46

  • [postgresql] [bug]

    A CompileError is raised if attempting to create a PostgreSQL ENUM or DOMAIN datatype using a name that matches a known pg_catalog datatype name, and a default schema is not specified. These types must be explicit within a schema in order to be differentiated from the built-in pg_catalog type. The “public” or otherwise default schema is not chosen by default here since the type can only be reflected back using the explicit schema name as well (it is otherwise not visible due to the pg_catalog name). Pull request courtesy Kapil Dagur.

    References: #12761

mysql

  • [mysql] [feature]

    Added new construct limit() which can be applied to any update() or delete() to provide the LIMIT keyword to UPDATE and DELETE. This new construct supersedes the use of the “mysql_limit” dialect keyword argument.

  • [mysql] [mariadb] [reflection]

    Updated the reflection logic for indexes in the MariaDB and MySQL dialect to avoid setting the undocumented type key in the ReflectedIndex dicts returned by get_indexes method.

    References: #12240

mariadb

  • [mariadb] [usecase]

    Modified the MariaDB dialect so that when using the Uuid datatype with MariaDB >= 10.7, leaving the Uuid.native_uuid parameter at its default of True, the native UUID datatype will be rendered in DDL and used for database communication, rather than CHAR(32) (the non-native UUID type) as was the case previously. This is a behavioral change since 2.0, where the generic Uuid datatype delivered CHAR(32) for all MySQL and MariaDB variants. Support for all major DBAPIs is implemented including support for less common “insertmanyvalues” scenarios where UUID values are generated in different ways for primary keys. Thanks much to Volodymyr Kochetkov for delivering the PR.

    References: #10339

  • [mariadb] [bug]

    Fixed the SQL compilation for the mariadb sequence “NOCYCLE” keyword that is to be emitted when the Sequence.cycle parameter is set to False on a Sequence. Pull request courtesy Diego Dupin.

    This change is also backported to: 2.0.46

    References: #13070

  • [mariadb] [bug]

    Fixes to the MySQL/MariaDB dialect so that mariadb-specific features such as the INET4 and INET6 datatype may be used with an Engine that uses a mysql:// URL, if the backend database is actually a mariadb database. Previously, support for MariaDB features when mysql:// URLs were used instead of mariadb:// URLs was ad-hoc; with this issue resolution, the full set of schema / compiler / type features are now available regardless of how the URL was presented.

    References: #13076

sqlite

  • [sqlite] [bug]

    Fixed issue in the aiosqlite driver where SQLAlchemy’s setting of aiosqlite’s worker thread to “daemon” stopped working because the aiosqlite architecture moved the location of the worker thread in version 0.22.0. This “daemon” flag is necessary so that a program is able to exit if the SQLite connection itself was not explicitly closed, which is particularly likely with SQLAlchemy as it maintains SQLite connections in a connection pool. While it’s perfectly fine to call AsyncEngine.dispose() before program exit, this is not historically or technically necessary for any driver of any known backend, since a primary feature of relational databases is durability. The change also implements support for “terminate” with aiosqlite when using version version 0.22.1 or greater, which implements a sync .stop() method.

    This change is also backported to: 2.0.46

    References: #13039

  • [sqlite] [bug]

    Improved the behavior of JSON accessors Comparator.as_string(), Comparator.as_boolean(), Comparator.as_float(), Comparator.as_integer() to use CAST in a similar way that the PostgreSQL, MySQL and SQL Server dialects do to help enforce the expected Python type is returned.

    References: #11074

mssql

  • [mssql] [usecase]

    Added support for the IF EXISTS clause when dropping indexes on SQL Server 2016 (13.x) and later versions. The DropIndex.if_exists parameter is now honored by the SQL Server dialect, allowing conditional index drops that will not raise an error if the index does not exist. Pull request courtesy Edgar Ramírez Mondragón.

    This change is also backported to: 2.0.46

    References: #13045

  • [mssql] [bug]

    The Comparator.as_boolean() method when used on a JSON value on SQL Server will now force a cast to occur for values that are not simple true/false JSON literals, forcing SQL Server to attempt to interpret the given value as a 1/0 BIT, or raise an error if not possible. Previously the expression would return NULL.

    References: #11074

  • [mssql] [bug]

    Fix mssql+pyodbc issue where valid plus signs in an already-unquoted odbc_connect= (raw DBAPI) connection string are replaced with spaces.

    The pyodbc connector would unconditionally pass the odbc_connect value to unquote_plus(), even if it was not required. So, if the (unquoted) odbc_connect value contained PWD=pass+word that would get changed to PWD=pass word, and the login would fail. One workaround was to quote just the plus sign — PWD=pass%2Bword — which would then get unquoted to PWD=pass+word.

    References: #11250

oracle

  • [oracle] [feature]

    Added support for native BOOLEAN support in Oracle Database 23c and above. The Oracle dialect now renders BOOLEAN automatically when Boolean is used in DDL, and also now supports direct use of the BOOLEAN datatype, when 23c and above is in use. For Oracle versions prior to 23c, boolean values continue to be emulated using SMALLINT as before. Special case handling is also present to ensure a SMALLINT that’s interpreted with the Boolean datatype on Oracle Database 23c and above continues to return bool values. Pull request courtesy Yeongbae Jeon.

    See also

    Boolean Support

    References: #11633

  • [oracle] [usecase]

    The default DBAPI driver for the Oracle Database dialect has been changed to oracledb instead of cx_oracle. The cx_oracle driver remains fully supported and can be explicitly specified in the connection URL using oracle+cx_oracle://.

    The oracledb driver is a modernized version of cx_oracle with better performance characteristics and ongoing active development from Oracle.

    References: #13010

tests

  • [tests] [change]

    The top-level test runner has been changed to use nox, adding a noxfile.py as well as some included modules. The tox.ini file remains in place so that tox runs will continue to function in the near term, however it will be eventually removed and improvements and maintenance going forward will be only towards noxfile.py.

misc

  • [misc] [changed]

    Removed multiple api that were deprecated in the 1.3 series and earlier. The list of removed features includes:

    • The force parameter of IdentifierPreparer.quote and IdentifierPreparer.quote_schema;

    • The threaded parameter of the cx-Oracle dialect;

    • The _json_serializer and _json_deserializer parameters of the SQLite dialect;

    • The collection.converter decorator;

    • The Mapper.mapped_table property;

    • The Session.close_all method;

    • Support for multiple arguments in defer() and undefer().

    References: #12441