]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
improve typed column migration docs
authorFederico Caselli <cfederico87@gmail.com>
Thu, 4 Jun 2026 19:33:19 +0000 (21:33 +0200)
committerFederico Caselli <cfederico87@gmail.com>
Thu, 4 Jun 2026 19:39:13 +0000 (21:39 +0200)
Change-Id: I402580004d1c0d8583ebc9e2852f38d687ba6ae8

doc/build/changelog/migration_21.rst
lib/sqlalchemy/sql/_annotated_cols.py

index 166f80f9ec54e775987b38bd8726c04860d019dc..09c67a5b9c66c8ba8cb046f4b57ab529d295348b 100644 (file)
@@ -736,16 +736,23 @@ can infer the exact types of columns when accessing them via the
 
 Example usage::
 
-    from sqlalchemy import Table, TypedColumns, Column, Integer, MetaData, select
+    from sqlalchemy import Table, TypedColumns, Column, Integer
+    from sqlalchemy import MetaData, Named, SmallInteger, select
 
 
     class user_cols(TypedColumns):
+        # the name will be set to ``id``, type is inferred as Column[int]
+        # from the Integer SQL type.
         id = Column(Integer, primary_key=True)
-        name: Column[str]
-        age: Column[int]
+
+        # not null String column is generated
+        name: Named[str]
+
+        # nullable Integer column, the SQL type is manually set SmallInteger
+        age: Named[int | None] = Column(SmallInteger)
 
         # optional, used to infer the select types when selecting the table
-        __row_pos__: tuple[int, str, int]
+        __row_pos__: tuple[int, str, int | None]
 
 
     metadata = MetaData()
@@ -755,19 +762,20 @@ Example usage::
     stmt = select(user.c.id, user.c.name)  # Inferred as Select[int, str]
 
     # and also when selecting the whole table, when __row_pos__ is present
-    stmt = select(user)  # Inferred as Select[int, str, int]
+    stmt = select(user)  # Inferred as Select[int, str, int | None]
 
 The optional :attr:`sqlalchemy.sql._annotated_cols.HasRowPos.__row_pos__` annotation
 is used to infer the types of a select when selecting the table directly.
 
 Columns can be declared in :class:`.TypedColumns` subclasses by instantiating
-them directly or by using only a type annotations, that will be inferred when
-generating a :class:`_schema.Table`.
+them directly, like ``id``, by using only a type annotations, like ``name``, letting
+the :class:`_schema.Table` infer SQL type and nullability, or by mixing the two, like ``age``,
+to provide explicit column options while inferring nullability and/or SQL type.
 
 Other :class:`_sql.FromClause`, like :class:`_sql.Join`, :class:`_sql.CTE`, etc, can be made
 generic using the :meth:`_sql.FromClause.with_cols` method::
 
-    # using with_cols the ``c`` collection of the cte has typed tables
+    # using with_cols the ``c`` collection of the cte has typed columns
     cte = user.select().cte().with_cols(user_cols)
 
 ORM Integration
index 02c9456092b0e49e4c6e27c0c14ed66aba7bc1c8..4264c9a9bc9a5328b26541b6493d81799f143a97 100644 (file)
@@ -85,8 +85,9 @@ class TypedColumns(ReadOnlyColumnCollection[str, "Column[Any]"]):
     To resolve the columns, a simplified version of the ORM logic is used,
     in particular, columns can be declared by:
 
-    * directly instantiating them, to declare constraint, custom SQL types and
-      additional column options;
+    * directly instantiating :class:`_schema.Column`, to declare constraint,
+      custom SQL types and additional column options. The annotation is
+      usually inferred by type checkers from the column instance;
     * using only a :class:`.Named` or :class:`_schema.Column` type annotation,
       where nullability and SQL type will be inferred by the python type
       provided.
@@ -94,6 +95,9 @@ class TypedColumns(ReadOnlyColumnCollection[str, "Column[Any]"]):
     * a mix of both, where the instance can be used to declare
       constraints and other column options while the annotation will be used
       to set the SQL type and nullability if not provided by the instance.
+      The information provided in the instance will take precedence over
+      the annotation when they are conflicting, for example if setting
+      nullable=True with an annotation tha does not include ``None``.
 
     In all cases the name is inferred from the attribute name, unless
     explicitly provided.
@@ -111,6 +115,7 @@ class TypedColumns(ReadOnlyColumnCollection[str, "Column[Any]"]):
 
         class tbl_cols(TypedColumns):
             # the name will be set to ``id``, type is inferred as Column[int]
+            # from the Integer SQL type.
             id = Column(Integer, primary_key=True)
 
             # not null String column is generated
@@ -119,7 +124,7 @@ class TypedColumns(ReadOnlyColumnCollection[str, "Column[Any]"]):
             # nullable Double column is generated
             weight: Named[float | None]
 
-            # nullable Integer column, with sql name 'user_age'
+            # nullable Integer column, with SQL name 'user_age'
             age: Named[int | None] = Column("user_age")
 
             # not null column with type String(42)