✨ Support sqlalchemy `MappedColumn`
Reopening PR #896.
id: Optional[int] = Field(default=None, sa_column=mapped_column('id', Integer, primary_key=True))
This PR adds a test with sqlmodel.orm.mapped_column working for sa_column. This test fails on current main.
from typing import Optional
from sqlalchemy import Integer
from sqlalchemy.orm import mapped_column
from sqlmodel import Field
class Foo(SqlModel):
id: Optional[int] = Field(default=None, sa_column=mapped_column('id', Integer, primary_key=True))
This PR is failing one test because it does not have a label assigned.
How do I get a label assigned?
Do I need to open a separate issue to point to the PR?
Hello,
While trying to fix a problem probably similar to yours (I want to make a column deferrable but it can only be made on mapped_column and not on Column), I tested a simpler solution:
at the end of get_column_from_field, I returned mapped_column instead of Column, hence the deferred keyword can be passed as sa_column_kwargs
According to tests, this is fully compatible. Also it's slightly more readable:
not_loaded: str = Field(sa_column_kwargs={"deferred": True})
# vs
not_loaded: str = Field(sa_type=mapped_column(String, deferred=True))
Maybe there's an underlying reason why this cannot be acceptable but I think it's worth sharing (and also will help me follow this pull request)
When this PR will be merged?
@spazm Thanks for this PR. This saved a day for me.
Meanwhile, I noticed that you indicated that foreign_key would not work in the test.
However, I have a requirement to use foreign key with mapped_column, and looks like that I found the solution.
You may refer to the new test code below (test_sa_column_foreign_key_in_mapped_column_int and test_sa_column_foreign_key_in_mapped_column_custom_type added)
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_foreign_key_in_mapped_column_int() -> None:
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,
sa_column=mapped_column(Integer, ForeignKey("team.id")),
)
def test_sa_column_foreign_key_in_mapped_column_custom_type() -> None:
from sqlmodel import String, TypeDecorator
class CustomType:
def __init__(self, value: str):
self.value = value
@classmethod
def from_str(cls, value: str) -> "CustomType":
return cls(value)
def __str__(self) -> str:
return self.value
class CustomTypeDecorator(TypeDecorator):
impl = String() # CustomType
cache_ok = True
def process_bind_param(self, value: CustomType, dialect):
if value is not None:
return str(value)
return None
def process_result_value(self, value, dialect):
if value is not None:
return CustomType.from_str(value)
return None
@property
def python_type(self):
return CustomType
class Team(SQLModel, table=True):
id: Optional[CustomType] = Field(
sa_type=CustomTypeDecorator, default=None, primary_key=True
)
name: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
team_id: Optional[CustomType] = Field(
default=None,
sa_column=mapped_column(CustomTypeDecorator, ForeignKey("team.id")),
)
def test_sa_column_no_unique() -> None:
...
Wonderful to see interest and action on this ticket! Yay!
In my own use case (which has kept me too busy to follow this ticket) I made further changes to allow tagged custom types as used in v2 declarative SqlAlchemy. This was a slightly more invasive change. Would this make sense to add here or as a related ticket?
I'm on my phone and away from this code for some time, so apologies if I'm mangling the concept.
I think we should parameterize tests in
test_field_sa_column.pyto be run withcolumn_factoryinstead of duplicating them.
The parameterized tests are lovely, a really clean pattern. Thank you for implementing.