Skip to content

Conversation

@spazm
Copy link
Contributor

@spazm spazm commented Oct 21, 2024

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 spazm changed the title Support MappedColumn for sa_column add: Support MappedColumn for sa_column Oct 29, 2024
@spazm spazm changed the title add: Support MappedColumn for sa_column add: Support sqlalchemy MappedColumn for sa_column Oct 29, 2024
@spazm

This comment was marked as resolved.

isinstance directly supports using a tuple of allowed types.
@lachaib
Copy link

lachaib commented Feb 17, 2025

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)

@svlandeg svlandeg added the feature New feature or request label Feb 20, 2025
@svlandeg svlandeg changed the title add: Support sqlalchemy MappedColumn for sa_column ✨ Support sqlalchemy MappedColumn Feb 20, 2025
@tsuga
Copy link

tsuga commented Jul 17, 2025

When this PR will be merged?

@tsuga
Copy link

tsuga commented Jul 21, 2025

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

YuriiMotov

This comment was marked as resolved.

Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@spazm
Copy link
Contributor Author

spazm commented Oct 8, 2025

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
Copy link
Contributor Author

spazm commented Oct 8, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants