Skip to content

Commit 68135f0

Browse files
committed
Update: add support for _between
_gt _gte _lt _lte _in _like _is_null
1 parent a9354c2 commit 68135f0

File tree

2 files changed

+129
-15
lines changed

2 files changed

+129
-15
lines changed

flask_sql_pro/db.py

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,61 @@ def get_params_without_paginated(cls, params: typing.Dict):
4747
del params_cp[cls.page_size_param]
4848
return params_cp
4949

50+
@classmethod
51+
def filter_sql_injection(cls, input_string):
52+
for keyword in cls.sql_injection_keywords:
53+
if keyword in input_string.upper():
54+
raise ValueError('Keywords that may be at risk for SQL injection:' + keyword)
55+
return input_string
56+
57+
@classmethod
58+
def handle_ops(cls, key, val, opt_type="where"):
59+
"""
60+
包括的操作符包括: __in、__gt、__gte、__lt、__lte、__like、__isnull、__between
61+
Handle more types of operations.
62+
key: key for either "where" or "exclude"
63+
val: value for either "where" or "exclude"
64+
opt_type: "where" or "exclude"
65+
"""
66+
phrase = f"{key} = :_where_{key} AND"
67+
68+
filter_str = '_where_'
69+
if opt_type == 'exclude':
70+
filter_str = '_exclude_'
71+
72+
if key.endswith('__gt'):
73+
phrase = f"{key} >= :{filter_str}{key} AND"
74+
if opt_type == 'exclude':
75+
phrase = f"{key} < :{filter_str}{key} AND"
76+
if key.endswith('__gte'):
77+
phrase = f"{key} >= :{filter_str}{key} AND"
78+
if opt_type == 'exclude':
79+
phrase = f"{key} < :{filter_str}{key} AND"
80+
if key.endswith('__lt'):
81+
phrase = f"{key} < :{filter_str}{key} AND"
82+
if opt_type == 'exclude':
83+
phrase = f"{key} >= :{filter_str}{key} AND"
84+
if key.endswith('__lte'):
85+
phrase = f"{key} <= :{filter_str}{key} AND"
86+
if opt_type == 'exclude':
87+
phrase = f"{key} > :{filter_str}{key} AND"
88+
if key.endswith('__like'):
89+
phrase = f"{key} LIKE :{filter_str}{key} AND"
90+
if opt_type == 'exclude':
91+
phrase = f"{key} NOT LIKE :{filter_str}{key} AND"
92+
if key.endswith('__in'):
93+
phrase = f"{key} IN :{filter_str}{key} AND"
94+
if opt_type == 'exclude':
95+
phrase = f"{key} NOT IN :{filter_str}{key} AND"
96+
if key.endswith('__isnull'):
97+
phrase = f"{key} IS NULL AND " if val else f"{key} IS NOT NULL AND "
98+
if opt_type == 'exclude':
99+
phrase = f"{key} IS NOT NULL AND " if val else f"{key} IS NULL AND "
100+
if key.endswith('__between'):
101+
phrase = f"{key} BETWEEN :{filter_str}_between_1_{key} AND :{filter_str}_between_2_{key} AND"
102+
103+
return phrase
104+
50105
@classmethod
51106
def set_where_phrase(cls, sql, where):
52107
"""
@@ -55,22 +110,61 @@ def set_where_phrase(cls, sql, where):
55110
if not where:
56111
return sql
57112
where_str = " WHERE "
58-
for key in where.keys():
59-
where_str += key + " = :" + "_where_%s" % key + " and "
113+
for key, val in where.items():
114+
where_str += cls.handle_ops(key, val, opt_type="where")
115+
60116
where_str = where_str[0:-5]
61117
sql += where_str
62118

63119
return sql
120+
121+
@classmethod
122+
def set_exclude_phrase(cls, sql, exclude):
123+
"""
124+
Generate exclude statement
125+
"""
126+
if not exclude:
127+
return sql
128+
129+
if "WHERE" not in sql.upper():
130+
sql += " WHERE "
131+
else:
132+
sql += " AND "
133+
for key in exclude.keys():
134+
sql += key + " != :" + "_exclude_%s" % key + " and "
135+
sql = sql[0:-5]
136+
137+
return sql
64138

65139
@classmethod
66-
def filter_sql_injection(cls, input_string):
67-
for keyword in cls.sql_injection_keywords:
68-
if keyword in input_string.upper():
69-
raise ValueError('Keywords that may be at risk for SQL injection:' + keyword)
70-
return input_string
140+
def check_sql_injection(cls, k, v):
141+
"""
142+
Avoid sql injection
143+
"""
144+
if any(keyword in str(k).upper() for keyword in cls.sql_injection_keywords):
145+
raise ValueError('Keywords that may be at risk for SQL injection in key: ' + str(k))
146+
if any(keyword in str(v).upper() for keyword in cls.sql_injection_keywords):
147+
raise ValueError('Keywords that may be at risk for SQL injection in value: ' + str(v))
71148

72149
@classmethod
73-
def fullfilled_data(cls, data, where):
150+
def handle_range_type(cls, k, v, is_where=True):
151+
# Check if it is a tuple or list
152+
# Check if it is __between
153+
data = None
154+
bt_str = '_where__between_'
155+
if not is_where:
156+
bt_str = '_exclude__between_'
157+
if isinstance(v, (tuple, list)):
158+
if len(v) == 2:
159+
if k.endswith("__between"):
160+
data = {
161+
f"{bt_str}1_{k}": v[0],
162+
f"{bt_str}2_{k}": v[1],
163+
}
164+
return data
165+
166+
@classmethod
167+
def fullfilled_data(cls, data, where, exclude=None):
74168
"""
75169
The delete/update operation adds a _where_${field} field to each field in the where condition of the incoming data,
76170
which is used for the assignment of the where condition
@@ -79,22 +173,41 @@ def fullfilled_data(cls, data, where):
79173
return data
80174

81175
for k, v in where.items():
82-
# Avoid sql injection
83-
if any(keyword in str(k).upper() for keyword in cls.sql_injection_keywords):
84-
raise ValueError('Keywords that may be at risk for SQL injection in key: ' + str(k))
85-
if any(keyword in str(v).upper() for keyword in cls.sql_injection_keywords):
86-
raise ValueError('Keywords that may be at risk for SQL injection in value: ' + str(v))
176+
cls.check_sql_injection(k, v)
87177

88178
if k.startswith("_where_"):
89179
raise Exception("The where condition cannot contain a field starting with _where_")
180+
181+
_d = cls.handle_range_type(k, v)
182+
if _d:
183+
data.update(**_d)
184+
continue
185+
90186
data.update(**{
91187
"_where_%s" % k: v
92188
})
93189

190+
# Exclude
191+
if exclude:
192+
for k, v in exclude.items():
193+
cls.check_sql_injection(k, v)
194+
195+
if k.startswith("_exclude_"):
196+
raise Exception("The exclude condition cannot contain a field starting with _exclude_")
197+
198+
_d = cls.handle_range_type(k, v, is_where=False)
199+
if _d:
200+
data.update(**_d)
201+
continue
202+
203+
data.update(**{
204+
"_exclude_%s" % k: v
205+
})
206+
94207
return data
95208

96209
@classmethod
97-
def execute_update(cls, tb_name, data, where, app=None, bind=None, commit=False):
210+
def execute_update(cls, tb_name, data, where, app=None, bind=None, commit=False, exclude=None):
98211
"""
99212
Update data
100213
Possible problems with UPDATE :where and data fields have the same name, but different values
@@ -119,6 +232,7 @@ def execute_update(cls, tb_name, data, where, app=None, bind=None, commit=False)
119232

120233
data = cls.fullfilled_data(data, where)
121234
sql = cls.set_where_phrase(sql, where)
235+
sql = cls.set_exclude_phrase(sql, exclude=exclude)
122236
try:
123237
if app and bind:
124238
bind = cls.db.get_engine(app, bind=bind)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='Flask-SQL-Pro',
5-
version='4.4',
5+
version='4.5',
66
description='Based on Flask-SQLAlchemy, extract SQL statements, use Jinja2 syntax to achieve dynamic SQL, support contextual transactions, support paging',
77
long_description=open('README.rst').read(),
88
author='miaokela',

0 commit comments

Comments
 (0)