Skip to content

Commit c417257

Browse files
authored
Simplify or related operations (#48)
1 parent 8239aee commit c417257

File tree

4 files changed

+77
-79
lines changed

4 files changed

+77
-79
lines changed

docs/advanced/filter.md

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ items = await item_crud.select_models(
5252

5353
!!! note
5454

55-
此过滤器必须传递字典,且字典结构必须为 `{'value': xxx, 'condition': {'已支持的过滤器': xxx}}`
55+
此过滤器必须传递字典,且字典结构必须为
5656

57-
`value`:此值将与列值进行条件运算
58-
59-
`condition`:此值将作为运算条件和预期结果
57+
`{'value': xxx, 'condition': {'已支持的条件过滤(不带前导 __)': xxx}}`
58+
59+
| value | condition |
60+
|-----------------|----------------|
61+
| 此值将根据运算符与列值进行运算 | 此值将作为运算条件和预期结果 |
6062

6163
- `__add`: Python `+` 运算符
6264
- `__radd`: Python `+` 反向运算
@@ -121,34 +123,24 @@ items = await item_crud.select_models(
121123
)
122124
```
123125

124-
## MOR
125-
126126
!!! note
127127

128-
`or` 过滤器的高级用法,每个键都应是库已支持的过滤器,仅允许字典
128+
`or` 过滤器的高级用法,每个值都应是一个已受支持的条件过滤器,它应该是一个数组
129129

130-
```python title="__mor"
130+
```python title="__or__"
131131
# 获取年龄等于 30 岁或 40 岁的员工
132132
items = await item_crud.select_models(
133133
session=db,
134-
age__mor={'eq': [30, 40]}, # (1)
134+
__or__=[
135+
{'age__eq': 30},
136+
{'age__eq': 40}
137+
]
135138
)
136-
```
137-
138-
1. 原因:在 python 字典中,不允许存在相同的键值;<br/>
139-
场景:我有一个列,需要多个相同条件但不同条件值的查询,此时,你应该使用 `mor` 过滤器,正如此示例一样使用它
140-
141-
## GOR
142-
143-
!!! note
144-
145-
`or` 过滤器的更高级用法,每个值都应是一个已受支持的条件过滤器,它应该是一个数组
146139

147-
```python title="__gor__"
148140
# 获取年龄在 30 - 40 岁之间或薪资大于 20k 的员工
149141
items = await item_crud.select_models(
150142
session=db,
151-
__gor__=[
143+
__or__=[
152144
{'age__between': [30, 40]},
153145
{'payroll__gt': 20000}
154146
]

sqlalchemy_crud_plus/utils.py

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,33 @@
5656
'rmod': lambda column: column.__rmod__,
5757
}
5858

59+
_DYNAMIC_OPERATORS = [
60+
'concat',
61+
'add',
62+
'radd',
63+
'sub',
64+
'rsub',
65+
'mul',
66+
'rmul',
67+
'truediv',
68+
'rtruediv',
69+
'floordiv',
70+
'rfloordiv',
71+
'mod',
72+
'rmod',
73+
]
74+
5975

6076
def get_sqlalchemy_filter(operator: str, value: Any, allow_arithmetic: bool = True) -> Callable[[str], Callable] | None:
6177
if operator in ['in', 'not_in', 'between']:
6278
if not isinstance(value, (tuple, list, set)):
6379
raise SelectOperatorError(f'The value of the <{operator}> filter must be tuple, list or set')
6480

65-
if (
66-
operator
67-
in ['add', 'radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod', 'rmod']
68-
and not allow_arithmetic
69-
):
81+
if operator in _DYNAMIC_OPERATORS and not allow_arithmetic:
7082
raise SelectOperatorError(f'Nested arithmetic operations are not allowed: {operator}')
7183

7284
sqlalchemy_filter = _SUPPORTED_FILTERS.get(operator)
73-
if sqlalchemy_filter is None and operator not in ['or', 'mor', '__gor']:
85+
if sqlalchemy_filter is None and operator != 'or':
7486
warnings.warn(
7587
f'The operator <{operator}> is not yet supported, only {", ".join(_SUPPORTED_FILTERS.keys())}.',
7688
SyntaxWarning,
@@ -94,12 +106,6 @@ def _create_or_filters(column: str, op: str, value: Any) -> list[ColumnElement |
94106
sqlalchemy_filter = get_sqlalchemy_filter(or_op, or_value)
95107
if sqlalchemy_filter is not None:
96108
or_filters.append(sqlalchemy_filter(column)(or_value))
97-
elif op == 'mor':
98-
for or_op, or_values in value.items():
99-
for or_value in or_values:
100-
sqlalchemy_filter = get_sqlalchemy_filter(or_op, or_value)
101-
if sqlalchemy_filter is not None:
102-
or_filters.append(sqlalchemy_filter(column)(or_value))
103109
return or_filters
104110

105111

@@ -131,43 +137,50 @@ def _create_and_filters(column: str, op: str, value: Any) -> list[ColumnElement
131137
def parse_filters(model: Type[Model] | AliasedClass, **kwargs) -> list[ColumnElement]:
132138
filters = []
133139

134-
def process_filters(target_column: str, target_op: str, target_value: Any):
135-
# OR / MOR
136-
or_filters = _create_or_filters(target_column, target_op, target_value)
137-
if or_filters:
138-
filters.append(or_(*or_filters))
139-
140-
# ARITHMETIC
141-
arithmetic_filters = _create_arithmetic_filters(target_column, target_op, target_value)
142-
if arithmetic_filters:
143-
filters.append(and_(*arithmetic_filters))
144-
else:
145-
# AND
146-
and_filters = _create_and_filters(target_column, target_op, target_value)
147-
if and_filters:
148-
filters.append(*and_filters)
149-
150140
for key, value in kwargs.items():
151-
if '__' in key:
152-
field_name, op = key.rsplit('__', 1)
153-
154-
# OR GROUP
155-
if field_name == '__gor' and op == '':
156-
_or_filters = []
157-
for field_or in value:
158-
for _key, _value in field_or.items():
159-
_field_name, _op = _key.rsplit('__', 1)
160-
_column = get_column(model, _field_name)
161-
process_filters(_column, _op, _value)
162-
if _or_filters:
163-
filters.append(or_(*_or_filters))
164-
else:
165-
column = get_column(model, field_name)
166-
process_filters(column, op, value)
167-
else:
168-
# NON FILTER
141+
if '__' not in key:
142+
# NO FILTER
169143
column = get_column(model, key)
170144
filters.append(column == value)
145+
continue
146+
147+
field_name, op = key.rsplit('__', 1)
148+
149+
# OR GROUP
150+
if field_name == '__or' and op == '':
151+
__or__filters = []
152+
153+
for field_or in value:
154+
for _key, _value in field_or.items():
155+
_field_name, _op = _key.rsplit('__', 1)
156+
_column = get_column(model, _field_name)
157+
158+
if '__' not in key:
159+
__or__filters.append(_column == _value)
160+
161+
if _op == 'or':
162+
__or__filters.append(*_create_or_filters(_column, _op, _value))
163+
continue
164+
165+
if _op in _DYNAMIC_OPERATORS:
166+
__or__filters.append(*_create_arithmetic_filters(_column, _op, _value))
167+
continue
168+
169+
__or__filters.append(*_create_and_filters(_column, _op, _value))
170+
171+
filters.append(or_(*__or__filters))
172+
else:
173+
column = get_column(model, field_name)
174+
175+
if op == 'or':
176+
filters.append(or_(*_create_or_filters(column, op, value)))
177+
continue
178+
179+
if op in _DYNAMIC_OPERATORS:
180+
filters.append(and_(*_create_arithmetic_filters(column, op, value)))
181+
continue
182+
183+
filters.append(*_create_and_filters(column, op, value))
171184

172185
return filters
173186

tests/test_select.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -396,23 +396,16 @@ async def test_select_model_by_column_with_or(create_test_model, async_db_sessio
396396

397397

398398
@pytest.mark.asyncio
399-
async def test_select_model_by_column_with_mor(create_test_model, async_db_session):
400-
async with async_db_session() as session:
401-
crud = CRUDPlus(Ins)
402-
result = await crud.select_model_by_column(session, id__mor={'eq': [1, 2, 3, 4, 5, 6, 7, 8, 9]})
403-
assert result.id == 1
404-
405-
406-
@pytest.mark.asyncio
407-
async def test_select_model_by_column_with___gor__(create_test_model, async_db_session):
399+
async def test_select_model_by_column_with__or__(create_test_model, async_db_session):
408400
async with async_db_session() as session:
409401
crud = CRUDPlus(Ins)
410402
result = await crud.select_model_by_column(
411403
session,
412-
__gor__=[
404+
__or__=[
413405
{'id__eq': 1},
414-
{'name__mor': {'endswith': ['1', '2']}},
415406
{'id__mul': {'value': 1, 'condition': {'eq': 1}}},
407+
{'name__endswith': '1'},
408+
{'name__endswith': '2'},
416409
],
417410
)
418411
assert result.id == 1

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)