Skip to content

Commit 1e3b72f

Browse files
committed
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>
1 parent 07b69be commit 1e3b72f

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

sqlmodel/_compat.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,15 @@ def get_sa_type_from_field(field: Any) -> Any:
199199

200200

201201
def get_field_metadata(field: Any) -> Any:
202+
result = FakeMetadata()
202203
for meta in field.metadata:
203204
if isinstance(meta, (PydanticMetadata, MaxLen)):
204-
return meta
205-
return FakeMetadata()
205+
for attr in ("max_length", "max_digits", "decimal_places"):
206+
if getattr(result, attr, None) is None:
207+
value = getattr(meta, attr, None)
208+
if value is not None:
209+
setattr(result, attr, value)
210+
return result
206211

207212

208213
def sqlmodel_table_construct(

tests/test_main.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,26 @@ class Hero(SQLModel, table=True):
216216
assert len(foreign_keys) == 1
217217
assert foreign_keys[0].ondelete == "CASCADE"
218218
assert team_id_column.nullable is False
219+
220+
221+
def test_metadata_constraints_not_shadowed_by_earlier_metadata(clear_sqlmodel):
222+
from decimal import Decimal
223+
224+
from pydantic import Field as PydanticField
225+
226+
class Item(SQLModel, table=True):
227+
id: int | None = Field(default=None, primary_key=True)
228+
code: Annotated[
229+
str, PydanticField(pattern=r"^[a-z]+$"), PydanticField(max_length=10)
230+
]
231+
price: Annotated[
232+
Decimal,
233+
PydanticField(allow_inf_nan=False),
234+
PydanticField(max_digits=8, decimal_places=2),
235+
]
236+
237+
code_column = Item.__table__.c.code # type: ignore[attr-defined]
238+
price_column = Item.__table__.c.price # type: ignore[attr-defined]
239+
assert code_column.type.length == 10
240+
assert price_column.type.precision == 8
241+
assert price_column.type.scale == 2

0 commit comments

Comments
 (0)