Skip to content

✨ Support sqlalchemy MappedColumn #1143

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

spazm
Copy link

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

spazm commented Oct 29, 2024

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?

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

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.

5 participants