Skip to content

Simplify or related operations #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 13 additions & 21 deletions docs/advanced/filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `+` 反向运算
Expand Down Expand Up @@ -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 字典中,不允许存在相同的键值;<br/>
场景:我有一个列,需要多个相同条件但不同条件值的查询,此时,你应该使用 `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}
]
Expand Down
105 changes: 59 additions & 46 deletions sqlalchemy_crud_plus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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


Expand Down Expand Up @@ -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

Expand Down
15 changes: 4 additions & 11 deletions tests/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.