From bf9dca60ec48dcf074b3800260353d565df697ce Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 10 Jun 2025 17:46:19 +0800 Subject: [PATCH] Simplify or related operations --- docs/advanced/filter.md | 34 +++++------ sqlalchemy_crud_plus/utils.py | 105 +++++++++++++++++++--------------- tests/test_select.py | 15 ++--- uv.lock | 2 +- 4 files changed, 77 insertions(+), 79 deletions(-) diff --git a/docs/advanced/filter.md b/docs/advanced/filter.md index c35d505..cc6a51f 100644 --- a/docs/advanced/filter.md +++ b/docs/advanced/filter.md @@ -52,11 +52,13 @@ items = await item_crud.select_models( !!! note - 此过滤器必须传递字典,且字典结构必须为 `{'value': xxx, 'condition': {'已支持的过滤器': xxx}}` + 此过滤器必须传递字典,且字典结构必须为: - `value`:此值将与列值进行条件运算 - - `condition`:此值将作为运算条件和预期结果 + `{'value': xxx, 'condition': {'已支持的条件过滤(不带前导 __)': xxx}}` + + | value | condition | + |-----------------|----------------| + | 此值将根据运算符与列值进行运算 | 此值将作为运算条件和预期结果 | - `__add`: Python `+` 运算符 - `__radd`: Python `+` 反向运算 @@ -121,34 +123,24 @@ items = await item_crud.select_models( ) ``` -## MOR - !!! note - `or` 过滤器的高级用法,每个键都应是库已支持的过滤器,仅允许字典 + `or` 过滤器的高级用法,每个值都应是一个已受支持的条件过滤器,它应该是一个数组 -```python title="__mor" +```python title="__or__" # 获取年龄等于 30 岁或 40 岁的员工 items = await item_crud.select_models( session=db, - age__mor={'eq': [30, 40]}, # (1) + __or__=[ + {'age__eq': 30}, + {'age__eq': 40} + ] ) -``` - -1. 原因:在 python 字典中,不允许存在相同的键值;
- 场景:我有一个列,需要多个相同条件但不同条件值的查询,此时,你应该使用 `mor` 过滤器,正如此示例一样使用它 - -## GOR - -!!! note - - `or` 过滤器的更高级用法,每个值都应是一个已受支持的条件过滤器,它应该是一个数组 -```python title="__gor__" # 获取年龄在 30 - 40 岁之间或薪资大于 20k 的员工 items = await item_crud.select_models( session=db, - __gor__=[ + __or__=[ {'age__between': [30, 40]}, {'payroll__gt': 20000} ] diff --git a/sqlalchemy_crud_plus/utils.py b/sqlalchemy_crud_plus/utils.py index 1d2e728..c9ebba1 100644 --- a/sqlalchemy_crud_plus/utils.py +++ b/sqlalchemy_crud_plus/utils.py @@ -56,21 +56,33 @@ 'rmod': lambda column: column.__rmod__, } +_DYNAMIC_OPERATORS = [ + 'concat', + 'add', + 'radd', + 'sub', + 'rsub', + 'mul', + 'rmul', + 'truediv', + 'rtruediv', + 'floordiv', + 'rfloordiv', + 'mod', + 'rmod', +] + def get_sqlalchemy_filter(operator: str, value: Any, allow_arithmetic: bool = True) -> Callable[[str], Callable] | None: if operator in ['in', 'not_in', 'between']: if not isinstance(value, (tuple, list, set)): raise SelectOperatorError(f'The value of the <{operator}> filter must be tuple, list or set') - if ( - operator - in ['add', 'radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod', 'rmod'] - and not allow_arithmetic - ): + if operator in _DYNAMIC_OPERATORS and not allow_arithmetic: raise SelectOperatorError(f'Nested arithmetic operations are not allowed: {operator}') sqlalchemy_filter = _SUPPORTED_FILTERS.get(operator) - if sqlalchemy_filter is None and operator not in ['or', 'mor', '__gor']: + if sqlalchemy_filter is None and operator != 'or': warnings.warn( f'The operator <{operator}> is not yet supported, only {", ".join(_SUPPORTED_FILTERS.keys())}.', SyntaxWarning, @@ -94,12 +106,6 @@ def _create_or_filters(column: str, op: str, value: Any) -> list[ColumnElement | sqlalchemy_filter = get_sqlalchemy_filter(or_op, or_value) if sqlalchemy_filter is not None: or_filters.append(sqlalchemy_filter(column)(or_value)) - elif op == 'mor': - for or_op, or_values in value.items(): - for or_value in or_values: - sqlalchemy_filter = get_sqlalchemy_filter(or_op, or_value) - if sqlalchemy_filter is not None: - or_filters.append(sqlalchemy_filter(column)(or_value)) return or_filters @@ -131,43 +137,50 @@ def _create_and_filters(column: str, op: str, value: Any) -> list[ColumnElement def parse_filters(model: Type[Model] | AliasedClass, **kwargs) -> list[ColumnElement]: filters = [] - def process_filters(target_column: str, target_op: str, target_value: Any): - # OR / MOR - or_filters = _create_or_filters(target_column, target_op, target_value) - if or_filters: - filters.append(or_(*or_filters)) - - # ARITHMETIC - arithmetic_filters = _create_arithmetic_filters(target_column, target_op, target_value) - if arithmetic_filters: - filters.append(and_(*arithmetic_filters)) - else: - # AND - and_filters = _create_and_filters(target_column, target_op, target_value) - if and_filters: - filters.append(*and_filters) - for key, value in kwargs.items(): - if '__' in key: - field_name, op = key.rsplit('__', 1) - - # OR GROUP - if field_name == '__gor' and op == '': - _or_filters = [] - for field_or in value: - for _key, _value in field_or.items(): - _field_name, _op = _key.rsplit('__', 1) - _column = get_column(model, _field_name) - process_filters(_column, _op, _value) - if _or_filters: - filters.append(or_(*_or_filters)) - else: - column = get_column(model, field_name) - process_filters(column, op, value) - else: - # NON FILTER + if '__' not in key: + # NO FILTER column = get_column(model, key) filters.append(column == value) + continue + + field_name, op = key.rsplit('__', 1) + + # OR GROUP + if field_name == '__or' and op == '': + __or__filters = [] + + for field_or in value: + for _key, _value in field_or.items(): + _field_name, _op = _key.rsplit('__', 1) + _column = get_column(model, _field_name) + + if '__' not in key: + __or__filters.append(_column == _value) + + if _op == 'or': + __or__filters.append(*_create_or_filters(_column, _op, _value)) + continue + + if _op in _DYNAMIC_OPERATORS: + __or__filters.append(*_create_arithmetic_filters(_column, _op, _value)) + continue + + __or__filters.append(*_create_and_filters(_column, _op, _value)) + + filters.append(or_(*__or__filters)) + else: + column = get_column(model, field_name) + + if op == 'or': + filters.append(or_(*_create_or_filters(column, op, value))) + continue + + if op in _DYNAMIC_OPERATORS: + filters.append(and_(*_create_arithmetic_filters(column, op, value))) + continue + + filters.append(*_create_and_filters(column, op, value)) return filters diff --git a/tests/test_select.py b/tests/test_select.py index 025ce3c..e0b2c3e 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -396,23 +396,16 @@ async def test_select_model_by_column_with_or(create_test_model, async_db_sessio @pytest.mark.asyncio -async def test_select_model_by_column_with_mor(create_test_model, async_db_session): - async with async_db_session() as session: - crud = CRUDPlus(Ins) - result = await crud.select_model_by_column(session, id__mor={'eq': [1, 2, 3, 4, 5, 6, 7, 8, 9]}) - assert result.id == 1 - - -@pytest.mark.asyncio -async def test_select_model_by_column_with___gor__(create_test_model, async_db_session): +async def test_select_model_by_column_with__or__(create_test_model, async_db_session): async with async_db_session() as session: crud = CRUDPlus(Ins) result = await crud.select_model_by_column( session, - __gor__=[ + __or__=[ {'id__eq': 1}, - {'name__mor': {'endswith': ['1', '2']}}, {'id__mul': {'value': 1, 'condition': {'eq': 1}}}, + {'name__endswith': '1'}, + {'name__endswith': '2'}, ], ) assert result.id == 1 diff --git a/uv.lock b/uv.lock index cc305b8..f657745 100644 --- a/uv.lock +++ b/uv.lock @@ -394,7 +394,7 @@ wheels = [ [[package]] name = "sqlalchemy-crud-plus" -version = "1.8.0" +version = "1.9.0" source = { editable = "." } dependencies = [ { name = "pydantic" },