From c789d0ae827cc928a6b0a1bfa6be83d0a5f32c68 Mon Sep 17 00:00:00 2001 From: Zisen Liu <29354199+rabbull@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:18:37 +0800 Subject: [PATCH] Tests: Add Tests to `contains` Filter Operator on PostgreSQL Backend (#6617) This PR adds tests to validate the behavior of the `contains` operator when applied to JSON objects and arrays in the PostgreSQL backend. The tests cover a variety of scenarios, including type matching, negations, and edge cases such as empty arrays or objects. This is a test-only PR, and no features or functionality in aiida-core have been modified. --- tests/orm/test_querybuilder.py | 161 +++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/orm/test_querybuilder.py b/tests/orm/test_querybuilder.py index e39f20a7b9..8797fe4e03 100644 --- a/tests/orm/test_querybuilder.py +++ b/tests/orm/test_querybuilder.py @@ -9,6 +9,7 @@ """Tests for the QueryBuilder.""" import copy +import json import uuid import warnings from collections import defaultdict @@ -1702,3 +1703,163 @@ def test_statistics_default_class(self, aiida_localhost): # data are correct res = next(iter(qb.dict()[0].values())) assert res == expected_dict + + +class TestJsonFilters: + @pytest.mark.parametrize( + 'data,filters,is_match', + ( + # contains different types of element + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': [1]}}, True), + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': ['2']}}, True), + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': [None]}}, True), + # contains multiple elements of various types + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': [1, None]}}, True), + # contains non-exist elements + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': [114514]}}, False), + # contains empty set + ({'arr': [1, '2', None]}, {'attributes.arr': {'contains': []}}, True), + ({'arr': []}, {'attributes.arr': {'contains': []}}, True), + # nested arrays + ({'arr': [[1, 0], [0, 2]]}, {'attributes.arr': {'contains': [[1, 0]]}}, True), + ({'arr': [[2, 3], [0, 1], []]}, {'attributes.arr': {'contains': [[1, 0]]}}, True), + ({'arr': [[2, 3], [1]]}, {'attributes.arr': {'contains': [[4]]}}, False), + ({'arr': [[1, 0], [0, 2]]}, {'attributes.arr': {'contains': [[3]]}}, False), + ({'arr': [[1, 0], [0, 2]]}, {'attributes.arr': {'contains': [3]}}, False), + ({'arr': [[1, 0], [0, 2]]}, {'attributes.arr': {'contains': [[2]]}}, True), + ({'arr': [[1, 0], [0, 2]]}, {'attributes.arr': {'contains': [2]}}, False), + ({'arr': [[1, 0], [0, 2], 3]}, {'attributes.arr': {'contains': [[3]]}}, False), + ({'arr': [[1, 0], [0, 2], 3]}, {'attributes.arr': {'contains': [3]}}, True), + # negations + ({'arr': [1, '2', None]}, {'attributes.arr': {'!contains': [1]}}, False), + ({'arr': [1, '2', None]}, {'attributes.arr': {'!contains': []}}, False), + ({'arr': [1, '2', None]}, {'attributes.arr': {'!contains': [114514]}}, True), + ({'arr': [1, '2', None]}, {'attributes.arr': {'!contains': [1, 114514]}}, True), + # TODO: these pass, but why? are these behaviors expected? + # non-exist `attr_key`s + ({'foo': []}, {'attributes.arr': {'contains': []}}, False), + ({'foo': []}, {'attributes.arr': {'!contains': []}}, False), + ), + ids=json.dumps, + ) + @pytest.mark.usefixtures('aiida_profile_clean') + @pytest.mark.requires_psql + def test_json_filters_contains_arrays(self, data, filters, is_match): + """Test QueryBuilder filter `contains` for JSON array fields""" + orm.Dict(data).store() + qb = orm.QueryBuilder().append(orm.Dict, filters=filters) + assert qb.count() in {0, 1} + found = qb.count() == 1 + assert found == is_match + + @pytest.mark.parametrize( + 'data,filters,is_match', + ( + # contains different types of values + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'contains': {'k1': 1}}}, + True, + ), + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'contains': {'k1': 1, 'k2': '2'}}}, + True, + ), + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'contains': {'k3': None}}}, + True, + ), + # contains empty set + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'contains': {}}}, + True, + ), + # doesn't contain non-exist entries + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'contains': {'k1': 1, 'k': 'v'}}}, + False, + ), + # negations + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'!contains': {'k1': 1}}}, + False, + ), + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'!contains': {'k1': 1, 'k': 'v'}}}, + True, + ), + ( + { + 'dict': { + 'k1': 1, + 'k2': '2', + 'k3': None, + } + }, + {'attributes.dict': {'!contains': {}}}, + False, + ), + # TODO: these pass, but why? are these behaviors expected? + # non-exist `attr_key`s + ({'map': {}}, {'attributes.dict': {'contains': {}}}, False), + ({'map': {}}, {'attributes.dict': {'!contains': {}}}, False), + ), + ids=json.dumps, + ) + @pytest.mark.usefixtures('aiida_profile_clean') + @pytest.mark.requires_psql + def test_json_filters_contains_object(self, data, filters, is_match): + """Test QueryBuilder filter `contains` for JSON object fields""" + orm.Dict(data).store() + qb = orm.QueryBuilder().append(orm.Dict, filters=filters) + assert qb.count() in {0, 1} + found = qb.count() == 1 + assert found == is_match