From 0e281d5fd36194d37fec4c9be7df8d5a8daa845a Mon Sep 17 00:00:00 2001 From: Viraj Kanwade Date: Sat, 13 Apr 2024 21:42:00 -0700 Subject: [PATCH 1/7] add: support mapped_column with sa_column --- sqlmodel/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 3532e81a8e..cb2df8fb29 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -42,6 +42,7 @@ from sqlalchemy import Enum as sa_Enum from sqlalchemy.orm import ( Mapped, + MappedColumn, RelationshipProperty, declared_attr, registry, @@ -702,13 +703,13 @@ def get_sqlalchemy_type(field: Any) -> Any: raise ValueError(f"{type_} has no matching SQLAlchemy type") -def get_column_from_field(field: Any) -> Column: # type: ignore +def get_column_from_field(field: Any) -> Column | MappedColumn: # type: ignore if IS_PYDANTIC_V2: field_info = field else: field_info = field.field_info sa_column = getattr(field_info, "sa_column", Undefined) - if isinstance(sa_column, Column): + if isinstance(sa_column, Column) or isinstance(sa_column, MappedColumn): return sa_column sa_type = get_sqlalchemy_type(field) primary_key = getattr(field_info, "primary_key", Undefined) From 63abd0a2fda7f4f7cf54ad969eacff3a94190068 Mon Sep 17 00:00:00 2001 From: Viraj Kanwade Date: Sat, 13 Apr 2024 21:46:22 -0700 Subject: [PATCH 2/7] add: support mapped_column with sa_column --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index cb2df8fb29..09717a70ed 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -703,7 +703,7 @@ def get_sqlalchemy_type(field: Any) -> Any: raise ValueError(f"{type_} has no matching SQLAlchemy type") -def get_column_from_field(field: Any) -> Column | MappedColumn: # type: ignore +def get_column_from_field(field: Any) -> Union[Column, MappedColumn]: # type: ignore if IS_PYDANTIC_V2: field_info = field else: From 3fcbffd35e2c8cba2eb8287cf73660a2aac2f436 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Mon, 21 Oct 2024 16:22:04 -0700 Subject: [PATCH 3/7] Test MappedColumn support for sa_column field --- tests/test_field_sa_column_mapped_column.py | 122 ++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/test_field_sa_column_mapped_column.py diff --git a/tests/test_field_sa_column_mapped_column.py b/tests/test_field_sa_column_mapped_column.py new file mode 100644 index 0000000000..1bd00580e1 --- /dev/null +++ b/tests/test_field_sa_column_mapped_column.py @@ -0,0 +1,122 @@ +from typing import Optional + +import pytest +from sqlalchemy import Integer, String +from sqlalchemy.orm import mapped_column +from sqlmodel import Field, SQLModel + + +def test_sa_column_takes_precedence() -> None: + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column=mapped_column(String, primary_key=True, nullable=False), + ) + + # It would have been nullable with no sa_column + assert Item.id.nullable is False # type: ignore + assert isinstance(Item.id.type, String) # type: ignore + + +def test_sa_column_no_sa_args() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column_args=[Integer], + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_sa_kargs() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column_kwargs={"primary_key": True}, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_type() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_type=Integer, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_primary_key() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + primary_key=True, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_nullable() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + nullable=True, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_foreign_key() -> None: + with pytest.raises(RuntimeError): + + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + team_id: Optional[int] = Field( + default=None, + foreign_key="team.id", + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_unique() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + unique=True, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_index() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + index=True, + sa_column=mapped_column(Integer, primary_key=True), + ) + + +def test_sa_column_no_ondelete() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column=mapped_column(Integer, primary_key=True), + ondelete="CASCADE", + ) From 2853efedc319750fbf61d4cff4f494393dc1de64 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Mon, 21 Oct 2024 16:42:29 -0700 Subject: [PATCH 4/7] tests: use fixture to clear sqlmodel between tests These tests share the same structure and field names, so they conflict if not cleaned up. --- tests/test_field_sa_column.py | 2 +- tests/test_field_sa_column_mapped_column.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index e2ccc6d7ef..8e4f0a4c33 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -5,7 +5,7 @@ from sqlmodel import Field, SQLModel -def test_sa_column_takes_precedence() -> None: +def test_sa_column_takes_precedence(clear_sqlmodel) -> None: class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, diff --git a/tests/test_field_sa_column_mapped_column.py b/tests/test_field_sa_column_mapped_column.py index 1bd00580e1..ad4f22e053 100644 --- a/tests/test_field_sa_column_mapped_column.py +++ b/tests/test_field_sa_column_mapped_column.py @@ -6,7 +6,7 @@ from sqlmodel import Field, SQLModel -def test_sa_column_takes_precedence() -> None: +def test_sa_column_takes_precedence(clear_sqlmodel) -> None: class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, From 1f0351b475abfa1b523bb9ad5b28f76744148e7b Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Mon, 28 Oct 2024 20:28:49 -0700 Subject: [PATCH 5/7] Simplify type checking of sa_column isinstance directly supports using a tuple of allowed types. --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 09717a70ed..ae78cc9f2b 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -709,7 +709,7 @@ def get_column_from_field(field: Any) -> Union[Column, MappedColumn]: # type: i else: field_info = field.field_info sa_column = getattr(field_info, "sa_column", Undefined) - if isinstance(sa_column, Column) or isinstance(sa_column, MappedColumn): + if isinstance(sa_column, (Column, MappedColumn)): return sa_column sa_type = get_sqlalchemy_type(field) primary_key = getattr(field_info, "primary_key", Undefined) From 91da939783732f5cf297993ffc09555201792ef4 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 7 Oct 2025 23:12:20 +0200 Subject: [PATCH 6/7] Reduced code duplication in tests using parametrization --- tests/test_field_sa_column.py | 53 +++++---- tests/test_field_sa_column_mapped_column.py | 122 -------------------- 2 files changed, 32 insertions(+), 143 deletions(-) delete mode 100644 tests/test_field_sa_column_mapped_column.py diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index 8e4f0a4c33..5e6cc81e75 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -1,15 +1,17 @@ -from typing import Optional +from typing import Optional, Union import pytest from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import mapped_column from sqlmodel import Field, SQLModel -def test_sa_column_takes_precedence(clear_sqlmodel) -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_takes_precedence(clear_sqlmodel, column_class) -> None: class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, - sa_column=Column(String, primary_key=True, nullable=False), + sa_column=column_class(String, primary_key=True, nullable=False), ) # It would have been nullable with no sa_column @@ -17,62 +19,68 @@ class Item(SQLModel, table=True): assert isinstance(Item.id.type, String) # type: ignore -def test_sa_column_no_sa_args() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_sa_args(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, sa_column_args=[Integer], - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_sa_kargs() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_sa_kargs(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, sa_column_kwargs={"primary_key": True}, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_type() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_type(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, sa_type=Integer, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_primary_key() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_primary_key(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, primary_key=True, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_nullable() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_nullable(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, nullable=True, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_foreign_key() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_foreign_key(clear_sqlmodel, column_class) -> None: with pytest.raises(RuntimeError): class Team(SQLModel, table=True): @@ -84,38 +92,41 @@ class Hero(SQLModel, table=True): team_id: Optional[int] = Field( default=None, foreign_key="team.id", - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_unique() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_unique(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, unique=True, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_index() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_index(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, index=True, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ) -def test_sa_column_no_ondelete() -> None: +@pytest.mark.parametrize("column_class", [Column, mapped_column]) +def test_sa_column_no_ondelete(column_class) -> None: with pytest.raises(RuntimeError): class Item(SQLModel, table=True): id: Optional[int] = Field( default=None, - sa_column=Column(Integer, primary_key=True), + sa_column=column_class(Integer, primary_key=True), ondelete="CASCADE", ) diff --git a/tests/test_field_sa_column_mapped_column.py b/tests/test_field_sa_column_mapped_column.py deleted file mode 100644 index ad4f22e053..0000000000 --- a/tests/test_field_sa_column_mapped_column.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import Optional - -import pytest -from sqlalchemy import Integer, String -from sqlalchemy.orm import mapped_column -from sqlmodel import Field, SQLModel - - -def test_sa_column_takes_precedence(clear_sqlmodel) -> None: - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - sa_column=mapped_column(String, primary_key=True, nullable=False), - ) - - # It would have been nullable with no sa_column - assert Item.id.nullable is False # type: ignore - assert isinstance(Item.id.type, String) # type: ignore - - -def test_sa_column_no_sa_args() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - sa_column_args=[Integer], - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_sa_kargs() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - sa_column_kwargs={"primary_key": True}, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_type() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - sa_type=Integer, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_primary_key() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - primary_key=True, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_nullable() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - nullable=True, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_foreign_key() -> None: - with pytest.raises(RuntimeError): - - class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - - class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - team_id: Optional[int] = Field( - default=None, - foreign_key="team.id", - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_unique() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - unique=True, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_index() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - index=True, - sa_column=mapped_column(Integer, primary_key=True), - ) - - -def test_sa_column_no_ondelete() -> None: - with pytest.raises(RuntimeError): - - class Item(SQLModel, table=True): - id: Optional[int] = Field( - default=None, - sa_column=mapped_column(Integer, primary_key=True), - ondelete="CASCADE", - ) From 1b3ecc843731ea2b222c4906dee9f0997bfc9254 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:13:04 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_column.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index 5e6cc81e75..3ee5f50b9d 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional import pytest from sqlalchemy import Column, Integer, String