From dafbd30ef8d2dd1b5406a84b68dd171237cacbf1 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Thu, 11 Oct 2018 22:46:37 +0000 Subject: [PATCH] Support custom filters in OVO In sqlalchemy, there are some advanced filter criterion but such filter criterion is not supported in OVO. An example is the "not in" criterion which is achieved by "query.filter(~db_model.column.in_(values))". Another example is the "not equal" criterion which is achieved by "query.filter(db_model.column != value)" This commit adds support for custom filtering. We introduce a base class called "FilterObj" from which the custom filter class should inherit. This commit also implements two filter class: one for implementing the "not in" criterion and the other for the "not equal" criterion. In addition, it makes StringMatchingFilterObj inherit from the FilterObj class. Needed-By: https://review.openstack.org/#/c/609848/ Change-Id: I9ac7fb000d2bed445efbc226c30abdcd981b90cb Partial-Implements: blueprint adopt-oslo-versioned-objects-for-db --- neutron_lib/db/model_query.py | 12 +--- neutron_lib/objects/utils.py | 42 ++++++++++++- neutron_lib/tests/unit/objects/__init__.py | 0 neutron_lib/tests/unit/objects/test_utils.py | 59 +++++++++++++++++++ ...upport-custom-filter-f4a15bb5b38b7d3e.yaml | 9 +++ 5 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 neutron_lib/tests/unit/objects/__init__.py create mode 100644 neutron_lib/tests/unit/objects/test_utils.py create mode 100644 releasenotes/notes/support-custom-filter-f4a15bb5b38b7d3e.yaml diff --git a/neutron_lib/db/model_query.py b/neutron_lib/db/model_query.py index 2cf33a0ab..18c74bc1b 100644 --- a/neutron_lib/db/model_query.py +++ b/neutron_lib/db/model_query.py @@ -174,16 +174,8 @@ def apply_filters(query, model, filters, context=None): # do multiple equals matches query = query.filter( or_(*[column == v for v in value])) - elif isinstance(value, obj_utils.StringMatchingFilterObj): - if value.is_contains: - query = query.filter( - column.contains(value.contains)) - elif value.is_starts: - query = query.filter( - column.startswith(value.starts)) - elif value.is_ends: - query = query.filter( - column.endswith(value.ends)) + elif isinstance(value, obj_utils.FilterObj): + query = query.filter(value.filter(column)) elif None in value: # in_() operator does not support NULL element so we have # to do multiple equals matches diff --git a/neutron_lib/objects/utils.py b/neutron_lib/objects/utils.py index faafa3eb6..11c4599e5 100644 --- a/neutron_lib/objects/utils.py +++ b/neutron_lib/objects/utils.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import abc import copy +import six + from neutron_lib import exceptions @@ -25,7 +28,15 @@ def convert_filters(**kwargs): return result -class StringMatchingFilterObj(object): +@six.add_metaclass(abc.ABCMeta) +class FilterObj(object): + + @abc.abstractmethod + def filter(self, column): + pass + + +class StringMatchingFilterObj(FilterObj): @property def is_contains(self): return bool(getattr(self, "contains", False)) @@ -45,6 +56,9 @@ class StringContains(StringMatchingFilterObj): super(StringContains, self).__init__() self.contains = matching_string + def filter(self, column): + return column.contains(self.contains) + class StringStarts(StringMatchingFilterObj): @@ -52,9 +66,35 @@ class StringStarts(StringMatchingFilterObj): super(StringStarts, self).__init__() self.starts = matching_string + def filter(self, column): + return column.startswith(self.starts) + class StringEnds(StringMatchingFilterObj): def __init__(self, matching_string): super(StringEnds, self).__init__() self.ends = matching_string + + def filter(self, column): + return column.endswith(self.ends) + + +class NotIn(FilterObj): + + def __init__(self, value): + super(NotIn, self).__init__() + self.value = value + + def filter(self, column): + return ~column.in_(self.value) + + +class NotEqual(FilterObj): + + def __init__(self, value): + super(NotEqual, self).__init__() + self.value = value + + def filter(self, column): + return column != self.value diff --git a/neutron_lib/tests/unit/objects/__init__.py b/neutron_lib/tests/unit/objects/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_lib/tests/unit/objects/test_utils.py b/neutron_lib/tests/unit/objects/test_utils.py new file mode 100644 index 000000000..7464dd1bd --- /dev/null +++ b/neutron_lib/tests/unit/objects/test_utils.py @@ -0,0 +1,59 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.objects import utils as obj_utils +from neutron_lib.tests import _base as base + + +class TestUtils(base.BaseTestCase): + + def test_get_objects_with_filters_not_in(self): + + class FakeColumn(object): + def __init__(self, column): + self.column = column + + def in_(self, value): + self.value = value + return self + + def __invert__(self): + return list(set(self.column) - set(self.value)) + + filter_obj = obj_utils.NotIn([1, 2, 3]) + fake_column = FakeColumn([1, 2, 4, 5]) + self.assertEqual([4, 5], filter_obj.filter(fake_column)) + + fake_column = FakeColumn([1, 2]) + self.assertEqual([], filter_obj.filter(fake_column)) + + fake_column = FakeColumn([4, 5]) + self.assertEqual([4, 5], filter_obj.filter(fake_column)) + + def test_get_objects_with_filters_not_equal(self): + + class FakeColumn(object): + def __init__(self, column): + self.column = column + + def __ne__(self, value): + return [item for item in self.column if item != value] + + filter_obj = obj_utils.NotEqual(1) + fake_column = FakeColumn([1, 2, 4, 5]) + self.assertEqual([2, 4, 5], filter_obj.filter(fake_column)) + + fake_column = FakeColumn([1]) + self.assertEqual([], filter_obj.filter(fake_column)) + + fake_column = FakeColumn([4, 5]) + self.assertEqual([4, 5], filter_obj.filter(fake_column)) diff --git a/releasenotes/notes/support-custom-filter-f4a15bb5b38b7d3e.yaml b/releasenotes/notes/support-custom-filter-f4a15bb5b38b7d3e.yaml new file mode 100644 index 000000000..743ffbd22 --- /dev/null +++ b/releasenotes/notes/support-custom-filter-f4a15bb5b38b7d3e.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + This release adds support for custom filtering in versioned object. +features: + - | + A class called ``FilterObj`` is introduced. + This is the base class from which the custom filter class should inherit. + This release also implements two filter class: ``NotIn`` and ``NotEqual``. + The class ``StringMatchingFilterObj`` is now a subclass of ``FilterObj``.