Skip to content

Commit caaf85d

Browse files
committed
feat: resolve_wheres
1 parent e3ba23e commit caaf85d

File tree

7 files changed

+87
-36
lines changed

7 files changed

+87
-36
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.vscode
22

3+
__pycache__
4+
35
Pipfile.lock

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,26 @@ class AccountMgr(BaseManager, metaclass=DemoMetaclass):
3030
### **select_custom_fields**
3131
**basic example**
3232
```python
33+
aids = [1, 2, 3]
34+
3335
await AccountMgr.select_custom_fields(
3436
fields=[
3537
"id", "extend ->> '$.last_login.ipv4' ipv4",
3638
"extend ->> '$.last_login.start_datetime' start_datetime",
3739
"CAST(extend ->> '$.last_login.online_sec' AS SIGNED) online_sec"
3840
],
39-
wheres=[f"id IN (1, 2, 3)"],
41+
wheres=f"id IN ({', '.join(map(str, aids))}) AND gender = 1", # These 4 ways of `wheres` can work equally
42+
# wheres=Q(Q(id__in=aids), Q(gender=1), join_type="AND"),
43+
# wheres={"id__in": aids, "gender": 1},
44+
# wheres=[Q(id__in=aids), Q(gender=1)],
4045
)
4146
```
4247
Generate sql and execute
4348
```sql
4449
SELECT
4550
id, extend ->> '$.last_login.ipv4' ipv4, extend ->> '$.last_login.start_datetime' start_datetime, CAST(extend ->> '$.last_login.online_sec' AS SIGNED) online_sec
4651
FROM account
47-
WHERE id IN (1, 2, 3)
52+
WHERE `id` IN (1,2,3) AND `gender`=1
4853
```
4954

5055
**complex example**
@@ -53,9 +58,9 @@ await AccountMgr.select_custom_fields(
5358
fields=[
5459
"locale", "gender", "COUNT(1) cnt"
5560
],
56-
wheres=["id BETWEEN 1 AND 12"],
61+
wheres="id BETWEEN 1 AND 12",
5762
groups=["locale", "gender"],
58-
havings=["cnt > 0"],
63+
having="cnt > 0",
5964
orders=["locale", "-gender"],
6065
limit=10,
6166
)
@@ -84,7 +89,7 @@ await AccountMgr.upsert_json_field(
8489
},
8590
"$.uuid": "fd04f7f2-24fc-4a73-a1d7-b6e99a464c5f",
8691
},
87-
wheres=[f"id = 8"],
92+
wheres=f"id = 8",
8893
)
8994
```
9095
Generate sql and execute
@@ -119,7 +124,7 @@ Generate sql and execute
119124
### **insert_into_select**
120125
```python
121126
await AccountMgr.insert_into_select(
122-
wheres=[f"id IN (4, 5, 6)"],
127+
wheres=f"id IN (4, 5, 6)",
123128
remain_fields=["gender", "locale"],
124129
assign_field_dict={
125130
"active": False,

examples/service/routers/account.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from fastapi import APIRouter, Body, Query
66
from fastapi_esql.utils.sqlizer import RawSQL
77
from pydantic import BaseModel, Field
8+
from tortoise.expressions import Q
89

910
from examples.service.constants.enums import GenderEnum, LocaleEnum
1011
from examples.service.managers.demo.account import AccountMgr
@@ -55,9 +56,9 @@ async def query_group_by_locale_view(
5556
fields=[
5657
"locale", "gender", "COUNT(1) cnt"
5758
],
58-
wheres=["id BETWEEN 1 AND 12"],
59+
wheres="id BETWEEN 1 AND 12",
5960
groups=["locale", "gender"],
60-
havings=["cnt > 0"],
61+
having="cnt > 0",
6162
orders=["locale", "-gender"],
6263
limit=10,
6364
)
@@ -103,7 +104,10 @@ async def query_last_login_view(
103104
"extend ->> '$.last_login.start_datetime' start_datetime",
104105
"CAST(extend ->> '$.last_login.online_sec' AS SIGNED) online_sec"
105106
],
106-
wheres=[f"id IN ({', '.join(map(str, aids))})"],
107+
wheres=f"id IN ({', '.join(map(str, aids))}) AND gender = 1",
108+
# wheres=Q(Q(id__in=aids), Q(gender=1), join_type="AND"),
109+
# wheres={"id__in": aids, "gender": 1},
110+
# wheres=[Q(id__in=aids), Q(gender=1)],
107111
)
108112
return {"dicts": dicts}
109113

@@ -123,7 +127,7 @@ async def update_last_login_view(
123127
},
124128
"$.uuid": faker.uuid4(),
125129
},
126-
wheres=[f"id = {aid}"],
130+
wheres=f"id = {aid}",
127131
)
128132
return {"row_cnt": row_cnt}
129133

@@ -154,7 +158,7 @@ async def bulk_clone_view(
154158
aids: List[int] = Body(..., embed=True),
155159
):
156160
ok = await AccountMgr.insert_into_select(
157-
wheres=[f"id IN ({', '.join(map(str, aids))})"],
161+
wheres=f"id IN ({', '.join(map(str, aids))})",
158162
remain_fields=["gender", "locale"],
159163
assign_field_dict={
160164
"active": False,

fastapi_esql/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from logging.config import dictConfig
22

3-
from .const.error import WrongParamsError
3+
from .const.error import QsParsingError, WrongParamsError
44
from .orm.base_app import AppMetaclass
55
from .orm.base_manager import BaseManager
66
from .orm.base_model import BaseModel
@@ -17,6 +17,7 @@
1717
"CursorHandler",
1818
"RawSQL",
1919
"SQLizer",
20+
"QsParsingError"
2021
"WrongParamsError",
2122
"timing",
2223
]

fastapi_esql/const/error.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
class WrongParamsError(Exception):
22
...
3+
4+
5+
class QsParsingError(Exception):
6+
...

fastapi_esql/orm/base_manager.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from logging import getLogger
2-
from typing import Any, Dict, List, Optional
2+
from typing import Any, Dict, List, Optional, Union
33

44
from tortoise.backends.base.client import BaseDBAsyncClient
5+
from tortoise.expressions import Q
56
from tortoise.models import Model
67

78
from .base_app import AppMetaclass
@@ -63,9 +64,9 @@ async def bulk_create_from_dicts(cls, dicts: List[Dict[str, Any]], **kwargs) ->
6364
async def select_custom_fields(
6465
cls,
6566
fields: List[str],
66-
wheres: List[str],
67+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
6768
groups: Optional[List[str]] = None,
68-
havings: Optional[List[str]] = None,
69+
having: Optional[str] = None,
6970
orders: Optional[List[str]] = None,
7071
limit: int = 0,
7172
conn: Optional[BaseDBAsyncClient] = None,
@@ -75,9 +76,10 @@ async def select_custom_fields(
7576
fields,
7677
wheres,
7778
groups,
78-
havings,
79+
having,
7980
orders,
8081
limit,
82+
cls.model,
8183
)
8284
conn = conn or cls.ro_conn
8385
return await CursorHandler.fetch_dicts(sql, conn, logger)
@@ -87,13 +89,14 @@ async def upsert_json_field(
8789
cls,
8890
json_field: str,
8991
path_value_dict: Dict[str, Any],
90-
wheres: List[str],
92+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
9193
):
9294
sql = SQLizer.upsert_json_field(
9395
cls.table,
9496
json_field,
9597
path_value_dict,
9698
wheres,
99+
cls.model,
97100
)
98101
return await CursorHandler.calc_row_cnt(sql, cls.rw_conn, logger)
99102

@@ -115,7 +118,7 @@ async def upsert_on_duplicated(
115118
@classmethod
116119
async def insert_into_select(
117120
cls,
118-
wheres: List[str],
121+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
119122
remain_fields: List[str],
120123
assign_field_dict: Dict[str, Any],
121124
):
@@ -124,6 +127,7 @@ async def insert_into_select(
124127
wheres,
125128
remain_fields,
126129
assign_field_dict,
130+
cls.model,
127131
)
128132
return await CursorHandler.exec_if_ok(sql, cls.rw_conn, logger)
129133

fastapi_esql/utils/sqlizer.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
from logging import getLogger
22
from json import dumps
3-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Dict, List, Optional, Union
44

5-
from ..const.error import WrongParamsError
5+
from tortoise.expressions import Q
6+
from tortoise.models import Model
7+
from tortoise.query_utils import QueryModifier
8+
9+
from ..const.error import QsParsingError, WrongParamsError
610

711
logger = getLogger(__name__)
812
# ensure the functionality of the RawSQL
@@ -19,11 +23,35 @@ def __init__(self, sql: str):
1923
class SQLizer:
2024

2125
@classmethod
22-
def resolve_condition(cls, conditions: List[str]) -> str:
23-
return " AND ".join(conditions)
26+
def resolve_wheres(
27+
cls,
28+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
29+
model: Optional[Model] = None,
30+
) -> str:
31+
if not model and not isinstance(wheres, str):
32+
raise WrongParamsError("Parameter `wheres` only support str type if no model passed")
33+
34+
if isinstance(wheres, str):
35+
return wheres
36+
elif isinstance(wheres, Q):
37+
qs = [wheres]
38+
elif isinstance(wheres, dict):
39+
qs = [Q(**{key: value}) for (key, value) in wheres.items()]
40+
elif isinstance(wheres, list):
41+
qs = [q for q in wheres if isinstance(q, Q)]
42+
else:
43+
raise WrongParamsError("Parameter `wheres` only supports str, dict and list type")
44+
45+
if not qs:
46+
raise QsParsingError("Parsing `wheres` for qs failed!")
47+
48+
modifier = QueryModifier()
49+
for q in qs:
50+
modifier &= q.resolve(model, model._meta.basetable)
51+
return modifier.where_criterion.get_sql(quote_char="`")
2452

2553
@classmethod
26-
def resolve_ordering(cls, orderings: List[str]) -> str:
54+
def resolve_orders(cls, orderings: List[str]) -> str:
2755
orders = []
2856
for o in orderings:
2957
if o.startswith("-"):
@@ -59,23 +87,24 @@ def select_custom_fields(
5987
cls,
6088
table: str,
6189
fields: List[str],
62-
wheres: List[str],
90+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
6391
groups: Optional[List[str]] = None,
64-
havings: Optional[List[str]] = None,
92+
having: Optional[str] = None,
6593
orders: Optional[List[str]] = None,
6694
limit: int = 0,
95+
model: Optional[Model] = None,
6796
) -> Optional[str]:
6897
if not all([table, fields, wheres]):
6998
raise WrongParamsError("Please check your params")
70-
if havings and not groups:
71-
raise WrongParamsError("Please check your params")
99+
if having and not groups:
100+
raise WrongParamsError("Parameter `groups` shoud be no empty when `having` isn't")
72101

73102
group_by = f" GROUP BY {', '.join(groups)}" if groups else ""
74-
having = f" HAVING {cls.resolve_condition(havings)}" if havings else ""
75-
order_by = f" ORDER BY {cls.resolve_ordering(orders)}" if orders else ""
103+
having_ = f" HAVING {having}" if having else ""
104+
order_by = f" ORDER BY {cls.resolve_orders(orders)}" if orders else ""
76105
limit_ = f" LIMIT {limit}" if limit else ""
77106
# NOTE Doesn't support `offset` parameter due to it's bad performance
78-
extras = [group_by, having, order_by, limit_]
107+
extras = [group_by, having_, order_by, limit_]
79108

80109
sql = """
81110
SELECT
@@ -85,8 +114,8 @@ def select_custom_fields(
85114
{}""".format(
86115
", ".join(fields),
87116
table,
88-
cls.resolve_condition(wheres),
89-
"\n".join(extras) if extras else "",
117+
cls.resolve_wheres(wheres, model),
118+
"\n".join(i for i in extras if i),
90119
)
91120
logger.debug(sql)
92121
return sql
@@ -97,7 +126,8 @@ def upsert_json_field(
97126
table: str,
98127
json_field: str,
99128
path_value_dict: Dict[str, Any],
100-
wheres: List[str],
129+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
130+
model: Optional[Model] = None,
101131
) -> Optional[str]:
102132
if not all([table, json_field, path_value_dict, wheres]):
103133
raise WrongParamsError("Please check your params")
@@ -110,7 +140,7 @@ def upsert_json_field(
110140
sql = f"""
111141
UPDATE {table}
112142
SET {json_field} = JSON_SET(COALESCE({json_field}, '{{}}'), {", ".join(params)})
113-
WHERE {cls.resolve_condition(wheres)}"""
143+
WHERE {cls.resolve_wheres(wheres, model)}"""
114144
logger.debug(sql)
115145
return sql
116146

@@ -147,9 +177,10 @@ def upsert_on_duplicated(
147177
def insert_into_select(
148178
cls,
149179
table: str,
150-
wheres: List[str],
180+
wheres: Union[str, Q, Dict[str, Any], List[Q]],
151181
remain_fields: List[str],
152182
assign_field_dict: Dict[str, Any],
183+
model: Optional[Model] = None,
153184
) -> Optional[str]:
154185
if not all([table, wheres] or not any([remain_fields, assign_field_dict])):
155186
raise WrongParamsError("Please check your params")
@@ -166,7 +197,7 @@ def insert_into_select(
166197
({", ".join(fields)})
167198
SELECT {", ".join(remain_fields + assign_fields)}
168199
FROM {table}
169-
WHERE {cls.resolve_condition(wheres)}"""
200+
WHERE {cls.resolve_wheres(wheres, model)}"""
170201
logger.debug(sql)
171202
return sql
172203

0 commit comments

Comments
 (0)