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

Add Filters #357

Merged
merged 90 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
ac57fd4
Enable sorting when batching is enabled
PaulSchweizer Jul 23, 2022
6417061
Deprecate UnsortedSQLAlchemyConnectionField and resetting Relationshi…
PaulSchweizer Jul 31, 2022
535afbe
Use field_name instead of column.key to build sort enum names to ensu…
PaulSchweizer Jul 31, 2022
b91fede
Adjust batching test to honor different selet in query structure in s…
PaulSchweizer Aug 6, 2022
5b8d068
add filter tests for discussion
sabard Jul 25, 2022
861613e
add typing for custom filter
sabard Aug 2, 2022
1f47f92
update 1:n and n:m filter tests to use RelationshipFilter syntax
sabard Aug 2, 2022
e0cd465
add models and mark tests to fail
sabard Aug 2, 2022
32254b6
make filter tests run
sabard Aug 2, 2022
172985c
Added draft methods & classes for filter registry
erikwrede Aug 11, 2022
6319278
Drafted abstract filters, generation of filter fields from methods.
erikwrede Aug 11, 2022
69680a9
Prototype: Filter Schema generation
erikwrede Aug 12, 2022
c31bbac
use re sub instead of removesuffix for python<3.9 support
sabard Aug 12, 2022
f532857
fix syntax for python<3.9 support
sabard Aug 12, 2022
7e2bf6e
field-level filtering working for equals and not equals
sabard Aug 12, 2022
b21e449
Made filter query generation modular & fix flake8
erikwrede Aug 13, 2022
4fbbe81
Fixed variable name in execute_filters
erikwrede Aug 13, 2022
675a264
Drafted :1 and :many relationship filter construction
erikwrede Aug 13, 2022
60bfd3b
Prototype: :1 relationship filtering is working
erikwrede Aug 13, 2022
a30d77d
error on simple filter test
sabard Aug 16, 2022
fcce1f7
get simple and 1:1 filter tests passing
sabard Oct 5, 2022
4862737
fix a couple errors to get basic, incomplete 1:n test working
sabard Oct 24, 2022
15cfbd7
revert scalar_subquery to as_scalar for sqlalchemy<1.4 tests
sabard Oct 24, 2022
122d182
initial implementation of and/or logic
sabard Nov 21, 2022
11f7d91
revert filter logic so 1:n test passes
sabard Nov 21, 2022
c187c11
replicated tox error in CI
sabard Nov 21, 2022
6263540
partial: support custom filter fields, custom types of filter inputs
erikwrede Dec 1, 2022
4c7efb8
chore: add missing workflow setting
erikwrede Dec 1, 2022
c16ccf6
chore: change workflows back to the original setting
erikwrede Dec 1, 2022
9b86332
fix: 3.7 type hints
erikwrede Dec 1, 2022
ae28fe6
fix: relationship contains filters for n:m working
erikwrede Dec 19, 2022
d334593
test: add nested filter test and reverse relationship tests
sabard Dec 19, 2022
a4621e2
fix: chain or/and in contains
sabard Dec 19, 2022
0f34c05
test: try filtering ids on each containsExactly subquery
sabard Jan 1, 2023
7f7e98a
test: working group_by/having clause for specific case
sabard Jan 1, 2023
98db652
fix: group_by/having working for all tests but containsExactly 2
sabard Jan 1, 2023
ca1b498
chore: remove some debugging statements
sabard Jan 2, 2023
8f012f4
test: cover additional filter types
sabard Jan 2, 2023
14c657c
fix: use notin_
sabard Jan 2, 2023
2e321e9
chore: prepare for sqlalchemy2.0 adjustments
erikwrede Dec 4, 2022
e4b1a7f
update envlist for tox,reduce number of python versions
erikwrede Dec 4, 2022
812b28b
fix: set sqlalchemy max version to 2.1
erikwrede Dec 4, 2022
7288117
fix: all unit tests running
erikwrede Jan 2, 2023
e562cc2
fix: corrected sql version check for batching
erikwrede Jan 2, 2023
e525a8d
fix: added pragma no cover to version checks
erikwrede Jan 2, 2023
b84aa9f
chore: test with all python versions
erikwrede Jan 2, 2023
b832fff
wip: test: add hybrid_prop tests
sabard Jan 2, 2023
5fc1be3
test: fix hybrid_prop converter test
sabard Jan 3, 2023
156fb68
test: fix hybrid_prop test by using string typevars
sabard Jan 3, 2023
5d81991
test: revert test models
sabard Jan 5, 2023
b5c9054
Merge branch 'master' into add-filters
sabard Jan 9, 2023
ec62082
fix: filters support interfaces with BaseType
sabard Jan 16, 2023
b88280d
test: fix basic tests for async
sabard Jan 16, 2023
8e45a3a
fix: remove print statements
sabard Jan 16, 2023
ca23083
Merge branch 'master' into add-filters
sabard Jan 16, 2023
2495448
chore: cleanup
sabard Jan 16, 2023
f37dd8b
fix: make converter tests work
sabard Jan 16, 2023
5942291
fix: revert ci
sabard Jan 16, 2023
acfd97d
Merge branch 'master' into add-filters
erikwrede Jan 27, 2023
ace0aba
add initial filter docs
sabard Feb 13, 2023
fa0eecc
fix: typo
sabard Feb 13, 2023
3db7411
fix: test nits
sabard Feb 15, 2023
5662015
add basic filter example app
sabard Feb 27, 2023
9e8f1a5
add like methods to StringFilter
sabard Feb 27, 2023
d2360a1
raise errors in filters tests
sabard Feb 28, 2023
589c7d7
remove contains_exactly logic
sabard Mar 27, 2023
68194cc
Merge branch 'master' into add-filters
sabard Apr 10, 2023
48b1c6c
fix lint
sabard Apr 10, 2023
cab376a
fix: sqla 1.4 async filter tests passing with distinct
sabard Apr 24, 2023
db9f794
cleanup: automatically register field filters
sabard Apr 24, 2023
21eba2e
fix: convert type vars in converter.py
erikwrede Apr 24, 2023
39834ed
chore: fix review comments
erikwrede May 14, 2023
a9915d6
chore: update dependencies and fix test
erikwrede May 14, 2023
4712e10
chore: update sqa-utils fix
erikwrede May 14, 2023
cf0ba76
Merge branch 'master' into sqa-2.0
erikwrede May 14, 2023
a0abf8a
fix: adjust test after sqlalchemy 2.0 update
erikwrede May 14, 2023
474f6fa
Merge branch 'sqa-2.0' into add-filters
erikwrede May 31, 2023
1052b52
fix: keep aliases during and + or filtering
erikwrede Jun 4, 2023
0fb1db3
chore: make flake8 happy
erikwrede Jun 4, 2023
87bbd6f
test: breaking tests on enums
sabard Jun 5, 2023
06c90cb
fix: create special enum filters. Code pending refactor.
erikwrede Jul 28, 2023
4e34a79
fix: use typing extensions
erikwrede Jul 28, 2023
c38ebb3
refactor: cleanup filter type generation
erikwrede Oct 6, 2023
064adc7
feat(filters): support filter aliasing (PR #378)
erikwrede Oct 6, 2023
1aef748
chore: remove print statements
erikwrede Oct 6, 2023
18a7c54
chore: move base filter creation
erikwrede Oct 6, 2023
4063ea6
Merge branch 'master' into add-filters
erikwrede Oct 6, 2023
a2b8a9b
chore: run pre commit on PR (hook update due to incompatible hooks wi…
erikwrede Oct 6, 2023
ad9c1aa
Merge branch 'master' into add-filters
erikwrede Nov 20, 2023
e698d7c
chore: fix newly added merge conflict due to association proxies
erikwrede Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@ target/
*.sqlite3
.vscode

# Schema
*.gql

# mypy cache
.mypy_cache/
213 changes: 213 additions & 0 deletions docs/filters.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
=======
Filters
=======

Starting in graphene-sqlalchemy version 3, the SQLAlchemyConnectionField class implements filtering by default. The query utilizes a ``filter`` keyword to specify a filter class that inherits from ``graphene.InputObjectType``.

Migrating from graphene-sqlalchemy-filter
---------------------------------------------

If like many of us, you have been using |graphene-sqlalchemy-filter|_ to implement filters and would like to use the in-built mechanism here, there are a couple key differences to note. Mainly, in an effort to simplify the generated schema, filter keywords are nested under their respective fields instead of concatenated. For example, the filter partial ``{usernameIn: ["moderator", "cool guy"]}`` would be represented as ``{username: {in: ["moderator", "cool guy"]}}``.

.. |graphene-sqlalchemy-filter| replace:: ``graphene-sqlalchemy-filter``
.. _graphene-sqlalchemy-filter: https://github.com/art1415926535/graphene-sqlalchemy-filter

Further, some of the constructs found in libraries like `DGraph's DQL <https://dgraph.io/docs/query-language/>`_ have been implemented, so if you have created custom implementations for these features, you may want to take a look at the examples below.


Example model
-------------

Take as example a Pet model similar to that in the sorting example. We will use variations on this arrangement for the following examples.

.. code::

class Pet(Base):
__tablename__ = 'pets'
id = Column(Integer(), primary_key=True)
name = Column(String(30))
age = Column(Integer())


class PetNode(SQLAlchemyObjectType):
class Meta:
model = Pet


class Query(graphene.ObjectType):
allPets = SQLAlchemyConnectionField(PetNode.connection)


Simple filter example
---------------------

Filters are defined at the object level through the ``BaseTypeFilter`` class. The ``BaseType`` encompasses both Graphene ``ObjectType``\ s and ``Interface``\ s. Each ``BaseTypeFilter`` instance may define fields via ``FieldFilter`` and relationships via ``RelationshipFilter``. Here's a basic example querying a single field on the Pet model:

.. code::

allPets(filter: {name: {eq: "Fido"}}){
edges {
node {
name
}
}
}

This will return all pets with the name "Fido".


Custom filter types
-------------------

If you'd like to implement custom behavior for filtering a field, you can do so by extending one of the base filter classes in ``graphene_sqlalchemy.filters``. For example, if you'd like to add a ``divisible_by`` keyword to filter the age attribute on the ``Pet`` model, you can do so as follows:

.. code:: python

class MathFilter(FloatFilter):
class Meta:
graphene_type = graphene.Float

@classmethod
def divisible_by_filter(cls, query, field, val: int) -> bool:
return is_(field % val, 0)

class PetType(SQLAlchemyObjectType):
...

age = ORMField(filter_type=MathFilter)

class Query(graphene.ObjectType):
pets = SQLAlchemyConnectionField(PetType.connection)


Filtering over relationships with RelationshipFilter
----------------------------------------------------

When a filter class field refers to another object in a relationship, you may nest filters on relationship object attributes. This happens directly for 1:1 and m:1 relationships and through the ``contains`` and ``containsExactly`` keywords for 1:n and m:n relationships.


:1 relationships
^^^^^^^^^^^^^^^^

When an object or interface defines a singular relationship, relationship object attributes may be filtered directly like so:

Take the following SQLAlchemy model definition as an example:

.. code:: python

class Pet
...
person_id = Column(Integer(), ForeignKey("people.id"))

class Person
...
pets = relationship("Pet", backref="person")


Then, this query will return all pets whose person is named "Ada":

.. code::

allPets(filter: {
person: {name: {eq: "Ada"}}
}) {
...
}


:n relationships
^^^^^^^^^^^^^^^^

However, for plural relationships, relationship object attributes must be filtered through either ``contains`` or ``containsExactly``:

Now, using a many-to-many model definition:

.. code:: python

people_pets_table = sqlalchemy.Table(
"people_pets",
Base.metadata,
Column("person_id", ForeignKey("people.id")),
Column("pet_id", ForeignKey("pets.id")),
)

class Pet
...

class Person
...
pets = relationship("Pet", backref="people")


this query will return all pets which have a person named "Ben" in their ``people`` list.

.. code::

allPets(filter: {
people: {
contains: [{name: {eq: "Ben"}}],
}
}) {
...
}


and this one will return all pets which hvae a person list that contains exactly the people "Ada" and "Ben" and no fewer or people with other names.

.. code::

allPets(filter: {
articles: {
containsExactly: [
{name: {eq: "Ada"}},
{name: {eq: "Ben"}},
],
}
}) {
...
}

And/Or Logic
------------

Filters can also be chained together logically using `and` and `or` keywords nested under `filter`. Clauses are passed directly to `sqlalchemy.and_` and `slqlalchemy.or_`, respectively. To return all pets named "Fido" or "Spot", use:


.. code::

allPets(filter: {
or: [
{name: {eq: "Fido"}},
{name: {eq: "Spot"}},
]
}) {
...
}

And to return all pets that are named "Fido" or are 5 years old and named "Spot", use:

.. code::

allPets(filter: {
or: [
{name: {eq: "Fido"}},
{ and: [
{name: {eq: "Spot"}},
{age: {eq: 5}}
}
]
}) {
...
}


Hybrid Property support
-----------------------

Filtering over SQLAlchemy `hybrid properties <https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html>`_ is fully supported.


Reporting feedback and bugs
---------------------------

Filtering is a new feature to graphene-sqlalchemy, so please `post an issue on Github <https://github.com/graphql-python/graphene-sqlalchemy/issues>`_ if you run into any problems or have ideas on how to improve the implementation.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Contents:
inheritance
relay
tips
filters
examples
tutorial
api
47 changes: 47 additions & 0 deletions examples/filters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Example Filters Project
================================

This example highlights the ability to filter queries in graphene-sqlalchemy.

The project contains two models, one named `Department` and another
named `Employee`.

Getting started
---------------

First you'll need to get the source of the project. Do this by cloning the
whole Graphene-SQLAlchemy repository:

```bash
# Get the example project code
git clone https://github.com/graphql-python/graphene-sqlalchemy.git
cd graphene-sqlalchemy/examples/filters
```

It is recommended to create a virtual environment
for this project. We'll do this using
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
to keep things simple,
but you may also find something like
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
to be useful:

```bash
# Create a virtualenv in which we can install the dependencies
virtualenv env
source env/bin/activate
```

Install our dependencies:

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

The following command will setup the database, and start the server:

```bash
python app.py
```

Now head over to your favorite GraphQL client, POST to [http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql) and run some queries!
Empty file added examples/filters/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions examples/filters/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from database import init_db
from fastapi import FastAPI
from schema import schema
from starlette_graphene3 import GraphQLApp, make_playground_handler


def create_app() -> FastAPI:
init_db()
app = FastAPI()

app.mount("/graphql", GraphQLApp(schema, on_get=make_playground_handler()))

return app


app = create_app()
49 changes: 49 additions & 0 deletions examples/filters/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()
engine = create_engine(
"sqlite://", connect_args={"check_same_thread": False}, echo=True
)
session_factory = sessionmaker(autocommit=False, autoflush=False, bind=engine)

from sqlalchemy.orm import scoped_session as scoped_session_factory

scoped_session = scoped_session_factory(session_factory)

Base.query = scoped_session.query_property()
Base.metadata.bind = engine


def init_db():
from models import Person, Pet, Toy

Base.metadata.create_all()
scoped_session.execute("PRAGMA foreign_keys=on")
db = scoped_session()

person1 = Person(name="A")
person2 = Person(name="B")

pet1 = Pet(name="Spot")
pet2 = Pet(name="Milo")

toy1 = Toy(name="disc")
toy2 = Toy(name="ball")

person1.pet = pet1
person2.pet = pet2

pet1.toys.append(toy1)
pet2.toys.append(toy1)
pet2.toys.append(toy2)

db.add(person1)
db.add(person2)
db.add(pet1)
db.add(pet2)
db.add(toy1)
db.add(toy2)

db.commit()
34 changes: 34 additions & 0 deletions examples/filters/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import sqlalchemy
from database import Base
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship


class Pet(Base):
__tablename__ = "pets"
id = Column(Integer(), primary_key=True)
name = Column(String(30))
age = Column(Integer())
person_id = Column(Integer(), ForeignKey("people.id"))


class Person(Base):
__tablename__ = "people"
id = Column(Integer(), primary_key=True)
name = Column(String(100))
pets = relationship("Pet", backref="person")


pets_toys_table = sqlalchemy.Table(
"pets_toys",
Base.metadata,
Column("pet_id", ForeignKey("pets.id")),
Column("toy_id", ForeignKey("toys.id")),
)


class Toy(Base):
__tablename__ = "toys"
id = Column(Integer(), primary_key=True)
name = Column(String(30))
pets = relationship("Pet", secondary=pets_toys_table, backref="toys")
3 changes: 3 additions & 0 deletions examples/filters/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-e ../../
fastapi
uvicorn
1 change: 1 addition & 0 deletions examples/filters/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uvicorn app:app --port 5000
Loading