From 35c5b40e2a0a5c806be15f7d71591dfcd44add96 Mon Sep 17 00:00:00 2001 From: nsaccente Date: Wed, 23 Jul 2025 21:56:53 -0400 Subject: [PATCH 1/8] Add check for Literal type annotation in get_sqlalchemy_type to return an AutoString --- sqlmodel/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 38c85915aa..404d1efd0d 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -655,6 +655,9 @@ def get_sqlalchemy_type(field: Any) -> Any: type_ = get_sa_type_from_field(field) metadata = get_field_metadata(field) + # Checks for `Literal` type annotation + if type_ is Literal: + return AutoString # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI if issubclass(type_, Enum): return sa_Enum(type_) From e562654a23be28f13d4ca2a820f267431073c937 Mon Sep 17 00:00:00 2001 From: nsaccente Date: Thu, 7 Aug 2025 17:58:19 -0400 Subject: [PATCH 2/8] Add unit test for Literal parsing --- tests/test_main.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 60d5c40ebb..0155a06c64 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Literal import pytest from sqlalchemy.exc import IntegrityError @@ -125,3 +125,26 @@ class Hero(SQLModel, table=True): # The next statement should not raise an AttributeError assert hero_rusty_man.team assert hero_rusty_man.team.name == "Preventers" + + +def test_literal_typehints_are_treated_as_strings(clear_sqlmodel): + """Test https://github.com/fastapi/sqlmodel/issues/57""" + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(unique=True) + weakness: Literal["Kryptonite", "Dehydration", "Munchies"] + + + superman = Hero(name="Superman", weakness="Kryptonite") + + engine = create_engine("sqlite://", echo=True) + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(superman) + session.commit() + session.refresh(superman) + assert superman.weakness == "Kryptonite" + assert isinstance(superman.weakness, str) From fcabf3f4204c8c861b14da50d6ad31ca78f96622 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 21:58:31 +0000 Subject: [PATCH 3/8] =?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_main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 0155a06c64..297002de3c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Literal +from typing import List, Literal, Optional import pytest from sqlalchemy.exc import IntegrityError @@ -135,7 +135,6 @@ class Hero(SQLModel, table=True): name: str = Field(unique=True) weakness: Literal["Kryptonite", "Dehydration", "Munchies"] - superman = Hero(name="Superman", weakness="Kryptonite") engine = create_engine("sqlite://", echo=True) From 859a4af460230179e4d112f769efad116d20eb04 Mon Sep 17 00:00:00 2001 From: nsaccente Date: Thu, 7 Aug 2025 19:11:57 -0400 Subject: [PATCH 4/8] Testing git test runner with commented out code --- sqlmodel/_compat.py | 12 ++++++++---- tests/test_main.py | 10 +++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 38dd501c4a..9103f8e2cd 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -10,6 +10,7 @@ Dict, ForwardRef, Generator, + Literal, Mapping, Optional, Set, @@ -22,6 +23,7 @@ from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Annotated, get_args, get_origin +from .sql.sqltypes import AutoString # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION @@ -458,10 +460,12 @@ def is_field_noneable(field: "FieldInfo") -> bool: ) return field.allow_none # type: ignore[no-any-return, attr-defined] - def get_sa_type_from_field(field: Any) -> Any: - if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: - return field.type_ - raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") + # def get_sa_type_from_field(field: Any) -> Any: + # if field is Literal: + # return AutoString + # elif isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: + # return field.type_ + # raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") def get_field_metadata(field: Any) -> Any: metadata = FakeMetadata() diff --git a/tests/test_main.py b/tests/test_main.py index 297002de3c..5416bfc666 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -135,15 +135,15 @@ class Hero(SQLModel, table=True): name: str = Field(unique=True) weakness: Literal["Kryptonite", "Dehydration", "Munchies"] - superman = Hero(name="Superman", weakness="Kryptonite") + superguy = Hero(name="Superguy", weakness="Kryptonite") engine = create_engine("sqlite://", echo=True) SQLModel.metadata.create_all(engine) with Session(engine) as session: - session.add(superman) + session.add(superguy) session.commit() - session.refresh(superman) - assert superman.weakness == "Kryptonite" - assert isinstance(superman.weakness, str) + session.refresh(superguy) + assert superguy.weakness == "Kryptonite" + assert isinstance(superguy.weakness, str) From e193bcdc2369245ae39b9577b218c67bddd94d2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 23:12:08 +0000 Subject: [PATCH 5/8] =?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 --- sqlmodel/_compat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 9103f8e2cd..8d511020b3 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -10,7 +10,6 @@ Dict, ForwardRef, Generator, - Literal, Mapping, Optional, Set, @@ -23,7 +22,6 @@ from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Annotated, get_args, get_origin -from .sql.sqltypes import AutoString # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION From cdc863dc48a197626138fa4de04e1c7554be9171 Mon Sep 17 00:00:00 2001 From: nsaccente Date: Thu, 7 Aug 2025 19:18:23 -0400 Subject: [PATCH 6/8] Test literal patch in _compat --- sqlmodel/_compat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 9103f8e2cd..f2473aa626 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -460,12 +460,12 @@ def is_field_noneable(field: "FieldInfo") -> bool: ) return field.allow_none # type: ignore[no-any-return, attr-defined] - # def get_sa_type_from_field(field: Any) -> Any: - # if field is Literal: - # return AutoString - # elif isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: - # return field.type_ - # raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") + def get_sa_type_from_field(field: Any) -> Any: + if get_origin(field.type_) is Literal: + return AutoString + elif isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: + return field.type_ + raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") def get_field_metadata(field: Any) -> Any: metadata = FakeMetadata() From 8e149c83016b4fd4fd8651e7e3d2534e953ff7dd Mon Sep 17 00:00:00 2001 From: nsaccente Date: Thu, 7 Aug 2025 19:21:14 -0400 Subject: [PATCH 7/8] Test patch for literal --- sqlmodel/_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 7f8669cac7..f2473aa626 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -10,6 +10,7 @@ Dict, ForwardRef, Generator, + Literal, Mapping, Optional, Set, @@ -22,6 +23,7 @@ from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Annotated, get_args, get_origin +from .sql.sqltypes import AutoString # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION From 2aaef2d12945fa934be37a572d8f360c2d2b70ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 23:21:54 +0000 Subject: [PATCH 8/8] =?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 --- sqlmodel/_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index f2473aa626..60cb6f4000 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -23,6 +23,7 @@ from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import Annotated, get_args, get_origin + from .sql.sqltypes import AutoString # Reassign variable to make it reexported for mypy