strawberry-sqlalchemy-mapper 0.8.0
A library for autogenerating Strawberry GraphQL types from SQLAlchemy models.
License: MIT
Requires Python: <4.0,>=3.8
Keywords: graphql, sqlalchemy, strawberry

# Strawberry SQLAlchemy Mapper


[![PyPI Version][pypi-badge]][pypi-url]
![python][python-badge]
[![Downloads][downloads-badge]][downloads-url]
[![Test Coverage][coverage-badge]][coverage-url]
[![CI/CD Status][ci-badge]][ci-url]

[![Discord][discord-badge]][discord-url]

The simplest way to implement autogenerated [Strawberry][strawberry-url] 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. (See all supported types [here](#supported-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.

## Table of Contents

- [Getting Started](#getting-started)
    - [Installation](#installation)
    - [Basic Usage](#basic-usage)
- [Limitations](#limitations)
    - [Supported Types](#supported-types)
    - [Association Proxies](#association-proxies)
    - [Polymorphic Hierarchies](#polymorphic-hierarchies)
- [Contributing](#contributing)
- [⚖️ LICENSE](#️-license)


## Getting Started

### Installation
strawberry-sqlalchemy-mapper is available on [PyPi](https://pypi.org/project/strawberry-sqlalchemy-mapper/)

```
pip install strawberry-sqlalchemy-mapper
```

### Basic Usage

First, define your sqlalchemy model:

```python
# models.py
from sqlalchemy import Column, ForeignKey, Integer, String
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")
```

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:

```python
# In another file
# ...
import strawberry
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):
        # This db.session was created with sqlalchemy.orm.sessionmaker(...)
        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
                    }
                }
            }
        }
    }
}
"""
```

### Type Inheritance

You can inherit fields from other mapped types using standard Python class inheritance.

- Fields from the parent type (e.g., ApiA) are inherited by the child (e.g., ApiB).

- The `__exclude__` setting applies to inherited fields.

- If both SQLAlchemy models define the same field name, the field from the model inside `.type(...)` takes precedence.

- Declaring a field manually in the mapped type overrides everything else.

```python
class ModelA(base):
    __tablename__ = "a"

    id = Column(String, primary_key=True)
    common_field = Column(String(50))


class ModelB(base):
    __tablename__ = "b"

    id = Column(String, primary_key=True)
    common_field = Column(Integer)  # Conflicting field
    extra_field = Column(String(50))


@mapper.type(ModelA)
class ApiA:
    __exclude__ = ["id"]  # This field will be excluded in ApiA (and its children)


@mapper.type(ModelB)
class ApiB(ApiA):
    # Inherits fields from ApiA, except "id"
    # "common_field" will come from ModelB, not ModelA, so it will be a Integer
    # "extra_field" will be overridden and will be a float now instead of the String type declared in ModelB:
    extra_field: float = strawberry.field(name="extraField")
```

### Relay connections

By default, StrawberrySQLAlchemyMapper() will create [Relay connections](https://relay.dev/graphql/connections.htm) for relationships to lists. If instead you want these relationships to present as plain lists, you have two options:

1. Declare `__use_list__` in your models, for example:

```python
@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
    __use_list__ = ["employees"]
```

2. Alternatively, you can disable relay style connections for all models via the `always_use_list` constructor parameter:

```python
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper(always_use_list=True)
```

## Limitations

### Supported Types
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:


| SQLAlchemy Type       | Strawberry Equivalent     | Notes                  |
|-----------------------|---------------------------|------------------------|
| `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`                | `enum.Enum`               |  The Python enum that the column is mapped to must be decorated with`@strawberry.enum` ([strawberry enum docs][strawberry-enum-docs-url])  |



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

### Association Proxies

Association proxies are expected to be of the form `association_proxy('relationship1', 'relationship2')`,
i.e., both properties are expected to be relationships.
If your `association_proxy` does not follow the expected form, you should add it to `__exclude__` to prevent an exception from being raised.

### Polymorphic Hierarchies

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:

```python
# models.py
from sqlalchemy import Column

Base = declarative_base()


class Book(Base):
    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

For more details on how to contribute, as well as how to setup the project on your local machine, please refer to [the contributing docs](CONTRIBUTING.rst)


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


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

>💡 Tip: You can also use our DevContainer setup for a fully configured development environment, including pre-commit, Python, PostgreSQL, and all required dependencies. This is the fastest way to get started.

### Installation

```bash
pip install -r requirements.txt
```

Install [PostgreSQL 14+](https://www.postgresql.org/download/)

### Test

```bash
pytest
```

## ⚖️ LICENSE

MIT © [strawberry-sqlalchemy-mapper](LICENSE.txt)

<!-- Badge Links -->
[pypi-badge]: https://img.shields.io/pypi/v/strawberry-sqlalchemy-mapper?color=blue
[pypi-url]: https://pypi.org/project/strawberry-sqlalchemy-mapper/
[python-badge]: https://img.shields.io/pypi/pyversions/strawberry-sqlalchemy-mapper
[downloads-badge]: https://static.pepy.tech/badge/strawberry-sqlalchemy-mapper/month
[downloads-url]: https://pepy.tech/projects/strawberry-sqlalchemy-mapper
[ci-badge]: https://img.shields.io/github/actions/workflow/status/strawberry-graphql/strawberry-sqlalchemy/test.yml?branch=main
[ci-url]: https://github.com/strawberry-graphql/strawberry-sqlalchemy/actions
[coverage-badge]: https://codecov.io/gh/strawberry-graphql/strawberry-sqlalchemy/branch/main/graph/badge.svg
[coverage-url]: https://codecov.io/gh/strawberry-graphql/strawberry-sqlalchemy
[issues-url]: https://github.com/strawberry-graphql/strawberry-sqlalchemy/issues
[discord-badge]: https://img.shields.io/discord/689806334337482765?label=discord&logo=discord&logoColor=white&style=for-the-badge&color=blue
[discord-url]: https://discord.gg/ZkRTEJQ
[strawberry-url]: https://strawberry.rocks/
[strawberry-enum-docs-url]: https://strawberry.rocks/docs/types/enums#enums