sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

✨ Support sqlalchemy `MappedColumn`

Open spazm opened this issue 1 year ago • 6 comments

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))

spazm avatar Oct 21 '24 23:10 spazm

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?

spazm avatar Oct 29 '24 03:10 spazm

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)

lachaib avatar Feb 17 '25 12:02 lachaib

When this PR will be merged?

tsuga avatar Jul 17 '25 19:07 tsuga

@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:
    ...

tsuga avatar Jul 21 '25 13:07 tsuga

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.

spazm avatar Oct 08 '25 00:10 spazm

I think we should parameterize tests in test_field_sa_column.py to be run with column_factory instead of duplicating them.

The parameterized tests are lovely, a really clean pattern. Thank you for implementing.

spazm avatar Oct 08 '25 00:10 spazm