Repository navigation
SQLModel fails to create DB constraints when using Annotated types after Pydantic 2.12.0 #1623
Description
Activity
- addedquestionFurther information is requestedFurther information is requested
on Oct 27, 2025 - marked could not assemble any primary key columns for mapped table #1606 as a duplicate of this issue
on Oct 27, 2025 Test taken from #1603
from sqlmodel import Field, SQLModel from typing_extensions import Annotated def test_declaration_syntax_1(): class Person1(SQLModel): name: str = Field(primary_key=True) class Person1Final(Person1, table=True): pass def test_declaration_syntax_2(): class Person2(SQLModel): name: Annotated[str, Field(primary_key=True)] class Person2Final(Person2, table=True): pass def test_declaration_syntax_3(): class Person3(SQLModel): name: Annotated[str, ...] = Field(primary_key=True) class Person3Final(Person3, table=True): pass
test_declaration_syntax_2andtest_declaration_syntax_3work well with Pydantic 2.11.10, but fail with Pydantic 2.12PRs:
- linked a pull request that will close this issue🐛 Fix support for `Annotated` fields with Pydantic 2.12+ #1607
on Oct 30, 2025 - linked a pull request that will close this issue🐛 Fix support for `Annotated` fields with Pydantic 2.12+ #1603
on Oct 30, 2025 I think I just ran into this issue too, but in relation to timestamps with timezone:
from datetime import datetime as dt from datetime import timezone as tz from typing_extensions import Annotated from sqlmodel import TIMESTAMP, Field, SQLModel from pydantic import BeforeValidator TIMESTAMP_WITH_TZ: type = TIMESTAMP(timezone=True) IntDatetime = Annotated[ dt, BeforeValidator(lambda x: x if isinstance(x, dt) else dt.fromtimestamp(int(x) / 1000, tz=tz.utc)), ] # This model works with both 2.11.10 and 2.12.0 class Message(SQLModel, table=True): time: dt = Field(sa_type=TIMESTAMP_WITH_TZ) # This model works with <=2.11.10, but 2.12.0 generates an error # `can't subtract offset-naive and offset-aware datetimes` # As the SQL generated is $1::TIMESTAMP WITHOUT TIME ZONE class Message(SQLModel, table=True): time: IntDatetime = Field(sa_type=TIMESTAMP_WITH_TZ)
I wanted to share this example which can also be used as a test.
With Pydantic 2.12 it prints an empty set while it should print:
{ForeignKey('vehicle.id')}from pydantic import PositiveInt from sqlmodel import Field, Session, SQLModel, col, select class Vehicle(SQLModel, table=True): name: str id: PositiveInt | None = Field(default=None, primary_key=True) class Drivers_Logbook(SQLModel, table=True): # To make this example succeed, replace the next line with either # vehicle_id: int = Field(foreign_key='vehicle.id') # vehicle_id: PositiveInt | None = Field(foreign_key='vehicle.id', default=None) vehicle_id: PositiveInt = Field(foreign_key='vehicle.id') id: PositiveInt | None = Field(default=None, primary_key=True) drivers_logbook = SQLModel.metadata.tables['drivers_logbook'] print(drivers_logbook.foreign_keys)
I also have a version using pure SQLAlchemy and couldn't reproduce the issue there.
Ran into this. The fundamental issue appears to be that the subclass
sqlmodel.main.FieldInfois no longer respected in Pydantic 2.12+. The object being passed toget_column_from_fieldis of typepydantic.fields.FieldInfo, whereas in previous versions it was the sqlmodel subclass. #1603 seems like a reasonable resolution as it restores the originalsqlmodel.main.FieldInfoobject to pass into everything else.Reacted by Brian DeRocher- added a commit that references this issue
on Nov 21, 2025 I just found out I had the same problem: no issue with
pydantic==2.11.9, but trouble after that. For reference, in both cases I have the same version ofsqlalchemy==2.0.44andsqlmodel==0.0.27in a locked container.My snippet for the regression:
import uuid from sqlmodel import Field, SQLModel class User(SQLModel, table=True): __tablename__ = "users" id: Annotated[UUID4 | None, Field(default_factory=uuid.uuid4, primary_key=True)] name: str
the error with
pydantic==2.12.4:Traceback (most recent call last): File "/app/test.py", line 4, in <module> class User(SQLModel, table=True): File "/opt/venv/lib/python3.11/site-packages/sqlmodel/main.py", line 644, in __init__ DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw) File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py", line 199, in __init__ _as_declarative(reg, cls, dict_) File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 245, in _as_declarative return _MapperConfig.setup_mapping(registry, cls, dict_, None, {}) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 326, in setup_mapping return _ClassScanMapperConfig( ^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 581, in __init__ self._early_mapping(mapper_kw) File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 367, in _early_mapping self.map(mapper_kw) File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 1995, in map mapper_cls(self.cls, self.local_table, **self.mapper_args), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<string>", line 2, in __init__ File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/util/deprecations.py", line 281, in warned return fn(*args, **kwargs) # type: ignore[no-any-return] ^^^^^^^^^^^^^^^^^^^ File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 866, in __init__ self._configure_pks() File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 1652, in _configure_pks raise sa_exc.ArgumentError( sqlalchemy.exc.ArgumentError: Mapper Mapper[User(users)] could not assemble any primary key columns for mapped table 'users'Reacted by FelixYe0509I have pinned pydantic to 2.11.9 in my
pyproject.toml:dependencies = [ # pin pydantic to < 2.12 # see https://github.com/fastapi/sqlmodel/issues/1623 "sqlmodel==0.0.26", "pydantic==2.11.9",and
uv syncbut I still get this error:class AnsDBRow(SQLModel, table=True): """Represents a question in the database.""" __table_args__ = ({"extend_existing": True},) __tablename__ = "question" id: int|None = Field(primary_key=True) project_id: int = Field(index=True, nullable=False, unique=True) question: str retrieved_docs: str retrieved_docs_n: int raw_resp: str validation_error: bool is_ai: bool | None = Field(nullable=True) explanation: str | None = Field(nullable=True)--------------------------------------------------------------------------- ArgumentError Traceback (most recent call last) Cell In[17], [line 1](vscode-notebook-cell:?execution_count=17&line=1) ----> [1](vscode-notebook-cell:?execution_count=17&line=1) class AnsDBRow(SQLModel, table=True): 2 """Represents a question in the database.""" 4 __table_args__ = ({"extend_existing": True},) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlmodel\main.py:644, in SQLModelMetaclass.__init__(cls, classname, bases, dict_, **kw) 640 setattr(cls, rel_name, rel_value) # Fix #315 641 # SQLAlchemy no longer uses dict_ 642 # Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77 643 # Tag: 1.4.36 --> [644](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlmodel/main.py:644) DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw) 645 else: 646 ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_api.py:199, in DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw) 196 cls._sa_registry = reg 198 if not cls.__dict__.get("__abstract__", False): --> [199](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_api.py:199) _as_declarative(reg, cls, dict_) 200 type.__init__(cls, classname, bases, dict_) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:245, in _as_declarative(registry, cls, dict_) 240 def _as_declarative( 241 registry: _RegistryType, cls: Type[Any], dict_: _ClassDict 242 ) -> Optional[_MapperConfig]: 243 # declarative scans the class for attributes. no table or mapper 244 # args passed separately. --> [245](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:245) return _MapperConfig.setup_mapping(registry, cls, dict_, None, {}) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:326, in _MapperConfig.setup_mapping(cls, registry, cls_, dict_, table, mapper_kw) 322 return _DeferredMapperConfig( 323 registry, cls_, dict_, table, mapper_kw 324 ) 325 else: --> [326](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:326) return _ClassScanMapperConfig( 327 registry, cls_, dict_, table, mapper_kw 328 ) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:581, in _ClassScanMapperConfig.__init__(self, registry, cls_, dict_, table, mapper_kw) 577 self._setup_table(table) 579 self._setup_inheriting_columns(mapper_kw) --> [581](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:581) self._early_mapping(mapper_kw) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:367, in _MapperConfig._early_mapping(self, mapper_kw) 366 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None: --> [367](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:367) self.map(mapper_kw) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:1995, in _ClassScanMapperConfig.map(self, mapper_kw) 1990 else: 1991 mapper_cls = Mapper 1993 return self.set_cls_attribute( 1994 "__mapper__", -> [1995](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:1995) mapper_cls(self.cls, self.local_table, **self.mapper_args), 1996 ) File <string>:2, in __init__(self, class_, local_table, properties, primary_key, non_primary, inherits, inherit_condition, inherit_foreign_keys, always_refresh, version_id_col, version_id_generator, polymorphic_on, _polymorphic_map, polymorphic_identity, concrete, with_polymorphic, polymorphic_abstract, polymorphic_load, allow_partial_pks, batch, column_prefix, include_properties, exclude_properties, passive_updates, passive_deletes, confirm_deleted_rows, eager_defaults, legacy_is_orphan, _compiled_cache_size) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\util\deprecations.py:281, in deprecated_params.<locals>.decorate.<locals>.warned(fn, *args, **kwargs) 274 if m in kwargs: 275 _warn_with_version( 276 messages[m], 277 versions[m], 278 version_warnings[m], 279 stacklevel=3, 280 ) --> [281](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/util/deprecations.py:281) return fn(*args, **kwargs) File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\mapper.py:866, in Mapper.__init__(self, class_, local_table, properties, primary_key, non_primary, inherits, inherit_condition, inherit_foreign_keys, always_refresh, version_id_col, version_id_generator, polymorphic_on, _polymorphic_map, polymorphic_identity, concrete, with_polymorphic, polymorphic_abstract, polymorphic_load, allow_partial_pks, batch, column_prefix, include_properties, exclude_properties, passive_updates, passive_deletes, confirm_deleted_rows, eager_defaults, legacy_is_orphan, _compiled_cache_size) 864 self._configure_properties() 865 self._configure_polymorphic_setter() --> [866](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/mapper.py:866) self._configure_pks() 867 self.registry._flag_new_mapper(self) 868 self._log("constructed") File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\mapper.py:1652, in Mapper._configure_pks(self) 1647 # otherwise, see that we got a full PK for the mapped table 1648 elif ( 1649 self.persist_selectable not in self._pks_by_table 1650 or len(self._pks_by_table[self.persist_selectable]) == 0 1651 ): -> [1652](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/mapper.py:1652) raise sa_exc.ArgumentError( 1653 "Mapper %s could not assemble any primary " 1654 "key columns for mapped table '%s'" 1655 % (self, self.persist_selectable.description) 1656 ) 1657 elif self.local_table not in self._pks_by_table and isinstance( 1658 self.local_table, schema.Table 1659 ): 1660 util.warn( 1661 "Could not assemble any primary " 1662 "keys for locally mapped table '%s' - " 1663 "no rows will be persisted in this Table." 1664 % self.local_table.description 1665 ) ArgumentError: Mapper Mapper[AnsDBRow(question)] could not assemble any primary key columns for mapped table 'question'Any idea why?
Any idea why?
You didn't use Annotated types (unless you rebound the built in types), so this is a different issue.
It may be related to your usage of
extend_existingor you're missing adefaultfor theidcolumn, but it doesn't belong here. You could open a new discussion.However I hope the issue here will be resolved soon as it blocks upgrading to Python 3.14.
I receive the same error after upgrading python to 3.14 and pydantic to the new version.
pydantic 2.12.5
pydantic-core 2.41.5
pydantic-extra-types 2.11.0
pydantic-settings 2.12.0This is the definition:
class MyModel(SQLModel, table=True):
id: UUID4 = Field(default_factory=uuid4, primary_key=True, index=True, nullable=False)
description: str = Field(default="Default Description", nullable=False)
...Is this the same issue that prevents the 'new style' Pydantic Annotated pattern from working?
For example, if I define a field in a table model as:
foo: Annotated[int, Field(sa_type=BigInteger)]then it ignores the sa_type and the column is created with a regular INT type. But if I use
foo: int = Field(sa_type=BigInteger)then it works as expected.
foo: Annotated[int, Field(sa_type=BigInteger)]
I think the issue is that you mixed field and type metadata. As mentioned in the Tip box in the section of the Pydantic docs you linked, you annotated the
inttype withsa_typeand it doesn't have any effect there.You need to put that on the field
foo, as done in your second example. I'm not sure it could be written differently with the same effect.@KrilleGH In both cases the sa_type is applied to the FieldInfo definition. But in the first case it is injected into the model via a 'default' value of the model field (which is a little bit of a hack/abuse) and the other via metadata on the model field type. Pydantic understands both approaches, but it appears that SQLModel doesn't (yet?) understand the latter. Since SQLModel has other issues with type metadata introduced via Annotated (the discussion above) I wondered if these two issues were actually the same thing.
Just use pydantic<=2.11.9
Reacted by ianReacted by iangoldbyJust use pydantic<=2.11.9
I thoght the previous text with the detailed description that was here before is much more interesting. Could you bring it back?
只需使用 pydantic<=2.11.9 即可。
我觉得之前那段描述详尽的文字更有意思。您能把它恢复过来吗?
At the beginning, i thought its' because pydantic/pydantic@d3a57eb
this fix change the FieldInfo copy logic, don't use copy.copy() to clone the FieldInfo
if type(self) is FieldInfo: # Fast-path if the instance isn't a subclass (copy.copy()relies on pickling which is slower): copied = FieldInfo.__new__(FieldInfo) for attr_name in FieldInfo.__slots__: setattr(copied, attr_name, getattr(self, attr_namebut when i check the commit date i realize it still not release
so i delete what i wrote
Reacted by Christian Hattemer
Discussed in #1597
Originally posted by MarishLakshmanan October 8, 2025
First Check
Commit to Help
Example Code
Environment:
Pydantic==2.12.0
Code:
Description
Description
After upgrading to Pydantic 2.12.0,
SQLModelno longer creates database constraints (like primary keys, unqiue keys ) for fields defined usingAnnotatedtypes.Example:
Expected Behavior
The table should be created with id as an varchar and primary key
Actual Behavior
it throws an error
Additional notes
In case you use an unique key constraint the table will be created, you will not get any error but the unique key constraint will not be created.
Operating System
Linux
Operating System Details
No response
SQLModel Version
0.0.26
Python Version
3.13.3
Additional Context
No response