@@ -198,7 +198,7 @@ dataclass-level default (i.e. set using any of the
198198:paramref: `_orm.column_property.default `, or :paramref: `_orm.deferred.default `
199199parameters) is directed to be delivered at the
200200Python :term: `descriptor ` level using mechanisms in SQLAlchemy's attribute
201- system that normally return ``None `` for un-popualted columns, so that even though the default is not
201+ system that normally return ``None `` for un-populated columns, so that even though the default is not
202202populated into ``__dict__ ``, it's still delivered when the attribute is
203203accessed. This behavior is based on what Python dataclasses itself does
204204when a default is indicated for a field that also includes ``init=False ``.
@@ -721,6 +721,92 @@ up front, which would be verbose and not automatic.
721721
722722:ticket: `10635 `
723723
724+ .. _change_13085 :
725+
726+ Better type checker integration for Core froms, like Table
727+ ----------------------------------------------------------
728+
729+ SQLAlchemy 2.1 changes :class: `_schema.Table `, along with most
730+ :class: `_sql.FromClause ` subclasses, to be generic on the column collection,
731+ providing the option for better static type checking support.
732+ By declaring the columns using a :class: `_schema.TypedColumns ` subclass and
733+ providing it to the :class: `_schema.Table ` instance, IDEs and type checkers
734+ can infer the exact types of columns when accessing them via the
735+ :attr: `_schema.Table.c ` attribute, enabling better autocomplete and type validation.
736+
737+ Example usage::
738+
739+ from sqlalchemy import Table, TypedColumns, Column, Integer, MetaData, select
740+
741+
742+ class user_cols(TypedColumns):
743+ id = Column(Integer, primary_key=True)
744+ name: Column[str]
745+ age: Column[int]
746+
747+ # optional, used to infer the select types when selecting the table
748+ __row_pos__: tuple[int, str, int]
749+
750+
751+ metadata = MetaData()
752+ user = Table("user", metadata, user_cols)
753+
754+ # Type checkers now understand the column types when selecting single columns
755+ stmt = select(user.c.id, user.c.name) # Inferred as Select[int, str]
756+
757+ # and also when selecting the whole table, when __row_pos__ is present
758+ stmt = select(user) # Inferred as Select[int, str, int]
759+
760+ The optional :attr: `sqlalchemy.sql._annotated_cols.HasRowPos.__row_pos__ ` annotation
761+ is used to infer the types of a select when selecting the table directly.
762+
763+ Columns can be declared in :class: `.TypedColumns ` subclasses by instantiating
764+ them directly or by using only a type annotations, that will be inferred when
765+ generating a :class: `_schema.Table `.
766+
767+ Other :class: `_sql.FromClause `, like :class: `_sql.Join `, :class: `_sql.CTE `, etc, can be made
768+ generic using the :meth: `_sql.FromClause.with_cols ` method::
769+
770+ # using with_cols the ``c`` collection of the cte has typed tables
771+ cte = user.select().cte().with_cols(user_cols)
772+
773+ ORM Integration
774+ ^^^^^^^^^^^^^^^
775+
776+ This functionality also offers some integration with the ORM, by using
777+ :class: `_orm.MappedColumn ` annotated attributes in the ORM model and
778+ :func: `_orm.as_typed_table ` to get an annotated :class: `_sql.FromClause `::
779+
780+ from sqlalchemy import TypedColumns
781+ from sqlalchemy.orm import DeclarativeBase, mapped_column
782+ from sqlalchemy.orm import MappedColumn, as_typed_table
783+
784+
785+ class Base(DeclarativeBase):
786+ pass
787+
788+
789+ class A(Base):
790+ __tablename__ = "a"
791+ __typed_cols__: "a_cols"
792+
793+ id: MappedColumn[int] = mapped_column(primary_key=True)
794+ data: MappedColumn[str]
795+
796+
797+ class a_cols(A, TypedColumns):
798+ pass
799+
800+
801+ # table_a is annotated as FromClause[a_cols], and is just A.__table__
802+ table_a = as_typed_table(A)
803+
804+ For proper typing integration :class: `_orm.MappedColumn ` should be used
805+ to annotate the single columns, since it's a more specific annotation than
806+ the usual :class: `_orm.Mapped ` used for ORM attributes.
807+
808+ :ticket: `13085 `
809+
724810.. _change_8601 :
725811
726812``filter_by() `` now searches across all FROM clause entities
0 commit comments