Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
66e1bfe
Fix sqlmodel field being overide by pydantic
stickm4n Oct 11, 2025
93e15c1
Add tests for syntax declaration
stickm4n Oct 11, 2025
38dc566
Simplify logic
stickm4n Oct 11, 2025
1b69033
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2025
46ce312
Add type ignore comment for field assignment
stickm4n Oct 11, 2025
cf62a18
Remove type ignore comments from execute method signatures
stickm4n Oct 11, 2025
7fd7f0c
Fix inheritance attrib not being properly parsed
stickm4n Oct 11, 2025
28445d1
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2025
b867ad7
Fix inheritance when getting annotations
stickm4n Oct 11, 2025
68beb72
Fix type ignore comment for annotation retrieval
stickm4n Oct 11, 2025
45af011
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2025
b50566d
Fix col form field for pydantic 1
stickm4n Oct 11, 2025
6fd5534
Fix import
stickm4n Oct 11, 2025
ee8fd7b
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2025
eb3cfcc
Fix field annotation retrieval for pydantic integration
stickm4n Oct 20, 2025
7d75b6e
Merge branch 'main' into pydantic-2.12-integration
svlandeg Oct 21, 2025
16c21c1
🐛 Fix composite primary with AfterValidator/Annotated
patrick91 Oct 9, 2025
984e040
Merge branch 'main' into pydantic-2.12-integration
svlandeg Oct 30, 2025
9235285
refactor according to Yurii's suggestion
svlandeg Oct 30, 2025
57f75ef
Merge branch 'pydantic-2.12-integration' of https://github.com/stickM…
svlandeg Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion sqlmodel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -562,7 +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():
col = get_column_from_field(v)
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:
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.
Expand Down
26 changes: 26 additions & 0 deletions tests/test_declaration_syntax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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
75 changes: 75 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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()
Loading