Skip to content
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

feat: SQLAlchemy 2.0 support #368

Merged
merged 12 commits into from
May 14, 2023
13 changes: 4 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
name: Tests

on:
push:
branches:
- 'master'
pull_request:
branches:
- '*'
on: [ push, pull_request ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 10
matrix:
sql-alchemy: ["1.2", "1.3", "1.4"]
python-version: ["3.7", "3.8", "3.9", "3.10"]
sql-alchemy: [ "1.2", "1.3", "1.4","2.0" ]
python-version: [ "3.7", "3.8", "3.9", "3.10" ]

steps:
- uses: actions/checkout@v3
Expand Down
20 changes: 18 additions & 2 deletions graphene_sqlalchemy/batching.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
import sqlalchemy
from sqlalchemy.orm import Session, strategies
from sqlalchemy.orm.query import QueryContext
from sqlalchemy.util import immutabledict

from .utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4, is_graphene_version_less_than
from .utils import (
SQL_VERSION_HIGHER_EQUAL_THAN_1_4,
SQL_VERSION_HIGHER_EQUAL_THAN_2,
is_graphene_version_less_than,
)


def get_data_loader_impl() -> Any: # pragma: no cover
Expand Down Expand Up @@ -76,7 +81,18 @@ async def batch_load_fn(self, parents):
query_context = parent_mapper_query._compile_context()
else:
query_context = QueryContext(session.query(parent_mapper.entity))
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
if SQL_VERSION_HIGHER_EQUAL_THAN_2: # pragma: no cover
self.selectin_loader._load_for_path(
query_context,
parent_mapper._path_registry,
states,
None,
child_mapper,
None,
None, # recursion depth can be none
immutabledict(), # default value for selectinload->lazyload
)
elif SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
self.selectin_loader._load_for_path(
query_context,
parent_mapper._path_registry,
Expand Down
7 changes: 7 additions & 0 deletions graphene_sqlalchemy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# fmt: off
# Fixme remove when https://github.com/kvesteri/sqlalchemy-utils/pull/644 is released #noqa
import sqlalchemy # noqa # isort:skip
if sqlalchemy.__version__ == "2.0.0b3": # noqa # isort:skip
sqlalchemy.__version__ = "2.0.0" # noqa # isort:skip
# fmt: on

import graphene
from graphene_sqlalchemy.utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4

Expand Down
20 changes: 16 additions & 4 deletions graphene_sqlalchemy/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import backref, column_property, composite, mapper, relationship

from graphene_sqlalchemy.utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4

PetKind = Enum("cat", "dog", name="pet_kind")


Expand Down Expand Up @@ -116,9 +118,15 @@ def hybrid_prop_bool(self) -> bool:
def hybrid_prop_list(self) -> List[int]:
return [1, 2, 3]

column_prop = column_property(
select([func.cast(func.count(id), Integer)]), doc="Column property"
)
# TODO Remove when switching min sqlalchemy version to SQLAlchemy 1.4
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
column_prop = column_property(
select(func.cast(func.count(id), Integer)), doc="Column property"
)
else:
column_prop = column_property(
select([func.cast(func.count(id), Integer)]), doc="Column property"
)

composite_prop = composite(
CompositeFullName, first_name, last_name, doc="Composite"
Expand Down Expand Up @@ -161,7 +169,11 @@ def __subclasses__(cls):

editor_table = Table("editors", Base.metadata, autoload=True)

mapper(ReflectedEditor, editor_table)
# TODO Remove when switching min sqlalchemy version to SQLAlchemy 1.4
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
Base.registry.map_imperatively(ReflectedEditor, editor_table)
else:
mapper(ReflectedEditor, editor_table)


############################################
Expand Down
13 changes: 10 additions & 3 deletions graphene_sqlalchemy/tests/models_batching.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import column_property, relationship

from graphene_sqlalchemy.utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4

PetKind = Enum("cat", "dog", name="pet_kind")


Expand Down Expand Up @@ -60,9 +62,14 @@ class Reporter(Base):
articles = relationship("Article", backref="reporter")
favorite_article = relationship("Article", uselist=False)

column_prop = column_property(
select([func.cast(func.count(id), Integer)]), doc="Column property"
)
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
column_prop = column_property(
select(func.cast(func.count(id), Integer)), doc="Column property"
)
else:
column_prop = column_property(
select([func.cast(func.count(id), Integer)]), doc="Column property"
erikwrede marked this conversation as resolved.
Show resolved Hide resolved
)


class Article(Base):
Expand Down
25 changes: 25 additions & 0 deletions graphene_sqlalchemy/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from ..fields import UnsortedSQLAlchemyConnectionField, default_connection_field_factory
from ..registry import Registry, get_global_registry
from ..types import ORMField, SQLAlchemyObjectType
from ..utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4, is_sqlalchemy_version_less_than
from .models import (
Article,
CompositeFullName,
Expand Down Expand Up @@ -336,6 +337,22 @@ class TestEnum(enum.IntEnum):
assert graphene_type._meta.enum.__members__["two"].value == 2


@pytest.mark.skipif(
not SQL_VERSION_HIGHER_EQUAL_THAN_1_4,
reason="SQLAlchemy <1.4 does not support this",
)
def test_should_columproperty_convert_sqa_20():
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: You could hand the select function and parametrize the test depending on the SQL version 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Good Idea!

field = get_field_from_column(
column_property(select(func.sum(func.cast(id, types.Integer))).where(id == 1))
)

assert field.type == graphene.Int


@pytest.mark.skipif(
not is_sqlalchemy_version_less_than("2.0.0b1"),
reason="SQLAlchemy >=2.0 does not support this syntax, see convert_sqa_20",
)
def test_should_columproperty_convert():
field = get_field_from_column(
column_property(select([func.sum(func.cast(id, types.Integer))]).where(id == 1))
Expand All @@ -355,10 +372,18 @@ def test_should_jsontype_convert_jsonstring():
assert get_field(types.JSON).type == graphene.JSONString


@pytest.mark.skipif(
(not is_sqlalchemy_version_less_than("2.0.0b1")),
reason="SQLAlchemy >=2.0 does not support this: Variant is no longer used in SQLAlchemy",
)
def test_should_variant_int_convert_int():
assert get_field(types.Variant(types.Integer(), {})).type == graphene.Int


@pytest.mark.skipif(
(not is_sqlalchemy_version_less_than("2.0.0b1")),
reason="SQLAlchemy >=2.0 does not support this: Variant is no longer used in SQLAlchemy",
)
def test_should_variant_string_convert_string():
assert get_field(types.Variant(types.String(), {})).type == graphene.String

Expand Down
8 changes: 7 additions & 1 deletion graphene_sqlalchemy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ def is_graphene_version_less_than(version_string): # pragma: no cover

SQL_VERSION_HIGHER_EQUAL_THAN_1_4 = False

if not is_sqlalchemy_version_less_than("1.4"):
if not is_sqlalchemy_version_less_than("1.4"): # pragma: no cover
from sqlalchemy.ext.asyncio import AsyncSession

SQL_VERSION_HIGHER_EQUAL_THAN_1_4 = True


SQL_VERSION_HIGHER_EQUAL_THAN_2 = False

if not is_sqlalchemy_version_less_than("2.0.0b1"): # pragma: no cover
SQL_VERSION_HIGHER_EQUAL_THAN_2 = True


def get_session(context):
return context.get("session")

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# To keep things simple, we only support newer versions of Graphene
"graphene>=3.0.0b7",
"promise>=2.3",
"SQLAlchemy>=1.1,<2",
"SQLAlchemy>=1.1,<2.1",
"aiodataloader>=0.2.0,<1.0",
]

Expand Down
8 changes: 6 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = pre-commit,py{37,38,39,310}-sql{12,13,14}
envlist = pre-commit,py{37,38,39,310}-sql{12,13,14,20}
skipsdist = true
minversion = 3.7.0

Expand All @@ -15,6 +15,7 @@ SQLALCHEMY =
1.2: sql12
1.3: sql13
1.4: sql14
2.0: sql20

[testenv]
passenv = GITHUB_*
Expand All @@ -23,8 +24,11 @@ deps =
sql12: sqlalchemy>=1.2,<1.3
sql13: sqlalchemy>=1.3,<1.4
sql14: sqlalchemy>=1.4,<1.5
sql20: sqlalchemy>=2.0.0b3,<2.1
setenv =
SQLALCHEMY_WARN_20 = 1
commands =
pytest graphene_sqlalchemy --cov=graphene_sqlalchemy --cov-report=term --cov-report=xml {posargs}
python -W always -m pytest graphene_sqlalchemy --cov=graphene_sqlalchemy --cov-report=term --cov-report=xml {posargs}

[testenv:pre-commit]
basepython=python3.10
Expand Down