Giter VIP home page Giter VIP logo

strawberry-sqlalchemy's Introduction

strawberry-sqlalchemy-mapper

Strawberry-sqlalchemy-mapper is the simplest way to implement autogenerated strawberry types for columns and relationships in SQLAlchemy models.

  • Instead of manually listing every column and relationship in a SQLAlchemy model, strawberry-sqlalchemy-mapper lets you decorate a class declaration and it will automatically generate the necessary strawberry fields for all columns and relationships (subject to the limitations below) in the given model.

  • Native support for most of SQLAlchemy's most common types.

  • Extensible to arbitrary custom SQLAlchemy types.

  • Automatic batching of queries, avoiding N+1 queries when getting relationships

  • Support for SQLAlchemy >=1.4.x

  • Lightweight and fast.

Getting Started

strawberry-sqlalchemy-mapper is available on PyPi

pip install strawberry-sqlalchemy-mapper

First, define your sqlalchemy model:

# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Employee(Base):
    __tablename__ = "employee"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    password_hash = Column(String, nullable=False)
    department_id = Column(UUID, ForeignKey("department.id"))
    department = relationship("Department", back_populates="employees")


class Department(Base):
    __tablename__ = "department"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    employees = relationship("Employee", back_populates="department")

Next, decorate a type with strawberry_sqlalchemy_mapper.type() to register it as a strawberry type for the given SQLAlchemy model. This will automatically add fields for the model's columns, relationships, association proxies, and hybrid properties. For example:

# elsewhere
# ...
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper

strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()


@strawberry_sqlalchemy_mapper.type(models.Employee)
class Employee:
    __exclude__ = ["password_hash"]


@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
    pass


@strawberry.type
class Query:
    @strawberry.field
    def departments(self):
        return db.session.scalars(select(models.Department)).all()


# context is expected to have an instance of StrawberrySQLAlchemyLoader
class CustomGraphQLView(GraphQLView):
    def get_context(self):
        return {
            "sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=YOUR_SESSION),
        }


# call finalize() before using the schema:
# (note that models that are related to models that are in the schema
# are automatically mapped at this stage -- e.g., Department is mapped
# because employee.department is a relationshp to Department)
strawberry_sqlalchemy_mapper.finalize()
# only needed if you have polymorphic types
additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    extensions=extensions,
    types=additional_types,
)

# You can now query, e.g.:
"""
query {
    departments {
        id
        name
        employees {
            edge {
                node {
                    id
                    name
                    department {
                        # Just an example of nested relationships
                        id
                        name
                    }
                }
            }
        }
    }
}
"""

Limitations

SQLAlchemy Models -> Strawberry Types and Interfaces are expected to have a consistent (customizable) naming convention. These can be configured by passing model_to_type_name and model_to_interface_name when constructing the mapper.

Natively supports the following SQLAlchemy types:

Integer: int,
Float: float,
BigInteger: BigInt,
Numeric: Decimal,
DateTime: datetime,
Date: date,
Time: time,
String: str,
Text: str,
Boolean: bool,
Unicode: str,
UnicodeText: str,
SmallInteger: int,
SQLAlchemyUUID: uuid.UUID,
VARCHAR: str,
ARRAY[T]: List[T] # PostgreSQL array
JSON: JSON # SQLAlchemy JSON
Enum: (the Python enum it is mapped to, which should be @strawberry.enum-decorated)

Additional types can be supported by passing extra_sqlalchemy_type_to_strawberry_type_map, although support for TypeDecorator types is untested.

Association proxies are expected to be of the form association_proxy('relationship1', 'relationship2'), i.e., both properties are expected to be relationships.

Roots of polymorphic hierarchies are supported, but are also expected to be registered via strawberry_sqlalchemy_mapper.interface(), and its concrete type and its descendants are expected to inherit from the interface:

class Book(Model):
    id = Column(UUID, primary_key=True)


class Novel(Book):
    pass


class ShortStory(Book):
    pass


# in another file
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()


@strawberry_sqlalchemy_mapper.interface(models.Book)
class BookInterface:
    pass


@strawberry_sqlalchemy_mapper.type(models.Book)
class Book:
    pass


@strawberry_sqlalchemy_mapper.type(models.Novel)
class Novel:
    pass


@strawberry_sqlalchemy_mapper.type(models.ShortStory)
class ShortStory:
    pass

Contributing

We encourage you to contribute to strawberry-sqlalchemy-mapper! Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. Don't forget to give the project a star! Thanks again!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature)
  3. Commit your Changes (git commit -m 'Add some feature')
  4. Push to the Branch (git push origin feature)
  5. Open a Pull Request

Prerequisites

This project uses pre-commit_, please make sure to install it before making any changes::

pip install pre-commit
cd strawberry-sqlalchemy-mapper
pre-commit install

It is a good idea to update the hooks to the latest version::

pre-commit autoupdate

Don't forget to tell your contributors to also install and use pre-commit.

Installation

pip install -r requirements.txt

Install PostgreSQL 14+

Test

pytest

⚖️ LICENSE

MIT © strawberry-sqlalchemy-mapper

strawberry-sqlalchemy's People

Contributors

asimov-layton avatar bellini666 avatar botberry avatar cesardddp avatar ckk3 avatar dependabot[bot] avatar dsully avatar erikwrede avatar gravy-jones-locker avatar healyoudown avatar idokendo avatar mattalbr avatar raguiar2 avatar timdumol avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

strawberry-sqlalchemy's Issues

Is it possible to instantiate the mapped strawberry classes?

I'm struggling with some complicated resolvers for the fact that, from my understanding, the whole execution flow works without ever instantiating the strawberry types, just leveraging the fact that the fields are static methods and strawberry will take care of the execution for us.

I'd like to be more rigorous about typing and convert the return sqlalchemy models to the strawberry types before resolvers return them. Right now, the generated init seems to take every single field, and they're all positional arguments, making it hugely inconvenient. Is there a "copy constructor" of sorts to convert from ORM to Strawberry? If not, would it be hard to make one?

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

What is the rational for wrapping relationships into a Connection?

I'll preface this with saying I am not an expert at GraphQL, so might be missing an obvious point.

Through my testing, it seems like wrapping of relationships adds 2 levels of unnecessary hierarchy in the schema. The code references "to support future pagination" but I am not privy to how this wrapping will make pagination possible and why it is not without it.

As an example, I will steal the query from the README:

With wrapping

query {
    departments {
        id
        name
        employees {
            edge {        # First added level of hierarchy
                node {    # Second
                      # ...
                    }
                }
            }
        }
    }
}

Without wrapping

query {
    departments {
        id
        name
        employees {
            # ...
        }
    }
}

My Idea

I spent a few minutes with the code and was able to remove wrapping by adding an attribute to the StrawberrySQLAlchemyMapper class:

    def __init__(
        self,
        paginate_relationships: Optional[bool] = True,
        model_to_type_name: Optional[Callable[[Type[BaseModelType]], str]] = None,
        model_to_interface_name: Optional[Callable[[Type[BaseModelType]], str]] = None,
        extra_sqlalchemy_type_to_strawberry_type_map: Optional[
            Mapping[Type[TypeEngine], Type[Any]]
        ] = None,
    ) -> None:
        self.paginate_relationships = paginate_relationships
        # ...

Then adding a few conditionals:

    def _convert_relationship_to_strawberry_type(
        self, relationship: RelationshipProperty
    ) -> Union[Type[Any], ForwardRef]:
        # ...
        if relationship.uselist:
            if self.paginate_relationships:
                return self._connection_type_for(type_name)
            else:
                return List[ForwardRef(type_name)]
        # ...
    def connection_resolver_for(
        self, relationship: RelationshipProperty
    ) -> Callable[..., Awaitable[Any]]:
        # ...
        if self.paginate_relationships and relationship.uselist:
            return self.make_connection_wrapper_resolver(
                relationship_resolver,
                self.model_to_type_or_interface_name(relationship.entity.entity),
            )
        else:
            return relationship_resolver

This maintains the current behavior as the default.

I do not expect this solution to be complete or even valid, but more of a proof of concept. Mostly, I am interested in learning the rationale behind the wrapping of related models.

Thanks!

Mapping fails after a session.commit(), Instance not bound to session

Describe the Bug

After a session.commit(), SQLAlchemy entities can't be turned into GraphQL object because of access on outside session objects.
To explain the bug, consider this mutation:

@strawberry.type
class MutationSoftware:

    @strawberry.mutation
    def upsert_software(self, info, id: str, full_name: str, editor: str, description: str,
                        language: str) -> OBugsError | SoftwareGQL:

        with info.context['session_factory']() as session:
            db_software = session.query(Software).where(Software.id == id).one_or_none()
            if db_software is None:
                db_software = Software(id=id)
                session.add(db_software)
            db_software.full_name = full_name
            db_software.editor = editor
            db_software.description = description
            db_software.language = language
            session.commit()
            return db_software
            # return session.query(Software).where(Software.id == id).one_or_none() (1)

This code gives me this error (doesn't matter if the object is added or just updated, both cases raise the error)

Instance <Software at 0x22cdfd99410> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)

GraphQL request:22:3
21 | fragment SoftwareFragment on Software {
22 |   id
   |   ^
23 |   fullName
Traceback (most recent call last):
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\graphql\execution\execute.py", line 521, in execute_field
    result = resolve_fn(source, info, **args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema_converter.py", line 541, in _get_basic_result
    return field.get_result(_source, info=None, args=[], kwargs={})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\field.py", line 212, in get_result
    return self.default_resolver(source, self.python_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 566, in __get__
    return self.impl.get(state, dict_)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1086, in get
    value = self._fire_loader_callables(state, key, passive)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1116, in _fire_loader_callables
    return state._load_expired(state, passive)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\state.py", line 798, in _load_expired
    self.manager.expired_attribute_loader(self, toload, passive)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\loading.py", line 1558, in load_scalar_attributes
    raise orm_exc.DetachedInstanceError(
sqlalchemy.orm.exc.DetachedInstanceError: Instance <Software at 0x22cdfd99410> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
Stack (most recent call last):
  File "C:\Program Files\Python311\Lib\threading.py", line 995, in _bootstrap
    self._bootstrap_inner()
  File "C:\Program Files\Python311\Lib\threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "C:\Program Files\Python311\Lib\threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 83, in _worker
    work_item.run()
  File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 285, in _run_event_loop
    loop.run_until_complete(coro)
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 640, in run_until_complete
    self.run_forever()
  File "C:\Program Files\Python311\Lib\asyncio\windows_events.py", line 321, in run_forever
    super().run_forever()
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 607, in run_forever
    self._run_once()
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 1919, in _run_once
    handle._run()
  File "C:\Program Files\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 353, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 158, in dispatch_request
    return await self.run(request=request)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 186, in run
    result = await self.execute_operation(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 118, in execute_operation
    return await self.schema.execute(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 256, in execute
    result = await execute(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 156, in execute
    process_errors(result.errors, execution_context)

However, if I use the return line commented with a (1), there is no problem. Am I missing some config on the sessions or the commit?

System Information

  • Operating system: Windows 11
  • Python 3.11.1
PS C:\dev\obugs-backend\src> .\.env\Scripts\pip freeze
alembic==1.12.0
asgiref==3.7.2
blinker==1.6.2
certifi==2023.7.22
charset-normalizer==3.3.0
click==8.1.7
colorama==0.4.6
Flask==3.0.0
Flask-Cors==4.0.0
Flask-JWT-Extended==4.5.3
Flask-Mail==0.9.1
graphql-core==3.2.3
greenlet==3.0.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
Mako==1.2.4
MarkupSafe==2.1.3
psycopg2-binary==2.9.9
PyJWT==2.8.0
python-dateutil==2.8.2
requests==2.31.0
sentinel==1.0.0
six==1.16.0
SQLAlchemy==2.0.21
strawberry-graphql==0.209.5
strawberry-sqlalchemy-mapper==0.3.1
tornado==6.3.3
typing_extensions==4.8.0
urllib3==2.0.6
Werkzeug==3.0.0

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Could not resolve the type of 'node'. Check that the class is accessible from the global module scope.

Could not resolve the type of 'node'. Check that the class is accessible from the global module scope

Description

After adding a relationship to two SQLAlchemy models, the following error from strawberry is produced when the models are being mapped by this package.

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/usr/local/lib/python3.11/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.11/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/usr/local/lib/python3.11/site-packages/uvicorn/server.py", line 59, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "/usr/local/lib/python3.11/site-packages/uvicorn/server.py", line 66, in serve
    config.load()
  File "/usr/local/lib/python3.11/site-packages/uvicorn/config.py", line 471, in load
    self.loaded_app = import_from_string(self.app)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1149, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/usr/app/run_server.py", line 8, in <module>
    from src.graphql.schema import schema
  File "/usr/app/src/graphql/schema.py", line 44, in <module>
    schema = strawberry.federation.Schema(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/federation/schema.py", line 73, in __init__
    super().__init__(
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema.py", line 158, in __init__
    raise error.__cause__ from None
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 808, in fields
    fields = resolve_thunk(self._fields)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
    return thunk() if callable(thunk) else thunk
           ^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 437, in <lambda>
    fields=lambda: self.get_graphql_fields(object_type),
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 326, in get_graphql_fields
    return self._get_thunk_mapping(
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 316, in _get_thunk_mapping
    raise UnresolvedFieldTypeError(type_definition, field)
strawberry.exceptions.unresolved_field_type.UnresolvedFieldTypeError: Could not resolve the type of 'node'. Check that the class is accessible from the global module scope.

Steps to Reproduce

  1. Add relationships to SQLAlchemy models.
class Books(Base):
    '''Book model.
    '''
    __tablename__ = 'books'

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    title = Column(String(256), nullable=False)

    reviews = relationship('Reviews', back_populates='book')

class Reviews(Base):
    '''Review model.
    '''
    __tablename__ = 'reviews'

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    body = Column(String(256), nullable=False)
    book_id = Column(UUID(as_uuid=True), ForeignKey('books.id'), nullable=False)

    book = relationship('Books', back_populates='reviews')
  1. Create GraphQL type for Book.
strawberry_model = StrawberrySQLAlchemyMapper()

@strawberry_model.type(model=Books)
class Book:
    '''Book type.
    '''
    __exclude__ = ['deleted_at']
  1. Finalize the mapper class
strawberry_model.finalize()

Environment

  • Python: 3.11.3
  • sqlalchemy: 2.0.15
  • strawberry-sqlalchemy-mapper: 0.1.1

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Flask, SQLAlchemy, Mapper: Event Loop error when querrying relationships

I have a backend in Flask, with SQLAlchemy models and strawberry for the GraphQL. I have been using Manual GraphQL Types and converting manually the SQLalchemy models to graphQL Types. Now, I am switching to this library to ditch these GQL Types and generate them from SQLAlchemy. It works fine for almost everything. I got a strange gevent error when fetcing relationships data.

Example of my models

class Software(Base):
    __tablename__ = "software"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    full_name: Mapped[str] = mapped_column()
    editor: Mapped[str] = mapped_column()
    description: Mapped[str] = mapped_column()
    language: Mapped[str] = mapped_column()

    tags: Mapped[List["Tag"]] = relationship(back_populates="software", cascade="all, delete-orphan")

class Tag(Base):
    __tablename__ = "tag"

    id: Mapped[uuid.UUID] = mapped_column(UUID(), primary_key=True, default=uuid.uuid4)
    name: Mapped[str] = mapped_column()
    software_id: Mapped[str] = mapped_column(ForeignKey("software.id"), index=True)
    font_color: Mapped[str] = mapped_column()
    background_color: Mapped[str] = mapped_column()

    software: Mapped["Software"] = relationship("Software", foreign_keys=software_id, back_populates="tags")

Gennerate types this way

# ORM MAPPING
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()

@strawberry_sqlalchemy_mapper.type(SoftwareEntity)
class Software:
    pass


@strawberry_sqlalchemy_mapper.type(TagEntity)
class Tag:
    pass

strawberry_sqlalchemy_mapper.finalize()
additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(query=Query, mutation=Mutation, types=additional_types)

And finally I link it that way to flask:

class MyGraphQLView(GraphQLView):
    init_every_request = False
    config = None
    session_factory = None

    def get_context(self, request: Request, response: Response) -> Any:
        return {
            "config": MyGraphQLView.config,
            "session_factory": MyGraphQLView.session_factory,
            "sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=MyGraphQLView.session_factory),
        }

# ...
self.engine = create_engine(self.uri, echo=False)
self.session_factory = sessionmaker(self.engine)
MyGraphQLView.config = self.config
MyGraphQLView.session_factory = self.session_factory
self.app.add_url_rule(
    "/graphql",
    view_func=MyGraphQLView.as_view("graphql_view", schema=schema)
)

The error I'm facing is the following:

Traceback (most recent call last):
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1478, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1458, in wsgi_app
    response = self.handle_exception(e)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1455, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 869, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 867, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 852, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\views.py", line 115, in view
    return current_app.ensure_sync(self.dispatch_request)(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 100, in dispatch_request
    return self.run(request=request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 199, in run
    result = self.execute_operation(
             ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 128, in execute_operation
    return self.schema.execute_sync(
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 288, in execute_sync
    result = execute_sync(
             ^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 236, in execute_sync
    ensure_future(result).cancel()
  File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 649, in ensure_future
    return _ensure_future(coro_or_future, loop=loop)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 668, in _ensure_future
    loop = events._get_event_loop(stacklevel=4)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\asyncio\events.py", line 692, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Thread-5 (process_request_thread)'.

when running the following request in graphql

{
  softwares{
    id
    fullName
    editor
    description
    language
    tags {
      __typename
      edges{
        node{
          id
        }
      }
    }
  }
}

If I remove the tag section in the querry, I get no error, so I guess I have an issue with relationships. From the error, I figuered it's because fetching relationship is done as an async task. However, because Flask is usin strawberry as a View, I guess It is a thread spawned for every request, but it has no event loop running on it. I figured that I would need to run a asyncio.get_event_loop() to spawn one in this new thread, but I'm not sure where to do it.
If you guys have a better idea how to do it, or if I should just dump Flask for something not view based ?

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Generate description out of sqlalchemy doc

Would be nice if docs from sqlalchemy fields form schema description

class Project(Base):
    __tablename__ = "projects"
    address = Column(String, nullable=False, doc="This is an address")

Feature Request Type

  • Core functionality
  • Alteration (enhancement/optimization) of existing feature(s)
  • New behavior

Description

For some reason chatgpt is sure that it will work, but I tested it with latest version and I can't see comments from doc.
What works for me is only setting it via core stawberry manually:

name: str = strawberry.field(description="The name of the user")

but this kills the whole idea of this library since I have to write boilerplate code for every model.
Would be perfect for autodocumentation support if I can set up docs in my entites and get them converted in the schema description.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

is there a way to pass selected fields to the query?

Hi,
is there a way to pass selected fields to the Query so that instead select * from a huge document/table I get only fields I really need?

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Package doesn't sucessfully build when installing on Windows 11

Below is the error stack

Collecting strawberry-sqlalchemy-mapper
  Downloading strawberry-sqlalchemy-mapper-0.1.0.tar.gz (13 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... error
  error: subprocess-exited-with-error
  
  × Getting requirements to build wheel did not run successfully.
  │ exit code: 1
  ╰─> [19 lines of output]
      Traceback (most recent call last):
        File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 363, in <module>
          main()
        File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 345, in main
          json_out['return_val'] = hook(**hook_input['kwargs'])
        File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 130, in get_requires_for_build_wheel
          return hook(config_settings)
        File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 338, in get_requires_for_build_wheel
          return self._get_build_requires(config_settings, requirements=['wheel'])
        File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 320, in _get_build_requires
          self.run_setup()
        File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 335, in run_setup
          exec(code, locals())
        File "<string>", line 14, in <module>
        File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\pathlib.py", line 1135, in read_text
          return f.read()
        File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\encodings\cp1252.py", line 23, in decode     
          return codecs.charmap_decode(input,self.errors,decoding_table)[0]
      UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 3209: character maps to <undefined>      
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> See above for output.

It installs successfully on WSL2 running Ubuntu 22.04.1

Future of this integration

Hi folks!

I wanted to write something I was discussing with @bellini666 a few weeks ago, at the moment we don't really have a maintainer for this library, I think @mattalbr is quite busy with his work, so we were wondering if we need to do a call for maintainers (if you, reading this, are interested let us know!)

Additionally we were thinking about how we can converge the integrations a bit, Strawberry Django has a lot of features that can be reuse/reimplemented here, but I think some of the APIs are different, maybe there's an opportunity to update this library to have a similar API to strawberry django (of course trying to prevent breaking changes if possible)

What do you think? /cc @erikwrede

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

AttributeError when sqlalchemy model has field created by column_property

I have a model, which includes column_property

class User(Base):
  name = Column(Text)
  lastname = Column(Text)
  full_name = column_property(name + " " + lastname)

When I start application I get this stack trace:

...
  File "/home/myhome/strawberry-test/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 371, in _convert_column_to_strawberry_type
    if column.nullable:
  File "/home/myhome/strawberry-test/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 1495, in __getattr__
    raise AttributeError(
AttributeError: Neither 'Label' object nor 'Comparator' object has an attribute 'nullable'

If I comment out the full_name field in the model, it works as expected.

Ubuntu 22.04, python 3.10, SQLAlchemy 2.027, psycopg 3.1.18, strawberry-sqlalchemy-mapper 0.4.2, strawberry-graphql 0.219.2

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Make Pagination Optional

Currently, pagination is configured for a model automatically if any relationship contains uselist=True, as seen in mapper.py:389. This seems undesirable as a strict rule, particularly in cases with a one-to-few relationship (think tires on cars). Obviously, there are cases where pagination is desirable (such as navigating a e-commerce catalog), so pagination should be toggleable.

Feature Request Type

  • Core functionality
  • Alteration (enhancement/optimization) of existing feature(s)
  • New behavior

Description

  • Add a flag for pagination, which, when True, configures the schema using relay pagination for the field, and, when False, creates a simple array of objects for the field

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Not all items in a relationship are loaded

Hi 👋

I really like what you have here. However, when I was trying to use it I came across some issues. Primarily, not all items in a relationship are being returned.

Minimal Example

Here is a minimal example:

1. Tables

class Location(Base):
    __tablename__ = "locations"  # type: ignore
    id: Column[int] = Column(Integer, primary_key=True, index=True)
    name: Column[str] = Column(String, nullable=False, unique=True)

    tasks = relationship("Task", lazy="joined", back_populates="location", uselist=False)


class Task(Base):
    __tablename__ = "tasks"  # type: ignore
    id: Column[int] = Column(Integer, primary_key=True, index=True)
    name: Column[str] = Column(String, nullable=False)
    location_id: Column[Optional[int]] = Column(Integer, ForeignKey(Location.id), nullable=True)

    location = relationship(Location, lazy="joined", back_populates="tasks", uselist=False)

2. Mapper

strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()

# ...

@strawberry_sqlalchemy_mapper.type(app.models.Location)
class LocationType:
    pass


@strawberry_sqlalchemy_mapper.type(app.models.Task)
class TaskType:
    pass


async def get_context() -> dict:
    return {
        "sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=SessionLocal()),
    }

# ...

additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(Query, Mutation, extensions=[SQLAlchemySession], types=additional_types)

graphql_app = GraphQLRouter(schema, context_getter=get_context,)

3. Queries

strawberry_sqlalchemy_mapper.finalize()

# ...

@strawberry.type
class Query:
    @strawberry.field
    async def tasks(self, info: Info) -> list[TaskType]:
        db = info.context["db"]
        sql = db.query(app.models.Task).order_by(app.models.Task.name)
        db_tasks = sql.all()
        return db_tasks

    @strawberry.field
    async def locations(self, info: Info) -> list[LocationType]:
        db = info.context["db"]
        sql = db.query(app.models.Location).order_by(app.models.Location.name)
        db_locations = sql.all()
        return db_locations

My database contains this data:

image

Issue

Now when I query using GraphQl, this is the output:

Query:

query Tasks {
  tasks {
    id
    name
    location {
      id
      name
      tasks {
        id
        name
      }
    }
  }
}

Result:

{
  "data": {
    "tasks": [
      {
        "id": 1,
        "name": "new Task",
        "location": {
          "id": 1,
          "name": "New Location",
          "tasks": {
            "id": 4,
            "name": "new Task 1-4"
          }
        }
      },
      {
        "id": 2,
        "name": "new Task 1-2",
        "location": {
          "id": 1,
          "name": "New Location",
          "tasks": {
            "id": 4,
            "name": "new Task 1-4"
          }
        }
      },
      {
        "id": 3,
        "name": "new Task 1-3",
        "location": {
          "id": 1,
          "name": "New Location",
          "tasks": {
            "id": 4,
            "name": "new Task 1-4"
          }
        }
      },
      {
        "id": 4,
        "name": "new Task 1-4",
        "location": {
          "id": 1,
          "name": "New Location",
          "tasks": {
            "id": 4,
            "name": "new Task 1-4"
          }
        }
      },

     // ...

    ]
  }
}

I would have expected all 4 tasks would show up in the nested tasks object. I am very new to GraphQL and not a 100% sure if this is an issue with my query or it is an issue in your wrapper.

SQLAlchemy Relationships fail when using secondary Table

Hey! Thanks for your great work on this!

I've queried SQLAlchemy Relationships with this library which works just fine, but fails when I use a secondary table. Example:

class Account():
    ...
    practice = relationship("Practice", secondary="user_practice", back_populates="accounts", uselist=False)


class Practice():
    ...
    accounts = relationship("Account", secondary="user_practice", back_populates="practice")

user_practice = Table(
    ...
    Column("account_id", ForeignKey("account.id", ondelete="CASCADE"), primary_key=True, unique=True),
    Column("practice_id", ForeignKey("practice.id", ondelete="CASCADE"), primary_key=True, index=True),
)

My query is something like this:

{
    accounts {
        edges {
            node {
                practice {
                    id
                }
            }
        }
    }
}

The library tries to look for account_id in the Practice table rather than the secondary table. I've also attached part of a stack trace below:

  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 420, in resolve
    related_objects = await loader.loader_for(relationship).load(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry/dataloader.py", line 251, in dispatch_batch
    values = await loader.load_fn(keys)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/backend/routers/graphql/mapper.py", line 109, in load_fn
    loaded = await loader.load_many(keys)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry/dataloader.py", line 251, in dispatch_batch
    values = await loader.load_fn(keys)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 49, in load_fn
    grouped_keys[group_by_remote_key(row)].append(row)
                 ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 41, in group_by_remote_key
    [
  File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 42, in <listcomp>
    getattr(row, remote.key)
AttributeError: 'Practice' object has no attribute 'account_id'

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

input type and optional fields

When combining the @strawberry.input and @strawberry_sqlalchemy_mapper.type decorators i was able to declare a valid strawberry input type.
However, i came into a problem trying to define which fields (generated by strawberry_sqlalchemy_mapper.type from the model) are optional (without overriding their previous strawberry_sqlalchemy_mapper declaration).
Could be useful to make a strawberry_sqlalchemy_mapper.input decorator that implements this (perhaps by checking 'SQLAlchemy.Column.nullable' property, or simply by listing them) and wraps both @strawberry.input and @strawberry_sqlalchemy_mapper.type.

EDIT
After examining the source code i see the mapper does take optional into account, it just does not reflect that when working with the @strawberry.input decorator (raises __init__() missing # required keyword-only argument(s))

possible use:

# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    username= Column(String, primary_key=True)
    password_hash = Column(String, nullable=False)
    first_name = Column(String, nullable=False)
    last_name = Column(String, nullable=True)
    


# elsewhere
# ...
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper

strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()


@strawberry_sqlalchemy_mapper.input(models.User)
class UserInput:
    __exclude__ = ["password_hash"]
    __optional__ = ["last_name"] # or read nullable attribute
    password : str

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Add `__include__` to explicitly define exposed fields.

Feature Request Type

  • Core functionality
  • Alteration (enhancement/optimization) of existing feature(s)
  • New behavior

Description

I see that the __exclude__ property is available to specify the model fields that should be excluded from the graphql API but i'm wonder how you would go the other direction and only expose the fields that explicitly specified in an __include__ list.

The reason for this is that, with the current functionality, it would be easy for a developer to add a sensitive field to the data model and forget to exclude it from the graphql schema definition, thus exposing it to the API.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Input type?

Hello,

I was wondering if there's a way to map to an input type?

In Strawberry, you could just do:

@strawberry.type
class User:
    username: str

@strawberry.input
class UserInput(User):
    pass

But doing that with this library doesn't make strawberry recognize it as an input

@_strawberryMapper.type(schema.User)
class User:
    pass

@strawberry.input
class UserInput(User):
    pass

_strawberryMapper.finalize()

Error:

25 TypeError: UserInput fields cannot be resolved. Input field type must be a GraphQL input type.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Support relay-style pagination (e.g. via sqlakeyset)

sqlakeyset provides a neat library to do keyset-based pagination of sqlalchemy queries. I think it could be awesome if our generated connection types supported input (first, after, last, before, order, condition) and we autogenerated pagination, ordering, and filtering support. The tricky part would be some of the assumptions built into sqlakeyset (you almost always need to sort it by some primary key if the values that you're sorting aren't unique, it doesn't support sorting on nullable fields unless you coalesce them, it doesn't support sqlalchemy 2.0-style queries).

What do you think? How would you feel about this feature if someone were to implement it? With something like this, the mapper becomes really powerful and magical right off the bat, and could conceivably be used to solve real-world problems with minimal customization. As is, I have to implement every list-based relationship by hand to deal with pagination and filtering, which is a bummer. Not the end of the world, just seems like something the mapper could do on its own.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Relationship fails when attribute name != column name

My model has a Survey class with owner_id attribute, which is using a different column name (user_id) for historic reasons

class User(Base):
    __tablename__ = "user"
    user_id: Mapped[int] = mapped_column("id", primary_key=True)
    username: Mapped[str]

class Survey(Base):
    __tablename__ = "survey"
    survey_id: Mapped[int] = mapped_column("id", primary_key=True)
    name: Mapped[str]
    owner_id: Mapped[int] = mapped_column("user_id", ForeignKey("user.id"))
    owner: Mapped[User] = relationship("User", backref="surveys", lazy=True)
import models

@strawberry_sqlalchemy_mapper.type(models.User)
class User:
    pass

@strawberry_sqlalchemy_mapper.type(models.Survey)
class Survey:
    pass

@strawberry.type
class Query:
    @strawberry.field
    def survey(self, info: Info, survey_id: int) -> typing.Optional[Survey]:
        db = info.context["db"]
        return db.execute(select(models.Survey).where(models.Survey.survey_id == survey_id)).scalars().first()

In relationship_resolver_for, the code tries to access getattr(self, sql_column_name) instead of getattr(self, python_attr_name)

query MyQuery {
  survey(surveyId: 1) {
    name
    owner {
      username
    }
  }
}
  File ".../strawberry_sqlalchemy_mapper/mapper.py", line 409, in <listcomp>
    getattr(self, local.key)
AttributeError: 'Survey' object has no attribute 'user_id'

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Mapping BigInteger to int limits the max value of BigInteger

Describe the Bug

I have a sqlalchemy model that is set up to something like this:

class UsersModel(Base):
    __tablename__ = "users"

    id = Column(BigInteger, unique=True, primary_key=True)
    # ... other fields

and I am mapping the schema like this:

@mapper.type(UsersModel)
class Users:
    ...

Some of the IDs that are being used are very large, e.g.: 1_029_320_583_927 which is way beyond the limit of Int32 (2_147_483_647), thus when I run a pretty simple query:

query {
  users {
    id
    name
  }
}

I am getting an error:
Int cannot represent non 32-bit signed integer value: 1029320583927

I think the mapping should be done to a custom scalar type in order to allow big integer values.

System Information

  • Operating system: macOS Sonoma 14.1.2
  • Strawberry version (if applicable): 0.215.1

Additional Context

Unfortunately the IDs must be BigInteger since this is already being used by other services so I can't change it to be UUID or even String which might be a different approach to solve it.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

The unique() method must be invoked on this Result, as it contains results that include joined eager loads against collections

First off, great job on this library overall. It has made integrating graphql with sqlalchemy a breeze.

I've run into this issue when setting up a pretty basic implementation using this library. The schema loads, but whenever I attempt to query nested relationships using the built-in dataloader sqlalchemy raises this error. I've tracked this down to the load_fn at loader.py line 37. Simply adding a .unique() to the call at rows = self.bind.scalars(query).all() fixes the issue.

sqlalchemy.exc.InvalidRequestError: The unique() method must be invoked on this Result, as it contains results that include joined eager loads against collections
# raises InvalidRequestError
rows = self.bind.scalars(query).all()

# works
rows = self.bind.scalars(query).unique().all()

Can't get relationships to work. Unexpected type ForwardRef?

python 3.11
sqlalchemy 2.0.23
strawberry-graphql 0.216.0
strawberry-sqlalchemy-mapper 0.4.0

Trying to use this mapper and was playing around with some basics but can't get relationships to work. It doesn't seem like i do something wrong, this is literally the example from readme:

models.py

from sqlalchemy import Column, UUID, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()


class Employee(Base):
    __tablename__ = "employee"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    password_hash = Column(String, nullable=False)
    department_id = Column(UUID, ForeignKey("department.id"))
    department = relationship("Department", back_populates="employees")


class Department(Base):
    __tablename__ = "department"
    id = Column(UUID, primary_key=True)
    name = Column(String, nullable=False)
    employees = relationship("Employee", back_populates="department")

app.py

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry, models
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper

strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()


@strawberry_sqlalchemy_mapper.type(models.Employee)
class Employee:
    __exclude__ = ["password_hash"]


@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
    pass


@strawberry.type
class Query:
    @strawberry.field
    def departments(self) -> Department:
        return Department(name="Test", employees=[Employee(name="1"), Employee(name="2")])


strawberry_sqlalchemy_mapper.finalize()
schema = strawberry.Schema(
    query=Query,
)
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix='/graphql')

main.py

import uvicorn
from app import app

if __name__ == "__main__":
    uvicorn.run(app, port=5000, log_level="info")

Got this error:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 808, in fields
    fields = resolve_thunk(self._fields)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
    return thunk() if callable(thunk) else thunk
           ^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 514, in <lambda>
    fields=lambda: self.get_graphql_fields(object_type),
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 373, in get_graphql_fields
    return _get_thunk_mapping(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 133, in _get_thunk_mapping
    thunk_mapping[name_converter(field)] = field_converter(
                                           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 314, in from_field
    self.from_maybe_optional(
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 768, in from_maybe_optional
    return GraphQLNonNull(self.from_type(type_))
                          ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 801, in from_type
    raise TypeError(f"Unexpected type '{type_}'")
TypeError: Unexpected type 'ForwardRef('Employee')'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/app/app.py", line 28, in <module>
    schema = strawberry.Schema(
             ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema.py", line 141, in __init__
    self._schema = GraphQLSchema(
                   ^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 224, in __init__
    collect_referenced_types(query)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 432, in collect_referenced_types
    for field in named_type.fields.values():
                 ^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/functools.py", line 1001, in __get__
    val = self.func(instance)
          ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 811, in fields
    raise cls(f"{self.name} fields cannot be resolved. {error}") from error
TypeError: EmployeeEdge fields cannot be resolved. Unexpected type 'ForwardRef('Employee')'

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Unsupported column type: `JSON`

Describe the Bug

Using the JSON type as a column in sqlalchemy shows as not supported for strawberry sqlalchemy mapper.

2023-08-08 12:39:13   File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 584, in convert
2023-08-08 12:39:13     self._handle_columns(mapper, type_, excluded_keys, generated_field_keys)
2023-08-08 12:39:13   File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 528, in _handle_columns
2023-08-08 12:39:13     type_annotation = self._convert_column_to_strawberry_type(column)
2023-08-08 12:39:13   File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 279, in _convert_column_to_strawberry_type
2023-08-08 12:39:13     raise UnsupportedColumnType(column.key, column.type)
2023-08-08 12:39:13 strawberry_sqlalchemy_mapper.exc.UnsupportedColumnType: Unsupported column type: `JSON` on column: `readingStart`. Possible fix: exclude this column

However, strawberry does already have a built-in JSON type.

from strawberry.scalars import JSON

And using this works perfectly fine when creating the type manually.

Example code:

from sqlalchemy import Column, JSON, Text
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase
from uuid import uuid4

class Base(AsyncAttrs, DeclarativeBase):
    pass

class Test(Base):
    __tablename__ = "test"
    id = Column(Text, primary_key=True)
    json = Column(JSON)

from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
_strawberryMapper = StrawberrySQLAlchemyMapper()

@_strawberryMapper.type(Test)
class StrawberryTest:
    pass

_strawberryMapper.finalize()

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Support mypy type checking

Currently, imports lead to the following error (on mypy 1.10.0):

app/graphql/schemas.py:9: error: Need type annotation for "strawberry_sqlalchemy_mapper"  [var-annotated]

Describe the Bug

I've noticed that this library has a py.typed file within it, but it appears as though it isn't being recognized

System Information

  • Operating system: Linux Mint
  • Strawberry version (if applicable): 0.229.0

Additional Context

N/A

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.