From 66e1bfe3d1db2e502416897e32e3fd444d28bc47 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 05:26:06 +0200 Subject: [PATCH 01/17] Fix sqlmodel field being overide by pydantic --- sqlmodel/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7c916f79af..536ae48fcb 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -562,7 +562,14 @@ def get_config(name: str) -> Any: # If it was passed by kwargs, ensure it's also set in config set_config_value(model=new_cls, parameter="table", value=config_table) for k, v in get_model_fields(new_cls).items(): - col = get_column_from_field(v) + original_field = getattr(v, "_original_assignment", Undefined) + annotated_field_meta = new_cls.__annotations__[k].__dict__.get("__metadata__", []) + annotated_field = next((f for f in annotated_field_meta if isinstance(f, FieldInfo)), None) + field = original_field if isinstance(original_field, FieldInfo) else (annotated_field or v) + # Get the original sqlmodel FieldInfo, pydantic >=v2.12 changes the model + field.annotation = v.annotation + # Guarantee the field has the correct type + col = get_column_from_field(field) setattr(new_cls, k, col) # Set a config flag to tell FastAPI that this should be read with a field # in orm_mode instead of preemptively converting it to a dict. From 93e15c1b88813e9bfa00525161769a788c8673a6 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 05:27:23 +0200 Subject: [PATCH 02/17] Add tests for syntax declaration --- tests/test_declaration_syntax.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_declaration_syntax.py diff --git a/tests/test_declaration_syntax.py b/tests/test_declaration_syntax.py new file mode 100644 index 0000000000..34bc92186b --- /dev/null +++ b/tests/test_declaration_syntax.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from sqlmodel import SQLModel, Field + + +def test_declaration_syntax_1(): + class Person1(SQLModel, table=True): + name: str = Field(primary_key=True) + + +def test_declaration_syntax_2(): + class Person2(SQLModel, table=True): + name: Annotated[str, Field(primary_key=True)] + + +def test_declaration_syntax_3(): + class Person3(SQLModel, table=True): + name: Annotated[str, ...] = Field(primary_key=True) From 38dc566d1abb3ac121ea90708f68637970541705 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 05:37:55 +0200 Subject: [PATCH 03/17] Simplify logic --- sqlmodel/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 536ae48fcb..469ed9706a 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -563,12 +563,14 @@ def get_config(name: str) -> Any: set_config_value(model=new_cls, parameter="table", value=config_table) for k, v in get_model_fields(new_cls).items(): original_field = getattr(v, "_original_assignment", Undefined) - annotated_field_meta = new_cls.__annotations__[k].__dict__.get("__metadata__", []) - annotated_field = next((f for f in annotated_field_meta if isinstance(f, FieldInfo)), None) - field = original_field if isinstance(original_field, FieldInfo) else (annotated_field or v) # Get the original sqlmodel FieldInfo, pydantic >=v2.12 changes the model - field.annotation = v.annotation - # Guarantee the field has the correct type + if isinstance(original_field, FieldInfo): + field = original_field + else: + annotated_field_meta = new_cls.__annotations__[k].__dict__.get("__metadata__", []) + field = next((f for f in annotated_field_meta if isinstance(f, FieldInfo)), v) + field.annotation = v.annotation + # Guarantee the field has the correct type col = get_column_from_field(field) setattr(new_cls, k, col) # Set a config flag to tell FastAPI that this should be read with a field From 1b69033fbce85dc015f912887bd8d31103b8f9f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:38:39 +0000 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 8 ++++++-- tests/test_declaration_syntax.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 469ed9706a..4fa9ea12da 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -567,8 +567,12 @@ def get_config(name: str) -> Any: if isinstance(original_field, FieldInfo): field = original_field else: - annotated_field_meta = new_cls.__annotations__[k].__dict__.get("__metadata__", []) - field = next((f for f in annotated_field_meta if isinstance(f, FieldInfo)), v) + annotated_field_meta = new_cls.__annotations__[k].__dict__.get( + "__metadata__", [] + ) + field = next( + (f for f in annotated_field_meta if isinstance(f, FieldInfo)), v + ) field.annotation = v.annotation # Guarantee the field has the correct type col = get_column_from_field(field) diff --git a/tests/test_declaration_syntax.py b/tests/test_declaration_syntax.py index 34bc92186b..c27bcadb81 100644 --- a/tests/test_declaration_syntax.py +++ b/tests/test_declaration_syntax.py @@ -1,6 +1,6 @@ from typing import Annotated -from sqlmodel import SQLModel, Field +from sqlmodel import Field, SQLModel def test_declaration_syntax_1(): From 46ce3122cdc5b27392834e496b317aebff95f620 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 06:02:29 +0200 Subject: [PATCH 05/17] Add type ignore comment for field assignment --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 4fa9ea12da..884ec9f0f0 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -572,7 +572,7 @@ def get_config(name: str) -> Any: ) field = next( (f for f in annotated_field_meta if isinstance(f, FieldInfo)), v - ) + ) # type: ignore[assignment] field.annotation = v.annotation # Guarantee the field has the correct type col = get_column_from_field(field) From cf62a1879649240a93063b10e9028220bc381890 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 06:05:25 +0200 Subject: [PATCH 06/17] Remove type ignore comments from execute method signatures --- sqlmodel/ext/asyncio/session.py | 2 +- sqlmodel/orm/session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 54488357bb..ff99dff899 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -129,7 +129,7 @@ async def exec( ``` """ ) - async def execute( # type: ignore + async def execute( self, statement: _Executable, params: Optional[_CoreAnyExecuteParams] = None, diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index dca4733d61..9e82d48a73 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -113,7 +113,7 @@ def exec( """, category=None, ) - def execute( # type: ignore + def execute( self, statement: _Executable, params: Optional[_CoreAnyExecuteParams] = None, From 7fd7f0cf03b4b86ad9699968edcd6aeee2eb3a19 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 14:26:18 +0200 Subject: [PATCH 07/17] Fix inheritance attrib not being properly parsed --- sqlmodel/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 884ec9f0f0..8a542e4279 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -567,14 +567,13 @@ def get_config(name: str) -> Any: if isinstance(original_field, FieldInfo): field = original_field else: - annotated_field_meta = new_cls.__annotations__[k].__dict__.get( - "__metadata__", [] - ) + annotated_field = get_annotations(new_cls.__dict__).get(k, {}) + annotated_field_meta = getattr(annotated_field,"__metadata__", (())) field = next( (f for f in annotated_field_meta if isinstance(f, FieldInfo)), v ) # type: ignore[assignment] - field.annotation = v.annotation - # Guarantee the field has the correct type + field.annotation = v.annotation + # Guarantee the field has the correct type col = get_column_from_field(field) setattr(new_cls, k, col) # Set a config flag to tell FastAPI that this should be read with a field From 28445d114b3e49a80bd5a8d93f9655f8b806a7ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:26:32 +0000 Subject: [PATCH 08/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 8a542e4279..ccffd7aae9 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -568,7 +568,9 @@ def get_config(name: str) -> Any: field = original_field else: annotated_field = get_annotations(new_cls.__dict__).get(k, {}) - annotated_field_meta = getattr(annotated_field,"__metadata__", (())) + annotated_field_meta = getattr( + annotated_field, "__metadata__", (()) + ) field = next( (f for f in annotated_field_meta if isinstance(f, FieldInfo)), v ) # type: ignore[assignment] From b867ad76681ddaf5c4dd8b1050f5a3beec0be720 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 14:54:43 +0200 Subject: [PATCH 09/17] Fix inheritance when getting annotations --- sqlmodel/main.py | 6 +++++- tests/test_declaration_syntax.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index ccffd7aae9..b486e0a85f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -567,7 +567,11 @@ def get_config(name: str) -> Any: if isinstance(original_field, FieldInfo): field = original_field else: - annotated_field = get_annotations(new_cls.__dict__).get(k, {}) + annotated_field = next( + ann + for c in new_cls.__mro__ + if (ann := get_annotations(c.__dict__).get(k)) + ) annotated_field_meta = getattr( annotated_field, "__metadata__", (()) ) diff --git a/tests/test_declaration_syntax.py b/tests/test_declaration_syntax.py index c27bcadb81..e1775d14ea 100644 --- a/tests/test_declaration_syntax.py +++ b/tests/test_declaration_syntax.py @@ -4,15 +4,24 @@ def test_declaration_syntax_1(): - class Person1(SQLModel, table=True): + class Person1(SQLModel): name: str = Field(primary_key=True) + class Person1Final(Person1, table=True): + pass + def test_declaration_syntax_2(): - class Person2(SQLModel, table=True): + class Person2(SQLModel): name: Annotated[str, Field(primary_key=True)] + class Person2Final(Person2, table=True): + pass + def test_declaration_syntax_3(): - class Person3(SQLModel, table=True): + class Person3(SQLModel): name: Annotated[str, ...] = Field(primary_key=True) + + class Person3Final(Person3, table=True): + pass From 68beb7271314b88590ca7f0fd525a2fcada288b4 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 14:57:09 +0200 Subject: [PATCH 10/17] Fix type ignore comment for annotation retrieval --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index b486e0a85f..d0f3f7b247 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -570,7 +570,7 @@ def get_config(name: str) -> Any: annotated_field = next( ann for c in new_cls.__mro__ - if (ann := get_annotations(c.__dict__).get(k)) + if (ann := get_annotations(c.__dict__).get(k)) # type: ignore[arg-type] ) annotated_field_meta = getattr( annotated_field, "__metadata__", (()) From 45af0114d0676130d48200677bfae35c71c351ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:57:21 +0000 Subject: [PATCH 11/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d0f3f7b247..920ed1c74a 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -570,7 +570,7 @@ def get_config(name: str) -> Any: annotated_field = next( ann for c in new_cls.__mro__ - if (ann := get_annotations(c.__dict__).get(k)) # type: ignore[arg-type] + if (ann := get_annotations(c.__dict__).get(k)) # type: ignore[arg-type] ) annotated_field_meta = getattr( annotated_field, "__metadata__", (()) From b50566d1847e6584f9234650e9ec959299b6b52e Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:11:27 +0200 Subject: [PATCH 12/17] Fix col form field for pydantic 1 --- sqlmodel/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 920ed1c74a..00a626158f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -562,6 +562,11 @@ def get_config(name: str) -> Any: # If it was passed by kwargs, ensure it's also set in config set_config_value(model=new_cls, parameter="table", value=config_table) for k, v in get_model_fields(new_cls).items(): + if not IS_PYDANTIC_V2: + col = get_column_from_field(v) + setattr(new_cls, k, col) + continue + original_field = getattr(v, "_original_assignment", Undefined) # Get the original sqlmodel FieldInfo, pydantic >=v2.12 changes the model if isinstance(original_field, FieldInfo): @@ -576,7 +581,8 @@ def get_config(name: str) -> Any: annotated_field, "__metadata__", (()) ) field = next( - (f for f in annotated_field_meta if isinstance(f, FieldInfo)), v + (f for f in annotated_field_meta if isinstance(f, FieldInfo)), + v, ) # type: ignore[assignment] field.annotation = v.annotation # Guarantee the field has the correct type From 6fd5534f6b13f708718ed544e7fa9083766ddb48 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:15:05 +0200 Subject: [PATCH 13/17] Fix import --- tests/test_declaration_syntax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_declaration_syntax.py b/tests/test_declaration_syntax.py index e1775d14ea..227ce73ac4 100644 --- a/tests/test_declaration_syntax.py +++ b/tests/test_declaration_syntax.py @@ -1,4 +1,4 @@ -from typing import Annotated +from typing_extensions import Annotated from sqlmodel import Field, SQLModel From ee8fd7bb8167a52c1dd385545be927b49c869626 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:15:16 +0000 Subject: [PATCH 14/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_declaration_syntax.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_declaration_syntax.py b/tests/test_declaration_syntax.py index 227ce73ac4..803cb9f0fd 100644 --- a/tests/test_declaration_syntax.py +++ b/tests/test_declaration_syntax.py @@ -1,6 +1,5 @@ -from typing_extensions import Annotated - from sqlmodel import Field, SQLModel +from typing_extensions import Annotated def test_declaration_syntax_1(): From eb3cfcc11d289de52c3fe6090a31a7035108db8f Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:46:11 +0200 Subject: [PATCH 15/17] Fix field annotation retrieval for pydantic integration --- sqlmodel/main.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 00a626158f..a848a60a3a 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -572,18 +572,18 @@ def get_config(name: str) -> Any: if isinstance(original_field, FieldInfo): field = original_field else: - annotated_field = next( - ann - for c in new_cls.__mro__ - if (ann := get_annotations(c.__dict__).get(k)) # type: ignore[arg-type] - ) - annotated_field_meta = getattr( - annotated_field, "__metadata__", (()) - ) - field = next( - (f for f in annotated_field_meta if isinstance(f, FieldInfo)), - v, - ) # type: ignore[assignment] + field = v # type: ignore[assignment] + # Update the FieldInfo with the correct class from annotation. + # This is required because pydantic overrides the field with its own + # class and the reference for sqlmodel.FieldInfo is lost. + for c in new_cls.__mro__: + if annotated := get_annotations(c.__dict__).get(k): # type: ignore[arg-type] + for meta in getattr(annotated, "__metadata__", ()): + if isinstance(meta, FieldInfo): + field = meta + break + break + field.annotation = v.annotation # Guarantee the field has the correct type col = get_column_from_field(field) From 16c21c1a97f9997a651cb04ca9a5c37f6fbbba3b Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Thu, 9 Oct 2025 23:26:22 +0100 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=90=9B=20Fix=20composite=20primary?= =?UTF-8?q?=20with=20AfterValidator/Annotated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_main.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 60d5c40ebb..c0e936ee72 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,14 @@ from typing import List, Optional import pytest +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import RelationshipProperty from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select +from typing_extensions import Annotated + +from .conftest import needs_pydanticv2 def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): @@ -125,3 +130,73 @@ class Hero(SQLModel, table=True): # The next statement should not raise an AttributeError assert hero_rusty_man.team assert hero_rusty_man.team.name == "Preventers" + + +def test_composite_primary_key(clear_sqlmodel): + class UserPermission(SQLModel, table=True): + user_id: int = Field(primary_key=True) + resource_id: int = Field(primary_key=True) + permission: str + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + insp: Inspector = inspect(engine) + pk_constraint = insp.get_pk_constraint(str(UserPermission.__tablename__)) + + assert len(pk_constraint["constrained_columns"]) == 2 + assert "user_id" in pk_constraint["constrained_columns"] + assert "resource_id" in pk_constraint["constrained_columns"] + + with Session(engine) as session: + perm1 = UserPermission(user_id=1, resource_id=1, permission="read") + perm2 = UserPermission(user_id=1, resource_id=2, permission="write") + session.add(perm1) + session.add(perm2) + session.commit() + + with pytest.raises(IntegrityError): + with Session(engine) as session: + perm3 = UserPermission(user_id=1, resource_id=1, permission="admin") + session.add(perm3) + session.commit() + + +@needs_pydanticv2 +def test_composite_primary_key_and_validator(clear_sqlmodel): + from pydantic import AfterValidator + + def validate_resource_id(value: int) -> int: + if value < 1: + raise ValueError("Resource ID must be positive") + return value + + class UserPermission(SQLModel, table=True): + user_id: int = Field(primary_key=True) + resource_id: Annotated[int, AfterValidator(validate_resource_id)] = Field( + primary_key=True + ) + permission: str + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + insp: Inspector = inspect(engine) + pk_constraint = insp.get_pk_constraint(str(UserPermission.__tablename__)) + + assert len(pk_constraint["constrained_columns"]) == 2 + assert "user_id" in pk_constraint["constrained_columns"] + assert "resource_id" in pk_constraint["constrained_columns"] + + with Session(engine) as session: + perm1 = UserPermission(user_id=1, resource_id=1, permission="read") + perm2 = UserPermission(user_id=1, resource_id=2, permission="write") + session.add(perm1) + session.add(perm2) + session.commit() + + with pytest.raises(IntegrityError): + with Session(engine) as session: + perm3 = UserPermission(user_id=1, resource_id=1, permission="admin") + session.add(perm3) + session.commit() From 923528572d5b3af616b6467675916884d1dc757b Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 30 Oct 2025 22:29:45 +0100 Subject: [PATCH 17/17] refactor according to Yurii's suggestion --- sqlmodel/main.py | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index a848a60a3a..8e1007f2a6 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -476,6 +476,17 @@ def Relationship( return relationship_info +# Helper function to support Pydantic 2.12+ compatibility +def _find_field_info(cls: type, field_name: str) -> Optional[FieldInfo]: + for c in cls.__mro__: + annotated = get_annotations(c.__dict__).get(field_name) # type: ignore[arg-type] + if annotated: + for meta in getattr(annotated, "__metadata__", ()): + if isinstance(meta, FieldInfo): + return meta + return None + + @__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): __sqlmodel_relationships__: Dict[str, RelationshipInfo] @@ -562,31 +573,17 @@ def get_config(name: str) -> Any: # If it was passed by kwargs, ensure it's also set in config set_config_value(model=new_cls, parameter="table", value=config_table) for k, v in get_model_fields(new_cls).items(): - if not IS_PYDANTIC_V2: - col = get_column_from_field(v) - setattr(new_cls, k, col) - continue - - original_field = getattr(v, "_original_assignment", Undefined) - # Get the original sqlmodel FieldInfo, pydantic >=v2.12 changes the model - if isinstance(original_field, FieldInfo): - field = original_field + if PYDANTIC_MINOR_VERSION >= (2, 12): + original_field = getattr(v, "_original_assignment", Undefined) + # Get the original sqlmodel FieldInfo, pydantic >=v2.12 changes the model + if isinstance(original_field, FieldInfo): + field = original_field + else: + field = _find_field_info(new_cls, field_name=k) or v # type: ignore[assignment] + field.annotation = v.annotation + col = get_column_from_field(field) else: - field = v # type: ignore[assignment] - # Update the FieldInfo with the correct class from annotation. - # This is required because pydantic overrides the field with its own - # class and the reference for sqlmodel.FieldInfo is lost. - for c in new_cls.__mro__: - if annotated := get_annotations(c.__dict__).get(k): # type: ignore[arg-type] - for meta in getattr(annotated, "__metadata__", ()): - if isinstance(meta, FieldInfo): - field = meta - break - break - - field.annotation = v.annotation - # Guarantee the field has the correct type - col = get_column_from_field(field) + col = get_column_from_field(v) setattr(new_cls, k, col) # Set a config flag to tell FastAPI that this should be read with a field # in orm_mode instead of preemptively converting it to a dict.