On this page · 6 sections
Anyone who calls tstring() in SQLAlchemy 2.1 must review the string literals in those calls before upgrading to 2.1.4. The Release 2.1.2 notes describe a breaking change: plain string portions of a tstring() construct are no longer scanned for bound parameter names in the :name format, and the \: escape is no longer interpreted there. Before this change, each string portion was parsed the same way as text(), so a colon inside a quoted SQL literal could become a bound parameter. The notes say such a literal failed at execution time. The change matters most for code that writes colons inside quoted text, such as a label containing a time.
The same notes say the old parsing was not an intended part of the feature. The text was parsed that way only because the feature used text() internally. Your code should therefore not depend on colon parsing in plain string portions. When you need a real parameter, the notes show bindparam() inside the template. The upgrade to 2.1.4 should follow the tstring() review, because the 2.1.4 release notes describe a set of fixes that can change behavior in other areas as well.
The Release 2.1.4 notes, dated October 7, 2026, cover the engine, the SQL compiler cache, PostgreSQL types, asyncpg transactions, SQL Server division, and the mutable extension. The Release 2.1.0 notes, dated September 24, 2026, add a change to how aliased() treats an existing alias. This guide uses these three releases as a checklist. Each step asks you to search your own code for the features involved, because not every application uses every affected area.
Who must migrate, and why #
Teams that call tstring() are the most exposed, because the 2.1.2 change alters how plain string portions are rendered. The risky pattern is a colon followed by a name inside a quoted SQL literal. Teams that use tstring() in only a few reporting queries should still review each call, since a single literal can change meaning.
Applications that do not use tstring() are not affected by that specific change, but they should still review the 2.1.4 fixes if they use any of these features: iterating a Result directly or calling scalar-returning methods, copying Enum or PostgreSQL DOMAIN types, using selectinload() on a many-to-many relationship with extra criteria, using in-place operators on the mutable extension types, using asyncpg across event loops, or dividing values on SQL Server.
Note
The regressions were introduced in different releases. The 2.1.4 notes say the Result reference-cycle problem came from a 2.1 refactoring of Result into Cython code. The cursor-closing regression came from the 1.4 series. The DOMAIN copy regression came from 2.0.42, and the logging token problem came from 2.0.30. Upgrading to 2.1.4 therefore fixes some issues that were not created by a move to 2.1 itself.
Migration steps #
Work through these steps in order. The first two address the breaking tstring() change. The later steps address behavior changes that the release notes describe in other areas.
- Search your codebase for every call to
tstring(. Read the literal portions of each template, not only the interpolated expressions, because a colon followed by a name inside a quoted literal is the pattern to find. - For each colon you find, decide whether it is data or a parameter. A colon that is data should stay as written, and 2.1.2 renders it exactly as given. A colon that was meant as a parameter should become
bindparam()inside the template, with a value supplied at execution time. - Upgrade to SQLAlchemy 2.1.4 only after the
tstring()review is complete. The 2.1.4 notes contain the fixes described in the remaining steps. - Find code that relied on garbage collection to release cursors. The 2.1.4 notes say that cursors for compiled statements returning no rows, including DDL and the SAVEPOINT statements emitted by
begin_nested(), are now closed immediately. Close or consume results explicitly where your code depends on timing. - Check direct iteration of
Resultobjects and scalar-returning methods such asscalars(). The notes say these no longer leave reference cycles that delayed freeing the result and its DBAPI cursor until cyclic garbage collection ran. Include memory behavior in your load tests if your service holds many results. - Compare schema type copies. The 2.1.2 notes say only the user-constructed type is registered with the
MetaDatacollection, which previously grew with each dialect-level copy. The 2.1.4 notes say copies of PostgreSQLENUMkeepsort_key_function,create_constraintandlength, while copies ofEnumkeepsort_key_function. Copies ofDOMAINkeepcollation,default,constraint_name,not_nullandcheck. Compare generated DDL before and after the upgrade. - Re-test ORM loading and mutable types. The 2.1.2 notes limit the
omit_joinoptimization for many-to-manyselectinload()to primaryjoins made only of comparisons between the parent's primary key columns and the association table. The 2.1.4 notes say the in-place|=onMutableDictand*=onMutableListnow emit change events in the mutable_toplevel extension. - Check driver and dialect paths. Drivers such as the Databricks SQL connector return tuple subclasses, which 2.1.2 converts to plain tuples when Cython extensions are in use. For asyncpg, 2.1.4 addresses a failed transaction start masking the original error. For SQL Server, the divisor of a true division is now rendered so that its fractional part is not truncated.
- If you set
logging_tokenthroughexecution_options, review code that sets it toNoneor another false value. The 2.1.4 notes say this could raiseAttributeErrorin some cases, and the fix is included in that release.
Before and after: colons inside tstring() #
The first block shows the source line that the 2.1.2 notes describe. The comment records the behavior before 2.1.2, when the colon in the quoted literal was parsed like text() and could become a bound parameter that failed at execution time. Do not treat this block as a finished fix. It is the pattern to find during your review.
# Before 2.1.2: the colon inside the quoted literal could be parsed as a bound parameter and fail at execution time.
query = tstring(t"SELECT 'time is :now' AS label")The next block is the same source line under 2.1.2 and later. The difference is only the comment, because the source line itself is unchanged. The literal is now rendered exactly as written, so no parameter is created and no value is needed for it:
# 2.1.2 and later: the literal 'time is :now' is rendered as written.
query = tstring(t"SELECT 'time is :now' AS label")The final block is the new way for a value that really should be a parameter. Use bindparam() inside the interpolation, as the 2.1.2 notes show, and supply the value when you execute the statement:
# 2.1.2 and later: an explicit parameter is written with bindparam().
query = tstring(t"SELECT {bindparam('p')}")Do not remove colons from SQL text without first deciding whether they were meant as data. A literal colon and a parameter are different things, and the migration should keep them separate.
Warning
The main pitfall is assuming that tstring() still treats colons in plain string portions the way text() does. Since 2.1.2, plain string portions are rendered exactly as given. Code whose colon literals previously failed at execution time will now send those literals as written, and code that depended on parameter parsing in those portions needs an explicit bindparam() instead.
Other regressions to check during the upgrade #
The Result fix in the 2.1.4 notes is the first item to measure. Iterating a Result directly, or using scalar-returning methods, previously could create reference cycles that kept the result and cursor alive until cyclic garbage collection. The notes say this came from the 2.1 refactoring of Result into Cython code. Services that open many results should compare memory behavior before and after the upgrade.
The ORM relationship fix in the 2.1.2 notes affects selectinload() on many-to-many relationships. The notes say it previously ignored additional criteria in the primaryjoin, such as a comparison against a column on the association table, and could load rows that should have been excluded. The omit_join optimization is now used only when the primaryjoin consists solely of comparisons between the parent's primary key columns and the association table. If your relationships include extra criteria, compare the loaded collections before and after the upgrade.
The aliased() change in the Release 2.1.0 notes applies when you call aliased() on an existing aliased construct without an explicit selectable. The notes say that case could disregard the existing selectable and alias the mapped table instead. The same applies to with_polymorphic(), where the polymorphic selectable and additional mappers are now retained. Criteria on subclass attributes and the innerjoin and selectable parameters now take effect, and the rendered SQL includes the polymorphic selectable. Review queries that alias polymorphic entities, because their SQL will change.
The 2.1.0 notes also add the reflect_only_elements mapping on reflected indexes and report invalid PostgreSQL indexes through postgresql_invalid=True. These matter only if your code reflects indexes, but they are useful to know before a schema comparison.
Verification checklist #
Use this checklist before deploying an upgraded SQLAlchemy 2.1 service. The items come from the release notes above. Confirm each one against your own application, because a change that does not affect one service may matter for another.
- Confirm that every
tstring(call has been reviewed, and that each colon inside a quoted literal is intentional data. - Confirm that every real parameter inside a
tstring()is written withbindparam()and receives a value at execution time. - Confirm that the application runs on SQLAlchemy 2.1.4 or later, after the
tstring()review. - Confirm that no code depends on cursors for no-row statements being released by garbage collection.
- Measure memory use for services that iterate
Resultobjects directly or call scalar-returning methods. - Compare generated
CREATE DOMAINandEnumDDL from copied types with the output of your previous release. - Compare results of many-to-many
selectinload()queries that include extra criteria in the primaryjoin. - Test in-place
|=onMutableDictand*=onMutableList, and confirm that changes persist after flush. - Review queries that call
aliased()on an existing alias or on awith_polymorphic()construct, and confirm that subclass criteria behave as intended. - Review any code that sets
logging_tokentoNoneor another false value throughexecution_options. - For asyncpg, confirm that a failed transaction start reports the original error.
- For SQL Server, verify true division where the divisor comes from an expression with no known numeric type.
This review was done by reading the release notes only. No tests were run against a database for this article, so each item above should be confirmed by running your own test suite against a real database.
The tstring() change in 2.1.2 is the one most likely to alter query behavior, so start there. After that, use the 2.1.4 notes as a list of behaviors to test, and confirm each one in your own application before you treat the upgrade as finished.
Sources
- Release 2.1.4 · sqlalchemy/sqlalchemy · GitHub github.com · Oct 7, 2026
- Release 2.1.2 · sqlalchemy/sqlalchemy · GitHub github.com · Oct 2, 2026
- Release 2.1.0 · sqlalchemy/sqlalchemy · GitHub github.com · Sep 24, 2026
Edited and approved by Jarosław Krzemiński. Spotted a mistake? Report a correction or read the corrections policy.




Comments