From 4885b991444b6b927957cfeb68cb55e7f2793cf2 Mon Sep 17 00:00:00 2001 From: A00474880 Date: Fri, 1 May 2026 15:18:41 -0300 Subject: [PATCH 1/8] add model_dump options to sqlmodel_update --- sqlmodel/main.py | 24 ++++++++---------------- tests/test_update.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 9a1a676775..b82f642efc 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -984,25 +984,17 @@ def sqlmodel_update( obj: builtins.dict[str, Any] | BaseModel, *, update: builtins.dict[str, Any] | None = None, + **model_dump_kwargs, ) -> _TSQLModel: - use_update = (update or {}).copy() - if isinstance(obj, dict): - for key, value in {**obj, **use_update}.items(): - if key in get_model_fields(self): - setattr(self, key, value) - elif isinstance(obj, BaseModel): - for key in get_model_fields(obj): - if key in use_update: - value = use_update.pop(key) - else: - value = getattr(obj, key) - setattr(self, key, value) - for remaining_key, value in use_update.items(): - if remaining_key in get_model_fields(self): - setattr(self, remaining_key, value) - else: + if not (isinstance(obj, dict) or isinstance(obj, BaseModel)): raise ValueError( "Can't use sqlmodel_update() with something that " f"is not a dict or SQLModel or Pydantic model: {obj}" ) + if isinstance(obj, BaseModel): + obj = obj.model_dump(**model_dump_kwargs) + use_update = (update or {}).copy() + for key, value in {**obj, **use_update}.items(): + if key in get_model_fields(self): + setattr(self, key, value) return self diff --git a/tests/test_update.py b/tests/test_update.py index de4bd6cdd2..ec15224321 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -1,3 +1,4 @@ +from pytest import raises from sqlmodel import Field, SQLModel @@ -5,10 +6,12 @@ def test_sqlmodel_update(): class Organization(SQLModel, table=True): id: int = Field(default=None, primary_key=True) name: str + city: str headquarters: str class OrganizationUpdate(SQLModel): name: str + city: str | None = None org = Organization(name="Example Org", city="New York", headquarters="NYC HQ") org_in = OrganizationUpdate(name="Updated org") @@ -17,4 +20,13 @@ class OrganizationUpdate(SQLModel): update={ "headquarters": "-", # This field is in Organization, but not in OrganizationUpdate }, + exclude_unset=True ) + # fields that should stay the same + assert org.city == "New York" + #fields that should be updated + assert org.name == "Updated org" + assert org.headquarters == "-" + # test raise value error when passing in updates other than dict or BaseModel + with raises(ValueError): + org.sqlmodel_update(["Boston"]) From 3d17d082f58bae14ae5711fa0b7a34c070a6ffae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 18:19:36 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_update.py b/tests/test_update.py index ec15224321..872efcb6fb 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -20,11 +20,11 @@ class OrganizationUpdate(SQLModel): update={ "headquarters": "-", # This field is in Organization, but not in OrganizationUpdate }, - exclude_unset=True + exclude_unset=True, ) # fields that should stay the same assert org.city == "New York" - #fields that should be updated + # fields that should be updated assert org.name == "Updated org" assert org.headquarters == "-" # test raise value error when passing in updates other than dict or BaseModel From 59d71de82811fc831f67471f376aa896b3521e8d Mon Sep 17 00:00:00 2001 From: A00474880 Date: Thu, 7 May 2026 19:06:25 -0300 Subject: [PATCH 3/8] type support for model_dump_kwargs --- sqlmodel/main.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index b82f642efc..62ed103d11 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -16,6 +16,7 @@ Literal, TypeAlias, TypeVar, + TypedDict, Union, cast, get_origin, @@ -49,7 +50,7 @@ from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid -from typing_extensions import deprecated +from typing_extensions import deprecated, Unpack from ._compat import ( PYDANTIC_MINOR_VERSION, @@ -800,6 +801,20 @@ def get_column_from_field(field: Any) -> Column: _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") +class _ModelDumpKwargs(TypedDict): + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, # v2.7 + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, # v2.12 + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, # v2.11 + serialize_as_any: bool = False, # v2.7 class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values @@ -984,7 +999,7 @@ def sqlmodel_update( obj: builtins.dict[str, Any] | BaseModel, *, update: builtins.dict[str, Any] | None = None, - **model_dump_kwargs, + **model_dump_kwargs: Unpack[_ModelDumpKwargs], ) -> _TSQLModel: if not (isinstance(obj, dict) or isinstance(obj, BaseModel)): raise ValueError( From ac676fbf62a8442631cffcab08754b06436a81ee Mon Sep 17 00:00:00 2001 From: A00474880 Date: Thu, 7 May 2026 19:08:29 -0300 Subject: [PATCH 4/8] creates temp UpdateModel to avoid issues with SQLModel's serialization settings like `include` and `exclude` --- sqlmodel/main.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 62ed103d11..b5830e64e7 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -23,7 +23,7 @@ overload, ) -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, create_model from pydantic.fields import FieldInfo as PydanticFieldInfo from sqlalchemy import ( Boolean, @@ -1007,6 +1007,20 @@ def sqlmodel_update( f"is not a dict or SQLModel or Pydantic model: {obj}" ) if isinstance(obj, BaseModel): + # Create a temp UpdateModel schema (removes extra serialization settings) + ObjClass = obj.__class__ + fields_def = { + fname: finfo.annotation + for fname, finfo in ObjClass.model_fields.items() + } + UpdateModel = create_model( + f"_{ObjClass.__name__}Update_", **fields_def + ) + # rebuild obj instance with model_construct + obj = UpdateModel.model_construct( + _fields_set=obj.model_fields_set, **obj.__dict__ + ) + # Now `obj.model_dump` works with **model_dump_kwargs obj = obj.model_dump(**model_dump_kwargs) use_update = (update or {}).copy() for key, value in {**obj, **use_update}.items(): From d9ffaf1b8f3bf0dc811e09ae99405c70e0310e23 Mon Sep 17 00:00:00 2001 From: A00474880 Date: Thu, 7 May 2026 19:09:16 -0300 Subject: [PATCH 5/8] updating test --- tests/test_update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_update.py b/tests/test_update.py index ec15224321..0d6beffbb3 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,7 +10,7 @@ class Organization(SQLModel, table=True): headquarters: str class OrganizationUpdate(SQLModel): - name: str + name: str = Field(exclude=True) city: str | None = None org = Organization(name="Example Org", city="New York", headquarters="NYC HQ") @@ -20,11 +20,11 @@ class OrganizationUpdate(SQLModel): update={ "headquarters": "-", # This field is in Organization, but not in OrganizationUpdate }, - exclude_unset=True + exclude_unset=True, ) # fields that should stay the same assert org.city == "New York" - #fields that should be updated + # fields that should be updated assert org.name == "Updated org" assert org.headquarters == "-" # test raise value error when passing in updates other than dict or BaseModel From 66c8b888859de63bfc259bd9c3e7f1d3e4ab6d13 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:17:03 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index b5830e64e7..0e62515c07 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -15,8 +15,8 @@ ClassVar, Literal, TypeAlias, - TypeVar, TypedDict, + TypeVar, Union, cast, get_origin, @@ -50,7 +50,7 @@ from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid -from typing_extensions import deprecated, Unpack +from typing_extensions import Unpack, deprecated from ._compat import ( PYDANTIC_MINOR_VERSION, @@ -801,20 +801,22 @@ def get_column_from_field(field: Any) -> Column: _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") + class _ModelDumpKwargs(TypedDict): - mode: Literal["json", "python"] | str = "python", - include: IncEx | None = None, - exclude: IncEx | None = None, - context: Any | None = None, # v2.7 - by_alias: bool | None = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_computed_fields: bool = False, # v2.12 - round_trip: bool = False, - warnings: bool | Literal["none", "warn", "error"] = True, - fallback: Callable[[Any], Any] | None = None, # v2.11 - serialize_as_any: bool = False, # v2.7 + mode: Literal["json", "python"] | str = ("python",) + include: IncEx | None = (None,) + exclude: IncEx | None = (None,) + context: Any | None = (None,) # v2.7 + by_alias: bool | None = (None,) + exclude_unset: bool = (False,) + exclude_defaults: bool = (False,) + exclude_none: bool = (False,) + exclude_computed_fields: bool = (False,) # v2.12 + round_trip: bool = (False,) + warnings: bool | Literal["none", "warn", "error"] = (True,) + fallback: Callable[[Any], Any] | None = (None,) # v2.11 + serialize_as_any: bool = (False,) # v2.7 + class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values @@ -1013,9 +1015,7 @@ def sqlmodel_update( fname: finfo.annotation for fname, finfo in ObjClass.model_fields.items() } - UpdateModel = create_model( - f"_{ObjClass.__name__}Update_", **fields_def - ) + UpdateModel = create_model(f"_{ObjClass.__name__}Update_", **fields_def) # rebuild obj instance with model_construct obj = UpdateModel.model_construct( _fields_set=obj.model_fields_set, **obj.__dict__ From 8fe99e9380245f6b3c6985b40057c1b1f96faf76 Mon Sep 17 00:00:00 2001 From: A00474880 Date: Thu, 7 May 2026 19:23:16 -0300 Subject: [PATCH 7/8] remove defaults from _ModelDumpKwargs TypedDict --- sqlmodel/main.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index b5830e64e7..fb84fcf687 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -802,19 +802,19 @@ def get_column_from_field(field: Any) -> Column: _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") class _ModelDumpKwargs(TypedDict): - mode: Literal["json", "python"] | str = "python", - include: IncEx | None = None, - exclude: IncEx | None = None, - context: Any | None = None, # v2.7 - by_alias: bool | None = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_computed_fields: bool = False, # v2.12 - round_trip: bool = False, - warnings: bool | Literal["none", "warn", "error"] = True, - fallback: Callable[[Any], Any] | None = None, # v2.11 - serialize_as_any: bool = False, # v2.7 + mode: Literal["json", "python"] | str + include: IncEx | None + exclude: IncEx | None + context: Any | None # v2.7 + by_alias: bool | None + exclude_unset: bool + exclude_defaults: bool + exclude_none: bool + exclude_computed_fields: bool # v2.12 + round_trip: bool + warnings: bool | Literal["none", "warn", "error"] + fallback: Callable[[Any], Any] | None # v2.11 + serialize_as_any: bool # v2.7 class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values From 51ad5c3eb9eadb7519636ae65fe8cda295a242ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:27:11 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index a27f0a1c3a..33023eec74 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -806,16 +806,17 @@ class _ModelDumpKwargs(TypedDict): mode: Literal["json", "python"] | str include: IncEx | None exclude: IncEx | None - context: Any | None # v2.7 + context: Any | None # v2.7 by_alias: bool | None exclude_unset: bool exclude_defaults: bool exclude_none: bool - exclude_computed_fields: bool # v2.12 + exclude_computed_fields: bool # v2.12 round_trip: bool warnings: bool | Literal["none", "warn", "error"] - fallback: Callable[[Any], Any] | None # v2.11 - serialize_as_any: bool # v2.7 + fallback: Callable[[Any], Any] | None # v2.11 + serialize_as_any: bool # v2.7 + class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values