diff --git a/README.rst b/README.rst index 42e5fb5..3eb9a64 100644 --- a/README.rst +++ b/README.rst @@ -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 ^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_filters/filters.py b/sqlalchemy_filters/filters.py index 3727dfc..f3875b0 100644 --- a/sqlalchemy_filters/filters.py +++ b/sqlalchemy_filters/filters.py @@ -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): diff --git a/test/conftest.py b/test/conftest.py index 164cc3e..4829cb8 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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' @@ -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 @@ -126,7 +129,7 @@ def connection(db_uri, db_engine_options): @pytest.fixture() -def session(connection): +def session(connection, is_postgresql): Session = sessionmaker(bind=connection) db_session = Session() @@ -134,6 +137,9 @@ def session(connection): 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() diff --git a/test/interface/test_filters.py b/test/interface/test_filters.py index 3c8cf75..41352ef 100644 --- a/test/interface/test_filters.py +++ b/test/interface/test_filters.py @@ -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 @@ -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): @@ -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'] diff --git a/test/models.py b/test/models.py index 52a1957..5be8193 100644 --- a/test/models.py +++ b/test/models.py @@ -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 @@ -12,6 +13,7 @@ class Base(object): Base = declarative_base(cls=Base) +BasePostgresqlSpecific = declarative_base(cls=Base) class Foo(Base): @@ -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))