sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

SQLModel fails to create DB constraints when using Annotated types after Pydantic 2.12.0

Open YuriiMotov opened this issue 1 month ago • 5 comments

Discussed in https://github.com/fastapi/sqlmodel/discussions/1597

Originally posted by MarishLakshmanan October 8, 2025

First Check

  • [X] I added a very descriptive title here.
  • [X] I used the GitHub search to find a similar question and didn't find it.
  • [X] I searched the SQLModel documentation, with the integrated search.
  • [X] I already searched in Google "How to X in SQLModel" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [x] I already checked if it is not related to SQLModel but to Pydantic.
  • [x] I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

Environment: Pydantic==2.12.0

Code:

from pydantic import StringConstraints
NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]

Class Test(SQLModel):
    id: NonEmptyString = Field(primary_key=True) # this was working fine on pydantic v2.11.9

Description

Description

After upgrading to Pydantic 2.12.0, SQLModel no longer creates database constraints (like primary keys, unqiue keys ) for fields defined using Annotated types.

Example:

from typing import Annotated
from sqlmodel import SQLModel, Field
from pydantic import StringConstraints

NonEmptyString = Annotated[str, StringConstraints()]

class Test(SQLModel, table=True):
    id: NonEmptyString = Field(primary_key=True)

Expected Behavior

The table should be created with id as an varchar and primary key

Actual Behavior

it throws an error

sqlalchemy.exc.ArgumentError: Mapper Mapper[Test(test)] could not assemble any primary key columns for mapped table 'test'

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

YuriiMotov avatar Oct 27 '25 06:10 YuriiMotov

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_2 and test_declaration_syntax_3 work well with Pydantic 2.11.10, but fail with Pydantic 2.12

PRs:

  • #1603
  • https://github.com/fastapi/sqlmodel/pull/1607

YuriiMotov avatar Oct 27 '25 06:10 YuriiMotov

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)

Samreay avatar Nov 05 '25 08:11 Samreay

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.

KrilleGH avatar Nov 12 '25 15:11 KrilleGH

Ran into this. The fundamental issue appears to be that the subclass sqlmodel.main.FieldInfo is no longer respected in Pydantic 2.12+. The object being passed to get_column_from_field is of type pydantic.fields.FieldInfo, whereas in previous versions it was the sqlmodel subclass. #1603 seems like a reasonable resolution as it restores the original sqlmodel.main.FieldInfo object to pass into everything else.

dwreeves avatar Nov 18 '25 19:11 dwreeves

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 of sqlalchemy==2.0.44 and sqlmodel==0.0.27 in 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'

frgfm avatar Nov 25 '25 16:11 frgfm

I 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 sync but 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?

raffaem avatar Dec 13 '25 19:12 raffaem

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_existing or you're missing a default for the id column, 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.

KrilleGH avatar Dec 14 '25 15:12 KrilleGH