From 1e3b72fbf451e98ba513aa8005e771c58fc911ec Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:39:53 +0530 Subject: [PATCH] fix: merge field constraints across all metadata items get_field_metadata returned the first field.metadata item matching (PydanticMetadata, MaxLen) and stopped. Pydantic v2 spreads constraints across several metadata items, so when a matching item without the length or precision (for example a pattern or allow_inf_nan constraint) precedes the item that carries max_length, max_digits, or decimal_places, those values were silently dropped and the generated SQLAlchemy column was unbounded (VARCHAR / NUMERIC with no length or precision). Scan all matching metadata items and merge max_length, max_digits, and decimal_places into a single result, keeping the first non-None value for each attribute. Single-metadata fields are unaffected. Add a regression test covering the order-dependent str and Decimal cases. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- sqlmodel/_compat.py | 9 +++++++-- tests/test_main.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) 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