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 support for sqlalchemy any operator to query arrays #36

Merged
merged 1 commit into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,14 @@ This is the list of operators that can be used:
- ``not_ilike``
- ``in``
- ``not_in``
- ``any``
- ``not_any``

any / not_any
^^^^^^^^^^^^^

PostgreSQL specific operators allow to filter queries on columns of type ``ARRAY``.
Use ``any`` to filter if a value is present in an array and ``not_any`` if it's not.

Boolean Functions
^^^^^^^^^^^^^^^^^
Expand Down
4 changes: 3 additions & 1 deletion sqlalchemy_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from itertools import chain

from six import string_types
from sqlalchemy import and_, or_, not_
from sqlalchemy import and_, or_, not_, func

from .exceptions import BadFilterFormat
from .models import Field, auto_join, get_model_from_spec, get_default_model
Expand Down Expand Up @@ -56,6 +56,8 @@ class Operator(object):
'not_ilike': lambda f, a: ~f.ilike(a),
'in': lambda f, a: f.in_(a),
'not_in': lambda f, a: ~f.in_(a),
'any': lambda f, a: f.any(a),
'not_any': lambda f, a: func.not_(f.any(a)),
}

def __init__(self, operator=None):
Expand Down
12 changes: 9 additions & 3 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import create_database, drop_database, database_exists

from test.models import Base
from test.models import Base, BasePostgresqlSpecific


SQLITE_TEST_DB_URI = 'SQLITE_TEST_DB_URI'
Expand Down Expand Up @@ -119,12 +119,15 @@ def db_engine_options(db_uri, is_postgresql):


@pytest.fixture(scope='session')
def connection(db_uri, db_engine_options):
def connection(db_uri, db_engine_options, is_postgresql):
create_db(db_uri)
engine = create_engine(db_uri, **db_engine_options)
Base.metadata.create_all(engine)
connection = engine.connect()
Base.metadata.bind = engine
if is_postgresql:
BasePostgresqlSpecific.metadata.create_all(engine)
BasePostgresqlSpecific.metadata.bind = engine

yield connection

Expand All @@ -133,14 +136,17 @@ def connection(db_uri, db_engine_options):


@pytest.fixture()
def session(connection):
def session(connection, is_postgresql):
Session = sessionmaker(bind=connection)
db_session = Session()

yield db_session

for table in reversed(Base.metadata.sorted_tables):
db_session.execute(table.delete())
if is_postgresql:
for table in reversed(BasePostgresqlSpecific.metadata.sorted_tables):
db_session.execute(table.delete())

db_session.commit()
db_session.close()
Expand Down
51 changes: 50 additions & 1 deletion test/interface/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
BadFilterFormat, BadSpec, FieldNotFound
)

from test.models import Foo, Bar, Qux
from test.models import Foo, Bar, Qux, Corge


ARRAY_NOT_SUPPORTED = (
"ARRAY type and operators supported only by PostgreSQL"
)


STRING_DATE_TIME_NOT_SUPPORTED = (
Expand Down Expand Up @@ -69,6 +74,17 @@ def multiple_quxs_inserted(session):
session.commit()


@pytest.fixture
def multiple_corges_inserted(session, is_postgresql):
if is_postgresql:
corge_1 = Corge(id=1, name='name_1', tags=[])
corge_2 = Corge(id=2, name='name_2', tags=['foo'])
corge_3 = Corge(id=3, name='name_3', tags=['foo', 'bar'])
corge_4 = Corge(id=4, name='name_4', tags=['bar', 'baz'])
session.add_all([corge_1, corge_2, corge_3, corge_4])
session.commit()


class TestFiltersNotApplied:

def test_no_filters_provided(self, session):
Expand Down Expand Up @@ -1166,3 +1182,36 @@ def test_complex_using_tuples(self, session):

assert len(result) == 1
assert result[0].id == 3


class TestApplyArrayFilters:

@pytest.mark.usefixtures('multiple_corges_inserted')
def test_any_value_in_array(self, session, is_postgresql):
if not is_postgresql:
pytest.skip(ARRAY_NOT_SUPPORTED)

query = session.query(Corge)
filters = [{'field': 'tags', 'op': 'any', 'value': 'foo'}]

filtered_query = apply_filters(query, filters)
result = filtered_query.all()

assert len(result) == 2
assert result[0].id == 2
assert result[1].id == 3

@pytest.mark.usefixtures('multiple_corges_inserted')
def test_not_any_values_in_array(self, session, is_postgresql):
if not is_postgresql:
pytest.skip(ARRAY_NOT_SUPPORTED)

query = session.query(Corge)
filters = [{'field': 'tags', 'op': 'not_any', 'value': 'foo'}]

filtered_query = apply_filters(query, filters)
result = filtered_query.all()

assert len(result) == 2
assert result[0].id == 1
assert result[1].id == 4
9 changes: 9 additions & 0 deletions test/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqlalchemy import (
Column, Date, DateTime, ForeignKey, Integer, String, Time
)
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Expand All @@ -14,6 +15,7 @@ class Base(object):


Base = declarative_base(cls=Base)
BasePostgresqlSpecific = declarative_base(cls=Base)


class Foo(Base):
Expand Down Expand Up @@ -44,3 +46,10 @@ class Qux(Base):
created_at = Column(Date)
execution_time = Column(DateTime)
expiration_time = Column(Time)


class Corge(BasePostgresqlSpecific):

__tablename__ = 'corge'

tags = Column(ARRAY(String, dimensions=1))