Skip to content

Commit

Permalink
Add support for sqlalchemy any operator to query arrays. "any" filter
Browse files Browse the repository at this point in the history
models having value in attribute of array type, "not_all" filter models
not having value in attribute of array. Adds postgresql specific
tests/models for querying arrays.
  • Loading branch information
bodik committed May 11, 2019
1 parent 3d113dc commit ae0c9cd
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 4 deletions.
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,14 @@ This is the list of operators that can be used:
- ``ilike``
- ``in``
- ``not_in``
- ``any``
- ``not_all``
any / not_all
^^^^^^^^^^^^^
PostgreSQL specific operators can be used to filter queries based on presence
(``any``) or absence (``not_all``) value in model attribute of ARRAY type.
Boolean Functions
^^^^^^^^^^^^^^^^^
Expand Down
2 changes: 2 additions & 0 deletions sqlalchemy_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class Operator(object):
'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_all': lambda f, a: ~f.all(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 @@ -112,12 +112,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 @@ -126,14 +129,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
49 changes: 48 additions & 1 deletion test/interface/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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"
)


@pytest.fixture
Expand Down Expand Up @@ -59,6 +64,15 @@ 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=['foo'])
corge_2 = Corge(id=2, name='name_2', tags=['bar', 'baz'])
session.add_all([corge_1, corge_2])
session.commit()


class TestFiltersNotApplied:

def test_no_filters_provided(self, session):
Expand Down Expand Up @@ -1068,3 +1082,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_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) == 1
assert result[0].id == 1
assert result[0].tags == ['foo']

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

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

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

assert len(result) == 1
assert result[0].id == 2
assert result[0].tags == ['bar', 'baz']
9 changes: 9 additions & 0 deletions test/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-

from sqlalchemy import Column, Date, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Expand All @@ -12,6 +13,7 @@ class Base(object):


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


class Foo(Base):
Expand Down Expand Up @@ -41,3 +43,10 @@ class Qux(Base):

created_at = Column(Date)
execution_time = Column(DateTime)


class Corge(BasePostgresqlSpecific):

__tablename__ = 'corge'

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

0 comments on commit ae0c9cd

Please sign in to comment.