diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index a220b193f1..203a8f36c0 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -199,10 +199,15 @@ def get_sa_type_from_field(field: Any) -> Any: def get_field_metadata(field: Any) -> Any: + result = FakeMetadata() for meta in field.metadata: if isinstance(meta, (PydanticMetadata, MaxLen)): - return meta - return FakeMetadata() + for attr in ("max_length", "max_digits", "decimal_places"): + if getattr(result, attr, None) is None: + value = getattr(meta, attr, None) + if value is not None: + setattr(result, attr, value) + return result def sqlmodel_table_construct( diff --git a/tests/test_main.py b/tests/test_main.py index fa40b71853..ca01554e7a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -216,3 +216,26 @@ class Hero(SQLModel, table=True): assert len(foreign_keys) == 1 assert foreign_keys[0].ondelete == "CASCADE" assert team_id_column.nullable is False + + +def test_metadata_constraints_not_shadowed_by_earlier_metadata(clear_sqlmodel): + from decimal import Decimal + + from pydantic import Field as PydanticField + + class Item(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + code: Annotated[ + str, PydanticField(pattern=r"^[a-z]+$"), PydanticField(max_length=10) + ] + price: Annotated[ + Decimal, + PydanticField(allow_inf_nan=False), + PydanticField(max_digits=8, decimal_places=2), + ] + + code_column = Item.__table__.c.code # type: ignore[attr-defined] + price_column = Item.__table__.c.price # type: ignore[attr-defined] + assert code_column.type.length == 10 + assert price_column.type.precision == 8 + assert price_column.type.scale == 2