-
-
Notifications
You must be signed in to change notification settings - Fork 782
✨ Automatically map dictionaries to JSON #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| from typing import Dict, Optional | ||
|
|
||
| from sqlmodel import Field, Session, SQLModel, create_engine | ||
| from typing_extensions import TypedDict | ||
|
|
||
| from .conftest import needs_pydanticv2 | ||
|
|
||
| pytestmark = needs_pydanticv2 | ||
|
|
||
|
|
||
| def test_dict_maps_to_json(clear_sqlmodel): | ||
| class Resource(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| name: str | ||
| data: dict | ||
|
|
||
| engine = create_engine("sqlite://") | ||
| SQLModel.metadata.create_all(engine) | ||
|
|
||
| resource = Resource(name="test", data={"key": "value", "num": 42}) | ||
|
|
||
| with Session(engine) as session: | ||
| session.add(resource) | ||
| session.commit() | ||
| session.refresh(resource) | ||
|
|
||
| assert resource.data["key"] == "value" | ||
| assert resource.data["num"] == 42 | ||
|
|
||
|
|
||
| def test_typing_dict_maps_to_json(clear_sqlmodel): | ||
| """Test if typing.Dict type annotation works without explicit sa_type""" | ||
|
|
||
| class Resource(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| name: str | ||
| data: Dict[str, int] | ||
|
|
||
| engine = create_engine("sqlite://") | ||
| SQLModel.metadata.create_all(engine) | ||
|
|
||
| resource = Resource(name="test", data={"count": 100}) | ||
|
|
||
| with Session(engine) as session: | ||
| session.add(resource) | ||
| session.commit() | ||
| session.refresh(resource) | ||
|
|
||
| assert resource.data["count"] == 100 | ||
|
|
||
|
|
||
| class Metadata(TypedDict): | ||
| name: str | ||
| email: str | ||
|
|
||
|
|
||
| def test_typeddict_automatic_json_mapping(clear_sqlmodel): | ||
| """ | ||
| Test that TypedDict fields automatically map to JSON type. | ||
| This fixes the original error: | ||
| ValueError: <class 'app.models.NeonMetadata'> has no matching SQLAlchemy type | ||
| """ | ||
|
|
||
| class ConnectedResource(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| name: str | ||
| neon_metadata: Metadata | ||
|
|
||
| engine = create_engine("sqlite://") | ||
| SQLModel.metadata.create_all(engine) | ||
|
|
||
| resource = ConnectedResource( | ||
| name="my-resource", | ||
| neon_metadata={"name": "John Doe", "email": "[email protected]"}, | ||
| ) | ||
|
|
||
| with Session(engine) as session: | ||
| session.add(resource) | ||
| session.commit() | ||
| session.refresh(resource) | ||
|
|
||
| assert resource.neon_metadata["name"] == "John Doe" | ||
| assert resource.neon_metadata["email"] == "[email protected]" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| from typing import Any, Dict, List, Optional, Union | ||
| from typing import List, Optional, Union | ||
|
|
||
| import pytest | ||
| from sqlmodel import Field, SQLModel | ||
|
|
@@ -12,14 +12,6 @@ class Hero(SQLModel, table=True): | |
| tags: List[str] | ||
|
|
||
|
|
||
| def test_type_dict_breaks() -> None: | ||
| with pytest.raises(ValueError): | ||
|
|
||
| class Hero(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| tags: Dict[str, Any] | ||
|
|
||
|
|
||
|
Comment on lines
-15
to
-22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will still fail with Pydantic V1. |
||
| def test_type_union_breaks() -> None: | ||
| with pytest.raises(ValueError): | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| """Test that Python type annotations map to correct SQLAlchemy column types""" | ||
|
|
||
| from typing import Dict, Optional | ||
|
|
||
| from sqlalchemy import JSON, Boolean, Date, DateTime, Float, Integer | ||
| from sqlalchemy.sql.sqltypes import LargeBinary | ||
| from sqlmodel import Field, SQLModel | ||
| from sqlmodel.sql.sqltypes import AutoString | ||
| from typing_extensions import TypedDict | ||
|
|
||
| from .conftest import needs_pydanticv2 | ||
|
|
||
|
|
||
| def test_type_mappings(clear_sqlmodel): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand, the idea of this test module is to test all type mapping in one module.
|
||
| from datetime import date, datetime | ||
|
|
||
| class Model(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| name: str | ||
| count: int | ||
| price: float | ||
| active: bool | ||
| created: datetime | ||
| birth_date: date | ||
| data_bytes: bytes | ||
|
|
||
| # Verify the type mappings | ||
| assert isinstance(Model.name.type, AutoString) # type: ignore | ||
| assert isinstance(Model.count.type, Integer) # type: ignore | ||
| assert isinstance(Model.price.type, Float) # type: ignore | ||
| assert isinstance(Model.active.type, Boolean) # type: ignore | ||
| assert isinstance(Model.created.type, DateTime) # type: ignore | ||
| assert isinstance(Model.birth_date.type, Date) # type: ignore | ||
| assert isinstance(Model.data_bytes.type, LargeBinary) # type: ignore | ||
|
|
||
|
|
||
| @needs_pydanticv2 | ||
| def test_dict_maps_to_json(clear_sqlmodel): | ||
| """Test that plain dict annotation maps to JSON column type""" | ||
|
|
||
| class Model(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| data: dict | ||
|
|
||
| assert isinstance(Model.data.type, JSON) # type: ignore | ||
|
|
||
|
|
||
| @needs_pydanticv2 | ||
| def test_typing_dict_maps_to_json(clear_sqlmodel): | ||
| class Model(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| data: Dict[str, int] | ||
|
|
||
| assert isinstance(Model.data.type, JSON) # type: ignore | ||
|
|
||
|
|
||
| @needs_pydanticv2 | ||
| def test_typeddict_maps_to_json(clear_sqlmodel): | ||
| class MyDict(TypedDict): | ||
| name: str | ||
| count: int | ||
|
|
||
| class Model(SQLModel, table=True): | ||
| id: Optional[int] = Field(default=None, primary_key=True) | ||
| data: MyDict | ||
|
|
||
| assert isinstance(Model.data.type, JSON) # type: ignore | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need this part here